天天看點

09.ThreeJs開發指南-第九章-建立動畫和移動相機

第九章 建立動畫和移動相機

基礎動畫:

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 =  + ( * (Math.cos(step));
    sphere.position.y =  + ( * Math.abs((Math.sin(step)));

    scalingStep += controls.scalingSpeed;

    var scaleX = Math.abs(Math.sin(scalingStep / ));
    var scaleY = Math.abs(Math.cos(scalingStep / ));
    var scaleZ = Math.abs(Math.sin(scalingStep / ));
    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) *  -,
        (event.clientY / window.innderHeight) *  + ,
        
    );

    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 >  ){
        intersects[].object.material.transparent = true ;
        intersects[].object.material.opacity =  ;
    }
}
           

點選螢幕:

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

這個庫可以定義某個屬性在兩個值之間的過渡,自動計算出起始值和結束值之間的所有中間值。這個過程叫做:補間。

//s從x=遞減到x=
var tween = new THREE.Tween({x:}).to({x:},).easing(TWEEN.Easing.Elastic.InOut).onUpdate(function(){

});
           

這個漸變的過程可以是線性的,指數性的,還可能是其他的方式。

http://sole.github.io/tween.js/examples/03_graphs.html

屬性值在指定時間内的變化稱為 easing (緩動)

var posSrc = {pos:};
var tween = new THREE.Tween(posSrc).to({pos:},);
tween.easing (TWEEN.Easing.Sinusoidal.InOut);

var tweenBack = new THREE.Tween(posSrc).to({pos:},);
tweenBack.easing(TWEEN.Easing.Sinusoidal.InOut);

//使兩個補間動畫首尾相連
tween.chain(tweenBack);
tweenBack.chain(tween);

//周遊粒子系統中的每個點,并用補間動畫提供的位置更新頂點的位置。
var onUpdate = function(){

    var count = ;
    var pos = this.pos;

    loadedGeometry.vertices.forEach(function(e){
        var newY = ((e.y + ) * pos) - ;
        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 = ;
trackballControls.zoomSpeed = ;
trackballControls.panSpeed = ;
//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 = ;
flyControls.domElement = document.querySelector('#WebGL-output');
flyControls.rollSpeed = Math.PI / ;
flyControls.autoForward = true;
flyControls.dragToLook = false;
           

三、翻滾控件

RollControls和FLyControls基本一緻。

var rollControls = new THREE.RollControls(camera);
rollControls.movementSpeed = ;
rollControls.lookSpeed = ;
           

四、第一人稱控件

FirstPersonControls

var camControls = new THREE.FirstPersonControls(camera);
camControls.lookSpeed = ;
camControls.movementSpeed = ;
camControls.noFly = true;
camControls.lookVertical = true;
camControls.constrainVertical = true;
camControls.verticalMin = ;
camControls.verticalMax = ;
//下面兩個屬性定義場景初次渲染時相機指向的位置
camControls.lon = -;
camControls.lat = ;
           

五、軌道控件

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 = ;
    var cX = ;
    var cY = ;

    for (var i =  ; i <  ; i += ){

        var x = r * Math.cos(i * (Math.PI / )) + cX;
        var z = r * Math.sin(i * (Math.PI / )) + cY;
        var y = i / ;

        points.push(new THREE.Vector3(x,y,z));
    }

    return points;  
}
           

引用:PathControls.js

注意:加載控件之前要保證沒有手動設定相機的位置,或者使用過相機的lookAt()函數,因為這可能會跟特定的控件相抵觸。

var pathControls = new THREE.PathControls(camera);

//配置pathControls
pathControls.duration = ;
pathControls.useConstantSpeed = true;
pathControls.lookSpeed = ;
pathControls.lookVertical = true;
pathControls.lookHorizontal = true;
pathControls.verticalAngleMap = {
    srcRange:[,*Math.PI],
    dsRange:[,]
};
pathControls.horizontalRange = {
    srcRange:[,*Math.PI],
    dsRange:[,Math.PI - ]
};
pathControls.lon = ;
pathControls.lat = ;

//添加路徑
controls.points.forEach(function(e){
    pathControls.wayPoints.push([e.x,e.y,e.z]);
});

//初始化控件
pathControls.init();

//開始動畫,保證相機可以自動移動
scene.add(pathControls.animationParent);
pathControls.animation.play(true,);


幀循環:
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:,
        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[];

        for(var i = ; i < colorMap.colors.length;i++){
            geometry.faces[i].color = colorMap.colors[i];
            geometry.faces[i].color.offsetHSL(,,);
        }
    }
}
           

幀循環:

function render(){

    var delta = color.getDelta();

    webGLRenderer.clear();
    if(meshAnim){
        meshAnim.updateAnimation(delta*100);
        meshAnim.rotation.y += ;
    }

    requestAnimationFrame(render);
    webGLRenderer.render(scene,camera);
}
           

2.通過設定morphTargetInfluence屬性建立動畫

var cubeGeometry = new THREE.CubeGeometry(,,);
var cubeMaterial = new THREE.MeshLambertMaterial({
    morphTargets:true,
    color:
});

var cubeTarget1 = new THREE.CubeGeometry(,,);
var cubeTarget2 = new THREE.CubeGeometry(,,);

cubeGeometry.morphTargets[] = {name:'t1',vertices:cubeTarget2.vertices};
cubeGeometry.morphTargets[] = {name:'t2',vertices:cubeTarget1.vertices};

cubeGeometry.computeMorphNormals();

var cube = new THREE.Mesh(cubeGeometry,cubeMaterial);
           
var controls = new function(){
    this.influence1 = ;
    this.influence2 = ;

    this.update = function(){

        cube.morphTargetInfluences[] = controls.influence1;
        cube.morphTargetInfluences[] = 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:,
        skinning:true //使用帶有蒙皮的網格對象,需要對模型所用材質的skinning屬性設定為true。
    });

    //帶有蒙皮的網格對象
    mesh = new THREE.SkinnedMesh(geometry,mat);

    mesh.rotation.x =  * Math.PI;
    mesh.rotation.z =  * 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[].rotation.set(,,pos);
    mesh.bones[].rotation.set(,,pos);
    mesh.bones[].rotation.set(,,pos);
    mesh.bones[].rotation.set(,,pos);
    mesh.bones[].rotation.set(,,pos);
    mesh.bones[].rotation.set(,,pos);
    mesh.bones[].rotation.set(,,pos);
    mesh.bones[].rotation.set(,,pos);

    //旋轉手腕
    mesh.bones[].rotation.set(pos,,);

}
           

這裡我們是手動變化骨骼的位置,以達到動畫的效果。

這裡缺少的是如何以固定的時間間隔調用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:,
        skinning:true
    });

    mesh = new THREE.SkinnedMesh(geometry,mat);
    mesh.rotation.x =  * Math.PI;
    mesh.rotation.z =  * Math.PI;

    scene.add(mesh);

    var animation = new THREE.Animation(mesh,'wave');//建立動畫,動畫的名字要和Blender中的名字一緻

    animation.play();

},'../assets/models');
           

幀循環:

二、從Collada模型中加載動畫

引入:ColladaLoader.js

var loader = new THREE.ColladaLoader();
loader.load('../assets/models/moster.dae',function(collada){

    var geom = collada.skins[].geometry;
    var mat = collada.skins[].material;

    geom.computeMorphNormals();
    mat.morphNormals = true;

    meshAnim = new THREE.MorphAnimMesh(geom,mat);

    meshAnim.scale.set(,,);
    meshAnim.rotation.x = - * Math.PI;
    meshAnim.rotation.z = -;
    meshAnim.rotation.y = -;

    scene.add(meshAnim);
    meshAnim.duration = ;

});
           

一個Collada檔案中不僅可以包含模型,還可以儲存整個場景,包括相機、光源、動畫等。

使用Collada模型最好的方式是将loader.load函數調用結果輸出到控制台,然後決定使用哪些元件。

幀循環:

function render(){
    ...
    meshAnim.updateAnimation(delta*);
    ...
}
           

從雷神之錘模型中加載動畫

首先得将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類

繼續閱讀