在 Three.js 中实现摄像机运动动画
今日目标
步骤 1:创建 3D 场景
步骤 2:添加点击处理程序
步骤三:移动摄像机
步骤 4:补间动画
什么是补间动画?
JavaScript 库 Tween.js
最终结果
待续…
我正在构建一个基于思维导图的社交媒体网络和协作工具,并将通过这一系列博客文章记录我的工作。如果你对我在构建 Web 应用的过程中,使用 React、Tailwind CSS、Firebase、Apollo/GraphQL、three.js 和 TypeScript 所学到的知识感兴趣,欢迎关注我。
今日目标
让我们来探讨一下如何让用three.js构建的 3D 模型具有交互性——当我们点击它时,摄像机会移动,将点击的对象置于屏幕中心,就像这样:
我目前正在开发一个名为Nuffshell 的项目,这是一个基于思维导图的社交网络和协作工具,我目前需要用到它。
不过,在本系列的这一部分中,我不会继续编写我的 Nuffshell 代码,而是从头开始构建一些东西,以便你们更容易地跟着操作。
如果你想跟着教程操作,我建议你使用CodeSandbox并使用“Vanilla”模板创建一个新项目:
它已经为您准备好了使用 JavaScript 模块所需的一切。
步骤 1:创建 3D 场景
首先,我将搭建一个基本的 3D 场景,其中显示四个彩色立方体,目前暂不添加交互或动画功能。
函数,函数,无处不在
我认为将程序的每个部分都分解成只做一件事的函数是一个很好的做法。函数的长度不应该超过屏幕长度。
基于此,以下是我为设置 3D 场景而创建的 JavaScript 模块。每个模块包含一个函数。
import * as THREE from "three";
export default function createRenderer() {
const app = document.getElementById("app");
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
app.appendChild(renderer.domElement);
return renderer;
}
我的createRenderer设置 3D渲染器并将其附加到 HTML 页面(通过名为app 的DOM 元素)。
我正在设置大小,使 3D 场景充满整个浏览器视口。
import * as THREE from "three";
export default function createScene() {
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);
return scene;
}
在这个函数中,我正在设置一个场景。在three.js中,场景是所有要渲染的 3D 对象的顶级容器。
import * as THREE from "three";
export default function createCamera() {
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 5;
return camera;
}
在three.js中制作 3D 模型与制作现实生活中的电影类似:你需要摄像机和光源来“拍摄”。
此功能用于设置摄像头。
注意我将相机的 Z 坐标设置为 5。这意味着相机与它要“拍摄”的物体之间有一定的距离。
import * as THREE from "three";
export default function createCube({ color, x, y }) {
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshLambertMaterial({ color });
const cube = new THREE.Mesh(geometry, material);
cube.position.set(x, y, 0);
return cube;
}
接下来是用于在我的 3D 场景中显示对象的功能。它会创建简单的 3D 立方体。我可以指定颜色和 X/Y 坐标。所有立方体的 Z 坐标默认设置为 0。
three.js中的 3D 对象由几何体和材质组成。
import * as THREE from "three";
export default function createLight() {
const light = new THREE.PointLight(0xffffff, 1, 1000);
light.position.set(0, 0, 10);
return light;
}
就像在现实生活中一样,我们需要光源才能看清 3D 场景中的任何物体,所以这个函数就是用来创建光源的。
export default function animate(callback) {
function loop(time) {
callback(time);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
}
由于我们需要一个动画 3D 模型,因此需要以每秒 60 次的频率,在一个永无止境的循环中不断渲染模型。我的动画函数接受一个回调参数,该回调函数会根据浏览器的处理能力尽可能频繁地执行。
我使用浏览器的requestAnimationFrame函数来实现这个功能,这几乎是所有浏览器游戏的标准做法。
综合起来
现在我们有了初始化渲染器和场景、创建摄像机、光源和一些立方体并为其添加动画的功能,我们终于可以用这些功能来创建我们的 3D 场景了。
import "./styles.css";
import createCube from "./createCube";
import createLight from "./createLight";
import animate from "./animate";
import createCamera from "./createCamera";
import createRenderer from "./createRenderer";
import createScene from "./createScene";
const renderer = createRenderer();
const scene = createScene();
const camera = createCamera();
const cubes = {
pink: createCube({ color: 0xff00ce, x: -1, y: -1 }),
purple: createCube({ color: 0x9300fb, x: 1, y: -1 }),
blue: createCube({ color: 0x0065d9, x: 1, y: 1 }),
cyan: createCube({ color: 0x00d7d0, x: -1, y: 1 })
};
const light = createLight();
for (const object of Object.values(cubes)) {
scene.add(object);
}
scene.add(light);
animate(() => {
renderer.render(scene, camera);
});
注意镜头、灯光和背景是如何添加到场景中的。
我们的动画函数确保场景不断重复渲染。目前来说,它几乎没什么用处,但在接下来的步骤中会变得非常重要。
以下是目前为止的项目进展:
步骤 2:添加点击处理程序
我需要能够点击 3D 物体来控制相机移动到哪里。为了实现这一点,我正在我的项目中添加对 npm 包three.interactive 的依赖。
这个库允许我们像给 HTML DOM 节点添加事件监听器一样,给 3D 对象添加事件监听器。
在我的index.js 文件开头,我添加了一个 import 语句来使用three.interactive:
import { InteractionManager } from "three.interactive";
除了渲染器、场景和摄像机之外,我还在创建一个交互管理器:
const interactionManager = new InteractionManager(
renderer,
camera,
renderer.domElement
);
如您所见,交互管理器需要能够控制渲染器、摄像机和场景渲染到的 canvas DOM 元素。
我修改了创建立方体对象并将其添加到场景中的for循环,以便在单击立方体时向控制台写入一条日志语句,看看是否有效:
for (const [name, object] of Object.entries(cubes)) {
object.addEventListener("click", (event) => {
event.stopPropagation();
console.log(`${name} cube was clicked`);
});
interactionManager.add(object);
scene.add(object);
}
注意event.stopPropagation——这是必要的,这样当对象重叠时,只有最上面的对象会处理点击事件。同样,它的工作方式与 DOM 节点上的点击事件处理程序完全相同。
我们还需要做的一件事是编辑动画循环,以便交互管理器在每次迭代时都能更新:
animate(() => {
renderer.render(scene, camera);
interactionManager.update();
});
以下是目前为止的项目进展:
当您打开此沙盒中的控制台(点击左下角的“控制台”),然后点击 3D 立方体时,您将看到我添加的点击处理程序发出的日志语句。
步骤三:移动摄像机
现在让我们把相机移动到被点击的立方体的位置。
这其实很简单——我只需要更新相机的位置,使其与被点击的立方体的 X/Y 坐标相匹配。
以下是更新后的用于创建立方体的for循环:
for (const [name, object] of Object.entries(cubes)) {
object.addEventListener("click", (event) => {
event.stopPropagation();
console.log(`${name} cube was clicked`);
const cube = event.target;
camera.position.set(cube.position.x, cube.position.y, camera.position.z);
});
interactionManager.add(object);
scene.add(object);
}
请注意,虽然相机的 X 和 Y 坐标会发生变化,但 Z 坐标保持不变——相机与它“拍摄”的物体之间仍将保持 5 个单位的距离。
以下是更新后的沙盒环境:
点击方块即可试玩!
步骤 4:补间动画
目前,点击立方体时,镜头会立即跳到立方体的位置。这虽然是朝着正确方向迈出的一步,但我们实际上希望镜头能够平滑地移动到立方体的位置(技术上讲,这叫做“平移”)。
简而言之,我们想加入一些真正的动画魔法!
什么是补间动画?
为了在动画中创建流畅的运动,我们使用一种称为中间帧渲染(或简称“tweeting”)的技术。
这种技术与动画本身一样古老,20 世纪 30 年代制作《白雪公主》的艺术家们就使用了这种技术,就像今天的动画艺术家们使用这种技术一样。
基本思路是,你有一个开始状态和一个结束状态,或者要制作动画的内容(也称为“关键帧”),然后绘制中间的所有状态,以创造逐渐变化的错觉。
请看这个弹跳球的动画:
这里有 3 个关键帧:
- 球在屏幕左上方。
- 球在底部中间。
- 球在右上角
通过添加补间动画,球在地板上弹跳的画面会显得流畅自然。如果没有补间动画,球只会断断续续地从一个地方跳到另一个地方。
JavaScript 库 Tween.js
为了使我们的相机平滑移动,我们需要使用补间动画。与交互性一样,three.js本身并不提供此功能。我们需要向项目中添加另一个 npm 包依赖项:@tweenjs/tween.js。
这个库并非专门用于three.js。当需要对某些内容进行一段时间的更改时,您可以随时使用它。
让我们在index.js 文件中添加导入语句来使用它:
import * as TWEEN from "@tweenjs/tween.js";
我在创建立方体的for循环中创建了一个补间动画,该动画会跳转到点击处理程序,当其中一个立方体被点击时触发该处理程序:
for (const [name, object] of Object.entries(cubes)) {
object.addEventListener("click", (event) => {
event.stopPropagation();
console.log(`${name} cube was clicked`);
const cube = event.target;
const coords = { x: camera.position.x, y: camera.position.y };
new TWEEN.Tween(coords)
.to({ x: cube.position.x, y: cube.position.y })
.onUpdate(() =>
camera.position.set(coords.x, coords.y, camera.position.z)
)
.start();
});
interactionManager.add(object);
scene.add(object);
}
要添加补间动画,我只需要实例化一个Tween对象。传递给构造函数的参数是我想要进行补间动画处理的数据。在我的例子中,这是一个包含 X 和 Y 坐标的对象。在补间动画开始时,这些 X/Y 坐标是相机的原始位置。
通过`to`方法,我告诉补间动画库补间动画数据的最终状态应该是什么。这将是被点击的立方体的位置。
通过onUpdate方法,我决定如何使用正在被补间动画处理的数据来影响我的动画。它会在每个补间动画步骤中被调用。我用它来更新摄像机的位置。
最后,start方法告诉库立即开始动画处理。
还有一件事——现在我们需要在动画循环中添加对补间动画库的更新方法的调用:
animate((time) => {
renderer.render(scene, camera);
interactionManager.update();
TWEEN.update(time);
});
最终结果
这是我们项目的最终版本:
点击立方体后,相机平滑地移动到它的位置——真棒!
待续…
本教程是我项目日志的一部分。我正在构建一个基于思维导图的社交媒体网络和协作工具。我会在后续文章中继续记录我的进展。敬请期待!
文章来源:https://dev.to/pahund/animating-camera-movement-in- Three-js-17e9


