第九章 创建动画和移动相机
基础动画:
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=3var 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 = http://www.ithao123.cn/{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);//配置pathControlspathControls.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类
联系客服