Three.jsに3Dモデルをインポートする方法【GLTFLoaderとアニメーション再生】

箱と球だけのシーンを卒業して、ちゃんとした3Dモデルを表示します。Three.jsでのモデル読み込みはglTF形式+GLTFLoaderが現在の標準です。

デモにはKhronosグループが配布しているCC0のキツネモデルを使いました。上のボタンでアニメーション(きょろきょろ・歩く・走る)を切り替えられます。切り替わりがパッとではなくヌルッと繋がるのにも注目してください。

デモを別タブで開く

なぜglTF形式なのか

3Dモデルの形式はFBX、OBJ、STLなどいろいろありますが、Web用途ならglTF一択です。「3D界のJPEG」を目指して作られた形式で、メッシュ・マテリアル・テクスチャ・ボーン・アニメーションまで1つにまとまり、しかもWebでの読み込みに最適化されています。

拡張子は2種類あります。

  • .gltf — JSON本体+バイナリ(.bin)+テクスチャ画像が別ファイルの構成
  • .glb — 全部を1ファイルに固めたバイナリ形式

配布や管理が楽なので、実用上は**.glbにまとめるのがおすすめ**です。Blenderからエクスポートするときも標準でglb形式が選べます(ファイル → エクスポート → glTF 2.0)。

GLTFLoaderで読み込む

GLTFLoaderは公式アドオンです。第1回で設定したimportmapに three/addons/ があればそのままimportできます。

import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

const loader = new GLTFLoader();
loader.load(
  './model/fox/Fox.gltf',
  // 読み込み完了
  (gltf) => {
    scene.add(gltf.scene);
  },
  // 読み込み中(進捗表示に使える)
  (xhr) => {
    console.log(`loading: ${Math.round((xhr.loaded / xhr.total) * 100)}%`);
  },
  // 読み込み失敗
  (error) => {
    console.error('モデルの読み込みに失敗しました', error);
  }
);

gltf.scene がモデル本体(Object3Dのツリー)で、これを scene.add() すれば表示されます。大きさが合わないときは gltf.scene.scale.setScalar(0.1) のようにスケールを調整します。

1つ注意点があって、読み込みは非同期です。loader.load() の次の行ではまだモデルは存在しないので、モデルに触る処理は必ずコールバックの中に書きます。「undefined になる!」というときはだいたいこれが原因です。

あと、ローカルで file:// で開くとCORSエラーで読み込めません。VSCodeのLive Serverでも npx serve でも何でもいいので、ローカルサーバー越しで確認してください。

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

glTFにはアニメーションも入っています。再生の仕組みは登場人物が3つあって、最初はややこしく感じますが役割を押さえれば単純です。

  • AnimationClip — アニメーションのデータそのもの(gltf.animationsに配列で入っている)
  • AnimationMixer — 特定のモデルのアニメーション進行を管理する再生装置
  • AnimationAction — クリップの再生状態(再生・停止・フェードなど)を操作するハンドル
let mixer;
const actions = [];

loader.load('./model/fox/Fox.gltf', (gltf) => {
  scene.add(gltf.scene);

  mixer = new THREE.AnimationMixer(gltf.scene);
  for (const clip of gltf.animations) {
    actions.push(mixer.clipAction(clip));
  }

  actions[0].play(); // 最初のアニメーションを再生
});

そしてループの中で mixer.update(deltaTime) を必ず呼びます

const clock = new THREE.Clock();
function animate() {
  const deltaTime = clock.getDelta();
  if (mixer) mixer.update(deltaTime);

  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

play() したのに動かないときは、ほぼ100%これを忘れています。mixerは「時間を進めてもらって初めて動く」装置なので、update無しでは1フレーム目で止まったままです。

クロスフェードで滑らかに切り替える

アニメーションの切り替えを stop()play() でやると、ポーズが一瞬でパチッと切り替わって不自然です。fadeIn / fadeOut を組み合わせると、2つのアニメーションがブレンドされながら切り替わります。

const crossFadeDuration = 0.3; // 0.3秒かけて切り替える
let currentAction = actions[0];

function switchAnimation(next) {
  if (next === currentAction) return;
  next.reset().fadeIn(crossFadeDuration).play();
  currentAction.fadeOut(crossFadeDuration);
  currentAction = next;
}

reset() を挟んでいるのは、一度fadeOutしたアクションを再度再生するときに、ウェイトや再生位置が前回の状態のまま残っているからです。これを忘れると「2回目から切り替わらない」という地味にハマるバグになります(私はハマりました)。

キャラ操作と組み合わせるなら、前回のジョイスティックの倒し具合(0〜1)を使って「0なら待機、半分まで歩き、それ以上は走り」のように出し分けると、それっぽいキャラクター操作が完成します。

モデルが重いときはDraco圧縮

ハイポリのモデルはファイルサイズがすぐ数十MBになります。glTFはDraco圧縮に対応していて、ジオメトリを1/5〜1/10程度に圧縮できます。読み込みには専用のデコーダーを追加します。

import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';

const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.7/');
loader.setDRACOLoader(dracoLoader);

Blenderのエクスポート設定に「圧縮」のチェックがあるので、そこでDraco圧縮したglbを吐き出せます。デコードのぶんCPU負荷は少し増えますが、通信量の削減効果の方が大きいので、Webで配信するモデルは基本圧縮でいいと思います。

まとめ

  • Webで使うモデル形式はglTF(実用上は.glbにまとめる)一択
  • 読み込みは非同期。モデルに触る処理はコールバックの中で
  • アニメーションは AnimationMixer を作って、ループで mixer.update(deltaTime) を忘れずに
  • 切り替えは reset().fadeIn().play() + fadeOut() のクロスフェードで
  • 重いモデルはDraco圧縮

ここまでで「シーンを作って、動かして、モデルを出す」が揃いました。ただ、全部1ファイルに書いているとそろそろ限界が来る頃です。次回はコードの整理術の話をします。


Three.js解説シリーズ(全7回)

  1. 【Three.js入門】ビルドツール不要、CDNだけで始める基本の使い方
  2. レンダラー・ライト・カメラの使い方と設定まとめ
  3. キーボードとNipplejsでプレイヤーを動かす
  4. GLTFLoaderで3Dモデルをインポートする(この記事)
  5. ファイル分割はDIパターンがおすすめ
  6. CloudflareへのデプロイとWebSocketでの位置同期
  7. WebGLレンダラーとCSS3Dレンダラーを共存させる