2019年10月16日

Three.jsでglTFアニメーション

Blender2.8でアニメーション」で制作したアニメーションを、glTFに出力してThree.jsで読み込んでみました。

Blender2.8からglTFを出力

まず、Blender2.8でglTFを出力します。箱ねこの位置を調整しやすいように、道と街灯と箱ねこを別々にglTFに出力しました。

● 道と街灯を出力

道と街灯のオブジェクトを選択して、トップバーの「ファイル > エクスポート > glTF2.0(.glb/.gltf)」をクリックします。glTF2.0をエクスポートする画面が表示されるので、選択したオブジェクトとモディファイアーを適用をチェックして、右上のglTF2.0をエクスポートをクリックしてglTFを出力します。

● 箱ねこのアニメーションを出力

箱ねこの全パーツを選択して、トップバーの「ファイル > エクスポート > glTF2.0(.glb/.gltf)」をクリックします。glTF2.0をエクスポートする画面が表示されるので、アニメーションタブのアニメーションをチェックして、右上のglTF2.0をエクスポートをクリックしてglTFを出力します。

出力したglTFは、Three.jsに読み込む前にglTF Viewerで確認することができます。

Three.jsでglTFアニメーション

● Three.jsでglTFを読みこむ

出力したglTFは、THREE.GLTFLoaderで読み込むことができます。glTFで読み込んだオブジェクトの影のつけ方は、「glTFをThree.jsで読み込み」を参照してください。

//glTFの読み込み
var loader = new THREE.GLTFLoader();

loader.load('./road.glb',function(data){
	var gltf = data;
	var obj = gltf.scene;
	scene.add(obj);
});

//読み込んだシーンが暗いので、明るくする
renderer.gammaOutput = true;

● glTFのアニメーション

glTFのアニメーションを再生するには、Three.jsのアニメーションシステムのAnimation Mixer、Animation Clip、Animation Actionを使用します。

Animation Mixerはアニメーションを管理するクラスです。Animation Mixerのインスタンスをレンダリング関数で実行してアニメーションを再生します。glTFとして出力したアニメーションは、タイムラインを持つアニメーションデータであるAnimation Clipとしてanimationsに格納されます。

再生や停止などのアニメーションの操作は、Animation MixerのclipActionメソッドで生成するAnimation Actionで行います。

var mixer;
var clock = new THREE.Clock();

var loader = new THREE.GLTFLoader();
loader.load('./cat.glb',function(data){
	var gltf = data;
	var obj = gltf.scene;
	var animations = gltf.animations;

	if(animations && animations.length) {

		//Animation Mixerインスタンスを生成
		mixer = new THREE.AnimationMixer(obj);

		//全てのAnimation Clipに対して
		for ( var i = 0; i < animations.length; i ++ ) {
			var animation = animations[i];

			//Animation Actionを生成
			var action = mixer.clipAction(animation) ;

			//ループ設定(1回のみ)
			action.setLoop(THREE.LoopOnce);

			//アニメーションの最後のフレームでアニメーションが終了
			action.clampWhenFinished = true;

			//アニメーションを再生
			action.play();
		}
	}
	scene.add(obj);
});

function render() {
	requestAnimationFrame(render);
	renderer.render(scene, camera);

	//Animation Mixerを実行
	if(mixer){
		mixer.update(clock.getDelta());
	}
}

● 星空の制作

THREE.Pointsをクラスを使用して星空を制作しました。詳細は下記ページを参照してください。

//空の形状データを生成
var geometry = new THREE.Geometry();

//形状データに頂点座標を追加
for(var i = 0; i < 50000; i++){
	var vertex = new THREE.Vector3();
	vertex.x = Math.random() * 100 - 50;
	vertex.y = Math.random() * 100 - 50;
	vertex.z = Math.random() * 100 - 50;
	geometry.vertices.push(vertex);
}
var material = new THREE.PointsMaterial({size:0.025});
var particles = new THREE.Points(geometry,material);
scene.add(particles);

● OrbitControlsの設定

星空が綺麗に見えるようにOrbitControlsのズームの設定をしました。

var orbitControls = new THREE.OrbitControls(camera);
orbitControls.maxDistance = 20;
orbitControls.minDistance = 3;

● ライティング

ライティングはThree.jsで設定しました。

ポイントライトでは影がつかないため、影をつけるためのスポットライトも設定しました。

● script.js

必要なライブラリを読み込みます。

<script src="js/lib/preloadjs.min.js"></script>
<script src="js/lib/TweenMax.min.js"></script>
<script src="js/lib/three_vr/three.min.js"></script>
<script src="js/lib/three_vr/OrbitControls.js"></script>
<script src="js/lib/three_vr/GLTFLoader.js"></script>
<script src="js/script.js"></script>

最近ChromeのAddEventListenerOptionsのpassiveがデフォルトでtrueになり、OrbitControlsを使用するとpreventDefaultが効かないため、スマホでタッチした時に意図せず画面がスクロールしてしまいます。そこで、passiveをfalseにする処理を追加しました。

document.addEventListener('touchmove', function(e) {e.preventDefault();}, {passive: false});

完成したscript.jsです。

(function () {
	window.addEventListener("load", function () {
	   startLoading();
	});

	var scene,camera,renderer;
	var mixer;
	var clock = new THREE.Clock();

	//ローディング処理
	function startLoading(){
		var nameArray = ['road','cat'];
		var manifestArray = [];
		var path;

		for(var i = 0; i < nameArray.length; i++){
			var name = nameArray[i];
			path = 'data/'+name+'.glb';
			manifestArray.push({id:name,src:path})
		}

		var loadQueue = new createjs.LoadQueue();

		loadQueue.on('progress',function(e){
			var progress = e.progress;
		});

		loadQueue.on('complete',function(){
			TweenMax.to("#loader_wrapper" , 1 , {opacity:0});
			init();
			initObject();
			initLight();
		});

		loadQueue.loadManifest(manifestArray);
	}

	//シーン、カメラ、レンダラー生成
	function init(){
		scene = new THREE.Scene();
	   	camera = new THREE.PerspectiveCamera(45,window.innerWidth/window.innerHeight,0.1,1000);
		camera.position.set(-3, 2, 3);
		scene.add(camera);

		renderer = new THREE.WebGLRenderer({antialias: true});
		renderer.setSize(window.innerWidth, window.innerHeight);
		renderer.render(scene,camera);
		renderer.shadowMap.enabled = true;
		renderer.shadowMap.type = THREE.PCFSoftShadowMap;

		document.addEventListener('touchmove', function(e) {e.preventDefault();}, {passive: false});
		var orbitControls = new THREE.OrbitControls(camera);
		orbitControls.maxDistance = 20;
		orbitControls.minDistance = 3;

		var container = document.createElement('div');
		document.body.appendChild(container);
		container.appendChild(renderer.domElement);

		render();

		window.addEventListener('resize',onWindowResize,false);
		function onWindowResize(){
			camera.aspect = window.innerWidth/window.innerHeight;
			camera.updateProjectionMatrix();
			renderer.setSize(window.innerWidth,window.innerHeight);
		}
	}

	//オブジェクト生成
	function initObject(){

		//glTF(床、街灯)の読み込み
		var loader = new THREE.GLTFLoader();
		loader.load('data/road.glb',function(data){
			var gltf = data;
			var obj = gltf.scene;

			var mesh_floor = obj.children[0].children[1];
			mesh_floor.receiveShadow = true;

			scene.add(obj);
			obj.position.set(2,0,0);
		});

		//glTF(箱ねこ)の読み込み
		var loader = new THREE.GLTFLoader();
		loader.load('data/cat.glb',function(data){
			var gltf = data;
			var obj = gltf.scene;
			var animations = gltf.animations;

			for(var i = 0; i < obj.children.length; i++){
				var mesh = obj.children[i];

				for(var j = 0; j < mesh.children.length; j++){
					if(j == 0){
						var mesh_child = mesh.children[j];
						mesh_child.castShadow = true;
					}
				}
			}

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

				for ( var i = 0; i < animations.length; i ++ ) {
					var animation = animations[i];
					var action = mixer.clipAction(animation) ;
					action.setLoop(THREE.LoopOnce);
					action.clampWhenFinished = true;
					action.play();
				}
			}
			scene.add(obj);
			obj.position.set(2,0,0);
		});

		//星空
		var geometry = new THREE.Geometry();

		for(var i = 0; i < 50000; i++){
			var vertex = new THREE.Vector3();
			vertex.x = Math.random() * 100 - 50;
			vertex.y = Math.random() * 100 - 50;
			vertex.z = Math.random() * 100 - 50;
			geometry.vertices.push(vertex);
		}

		var material = new THREE.PointsMaterial({size:0.05});
		var particles = new THREE.Points(geometry,material);
		scene.add(particles);

		renderer.gammaOutput = true;
	}

	//ライト生成
	function initLight(){
		var ambientLight = new THREE.AmbientLight(0x222222);
		scene.add(ambientLight);

		var positionArr = [
			[-1.2,1.2,-0.75],
			[-0.2,1.2,-0.75],
			[0.8,1.2,-0.75],
			[1.8,1.2,-0.75]
		];

		for(var i = 0; i < positionArr.length; i++){
			var pointLight = new THREE.PointLight(0xFFFFFF, 3, 2, 1);
			pointLight.position.set( positionArr[i][0], positionArr[i][1], positionArr[i][2]);
			scene.add(pointLight);
		}

		var spotLight = new THREE.SpotLight(0xFFFFFF, 2, 20, Math.PI / 4, 10, 1);
		spotLight.position.set(-3,3,-1);
		spotLight.castShadow = true;
		scene.add(spotLight);
	}

	//アニメーション
	function render() {
		requestAnimationFrame(render);
		renderer.render(scene, camera);

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

完成したscript.jsを調整したデモになります。glTFをThree.jsで読み込んでアニメーションするデモなので、パソコンとスマホで見ることができるようにしました。

  • このエントリーをはてなブックマークに追加

関連記事

前の記事へ

Blender2.8でアニメーション

次の記事へ

Three.jsで海を制作