动画循环
此笔记记录于DISCOVER three.js,大多数为其中的摘要,少数为笔者自己的理解
在每一帧就 render 一下,如果对象的属性变化了,那么就形成了动画。
设置这个循环很简单,因为 three.js 通过renderer.setAnimationLoop
方法为我们完成了所有困难的工作。
新建 Loop.js
创建一个systems/Loops.js
:
js
import { Clock } from 'three';
const clock = new Clock();
class Loop {
constructor(camera, scene, renderer) {
this.camera = camera;
this.scene = scene;
this.renderer = renderer;
this.updatables = [];
}
start() {
this.renderer.setAnimationLoop(() => {
// tell every animated object to tick forward one frame
this.tick();
// render a frame
this.renderer.render(this.scene, this.camera);
});
}
stop() {
this.renderer.setAnimationLoop(null);
}
tick() {
// only call the getDelta function once per frame!
const delta = clock.getDelta();
// console.log(
// `The last frame rendered in ${delta * 1000} milliseconds`,
// );
for (const object of this.updatables) {
object.tick(delta);
}
}
}
export { Loop };
- 使用
.setAnimationLoop(callback)
创建循环,可以传递.setAnimationLoop(null)
来结束循环 - 循环内部实现是使用
.requestAnimationFrame
。 tick()
是更新所有动画的函数,并且这个函数应该在每一帧开始时运行一次。然而,update 这个词已经在整个 three.js 中被大量使用,所以我们将选择 tick 这个词。- 这里做了解耦逻辑,自动调用 updatables 里面对象的
tick()
方法
World.js 使用它
js
import { createCamera } from './components/camera.js';
import { createCube } from './components/cube.js';
import { createLights } from './components/lights.js';
import { createScene } from './components/scene.js';
import { createRenderer } from './systems/renderer.js';
import { Resizer } from './systems/Resizer.js';
import { Loop } from './systems/Loop.js';
let camera;
let renderer;
let scene;
let loop;
class World {
constructor(container) {
camera = createCamera();
renderer = createRenderer();
scene = createScene();
loop = new Loop(camera, scene, renderer);
container.append(renderer.domElement);
const cube = createCube();
const light = createLights();
loop.updatables.push(cube);
scene.add(cube, light);
const resizer = new Resizer(container, camera, renderer);
resizer.onResize = () => {
this.render();
};
}
render() {
// draw a single frame
renderer.render(scene, camera);
}
start() {
loop.start();
}
stop() {
loop.stop();
}
}
export { World };
- cube 作为动画对象,添加到
updatables
中,注意需要自实现cube.tick()
方法,Loop 里面会自动调用该方法 - 现在循环正在运行,每当我们调整窗口大小时,都会在循环的下一次迭代中生成一个新帧。这足够快,您不会注意到任何延迟,因此我们不再需要在调整大小时手动重绘场景。
main.js 中调用
js
import { World } from './World/World.js';
function main() {
// Get a reference to the container element
const container = document.querySelector('#scene-container');
// create a new world
const world = new World(container);
// draw the scene
world.render();
// start the animation loop
world.start();
}
main();
cube 中添加 tick
js
import {
BoxBufferGeometry,
MathUtils,
Mesh,
MeshStandardMaterial,
} from 'three';
function createCube() {
const geometry = new BoxBufferGeometry(2, 2, 2);
const material = new MeshStandardMaterial({ color: 'purple' });
const cube = new Mesh(geometry, material);
cube.rotation.set(-0.5, -0.1, 0.8);
const radiansPerSecond = MathUtils.degToRad(30);
// this method will be called once per frame
cube.tick = (delta) => {
// increase the cube's rotation each frame
cube.rotation.z += radiansPerSecond * delta;
cube.rotation.x += radiansPerSecond * delta;
cube.rotation.y += radiansPerSecond * delta;
};
return cube;
}
export { createCube };
注意:像这样在运行时向现有类添加属性称为 猴子补丁(这里,我们添加
.tick
到Mesh
实例)。这是常见的做法,在我们简单的应用程序中不会引起任何问题。但是,我们不应该养成这样粗心大意的习惯,因为在某些情况下它会导致性能问题。我们只允许自己在这里这样做,因为替代方案更复杂
这里为什么* delta
呢?解释如下:
帧速率并不是完全稳定的
- 我们可能无法成功的快速生成帧。如果运行您的应用程序的设备功能不足以达到目标帧速率,则动画循环将运行得更慢。
- 即使在快速硬件上,您的应用程序也必须与其他应用程序共享计算资源,而且可能并不总是足够的。
- 即使有一个强大的 GPU 和一个像这个单一立方体这样简单的场景,我们也不会达到每秒 60 帧的精度。有些帧渲染得有点快,有些帧渲染得有点慢。这个是正常的。部分原因是, 出于安全原因,浏览器会在
.getDelta
的结果中增加大约 1 毫秒的抖动。
这里的delta
是通过 Loop.js 中国的.getDelta
告诉我们自上次调用.getDelta
以来已经过去了多少时间。
这样,如果帧慢了,delta 就大,动画变化幅度就大。即时间花费越多,运动距离就越长,速率就相对稳定了
其他
有时候我们需要按需渲染,就需要手动start
并且及时stop