跳到主要内容

动画和摄像机移动

在前几章中,我们看到了一些简单的动画,但没有太复杂的。 在第1章《使用Three.js创建你的第一个3D场景》中,我们介绍了基本的渲染循环, 随后的章节中,我们使用它来旋转一些简单的对象并展示一些其他基本的动画概念。 在本章中,我们将更详细地了解Three.js如何支持动画。 我们将讨论以下四个主题:

  • 基本动画
  • 与摄像机一起工作
  • 变形和骨骼动画
  • 使用外部模型创建动画

我们将从基本动画的基本概念开始。

基本动画​

在查看示例之前,让我们快速回顾一下在第1章中关于渲染循环的内容。 为了支持动画,我们需要告诉Three.js定期渲染场景。 为此,我们使用标准的HTML5 requestAnimationFrame功能, 如下所示:

function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}
animate();

有了这段代码,我们只需要在初始化场景后调用一次render()函数。 在render()函数本身中,我们使用requestAnimationFrame来安排下一次渲染。 这样,浏览器会确保以正确的间隔(通常约60次或120次每秒)调用render()函数。 在浏览器添加requestAnimationFrame之前, 使用setInterval(function, interval)或setTimeout(function, interval)。 它们将在每个设定的间隔内调用指定的函数。 这种方法的问题在于它不考虑其他正在进行的事务。 即使您的动画未显示或在隐藏选项卡中,它仍然被调用并仍在使用资源。 另一个问题是,这些函数在调用它们时更新屏幕,而不是在浏览器认为最佳时机更新时,这导致CPU使用率较高。 使用requestAnimationFrame,我们不告诉浏览器何时更新屏幕; 我们要求浏览器在最合适时运行提供的函数。 通常,这会产生大约60或120 FPS的帧速率(取决于您的硬件)。 使用requestAnimationFrame,您的动画将更加流畅,对CPU和GPU更友好,而且您不必担心时间问题。 在下一节中,我们将开始创建一个简单的动画。

简单动画​

通过这种方法,我们可以通过更改对象的旋转、缩放、位置、材质、顶点、面等很容易地实现动画。 在下一个渲染循环中,Three.js将渲染更改的属性。 一个非常简单的示例,基于我们在第7章“点和精灵”中已经看到的示例, 在01-basic-animations.html中可以找到。 以下截图显示了这个示例:

这个渲染循环非常简单。 首先,我们在userData对象上初始化各种属性,这是存储在THREE.Mesh本身中的自定义数据的地方, 然后使用在userData对象上定义的数据在网格上更新这些属性。 在动画循环中,根据这些属性更改旋转、位置和缩放,Three.js处理其余的事情。 下面是我们这样做的方法:

const geometry = new THREE.TorusKnotGeometry(2, 0.5, 150, 50, 3, 4)
const material = new THREE.PointsMaterial({
size: 0.1,
vertexColors: false,
color: 0xffffff,
map: texture,
depthWrite: false,
opacity: 0.1,
transparent: true,
blending: THREE.AdditiveBlending
})
const points = new THREE.Points(geometry, material)
points.userData.rotationSpeed = 0
points.userData.scalingSpeed = 0
points.userData.bouncingSpeed = 0
points.userData.currentStep = 0
points.userData.scalingStep = 0

// 在渲染循环中
function render() {
const rotationSpeed = points.userData.rotationSpeed
const scalingSpeed = points.userData.scalingSpeed
const bouncingSpeed = points.userData.bouncingSpeed
const currentStep = points.userData.currentStep
const scalingStep = points.userData.scalingStep

points.rotation.x += rotationSpeed
points.rotation.y += rotationSpeed
points.rotation.z += rotationSpeed

points.userData.currentStep = currentStep + bouncingSpeed
points.position.x = Math.cos(points.userData.currentStep)
points.position.y = Math.abs(Math.sin(points.userData.currentStep)) * 2

points.userData.scalingStep = scalingStep + scalingSpeed
var scaleX = Math.abs(Math.sin(scalingStep * 3 + 0.5 * Math.PI))
var scaleY = Math.abs(Math.cos(scalingStep * 2))
var scaleZ = Math.abs(Math.sin(scalingStep * 4 + 0.5 * Math.PI))
points.scale.set(scaleX, scaleY, scaleZ)
}

这里没有什么特别的,但它很好地展示了我们将在本书中讨论的基本动画背后的概念。 我们只是改变缩放、旋转和位置属性,而Three.js负责其余的事情。

在下一节中,我们将迅速转个弯。 除了动画之外,在使用Three.js在更复杂的场景中工作时, 您将迅速遇到的一个重要方面是使用鼠标选择屏幕上的对象的能力。

选择和移动对象​

尽管与动画不直接相关,因为在本章中我们将讨论摄像机和动画, 了解如何选择和移动对象是对本章中解释的主题的一个不错的补充。 在这里,我们将向您展示如何执行以下操作:

  • 使用鼠标从场景中选择对象
  • 使用鼠标在场景中拖动对象

我们将首先查看选择对象所需的步骤。

选择对象​

首先,打开选择对象的 selecting-objects.html 示例,您将看到以下内容: 当您在场景中移动鼠标时,您会看到每当鼠标碰到一个对象时,该对象会突显显示。 您可以通过使用 THREE.Raycaster 轻松实现这一点。 射线投射器将查看您当前的摄像机,并从摄像机到鼠标位置发射一条射线。 基于此,它可以计算鼠标位置击中的对象。 为此,我们需要执行以下步骤:

  • 创建一个对象,用于跟踪鼠标指向的位置
  • 每当移动鼠标时,更新该对象
  • 在渲染循环中,使用此更新的信息查看我们指向的Three.js对象

这在以下代码片段中显示:

// 最初将位置设置为-1,-1
let pointer = {
x: -1,
y: -1
}
// 当鼠标移动时更新指针
document.addEventListener('mousemove', (event) => {
pointer.x = (event.clientX / window.innerWidth) * 2 - 1
pointer.y = -(event.clientY / window.innerHeight) * 2 + 1
})
// 包含场景中所有立方体的数组
const cubes = ...
// 在渲染循环中用于确定要突出显示的对象
const raycaster = new THREE.Raycaster()
function render() {
raycaster.setFromCamera(pointer, camera)
const cubes = scene.getObjectByName('group').children
const intersects = raycaster.intersectObjects(cubes)
// 对交叉的对象进行一些操作
}

在这里,我们使用 THREE.Raycaster 确定哪些对象与鼠标位置相交,从摄像机的位置到射线的结束。 结果(在上述示例中的 intersects)包含所有与我们的鼠标相交的立方体, 因为射线是从摄像机位置穿过摄像机范围的。数组中的第一个是我们悬停在上面的对象, 数组中的其他值(如果有的话)指向第一个网格后面的对象。 THREE.Raycaster 还提供有关确切击中对象位置的其他信息:

在这里,我们点击了面对象。 faceIndex 指向所选择的网格的面。 distance 值是从摄像机到点击对象的距离,而 point 是点击网格的确切位置。 最后,我们有 uv 值,当使用纹理时, 它确定点击点在2D纹理上的位置(从0到1;有关 uv 的更多信息,请参见第10章《加载和使用纹理》)。

拖动对象​

除了选择对象,通常的需求是能够拖动和移动对象。 Three.js 也提供了对此的默认支持。 如果在浏览器中打开 dragging-objects.html 示例,您将看到与图9.2中显示的类似的场景。 这一次,当您点击一个对象时,您可以将其拖动到场景中:

为了支持拖动对象,Three.js 使用了一个称为 DragControls 的东西。 它处理一切,并在拖动开始和结束时提供方便的回调。完成此操作的代码如下所示:

const orbit = new OrbitControls(camera, renderer.domElement)
orbit.update()
const controls = new DragControls(cubes, camera, renderer.
domElement)
controls.addEventListener('dragstart', function (event) {
orbit.enabled = false
event.object.material.emissive.set(0x33333)
})
controls.addEventListener('dragend', function (event) {
orbit.enabled = true
event.object.material.emissive.set(0x000000)
})

就是这么简单。 在这里,我们添加了 DragControls,并传入可以被拖动的元素(在我们的例子中,是所有随机放置的立方体)。 然后,我们添加了两个事件监听器。 第一个,dragstart,在我们开始拖动一个立方体时调用, 而 dragend 在我们停止拖动对象时调用。 在这个例子中,当我们开始拖动时, 我们禁用 OrbitControls(这允许我们使用鼠标查看场景周围)并改变所选对象的颜色。 一旦我们停止拖动,我们将对象的颜色改回并再次启用 OrbitControls。

还有一个稍微更高级的 DragControls 版本称为 TransformControls。 我们不会详细讨论这个控件,但它允许您使用简单的 UI 来转换网格的属性。 当您在浏览器中打开 transform-controls-html 时,您会找到这个控件的一个示例:

如果您点击此控件的各个部分,您可以轻松地更改立方体的形状:

在本章的最后一个示例中, 我们将向您展示如何使用缓动库的替代方式来修改对象的属性(正如我们在本章的第一个示例中看到的)。

使用 Tween.js 进行动画​

Tween.js 是一个小型的 JavaScript 库,您可以从 https://github.com/sole/tween.js/ 下载, 并且可以用它轻松定义属性在两个值之间的过渡。 所有在起始值和结束值之间的中间点都会为您计算出来。 这个过程称为 tweening。 例如,您可以使用此库在 10 秒内将网格的 x 位置从 10 更改为 3,如下所示:

const tween = new TWEEN.Tween({x: 10}).to({x: 3}, 10000)
.easing(TWEEN.Easing.Elastic.InOut)
.onUpdate( function () {
// update the mesh
})

或者,您可以创建一个单独的对象并将其传递到您想要处理的网格中:

const tweenData = {
x: 10
}

new TWEEN.Tween(tweenData)
.to({ x: 3 }, 10000)
.yoyo(true)
.repeat(Infinity)
.easing(TWEEN.Easing.Bounce.InOut)
.start()
mesh.userData.tweenData = tweenData

在此示例中,我们创建了 TWEEN.Tween。 这个 tween 将确保 x 属性在 10,000 毫秒内从 10 更改为 3。 Tween.js 还允许您定义随时间如何更改此属性。 这可以通过使用线性、 二次或任何其他可能性来实现(有关完整概述,请参见 http://sole.github.io/tween.js/examples/03_graphs.html)。 该值通过一个称为 easing 的过程随时间更改。 使用 Tween.js,您可以使用 easing() 函数进行配置。 该库还提供其他控制此缓动方式的方法。 例如,我们可以设置缓动应该重复的频率(repeat(10)), 以及是否要使用 yoyo 效果(在此示例中,这意味着我们从 10 到 3,然后再回到 10)。

与 Three.js 一起使用此库非常简单。 如果打开 tween-animations.html 示例,您将看到 Tween.js 库的运行效果。 以下截图显示了示例的静止图像:

我们将使用 Tween.js 库将此移动到一个单一的点,使用特定的 easing(),在某一点看起来如下:

在这个示例中,我们从第7章中取了一个点云,并创建了一个动画,其中所有点都缓慢地移动到中心。 这些粒子的位置是通过使用使用 Tween.js 库创建的缓动来设置的,如下所示:

const geometry = new THREE.TorusKnotGeometry(2, 0.5, 150, 50,
3, 4)
geometry.setAttribute('originalPos', geometry.
attributes['position'].clone())
const material = new THREE.PointsMaterial(..)
const points = new THREE.Points(geometry, material)
const tweenData = {
pos: 1
}
new TWEEN.Tween(tweenData)
.to({ pos: 3 }, 10000)
.yoyo(true)
.repeat(Infinity)
.easing(TWEEN.Easing.Bounce.InOut)
.start()
points.userData.tweenData = tweenData
// in the render loop
const originalPosArray = points.geometry.attributes.
originalPos.array
const positionArray = points.geometry.attributes.position.array
TWEEN.update()
for (let i = 0; i < points.geometry.attributes.position.count;
i++) {
positionArray[i * 3] = originalPosArray[i * 3] * points.
userData.tweenData.pos
positionArray[i * 3 + 1] = originalPosArray[i * 3 + 1] *
points.userData.tweenData.pos
positionArray[i * 3 + 2] = originalPosArray[i * 3 + 2] *
points.userData.tweenData.pos
}
points.geometry.attributes.position.needsUpdate = true

通过这段代码,我们创建了一个 tween,将一个值从 1 过渡到 0,然后再次回到。 要使用 tween 的值,我们有两种不同的选择:我们可以使用此库提供的 onUpdate 函数, 每当 tween 更新时调用带有更新值的函数(通过调用 TWEEN.update() 完成), 或者我们可以直接访问更新后的值。在这个例子中,我们使用了后一种方法。 在查看需要在 render 函数中进行的更改之前,

我们必须在加载模型后执行一个额外的步骤。 我们想在原始值与零之间进行 tween。 为此,我们需要将顶点的原始位置存储在某个地方。 我们可以通过复制起始位置数组来完成这一点:

geometry.setAttribute('originalPos', geometry.
attributes['position'].clone())

现在,每当我们想要访问原始位置时,我们可以查看几何图形上的 originalPos 属性。 现在,我们只需使用 tween 的值计算每个顶点的新位置即可。 我们可以在 render 循环中这样做:

const originalPosArray = points.geometry.attributes.
originalPos.array
const positionArray = points.geometry.attributes.position.array
for (let i = 0; i < points.geometry.attributes.position.count;
i++) {
positionArray[i * 3] = originalPosArray[i * 3] * points.
userData.tweenData.pos
positionArray[i * 3 + 1] = originalPosArray[i * 3 + 1] *
points.userData.tweenData.pos
positionArray[i * 3 + 2] = originalPosArray[i * 3 + 2] *
points.userData.tweenData.pos
}
points.geometry.attributes.position.needsUpdate = true

有了这些步骤,Tween 库将负责将各个点定位在屏幕上。 正如您所看到的,使用这个库比自己管理过渡要容易得多。 除了对对象进行动画和更改之外,我们还可以通过移动摄像机来动画场景。 在前面的章节中,我们曾多次通过手动更新摄像机的位置来执行此操作。 Three.js 还提供了几种其他更新摄像机的方式。

使用相机​

Three.js 提供了几种相机控制方式,您可以在整个场景中使用这些控制方式来控制相机。 这些控制位于 Three.js 分发包中, 可以在 examples/js/controls 目录中找到。 在本节中,我们将更详细地查看以下控制方式:

  • ArcballControls:提供一个透明的覆盖层,您可以用来轻松地移动相机。
  • FirstPersonControls:这些控制方式类似于第一人称射击游戏中的控制方式。您可以使用键盘移动并使用鼠标查看周围。
  • FlyControls:这些是类似飞行模拟器的控制方式。您可以使用键盘和鼠标移动和转向。
  • OrbitControls:这模拟了绕特定场景轨道运行的卫星。这允许您使用鼠标和键盘在周围移动。
  • PointerLockControls:这类似于第一人称控制方式,但它还锁定了鼠标指针到屏幕上,使其成为简单游戏的理想选择。
  • TrackBallControls:这是最常用的控制方式,允许您使用鼠标(或轨球)轻松移动、平移和缩放场景。

除了使用这些相机控制方式,您还可以通过设置相机的位置并使用 lookAt() 函数改变其指向的位置来自己移动相机。

我们首先看一下 ArcballControls。

ArcballControls​

解释 ArcballControls 的工作原理的最简单方式是查看一个示例。 如果您打开 arcball-controls.html 示例,您将看到一个简单的场景, 如下所示:

如果您仔细观察这个截图,您会看到两条透明的线穿过场景。 这些是 ArcballControls 提供的线,您可以用它们来旋转和平移场景。 这些线称为 gizmos。左鼠标按钮用于旋转场景,右鼠标按钮用于在周围移动,滚轮可以进行缩放。

除了这个标准功能,此控制还允许您专注于显示的网格的特定部分。 如果双击场景,相机将聚焦在场景的那一部分。 要使用此控制,我们只需实例化它并传入 camera 属性、renderer 使用的 domElement 属性, 以及我们正在查看的 scene 属性:

import { ArcballControls } from 'three/examples/jsm/controls/ArcballControls'
const controls = new ArcballControls(camera, renderer.domElement, scene)
controls.update()

这个控制是一个非常灵活的控制方式,可以通过一组属性进行配置。 大多数这些属性可以通过此示例右侧的菜单进行探索。 对于此特定控制,我们将深入了解此对象提供的属性和方法,因为它是一种灵活的控制方式, 当您希望为用户提供一种良好的方式来探索场景时,这是一个不错的选择。 让我们提供此控制提供的属性和方法的概述。 首先,让我们查看属性:

  • adjustNearFar: 如果设置为 true,此控制在缩放时会更改相机的近和远属性。
  • camera: 创建此控制时使用的相机。
  • cursorZoom: 如果设置为 true,在缩放时焦点将放在鼠标指针的位置。
  • dampingFactor: 如果 enableAnimations 设置为 true,则此值将确定动画在操作后停止的速度。
  • domElement: 用于列出鼠标事件的元素。
  • enabled: 确定此控制是否已启用。
  • enableRotate、enableZoom、enablePan、enableGrid、enableAnimations: 启用和禁用此控制提供的功能的属性。
  • focusAnimationTime: 当我们双击并专注于场景的某个部分时,此属性确定专注动画的持续时间。
  • maxDistance/minDistance: PerspectiveCamera 的缩放距离范围。
  • maxZoom/minZoom: 正交摄像机的缩放距离范围。
  • scaleFactor: 缩放的速度。
  • scene: 在构造函数中传递的场景。
  • radiusFactor: gizmo 相对于屏幕宽度和高度的大小。
  • wMax: 允许我们旋转场景的速度。

此控制还提供了几种与其进行交互或进一步配置的方法:

  • activateGizmos(bool): 如果为 true,则突出显示 gizmos。
  • copyState(), pasteState(): 允许您将控件的状态复制到 JSON 格式的剪贴板上,然后粘贴回来。
  • saveState(), reset(): 在内部保存当前状态,并使用 reset() 应用保存的状态。
  • dispose(): 从场景中删除此控件的所有部分,并清理所有侦听器和动画。
  • setGizomsVisible(bool): 指定是否显示或隐藏 gizmos。
  • setTbRadius(radiusFactor): 更新 radiusFactor 属性并重绘 gizmos。
  • setMouseAction(operation, mouse, key): 确定哪个鼠标键提供哪个操作。
  • unsetMouseAction(mouse, key): 清除分配的鼠标操作。
  • update(): 每当相机属性更改时,调用此方法以将这些新设置应用于此控制。
  • getRayCaster(): 提供对 rayCaster 的访问,该 rayCaster 在这些控件内部使用。

ArcballControls 是 Three.js 中一个非常有用且相对较新的添加,它使用鼠标提供对场景的高级控制。 如果您正在寻找一种更简单的方法,您可以使用 TrackBallControls。

TrackBallControls​

使用 TrackBallControls 的方法与我们在 ArcballControls 中看到的方法相同:

import { TrackBallControls } from 'three/examples/jsm/controls/TrackBallControls'
const controls = new TrackBallControls(camera, renderer.domElement)

这次,我们只需要传入相机和渲染器的 domeElement 属性。 为了使轨迹球控件起作用,我们还需要添加一个 THREE.Clock 并更新渲染循环, 如下所示:

const clock = new THREE.Clock()
function animate() {
requestAnimationFrame(animate)
renderer.render(scene, camera)
controls.update(clock.getDelta())
}

在上面的代码片段中,我们可以看到一个新的 Three.js 对象, THREE.Clock。THREE.Clock 对象可用于计算特定调用或渲染循环完成所需的经过时间。 您可以通过调用 clock.getDelta() 函数来实现这一点。 此函数将返回此调用与上一次调用 getDelta() 之间的经过时间。 要更新相机的位置,我们可以调用 TrackBallControls.update() 函数。 在此函数中,我们需要提供自上次调用此更新函数以来经过的时间。 为此,我们可以使用 THREE.Clock 对象的 getDelta() 函数。 您可能想知道为什么我们不只是将帧速率(1/60 秒)传递给更新函数。 原因是,使用 requestAnimationFrame,我们可以期望 60 FPS,但这并不是保证的。 根据各种外部因素,帧速率可能会改变。为了确保相机旋转和转动平滑,我们需要传递确切的经过时间。

您可以在 trackball-controls-camera.html 中找到此操作的实际示例。 以下截图显示了此示例的静态图像:

您可以通过以下方式控制相机:

  • 鼠标左键和移动:围绕场景旋转和翻滚相机
  • 滚轮:放大和缩小
  • 鼠标中键和移动:放大和缩小
  • 鼠标右键和移动:在场景中平移

有一些属性可以用来微调相机的行为。 例如,您可以通过设置 rotateSpeed 属性来设置相机旋转的速度, 并通过将 noZoom 属性设置为 true 来禁用缩放。 在本章中,我们不会详细说明每个属性的作用,因为它们几乎是不言自明的。 要获取可能的完整概述,请查看 TrackBallControls.js 文件的源代码,其中列出了这些属性。

FlyControls​

接下来,我们将看一下 FlyControls。 使用 FlyControls,您可以使用在飞行模拟器中找到的控件在场景中飞行。 您可以在 fly-controls-camera.html 中找到一个示例。 以下截图显示了此示例的静态图像:

启用 FlyControls 的方式与其他控件相同:

import { FlyControls } from 'three/examples/jsm/controls/FlyControls'
const controls = new FlyControls(camera, renderer.domElement)
const clock = new THREE.Clock()
function animate() {
requestAnimationFrame(animate)
renderer.render(scene, camera)
controls.update(clock.getDelta())
}

FlyControls 接受相机和渲染器的 domElement 作为参数, 并要求您在渲染循环中使用经过的时间调用 update() 函数。 您可以使用 THREE.FlyControls 以以下方式控制相机:

  • 鼠标左键和中键:开始向前移动
  • 鼠标右键:向后移动
  • 鼠标移动:环顾四周
  • W:开始向前移动
  • S:向后移动
  • A:向左移动
  • D:向右移动
  • R:向上移动
  • F:向下移动
  • 左、右、上和下箭头:分别向左、向右、向上和向下看
  • G:向左翻滚
  • E:向右翻滚

接下来,我们将看看 THREE.FirstPersonControls。

FirstPersonControls​

顾名思义,FirstPersonControls 允许您像在第一人称射击游戏中一样控制相机。 鼠标用于环顾四周,键盘用于移动。 您可以在 07-first-person-camera.html 中找到一个示例。 以下截图显示了此示例的静态图像:

创建这些控件遵循迄今为止我们所见过的其他控件所遵循的相同原则:

import { FirstPersonControls } from 'three/examples/jsm/controls/FirstPersonControls'
const controls = new FirstPersonControls(camera, renderer.domElement)
const clock = new THREE.Clock()
function animate() {
requestAnimationFrame(animate)
renderer.render(scene, camera)
controls.update(clock.getDelta())
}

此控件提供的功能非常直观:

  • 鼠标移动:环顾四周
  • 左、右、上和下箭头:分别向左、向右、向前和向后移动
  • W:向前移动
  • A:向左移动
  • S:向后移动
  • D:向右移动
  • R:向上移动
  • F:向下移动
  • Q:停止所有移动

对于最后一个控件,我们将从第一人称透视转向太空的透视。

OrbitControls​

OrbitControls 控件是一个非常好的方式,可以围绕场景中心的对象进行旋转和平移。 这也是我们在其他章节中使用的控件,为您提供了一种简单的方式来探索所提供示例中的模型。

通过 orbit-controls-orbit-camera.html,我们提供了一个展示该控件如何工作的示例。 以下截图显示了此示例的静态图像:

使用 OrbitControls 与使用其他控件一样简单。 包含正确的 JavaScript 文件,使用相机设置控件,并再次使用 THREE.Clock 来更新控件:

import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
const controls = new OrbitControls(camera, renderer.domElement)
const clock = new THREE.Clock()
function animate() {
requestAnimationFrame(animate)
renderer.render(scene, camera)
controls.update(clock.getDelta())
}

OrbitControls 的控制重点是使用鼠标,如下所示:

  • 左键点击并移动:围绕场景中心旋转相机
  • 滚动轮或中键点击并移动:缩放进和缩放出
  • 右键点击并移动:在场景中平移

关于相机及其移动,就介绍到这里。 在本节中,我们看到了许多控件,它们允许您通过更改相机属性轻松与场景进行交互和移动。 在下一节中,我们将探讨更高级的动画方法:变形和蒙皮。

变形和骨骼动画​

在外部程序(例如 Blender)中创建动画时,通常有两种主要的定义动画的选项:

  • 变形目标:使用变形目标,您定义了网格的变形版本 - 也就是关键位置。 对于这个变形目标,存储了所有顶点位置。 要使形状动画化,您需要将所有顶点从一个位置移动到另一个关键位置,并重复该过程。 下面的截图显示了用于展示面部表情的各种变形目标(此截图由Blender基金会提供):
  • 骨骼动画:另一种选择是使用骨骼动画。 使用骨骼动画,您定义了网格的骨骼 - 也就是骨骼,并将顶点附加到特定的骨骼。 现在,当您移动一个骨骼时,任何连接的骨骼也会相应地移动,并且基于骨骼的位置、移动和缩放, 附加的顶点会移动和变形。

下面再次由 Blender 基金会提供的截图显示了如何使用骨骼移动和变形对象的示例: Three.js 支持这两种模式,但在希望使用基于骨骼/骨骼的动画时,获得良好的导出可能会有问题。 为了获得最佳结果,您应该将模型导出或转换为 glTF 格式,这成为交换模型、动画和场景的默认格式, 并且在 Three.js 中得到了很好的支持。

在本节中,我们将研究这两个选项,并查看 Three.js 支持的一些外部格式,这些格式中可以定义动画。

使用变形目标的动画​

变形目标是定义动画的最直接的方式。 您为每个重要位置(也称为关键帧)定义所有顶点,并告诉 Three.js 将顶点从一个位置移动到另一个位置。

我们将通过两个示例向您展示如何使用变形目标。 在第一个示例中,我们将让 Three.js 处理各种关键帧(或从现在开始称之为变形目标)之间的过渡, 在第二个示例中,我们将手动完成这个过程。 请记住,我们只是介绍了在 Three.js 中处理动画的可能性的一部分。 正如您将在本节中看到的,Three.js 对于控制动画具有出色的支持,支持同步动画, 并提供了从一个动画平稳过渡到另一个动画的方法,这足以成为一本专著的主题。 因此,在接下来的几节中,我们将向您提供在 Three.js 中动画的基础知识,这应该为您提供足够的信息, 以便开始并探索更复杂的主题。

使用混合器和变形目标的动画​

在我们深入示例之前,首先我们将看一下可以用于使用 Three.js 进行动画的三个核心类。 在本章后面的部分,我们将向您展示这些对象提供的所有函数和属性:

  • THREE.AnimationClip:当加载包含动画的模型时, 您可以在响应对象中查找一个通常称为 animations 的字段。 该字段将包含一系列 THREE.AnimationClip 对象。 请注意,根据加载器的不同,动画可以在 Mesh、Scene 上定义,或者完全单独提供。 THREE.AnimationClip 最常用于保存模型可以执行的某个特定动画的数据。 例如,如果您加载了一只鸟的模型,一个 THREE.AnimationClip 将包含展翅的信息,另一个可能是张闭嘴巴。
  • THREE.AnimationMixer:THREE.AnimationMixer 用于控制多个 THREE.AnimationClip 对象。 它确保动画的时间正确,并使同步动画成为可能,或者从一种动画平滑地过渡到另一种动画。
  • THREE.AnimationAction:THREE.AnimationMixer 本身并没有暴露大量用于控制动画的函数。 通过 THREE.AnimationAction 对象实现此目的, 当您将 THREE.AnimationClip 添加到 THREE.AnimationMixer 时返回这些对象(尽管您还可以使用 THREE.AnimationMixer 提供的函数在以后的时间获取它们)。

还有一个 AnimationObjectGroup,您可以使用它来将动画状态不仅提供给单个 Mesh,还提供给对象组。

在以下示例中,您可以控制 THREE.AnimationMixer 和 THREE.AnimationAction, 它们是使用从模型中创建的 THREE.AnimationClip 创建的。 在此示例中使用的 THREE.AnimationClip 对象将模型变形为立方体,然后变形为圆柱体。

对于这个第一个变形示例, 理解基于变形目标的动画如何工作的最简单方法是打开 morph-targets.html 示例。 以下截图显示了该示例的静态图像:

在此示例中,我们有一个简单的模型(猴子的头),可以使用变形目标将其变形为立方体或圆柱体。 您可以通过移动 cubeTarget 或 coneTarget 滑块轻松测试此功能, 您将看到头部被变形为不同的形状。 例如,将 cubeTarget 设置为 0.5,您将看到我们在将猴子的初始头部变形为立方体的过程中。 一旦它到达 1,初始几何形状就会完全变形:

这就是变形动画的基础。 您有多个变形目标(影响)可以控制,根据它们的值(从0到1),顶点就会移动到所需的位置。 使用变形目标的动画就是这样工作的。 它只是定义在某个时间点应该发生某些顶点位置。 在运行动画时,Three.js 将确保向 Mesh 实例的 morphTargets 属性传递正确的值。

要运行预定义的动画,您可以打开此示例的 AnimationMixer 菜单,然后单击 Play。 您会看到头部首先变形为立方体,然后变形为圆柱体,然后再变回头的形状。

在 Three.js 中设置完成此操作所需的组件可以使用以下代码片段完成。 首先,我们必须加载模型。 在这个示例中,我们将此示例从 Blender 导出为 glTF,因此我们的动画位于顶层。 我们只需将它们添加到可以在代码的其他部分中访问的变量中。 我们也可以将其设置为网格的属性或将其添加到网格的 userdata 属性中:

let animations = []
const loadModel = () => {
const loader = new GLTFLoader()
return loader.loadAsync('/assets/models/blender-morph-targets/morph-targets.gltf').then((container) => {
animations = container.animations
return container.scene
})
}

现在,我们从加载的模型获取了一个动画,我们可以设置具体的 Three.js 组件,以便我们可以播放它们:

const mixer = new THREE.AnimationMixer(mesh)
const action = mixer.clipAction(animations[0])
action.play()

还有最后一步,我们需要执行,以便在渲染时显示网格的正确形状,那就是在渲染循环中添加一行:

// 在渲染循环中
mixer.update(clock.getDelta())

在这里,我们再次使用 THREE.Clock 来确定从现在到上一个渲染循环之间经过的时间, 并调用 mixer.update()。 混合器使用此信息确定它应该将顶点变形到下一个变形目标(关键帧)的距离。

THREE.AnimationMixer 和 THREE.AnimationClip 还提供了一些其他函数, 您可以使用这些函数控制动画或创建新的 THREE.AnimationClip 对象。 您可以通过在本节示例的右侧菜单中使用它们来进行实验。我们将从 THREE.AnimationClip 开始:

  • duration:此轨迹的持续时间(以秒为单位)。
  • name:此剪辑的名称。
  • tracks:用于跟踪模型的某些属性如何进行动画的内部属性。
  • uuid:此剪辑的唯一 ID。这是自动分配的。
  • clone():复制此剪辑。
  • optimize():优化 THREE.AnimationClip。
  • resetDuration():确定此剪辑的正确持续时间。
  • toJson():将此剪辑转换为 JSON 对象。
  • trim():将所有内部轨迹修剪到在此剪辑上设置的持续时间。
  • validate():进行一些最小的验证,以查看这是否是有效的剪辑。
  • CreateClipsFromMorphTargetSequences(name, morphTargetSequences, fps, noLoop):根据一组变形目标序列创建 THREE.AnimationClip 实例的列表。
  • CreateFromMorphTargetSequences(name, morphTargetSequence, fps, noLoop):从变形目标序列创建单个 THREE.AnimationClip。
  • findByName(objectOrClipArray, name):按名称搜索 THREE.AnimationClip。
  • parse 和 toJson:允许您将 Three.AnimationClip 还原和保存为 JSON,分别。
  • parseAnimation(animation, bones):将 THREE.AnimationClip 转换为 JSON。

一旦您获得了 THREE.AnimationClip,就可以将其传递给 THREE.AnimationMixer 对象,该对象提供以下功能:

  • AnimationMixer(rootObject):此对象的构造函数。 此构造函数接受一个 THREE.Object3D 作为参数(例如,THREE.Mesh 或 THREE.Group 的 THREE.Mesh)。
  • time:此混音器的全局时间。这在创建混音器时从 0 开始。
  • timeScale:这可以用于加速或减缓此混音器管理的所有动画。 如果将此属性的值设置为 0,则所有动画实际上都已暂停。
  • clipAction(animationClip, optionalRoot):创建一个可以用于控制传入的 THREE.AnimationClip 的 THREE.AnimationAction。 如果动画剪辑是针对构造 AnimationMixer 的对象之外的不同对象的,则也可以将其传递。
  • fadeIn(durationInSeconds):在传递的时间间隔内从 0 缓慢增加权重属性到 1。
  • fadeOut(durationInSeconds):在传递的时间间隔内缓慢减小权重属性从 0 到 1。
  • getEffectiveTimeScale():根据当前运行的变形获取有效的时间刻度。
  • getEffectiveWeight():根据当前运行的淡出获取有效的权重。
  • getClip():返回此操作正在管理的 THREE.AnimationClip 属性。
  • getMixer():获取正在播放此操作的混音器。
  • getRoot():获取由此操作控制的根对象。
  • halt(durationInSeconds):在 durationInSeconds 内逐渐减小 timeScale 到 0。
  • isRunning():检查动画当前是否正在运行。
  • isScheduled():检查此操作是否当前在混音器中活动。
  • play():开始运行此操作(启动动画)。
  • reset():重置此操作。这将导致将 paused 设置为 false,enabled 设置为 true, 并将 time 设置为 0。
  • setDuration(durationInSeconds):设置单个循环的持续时间。 这将更改 timeScale,以便在 durationInSeconds 内播放完整的动画。
  • setEffectiveTimeScale(timeScale):将 timeScale 设置为提供的值。
  • setEffectiveWeight():将权重设置为提供的值。
  • setLoop(loopMode, repetitions):设置 loopMode 和重复次数。 有关选项及其效果的 loop 属性,请参见上文。
  • startAt(startTimeInSeconds):延迟 startTimeInSeconds 开始动画。
  • stop():停止此操作,并应用复位。
  • stopFading():停止任何计划的淡入淡出。
  • stopWarping():停止任何计划的弯曲。
  • syncWith(otherAction):将此操作与传入的操作同步。 这将设置此操作的 time 和 timeScale 值为传入的操作。
  • warp(startTimeScale, endTimeScale, durationInSeconds):在指定的 durationInSeconds 内将 timeScale 属性从 startTimeScale 更改为 endTimeScale。

除了您可以用于控制动画的所有函数和属性之外,THREE.AnimationMixer 还提供了两个事件, 您可以通过在混音器上调用 addEventListener 来监听这两个事件。 "loop" 事件在单个循环完成时发送,"finished" 事件在整个操作完成时发送。

使用骨骼和蒙皮进行动画​

正如我们在“使用混合器和变形目标的动画”部分中所看到的,变形动画非常直接。 Three.js 知道所有目标顶点位置,只需将每个顶点从一个位置过渡到下一个位置。 但对于骨骼和蒙皮,情况变得稍微复杂。 当您使用骨骼进行动画时,您移动骨头,而 Three.js 必须确定如何相应地转换附加的皮肤(一组顶点)。 在这个例子中, 我们将使用从 Blender 导出到 Three.js 格式的模型(在 models/blender-skeleton 文件夹中的 lpp-rigging.gltf)。 这是一个完整带有一组骨骼的人物模型。 通过移动骨头,我们可以对整个模型进行动画。 首先,让我们看一下如何加载模型:

let animations = []
const loadModel = () => {
const loader = new GLTFLoader()
return loader.loadAsync('/assets/models/blender-skeleton/lpp-rigging.gltf').then((container) => {
container.scene.translateY(-2)
applyShadowsAndDepthWrite(container.scene)
animations = container.animations
return container.scene
})
}

我们以 glTF 格式导出模型,因为 Three.js 对 glTF 的支持很好。 加载用于骨骼动画的模型与加载其他模型并没有太大区别。 我们只需指定模型文件并像加载任何其他 glTF 文件一样加载它。 对于 glTF,动画位于加载的对象的一个单独属性中, 因此我们只需将其分配给 animations 变量以便轻松访问。

在这个示例中,我们添加了一个控制台日志,显示了我们加载后的 THREE.Mesh 的样子:

console.log(mesh)

在这里,您可以看到网格由一组骨骼和网格组成。这也意味着如果您移动一个骨骼,相关的网格也将随之移动。

以下截图显示了此示例的静态图像:

此场景还包含一段动画,您可以通过勾选 animationIsPlaying 复选框来触发。 这将覆盖手动设置的骨骼位置和旋转,并使骨骼跳跃起伏。

要设置此动画,我们必须遵循前面看到的相同步骤:

const mixer = new THREE.AnimationMixer(mesh)
const action = mixer.clipAction(animations[0])
action.play()

正如您所看到的,与使用固定变形目标一样,使用骨骼同样简单。 在这个例子中,我们只调整了骨骼的旋转;您还可以移动位置或更改比例。 在下一节中,我们将看看如何从外部模型加载动画。

使用外部模型创建动画​

在第8章《创建和加载高级网格和几何体》中,我们看到了几种Three.js支持的3D格式。 其中有几种格式也支持动画。 在本章中,我们将看到以下示例:

  • COLLADA 模型:COLLADA 格式支持动画。 在此示例中,我们将从 COLLADA 文件加载动画并使用 Three.js 渲染它。
  • MD2 模型:MD2 模型是在较早的 Quake 引擎中使用的一种简单格式。 尽管该格式有点过时,但仍然是存储角色动画的很好的格式。
  • glTF 模型:GL 传输格式(glTF)是一种专门设计用于存储3D场景和模型的格式。 它专注于最小化资产大小,并尝试在解压模型时尽可能高效。
  • FBX 模型:FBX 是由 https://www.mixamo.com 上的 Mixamo 工具生成的格式。 使用 Mixamo,您可以轻松设置和为模型添加动画,而无需大量建模经验。
  • BVH 模型:Biovision(BVH)格式与其他加载器相比略有不同。 使用此加载器时,您不会加载具有骨架或一组动画的几何体。 使用 Autodesk MotionBuilder 使用的此格式时,您只会加载一个骨架,您可以可视化甚至将其附加到几何体。

我们将从 glTF 模型开始,因为该格式正在成为在不同工具和库之间交换模型的标准。

使用 gltfLoader​

近来越来越受关注的格式之一是 glTF 格式。 这种格式的详细解释可以在 https://github.com/KhronosGroup/glTF 找到, 它专注于优化大小和资源使用。 使用 glTFLoader 与使用其他加载器类似:

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
// ...
return loader.loadAsync('/assets/models/truffle_man/scene.gltf')
.then((container) => {
container.scene.scale.setScalar(4)
container.scene.translateY(-2)
scene.add(container.scene)
const mixer = new THREE.AnimationMixer(container.scene);
const animationClip = container.animations[0];
const clipAction = mixer.clipAction(animationClip).play();
})

此加载器还加载完整场景,因此您可以将所有内容添加到组中或选择子元素。 在这个示例中,您可以通过打开 load-gltf.js 查看结果。

对于下一个示例,我们将使用 FBX 模型。

使用fbxLoader可视化捕捉的模型动作​

Autodesk FBX 格式已经存在一段时间,非常易于使用。 有一个在线资源,您可以在该资源中找到许多以这种格式下载的动画: https://www.mixamo.com/。 该网站提供了 2,500 种您可以使用和定制的动画: 下载动画后,从Three.js中使用它很简单:

import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'
// ...
loader.loadAsync('/assets/models/salsa/salsa.fbx').then((mesh) => {
mesh.translateX(-0.8)
mesh.translateY(-1.9)
mesh.scale.set(0.03, 0.03, 0.03)
scene.add(mesh)
const mixer = new THREE.AnimationMixer(mesh)
const clips = mesh.animations
const clip = THREE.AnimationClip.findByName(clips, 'mixamo.com')
})

如您在 load-fbx.html 中所见,生成的动画效果非常出色: FBX 和 glTF 是现代格式,被广泛使用,是交换模型和动画的好方法。 还有一些较老的格式。其中一个有趣的是由旧 FPS 游戏 Quake 使用的 MD2 格式。

从 Quake 模型加载动画​

MD2 格式是为模拟 Quake 中的角色而创建的,Quake 是一款1996年的经典游戏。 尽管较新的引擎使用了不同的格式,但您仍然可以在 MD2 格式中找到许多有趣的模型。 使用 MD2 文件与我们迄今所见的其他格式有些不同。 加载 MD2 模型时,您会得到一个几何体,因此必须确保您也创建了一个材质并分配一个皮肤:

let animations = []
const loader = new MD2Loader()
loader.loadAsync('/assets/models/ogre/ogro.md2').then((object) => {
const mat = new THREE.MeshStandardMaterial({
color: 0xffffff,
metalness: 0,
map: new THREE.TextureLoader().load('/assets/models/ogre/skins/skin.jpg')
})
animations = object.animations
const mesh = new THREE.Mesh(object, mat)
// 添加到场景,然后您可以像我们已经看到的那样对其进行动画
})

一旦有了这个 Mesh,设置动画的方式与之前相同。 可以在这里查看该动画的结果 (load-md2.html):

接下来是 COLLADA。

从 COLLADA 模型加载动画​

尽管普通的 COLLADA 模型未经压缩(它们可能会变得相当大),Three.js 中也有一个 KMZLoader 可用。 这是一个压缩的 COLLADA 模型,因此如果您遇到 Keyhole Markup Language Zipped (KMZ) 模型, 可以使用 KMZLoader 而不是 ColladaLoader 加载模型:

const loader = new KMZLoader();
loader.loadAsync('/assets/models/collada_model/model.kmz').then((collada) => {
scene.add(collada.scene);
});

对于最后一个加载器,我们将看看 BVHLoader。

使用 BVHLoader 可视化骨架​

BVHLoader 是与我们迄今所见不同的加载器。 该加载器不返回带有动画的网格或几何体; 相反,它返回骨架和动画。 在 load-bvh.html 中展示了一个例子:

const loader = new BVHLoader();
let animation = undefined;
loader.loadAsync('/assets/models/amelia-dance/DanceNightClub7_t1.bvh').then((result) => {
const skeletonHelper = new THREE.SkeletonHelper(result.skeleton.bones[0]);
skeletonHelper.skeleton = result.skeleton;
const boneContainer = new THREE.Group();
boneContainer.add(result.skeleton.bones[0]);
animation = result.clip;
const group = new THREE.Group();
group.add(skeletonHelper);
group.add(boneContainer);
group.scale.setScalar(0.2);
group.translateY(-1.6);
group.translateX(-3);
// 现在我们可以像其他示例一样对该组进行动画处理
});

通过 THREE.SkeletonHelper,我们可以可视化网格的骨架。 BVH 模型只包含骨架信息,我们可以这样进行可视化。

在较早的 Three.js 版本中,支持其他种类的动画文件格式。 其中大多数已过时,并已从 Three.js 发布中删除。 如果您碰巧发现另一种格式要显示动画,您可以查看较旧的 Three.js 版本,可能可以从那里重用加载器。

总结​

在本章中,我们探讨了可以为场景添加动画的不同方式。 我们从一些基本的动画技巧开始,然后转移到相机移动和控制, 并最后看了如何使用形变目标和骨骼/骨骼动画对模型进行动画处理。

当渲染循环就绪时,添加简单的动画非常容易。 只需更改网格的属性;在下一个渲染步骤中,Three.js 将呈现更新后的网格。 对于更复杂的动画,通常会在外部程序中对其建模,并通过 Three.js 提供的加载器之一加载它们。

在前面的章节中,我们看到了各种可以用来装饰对象的材料。 例如,我们看到了如何更改这些材料的颜色、光泽和不透明度。 然而,我们尚未详细讨论如何与这些材料一起使用外部图像(也称为纹理)。 使用纹理,我们可以轻松地创建看起来像由木材、金属、石头等制成的对象。 在第10章中,我们将探讨纹理的所有不同方面以及它们在 Three.js 中的使用方式。