Skip to content

动画循环

此笔记记录于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 };

注意:像这样在运行时向现有类添加属性称为 猴子补丁(这里,我们添加.tickMesh实例)。这是常见的做法,在我们简单的应用程序中不会引起任何问题。但是,我们不应该养成这样粗心大意的习惯,因为在某些情况下它会导致性能问题。我们只允许自己在这里这样做,因为替代方案更复杂

这里为什么* delta呢?解释如下:

帧速率并不是完全稳定的

  • 我们可能无法成功的快速生成帧。如果运行您的应用程序的设备功能不足以达到目标帧速率,则动画循环将运行得更慢。
  • 即使在快速硬件上,您的应用程序也必须与其他应用程序共享计算资源,而且可能并不总是足够的。
  • 即使有一个强大的GPU和一个像这个单一立方体这样简单的场景,我们也不会达到每秒60帧的精度。有些帧渲染得有点快,有些帧渲染得有点慢。这个是正常的。部分原因是, 出于安全原因,浏览器会在.getDelta的结果中增加大约1毫秒的抖动。

这里的delta是通过Loop.js中国的.getDelta告诉我们自上次调用.getDelta以来已经过去了多少时间。

这样,如果帧慢了,delta就大,动画变化幅度就大。即时间花费越多,运动距离就越长,速率就相对稳定了

其他

有时候我们需要按需渲染,就需要手动start并且及时stop