Three.js简单动画

云图三维 连接你·创造的世界 致力于打造国内第一家集查看、建模、装配和渲染于一体的“云端CAD”协作设计平台。

应读者的要求,希望我们成立一个专业的、面向成渝地区的前端开发人员的webgl、Threejs行业QQ交流群,便于大家讨论问题。群里有研究webgl、Threejs大佬哦,欢迎大家加入!——点击链接加入群聊【three.js/webgl重庆联盟群】:jq.qq.com/?_wv=1027&k…

作者介绍

小刚,云图大前端研发工程师,负责云图三维 front 端的开发工作。

前言

在之前的文章中,我们介绍了几何体,材质,光源等基本概念,我们可以利用这些知识创建出一些简单的场景,本次将和大家分享一下,如何利用动画,让我们的场景动起来。

正文

一、动画简介

在threejs 中我们使用renderer.render方法来绘制场景,此方法将场景和摄像机作为输入,并将单个静止图像输入到HTML<canvas元素>。输出是您可以看到不动的紫色框。

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

image.png

本次,我们将为立方体添加一些简单的旋转动画,思考一下添加动画的过程

  • 调用render.render(...)
  • 等到绘制下一帧的时间
  • 将立方体旋转一点
  • 调用render.render(...)
  • 等到绘制下一帧的时间
  • 将立方体旋转一点
  • ...

在一个成为动画循环的无限循环中,设置这个循环很简单,因为threejs通过renderer.setAnimationLoop方法为我们完成了所有的工作。

我们还将介绍一下Clock,一个简单的秒表类,我们可以使用它来保持动画同步,使用毫秒(ms)作为单位。

一旦我们设置了循环,我们的目标就是以每秒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方法会检查苹果的成熟度和树叶沙沙作响,邪恶蜜蜂的每一种蜱虫方法都会检查女主人公的位置,然后将蜜蜂移向她一点点。如果她离得足够近,蜜蜂会试图蜇她。

在这里,我们将简单地更新立方体在X,是, Z每帧轴少量。这将使它看起来随机翻滚。

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.添加cube到Loop.updatables

在 World 中,将立方体添加到Loop.updatables列表中

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

这时就会看到,立方体动起来了

视差动画.gif

写在最后

在threejs 中使用动画可以使场景更有活力,增强交互性,本文简单介绍了一下最简单的动画使用方法,感兴趣的话,快去试试创建更多好玩,炫酷的效果吧。

猜你喜欢

转载自juejin.im/post/7077744904088059940