みかづきブログ・カスタム

基本的にはちょちょいのほいです。

Next.js + Three.jsでページのスクロールを管理して3Dモデルをアニメーションさせる 🎥

blog.kimizuka.org

以前つくったスクロール管理の仕組みをつかって、ページをスクロールするとダンスを踊るサイトをつくりました。

kimizuka.github.io

名付けて、ダンスクロールです。

スクロールとアニメーションを連動させる

前回の2Dのパラパラアニメバージョンは、スクロールに連動してクロールするという形で実装しました。

blog.kimizuka.org

大きな違いとしては、2Dが3Dになったところに目が行きがちなのですが、コマとコマの間が極限まで小さくなったことが挙げられます。
つまり、前回は8枚のパラパラ漫画を動かしていただけなので、コマとコマの間の補完が効かなかったのですが、今回は全2.4秒のアニメーションをスクロールの量に応じて動かしているのでパラパラ漫画感はほとんど感じません。

loader.load(
  url,
  (gltf) => {
    model = gltf.scene;
    const animations = gltf.animations;

    if (animations && animations.length) {
      mixer = new THREE.AnimationMixer(model);

      for (let i = 0; i < animations.length; i++) {
        const animation = animations[i];

        // アクションクリップをHooksAPIで登録
        // ただしアニメーションが複数設定されている場合は最後のものしか登録されない
        setAction(mixer.clipAction(animation));
      }
    }

    model.scale.set(1.8, 1.8, 1.8);
    model.position.set(0, -2, 0);
    scene.add(gltf.scene);

    renderer.setAnimationLoop(tick);

    function tick() {
      if (mixer){
        mixer.update(clock.getDelta());
      }

      renderer.render(scene, camera);
    }
  },
  (err) => {
    console.error(err);
  }
);
useEffect(() => {
  if (action) {
    action.play();
    action.paused = true; // アクションを再生しつつ一時停止状態にしておく
  }
}, [action]);
useEffect(() => {
  const diff = lastProgress - progress;

  if (diff < 0) {
    if (Math.abs(diff) > .99) {
      setDirection('up');
    } else {
      setDirection('down');
    }
  } else {
    if (Math.abs(diff) > .99) {
      setDirection('down');
    } else {
      setDirection('up');
    }
  }

  warp();
  setLastProgress(progress);

  if (action) {
    action.time = 2.4 * progress; // ここでアニメーションを進行している
  }
}, [progress]);

要点だけ抜き出すとこんな感じです。

GLTFLoaderの読み込み方法が謎

そもそも、Three.jsのGLTFLoaderが非常に優秀で、mixamoでつくったアニメーション付きのfbx(をBlenderで書き出したglbファイル)を簡単に読み込みつつ、アニメーションをAnimationMixerで管理してくれます。
mixamoとBlenderを使ったアニメーション付きのglbファイルの作り方は以前紹介したので、ここでは作り方は省略します)

blog.kimizuka.org

ただ、GLTFLoaderの読み込み方が謎で、npm install後、

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';

と読み込むと、

SyntaxError: Cannot use import statement outside a module

と怒られてしまうので、

import { GLTFLoader } from '../../../node_modules/three/examples/jsm/loaders/GLTFLoader';

という感じで読み込んでますが、もっと良いやり方があるような気がしてなりません。

今後の改善点(3Dを使う意味)

現状だとThree.jsで実装する意味がほとんどなくて、3Dのダンスを一旦映像として書き出し、スクロールに連動してvideoタグを動かすのと体験が変わりません。
むしろvideoタグで実装した方が動作や読み込みが軽くなる気がします。

折角3Dオブジェクトを読み込んでいるので、カメラをぐりぐり動かせるように、さくっとOrbitControlsを入れようかとも思ったのですが、スクロールのジェスチャーとバッティングするので諦めました。

ソースコード

そんなこんなで、やっぱり、まだまだ改善の余地がありますが、ソースコードの全貌はリポジトリに公開してますので、興味のある方は是非ともご覧ください!

github.com