Blog

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

blenderのカメラをThree.jsで使うサンプルとして、最新記事はこちらです。
Blenderでカメラアニメーションを作成し、スクロールに合わせて動かす
以下は、これ以前にボタンクリックでアニメーションスタートするものを作ってみたバージョンです。

※2021.8.21 OrbigControlsについて、一部内容を修正しました

blenderでカメラも出力できるので、簡単なものを作ってみました。

デモ

ロゴの双葉(木の先が双葉のやつ)をblenderで作って、エクスポート時にカメラも一緒に出力し、それをThree.jsで使うというものになります。
地面テクスチャに適当な砂漠をいれてますが、背景がないとカメラの角度が変わった時にわかりにくかったから入れました。…もう少しちゃんと探せばよかったかもしれない。

今回は、「アクション」ボタンで最初からスタート、「ストップ」ボタンでアニメーションストップ、「スタート」ボタンでリスタートするようにしました。

blenderで作成する時に気をつけておくこと

キーフレームの最初と最後は、各パーツ同じにする

たとえば、地面は最初にぱっとでてきて終わりですが、カメラは最後まで動き続けます。

地面や他のパーツのアニメーションが先に終了していても、他のパーツがフレーム90まで使っていたら全部のパーツを90にしておきます。

ずれていると、一度だけのアニメーションなら問題ないのですが、ループにした時によくわからない動きになります。

ワールドの中央を中心にして作る(訂正。大丈夫そう)

カメラをblenderからエクスポートすると、camera.positionなどを指定できなくなります。

そして、これはまだエクスポート機能で未対応なのか、方法があるのかはわからないのですが、カメラは常に中心を向いた状態でエクスポートされます。カメラを空に向けた状態でエクスポートしても、中心を向いています。

OrbitControlsを作成すると、自動的にカメラがワールド中央を向くようになります。作成しないようにしましょう。
controls.enabled = false; を設定しても、有効のままです。

カメラをThree.js側で動かせなくなるので、ワールドの中央にオブジェクトを置いて作成しましょう。

ちなみに、カメラのOrbitControlsは、エクスポートしたカメラを利用しているとききません。
これも何か解決策があればいいんですが…new THREEで作ったカメラと、エクスポートしたカメラの何が違うのか探ろうとしましたが、いまいちよくわかりませんでした。

OrbitControlsが効いていないのかと最初思っていましたが、作成することによってカメラの動き方が変わったので、別の理由で動かせない可能性が出てきました…。詳細はわかっておりません。

いろいろ後日訂正してしまいましたが、カメラが中央を向くことはなくなったので、必ずしもワールドの中央を中心に作らなくても大丈夫そうです。

「カメラ設定」をエクスポートに含める

glbファイルのエクスポート時に、内容>カメラ設定 のチェックボックスをオンにします。
デフォルトではオフになっています。

HTML / CSS

canvas要素と、各ボタンを用意します。

<div id="canvasCumalLeaf">
  <canvas id="canvas"></canvas>
</div>
<div class="btns">
  <button id="btn-action">アクション!</button>
  <button id="btn-stop">ストップ</button>
  <button id="btn-start">スタート</button>
</div>

CSSでカンバスは画面いっぱいにしています。

#canvasCumalLeaf{
  width : 100%;
  height : 100vh;
  canvas{
    width: 100%;
    height: 100%;
  }
}

必要なものを読み込み&定義

const THREE = window.THREE = require('three');
require('three/examples/js/loaders/GLTFLoader');
require('three/examples/js/controls/OrbitControls');

let renderer;
let scene;
let gltfLoader;
let camera;
let controls;
let mixer;
let model;
let clock;
let animations;
let animationFlame;
const url = '3d/cumakleaf/cumakleaf.glb';

const canvas = document.getElementById('canvas');
const btnAction = document.getElementById('btn-action');
const btnStop = document.getElementById('btn-stop');
const btnStart = document.getElementById('btn-start');

// ウィンドウサイズ設定
let width = canvas.getBoundingClientRect().width;
let height = canvas.getBoundingClientRect().height;

// 実行/イベント処理
window.addEventListener('DOMContentLoaded', init);
window.addEventListener('resize', onResize);

シーンの作成とglbファイルのロード

function init() {
  // レンダラーを作成
  renderer = new THREE.WebGLRenderer({
    canvas: canvas,
    alpha:true
  });
  renderer.outputEncoding = THREE.sRGBEncoding;
  renderer.setPixelRatio(1);
  renderer.setSize(width, height);

  // 時計を作成
  clock = new THREE.Clock();

  // シーンを作成
  scene = new THREE.Scene();

  gltfLoader = new THREE.GLTFLoader();

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

        model = gltf.scene;
        model.position.set(0, 0, 0);

        if (gltf.cameras[0]){
          // カメラ一つを想定
          camera = gltf.cameras[0];
        }

        if (animations && animations.length) {
          mixer = new THREE.AnimationMixer(gltf.scene);
          for (let i = 0; i < animations.length; i++) {
            let action = mixer.clipAction(animations[i]);
            action.setLoop(THREE.LoopOnce);
            action.clampWhenFinished = true;
            action.play();
          }
        }
        scene.add(model);
        resolve()
      },
      function() {
        onProgress();
      }
    )
  });
  pr().then(() => {
    loadedObj()
  })
}

function onProgress(){
//
}

gltf.camerasにカメラのデータが配列で入っています。カメラは一つなので[0]で取り出してcameraに入れておきます。

glbファイルのロードが終わったら、loadedObj()を呼び出します。

カメラとライトを作成してレンダー

function loadedObj(){
  createCamera()
  createLight();
  // レンダー前にアスペクト比調整
  onResize()
  // 最初の状態を表示
  renderer.render(scene, camera);
}

onResize()で、レンダー前にアスペクト比を調整します。
renderer.renderでアニメーション前の状態を表示します。(今回は何もないところから出現するのでなくてもよかった)

カメラの作成

function createCamera(){
  if (!camera){
    camera = new THREE.PerspectiveCamera(30, width / height, 1, 5000);
    camera.position.set(10,10,10)
    // 原点方向を見つめる
    camera.lookAt(new THREE.Vector3(0, 0, 0));
  }
  // controls = new THREE.OrbitControls(camera, canvas);
  // controls.enableDamping = true;
}

cameraプロパティがあるかどうかを判定します。blenderでカメラを出力してたらそれを使い、なければ作ります。

controlsは、エクスポートしたカメラでは動作しませんが、作った時のために入れています。

これがカメラが自動的にワールド中央に向く理由でした。コメントアウトしました。

ライトの作成

function createLight(){
  const light = new THREE.DirectionalLight(0xFFFFFF);
  const light2 = new THREE.AmbientLight(0xFFFFFF, .8);
  light.position.set(550, 300, 1200);
  // シーンに追加
  scene.add(light);
  scene.add(light2);
}

ライトは、平行光源と環境光源の2つを追加しました。

フレームアニメーション設定

function tick() {
  //controls.update();
  renderer.render(scene, camera);
  animationFlame = requestAnimationFrame(tick);
  if (mixer) {
    mixer.update(clock.getDelta());
  }
}

リサイズイベント作成

function onResize() {
  width = canvas.getBoundingClientRect().width;
  height = canvas.getBoundingClientRect().height;

  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(width, height);

  camera.aspect = width / height;
  camera.updateProjectionMatrix();
}

レンダラーのサイズと、カメラのアスペクト比を調整しています。

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

btnAction.addEventListener('click',function(){
  window.cancelAnimationFrame(animationFlame);
  if (animations && animations.length) {
    for (let i = 0; i < animations.length; i++) {
      let action = mixer.existingAction(animations[i]);
      action.reset()
      action.play()
    }
  }
  clock.start()
  tick()
})
btnStop.addEventListener('click',function(){
  window.cancelAnimationFrame(animationFlame);
  clock.stop()
})
btnStart.addEventListener('click',function(){
  tick()
  clock.start()
})

「アクション」ボタンでは、一度アクションをリセットし、再度再生しています。
existingActionで、既存のアクションを取得できます。

まとめ

Blenderでカメラワークを作り込んでからThree.jsで動かすことができるので、何か使い所がありそうだなと感じました。

最初は、なぜカメラが勝手に中央を向くのか…。使い方がかなり限定されるじゃないかと思っていましたが、単に自分のOrbitControlsの知識が乏しいだけでした。

とにかく、OrbitControlsが作成されるとカメラが中央を向くようだということがわかったので、そこだけが注意です。

おすすめの記事 recommend blog

新着 new blog

github