Three.js simple animation

Cloud Map 3D Connects Your Created World We are committed to building the first "cloud CAD" collaborative design platform in China that integrates viewing, modeling, assembly and rendering.

At the request of readers, I hope we can set up a professional webgl and Threejs industry QQ exchange group for front-end developers in the Chengdu-Chongqing area, so that everyone can discuss issues. There are big guys who study webgl and Threejs in the group, everyone is welcome to join! ——Click the link to join the group chat [three.js/webgl Chongqing Alliance Group]: jq.qq.com/?_wv=1027&k…

about the author

Xiaogang, the front-end R&D engineer of Yuntu, is responsible for the development of the 3D front-end of Yuntu.

foreword

In the previous article, we introduced basic concepts such as geometry, material, light source, etc. We can use this knowledge to create some simple scenes. This time, I will share with you how to use animation to make our scene move.

text

An introduction to animation

In threejs we renderer.renderdraw the scene using a method that takes the scene and the camera as input and a single still image to an HTML <canvas element>. The output is a purple box you can see that doesn't move.

render() {
  // draw a single frame
  renderer.render(scene, camera);
}
复制代码

image.png

This time, we will add some simple rotation animation to the cube, think about the process of adding animation

  • transferrender.render(...)
  • wait until the time to draw the next frame
  • Rotate the cube a bit
  • transferrender.render(...)
  • wait until the time to draw the next frame
  • Rotate the cube a bit
  • ...

In an infinite loop that becomes an animation loop , setting up this loop is simple because threejs renderer.setAnimationLoopdoes all the work for us via methods.

We'll also introduce Clocka simple stopwatch class that we can use to keep our animations in sync, using milliseconds( ms) as the unit.

一旦我们设置了循环,我们的目标就是以每秒60帧的速率生成稳定的帧流,这意味着我们需要.render大约每 16 毫秒调用一次。换句话说,我们需要确保在一帧中所做的所有处理都花费少于 16 毫秒。所以在需要更新动画,执行任何其他需要跨帧计算的任务,并在我们打算支持的最低规格硬件上在不到 16 毫秒的时间内渲染帧。在后续的部分,当我们设置循环并为立方体创建一个简单的旋转动画时,将讨论如何最好地实现这一点。

二、使用THREEJS创建动画循环

1.Loop.js 模块

新建Loop类,这个类将处理所有的循环逻辑和动画系统,首先导入Clock,使用它来保持动画同步,然后使用renderer.render(scene,camera)生成帧。最后创建启动/停止循环的方法 startstop

import { Clock } from 'three';
class Loop {
  constructor(camera, scene, renderer) {
    this.camera = camera;
    this.scene = scene;
    this.renderer = renderer;
  }
  start() {}
  stop() {}
}
export { Loop }
复制代码

在World中,将Loop导入

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';
复制代码

将循环作为World 的属性,在整个场景中都能访问到

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);
    ...
  }
复制代码

最后 添加.start.stop到World

render() {
  // draw a single frame
  renderer.render(scene, camera);
}
start() {
  loop.start();
}
stop() {
  loop.stop();
}
复制代码

在main.js中调用world.renderworld.start

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();
}
复制代码

到这一步的时候,整个场景会变黑,但是不要担心,一旦我们完成创建循环,它会马上恢复活力。

2.创建循环.setAnimationLoop

使用threejs中的WebGLRenderer.setAnimationLoop

import { WebGLRenderer } from 'three';
const renderer = new WebGLRenderer();
// start the loop
renderer.setAnimationLoop(() => {
  renderer.render(scene, camera);
});
复制代码

这时renderer.render一遍一遍的调用生成帧流,可以通过null作为回调来取消正在运行的循环

// stop the loop
renderer.setAnimationLoop(null);
复制代码

在内部,循环是使用.requestAnimationFrame这个内置的浏览器方法,可以智能地安排帧与显示器的刷新率同步,如果您的硬件跟不上,它会平滑地降低帧率。由于.setAnimationLoop是最近添加的,较旧的 three.js 示例和教程通常.requestAnimationFrame直接用于设置循环,这样做非常简单。

3.Loop.start 和 Loop.stop 方法

现在开始创建循环,使用setAnimationLoop

start() {
  this.renderer.setAnimationLoop(() => {
    // render a frame
    this.renderer.render(this.scene, this.camera);
  });
}
复制代码

创建对应的stop方法

stop() {
  this.renderer.setAnimationLoop(null);
}
复制代码

此时,场景将开始以大约60fps输出帧,但是看不到任何区别,为啥呢,回顾一下刚才我们所做的操作

  • 调用render.render(...)
  • 等到绘制下一帧的时间
  • 调用render.render(...)
  • 等到绘制下一帧的时间

是不是发现和本文开头我们描述的循环比较少了点什么,没错,将立方体旋转一点,下面先做一些准备工作

4.移除onResize钩子

首先,让我们整理一下。现在循环正在运行,每当我们调整窗口大小时,都会在循环的下一次迭代中生成一个新帧。看起来不会有任何延迟,所以现在不再需要在调整大小时手动重绘场景了。从世界中移除resizer.onResize钩子

constructor(container) {
 camera = createCamera();
 scene = createScene();
 renderer = createRenderer();
 container.append(renderer.domElement);
 const cube = createCube();
 const light = createLights();
 updatables.push(cube);
 scene.add(cube, light);
 const resizer = new Resizer(container, camera, renderer);
 resizer.onResize = () => {
   this.render();
 };
}
复制代码

三、动画系统

考虑一个简单的游戏,用户可以在其中探索地图并挑选苹果。以下是您可以添加到此游戏中的一些动画对象:

  • 女主角,拥有各种动画,如步行/跑步/跳跃/攀爬/挑选。
  • 苹果树。苹果随着时间长大,树叶随风飘扬。
  • 一些可怕的蜜蜂会试图把你从花园里赶出去。
  • 一个有趣的环境,其中包含水、风、树叶和岩石等物体。
  • 以悬停在地面上的旋转立方体的形式加电。

… 等等。每次循环运行时,我们都希望通过将它们向前移动一帧来更新所有这些动画。就在我们渲染每一帧之前,我们会让女主角向前迈出一点点,我们会让每只蜜蜂向她移动,我们会让叶子移动,苹果长大,道具旋转,每一个都有一点点, 几乎是肉眼无法看到的微小量,但随着时间的推移会产生流畅的动画效果。

1.Loop.tick 方法

为了处理上面所说的情况,我们需要一个更新所有动画的函数,并且这个函数应该在每一帧开始时运行一次。然而,update这个词已经在整个 three.js 中被大量使用,所以我们将选择这个词tick。在绘制每一帧之前,我们会让每个动画向前移动一帧。Loop.tick在类的末尾添加方法Loop,然后在动画循环中调用它:

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() {
  // Code to update animations will go here
}
复制代码

在实现tick 的时候,需要思考一个问题,我们想要在我们应用的不同地方调用还是将所有tick集中到一起

2.集中式还是分散式

集中式-- 如果我们的场景中只有几个动画对象,这可能没问题。当有五十或一百个动画对象的时候,就会显的非常杂乱。它还打破了各种软件设计原则,因为现在Loop必须深入了解每个动画对象的工作原理。

tick() {
  if(controls.state.run) {
    character.runAnimation.nextFrame();
  }

  beeA.moveTowards(character.position);
  beeB.moveTowards(character.position);
  beeC.moveTowards(character.position);

  powerupA.rotation.z += 0.01;
  powerupB.rotation.z += 0.01;
  powerupC.rotation.z += 0.01;

  leafA.rotation.y += 0.01;

  // ... and so on
}
复制代码

分散式-- 在对象本身上定义更新每个对象的逻辑。每个对象都将使用自己的通用.tick方法公开该逻辑。现在,Loop.tick方法会很简单。每一帧,我们将遍历一个动画对象列表,并告诉它们每个.tick向前一帧。

// somewhere in the Loop class:
this.updatables = [character, beeA, beeB, beeC, powerupA, powerupB, powerupC, leafA, ... ]
...

tick() {
  for(const object of this.updatables) {
    object.tick();
  }
}
复制代码

显而易见,分散式更符合设计应用程序的模块化理念,将每个对象设计为独立的实体,然后在实体上封装它的行为

3.动画对象列表

我们需要循环类中的动画对象列表。为此,我们将使用一个简单的数组,我们称之为 list updatables

constructor(camera, scene, renderer) {
  this.camera = camera;
  this.scene = scene;
  this.renderer = renderer;
  this.updatables = [];
}
复制代码

接下来, within Loop.tick,遍历这个列表并调用.tick中的任何对象。

tick() {
  for (const object of this.updatables) {
    object.tick();
  }
}
复制代码

4.cube.tick方法

添加cubeupdatables列表之前,它需要一个.tick方法,所以继续创建一个。在此.tick方法中定义旋转立方体的逻辑。

每种类型的动画对象都有不同的.tick方法。比如女主角的tick方法会检查她是在走、跑、跳还是站着不动,然后从其中一个动画中播放一帧,而苹果树的tick方法会检查苹果的成熟度和树叶沙沙作响,邪恶蜜蜂的每一种蜱虫方法都会检查女主人公的位置,然后将蜜蜂移向她一点点。如果她离得足够近,蜜蜂会试图蜇她。

Here, we'll simply update the cube on the X, Y, and  Z axes a small amount per frame. This will make it appear to tumble randomly.

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);

  // this method will be called once per frame
  cube.tick = () => {
    // increase the cube's rotation each frame
    cube.rotation.z += 0.01;
    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;
  };

  return cube;
}
复制代码

5. Add cube toLoop.updatables

In World, add the cube to the Loop.updatableslist

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);
}
复制代码

Then you will see that the cube moves

Parallax animation.gif

write at the end

Using animation in threejs can make the scene more dynamic and enhance interactivity. This article briefly introduces the easiest way to use animation. If you are interested, go and try to create more fun and cool effects.

Guess you like

Origin juejin.im/post/7077744904088059940