Three.js基础探寻十——动画
本篇将介绍如果使用Three.js进行动态画面的渲染。此外,将会介绍一个Three.js作者写的另外一个库stat.js,用来观测每秒帧数(FPS)。
1.实现动画效果
1.1 动画原理
对于Three.js程序而言,动画的实现是通过在每秒中多次重绘画面实现的。
为了衡量画面切换速度,引入了每秒帧数FPS(Frames Per Second)的概念,是指每秒画面重绘的次数。FPS越大,则动画效果越平滑,当FPS小于20时,一般就能明显感受到画面的卡滞现象。
那么FPS是不是越大越好呢?其实也未必。当FPS足够大(比如达到60),再增加帧数人眼也不会感受到明显的变化,反而相应地就要消耗更多资源(比如电影的胶片就需要更长了,或是电脑刷新画面需要消耗计算资源等等)。因此,选择一个适中的FPS即可。
对于Three.js动画而言,一般FPS在30到60之间都是可取的。
1.2 setInterval方法
如果要设置特定的FPS(虽然严格来说,即使使用这种方法,JavaScript也不能保证帧数精确性),可以使用JavaScript DOM定义的方法:
setInterval(func, msec)
其中,func是每过msec毫秒执行的函数,如果将func定义为重绘画面的函数,就能实现动画效果。setInterval函数返回一个id,如果需要停止重绘,需要使用clearInterval方法,并传入该id,具体的做法为:
首先,在init函数中定义每20毫秒执行draw函数的setInterval,返回值记录在全局变量id中:
id = setInterval(draw, 20);
在draw函数中,我们首先设定在每帧中的变化,这里我们让场景中的长方体绕y轴转动。然后,执行渲染:
function draw() { mesh.rotation.y = (mesh.rotation.y + 0.01) % (Math.PI * 2); renderer.render(scene, camera); }
这样,每20毫秒就会调用一次draw函数,改变长方体的旋转值,然后进行重绘。最终得到的效果就是FPS为50的旋转长方体。
我们在HTML中添加一个按钮,按下后停止动画:
<button id="stopBtn" onclick="stop()">Stop</button>
对应的stop函数为:
function stop() { if (id !== null) { clearInterval(id); id = null; } }
源码:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>3.js测试10.1</title> </head> <body onload="init()"> <canvas id="mainCanvas" width="400px" height="300px" ></canvas> <button id="stopBtn" onclick="stop()">Stop</button> </body> <script type="text/javascript" src="js/three.min.js"></script> <script type="text/javascript"> var scene = null; var camera = null; var renderer = null; var mesh = null; var id = null; function init() { renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('mainCanvas') }); renderer.setClearColor(0x000000); scene = new THREE.Scene(); camera = new THREE.OrthographicCamera(-5, 5, 3.75, -3.75, 0.1, 100); camera.position.set(5, 5, 20); camera.lookAt(new THREE.Vector3(0, 0, 0)); scene.add(camera); mesh = new THREE.Mesh(new THREE.CubeGeometry(1, 2, 3), new THREE.MeshLambertMaterial({ color: 0xffff00 })); scene.add(mesh); var light = new THREE.DirectionalLight(0xffffff); light.position.set(20, 10, 5); scene.add(light); id = setInterval(draw, 20); } function draw() { mesh.rotation.y = (mesh.rotation.y + 0.01) % (Math.PI * 2); renderer.render(scene, camera); } function stop() { if (id !== null) { clearInterval(id); id = null; } } </script> </html>
你会看到一个傻缺在一直转。
1.3 requestAnimationFrame方法
如果不在意多久重绘一次,可以使用requestAnimationFrame方法。它告诉浏览器在合适的时候调用指定函数,通常可能达到60FPS。
requestAnimationFrame同样有对应的cancelAnimationFrame取消动画:
function stop() { if (id !== null) { cancelAnimationFrame(id); id = null; } }
和setInterval不同的是,由于requestAnimationFrame只请求一帧画面,因此,除了在init函数中需要调用,在被其调用的函数中需要再次调用requestAnimationFrame:
function draw() { mesh.rotation.y = (mesh.rotation.y + 0.01) % (Math.PI * 2); renderer.render(scene, camera); id = requestAnimationFrame(draw); }
因为requestAnimationFrame较为“年轻”,因而一些老的浏览器使用的是试验期的名字:mozRequestAnimationFrame、webkitRequestAnimationFrame、msRequestAnimationFrame,为了支持这些浏览器,我们最好在调用之前,先判断是否定义了requestAnimationFrame以及上述函数:
var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; window.requestAnimationFrame = requestAnimationFrame;
源码:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>3.js测试10.2</title> </head> <body onload="init()"> <canvas id="mainCanvas" width="400px" height="300px" ></canvas> <button id="stopBtn" onclick="stop()">Stop</button> </body> <script type="text/javascript" src="js/three.min.js"></script> <script type="text/javascript"> var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; window.requestAnimationFrame = requestAnimationFrame; var scene = null; var camera = null; var renderer = null; var mesh = null; var id = null; function init() { renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('mainCanvas') }); renderer.setClearColor(0x000000); scene = new THREE.Scene(); camera = new THREE.OrthographicCamera(-5, 5, 3.75, -3.75, 0.1, 100); camera.position.set(5, 5, 20); camera.lookAt(new THREE.Vector3(0, 0, 0)); scene.add(camera); mesh = new THREE.Mesh(new THREE.CubeGeometry(1, 2, 3), new THREE.MeshLambertMaterial({ color: 0xffff00 })); scene.add(mesh); var light = new THREE.DirectionalLight(0xffffff); light.position.set(20, 10, 5); scene.add(light); id = requestAnimationFrame(draw); } function draw() { mesh.rotation.y = (mesh.rotation.y + 0.01) % (Math.PI * 2); renderer.render(scene, camera); id = requestAnimationFrame(draw); } function stop() { if (id !== null) { cancelAnimationFrame(id); id = null; } } </script> </html>
和上面的差不多,点stop会停止。
1.4 两种方法的比较
setInterval方法与requestAnimationFrame方法的区别较为微妙。一方面,最明显的差别表现在setInterval可以手动设定FPS,而requestAnimationFrame则会自动设定FPS;但另一方面,即使是setInterval也不能保证按照给定的FPS执行,在浏览器处理繁忙时,很可能低于设定值。当浏览器达不到设定的调用周期时,requestAnimationFrame采用跳过某些帧的方式来表现动画,虽然会有卡滞的效果但是整体速度不会拖慢,而setInterval会因此使整个程序放慢运行,但是每一帧都会绘制出来;总而言之,requestAnimationFrame适用于对于时间较为敏感的环境(但是动画逻辑更加复杂),而setInterval则可在保证程序的运算不至于导致延迟的情况下提供更加简洁的逻辑(无需自行处理时间)。
2.使用stat.js记录FPS
stat.js是Three.js的作者Mr. Doob的另一个有用的JavaScript库。很多情况下,我们希望知道实时的FPS信息,从而更好地监测动画效果。这时候,stat.js就能提供一个很好的帮助,它占据屏幕中的一小块位置(如左上角),效果为:
,单击后显示每帧渲染时间:。
首先,我们需要下载stat.js文件,可以在https://github.com/mrdoob/stats.js/blob/master/build/stats.min.js找到。下载后,将其放在项目文件夹下,然后在HTML中引用:
<script type="text/javascript" src="js/stat.js"></script>
在页面初始化的时候,对其初始化并将其添加至屏幕一角。这里,我们以左上角为例:
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); // Three.js init ... }
然后,在上一节介绍的动画重绘函数draw中调用stat.begin();与stat.end();分别表示一帧的开始与结束:
function draw() { stat.begin(); mesh.rotation.y = (mesh.rotation.y + 0.01) % (Math.PI * 2); renderer.render(scene, camera); stat.end(); }
最终就能得到FPS效果了。
源码:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>3.js测试10.4</title> </head> <body onload="init()"> <canvas id="mainCanvas" width="400px" height="300px" ></canvas> <button id="stopBtn" onclick="stop()">Stop</button> </body> <script type="text/javascript" src="js/three.js"></script> <script type="text/javascript" src="js/stats.min.js"></script> <script type="text/javascript"> var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; window.requestAnimationFrame = requestAnimationFrame; var scene = null; var camera = null; var renderer = null; var mesh = 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') }); renderer.setClearColor(0x000000); scene = new THREE.Scene(); camera = new THREE.OrthographicCamera(-5, 5, 3.75, -3.75, 0.1, 100); camera.position.set(5, 5, 20); camera.lookAt(new THREE.Vector3(0, 0, 0)); scene.add(camera); mesh = new THREE.Mesh(new THREE.CubeGeometry(1, 2, 3), new THREE.MeshLambertMaterial({ color: 0xffff00 })); scene.add(mesh); var light = new THREE.DirectionalLight(0xffffff); light.position.set(20, 10, 5); scene.add(light); id = requestAnimationFrame(draw); } function draw() { stat.begin(); mesh.rotation.y = (mesh.rotation.y + 0.01) % (Math.PI * 2); renderer.render(scene, camera); id = requestAnimationFrame(draw); stat.end(); } function stop() { if (id !== null) { cancelAnimationFrame(id); id = null; } } </script> </html>
整理自张雯莉《Three.js入门指南》