IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    libinfs发表于 2023-03-13 10:00:00
    love 0
    🛎 本文为《和我一起学 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 取值在 45 到 75 之间。
    💡 「视场角」是摄像机能够看到的视角的大小。它以角度为单位,表示摄像机能够看到的场景的宽度。
    • 宽高比(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 属性更新鼠标的 x,y 坐标,它们表示鼠标距离视窗左上角的位置。

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

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

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

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

    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 (一个完整的圆周弧度值是 2π)起到了将角度值转换为弧度值的作用,由此我们可以得到一个 0 到 2π 之间的弧度值。

    您可能会困惑,为什么对于 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. 💰 支持创作

    您有很多方式可以表达您喜欢这篇文章,并愿意支持我持续创作,例如:

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

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



沪ICP备19023445号-2号
友情链接