之前的幾篇文章都是靜態的,而這里主要介紹如何使物體動起來,並且學會使用性能監視器來監測性能。
而如果要讓物體動起來,實際上我們是有兩種方法的,第一種是讓物體真的動起來,另外一種是讓攝像機動起來這樣物體相對來說也就動起來了。另外,實際上在讓物體動起來的過程中,我們是不斷通過調用 renderer.render(scene, camera)這個函數實現的,那么怎么才能不斷調用這個函數呢?這就需要用到 requestAnimationFrame函數了,這個函數接受一個函數作為參數,並且會在每秒內調用60次,那么最終屏幕就會在一秒內渲染60次,這樣就可以形成動畫了。
一、物體運動
首先,我們先讓物體動起來,如下所示,就是一個讓物體運動起來的動畫:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>three.js</title> <style> * { margin: 0; padding: 0; } </style> <script src="./three.js"></script> </head> <body> <script> var scene = new THREE.Scene(); var axes = new THREE.AxesHelper(1000); scene.add(axes); var camera = new THREE.PerspectiveCamera(50, window.innerWidth/window.innerHeight, 1, 1000); camera.position.x = 300; camera.position.y = 300; camera.position.z = 300; camera.up.x = 0; camera.up.y = 0; camera.up.z = 1; // camera.up.z = 1, 所以渲染出來的結果是z軸朝上。 camera.lookAt(scene.position); var renderer = new THREE.WebGLRenderer(); renderer.setClearColor(0x111111); renderer.setSize(window.innerWidth, window.innerHeight); var cubeGeometry = new THREE.CubeGeometry(10, 10, 10); var meshCube = new THREE.MeshBasicMaterial({color: 0xff0000}); var cube = new THREE.Mesh(cubeGeometry, meshCube); cube.position.x = 0; cube.position.y = 0; cube.position.z = 0; scene.add(cube); document.body.append(renderer.domElement); var isDestination = false; function animation() { var interval = 5; if (!isDestination) { cube.position.x = cube.position.x + interval; } else { cube.position.x = cube.position.x - interval; } if (cube.position.x == 330) { isDestination = true; } if (cube.position.x == 0) { isDestination = false; } renderer.render(scene, camera); requestAnimationFrame(animation); } animation(); </script> </body> </html>
即首先創建場景,然后創建坐標軸並加入到場景中,接着創建相機,注意相機所接受的參數比較多,且相機需要指定position位置以及up位置,且使用lookAt函數,接下來創建一個渲染器,指定背景顏色和寬、高,然后創建一個物體,最后需要將渲染器加入到document.body中,接着是一個動畫,然后運行即可。但是,我們可以發現雖然上面代碼完成了,但是封裝的不好,我們可以嘗試着將其用函數封裝,如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>three.js</title> <style> * { margin: 0; padding: 0; } </style> <script src="./three.js"></script> </head> <body> <script> var scene; function initScene() { scene = new THREE.Scene(); } var axes; function initAxes() { axes = new THREE.AxesHelper(1000); scene.add(axes); } var camera; function initCamera() { camera = new THREE.PerspectiveCamera(50, window.innerWidth/window.innerHeight, 1, 1000); camera.position.x = 300; camera.position.y = 300; camera.position.z = 300; camera.up.x = 0; camera.up.y = 1; camera.up.z = 0; camera.lookAt(scene.position); } var renderer; function initRenderer() { renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setClearColor(0x111111); document.body.append(renderer.domElement); } var cube; function initObject() { var cubeGeometry = new THREE.CubeGeometry(10, 10, 10); var meshCube = new THREE.MeshBasicMaterial({color: 0xff0000}); cube = new THREE.Mesh(cubeGeometry, meshCube); cube.position.x = 0; cube.position.y = 0; cube.position.z = 0; scene.add(cube); } var isDestination = false; function animation() { var interval = 5; var destination = 200; var direction = "y"; if (!isDestination) { cube.position[direction] += interval; } else { cube.position[direction] -= interval; } if (cube.position[direction] == destination) { isDestination = true; } if (cube.position[direction] == 0) { isDestination = false; } renderer.render(scene, camera); requestAnimationFrame(animation); } function threeStart() { initScene(); initAxes(); initCamera(); initRenderer(); initObject(); animation(); } threeStart(); </script> </body> </html>
如上所示,通過函數封裝,程序的邏輯性更好了一些,並且僅僅暴露了比如scene、camera、axes、renderer等必要的變量,而其他的變量不會對全局造成污染,而最后的animation函數,我們定義了direction為"x",這樣就可以通過這里的修改控制cube運動的坐標軸了,這一點利用的就是JavaScript調用屬性的特點。最后我們統一將初始化調用函數寫在了threeStart中,這樣,就可以通過threeStart函數調用開啟這個項目了,最終得到的效果如下所示:
二、相機運動
如下所示:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>three.js</title> <style> * { margin: 0; padding: 0; } </style> <script src="./three.js"></script> </head> <body> <script> var scene; function initScene() { scene = new THREE.Scene(); } var axes; function initAxes() { axes = new THREE.AxesHelper(1000); scene.add(axes); } var camera; function initCamera() { camera = new THREE.PerspectiveCamera(50, window.innerWidth/window.innerHeight, 1, 1000); camera.position.x = 300; camera.position.y = 300; camera.position.z = 300; camera.up.x = 0; camera.up.y = 1; camera.up.z = 0; camera.lookAt(scene.position); } var renderer; function initRenderer() { renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setClearColor(0x111111); document.body.append(renderer.domElement); } var cube; function initObject() { var cubeGeometry = new THREE.CubeGeometry(10, 10, 10); var meshCube = new THREE.MeshBasicMaterial({color: 0xff0000}); cube = new THREE.Mesh(cubeGeometry, meshCube); cube.position.x = 0; cube.position.y = 0; cube.position.z = 0; scene.add(cube); } var isDestination = false; function animation() { var interval = 1; if (!isDestination) { camera.position.x -= interval; camera.position.y -= interval; camera.position.z -= interval; } else { camera.position.x += interval; camera.position.y += interval; camera.position.z += interval; } if (camera.position.x == 50) { isDestination = true; } if (camera.position.x == 300) { isDestination = false; } renderer.render(scene, camera); requestAnimationFrame(animation); } function threeStart() { initScene(); initAxes(); initCamera(); initRenderer(); initObject(); animation(); } threeStart(); </script> </body> </html>
這里的思路也非常簡單,就是給camera做了一個動畫,效果如下所示:
ok,到這里,我們就了解了使得場景運動起來的兩種方法,但是,我們應該如何監測他們的性能呢,下面來說一說。
三、性能評估
在3D世界里,我們經常用的是幀數來評價性能,幀數就是圖形處理器每秒鍾可以刷新的次數,用fps(Frames Per Second)來表示,毫無疑問,幀數越高,那么動畫就會越流暢,為了監視FPS,就需要學習性能監視器。通常,我們使用stats(github地址/stats.min.js)進行監視,而stats是非常有名的JavaScript性能監視庫,它提供了一些簡單的信息來幫助你檢測你的代碼性能:
- FPS 即上一秒渲染的幀數(Frames),顯然這個值越大,說明上一秒鍾內刷新的次數越多,那么性能越好。
- MS 即渲染一幀需要的毫秒數,顯然,MS越小越好。
- MB 是分配內存的字節數。
- CUSTOM是指用戶自定義的面板。
如下所示:
那么如何使用呢?如下所示:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>stats</title> <script src="./stats.js"></script> </head> <body> <script> var stats = new Stats(); stats.showPanel( 0 ); // 0: fps, 1: ms, 2: mb, 3+ custom document.body.appendChild( stats.dom ) function animate() { stats.begin(); // monitored code gose here stats.end(); requestAnimationFrame(animate); } animate(); </script> </body> </html>
即首先引入stats.js庫文件,然后實例化得到一個stats,接着通過stats.showPanel()顯示我們希望看到的面板,然后添加到dom中,最后,我們就可以將動畫相關代碼寫在stats.begin()和stats.end()之間,打開瀏覽器就可以看到效果了。
另外,stats.js庫的github中說明,可以直接將下面的js代碼粘貼到任何html網頁的script標簽中,然后就可以使用了,如下:
<script> (function () { var script = document.createElement('script'); script.onload = function () { var stats = new Stats(); document.body.appendChild(stats.dom); requestAnimationFrame(function loop() { stats.update(); requestAnimationFrame(loop) }); }; script.src = './stats.js'; document.head.appendChild(script); })() </script>
這段代碼非常好理解,就是創建了一個script標簽,然后在script標簽加載完成之后創建了stats實例,接着添加到dom中,最后檢測性能。注意,其中設定了script標簽的src屬性,根據需要自己設定即可。
這樣,我們只要將上述代碼加到之前我們寫的動畫頁面中即可看到評估性能了,如下:
如上所示,我們可以看到在左上角就進行了性能檢測,上一秒的FPS為60。而后面括號內的(60 - 60)說明FPS的變化范圍在60 - 60之間,因為我們使用的requestAnimationFrame,所以FPS幾乎始終為60。
四、游戲循環、幀循環、渲染循環
游戲循環、幀循環和渲染循環都是同一種循環。
我國早期的葫蘆娃動畫片,這種動畫片不是3D引擎做的,而是畫家做的剪紙,然后拍下的照片進行播放,然后再通過連續翻動的形式,就可以形成動畫了,比如在抖音上,我們可以看到有人在本子上每一頁都畫了畫,然后不停的翻動本子,然后這個動畫場景就出來了。
游戲循環就是如下所示的方式:
while (true) { updateStatus(); draw(); }
即先更新狀態,然后再draw就可以了。 更新狀態的步驟中主要做的就是比如控制游戲中人物的位置以及背景的顏色等等。
而draw()的主要步驟就是先清空場景,然后再繪制,顯然,我們是不可能在不清空這一幀就畫下一幀。
通用的代碼形式如下所示:
function animate() {
render();
requestAnimationFrame( animate );
}
其中render過程就做了更新狀態以及draw的工作,然后在使用requestAnimationFrame調用這個函數,達成這個死循環,但是不會卡死,因為計算機只是會在空閑的時候來執行這些函數。並且這里requestAnimationFrame是每秒更新60幀。
注意:一般電影的播放在24幀每秒就可以做到不卡,而游戲需要做到48 - 60幀每秒才會不卡。這是因為電影的膠片會有一定的曝光,導致殘影的存在,這樣就可以使得其在24幀每秒保持不卡。
五、動畫引擎 tween.js
上面介紹了通過移動相機或者移動物體來產生動畫的效果,但是如果動畫再復雜一些,我們用原生實現就回去比較麻煩,所以這里我么可以借助動畫引擎tween.js來實現動畫效果,它一般是和three.js結合比較緊密的。
tween.js的github中star在5k多一些,也是比較流行的,使用起來也比較方便,我們可以在tween.js的raw中下載,然后通過script標簽引入就可以使用了,如下所示:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>tween</title> <script src="./tween.js"></script> </head> <body> <script> var box = document.createElement('div'); box.style.setProperty('background-color', '#008800'); box.style.setProperty('width', '100px'); box.style.setProperty('height', '100px'); document.body.appendChild(box); function animate(time) { requestAnimationFrame(animate); TWEEN.update(time); } requestAnimationFrame(animate); var coords = { x: 0, y: 0 }; var tween = new TWEEN.Tween(coords) .to({x: 300, y: 200}, 1000) .easing(TWEEN.Easing.Quadratic.Out) .onUpdate(function () { box.style.setProperty('transform', 'translate(' + coords.x + 'px, ' + coords.y + 'px)'); }) .start(); </script> </body> </html>
如上所示,我們創建了一個div,然后創建了animate動畫,接着我們指定原點在(0, 0)處,最后我們構建了一個Tween對象,然后指定它在1000ms時移動到(300, 200)坐標處,且指定了移動的動畫方式,然后在update時不斷改變其位置。
ok,這一部分的內容就到這里了。