第九章 創建動畫和移動相機
基礎動畫:
render();
function render(){
renderer.render(scene,camera);
requestAnimationFrame(render);//通常保持60/s的幀率渲染
}
一、簡單動畫
複雜動畫的基礎
function render(){
cube.rotation.x += controls.rotationSpeed;
cube.rotation.y += controls.rotationSpeed;
cube.rotation.z += controls.rotationSpeed;
step += controls.bouncingSpeed;
sphere.position.x = 20 + (10 * (Math.cos(step));
sphere.position.y = 2 + (10 * Math.abs((Math.sin(step)));
scalingStep += controls.scalingSpeed;
var scaleX = Math.abs(Math.sin(scalingStep / 4));
var scaleY = Math.abs(Math.cos(scalingStep / 5));
var scaleZ = Math.abs(Math.sin(scalingStep / 7));
cylinder.scale.set(scaleX,scaleY,scaleZ);
renderer.render(scene,camera);
requestAnimationFrame(render);
}
選擇對象
var projector = new THREE.Projector();
function onDocumentMouseDown(event){
event.preventDefault();
var vector = new THREE.Vector3(
(event.clientX / window.innerWidth) * 2 -1,
(event.clientY / window.innderHeight) * 2 + 1,
0.5
);
projector.unprojectVector(vector,camera);
var raycaster = new THREE.Raycaster(camera.position,vector.sub(camera.position).normalize());
var intersects = raycaster.intersectObjects([
sphere,cylinder,cube
]);
if(intersects.length > 0 ){
intersects[0].object.material.transparent = true ;
intersects[0].object.material.opacity = 0.1 ;
}
}
點擊屏幕:
1.在點擊的位置創建一個向量
2.用unprojectVector函數,將屏幕上點的位置轉換成Three.js場景中的座標。
3.然後,用THREE.Raycaster對象(projector.pickingRay函數的返回值)從屏幕上的點擊位置想場景中發射一束光線。
4.最後,使用raycaster.intersectObjects函數來判斷指定的對象中有沒有被這束光線擊中的。
被擊中的對象信息:
distance:49.2555 // 從相機到被點物體間的距離
face:THREE.Face4 // 該網格被選中的面
faceIndex:4 // 該網格被選中的面
object:THREE.Mesh // 被點擊的網格
point:THREE.Vector3 //被選中的物體上的點
Tween.js
https://github.com/sole.tween.js
這個庫可以定義某個屬性在兩個值之間的過渡,自動計算出起始值和結束值之間的所有中間值。這個過程叫做:補間。
//10s從x=10遞減到x=3
var tween = new THREE.Tween({x:10}).to({x:3},10000).easing(TWEEN.Easing.Elastic.InOut).onUpdate(function(){
});
這個漸變的過程可以是線性的,指數性的,還可能是其他的方式。
http://sole.github.io/tween.js/examples/03_graphs.html
屬性值在指定時間內的變化稱爲 easing (緩動)
var posSrc = {pos:1};
var tween = new THREE.Tween(posSrc).to({pos:0},5000);
tween.easing (TWEEN.Easing.Sinusoidal.InOut);
var tweenBack = new THREE.Tween(posSrc).to({pos:1},5000);
tweenBack.easing(TWEEN.Easing.Sinusoidal.InOut);
//使兩個補間動畫首尾相連
tween.chain(tweenBack);
tweenBack.chain(tween);
//遍歷粒子系統中的每個點,並用補間動畫提供的位置更新頂點的位置。
var onUpdate = function(){
var count = 0;
var pos = this.pos;
loadedGeometry.vertices.forEach(function(e){
var newY = ((e.y + 3.22544) * pos) - 3.22544;
particleSystem.geometry.vertices[count++].set(e.x,newY,e.z);
});
particleSystem.sortPaticles = true;
};
tween.onUpdate(onUpdate);
tweenBack.onUpdate(onUpdate);
補間動畫在模型加載完畢時啓動。
var loader = new THREE.PLYLoader();
loader.load('../assets/models/test.ply',function(geometry){
...
tween.start();
...
});
開啓補間動畫之後,我們需要告知Three.js庫什麼時候應該刷新已知的所有補間。調用TWEEN.update().
function render(){
TWEEN.update();
WebGLRenderer.render(scene,camera);
requestAnimationFrame(render);
}
使用相機:
Three.js提供了幾個相機控件,可以用來控制場景中的相機。
example/js/controls
FirstPersonControls:第一人稱控件,鍵盤移動,鼠標轉動
FlyControls:飛行器模擬控件,鍵盤和鼠標來控制相機的移動和轉動
RollControls:翻滾控件,FlyControls的簡化版,可以繞z軸旋轉
TrackballControls:軌跡球控件,用鼠標來輕鬆移動、平移和縮放場景
OrbitControls:軌道控件,用於特定場景,模擬軌道中的衛星,可以用鼠標和鍵盤在場景中游走
PathControls:路徑控件,相機可以沿着預定義的路徑移動。可以四處觀看,但不能改變自身的位置。
一、軌跡球控件
最常用的控件
引用:TrackballControls.js
var trackballControls = new THREE.TrackballControls(camera);
trackballControls.rotateSpeed = 1.0;
trackballControls.zoomSpeed = 1.0;
trackballControls.panSpeed = 1.0;
//trackballControls.noZoom = true;//禁止縮放場景
更新相機的位置:
var clock = new THREE.Clock();
function render(){
var delta = clock.getDelta();
trackballControls.update(delta);
requestAnimationFrame(render);
webGLRenderer.render(scene,camera);
}
THREE.Clock對象,用來精確計算出上次調用後經過的時間,或者一個渲染循環耗費的時間。
clock.getDelta() 返回此次調用和上次調用之間的時間間隔。
二、飛行控件
引用:FlyControls.js
綁定到相機上:
var flyControls = new THREE.FlyControls(camera);
flyControls.movementSpeed = 25;
flyControls.domElement = document.querySelector('#WebGL-output');
flyControls.rollSpeed = Math.PI / 24;
flyControls.autoForward = true;
flyControls.dragToLook = false;
三、翻滾控件
RollControls和FLyControls基本一致。
var rollControls = new THREE.RollControls(camera);
rollControls.movementSpeed = 25;
rollControls.lookSpeed = 3;
四、第一人稱控件
FirstPersonControls
var camControls = new THREE.FirstPersonControls(camera);
camControls.lookSpeed = 0.4;
camControls.movementSpeed = 20;
camControls.noFly = true;
camControls.lookVertical = true;
camControls.constrainVertical = true;
camControls.verticalMin = 1.0;
camControls.verticalMax = 2.0;
//下面兩個屬性定義場景初次渲染時相機指向的位置
camControls.lon = -150;
camControls.lat = 120;
五、軌道控件
OrbitControls 控件是在場景中繞某個對象旋轉、平移的好方法。
引用:OrbitControls.js
var orbitControls = new THREE.OrbitControls(camera);
orbitControls.autoRotate = true;
var clock = new THREE.Clock();
...
var delta = clock.getDelta();
orbitControls.update(delta);
六、路徑控件
創建一個路徑
function getPath(){
var points = [];
var r = 20;
var cX = 0;
var cY = 0;
for (var i = 0 ; i < 1440 ; i += 5){
var x = r * Math.cos(i * (Math.PI / 180)) + cX;
var z = r * Math.sin(i * (Math.PI / 180)) + cY;
var y = i / 30;
points.push(new THREE.Vector3(x,y,z));
}
return points;
}
引用:PathControls.js
注意:加載控件之前要保證沒有手動設置相機的位置,或者使用過相機的lookAt()函數,因爲這可能會跟特定的控件相牴觸。
var pathControls = new THREE.PathControls(camera);
//配置pathControls
pathControls.duration = 70;
pathControls.useConstantSpeed = true;
pathControls.lookSpeed = 0.1;
pathControls.lookVertical = true;
pathControls.lookHorizontal = true;
pathControls.verticalAngleMap = {
srcRange:[0,2*Math.PI],
dsRange:[1.1,3.8]
};
pathControls.horizontalRange = {
srcRange:[0,2*Math.PI],
dsRange:[0.3,Math.PI - 0.3]
};
pathControls.lon = 300;
pathControls.lat = 40;
//添加路徑
controls.points.forEach(function(e){
pathControls.wayPoints.push([e.x,e.y,e.z]);
});
//初始化控件
pathControls.init();
//開始動畫,保證相機可以自動移動
scene.add(pathControls.animationParent);
pathControls.animation.play(true,0);
幀循環:
var delta = clock.getDelta();
THREE.AnimationHandler.update(delta);
pathControls.update(delta);
變形動畫和骨骼動畫
變形動畫:通過變形目標,可以定義經過變形之後的版本,或者說關鍵位置。對於這個變形目標其所有的頂點都會被存儲下來。
骨骼動畫(蒙皮動畫):通過定義骨骼,並把頂點綁定到特定的骨頭上。當移動一塊骨頭時,任何相連的骨頭都會做相應的移動,骨頭上的綁定的頂點也會隨之移動。
變形動畫比骨骼動畫能夠在three.js中更好的工作。骨骼動畫的主要問題是,如何從Blender等三維程序中比較好的導出數據。
一、變形動畫
變形目標是製作變形動畫直接的方法。
原理:爲所有的頂點都指定一個關鍵位置,然後讓three.js將這些頂點從一個關鍵位置移動到另一個。
不足:對於大型網格,模型文件會變得非常大。因爲在每個關鍵位置上,所有頂點的位置都要存儲兩遍。
Three.js提供了一種方法使得模型可以從一個位置遷移到另一個位置,但是這也意味着我們可能不得不手工記錄當前所處的位置,以及下一個變形目標的位置。一旦到達目標位置,我們就得重複這個過程以達到下一個位置。
爲此,Three.js爲我們提供了MorphAnimMesh 變形動畫網格
1.MorphAnimMesh 變形動畫網格
var loader = new THREE.JSONLoader();
loader.load('../assets/models/horse.js',function(geometry,mat){
var mat = new THREE.MeshLambertMaterial({
color:0xffffff,
morphNormals:false,
morphTargets:true,//使Mesh可以執行動畫
vertexColors:THREE.FaceColors
});
morphColorsToFaceColors(geometry);
geometry.computeMorphNormals();//確保變形目標的所有法向量都會被計算。這對於正確的光照和陰影是必須的。
meshAnim = new THREE.MorphAnimMesh(geometry,mat);
scene.add(meshAnim);
},'../assets/models');
//在某個特定的變形目標上爲某些面指定顏色是可能的。
//該函數保證動畫過程中使用正確的顏色。
function morphColorsToFaceColors(geometry){
if(geometry.morphColors && geometry.morphColors.length){
var colorMap = geometry.morphColors[0];
for(var i = 0; i < colorMap.colors.length;i++){
geometry.faces[i].color = colorMap.colors[i];
geometry.faces[i].color.offsetHSL(0,0.3,0);
}
}
}
幀循環:
function render(){
var delta = color.getDelta();
webGLRenderer.clear();
if(meshAnim){
meshAnim.updateAnimation(delta*100);
meshAnim.rotation.y += 0.01;
}
requestAnimationFrame(render);
webGLRenderer.render(scene,camera);
}
2.通過設置morphTargetInfluence屬性創建動畫
var cubeGeometry = new THREE.CubeGeometry(4,4,4);
var cubeMaterial = new THREE.MeshLambertMaterial({
morphTargets:true,
color:0xff0000
});
var cubeTarget1 = new THREE.CubeGeometry(2,10,2);
var cubeTarget2 = new THREE.CubeGeometry(8,2,8);
cubeGeometry.morphTargets[0] = {name:'t1',vertices:cubeTarget2.vertices};
cubeGeometry.morphTargets[1] = {name:'t2',vertices:cubeTarget1.vertices};
cubeGeometry.computeMorphNormals();
var cube = new THREE.Mesh(cubeGeometry,cubeMaterial);
var controls = new function(){
this.influence1 = 0.01;
this.influence2 = 0.01;
this.update = function(){
cube.morphTargetInfluences[0] = controls.influence1;
cube.morphTargetInfluences[1] = controls.influence2;
}
}
二、用骨骼和蒙皮製作動畫 THREE.SkinnedMesh
加載Three.js骨骼動畫模型,該文件中帶有骨骼的定義。
var loader = new THREE.JSONLoader();
loader.load('../assets/models/hand-1.js',function(geometry,mat){
var mat = new THREE.MeshLambertMaterial({
color:0xF0C8C9,
skinning:true //使用帶有蒙皮的網格對象,需要對模型所用材質的skinning屬性設置爲true。
});
//帶有蒙皮的網格對象
mesh = new THREE.SkinnedMesh(geometry,mat);
mesh.rotation.x = 0.5 * Math.PI;
mesh.rotation.z = 0.7 * Math.PI;
mesh.bones.forEach(function(e){
u.useQuaternion = false;//如果爲true,則必須使用四元素來定義骨頭的旋轉。爲false,我們就可以使用一般方式來設置這個旋轉。
});
scene.add(mesh);
tween.start();
},'../assets/models');
var onUpdate = function(){
var pos = this.pos;
//旋轉手指
mesh.bones[5].rotation.set(0,0,pos);
mesh.bones[6].rotation.set(0,0,pos);
mesh.bones[10].rotation.set(0,0,pos);
mesh.bones[11].rotation.set(0,0,pos);
mesh.bones[15].rotation.set(0,0,pos);
mesh.bones[16].rotation.set(0,0,pos);
mesh.bones[20].rotation.set(0,0,pos);
mesh.bones[21].rotation.set(0,0,pos);
//旋轉手腕
mesh.bones[1].rotation.set(pos,0,0);
}
這裏我們是手動變化骨骼的位置,以達到動畫的效果。
這裏缺少的是如何以固定的時間間隔調用update方法,即更新骨骼的位置,爲此,我們使用了Tween.js庫。
使用外部模型創建動畫
支持動畫的幾個模型:
1.帶有JSON導出器的Blender
2.Collada模型
3.MD2模型
一、加載Blender中導出的動畫:
在Blender中創建動畫:
1.模型中的頂點至少要在一個頂點組中
2.blender中頂點組的名字必須跟控制這個頂點組的骨頭的名字相對應。只有這樣,當骨頭被移動時Three.js才能找到需要修改的頂點。
3.只有第一個action可以導出,所以要保證你想要導出的動畫是第一個action。
4.創建keyframes(關鍵幀名)時,最好選擇所有骨頭,即便它們沒有變化。
5.導出模型時,要保證模型處於靜止狀態。如果不是這樣,那麼你看到的動畫將非常混亂。
var loader = new THREE.JSONLoader();
loader.load('../assets/models/hand-2.js',function(geometry,mat){
THREE.AnimationHandler.add(geometry.animation);//註冊動畫
var mat = new THREE.MeshLambertMaterial({
color:0xF0C8C9,
skinning:true
});
mesh = new THREE.SkinnedMesh(geometry,mat);
mesh.rotation.x = 0.5 * Math.PI;
mesh.rotation.z = 0.7 * Math.PI;
scene.add(mesh);
var animation = new THREE.Animation(mesh,'wave');//創建動畫,動畫的名字要和Blender中的名字一致
animation.play();
},'../assets/models');
幀循環:
THREE.AnimationHandler.update(clock.getDelta());
二、從Collada模型中加載動畫
引入:ColladaLoader.js
var loader = new THREE.ColladaLoader();
loader.load('../assets/models/moster.dae',function(collada){
var geom = collada.skins[0].geometry;
var mat = collada.skins[0].material;
geom.computeMorphNormals();
mat.morphNormals = true;
meshAnim = new THREE.MorphAnimMesh(geom,mat);
meshAnim.scale.set(0.15,0.15,0.15);
meshAnim.rotation.x = -0.15 * Math.PI;
meshAnim.rotation.z = -100;
meshAnim.rotation.y = -60;
scene.add(meshAnim);
meshAnim.duration = 5000;
});
一個Collada文件中不僅可以包含模型,還可以保存整個場景,包括相機、光源、動畫等。
使用Collada模型最好的方式是將loader.load函數調用結果輸出到控制檯,然後決定使用哪些組件。
幀循環:
function render(){
...
meshAnim.updateAnimation(delta*1000);
...
}
從雷神之錘模型中加載動畫
首先得將MD2格式轉換爲Three.js中的JavaScript格式。
http://oos.moxiecode.com/js_webgl/md2_converter/
MD2模型文件中通常會保存幾個角色動畫。Three.js提供了一種功能可以讓我們選擇動畫,並調用playAniamtion()進行播放。
mesh.parseAnimations();//返回一組動畫的名稱。
mesh.parseAnimations();
var animLabels = [];
for(var key in mesh.geometry.animations){
if(key == 'length' || !mesh.geometry.animations.hasOwnProperty(key))
continue;
animLabels.push(key);
}
gui.add(controls,'animations',animLabels).onchange(function(e){
mesh.playAnimation(controls.animations,controls.fps);
});
小結:
變形目標:MorphAnimMesh類
骨骼動畫:SkinnedMesh類