Blog

Blenderでカメラアニメーションを作成し、スクロールに合わせて動かす

3Dのカメラアニメーションの手法として、Three.jsで一からカメラとそのアニメーションを作成する方法もありますが、
Blenderでカメラアニメーションまで作成してThree.jsで読み込む方法もあります。

Blenderでは画面を見ながらキーフレームを打てるので、そのカメラアニメーションを使えないかと思い、以前にBlenderのカメラをThree.jsで動かすサンプルの記事書いたことがあります。

blenderで出力したカメラをThree.jsで使ったアニメーション

今回はそれの応用で、Blenderのカメラアニメーションをスクロールに合わせて動かそうというものです。

デモ

デモはこちら(表示に少し時間がかかるかもしれません)

「スクロールでカメラを進める」にチェックが入ってると、マウススクロールでカメラがフィールド内を移動します。

スクロールはブラウザのデフォルトのスクロールなので、当然ながらスクロールに合わせてHTMLが流れていきます。

canvasのアニメーションに合わせて、コンテンツを表示したり位置を調整したり、なんなりと制御することができます。

Blenderでのカメラアニメーションの作成

モデル配置

素材

Blenderでアニメーションを作成するにあたり、ポイントを書いていきます。

今回は、以前参加したYouTube参加型企画の素材を使わせていただきました。
3Dのメモ帳 みんなでジオラマ作る!~街編~ に参加しました!

(これを使いたいがために今回のサンプルを作ったと言っても過言ではない)
参考:素材リンク

モデルは「ファイル」>「アベンド」で読み込みができます。

Blenderファイルの「●●.blend」をクリックすると中に入ることができ、Collectionを追加することができます。

今回、社の後ろにある森を作るためにパーティクルのヘアーを使いましたが、パーティクル含めモディファイヤはエクスポート時に全て適用します。パーティクルは増やしすぎると重くなるので気をつけましょう。(今回重くなった原因)

カラーランプは再現できなかった

注意点として、カラーランプを使っているものは、Three.jsでは再現できないようです。

具体的には、マテリアルの「サーフェス」が下記のようになっている場合に色がその通りになりませんでした。

  • プリンシブルBSDF > ベースカラー「カラーランプ」
  • シェーダーミックス > 係数「カラーランプ」
  • ディフューズBSDF > カラー「カラーランプ」

BlenderのColor Ramp (カラーランプ)ノードは、グラデーションを用い、値をカラーにマッピングするために使用します。
どうしてもグラデーションが使いたい場合は、テクスチャを使えばThree.jsでも表現できます。

カメラアニメーションの作成

  1. アニメーションタブに切り替えます。
  2. オブジェクトモードで右側のレイヤーパネルのカメラを選択します。
  3. 3本線メニュー(下のキャプチャ)から「ビュー」→「視点」→「カメラ」を選択すると、カメラから見た画面になります。
  4. この状態でNキーを押すと左からビューのメニューがでてくるので、「カメラをビューにロック」すると、カメラから見た画面を見ながら操作ができます(画面左側)
  5. 下にタイムラインがあるので、ここにキーフレームをうってアニメーションを作成します。
    キーフレームは、3Dビューポートの上で(タイムラインの上ではなく)Iキーを押すと出てくるメニューから設定できます。今回は「位置・回転」のキーフレームをうちました。
  6. キーフレームをうつと位置と回転の状態が記録されるので、カメラを動かす→キーフレームをうつ→カメラを動かす→キーフレームをうつ…という作業を繰り返します。

※NキーやIキーなどのショートカットを押すときは英数モードである必要があります(iMac)。

ちなみに、カメラのscaleが歪んでいると、後々Three.jsで読み込んだ時にアスペクト比が狂うので、scaleは1にしておきます。
カメラを選択してNキーでメニューを出し、トランスフォームの「スケール」部分を確認してください。

エクスポート

「ファイル」>「エクスポート」から、glTF形式でエクスポートします。

  • フォーマットは今回は「glb」拡張子
  • 「内容」で「カメラ設定」にチェックを入れておきます。
  • トランスフォームの「+Yが上」にチェックが入っていることを確認します。
  • メッシュ「モディファイヤーを適用」にチェックを入れておきます。

あとはデフォルトのままでもOKです。
アニメーションにシェイプキーを使っている場合は、アニメーションの項目も確認します。

HTML

HTMLは、canvasと、その上に乗るコンテンツです。
デモではチェックボックスなどを作っていますが、それらのフォームは省略しています。

<main>
  <canvas id="canvas"></canvas>
  <div class="control">
    <span class="paramScrollY">スクロール量 <span id="scrollY"></span></span>
  </div>
  <div class="contents">
    <div class="sectionbox is-1">
      <p>これはHTMLコンテンツです。</p>
      <p>左には、バス停とポストがあるよ</p>
    </div>
    <div class="sectionbox is-2">
      <p>右には猫がいます</p>
    </div>
    <div class="sectionbox is-3">
      <p>まっすぐいくと</p>
    </div>
    <div class="sectionbox is-4">
      <p>おいなりさん</p>
    </div>
  </div>
</main>

CSS

canvasを画面いっぱいに表示させ、pointer-events:noneにして触れないようにしています。

#canvas{
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  pointer-events: none;
}

.contents{
  position: relative;
  z-index: 1;
}

Three.jsで読み込み

全体の流れ

import { WebGLRenderer, Scene, AnimationMixer, LoopOnce, DirectionalLight, AmbientLight } from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';

let renderer;
let scene;
let gltfLoader;
let camera;
let mixer;
let animations;

// glbのurlを指定
const url = '/blender-camera-jinja/3d/mycity.glb';

const canvas = document.getElementById('canvas');

// ウィンドウサイズ設定
const width = window.innerWidth;
const height = window.innerHeight;

window.addEventListener('DOMContentLoaded', init);
window.addEventListener('resize', onResize);

function init() {
  renderer = new WebGLRenderer({
    canvas: canvas,
    alpha: true,
  });

  renderer.setPixelRatio(1);
  renderer.setSize(width, height);

  scene = new Scene();

  // Load GLTF or GLB
  gltfLoader = new GLTFLoader();

  new Promise((resolve) => {
    gltfLoader.load(url, (gltf) => {
      animations = gltf.animations;
      const model = gltf.scene;

      if (animations && animations.length) {
        mixer = new AnimationMixer(gltf.scene);
        for (let i = 0; i < animations.length; i++) {
          const action = mixer.clipAction(animations[i]);
          //ループ設定(1回のみで終わらせる。これをコメントアウトするとループになる)
          action.setLoop(LoopOnce);
          //アニメーションの最後のフレームでアニメーションが終了(これがないと、最初の位置に戻って終了する)
          action.clampWhenFinished = true;
          action.play();
        }
      }
      scene.add(model);
      resolve(model);
    });
  }).then(() => {
    render();
  });
}

// gltfロード後の処理
function render() {
  createCamera();
  createLight();
  // 最初の状態を表示
  renderer.render(scene, camera);
}

function createCamera() {
  // blenderのカメラを取り出す
  camera = scene.children[0].children.find((child) => {
    return child.name === 'camera';
  });
  camera.aspect = width / height;
  camera.updateProjectionMatrix();
}

function createLight() {
  const directionalLight = new DirectionalLight(0xfffacd);
  const ambientLight = new AmbientLight(0xffffff, 0.1);
  directionalLight.position.set(55, 70, 120);
  scene.add(directionalLight);
  scene.add(ambientLight);
}

function onResize() {
  // サイズを取得
  const width = window.innerWidth;
  const height = window.innerHeight;

  // レンダラーのサイズを調整する
  renderer.setSize(width, height);
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.render(scene, camera);

  // カメラのアスペクト比を正す
  camera.aspect = width / height;
  camera.updateProjectionMatrix();
}

window.addEventListener('scroll', function () {
  const scrollY = window.scrollY;
  const scrollYEl = document.getElementById('scrollY');
  const cameraAnimation = animations.find((animation) => {
    return animation.name === 'cameraAction';
  });

  let action = mixer.existingAction(cameraAnimation);
  action.reset();
  mixer?.setTime(scrollY / 1000);
  renderer.render(scene, camera);
  scrollYEl.textContent = String(scrollY);
});

init関数ではレンダラーやシーンを用意します。

そして、Three.jsのGLTFLoaderで、glbファイルをロードします。

glbファイルをロードしたら、animationsプロパティからAnimationActionを生成し、再生(action.play();)してシーンに追加します。

全て読み込み終わったら、render関数を実行します。
render関数では以下の処理をしています。

  • createCamera関数を実行してカメラをBlenderからとりだし、camera変数に入れる
  • createLight関数を実行してライトを作成し、シーンに追加する(特別な処理なし)
  • 一度だけ「renderer.render(scene, camera);」でレンダリングし、ブラウザに表示。

スクロールに合わせてカメラを動かす

スクロールの処理は下記です。

window.addEventListener('scroll', function () {
  const scrollY = window.scrollY;
  const scrollYEl = document.getElementById('scrollY');
  const cameraAnimation = animations.find((animation) => {
    return animation.name === 'cameraAction';
  });

  let action = mixer.existingAction(cameraAnimation);
  action.reset();
  mixer?.setTime(scrollY / 1000);
  renderer.render(scene, camera);
  scrollYEl.textContent = String(scrollY);
});

windowのscrollイベントを監視して、スクロール量を取得します。
gltfLoaderでロードしたanimationの中で、カメラのアニメーションをfind()で取得します。
この時、animation.nameにはBlenderのタイムラインでつけたアクション名が入っています。

existingActionで既存のAnimationActionを取得し、reset()でアニメーションを一旦最初の状態に戻します。

AnimationMixerのsetTime関数で、スクロール量に合わせてアニメーションの再生箇所を設定します。

resetがなくてもsetTimeでアニメーションの再生箇所を設定できるのですが、
action.setLoop(LoopOnce); 
でループをオフにし一回だけ再生して終了するように設定していた場合、アニメーションが終了するとaction.play()し直しても再生されません。一度リセットする必要があるようです。

Note: Activating this action doesn’t necessarily mean that the animation starts immediately: If the action had already finished before (by reaching the end of its last loop), or if a time for a delayed start has been set (via startAt), a reset must be executed first.
訳)注意:このアクションをアクティブにしても、すぐにアニメーションが始まるとは限りません: アクションがすでに終了していた場合(最後のループの終わりに達した場合)、または遅延開始の時間が設定されていた場合(startAtを介して)、最初にリセットを実行する必要があります。

https://threejs.org/docs/#api/en/animation/AnimationAction.play

最後に、renderer.render(scene, camera);でレンダリングします。

まとめ

以前、Blenderのカメラを再生した記事の時点では、まだThree.jsについてもわからないことが多かったのですが、徐々にわかることが増えてきました。

どんなアニメーションも実装できるようになることを目標とすると、アニメーションを作るためにCGソフト(Blenderはモデリングソフト)を利用するのは良い選択肢だと個人的には思っています。

Blenderで表現できてThree.jsにできないこともまだありますが(カラーランプがそうでした)、現段階でもBlender側で作成できる部分は多いです。モデル+アニメーションをエクスポートするところまでが専用ソフトでできるなら、ぜひ積極的に使っていきたいです。

そしてそれをいい感じにサイトに応用したいですが…そこが一番難しい。

参考

素材
https://www.youtube.com/watch?v=PxotC-7xi-k&t=4s

おすすめの記事 recommend blog

新着 new blog

github