Three.jsでボーンアニメーションをする!

イントロダクション

初めまして!
面白法人カヤックでフロントエンドエンジニアをやっております、ごんと申します。
今回はタイトルの通り、Three.js の記事を書かせていただきました。

先月、JRA(日本中央競馬会)の年末のビッグレース・有馬記念のプロモーションとして、シン・アリマというキャンペーンが行われ、弊社では、そのWebサイト制作などをお手伝いさせていただきました。
このキャンペーンは、有馬記念とシン・ゴジラのコラボレーション企画で、スペシャルWebサイトでは、競走馬の名馬アリマとゴジラが、市街地や中山競馬場でデッドヒートを繰り広げるという、概ね荒唐無稽なゲームを遊ぶことができました。
残念ながらすでに、サイトの公開期間が終了してしまい、現在はゴジラや名馬アリマの雄姿を見ることはできないので、スクリーンショットを貼ります。

f:id:umai_bow:20170110114751j:plain
f:id:umai_bow:20170110114800j:plain

シン・アリマでは、WebGL を利用し、モバイルブラウザ上でもネイティブアプリレベルのグラフィックを実現させることが一つの目標でしたが、
その実現のためには、いくつもの課題がありました。
例えば、

  • モバイルでのパフォーマンス
  • 3Dモデルのファイスサイズ
  • 複数のブラウザでの対応

などなど……。

しかしながら、ことゲームを作るという部分で一番の課題になったのは、
ボーン付きの3Dモデルをブラウザ上でアニメーションさせるという部分でした。

今回の記事では、シン・アリマで実践した、
ボーンアニメーションを付きのモデルを Three.js で表示するまでの方法を順を追って説明していきます。
コードは全て載せていなかったりするので、コピペでそのまま動くチュートリアルという風にはなっていないのですが、
概要は掴めるように書いていきますので、自分でボーンアニメーションを組み込みたい方の参考になればと思います。
また、Three.js の基本的な説明は省いているので、Three.js をある程度触ったことのある方へ向けた内容となっております。ご了承いただければと……(。-人-。)

Three.js のボーン付きモデル

3Dモデルを変形したり、アニメーションさせるには、一般的にボーンと呼ばれるものを利用します。
ボーンは3Dモデルの骨格のような役割をし、ボーンに合わせて3Dモデルも変形させることで、関節のある生物の動きなどを簡単に表現することができます。
ボーンの表現の仕方は、おそらくいくつかあると思うのですが、Three.js では以下の情報を使って、ボーンを表現します。

全てのボーンの配列 geometry.bones
ボーンの初期位置 geometry.bones[i].pos
ボーンの初期回転 geometry.bones[i].rotq
ボーンの初期スケール geometry.bones[i].scl
ボーンの親のインデックス geometry.bones[i].parent
ある頂点に影響を与えるボーンのインデックスの配列 geometry.skinnedIndex
ある頂点に影響を与えるボーンのインデックス毎の重みの配列 geometry.skinnedWeights

ただ羅列しても意味が分からないかと思いますので、ミニマムのデモを作りながら、順に説明していきます。

元となる3Dモデルを作る

初めに、元となる Mesh を用意します。
今回は、簡単な触手みたいな腕を動かすのを目標にします。
まずは、腕っぽい CylinderGeometry を使っていきます。
シーンやカメラ、マテリアルを適当にセットして、ちゃんと写ったら準備完了です。

/*
   renderer, scene, camera を作る
*/

var renderer = new THREE.WebGLRenderer({canvas: view})
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(60, viewAspectRatio, 1, 100);
camera.position.set(0, 20, -40);
camera.lookAt(new THREE.Vector3(0, 5, 0));

/*
   arm を作る
*/

var armGeometry = new THREE.CylinderGeometry(5, 5, 40, 3, 4, true);

var armMaterial = new THREE.MeshNormalMaterial({
  side: THREE.DoubleSide,
  shading: THREE.FlatShading,
});

var arm = new THREE.Mesh(armGeometry, armMaterial);


/*
   毎フレーム呼び出す処理
*/

var lastTime = 0; // 前回の時間

function step(time) {
  var elapsedTime = time - lastTime; // 前回のstepからの経過時間

  arm.rotation.y += elapsedTime * .0002; // 見やすいようにモデルを少しずつ回す

  renderer.render(scene, camera);

  lastTime = time;

  requestAnimationFrame(step);
}

requestAnimationFrame(step);


ボーンを作る

次に、ボーンを作ります。

今回は、CylinderGeometry の節の数だけ、ボーンを配置していきます(全て相対位置です)。
CylinderGeometry は、Y方向に伸びているので、ルートを CylinderGeometry の底辺に合わせ、そこから、節の長さ分だけ伸ばしていきます。
真っ直ぐなので、回転はなし。スケールも1倍にしておきます。

armGeometry.bones = [
  {
    name: "bone0",
    parent: -1,
    pos: [0, -20, 0],
    rotq: [0, 0, 0, 1],
    scl: [1, 1, 1],
  },
  {
    name: "bone1",
    parent: 0,
    pos: [0, 10, 0],
    rotq: [0, 0, 0, 1],
    scl: [1, 1, 1],
  },
  {
    name: "bone2",
    parent: 1,
    pos: [0, 10, 0],
    rotq: [0, 0, 0, 1],
    scl: [1, 1, 1],
  },
  {
    name: "bone3",
    parent: 2,
    pos: [0, 10, 0],
    rotq: [0, 0, 0, 1],
    scl: [1, 1, 1],
  },
  {
    name: "bone4",
    parent: 3,
    pos: [0, 10, 0],
    rotq: [0, 0, 0, 1],
    scl: [1, 1, 1],
  },
];

ボーンができたら、作ったボーンに対して、頂点を対応させていきます。
ボーンを作っただけでは、あるボーンを動かしたときに、どの頂点を動かせばいいのか分かりません。

ある頂点が、どのボーンから影響をうけるかは、skinnedIndices で指定します。
skinnedIndices は Vector4 の配列で、要素数は geometry の頂点数と同じです。
Vector4 の中身に、対応させたいボーンのインデックスを入れていきます。
要素数が4つしかないので、1つの頂点に対して、ボーンは4つまでしか対応させられないことに注意してください(普通のアニメーションの場合、4つもあれば大体事足ります)。

armGeometry.skinIndices = [
  new THREE.Vector4(4, 3, -1, -1), new THREE.Vector4(4, 3, -1, -1), new THREE.Vector4(4, 3, -1, -1),
  new THREE.Vector4(3, 4,  2, -1), new THREE.Vector4(3, 4,  2, -1), new THREE.Vector4(3, 4,  2, -1),
  new THREE.Vector4(2, 3,  1, -1), new THREE.Vector4(2, 3,  1, -1), new THREE.Vector4(2, 3,  1, -1),
  new THREE.Vector4(1, 2,  0, -1), new THREE.Vector4(1, 2,  0, -1), new THREE.Vector4(1, 2,  0, -1),
  new THREE.Vector4(0, 1, -1, -1), new THREE.Vector4(0, 1, -1, -1), new THREE.Vector4(0, 1, -1, -1),
];

次に skinnedWeights に skinnedIndices で指定したそれぞれのボーンから、どれぐらいの影響を受けるのか、0〜1で指定します。
要素数などは、skinnedIndices と同じです。
今回は、ボーンと頂点が近いものほど影響が大きくなるように、適当な数字を入れてみました。

armGeometry.skinWeights = [
  new THREE.Vector4(.8, .2,  0, 0), new THREE.Vector4(.8, .2,  0, 0), new THREE.Vector4(.8, .2,  0, 0),
  new THREE.Vector4(.6, .2, .2, 0), new THREE.Vector4(.6, .2, .2, 0), new THREE.Vector4(.6, .2, .2, 0),
  new THREE.Vector4(.6, .2, .2, 0), new THREE.Vector4(.6, .2, .2, 0), new THREE.Vector4(.6, .2, .2, 0),
  new THREE.Vector4(.6, .2, .2, 0), new THREE.Vector4(.6, .2, .2, 0), new THREE.Vector4(.6, .2, .2, 0),
  new THREE.Vector4(.8, .2,  0, 0), new THREE.Vector4(.8, .2,  0, 0), new THREE.Vector4(.8, .2,  0, 0),
];

Geometryのボーンを設定したら、Mesh を SkinnedMesh に変更します。
SkinnedMesh を使う場合は、マテリアルに skinned: true のパラメータを指定する必要があるので注意してください。

var armMaterial = new THREE.MeshNormalMaterial({
  skinning: true, // これをつけないと、bone での変形が効かない
  side: THREE.DoubleSide,
  shading: THREE.FlatShading,
});

最後に SkeletonHelper という、ボーンを表示してくれる Helper を Scene に追加しておきます。
SkeletonHelper を使えば、ボーンの動きが線で確認できるようになるため、デバッグなどに重宝します。

/*
   ボーンの確認用のヘルパーを作る
*/

var skeletonHelper = new THREE.SkeletonHelper(arm);


ボーンを動かす

ここまでくれば、ほぼ完成です。
ボーンを回転させて、ちゃんと追従して動いているか、確認してみましょう。

/*
   毎フレーム呼び出す処理
*/

var lastTime = 0; // 前回の時間

function step(time) {
  var elapsedTime = time - lastTime; // 前回のstepからの経過時間

  arm.rotation.y += elapsedTime * .0002; // 見やすいようにモデルを少しずつ回す
  arm.skeleton.bones[1].rotation.z = Math.sin(time * 0.001) * .5;
  arm.skeleton.bones[2].rotation.y = Math.sin(time * 0.002) * 1;
  arm.skeleton.bones[3].rotation.x = Math.sin(time * 0.003) * .5;

  skeletonHelper.update();

  renderer.render(scene, camera);

  lastTime = time;

  requestAnimationFrame(step);
}

ちゃんとクネクネ動いているでしょうか?
スケールやポジションを変えてみて、どんな動きをするか、良ければ試してみてください。

Three.js のボーンアニメーション

無事、ボーンの設定ができましたが、ボーンの動きをスクリプトで表現するのは現実的ではありません
外部のアニメーションライブラリを利用して、ボーンのパラメータを動かすこともできますが、Three.js には、アニメーション関連のユーティリティがあるので、それを利用します。
Three.js のアニメーション関連のユーティリティには、異なるモーション間の補間スピードの調整をはじめとした、3Dゲームなどで必要になる基本的な機能が備わっています。

Three.js のアニメーションの基本

Three.js のアニメーション関連のユーティリティにはたくさんの機能が備わっており、実のところ、私も全部は把握してないのですが、基本的なオブジェクトとして、AnimationClip, AnimationAction, AnimationMixer の3つがあります。

AnimationClip アニメーションのタイムラインの情報を持つ
AnimationAction 実際に実行するアニメーションの状態やパラメータを管理する
AnimationMixer 複数の AnimationAction を管理し、アニメーションのミックスを行う

例の如く、これだけ読んでもさっぱりだと思うので、ミニマムのデモを作りながら、解説していきます。

AnimationClip を作る

初めに、タイムラインとなる AnimationClip を作ります。
AnimationClip のインスタンスの作り方はいくつかあるのですが、今回は AnimationClip.parseAnimation を使って作ります。

var waveClip = THREE.AnimationClip.parseAnimation({
  hierarchy: [
    {},
    {},
    {
      keys: [
        {
          pos: [0, 10, 0],
          rot: new THREE.Quaternion().setFromEuler(new THREE.Euler(- Math.PI / 4, 0, 0)),
          scl: [1, 1, 1],
          time: 0,
        },
        {
          pos: [0, 10, 0],
          rot: new THREE.Quaternion().setFromEuler(new THREE.Euler(Math.PI / 4, 0, 0)),
          scl: [1, 1, 1],
          time: 1000,
        },
        {
          pos: [0, 10, 0],
          rot: new THREE.Quaternion().setFromEuler(new THREE.Euler(- Math.PI / 4, 0, 0)),
          scl: [1, 1, 1],
          time: 2000,
        },
      ],
    },
    {},
    {},
  ]
}, armGeometry.bones);

hierarchy に個別のボーン毎のタイムラインの情報を記述していきます。記述する順番は、bones の並び順に対応します。
タイムラインは、keys という配列をもち、pos, rot, scl, time の情報を含んでいます。
ちなみに rot は rot とか言いつつ Quaternion なので注意。

AnimationMixer と AnimationAction を作る

次に、AnimationMixer と AnimationAction を作ります。
ここは特に解説することがないですね……。

var armMixer = new THREE.AnimationMixer(arm);
var waveAction = armMixer.clipAction(waveClip);

アニメーションを再生する

あとは、AnimationAction.play を呼んで、ループに AnimationMixer.update を仕込めば、アニメーションが再生されます。
update の引数は、前の呼び出しからの経過時間です。

waveAction.play();
function step(time) {
  var elapsedTime = time - lastTime; // 前回のstepからの経過時間

  arm.rotation.y += elapsedTime * .0002; // 見やすいようにモデルを少しずつ回す

  armMixer.update(elapsedTime);
  skeletonHelper.update();

  renderer.render(scene, camera);

  lastTime = time;

  requestAnimationFrame(step);
}


複数のアニメーションを再生する

AnimationMixer には、複数の AnimationClip を登録できます。
さらに、AnimationClip.crossFadeFrom などを使えば、異なるアニメーション間を補間して、滑らかにアニメーションを繋げることができます。

var actionSwitchFlag = false; // アクションを切り替えるためのフラグ

waveAction.play();

window.addEventListener("click", function() {
  /* 
     フラグを見て、アニメーションをクロスフェードで切り替える
  */
  if(actionSwitchFlag) {
    waveAction.crossFadeFrom(twistAction, 1000);
    waveAction.play();
    waveAction.enabled = true;
  } else {
    twistAction.crossFadeFrom(waveAction, 1000);
    twistAction.play();
    twistAction.enabled = true;
  }
  actionSwitchFlag = !actionSwitchFlag;
})

クリックでアニメーションが切り替わります!


アニメーション付き 3D モデルの書き出し

聡明な方なら分かると思うのですが、ボーンもキーフレームも、本来は人類が手で書く代物ではありません
アニメーション付き3Dモデルを書き出すには、three.js のリポジトリにある各種 Exporter を利用するのが簡単です。
(シン・アリマでは、諸般の事情により、FBX のキーフレームとボーンを JSON 形式へ自力で変換するスクリプトを書きました😱)
ココらへんの説明も詳しく書こうと思ったのですが、長くなりそうなので、また何かの機会に……。

実際に読み込んだデモです。
github にモデルの json などもあるので、よければ参考にしてみてください。

カヤックでは、WebGL を書きたいフロントエンドエンジニアを募集中です ✺◟(∗❛ัᴗ❛ั∗)◞✺