首页 > 代码库 > 创建动画和移动相机

创建动画和移动相机

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 = http://www.mamicode.com/{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。

创建动画和移动相机