和我一起学 Three.js【初级篇】:3. 掌握摄影机

欢迎关注「前端乱步」公众号,我会在此分享 Web 开发技术,前沿科技与互联网资讯。

0. 系列文章合集

本系列第 6,7 章节支持微信公众号内付费观看,将在全系列文章点赞数+评论数 >= 500, 1000 时分别解锁发布。
  1. 《0. 总论》
  2. 《1. 搭建 3D 场景》
  3. 《2. 掌握几何体》
  4. 您当前在这里 《3. 掌握摄影机》
  5. 《4. 掌握纹理》
  6. 《5. 掌握材质》
  7. 《6. 掌握光照》
  8. 《7. 掌握阴影》
  9. 《8. 融会贯通,神功小成》(将于 2023.4.17 更新,敬请期待)

    1. 什么是摄影机?

    在 Three.js 中,摄影机(Camera)用来定义场景中的「视角」以及「可见范围」(_为了提升渲染性能,超出可见范围的物体将不会被渲染,这被称为「视锥体剔除(frustum culling)」技术_)。除此之外,摄影机实际上还控制着场景中物体的「位置」和「大小」,通过移动摄影机,GPU 会渲染出符合直觉的物体透视效果,从而让我们有优秀的 3D 场景体验。

因此,掌握 Three.js 提供的各种摄影机以及控制摄影机的各种方法,能够使我们的 3D 场景和用户进行互动并极大的丰富场景的呈现方式。

掌握本章节的概念和内容非常重要,请您务必保持耐心,动手实践。

2. 摄影机的种类

在 Three.js 中,所有摄影机都封装为特定的子类,它们统一继承自一个抽象类:[Camera](https://threejs.org/docs/?q=camera#api/en/cameras/Camera)。Three.js 提供的摄影机有:

  • 数组摄影机[ArrayCamera](https://threejs.org/docs/?q=camera#api/en/cameras/ArrayCamera)):主要用于需要渲染多个视角的场景,例如 VR,AR,多屏幕游戏等。它通过将多个摄影机实例放入数组并传入 ArrayCamera 中,GPU 会渲染各个摄影机视角下的场景,并将它们合并在画布中,例如这个示例展示了不同角度的摄影机观察同一个物体的效果:

和我一起学 Three.js【初级篇】:3. 掌握摄影机_第1张图片

  • 立体摄影机[StereoCamera](https://threejs.org/docs/?q=StereoCamera#api/en/cameras/StereoCamera)):可以同时生成两个相机对象,一个渲染左眼图像,一个用于渲染右眼图像。这就很适合在 VR,AR,3D 视频等应用中产生更加真实的 3D 效果(_由 StereoCamera 生成的 VR 效果可以通过 Oculus Rift,HTC Vive 等 VR 头显设备观看_)。

和我一起学 Three.js【初级篇】:3. 掌握摄影机_第2张图片

  • 立方体摄影机[CubeCamera](https://threejs.org/docs/?q=CubeCamera#api/en/cameras/CubeCamera)):用于生成「环境贴图(Environment Map)」,它会生成一个立方体场景,捕捉场景内的环境信息,分别在 6 个面各渲染一次,然后将渲染结果用于物体的反射贴图,折射贴图,从而增强场景的真实感和细节效果。
环境贴图」是一种常用于增强场景真实感的技术,它通过将场景中周围环境信息捕捉下来,并将其应用于物体表面的材质中,从而使物体看起来更加真实和具有反射性。具体来说,环境贴图是一张包含了场景中周围环境的图像,通常为立方体贴图。它可以捕捉到场景中的反射、折射、漫反射、全局光照等信息,使得物体的表面看起来更加真实,同时也可以增强场景的光照效果。

和我一起学 Three.js【初级篇】:3. 掌握摄影机_第3张图片

  • 正交摄影机[OrthographicCamera](https://threejs.org/docs/?q=Ortho#api/en/cameras/OrthographicCamera)):将会渲染一个没有透视效果的场景,无论物体和摄影机的距离如何,物体的大小都不会放生变化。可以通过该摄影机创建 RTS 类游戏,例如帝国时代。

和我一起学 Three.js【初级篇】:3. 掌握摄影机_第4张图片

  • 透视摄影机[PerspectiveCamera](https://threejs.org/docs/?q=Camera#api/en/cameras/PerspectiveCamera)):这是 Three.js 中最常用的摄影机,它可以模拟人眼观察物体时近大远小的透视效果。

和我一起学 Three.js【初级篇】:3. 掌握摄影机_第5张图片

请注意透视摄影机与正交摄影机在渲染立方体时的不同,在正交摄影机中,所有立方体的尺寸是相似的。

3. 创建摄影机

下面将进入代码实践环节,请务必确保您已阅读之前的文章,和我拥有相同的开发环境!

本节我们不会涵盖「立方体摄影机」和「立体摄影机」,因为前者涉及我们下一章的内容,我会放在一起介绍。而后者由于需要额外设备,对于大多数人难以观察结果,因此略去不提。

3.1 透视摄像机

让我们首先介绍最常用的透视摄像机,创建一个透视摄像机非常简单,只需要实例化 PerspectiveCamera 类,并传入对应的参数:

const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 1, 100)

PerspectiveCamera 函数接收四个参数,从前到后依次为:

  • 视场角(FOV):表示视场角的大小,单位是「度(degree)」。视场角越大,摄像机所能看到的东西就越多,但是画面中物体的尺寸也会变得更小。通常 pov 取值在 4575 之间。
「视场角」是摄像机能够看到的视角的大小。它以角度为单位,表示摄像机能够看到的场景的宽度。
  • 宽高比(Aspect Ratio):表示画面的宽度与高度的比例(这是因为在视角中,物体在视平面上的大小与距离相互关联。如果宽高比不正确,那么最终渲染出的画面将会变形或扭曲。);
  • 近裁切面(Near Plane):表示摄影机能够看到的最近距离,如果物体比这个距离还要近,它就不会出现在画面中;
  • 远裁切面(Far Plane):表示摄影机能够看到的最远距离,如果物体比这个距离还要远,它就不会出现在画面中;

想要操作摄影机,仅仅实例化一个摄影机对象是不够的,我们还需要额外两个步骤(您应该在之前的代码已经做过了):

  1. 将摄影机添加至场景scene.add(camera)
  2. 将摄影机添加至渲染器中renderer.render(scene, camera)

3.2 数组摄像机

我们方才提过,数组相机允许用户在画布中同时出现多台摄影机视角下的渲染结果,它的使用也非常符合直觉,我们只需创建多个摄影机,并制定不同摄影机的位置,然后放入数组,传入 ArrayCamera 对象即可:

const camera1 = new THREE.PerspectiveCamera(
  75,
  sizes.width / sizes.height,
  0.1,
  10
);
camera1.position.set(0, 0.5, 3);
camera1.lookAt(0, 0, 0);
const camera2 = new THREE.PerspectiveCamera(
  75,
  sizes.width / sizes.height,
  0.1,
  10
);
camera2.position.set(0.5, -0.5, 2.5);
camera2.lookAt(0, 0, 0);
const camera3 = new THREE.PerspectiveCamera(
  75,
  sizes.width / sizes.height,
  0.1,
  10
);
camera3.position.set(-0.5, -0.5, 2);
camera3.lookAt(0, 0, 0);

const arrayCamera = new THREE.ArrayCamera([camera1, camera2, camera3]);
arrayCamera.position.z = 3;

和我一起学 Three.js【初级篇】:3. 掌握摄影机_第6张图片

可以看到,虽然我们在上一章中只创建了一个立方体,但是由于我们有三个不同角度的摄影机,最终画布上呈现了三个旋转的立方体。

3.3 正交摄像机

正交摄像机是一种平行投影(Parallel Projection)相机,因此我们需要指定投影屏幕的尺寸:

const aspectRatio = sizes.width / sizes.height
const camera = new THREE.OrthographicCamera(- 1 * aspectRatio, 1 * aspectRatio, 1, - 1, 0.1, 100)

OrthographicCamera 函数接收六个参数,从前到后依次为:

  • left:摄像机能够看到的最左边的距离;
  • right:摄像机能够看到的最右边的距离;
  • top:摄像机能够看到的最上边的距离;
  • bottom:摄像机能够看到的最下边的距离;
  • 近裁切面(Near Plane):表示摄影机能够看到的最近距离,如果物体比这个距离还要近,它就不会出现在画面中;
  • 远裁切面(Far Plane):表示摄影机能够看到的最远距离,如果物体比这个距离还要远,它就不会出现在画面中;

4. 操作摄影机

将摄影机放置在场景中,让物体得以被看见似乎并不那么令人激动,我们更想要的是和物体产生互动,例如通过鼠标移动物体或旋转整个场景。而这一切都是由操作摄影机实现的,在本章最后的一小节中,我们将学习这一技术。

4.1 手动实现

既然我们希望通过移动鼠标操作物体,首先我们需要获取鼠标的位置,好在这并不困难:

const cursor = {
    x: 0,
    y: 0,
}

window.addEventListener('mousemove', (event) => {
    cursor.x = event.clientX / sizes.width - 0.5
    cursor.y = - (event.clientY / sizes.height - 0.5)
})

4.1.1 思考题

  • 在上面的代码中,为什么 cursor.y 的值要取负值?

欢迎在评论区与我留言讨论!


在上面的代码中,我们监听 mousemove 事件,并及时通过 event.clientX 属性更新鼠标的 xy 坐标,它们表示鼠标距离视窗左上角的位置。

您应该注意到我们代码中的一些「小花招」,这样做的目的在于我们希望让鼠标的坐标值保持在 01 之间,这样,当 x 坐标值为屏幕的一半时,计算值刚好为 0.5,鼠标位于屏幕中心位置。这和我们在 Three.js 场景中的坐标系统相对应。

既然我们获取了鼠标的位置,并标准化了它的单位,下一步即是在每次渲染时,根据鼠标位置调整摄影机的位置:

const animate = () => {
    // ...
    camera.position.x = cursor.x * 10
    camera.position.y = cursor.y * 10 // 为了让效果更明显,我们乘以一个常量系数
    camera.lookAt(mesh.position)
    // ...
}

animate()

和我一起学 Three.js【初级篇】:3. 掌握摄影机_第7张图片

至此,我们终于获得设备与物体交互的能力!但这依然还有一个问题,由于每次鼠标移动时,都是同时改变摄影机的 xy 坐标,这使得我们的立方体会随着鼠标移动忽大忽小,这可能不是我们想要的,如果我们想要固定朝一个方向移动立方体,我们该怎么做呢?
答案是使用一些简单的几何知识,例如「三角函数」,代码如下:

const animate = () => {
    // ...
    camera.position.x = Math.sin(cursor.x * Math.PI * 2) * 2
    camera.position.z = Math.cos(cursor.x * Math.PI * 2) * 2
    camera.position.y = cursor.y * 3
    camera.lookAt(mesh.position)
    // ...
}

animate()

让我们先看看这段代码的效果:

和我一起学 Three.js【初级篇】:3. 掌握摄影机_第8张图片

非常完美 我们做了什么?可以看到,我们使用了 Math.sin()Math.cos() 方法设置了摄影机「左右方向」的 x 坐标与表示「前后方向」的 z 坐标值。我们知道 Math.sin()Math.cos() 接收弧度值,并始终返回 1-1 间的任意实数。这里的 Math.PI * 2 (一个完整的圆周弧度值是 )起到了将角度值转换为弧度值的作用,由此我们可以得到一个 0 之间的弧度值。

您可能会困惑,为什么对于 x 坐标我们使用正弦值,而对于 z 坐标我们却使用余弦值。这是因为如果我们在 x 轴和 z 轴上都使用正弦函数或余弦函数,摄影机的移动路径将会是沿着某个斜线方向移动,表现为物体会整体放大或缩小。而通过分别使用正弦与余弦值,我们可以让摄影机在向「右」移动的同时,向「前」移动,这样就会形成一个「环轨」的效果,我们的摄影机会在鼠标移动时,像是在围绕着物体旋转。

正弦函数与余弦函数的运用在 Three.js 中非常常见,请保障您真的理解本章所讲述的内容。

4.2 使用控制器

我们是否每次都需要通过代码控制摄影机的位置呢?非常幸运,答案是否定的。Three.js 提供了一系列称之为「控制器Controls)」的对象,让开发者可以快速,轻松地控制摄像机的位置和视角。这些控制器包括:

  • 弧球控制器[ArcballControls](https://threejs.org/docs/?q=Control#examples/en/controls/ArcballControls)):该控制器会创建一个轨迹球,并允许用户通过鼠标或触摸的方式,放大/缩小(滚轮)/旋转(鼠标)目标对象,官网的这个示例,非常清晰地表明了它的作用:

和我一起学 Three.js【初级篇】:3. 掌握摄影机_第9张图片

  • 拖拽控制器[DragControls](https://threejs.org/docs/?q=Control#examples/en/controls/DragControls)):该控制器实际上和摄影机无关,它接收一个物体组成的数组,并提供数组内物体拖放功能,官网的这个示例提供了一大堆立方体供您体验拖拽的乐趣!;
  • 第一人称视角控制器[FirstPersonControls](https://threejs.org/docs/#examples/en/controls/FirstPersonControls)):如果您玩过反恐精英等射击游戏,看到这个名称,您可能会感到激动,没错,该控制器提供了实现第一人称视角的效果,它类似于下面将介绍的飞行控制器,但附加了一个固定向上的轴;
  • 飞行控制器[FlyControl](https://threejs.org/docs/#examples/en/controls/FlyControls)):该控制器允许用户通过键盘和鼠标来控制摄像机的飞行。用户可以使用 FlyControls 来模拟飞机或者直升机的飞行,或者让摄像机在场景中自由移动;
  • 轨道控制器[OrbitControls](https://threejs.org/docs/?q=Control#examples/en/controls/OrbitControls)):该控制器可以实现我们之前手动实现的效果,更进一步,它允许用户使用鼠标左键围绕一个点旋转,使用鼠标右键横向平移,并使用滚轮放大或缩小;
  • 指针锁定控制器[PointerLockControls](https://threejs.org/docs/?q=Control#examples/en/controls/PointerLockControls)):通过使用 Pointer Lock API,可以隐藏鼠标指针,并在 mousemove 事件回调中不断发送移动事件。通过该 API,用户可以在浏览器中创建 FPS 游戏;
  • 轨迹球控制器[TrackballControls](https://threejs.org/docs/?q=Control#examples/en/controls/TrackballControls)):轨迹球控制器类似轨道控制器,但不限制垂直角度的旋转。即使场景颠倒,您也可以继续旋转并旋转相机;
  • 变换控制器[TransformControls](https://threejs.org/docs/#examples/en/controls/TransformControls)):和拖拽控制器一样,这个控制器与摄影机无关。它允许用户在 Three.js 场景中对场景中的 3D 对象进行变换操作,包括平移、旋转、缩放等。它通常应用于编辑器场景。

4.2.1 引入控制器

使用控制器非常简单,首先,您需要在 three/addons/controls/.js 路径下引入控制器,然后初始化该控制器即可:

const controls = new OrbitControls(camera, canvas)
注意,不同的控制器的参数和调用方式可能不同,您需要根据文档酌情处理。

对于一些控制器,您需要在动画函数中使用 update() 方法更新控制器。

4.2.2 优化控制器

默认情况下,摄影机注视着场景的正中心,我们可以通过修改控制器 target 属性改变摄像头的位置,在修改完成后,需要调用 udpate()方法:

controls.target.y = 1
controls.update()

为了使摄影机的变化更加自然,我们可以通过配置 enableDamping 属性,让动画更加平滑自然。

const controls = new OrbitControls(camera, canvas)
controls.enableDamping = true
注意,并非所有控制器都有该属性!

5. 总结

大功告成 !在本篇文章中,我向您介绍了 Three.js 中绝大多数关于「摄影机(Camera)」的概念和用法,通过恰当的使用摄影机和控制器,我们可以轻松地创建可交互的 3D 场景。这会让我们的 3D 世界更具吸引力!恭喜您又掌握了 3D Web 世界的一个重要概念,在下一篇文章中,我会想您介绍一个令人兴奋的主题「纹理(Textures)」,它可以使我们简单的几何体变成现实中我们熟悉的真实物体,敬请期待!

你可能感兴趣的:(和我一起学 Three.js【初级篇】:3. 掌握摄影机)