本节你将创建你的第一个场景,这是一个简单的3D场景,如图1.7所示。
图1.7 一个包含两个标准几何体的简单3D场景
图1.7中有两个旋转的对象。这些对象称为网格。网格描述了对象的几何形状,并包含对象材质的相关信息。网格通过一些属性(例如,颜色、是否具有光泽度或透明度等)来确定对象在屏幕上呈现出的外观。
在图1.7中,我们可以看出有三个网格,如表1.1所示。
表1.1 场景中对象的概览
在接下来的几小节中我们将讲述如何做出图1.7这个效果。
每个Three.js应用程序至少需要一个相机(camera)、一个场景(scene)和一个渲染器(renderer)。 场景 是一个容器,用于容纳所有对象(网格、相机和光源), 相机 用于确定需要渲染的场景部分, 渲染器 负责将相机视野中的场景渲染到屏幕上,这三者共同构成了Three.js渲染3D场景的基本流程。
我们讲解的所有代码都可以在chapter-1/getting-started.js文件中找到。该文件的基本结构如下:
对于每个Three.js场景,你可能都需要执行这些相同的步骤。为了简化示例代码,我们会将一些通用的设置代码(如添加相机、灯光、场景等)提取到一些辅助文件中,这样每个示例文件中只需要包含与该示例相关的代码,而不需要重复编写通用的设置代码,从而使得示例代码更加简洁并专注于演示Three.js的不同特性。具体参见本章末尾。接下来我们将逐步介绍创建Three.js场景的基本步骤和组件。
首先,我们必须创建一个THREE.Scene。THREE.Scene是一个基本的容器,用于存放场景中的所有网格、光源和相机,并具有一些简单的属性(我们将在第2章中更深入地探讨这些属性):
以上代码将创建一个容器对象来保存所有的对象,然后将该场景的背景颜色设置为白色(0xffffff),并在场景中启用雾效果。启用雾效果之后,离相机越远的对象会逐渐被雾遮挡,呈现模糊的效果。
下一步是创建相机和渲染器:
在以上代码中,我们创建了一个透视相机(PerspectiveCamera),它决定了场景中的哪部分将被渲染。这里我们先不需要关注具体参数,因为我们将在第3章详细讨论这些参数。我们为相机设置x、y、z坐标位置来指定相机在三维空间中的位置,使其能够从特定的角度观察场景。相机的默认设置是朝向场景的中心点(即坐标为(0,0,0)的位置),因此我们不需要修改相机的目标点,它已经正确地指向了场景的中心。
在上述代码中,我们还创建了一个WebGLRenderer,我们将用它来渲染从相机看到的场景。现在先忽略代码涉及的其他属性,稍后我们会解释这些内容,包括如何调整颜色和处理阴影。一个有趣的部分是document.body.appendChild(renderer.domElement)。这一步将在页面上添加一个HTML画布(canvas)元素,用于显示渲染器的输出。当你使用浏览器开发者工具的“检查”(Inspector)面板查看页面时,可以看到添加到页面中的这个canvas元素,如图1.8所示。
图1.8 Three.js添加的画布
到目前为止,我们有了一个空的THREE.Scene、一个THREE.PerspectiveCamera和一个THREE.WebGLRenderer。现在我们只要向场景添加一些对象,就可以在屏幕上看到渲染的结果。不过在添加对象到场景之前,我们还需要先添加一些额外的组件:
❑ 轨道控制 :这允许你使用鼠标来旋转和平移场景。
❑ 光源 :通过添加光源,我们可以使用更高级的材质,产生阴影效果,并且整体上让场景看起来更加美观。
接下来我们先添加光源。
如果场景中没有光源,则大部分材质将被渲染成黑色。所以,为了看到我们的网格(并产生阴影),我们将在场景中添加一些光源。在本例中,我们将添加两个光源:
❑ THREE.AmbientLight:是一种基本的光源,它会以相同的强度和颜色影响场景中的所有对象,使得所有对象都能获得一些基础的光照效果。
❑ THREE.DirectionalLight:是一种方向光源,其光线平行投射到场景中。这种光源通常用来模拟太阳光或类似平行光的效果。
以下是添加光源的详细代码:
这些光线可以有多种配置方式,具体将在第3章详细解释。现在我们已经准备好了渲染场景所需的全部组件,接下来我们将添加网格。
通过以下代码,我们在场景中创建了三个网格:
这里,我们创建了一个立方体、一个环状结和地面。
所有这些网格都遵循相同的思路:
1.创建形状,即对象的几何体:THREE.BoxGeometry、THREE.TorusKnotBuffer-Geometry和THREE.PlaneBufferGeometry。
2.创建材质。本例中我们对立方体使用THREE.MeshPhongMaterial,对环状结使用THREE.MeshStandardMaterial,对地面使用THREE.MeshLam-bertMaterial。立方体的颜色是蓝色,环状结的颜色是绿色,地面的颜色是白色。更具体的细节我们将在第4章讲解。
3.在创建立方体和环状结时,我们通过设置它们的castShadow属性为true来告诉Three.js这些对象能够投射阴影,我们通过设置地面对象的receiveShadow属性为true,让地面能够接收并显示来自立方体和环状结对象的阴影。
4.最后,我们使用几何体和材质创建一个THREE.Mesh对象,并设置其位置,然后将其添加到场景中。
此时我们只需调用renderer.render(scene, camera),然后你将在屏幕上看到如图1.9所示的结果。
图1.9 几何体静态渲染的结果
如果你有以上代码的源文件(chapter-01/getting-started.js),那么你可以在编辑器中打开它,然后通过以下练习来进行实验。更改torusKnot.position.x、torusKnot.position.y和torusKnot.position.z设置,可以在场景中移动环状结(记得要在编辑器中保存文件才能应用更改)。还可以通过更改材质的color属性轻松更改网格的颜色。
现在的场景非常静态。你无法移动相机,也没有任何动画。如果我们想要给场景添加动画,那么首先需要找到一种能够在指定时间间隔内重新渲染场景的方法。在HTML5和相关JavaScript API出现之前,这主要通过使用setInterval(function, interval)函数来实现。setInterval允许我们指定一个函数,每隔指定的时间间隔(例如每100毫秒)调用所指定的函数一次。setInterval函数存在的问题是它不考虑浏览器中实际发生的情况。例如,你已经切换到浏览器的其他标签页了,这个函数仍然会每隔几毫秒触发一次。setInterval函数定时执行的任务并不会与屏幕重绘同步。这可能导致较高的CPU使用率、闪烁等问题,并且性能较差。
幸运的是,现代浏览器提供了更好的方法:requestAnimationFrame函数。
使用requestAnimationFrame,你可以指定一个函数在一段时间间隔后被调用。然而,你不需要定义这个时间间隔,它由浏览器定义。你只需要在你所提供的函数中执行任何绘图操作,而浏览器将确保以尽可能平滑和高效的方式进行绘制。使用它很简单。我们只需要添加以下代码:
在上述的animate函数中,我们再次调用了requestAnimationFrame,以保持动画持续进行。我们在代码中唯一需要更改的是,在创建完整场景后,不再直接调用renderer.render,而是调用一次animate()函数来启动动画。如果你现在运行代码,你将看到与之前示例没有任何区别,因为我们还没有往animate()函数中添加更新场景的逻辑。然而,在添加更新场景的逻辑之前,我们将介绍一个叫作stats.js的小助手库,该库可以提供关于动画运行帧率的信息。该库由Three.js的同一作者开发,它可以在浏览器中渲染一个显示场景渲染帧率信息的小图表。
要添加这些统计信息,我们只需要导入正确的模块并将其添加到我们的页面中:
如果你只添加以上代码,那么你会在屏幕左上角看到一个统计计数器,但不会有任何其他效果。原因是我们还需要在requestAnimationFrame循环中通知这个元素需要更新。对此,我们只需在animate函数中添加以下内容:
添加完以后,如果你打开chapter-1/getting-started.html示例,你将看到屏幕左上角显示了帧率(FPS)计数器,如图1.10所示。
图1.10 FPS统计
在这个chapter-1/getting-started.html示例中,你已经可以看到环状结和立方体围绕其轴线移动。接下来我们将解释如何通过扩展animate()函数来实现这一点。
通过使用requestAnimationFrame并配置好统计信息,我们就有了一个放置动画代码的地方。我们所需要做的就是将以下内容添加到animate()函数中:
看起来很简单,不是吗?以上代码所做的是每次调用animate()函数时,我们将每个轴的旋转属性增加0.01,从而使得网格对象平滑地绕其所有轴旋转。如果我们改变网格对象的位置属性,网格对象就可以在场景中移动,而不仅仅是旋转:
现在我们已经通过改变立方体的旋转属性来让它旋转,我们也要改变它在场景中的位置(position)属性,让它移动起来。我们希望立方体能够以优美、平滑的曲线从场景中的一个点弹到另一个点。为此,我们需要同时更改它在 x 轴和 y 轴上的位置。我们可以结合使用Math.cos和Math.sin函数与step变量来生成平滑的轨迹,创建更加逼真的动画效果。这里我就不详细介绍具体的工作原理了。这里你只需要知道step+=0.04定义了弹跳球的速度。如果你想亲自体验这个效果,可以打开chapter-1/geometries.js文件,取消animate()函数中对相关代码的注释。然后你将在屏幕上看到类似于图1.11的立方体在场景中跳跃的效果:
现在如果你尝试用鼠标移动场景,可能不会发生什么明显的变化。这是因为我们将相机放置在一个固定的位置,并且没有在动画循环中更新它的位置。当然,我们可以像之前对立方体的位置那样手动更新相机的位置,但是Three.js提供了一些控件,可以让我们更轻松地实现相机视角的调整,而不需要手动更新相机位置。就这个示例而言,我们将介绍THREE.OrbitControls。使用这些控件,你可以使用鼠标在场景中移动相机,以查看不同的对象。我们需要做的就是创建这些控件的新实例,将它们附加到相机上,并从我们的动画循环中调用更新(update)函数:
图1.11 立方体跳跃
现在你可以使用鼠标在场景中导航了。这个功能已经在chapter-1/getting-started.html示例中启用了,你可以使用该示例来体验实际效果,如图1.12所示。
最后我们将在基本场景中添加一个元素。在处理3D场景、动画、颜色和属性时,我们往往需要进行一些实验才能获得正确的颜色、动画速度或材质属性。如果我们有一个简单的GUI(图形用户界面),可以实时调整这些属性,那么将会非常方便。幸运的是,我们真的有这么一个简单的GUI!
图1.12 使用轨道控件来缩放场景
在前面的例子中,我们为环状结和立方体添加了一点动画。现在我们将创建一个简单的UI元素来控制旋转和移动的速度。为此,我们将使用来自https://lil-gui.georgealways.com/的lil-gui库。这个库可以让我们快速创建一个简单的控制UI,从而更容易进行场景实验。我们可以使用以下方式添加它:
在以上代码中,我们创建了一个新的控制元素(new GUI()),并配置了两个控件:cubeSpeed和torusSpeed。在每个动画步骤中,我们只需要查找cubeSpeed和torusSpeed当前值,然后使用这些值来旋转立方体和环状结。现在通过使用lil-gui库,我们可以在浏览器中实时调整立方体和环状结的旋转速度,而不需要频繁地在浏览器和编辑器之间进行切换。在本书的大多数示例中,我们都会提供这个UI,以便你可以轻松地尝试不同的材质、光源和其他Three.js对象提供的不同属性。在图1.13中,你可以在屏幕的右上角看到用于控制场景的控件。
图1.13 使用控件修改场景属性
在进入本章最后一节之前,我们先简单概述一下到目前为止我们讲述过的内容。你可以想象得出,大多数场景基本上都需要类似的设置。它们都需要一些光源、相机、场景,也许还需要一个地面。为了避免每个示例都要添加所有这些内容,我们将大多数这些常见元素外部化到了一组辅助库中。通过这种方式,我们可以保持示例的简洁性,只展示与该示例相关的代码。如果你对具体细节感兴趣,可以查看bootstrap文件夹中的文件。
在之前的示例中,我们在场景中渲染了一些简单的网格,并直接指定了它们的位置。然而,有时候很难确定网格的位置,或者我们应该把它们旋转多少度。对此,Three.js提供了一些辅助工具,为你提供场景相关的附加信息。在下一节中,我们将介绍其中的一些辅助工具。