創建動畫和移動相機


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。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM