03.动画

本篇文章基于上篇文章02.移动/缩放/旋转对象,介绍如何在浏览器中使用Threejs渲染动画。

源码文件:03.animations

什么是动画

动画实际上是利用人眼的视觉暂留,即在很短的时间间隔内渲染多张图片,人眼主观感受就是动画,浏览器提供了 requestAnimationFrame 来允许程序定义在下次重绘之前的方法,我们可以借助它来更新动画。

requestAnimationFrame的主要优点是能够根据浏览器的刷新率自动调整回调的执行频率,从而实现流畅的动画效果,并在页面不可见时暂停动画,节省系统资源。

创建动画的逻辑大致是这样的:

const tick = () => {
  // update something...
  // render
  window.requestAnimationFrame(tick);
};
tick();

快速实现一个动画

我们借助之前 02.移动/缩放/旋转对象 加以改造就可以实现一个简单的旋转动画,下面是源码:

import * as THREE from "three";

const sizes = {
  width: 800,
  height: 600,
};

// 创建一个场景
const scene = new THREE.Scene();
// 创建几何形状
const geometry = new THREE.BoxGeometry(1, 1, 1);
// 创建材质
const material = new THREE.MeshBasicMaterial({ color: "red" });
// 组合mesh
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 创建相机
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height);
scene.add(camera);
camera.position.z = 3;
// 创建渲染器
const renderer = new THREE.WebGLRenderer({
  canvas: document.querySelector(".webgl") as HTMLCanvasElement,
});
renderer.setSize(sizes.width, sizes.height);
const tick = () => {
  // 更新物体
  mesh.rotation.z += 0.001;
  // 渲染
  renderer.render(scene, camera);
  window.requestAnimationFrame(tick);
};
tick();

可以看到,我们只是在每次重绘时修改了 mesh.rotation.z 属性的值,就能让立方体旋转起来!

渲染差异

前面我们知道 requestAnimationFrame 是在浏览器每一次重绘时调用,那么现在就存在一个问题:不同电脑的显卡性能不一致导致的渲染帧率不一致导致的渲染差异。

例如:120fps 下的动画和 60fps 下的动画,在一分钟内一个重绘了 120 次,而另外一个只重绘了 60 次,那么这两个动画必然表现是不同的。

如何解决这个问题呢?可以借助下面的两种方法。

时间差

核心思路:每次更新前计算和上次更新时的时间差,时间差毫秒值 * 1ms时间里的步进值 = 要更新的量

// 借助时间抹平渲染差异
let lastRenderTime = Date.now();
const tick = () => {
  const current = Date.now();
  const offset = current - lastRenderTime;
  lastRenderTime = current;
  // 更新物体
  mesh.rotation.z += Math.PI * 0.001 * offset;
  // 渲染
  renderer.render(scene, camera);
  window.requestAnimationFrame(tick);
};
tick();

THREE.Clock

核心思路:每次更新前计算开始运行到当前的时间,要设置的属性值 = 运行时长 * 1s需要更新的步进值

const clock = new THREE.Clock();
const tick = () => {
  // 更新物体:clock.getElapsedTime()获取clock运行到现在的秒数
  mesh.rotation.z = Math.PI * clock.getElapsedTime();
  // 渲染
  renderer.render(scene, camera);
  window.requestAnimationFrame(tick);
};
tick();

圆周动画

在平面直角坐标系中,若以原点为圆心,r 为半径的圆上一点的坐标为 (x, y),该点与 x 轴正半轴的夹角为 θ(弧度制),则它们之间的关系可以用以下三角函数表示:

  • x = r * cos(θ)
  • y = r * sin(θ)

当 θ 从 0 增加到 2π 时,(x, y) 将遍历整个圆周,我们可以借助三角函数和 Clock 来快速实现一个圆周动画:

const clock = new THREE.Clock();
const tick = () => {
  // 更新物体
  mesh.position.x = Math.cos(clock.getElapsedTime());
  mesh.position.y = Math.sin(clock.getElapsedTime());
  // 渲染
  renderer.render(scene, camera);
  window.requestAnimationFrame(tick);
};
tick();

换个方式,我们将动画运用到相机上,得到的动画是一样的效果,不过这次圆周运动的是相机:

const clock = new THREE.Clock();
const tick = () => {
  // 更新物体
  camera.position.x = Math.cos(clock.getElapsedTime());
  camera.position.y = Math.sin(clock.getElapsedTime());
  // 渲染
  renderer.render(scene, camera);
  window.requestAnimationFrame(tick);
};
tick();

加上一行代码,让相机lookAt立方体中心,又得到一个不一样的动画:

const clock = new THREE.Clock();
const tick = () => {
  // 更新物体
  camera.position.x = Math.cos(clock.getElapsedTime());
  camera.position.y = Math.sin(clock.getElapsedTime());
  camera.lookAt(mesh.position)
  // 渲染
  renderer.render(scene, camera);
  window.requestAnimationFrame(tick);
};
tick();

配合GSAP

GSAP(GreenSock Animation Platform)是一个功能强大且性能高效的 JavaScript 动画库。 为什么选择 GSAP 配合 Three.js 来制作动画:

  • 强大的动画控制:GSAP 提供了非常精细和灵活的动画控制能力,可以轻松实现复杂的动画效果,包括缓动函数、动画序列、暂停、恢复、反转等,这对于创建引人入胜的 Three.js 场景动画非常有用。

  • 良好的性能:GSAP 经过优化,能够在各种设备和浏览器上提供流畅的动画性能,减少卡顿和跳帧的情况。

  • 易于理解的 API:GSAP 的 API 相对简单直观,易于学习和使用,开发者可以快速上手并创建出所需的动画效果,降低了开发难度和时间成本。

  • 缓动函数支持:GSAP 拥有丰富的缓动函数库,能够为 Three.js 中的物体运动添加自然而平滑的过渡效果,使动画看起来更加真实和吸引人。

  • 跨浏览器兼容性:GSAP 具有出色的跨浏览器兼容性,确保在不同的浏览器环境中动画都能正常工作,为用户提供一致的体验。

  • 与 Three.js 集成方便:尽管 Three.js 本身有动画功能,但结合 GSAP 可以补充和增强其动画能力,提供更多的选择和灵活性。

安装依赖

pnpm add gsap@3.5.1

动画实现

gsap.to(mesh.rotation, {
  duration: 2,
  x: 2,
  y: 2,
  yoyo: true,
  repeat: -1,
});
const tick = () => {
  renderer.render(scene, camera);
  requestAnimationFrame(() => {
    tick();
  });
};
tick();

以上就是如何简单的借助 Three.js和相关方法和第三方依赖来实现动画的基本示例。