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。
