天天看点

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类

继续阅读