Twitter
2019年10月16日 - WebVR・Three.js

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は、GLTFLoaderで読み込むことができます。glTFで読み込んだオブジェクトの影のつけ方は、「glTFをThree.jsで読み込み」を参照してください。

//glTFの読み込み
const gltfLoader = new GLTFLoader();

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

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

● glTFのアニメーション

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

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

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

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

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

	if(animations && animations.length) {

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

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

			//Animation Actionを生成
			let 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をクラスを使用して星空を制作しました。詳細は下記ページを参照してください。

//頂点座標管理用の配列
const positionsArr =[];

//配列に頂点座標を追加
for(let i = 0; i < 50000; i++){
	let vertex = new THREE.Vector3();
	vertex.x = Math.random() * 100 - 50;
	vertex.y = Math.random() * 100 - 50;
	vertex.z = Math.random() * 100 - 50;
	positionsArr.push(vertex);
}

//形状データを生成
const geometry = new THREE.BufferGeometry().setFromPoints(positionsArr);
const material = new THREE.PointsMaterial({size:0.025});
const particles = new THREE.Points(geometry,material);
scene.add(particles);

● OrbitControlsの設定

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

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

● ライティング

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

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

● script.js

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

<script src="js/TweenMax.min.js"></script>

Three.js関連のライブラリはscript.jsからインポートするので、script.jsはtype="module"をつけて読み込みます。

<script src="js/script.js" type="module"></script>

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

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

完成したscript.jsです。

//===============================================================
// Import Library
//===============================================================
import * as THREE from './lib/three_jsm/three.module.js';
import { OrbitControls } from './lib/three_jsm/OrbitControls.js';
import { GLTFLoader } from './lib/three_jsm/GLTFLoader.js';

//===============================================================
// Main
//===============================================================
window.addEventListener('load',function(){
   init();
});

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

function init(){
    scene = new THREE.Scene();
    camera = new THREE.PerspectiveCamera(50,window.innerWidth/window.innerHeight,0.1,1000);
    camera.position.set(-3,2,3);
    scene.add(camera);
    renderer = new THREE.WebGLRenderer({antialias:true});
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth,window.innerHeight);
    renderer.physicallyCorrectLights = true;
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;

    const container = document.querySelector('#canvas_vr');
    container.appendChild(renderer.domElement);

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

    setLoading();
}

function setLoading(){
	TweenMax.to('.loader',0.1,{opacity:1});

	let gltfLoader = new GLTFLoader();
	gltfLoader.load('./data/cat.glb',function(gltf){
		const obj = gltf.scene;
		const animations = gltf.animations;

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

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

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

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

	gltfLoader.load('./data/road.glb',function(gltf){
		const obj = gltf.scene;

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

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

		TweenMax.to('#loader_wrapper',1,{
            opacity:0,
            delay:1,
            onComplete: function(){
                document.getElementById('loader_wrapper').style.display ='none';
            }
        });

        threeWorld();
		setLight();
		setController();
		rendering();
	});
}

function threeWorld(){
	const positionsArr =[];

	for(let i = 0; i < 50000; i++){
		let vertex = new THREE.Vector3();
		vertex.x = Math.random() * 100 - 50;
		vertex.y = Math.random() * 100 - 50;
		vertex.z = Math.random() * 100 - 50;
		positionsArr.push(vertex);
	}
	const geometry = new THREE.BufferGeometry().setFromPoints(positionsArr);
	const material = new THREE.PointsMaterial({size:0.05});
	const particles = new THREE.Points(geometry,material);
	scene.add(particles);

	renderer.outputEncoding = THREE.GammaEncoding;
}

function setLight(){
	const ambientLight = new THREE.AmbientLight(0x222222);
	scene.add(ambientLight);

	const 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(let i = 0; i < positionArr.length; i++){
		let pointLight = new THREE.PointLight(0xFFFFFF,3,2,1);
		pointLight.position.set(positionArr[i][0],positionArr[i][1],positionArr[i][2]);
		scene.add(pointLight);
	}

	const 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 setController(){
    document.addEventListener('touchmove',function(e){e.preventDefault();},{passive:false});
    orbitControls = new OrbitControls(camera,renderer.domElement);
    orbitControls.enableDamping = true;
    orbitControls.dampingFactor = 0.5;
    orbitControls.maxDistance = 20;
	orbitControls.minDistance = 3;
}

function rendering(){
	if(orbitControls){
        orbitControls.update();
    }
    if(mixer){
		mixer.update(clock.getDelta());
	}
	requestAnimationFrame(rendering);
    renderer.render(scene,camera);
}

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

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

関連記事

前の記事へ

Blender2.8でアニメーション

次の記事へ

Three.jsで海を制作