はじめに
対象読者は、以下のような方を想定しています。
というか、半年後の自分のための備忘録です。
- なんとなく Web(JavaScript)のことはわかる
- 3D グラフィックスよくわからん、難しそう
記事タイトルにもありますが、3D グラフィックスのことなんてよくわからんのでマサカリ大歓迎です。
また誰の役に立つかは不明ですが、理解の整理がてらメモした用語集も記事末尾に掲載しています。
やりたかったこと
- VRoid Studio で作成した 3D キャラクター を
- Web(Three.js)上で
- アニメーションさせる
できたもの
See the Pen @pixiv/three-vrm by blachocolat (@blachocolat) on CodePen.
CodePen 内の 3D キャラクターは、@pixiv/three-vrm
Examples の女の子を拝借させていただきました😃
3D キャラクターの作成
VRoid Studio
[出典:VRoid Studio 公式サイト]
「VRoid Studio」は、オリジナルの 3D キャラクターを簡単に作れる pixiv 社製のデスクトップアプリです。
MMO RPG のキャラメイクのようにテンプレートから選択したり、身長などのパラメータを調整するだけでなく、顔や服装のテクスチャを編集(差し替え)することができるので、自由度もかなり高い印象です。
対応する出力形式は、VRM(*.vrm
)と呼ばれる 人型 3D キャラクターを対象にしたファイルフォーマットで、3D モデル全般を扱う GLTF(*.gltf
)ファイルをベースにしているそうです。
詳しい仕様は、VRM 公式サイトにまとめられています。
Web 上での表示
@pixiv/three-vrm
@pixiv/three-vrm
は、VRM ファイルを Web(Three.js)上で取り扱うためのライブラリで、VRoid Studio と同じ開発元である pixiv 社が提供しています。
詳しくは、公式ドキュメントや Examples が充実しているので、そちらをご覧いただくのがオススメです。
Three.js
「Three.js」は、Web で 3D グラフィックスをやるなら必須と言っても過言ではない超有名&便利な WebGL ライブラリです。
公式サイトのトップページには、導入事例の数々がこれでもかと並べられています。
似たようなライブラリに Microsoft 社製の「Babylon.js」があり、記事執筆時点での GitHub Star は、それぞれ 56.5k / 10.4k となっています。
VRM の描画
基本的には Examples のソースコード通りに書けば、問題ないかと思います。
OrbitControls
は、3D キャラクターをグリグリ動かしたり、ズームイン/アウトする機能を提供するものなので、ユーザーインタラクションが不要であれば削除しましょう。
import * as THREE from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { VRM } from '@pixiv/three-vrm'
class VRMPlayer {
constructor (src) {
// renderer
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
this.renderer.setSize(window.innerWidth, window.innerHeight)
this.renderer.setPixelRatio(window.devicePixelRatio)
document.body.appendChild(this.renderer.domElement)
// camera
this.camera = new THREE.PerspectiveCamera(15.0, window.innerWidth / window.innerHeight, 0.1, 100.0)
this.camera.position.set(0.0, 1.25, 2.5)
// camera controls
const controls = new THREE.OrbitControls(this.camera, this.renderer.domElement)
controls.screenSpacePanning = true
controls.target.set(0.0, 1.25, 0.0)
controls.update()
// scene
this.scene = new THREE.Scene()
// light
const light = new THREE.DirectionalLight(0xffffff)
light.position.set(1.0, 1.0, 1.0).normalize()
this.scene.add(light)
// loader
const loader = new THREE.GLTFLoader()
loader.crossOrigin = 'anonymous'
loader.load(src, async (gltf) => {
this.model = await THREE.VRM.from()
this.scene.add(this.model.scene)
})
}
}
const vrmPlayer = new VRMPlayer('path-to.vrm')
Three.js は、「右手系・Y 軸の正方向が上側(Z 軸の正方向が手前側)」の座標系を持ちます。
表示された 3D キャラクターがちょっと近いな、と感じたら、camera.position.set(x, y, z)
の第 3 引数をより大きくし(3D キャラクターをカメラから遠ざけ)ましょう。
アニメーションの適用
アニメーションは、毎フレーム(例えば、1/30 秒)ごとに 3D キャラクターの表情(Blend Shape)やポーズ(各 Bone の位置・向き)を次々と変化させることで実現できます。
Blend Shape アニメーション
手始めに、3D キャラクターのまばたきを実装していきましょう。
class VRMPlayer {
constructor () {
this.animate = this.onAnimate.bind(this)
...
loader.load(src, async (gltf) => {
...
this.animate()
})
}
...
get blinkValue () {
return (Math.sin(this.clock.elapsedTime * 1/3) ** 1024) +
(Math.sin(this.clock.elapsedTime * 4/7) ** 1024)
}
...
onAnimate () {
const delta = this.clock.getDelta()
if (this.model) {
// blend shape animations
this.model.blendShapeProxy.setValue(
THREE.VRMSchema.BlendShapePresetName.Blink,
this.blinkValue
)
...
this.model.update(delta)
}
this.renderer.render(this.scene, this.camera)
window.requestAnimationFrame(this.animate)
}
}
各フレームにおける表情は、onAnimate()
内で設定(model.blendShapeProxy.setValue()
)され、この onAnimate()
を window.requestAnimationFrame()
により再帰的に呼び出すことでアニメーションを実現しています。
ここで blinkValue
は、経過時間(clock.elapsedTime
)にもとづいて変化する適当な周期関数で、具体的には以下のグラフのように緩やかなインパルスを出力します。
まばたきの Blend Shape(VRMSchema.BlendShapePresetName.Blink
)は、
-
0
のとき、目を(完全に)開く -
1
のとき、目を(完全に)閉じる
表情に対応しており、このインパルス部分がまばたきのときどき目をつむる動作を実現しています。
Blend Shape はその名の通り、複数の表情(Shape)を混合(Blend)することもできます。
記事冒頭にある 3D キャラクターの口パクは、異なる周期を持つ「母音 A の口」と「母音 O の口」を組み合わせることで表現しています。
...
get speakAValue () {
return (Math.sin(this.clock.elapsedTime * 11) ** 4) * 0.7
}
get speakOValue () {
return (Math.sin(this.clock.elapsedTime * 7) ** 4) * 0.5
}
...
onAnimate () {
...
if (this.model) {
// blend shape animations
this.model.blendShapeProxy.setValue(
THREE.VRMSchema.BlendShapePresetName.A,
this.speakAValue
)
this.model.blendShapeProxy.setValue(
THREE.VRMSchema.BlendShapePresetName.U,
this.speakOValue
)
}
...
}
公式ドキュメントによると、Blend Shape では、
- まばたき
- 各母音の口
- 喜怒哀楽
- 視線方向
を制御できるようです。
Bone アニメーション
3D キャラクターのポーズをアニメーションさせる方法も、Blend Shape を使った場合とあまり変わりません。
ここでは、model.humanoid.getBoneNode()
で取得した Bone の向き(Rotation)を変化させることで、待機モーション(静止時の微妙な上下な揺れ)を表現してみます。
...
get waitingValue () {
return (1 - (Math.sin(this.clock.elapsedTime * 4/5) ** 4)) * Math.PI / 32
}
...
onAnimate () {
...
if (this.model) {
// bone animations
this.model.humanoid.getBoneNode(
THREE.VRMSchema.HumanoidBoneName.LeftUpperLeg
).rotation.x = this.waitingValue
}
...
}
でも、本当にこれでいいの?
待機モーション程度であれば、適当な周期関数を作って値を設定してやることで、それっぽいアニメーションを実現できました。
しかしながら、キックやバク宙などのような複雑なモーションになると、動きを関数化するなんてほぼ不可能です。
Keyframe アニメーション
毎フレームごとではなく、一部のフレーム(Keyframe)の Blend Shape / Bone を与えることでアニメーションを実現する便利な仕組みがあります。
Web の CSS の世界にも、@keyframe
と呼ばれる同様の機能がありますね。
Three.js における Keyframe アニメーションについては、面白法人カヤックさんの記事が詳しいです。
...
constructor () {
...
loader.load(src, async (gltf) => {
...
// keyframe animations
const bones = [
THREE.VRMSchema.HumanoidBoneName.Neck
].map((boneName) => {
return this.model.humanoid.getBoneNode(boneName)
})
const clip = THREE.AnimationClip.parseAnimation({
hierarchy: [{
keys: [{
rot: new THREE.Quaternion().setFromEuler(new THREE.Euler(0, 0, 0)).toArray(),
time: 0.0,
}, ..., {
rot: new THREE.Quaternion().setFromEuler(new THREE.Euler(0, 0, 0)).toArray(),
time: 4.6,
}, {
rot: new THREE.Quaternion().setFromEuler(new THREE.Euler(-Math.PI / 16, 0, 0)).toArray(),
time: 4.8,
}, {
rot: new THREE.Quaternion().setFromEuler(new THREE.Euler(0, 0, 0)).toArray(),
time: 5.0,
}]
}]
}, bones)
clip.tracks.some((track) => {
track.name = track.name.replace(/^\.bones\[([^\]]+)\].(position|quaternion|scale)$/, '$1.$2')
})
this.mixer = new THREE.AnimationMixer(this.model.scene)
this.mixer.clipAction(clip).play()
})
...
}
...
onAnimate () {
...
if (this.mixer) {
this.mixer.update(delta)
}
}
}
ソースコードの中ほど、AnimationClip.parseAnimation()
で Keyframe を設定しています。
rot
は Rotation の略ですが、実際に指定すべき値は Quaternion なので注意が必要です。
rot
の他には、pos
(Position)や scl
(Scale)を指定することができます。
外部アニメーションの適用
ソースコード上で Keyframe をポチポチと指定することも、実作業を考えると現実的ではありません。
世の中には Blender や Unity といった 3D グラフィックスのための便利なアプリケーションがあるので、これらで作成したアニメーションを適用することとしましょう。
ただし、VRM ファイルにアニメーションを含めることはできないため、Unity で開いた VRM ファイルにアニメーションを適用して GLTF ファイルとしてエクスポートする、といった作業を行う必要があります**(要検証)**。
そうなると、もはや @pixiv/three-vrm
は必要ありませんね。。。
...
constructor () {
...
const loader = new THREE.GLTFLoader()
- loader.load('path-to.vrm', async (gltf) => {
- this.model = await THREE.VRM.from(gltf)
- this.scene.add(this.model.scene)
+ loader.load('path-to.gltf', (gltf) => {
+ this.scene.add(gltf)
...
- this.mixer = new THREE.AnimationMixer(this.model.scene)
+ this.mixer = new THREE.AnimationMixer(gltf)
+ gltf.animations.some((clip) => {
+ this.mixer.clipAction(clip).play()
+ })
})
}
}
...
おわりに
VRoid Studio で作成した 3D キャラクター を Web(Three.js)上でアニメーションさせる方法を試してきました。
VRM は、いわゆる VTuber や VRChat などの文化とともに生まれた仕様だからなのか、
「VRM に動きを与える=(モーションキャプチャーやコントローラーで)毎フレームごとに取得した Bone を適用する」
ことを想定していると感じました。
記事では疎かになってしまった、Keyframe アニメーションももっと追いかけたほうがよさそうです。。。
ちょっと寄り道になってしまった部分もありますが、勉強する以前はやけに高く見えた(Web における)3D グラフィックスの敷居が下がって見えるようになったのは大きな収穫でした。
何か面白いものがいろいろ作れそうです。知らんけど。
【おまけ】3D グラフィックス関連 オレオレ用語集
Skeleton
Bone の親子関係(ツリー構造)で表現された 3D モデルの骨組み。
Bone
位置(Position)と向き(Rotation または Quaternion)を持つ 3D オブジェクト上の点。
Position
親 Bone からの相対位置を表した3次元ベクトル(Vector3
)で定義される。
Rotation
XYZ 各軸に対する回転角と、回転を適用する順序('XYZ'
など)を並べた Euler
で定義される。
単純な回転操作(「3D モデルの向きを180°回転させたい」など)に向いている。
Quaternion
回転軸を表す3次元ベクトルと、その軸に対する回転角を並べた Quaternion
で定義される。
複雑な回転操作(「3D モデルの腕や脚を自在に曲げたい」など)に向いている。
例
子 Bone の位置は、親 Bone に設定された Rotation / Quaternion の回転の影響を受けるため、以下の childA
および childB
は同一に見える。
const parentA = new THREE.Bone()
parentA.rotation.set(0, 0, 0, 'XYZ')
const childA = new THREE.Bone()
childA.position.set(1, 1, 0)
childA.rotation.set(0, 0, 0, 'XYZ')
const parentB = new THREE.Bone()
parentB.rotation.set(0, 0, -Math.PI / 4, 'XYZ')
const child = new THREE.Bone()
childB.position.set(0, 1.41421356, 0)
childB.rotation.set(0, 0, Math.PI / 4, 'XYZ')
Mesh
Skeleton に合わせて動く 3D モデルの肉付け。
Material
Mesh に貼りつける 3D モデルの肌。
ベースとなる Texture(画像)と、素材感(光の反射度合いなど)を表現する Shader で構成される。
Blend Shape
パラメータで制御する 3D モデルの表情(「喜怒哀楽」「発話時の口の形」など)。
「喜び顔 70% 」かつ「悲しみ顔 30%」、「母音 A の口 50%」かつ「母音 O の口 20%」のように、あらかじめ定義された形(Shape)を適当な度合いで混合(Blend)した表情を出力する。
Animation
毎フレームごとの Bone(位置・向き)や Blend Shape(表情)を変化させることで表現する 3D モデルの動き。
一部のフレーム(Keyframe)の Bone / Blend Shape のみを定義し、Keyframe 間のフレームについては適当なアルゴリズムで補間する Keyframe アニメーション もよく用いられる。
FBX (*.fbx
)
従来から広く使用されている 3D モデル用のファイルフォーマット。
GLTF (*.gltf
)
3D モデルの標準(2D 画像における JPEG の位置付け)を目指す、比較的新しいファイルフォーマット。
VRM (*.vrm
)
GLTF を VRoid 用に拡張したファイルフォーマット。
GLTF とは異なり、Animation を含めることはできない。