创建动画和移动相机

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。

posted @ 2017-04-30 20:44  heavi  阅读(1859)  评论(1编辑  收藏  举报