创建动画和移动相机
1.如何通过鼠标获取网格对象
首先需要把鼠标的起始位置在左上角的屏幕坐标转换为笛卡尔坐标。然后将坐标转为为以Camera为中心点的三维空间坐标。接下来根据摄像头位置和鼠标位置的法向量创建射线对象。最终根据射线对象的intersectObjects函数确认哪个网格被选中。
下面是比较经典的使用方法:
function onDocumentMouseMove(event) { if (controls.showRay) { var vector = new THREE.Vector3(( event.clientX / window.innerWidth ) * 2 - 1, -( event.clientY / window.innerHeight ) * 2 + 1, 0.5); vector = vector.unproject(camera); var raycaster = new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize()); var intersects = raycaster.intersectObjects([sphere, cylinder, cube]); if (intersects.length > 0) { var points = []; points.push(new THREE.Vector3(-30, 39.8, 30)); points.push(intersects[0].point); var mat = new THREE.MeshBasicMaterial({color: 0xff0000, transparent: true, opacity: 0.6}); var tubeGeometry = new THREE.TubeGeometry(new THREE.SplineCurve3(points), 60, 0.001); if (tube) scene.remove(tube); if (controls.showRay) { tube = new THREE.Mesh(tubeGeometry, mat); scene.add(tube); } } } }
2.使用Tween.js动画
Tween.js是一个小型的Javascript库,可以从http://github.com/sole/tween.js/下载。这个库可以 用来定义某个属性在两个值之间的过度,自动计算出起始值和结束值之间的所有中间值。这个过程叫做tweening(补间)。例如下面的代码:
var pointCloud = new THREE.Object3D(); var loadedGeometry; var posSrc = {pos: 1}; var tween = new TWEEN.Tween(posSrc).to({pos: 0}, 5000); tween.easing(TWEEN.Easing.Sinusoidal.InOut); var tweenBack = new TWEEN.Tween(posSrc).to({pos: 1}, 5000); tween.easing(TWEEN.Easing.Sinusoidal.InOut); tween.chain(tweenBack); var onUpdate = function(){ var count = 0; var pos = this.pos; loadedGeometry.vertices.forEach(function(e){ var newY = ((e.y + 3.22544) * pos) - 3.22544; pointCloud.geometry.vertices[count++].set(e.x, newY, e.z); }); pointCloud.sortParticles = true; } tween.onUpdate(onUpdate); tweenBack.onUpdate(onUpdate); var loader = new THREE.PLYLoader(); loader.load("../assets/models/test.ply", function(geometry){ loadedGeometry = geometry.clone(); var material = new THREE.PointCloudMaterial({ color: 0xffffff, size: 0.4, opacity: 0.6, transparent: true, blending: THREE.AdditiveBlending, map: generateSprite() }); pointCloud = new THREE.PointCloud(geometry, material); pointCloud.sortParticles = true; tween.start(); scene.add(pointCloud); });
代码定义了两个补间对象tween和tweenBack,让pos值从1减到0,再从0增加到1。tween会在中间按照动画效果补充很多中间pos值,调用tween.OnUpdate给补间动画注册一个回调事件,这个回到事件中可获取补间值(this.pos)。我们可通过这个补间值来更新坐标值从而实现动画。另外我们可以调用tween.easing指定补间动画按照那种动画效果产生。
设置完成后,需要调用tween.start()启动动画。但现在我们还不知道什么时候执行补间更新通知。所以我们可以在渲染函数每次执行时调用。
function render() { stats.update(); TWEEN.update(); requestAnimationFrame(render); webGLRenderer.render(scene, camera); }
3.相机控件
Three.js提供了几个相机控件,可以用来控制场景中的相机。这些控件在Three.js发布包中,控件包括:
控件名称/描述
FirstPersonControls(第一人称控件)/该控件的行为类似第一人称设计游戏中的相机,用键盘移动,用鼠标转动
FlyControls(飞行控件)/飞机模拟器控件,用键盘和鼠标来控制相机的移动和转动
RollControls/该控件时FlyControls的简化版,让你可以绕着z轴旋转
TrackballControls(轨迹球控件)/最常用的控件,你可以用鼠标(或轨迹球)来轻松移动、平移和缩放场景
OrbitControls(轨道控件)/用于特定场景,模拟轨道中的卫星,你可以用鼠标和键盘在场景中游走
PathControls(路径控件)/使用这个控件,相机可以沿着预定义的路径移动。你可以将它跟过山车相比较,在过山车上你可以朝四周看,但不能改变自身位置
4.轨迹球控件TrackballControls
使用TrackballConrols之前需要引入TrackballControls.js文件。通过控件可以旋转、缩放、平移网格,并且操作速度可以控制。例下面一段代码实现了轨迹球控制功能。首先创建一个轨迹球控件对象,并设置旋转、缩放、移动速度。代码使用OBJMTLLoader把一个外部模型加载进来,setRandomColors函数用来随机设置外部模型外建筑的材质颜色。通过递归查询类型为THREE.Mesh对象,然后设置其材质的环境色以及透明度等参数。
var trackballControls = new THREE.TrackballControls(camera); trackballControls.rotateSpped = 1.0; trackballControls.zoomSpeed = 1.0; trackballControls.panSpeed = 1.0; trackballControls.staticMoving = true; var ambientLight = new THREE.AmbientLight(0x383838); scene.add(ambientLight); var spotLight = new THREE.SpotLight(0xffffff); spotLight.position.set(300, 300, 300); spotLight.intensity = 1; scene.add(spotLight); // add the output of the renderer to the html element document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement); var step = 0; var mesh; var loader = new THREE.OBJMTLLoader(); var load = function(object){ var scale = chroma.scale(["red", "green", "blue"]); setRandomColors(object, scale); mesh = object; scene.add(mesh); } render(); function setRandomColors(object, scale){ var children = object.children; if(children && children.length > 0){ children.forEach(function(e){ setRandomColors(e, scale); }); }else{ if(object instanceof THREE.Mesh){ object.material.color = new THREE.Color(scale(Math.random()).hex()); if(object.material.name.indexOf("building") === 0){ object.material.emissive = new THREE.Color(0x444444); object.material.transparent = true; object.material.opacity = 0.8; } } } } function render(){ stats.update(); var delta = clock.getDelta(); trackballControls.update(delta); requestAnimationFrame(render); webGLRenderer.render(scene, camera); }
这里使用了一个颜色操作的库chroma.js,它用来生成随机颜色。还需要注意的是,我们需要调用TrackballControls的update(delta)函数更新相机的位置。delta是此次调用和上次调用的时间间隔。
如何求时间间隔?这里我们使用Three.js自带的THREE.Clock对象,我们在初始化时就创建对象,在下次渲染时可调用它的getDelta()函数获取本次和上次的时间间隔。
使用TrackballControls,可以通过以下操作方式来旋转、缩放、移动网格:
操作/动作
按住左键,拖动/在场景中旋转、翻滚相机
转动鼠标滚轮/放大和缩小
按住中间,拖动/放大和缩小
按住右键,拖动/在场景中平移
5.飞行控件FlyControls
和TrackballControls功能相似。首先需要引入FlyControls.js文件。我们可以配置控件,并绑定到相机。
var flyControls = new THREE.FlyControls(camera); flyControls.movementSpeed = 25; flyControls.domElement = document.querySelector("#WebGL-output"); flyControls.rollSpeed = Math.PI/24; flyControls.dragToLook = false;
控件必须设置domElement属性,该属性和WebGLRenderer指向同一个Dom元素。movementSpeed设置移动速度,rollSpeed设置滚动速度,dragToLook设置鼠标悬浮时还是鼠标按下时移动摄像头。
最后也别忘了在render函数中调用flyControls.update(delta)去移动摄像头。控件操控方式如下:
操控/动作
按住左键和中间/往前移动
按住右键/往后移动
鼠标移动/往四周看
W/往前移动, S/往后移动,A/左移,D/右移,R/上移,F/下移
上、下、左、右键/向上、下、左、右看
Q/向左翻滚
E/向右翻滚
6.第一人称控件FirstPersonControls
第一人称控件对应的js库名称为FirstPersonControls.js,使用前需引入该js文件。下面实例化该对象的代码:
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;
使用该控件时只有最后两个属性(lon、lat)需要小心对待。这两个属性定义的是场景初次渲染时相机指向的位置。操控方式如下:
操控/动作
移动鼠标/往四周看
上、下、左、右方向键/前、后、左、右移动
W/前移,A/左移,S/后移,D/右移,R/上移,F/下移, Q/停止
7.轨道控件OrbitControl
OrbitControl控件时在场景中绕某个对象旋转、平移的好方法。OrbitControl是Three.js的扩展库,对应OrbitControls.js文件。实例化代码如下:
var orbitControls = new THREE.OrbitControls(camera); orbitControls.autoRotate = true;
代码中设置了autoRotate属性,使摄像头绕着lookAt位置旋转。OrbitControl也支持鼠标和键盘操作。操作如下:
操控/动作
按住左键,并移动/绕着场景中心旋转相机
按住滚动或按住中间,并移动/放大缩小
按住右键,并移动/在场景中移动
上、下、左、右方向键/在场景中移动
8.用MorphAnimMesh制作动画
hree.js提供一种方法使得模型可以从一个位置移到另一个位置,但是这也意味着我们可能不得不手工记录当前所处的位置,以及下一个变形目标的位置。一旦达到目标位置,我们就得重复这个过程已达到下一个位置。幸运的是,Three.js提供了一个特别的网格,MorphAnimMesh(变形动画网格),该网格帮我们处理这些细节。
下面是使用MorphAnimMesh的一段代码:
var loader = new THREE.JSONLoader(); loader.load("../assets/models/horse.js", function(geometry, mat){ var mat = new THREE.MeshLambertMaterial({ morphTargets: true, vertexColors: THREE.FaceColors }); var mat2 = new THREE.MeshLambertMaterial({ vertexColors: THREE.FaceColors, color: 0xffffff }); mesh = new THREE.Mesh(geometry, mat); mesh.position.x = -100; frames.push(mesh); currentMesh = mesh; morphColorsToFaceColors(geometry); mesh.geometry.morphTargets.forEach(function(e){ var geom = new THREE.Geometry(); geom.vertices = e.vertices; geom.faces = geometry.faces; var morphMesh = new THREE.Mesh(geom, mat2); frames.push(morphMesh); morphMesh.position.x = -100; }); geometry.computeVertexNormals(); geometry.computeFaceNormals(); geometry.computeMorphNormals(); meshAnim = new THREE.MorphAnimMesh(geometry, mat); meshAnim.duration = 1000; meshAnim.position.x = 200; meshAnim.position.z = 0; scene.add(meshAnim); showFrame(0); }, "../assets/models"); function showFrame(e){ scene.remove(currentMesh); scene.add(frames[e]); currentMesh = frames[e]; console.log(currentMesh); } function morphColorsToFaceColors(geom){ if(geom.morphColors && geom.morphColors.length){ var colorMap = geom.morphColors[0]; for(var i = 0; i < colorMap.colors.length; i++){ geom.faces[i].color = colorMap.colors[i]; geom.faces[i].color.offsetHSL(0, 0.3, 0); } } }
代码从外部加载了一个json格式的模型,当加载完成后,创建一个材质设置属性morphTargets为true,这样网格才会动起来。所有动画几何体都存储在mesh.geometry.morphTargets数组中,我们可以遍历该数组直接读取他获取不同位置的几何体。
导入的几何体我们还需要分别调用几何体的computeVertexNormals()、computeFaceNormals()、computeMorphNormals()函数重新计算顶点、面、变形发向量。最后使用MorphAnimMesh对象创建一个动画网格,并设置duration以及position属性等。和其他动画控件一样,要让网格动起来,每次渲染时还得调用updateAnimation函数,代码如下:
function render(){ stats.update(); var delta = clock.getDelta(); if(meshAnim){ meshAnim.updateAnimation(delta * 1000); meshAnim.rotation.y += 0.01; } webGLRenderer.render(scene, camera); requestAnimationFrame(render); }
9.通过设置morphTargetInfluence属性创建动画
网格包含morphTargetInflences属性,他对应了geometry的morphTargets数组。如下面的代码,cubeGeometry的morphTargets包含了两个值,对应了两个不同的顶点集合。在controls中的update函数,我们设置了cube的morphTargetInfluences属性。morphTargetInfluences[0]相当于是morphTargets[0]的动画时间戳,值从0到1。当morphTargetInfluences[0]等于0,网格显示的是cube原始的顶点,当morphTargetInfluences[0]等于1时cube的顶点完全过度到morphTargets[0]了。
var cubeGeometry = new THREE.BoxGeometry(4, 4, 4); var cubeMaterial = new THREE.MeshLambertMaterial({color: 0xff0000, morphTargets: true}); var cubeTarget1 = new THREE.BoxGeometry(2, 10, 2); var cubeTarget2 = new THREE.BoxGeometry(8, 2, 8); cubeGeometry.morphTargets[0] = {name: "t1", vertices: cubeTarget2.vertices}; cubeGeometry.morphTargets[1] = {name: "t2", vertices: cubeTarget1.vertices}; var cube = new THREE.Mesh(cubeGeometry, cubeMaterial); cube.position.x = 0; cube.position.y = 3; cube.position.z = 0; scene.add(cube); var controls = new function(){ this.influence1 = 0.01; this.influence2 = 0.02; this.update = function(){ cube.morphTargetInfluences[0] = controls.influence1; cube.morphTargetInfluences[1] = controls.influence2; } };
加入我们在render函数中逐渐提增influences的值,那么我们就可以看到变形动画了。代码如下:
function render() { stats.update(); controls.influence1 += 0.001; controls.influence2 += 0.001; controls.update(); // render using requestAnimationFrame renderer.render(scene, camera); requestAnimationFrame(render); }
10.用骨骼和蒙皮制作动画
骨骼动画比变形动画复杂些。当你用骨骼来做动画时,你移动一下骨骼,而Three.js必须决定如何相应地迁移附着在骨骼上的皮肤。针对此动画Three.js提供了SkinnedMesh网格对象,但我们修改它骨骼属性,该对象自动处理皮肤的位置。下面是的例子加载了一个骨骼手臂,并设置了它的位置属性。
var loader = new THREE.JSONLoader(); loader.load('../assets/models/hand-1.js', function (geometry, mat) { var mat = new THREE.MeshLambertMaterial({color: 0xF0C8C9, skinning: true}); mesh = new THREE.SkinnedMesh(geometry, mat); // rotate the complete hand mesh.rotation.x = 0.5 * Math.PI; mesh.rotation.z = 0.7 * Math.PI; // add the mesh scene.add(mesh); // and start the animation tween.start(); }, '../assets/models'); var onUpdate = function () { var pos = this.pos; console.log(mesh.skeleton); // rotate the fingers mesh.skeleton.bones[5].rotation.set(0, 0, pos); mesh.skeleton.bones[6].rotation.set(0, 0, pos); mesh.skeleton.bones[10].rotation.set(0, 0, pos); mesh.skeleton.bones[11].rotation.set(0, 0, pos); mesh.skeleton.bones[15].rotation.set(0, 0, pos); mesh.skeleton.bones[16].rotation.set(0, 0, pos); mesh.skeleton.bones[20].rotation.set(0, 0, pos); mesh.skeleton.bones[21].rotation.set(0, 0, pos); // rotate the wrist mesh.skeleton.bones[1].rotation.set(pos, 0, 0); }; var tween = new TWEEN.Tween({pos: -1}) .to({pos: 0}, 3000) .easing(TWEEN.Easing.Cubic.InOut) .yoyo(true) .repeat(Infinity) .onUpdate(onUpdate);
代码用了TWEEN动画库,具体的api可以在官网查看。这里主要介绍onUpdate函数,动画在执行时,tween的pos属性值也在变化,逐渐从-1变动0,正好用这个属性来设置骨骼对象的rotation属性。mesh.skeleton.bones包含了很多个骨骼对象,具体要设置哪一个,需要了解清楚模型文件。上面的代码只是实现了动画,要让骨骼动起来,还得在render函数中调用:TWEEN.update()。
11.用Blender创建骨骼动画
使用Blender可以创建动画,我们可以使用three.js导出插件导出包含动画的模型。在导出时需要注意一下细节:
模型中的顶点至少要在一个顶点组中;
Blender中顶点组的名字必须跟控制这个顶点组的骨头的名字相对应。只有这样,当过被移除时Three.js才能找到需要修改的顶点;
只有第一个action(动作)可以导出,所以要保证你想要导出的动画时第一个action;
创建keyframs时,最后选择所有骨头,即便没有改变;
导出模型时,要保证模型处于静止状态。如果不这样,那么你看到的动画将会非常混乱;
导出模型之后,使用JSONLoader加载模型。使用THREE.Animation创建动画对象。然后调用animation.play()函数开始播放动画。Three.js提供了一个辅助类SkeletonHelper,它可以通过连线查看我们的动画效果。示例代码如下:
var loader = new THREE.JSONLoader(); loader.load('../assets/models/hand-2.js', function (model, mat) { var mat = new THREE.MeshLambertMaterial({color: 0xF0C8C9, skinning: true}); mesh = new THREE.SkinnedMesh(model, mat); var animation = new THREE.Animation(mesh, model.animation); mesh.rotation.x = 0.5 * Math.PI; mesh.rotation.z = 0.7 * Math.PI; scene.add(mesh); helper = new THREE.SkeletonHelper(mesh); helper.material.linewidth = 2; helper.visible = false; scene.add(helper); // start the animation animation.play(); }, '../assets/models'); 和其他模型一样,在render函数中需要调用update函数。代码如下: function render() { stats.update(); var delta = clock.getDelta(); if (mesh) { helper.update(); THREE.AnimationHandler.update(delta); } // render using requestAnimationFrame requestAnimationFrame(render); webGLRenderer.render(scene, camera); }
12.加载Collada动画
加载Collada动画和其他加载方式相似。这里使用的是ColladaLoader加载器,加载完成返回模型是包含了整个场景。根据需要我们只取skins里边的骨骼网格。取出之后根据这个网格创建animation动画,并根据实际显示设置网格的位置和缩放。示例代码如下:
var loader = new THREE.ColladaLoader(); loader.load('../assets/models/monster.dae', function (collada) { var child = collada.skins[0]; scene.add(child); var animation = new THREE.Animation(child, child.geometry.animation); animation.play(); // position the mesh child.scale.set(0.15, 0.15, 0.15); child.rotation.x = -0.5 * Math.PI; child.position.x = -100; child.position.y = -60; });
当然,在render函数中我们还是的调用THREE.AnimationHandler.update(delta),根据时间戳更新动画。
13.加载MD2动画
MD2格式是设计用来构建雷神之锤的角色模型。尽管新一代引擎使用了不同的格式,但是你依然可以找到很多MD2格式的模型。在使用该模型时需要将其转换为Three.js格式的javascript文件。所以我们直接使用JSONLoader加载。动画可以调用mesh.playAnimation(animationName, fps)执行动画,由于模型文件提供了很多动画,所以我们需要传递一个name,让mesh知道执行哪个动画。在执行动画之前,还得重新计算下集合体的法向量。下面是加载并执行md2动画的示例代码:
var loader = new THREE.JSONLoader(); loader.load("../assets/models/ogre/ogro.js", function(geometry, mat){ geometry.computeMorphNormals(); var mat = new THREE.MeshLambertMaterial({ map: THREE.ImageUtils.loadTexture("../assets/models/ogre/skins/skin.jpg"), morphTargets: true, morphNormals: true }); mesh = new THREE.MorphAnimMesh(geometry, mat); mesh.rotation.y = 0.7; 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); }); gui.add(controls, "fps", 1, 20).onChange(function(e){ mesh.playAnimation(controls.animations, controls.fps); }); mesh.playAnimation("crattack", 10); scene.add(mesh); });
特别需要注意的是,加载进来的动画列表是空的,我们需要调用mesh.parseAnimations()函数把动画都转换出来。接下来我们可以遍历mesh.geometry.animations获取所有动画名称。想要动画执行起来,还得在render中调用 mesh.updateAnimation(delta * 1000)函数。clock.getDelta()获取的时间戳是单位是秒,所以要乘以1000。