第九章动画和移动摄像机

在前几章中,我们看到了一些简单的动画,但没有太复杂的。在第1章“使用Three.js创建第一个3D场景”中,我们介绍了基本的渲染循环,在接下来的章节中,我们使用它来旋转一些简单的对象,并展示其他一些基本的动画概念。

在本章中,我们将更详细地研究Three.js是如何支持动画的。我们将研究以下四个主题:

  • 基本动画
  • 使用相机
  • 变形和骨骼动画
  • 使用外部模式创建动画

我们将首先介绍动画背后的基本概念。

基本动画

在查看这些示例之前,让我们快速回顾一下第一章中在渲染循环中显示的内容。为了支持动画,我们需要告诉Three.js经常渲染场景。为此,我们使用了标准的HTML5请求程序动画框架功能,如下所示:

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

在这段代码中,我们只需要在初始化场景后调用 render() 函数。在rende() 函数本身中,我们使用 requestAnimationFrame 来安排下一次渲染。这样,浏览器将确保 render() 函数以正确的间隔调用(通常每秒调用60次或120次左右)。在将 requestAnimationFrame 添加到浏览器之前,使用了setInterval(function, interval) 或setTimeout(function, interval) 。这些函数将在每个设置的时间间隔调用指定的函数一次。

这种方法的问题在于,它没有考虑其他正在发生的事情。即使你的动画没有显示或在隐藏的选项卡中,它仍然被调用,并且仍然在使用资源。另一个问题是,这些函数在任何时候被调用时都会更新屏幕,而不是在浏览器的最佳时间,这会导致更高的CPU使用率。使用requestAnimationFrame,我们不会告诉浏览器何时需要更新屏幕;我们要求浏览器在最合适的时候运行所提供的功能。通常,这会导致大约60或120 FPS的帧速率(取决于您的硬件)。有了requestAnimationFrame,你的动画将运行得更流畅,对CPU和GPU更友好,而且你不必担心时间问题。

在下一节中,我们将开始创建一个简单的动画

简单的动画

使用这种方法,我们可以很容易地通过更改对象的旋转、比例、位置、材质、顶点、面和其他任何你能想象到的东西来设置对象的动画。在下一个渲染循环中,Three.js将渲染更改后的属性。 01-basic-animations.html 中提供了一个非常简单的例子,基于我们在第7章“点和精灵”中看到的例子。下面的屏幕截图显示了这个例子:

image-20230524164043792

图9.1-更改其属性后的动画

这方面的渲染循环非常简单。首先,我们初始化userDataobject上的各种属性,userDataobject是存储在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)
}

这里没有什么壮观的东西,但它很好地展示了我们将在这本书中讨论的基本动画背后的概念。我们只是改变比例,旋转,位置属性和三。剩下的部分由js来做。

在下一节中,我们将快速地回避一下。除了动画之外,在更复杂的场景中使用Three.js时,你会很快遇到一个重要的方面,那就是使用鼠标来选择屏幕上的对象。

选择和移动对象

尽管与动画没有直接关系,但由于我们将在本章中研究相机和动画,因此了解如何选择和移动对象是本章中解释的主题的一个很好的补充。在这里,我们将向您展示如何执行以下操作:

  • 使用鼠标从场景中选择一个对象
  • 用鼠标拖动场景周围的对象

我们将从查看选择对象所需要采取的步骤开始。

选择对象

首先,打开selecting-objects.html示例,其中您将看到以下内容:

image-20230524164744004

图9.2-随机放置可用鼠标选择的立方体

当您在场景中移动鼠标时,您会看到每当鼠标碰到对象时,该对象都会高亮显示。您可以通过使用THREE.Raycaster轻松创建。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 从相机的角度确定哪些对象与鼠标的位置相交。结果(在前面的示例中为相交)包含与鼠标相交的所有立方体,因为光线是从相机的位置投射到相机范围的末端。这个数组中的第一个值是我们悬停在上面的那个值,而这个数组中其他的值(如果有的话)指向第一个网格后面的对象。THREE.Raycaster 还提供了关于你击中物体的确切位置的其他信息:

image-20230524165205483

图9.3-来自光线光器的附加信息

在这里,我们点击了人脸对象。faceIndex指向选定网格的面。距离值是从相机到单击对象的距离,点是网格上单击对象的确切位置。最后,我们有一个uv值,它决定了在使用纹理时,单击的点在2D纹理上的位置(范围从0到1;有关uv的更多信息可以在第10章“加载和使用纹理”中找到)。

拖动对象

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

image-20230524165421005

图9.4-使用鼠标在场景周围拖动一个对象

为了支持拖动对象,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 时,可以找到此控件的示例:

image-20230524165904104

图9.5-变换控件允许您更改网格的属性

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

image-20230524170022449

图9.6-使用变换控件修改的形状

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

用Tween.js动画

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

const tween = new TWEEN.Tween({x: 10}).to({x: 3}, 10000)
.easing(TWEEN.Easing.Elastic.InOut)
.onUpdate( function () {
     // 更新网格
})

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

 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属性在10000毫秒内从10更改为3。Tween.js还允许您定义此属性如何随时间变化。这可以使用线性、二次或任何其他可能性来实现(请参见http://sole.github.io/tween.js/examples/03_graphs.html以获得完整的概述)。价值会随着时间的推移而变化,这一过程称为宽松。使用Tween.js,可以使用 easing() 函数进行配置。这个库还提供了额外的方法来控制这种宽松是如何实现的。例如,我们可以设置放松应该重复的频率(repeat(10)),以及我们是否想要yoyo效应(这意味着我们在这个例子中从10到3再回到10)。

将这个库与Three.js一起使用非常简单。如果您打开tween-animations.html示例,您将看到tween.js库正在运行。以下屏幕截图显示了示例的静态图像:

将这个库与Three.js一起使用非常简单。如果您打开tween-animations.html示例,您将看到tween.js库正在运行。以下屏幕截图显示了示例的静态图像:

image-20230524171346880

图9.7-在动作的中间调整一个点系统

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

image-20230524171459429

图9.8-将所有内容合并为单个点的微调

在这个例子中,我们从第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中的值,我们有两个不同的选项:无论何时更新tween(通过调用tween.update()来完成),我们都可以使用该库提供的onUpdate函数来调用具有更新值的函数,或者我们可以直接访问更新的值。在这个例子中,我们使用了后一种方法。

在我们查看需要在渲染函数中进行的更改之前,我们必须在加载模型后执行一个额外的步骤。我们想在原始值之间切换到零,然后再返回。为此,我们需要将顶点的原始位置存储在某个位置。我们可以通过复制起始位置数组来做到这一点:

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

现在,无论何时我们想要访问原始位置,我们都可以查看几何图形上的原始pos属性。现在,我们可以只使用补间中的值来计算每个顶点的新位置。我们可以在渲染循环中这样做:

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/controlsdirectory中找到。在本节中,我们将更详细地查看以下控件:

  • ArcballControls:一个广泛的控件,提供透明覆盖,可用于轻松移动相机。
  • FirstPersonControls:这些控件的行为与第一人称射击游戏中的控件类似。您可以使用键盘四处移动,也可以使用鼠标四处查看。
  • FlyControls制:这些是类似飞行模拟器的控制。你可以用键盘和鼠标移动和操纵。
  • OrbitControls:模拟卫星在特定场景周围的轨道上运行。这允许您使用鼠标和键盘四处移动。
  • PointerLockControls:这些控件类似于第一人称控件,但它们也将鼠标指针锁定在屏幕上,使其成为简单游戏的绝佳选择。
  • TrackBallControls:这些是最常用的控件,允许您使用鼠标(或轨迹球)在场景中轻松移动、平移和缩放。

除了使用这些相机控制,你还可以通过设置相机的位置和使用lookAt()功能改变它的位置来移动相机的自己。

我们将看的第一个控制 ArcballControls

ArcballControls

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

image-20230525084041876

图9.9-使用 ArcballControls 探索场景

如果你仔细看这个屏幕截图,你会看到两条半透明的线条穿过场景。这些是由弧球控件提供的线,您可以使用它来围绕场景旋转和平移。这些行被称为 gizmos(小发明)。鼠标左键用于旋转场景,鼠标右键可用于平移,您可以使用滚动轮进行放大。

除了此标准功能外,此控件还允许您将注意力集中在所显示的网格的特定部分上。如果双击场景,摄影机将聚焦在场景的该部分。要使用此控件,我们所需要做的就是实例化它并传入相机属性、渲染器使用的domElement property和我们正在查看的场景属性:

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:我们可以缩小和放大OrthographicCamera的距离
  • scaleFactor:放大和缩小的速度
  • scene:构造函数传递的场景
  • radiusFactor:“gizmo”相对于屏幕宽度和高度的大小
  • wMax:我们旋转场景的速度有多快

这个控件还提供了几种方法来进一步交互或配置它:

  • activateGizmo(bool):如果为true,则高亮显示 Gizmo
  • copyState(),pasteState():允许您用JSON将控件的状态复制并粘贴到剪贴板
  • saveState(),reset():在内部保存当前状态,并使用 reset() 应用保存的状态
  • dispose():从场景中删除此控件的所有部分,并清理所有侦听器和动画
  • setGizmo可见(bool):指定是显示还是隐藏 Gizmo
  • setTbRadius(radiusFactor):更新半径因子属性并重新绘制小控件
  • setMouseAction(operation, mouse, key):确定哪个鼠标键提供哪个操作
  • unsetMouseAction(mouse, key):清除指定的鼠标操作
  • update():每当相机属性更改时,调用此函数将这些新设置应用于此控件
  • getRayCaster():提供对这些控件内部使用的 rayCaster 的访问

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

TrackBallControls

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

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

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

这方面的一个工作示例可以在 trackball-controls-camera.html 中找到。以下屏幕截图显示了本例的静态图像:

image-20230525090200976

图9.10-使用 TrackBallControls 来控制一个场景

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

  • 鼠标左键并移动:围绕场景进行旋转和滚动照相机
  • 滚动轮:放大并缩小
  • 鼠标中键并移动:放大并缩小
  • 鼠标右键并移动:绕着场景转

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

FlyControls

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

image-20230525091201650

图9.11-使用 FlyControls 在一个场景中飞行

启用 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() 函数,并使用渲染循环中的经过时间。您可以通过以下方式使用THRE.FlyControls控制相机:

  • 鼠标左键和鼠标中键按钮:开始前进
  • 鼠标右键:倒退
  • 鼠标移动: 四下观望
  • W:开始前进
  • S:开始倒退
  • A:向左移动
  • D:向右移动
  • R:向上移动
  • F:向下移动
  • 向左、向右、向上、向下的箭头:分别看左、右、向上和向下
  • G:向左滚动
  • E:向右滚动

接下来的控制是 THREE.FirstPersonControls.

FirstPersonControls

顾名思义,FirstPersonControls允许您像在第一人称射击游戏中一样控制相机。鼠标是用来四处看的,键盘是用来四处走动的。你可以在07-first-pperson-camera.html中找到一个例子。下面的屏幕截图显示了这个例子的静态图像:

image-20230525094245428

图9.12-使用第一人称控件探索一个场景

创建这些控件所遵循的原则与我们目前所看到的其他控件所遵循的原则相同:

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,我们包含了一个显示此控件如何工作的示例。以下屏幕截图显示了本例的静态图像:

image-20230525100004545

图9.13-轨道控制特性

使用 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),您通常有两个主要选项来定义动画:

  • Morph targets(变形目标):使用变形目标,可以定义网格的变形版本,即关键点位置。对于该变形目标,将存储所有顶点位置。要设置形状的动画,只需将所有顶点从一个位置移动到另一个关键位置,然后重复该过程。以下截图显示了用于显示面部表情的各种变形目标(此截图由Blender基金会提供):

image-20230525100602009

图9.14-使用变形目标设置动画

  • Skeleton animation(骨骼动画):另一种选择是使用骨架动画。使用骨骼动画,可以定义网格的骨骼(即骨骼),并将顶点附加到特定骨骼。现在,当移动骨骼时,任何连接的骨骼也会相应地移动,并且附着的顶点会根据骨骼的位置、移动和缩放进行移动和变形。以下屏幕截图再次由Blender基金会提供,显示了如何使用骨骼来移动和变形对象的示例:

image-20230525100741778

图9.15-使用骨骼设置动画

Three.js支持这两种模式,但当您想要使用基于骨骼/骨骼的动画时,获得良好的导出可能会出现问题。为了获得最佳效果,您应该将模型导出或转换为glTF格式,这将成为交换模型、动画和场景的默认格式,并且Three.js提供了强大的支持

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

具有变形目标的动画

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

我们将使用两个示例向您展示如何使用变形目标。在第一个示例中,我们将让Three.js处理各种关键帧(或变形目标,从现在起我们将称之为变形目标)之间的过渡,在第二个示例中我们将手动执行此操作。请记住,我们只是触及Three.js中动画的表面。正如您将在本节中看到的,Three.jss对控制动画提供了极好的支持,支持动画同步,并提供了从一个动画平滑过渡到另一个动画的方法,这就保证了一本关于这个主题的书。因此,在接下来的几节中,我们将为您提供Three.js中动画的基本知识,这将为您入门和探索更复杂的主题提供足够的信息。

动画与一个混合器和变形的目标

在我们深入研究这些例子之前,首先,我们将看看可以用来使用three.js制作动画的三个核心类。在本章的后面,我们将向您展示这些对象提供的所有函数和属性:

  • THREE.AnimationClip:加载包含动画的模型时,可以在响应对象中查找通常称为动画的字段。此字段将包含THREE.AnimationClip对象的列表。请注意,根据加载程序的不同,动画可能在“网格”、“场景”上定义,也可能完全单独提供。THREE.AnimationClip通常保存加载的模型可以执行的某个动画的数据。例如,如果你加载了一只鸟的模型,一个THREE.AnimationClip将包含拍打翅膀所需的信息,另一只可能正在张开和闭合喙。
  • THRE.AnimationMixer:THRE.ActivationMixer用于控制多个THREE.EnimationClip对象。它确保动画的计时是正确的可以将动画同步在一起,或者干净地从一个动画移动到另一个动画。
  • THREE.AnimationAction:THREE.AnimationMixer本身并没有公开大量控制动画的功能。这是通过THREE.AnimationAction对象完成的,当您将THREE.AnimationClip添加到THREE.AnimationMixer时会返回这些对象(尽管您可以稍后使用THREE.AnimationMixer提供的函数来获取它们)。

还有一个 AnimationObjectGroup,您可以使用它不仅为单个网格提供动画状态,而且为一组对象提供动画状态。

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

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

image-20230525102215970

图9.16-使用变形目标的动画

在这个例子中,我们有一个简单的模型(猴子的头),可以使用变形目标将其转换为立方体或圆柱体。你可以通过移动立方体目标锥形目标滑块来轻松地自己测试这一点,你会看到头部被变形成不同的形状。例如,当cubeTarget为0.5时,你会看到我们已经完成了将猴子的初始头部变形为立方体的一半。一旦处于1,初始几何体将完全变形:

image-20230525102420266

图9.17-相同的模型,但现在将立方体目标设置为1

这就是变形动画工作的基本原理。您有几个可以控制的形态目标(影响),并且根据它们的值(从0到1),顶点会移动到所需的位置。使用变形目标的动画将使用此方法。它只是定义了某些顶点位置应该在什么时候出现。当运行动画时,Three.js将确保将正确的值传递给网格实例的形态目标属性。

要运行预定义动画,可以打开本例的动画混合器菜单,然后单击“播放”。你会看到,头部将首先变成一个立方体,然后变成一个圆柱体,然后移动回到头部的形状。

在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,morphTargetSequence,fps,noLoop):这将基于一组变形目标序列创建一个THREE.AnimationClip实例列表。

  • CreateFromMorpTargetSequences(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.Group的THREE.Mesh)。
  • time:此混音器的全球时间。这从创建此混合器时的0开始。
  • timeScale:可用于加快或减慢此混合器管理的所有动画。如果此属性的值设置为0,则所有动画都将有效地暂停。
  • clipAction(animationClip,optionalRoot):这将创建一个THREE.AnimationAction,可用于控制传入的THREE.animationClip。如果动画剪辑用于与AnimationMixer构造函数中提供的对象不同的对象,则也可以传入该对象。
  • existingAction(animationClip,optionalRoot):这将返回THREE.AnimationAction属性,该属性可用于控制传入的THREE.animationClip。同样,如果THREE.animationClip用于不同的rootObject,您也可以传入该属性。

当您取回THREE.AnimationClip时,可以使用它来控制动画:

  • clampWhenFinished:设置为true时,这将导致动画在到达最后一帧时暂停。默认值为false。
  • enabled:设置为false时,将禁用当前操作,以免影响模型。当重新启用该动作时,动画将继续其停止的位置。
  • loop:这是此操作的循环模式(可以使用setLoop函数设置)。这可以设置为以下内容:
    • THREE.LoopOnce:只播放一次剪辑
    • THRE.LoopRepeat:根据已设置的重复次数重复剪辑
    • THREE.LoopPingPong:根据重复次数播放片段,但在向前和向后播放片段之间交替播放
  • paused:将此属性设置为true将暂停此剪辑的执行。
  • repetitions:动画将被重复的次数。这是由循环属性使用的。默认值为“无限”。
  • time:此操作运行的时间。这是从0到剪辑的持续时间的换行。
  • timeScale:可用于加快或减慢此动画的速度。如果此属性的值设置为0,则此动画将有效地暂停。
  • weight:从0到1指定此动画对模型的影响。当设置为0时,将看不到此动画中的任何模型变换,并且当设置为1,你会看到这个动画的完整效果。
  • zeroSlopeAtEnd:当设置为true(默认值)时,这将确保在单独的片段之间有平滑的过渡。
  • zeroSlopeAtStart:当设置为true(默认值)时,这将确保在单独的片段之间有平滑的过渡。
  • crossFadeFrom(fadeOutAction, durationInSeconds, warpBoolean):这会导致此操作淡入,而fadeOutAction则淡出。总衰减持续时间为秒。这允许动画之间的平滑过渡。当warpBoolean设置为true时,它将应用额外的时间刻度平滑。
  • crossFadeTo(fadeInAction, durationInSeconds, warpBoolean):与crossFadeFrom相同,但这次它会在提供的操作中淡入,并淡出此操作。变形和骨架动画303
  • fadeIn(durationInSeconds):在经过的时间间隔内,将权重属性从0缓慢增加到1。
  • fadeOut(durationInSeconds):在经过的时间间隔内,将权重属性从0缓慢降低到1。
  • getEffectiveTimeScale():返回基于当前运行的扭曲的有效时间刻度。
  • getEffectiveWeight():返回基于当前正在运行的淡入淡出的有效权重。
  • getClip():返回此操作正在管理的THREE.AnimationClip属性。
  • getMixer():返回正在播放此操作的混音器。
  • getRoot():获取由该操作控制的根对象。
  • halt(durationInSeconds):在持续时间秒内逐渐减少时间刻度为0。
  • isRunning():检查动画当前是否正在运行。
  • isScheduled():检查此操作当前是否在混合器中处于活动状态。
  • play():开始运行此动作(开始动画)。
  • reset():重置此操作。这将导致将paused设置为false,将enabled设置为true,将time设置为0。
  • setDuration(durationInSeconds):设置单个循环的持续时间。这将更改时间比例,以便完整的动画可以在持续时间InSeconds内播放。
  • setEffectiveTimeScale(时间刻度):将时间刻度设置为提供的值。
  • setEffectiveWeight():将权重设置为提供的值。
  • setLoop(loopMode, repetitions):设置loopMode和重复次数。有关选项及其效果,请参见循环属性。
  • startAt(startTimeInSeconds):延迟启动动画startTimeInSseconds。
  • stop():停止此操作,并应用重置。
  • stopFading():停止任何计划的衰落。
  • stopWarping():停止任何计划扭曲。
  • syncWith(otherAction):将此操作与传入的操作同步。这会将此操作的时间和timeScale值设置为传入的操作。
  • warp(startTimeScale、endTimeScale、durationInSeconds):在指定的持续时间InSeconds内,将timeScale属性从startTimeScale更改为endTimeScale。

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

使用骨骼和蒙皮的动画

正如我们在“带有混合器和变形目标的动画”部分中看到的那样,变形动画非常简单。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变量即可轻松访问。

在这个例子中,我们添加了一个控制台日志,它显示了一旦我们加载它:

image-20230605091556704

图9.18–骨架结构反映在对象的层次结构中

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

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

image-20230605092150213

图9.19–手动更改手臂和腿部骨骼的旋转

此场景还包含一个动画,您可以通过选中animationIsPlayingcheckbox来触发该动画。这将覆盖手动设置的骨骼的位置和旋转,并使骨骼上下跳跃:

image-20230605091749290

图9.20–播放骨架动画

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

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

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

使用外部模型创建动画

在第8章“创建和加载高级网格和几何图形”中,我们介绍了Three.js支持的几种3D格式。其中一些格式也支持动画。在本章中,我们将查看以下示例:

  • COLLADA model:COLLADA格式支持动画。对于本例,我们将从COLLADA文件加载一个动画,并使用Three.js进行渲染。
  • MD2 model:MD2型号是旧款Quake发动机中使用的一种简单格式。尽管格式有点过时,但对于存储角色动画来说,它仍然是一个非常好的格式。
  • glTF models:GL传输格式(glTF)是专门为存储3D场景和模型而设计的格式。它专注于最大限度地减少资产规模,并尽量提高效率尽可能地拆开模型。
  • FBX model:FBX是由Mixamo工具生成的格式,可在https://www.mixamo.com.使用Mixamo,您可以轻松地对模型进行装配和动画设置,而不需要大量的建模经验。
  • BVH model:与其他装载机相比,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来查看结果:

image-20230605093246063

图9.21-使用glTF加载的动画

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

使用bxLoader可视化捕捉到的运动模型

Autodesk FBX格式已经存在了一段时间,并且非常易于使用。网上有一个很棒的资源,你可以在那里找到许多可以以这种格式下载的动画:https://www.mixamo.com/.此网站提供2500个动画,您可以使用和自定义:

image-20230605093811555

图9.22-从mixamo加载动画

下载动画后,从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中所见,生成的动画看起来很棒:

image-20230605093939437

图9.23-使用fbx加载的动画

FBX和glTF是被广泛使用的现代格式,是交换模型和动画的好方法。还有一些较旧的格式。一个有趣的是旧的FPS Quake(雷神之锤)使用的格式:MD2。

从Quake模型加载动画

MD2格式是为了模仿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)
 // 添加到场景中,您可以像我们已经看到的那样设置它的动画 
 already
})

一旦您有了这个网格,设置动画的工作方式也是一样的。这个动画的结果可以在这里看到(load-md2.html):

image-20230605100540949

图9.24——满载的 Quake 怪物

下一个是COLLADA

从COLLADA模型加载动画

虽然普通的COLLADA模型没有压缩(它们可能会变得很大),但Three.js中也有一个KMZLoader。这是一个压缩的COLLADA模型,所以如果你运行Keyhole Markup Language Zipped(KMZ)模型,你可以使用KMZLloader而不是ColladaLoader加载模型:

image-20230605100758126

图 9.25 -加载 COLLADA 模型

对于最终装载机,我们将查看BVHLoader

使用BVHLoader可视化骨骼

BVHLoader是一个与我们迄今为止看到的装载机略有不同的装载机。此加载程序不会返回带有动画的网格或几何图形;相反,它返回一个骨架和一个动画。示例如load-bvh.html所示:

image-20230605100926291

图9.26-加载BVH骨架

为了将其可视化,我们可以使用THREE.SkeletonHelper,如图所示。使用THREE.SkeletonHelper,我们可以可视化网格的骨架。BVH模型只包含骨架信息,我们可以这样可视化:

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.js的旧版本中,支持其他类型的动画文件格式。其中大部分已经过时,随后已从Three.js发行版中删除。如果你偶然发现了一种不同的格式来显示动画,你可以看看旧的Three.js版本,并可能重新使用那里的加载程序。

总结

在本章中,我们介绍了设置场景动画的不同方法。我们从一些基本的动画技巧开始,转到相机的移动和控制,最后看了使用变形目标和骨架/骨骼动画制作模型的动画。

当你有了渲染循环后,添加简单的动画非常容易。只需更改网格的一个特性;在下一个渲染步骤中,Three.js将渲染更新后的网格。对于更复杂的动画,您通常会在外部程序中对它们进行建模,并通过Three.js提供的一个加载程序加载它们。

在前几章中,我们研究了可以用来对物体进行蒙皮的各种材料。例如,我们看到了如何更改这些材质的颜色、光泽度和不透明度。然而,我们还没有详细讨论的是,我们如何将外部图像(也称为纹理)与这些材料一起使用。有了纹理,我们可以很容易地创建看起来像是由木材、金属、石头等制成的物体。在第10章中,我们将探讨纹理的所有不同方面,以及它们在Three.js中的使用方式。