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.setClearColor(new THREE.Color(0x000000));
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で読み込んでアニメーションするデモなので、パソコンとスマホで見ることができるようにしました。


