关于three.js:👋-和我一起学Threejs初级篇3-掌握摄影机

🛎 本文为《和我一起学 Three.js 系列》高级篇的第四篇文章,文中的示例代码基于上一篇文章中的代码进行相应的扩大,请确保您曾经浏览上一篇文章,并和我一起应用 vite 搭建了古代前端开发环境。

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 会渲染各个摄影机视角下的场景,并将它们合并在画布中,例如这个示例展现了不同角度的摄影机察看同一个物体的成果:
  • 平面摄影机[StereoCamera](https://threejs.org/docs/?q=StereoCamera#api/en/cameras/StereoCamera)):能够同时生成两个相机对象,一个渲染左眼图像,一个用于渲染右眼图像。这就很适宜在 VR,AR,3D 视频等利用中产生更加实在的 3D 成果(_由 StereoCamera 生成的 VR 成果能够通过 Oculus Rift,HTC Vive 等 VR 头显设施观看_)。
  • 立方体摄影机[CubeCamera](https://threejs.org/docs/?q=CubeCamera#api/en/cameras/CubeCamera)):用于生成「环境贴图(Environment Map)」,它会生成一个立方体场景,捕获场景内的环境信息,别离在 6 个面各渲染一次,而后将渲染后果用于物体的反射贴图,折射贴图,从而加强场景的真实感和细节成果。

💡「环境贴图」是一种罕用于加强场景真实感的技术,它通过将场景中周围环境信息捕获下来,并将其利用于物体外表的材质中,从而使物体看起来更加实在和具备反射性。具体来说,环境贴图是一张蕴含了场景中周围环境的图像,通常为立方体贴图。它能够捕捉到场景中的反射、折射、漫反射、全局光照等信息,使得物体的外表看起来更加实在,同时也能够加强场景的光照成果。

  • 正交摄影机[OrthographicCamera](https://threejs.org/docs/?q=Ortho#api/en/cameras/OrthographicCamera)):将会渲染一个没有透视成果的场景,无论物体和摄影机的间隔如何,物体的大小都不会放生变动。能够通过该摄影机创立 RTS 类游戏,例如帝国时代。
  • 透视摄影机[PerspectiveCamera](https://threejs.org/docs/?q=Camera#api/en/cameras/PerspectiveCamera)):这是 Three.js 中最罕用的摄影机,它能够模仿人眼察看物体时近大远小的透视成果。

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

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;

能够看到,尽管咱们在上一章中只创立了一个立方体,然而因为咱们有三个不同角度的摄影机,最终画布上出现了三个旋转的立方体。

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()

至此,咱们终于取得设施与物体交互的能力!但这仍然还有一个问题,因为每次鼠标挪动时,都是同时扭转摄影机的 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()

让咱们先看看这段代码的成果:

十分完满 🙌 咱们做了什么?能够看到,咱们应用了 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)):该控制器会创立一个轨迹球,并容许用户通过鼠标或触摸的形式,放大/放大(滚轮)/旋转(鼠标)指标对象,官网的这个示例,十分清晰地表明了它的作用:
  • 拖拽控制器[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/<your 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)」,它能够使咱们简略的几何体变成事实中咱们相熟的实在物体,敬请期待!

6. 参考资料

  • 一文读懂正交投影变换:https://zhuanlan.zhihu.com/p/473031788
  • Three.js 官网:https://threejs.org/
  • three.js journey:https://threejs-journey.com/

7. 应用到的工具

  • 截屏:Xnip;
  • 屏幕录制:QuickTime Player;
  • 视频转 GIF 图片:video-to-gif;

8. 💰 反对创作

您有很多形式能够表白您喜爱这篇文章,并违心反对我继续创作,例如:

  • 点击各类平台「喜爱」按钮;
  • 将文章转发在各类您喜爱的平台,并为它写一份简短的举荐语;
  • 在评论区留言;
  • 关注我的集体公众号「前端乱步」;

无论您抉择哪一项,我都会因为您的观赏而感到愉悦。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理