在这里,我们将动态画面简称为动画(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
要好好复习一下物理和数学了