在這里,我們將動態畫面簡稱為動畫(animation)。正如動畫片的原理一樣,動畫的本質是利用了人眼的視覺暫留特性,快速地變換畫面,從而產生物體在運動的假象。而對於Three.js程序而言,動畫的實現也是通過在每秒中多次重繪畫面實現的。
為了衡量畫面切換速度,引入了每秒幀數FPS(Frames Per Second)的概念,是指每秒畫面重繪的次數。FPS越大,則動畫效果越平滑,當FPS小於20時,一般就能明顯感受到畫面的卡滯現象。
那么FPS是不是越大越好呢?其實也未必。當FPS足夠大(比如達到60),再增加幀數人眼也不會感受到明顯的變化,反而相應地就要消耗更多資源(比如電影的膠片就需要更長了,或是電腦刷新畫面需要消耗計算資源等等)。因此,選擇一個適中的FPS即可。
NTSC標准的電視FPS是30,PAL標准的電視FPS是25,電影的FPS標准為24。而對於Three.js動畫而言,一般FPS在30到60之間都是可取的。
setInterval方法
如果要設置特定的FPS(雖然嚴格來說,即使使用這種方法,JavaScript也不能保證幀數精確性),可以使用JavaScript DOM定義的方法:
setInterval(func, msec)
其中,func
是每過msec
毫秒執行的函數,如果將func
定義為重繪畫面的函數,就能實現動畫效果。setInterval
函數返回一個id
,如果需要停止重繪,需要使用clearInterval
方法,並傳入該id
,具體的做法為:
requestAnimationFrame方法
大多數時候,我們並不在意多久重繪一次,這時候就適合用requestAnimationFrame方法了。它告訴瀏覽器在合適的時候調用指定函數,通常可能達到60FPS。
如何取舍
setInterval
方法與requestAnimationFrame
方法的區別較為微妙。一方面,最明顯的差別表現在setInterval
可以手動設定FPS,而requestAnimationFrame
則會自動設定FPS;但另一方面,即使是setInterval
也不能保證按照給定的FPS執行,在瀏覽器處理繁忙時,很可能低於設定值。當瀏覽器達不到設定的調用周期時,requestAnimationFrame
采用跳過某些幀的方式來表現動畫,雖然會有卡滯的效果但是整體速度不會拖慢,而setInterval
會因此使整個程序放慢運行,但是每一幀都會繪制出來;
總而言之,requestAnimationFrame
適用於對於時間較為敏感的環境(但是動畫邏輯更加復雜),而setInterval
則可在保證程序的運算不至於導致延遲的情況下提供更加簡潔的邏輯(無需自行處理時間)。
開始工作
完成init函數
var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; window.requestAnimationFrame = requestAnimationFrame; var scene = null; var camera = null; var renderer = null; var id = null; var stat = null; function init() { stat = new Stats(); stat.domElement.style.position = 'absolute'; stat.domElement.style.right = '0px'; stat.domElement.style.top = '0px'; document.body.appendChild(stat.domElement); renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('mainCanvas') }); scene = new THREE.Scene(); id = requestAnimationFrame(draw); } function draw() { stat.begin(); renderer.render(scene, camera); id = requestAnimationFrame(draw); stat.end(); } function stop() { if (id !== null) { cancelAnimationFrame(id); id = null; } }
然后,為了實現彈球彈動的效果,我們創建一個球體作為彈球模型,創建一個平面作為彈球反彈的平面。為了在draw
函數中改變彈球的位置,我們可以聲明一個全局變量ballMesh
,以及彈球半徑ballRadius
。
var ballMesh = null; var ballRadius = 0.5;
在init
函數中添加球體和平面,使彈球位於平面上,平面采用棋盤格圖像作材質:
// ball ballMesh = new THREE.Mesh(new THREE.SphereGeometry(ballRadius, 16, 8), new THREE.MeshLambertMaterial({ color: 0xffff00 })); ballMesh.position.y = ballRadius; scene.add(ballMesh); // plane var texture = THREE.ImageUtils.loadTexture('../img/chess.png', {}, function() { renderer.render(scene, camera); }); texture.wrapS = texture.wrapT = THREE.RepeatWrapping; texture.repeat.set(4, 4); var plane = new THREE.Mesh(new THREE.PlaneGeometry(5, 5), new THREE.MeshLambertMaterial({map: texture})); plane.rotation.x = -Math.PI / 2; scene.add(plane);
現在,每幀繪制的都是相同的效果:
為了記錄彈球的狀態,我們至少需要位置、速度、加速度三個矢量,為了簡單起見,這里彈球只做豎直方向上的自由落體運動,因此位置、速度、加速度只要各用一個變量表示。其中,位置就是ballMesh.position.y
,不需要額外的變量,因此我們在全局聲明速度v
和加速度a
:
var v = 0; var a = -0.1;
這里,a = -0.1
代表每幀小球向y方向負方向移動0.1
個單位。
一開始,彈球從高度為maxHeight
處自由下落,掉落到平面上時會反彈,並且速度有損耗。當速度很小的時候,彈球會在平面上作振幅微小的抖動,所以,當速度足夠小時,我們需要讓彈球停止跳動。因此,定義一個全局變量表示是否在運動,初始值為false
:
var isMoving = false;
在HTML中定義一個按鈕,點擊按鈕時,彈球從最高處下落:
function drop() { isMoving = true; ballMesh.position.y = maxHeight; v = 0; }
下面就是最關鍵的函數了,在draw
函數中,需要判斷當前的isMoving
值,並且更新小球的速度和位置:
function draw() { stat.begin(); if (isMoving) { ballMesh.position.y += v; v += a; if (ballMesh.position.y <= ballRadius) { // hit plane v = -v * 0.9; } if (Math.abs(v) < 0.001) { // stop moving isMoving = false; ballMesh.position.y = ballRadius; } } renderer.render(scene, camera); id = requestAnimationFrame(draw); stat.end(); }
這樣就實現小球的彈動效果了。最終的代碼為:
var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; window.requestAnimationFrame = requestAnimationFrame; var scene = null; var camera = null; var renderer = null; var id = null; var stat = null; var ballMesh = null; var ballRadius = 0.5; var isMoving = false; var maxHeight = 5; var v = 0; var a = -0.01; function init() { stat = new Stats(); stat.domElement.style.position = 'absolute'; stat.domElement.style.right = '0px'; stat.domElement.style.top = '0px'; document.body.appendChild(stat.domElement); renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('mainCanvas') }); scene = new THREE.Scene(); camera = new THREE.OrthographicCamera(-5, 5, 3.75, -3.75, 0.1, 100); camera.position.set(5, 10, 20); camera.lookAt(new THREE.Vector3(0, 3, 0)); scene.add(camera); // ball ballMesh = new THREE.Mesh(new THREE.SphereGeometry(ballRadius, 16, 8), new THREE.MeshLambertMaterial({ color: 0xffff00 })); ballMesh.position.y = ballRadius; scene.add(ballMesh); // plane var texture = THREE.ImageUtils.loadTexture('../img/chess.png', {}, function() { renderer.render(scene, camera); }); texture.wrapS = texture.wrapT = THREE.RepeatWrapping; texture.repeat.set(4, 4); var plane = new THREE.Mesh(new THREE.PlaneGeometry(5, 5), new THREE.MeshLambertMaterial({map: texture})); plane.rotation.x = -Math.PI / 2; scene.add(plane); var light = new THREE.DirectionalLight(0xffffff); light.position.set(10, 10, 15); scene.add(light); id = requestAnimationFrame(draw); } function draw() { stat.begin(); if (isMoving) { ballMesh.position.y += v; v += a; if (ballMesh.position.y <= ballRadius) { // hit plane v = -v * 0.9; } if (Math.abs(v) < 0.001) { // stop moving isMoving = false; ballMesh.position.y = ballRadius; } } renderer.render(scene, camera); id = requestAnimationFrame(draw); stat.end(); } function stop() { if (id !== null) { cancelAnimationFrame(id); id = null; } } function drop() { isMoving = true; ballMesh.position.y = maxHeight; v = 0; }
鏈接:http://runjs.cn/code/qqpikkwt
鏈接:http://runjs.cn/detail/ecll36ex
要好好復習一下物理和數學了