第2章
构建Three.js应用的基本组件
在第1章中我们介绍了Three.js库的基础知识。通过示例展示了Three.js是如何工作的,然后创建了第一个完整的Three.js应用。在本章中我们将会深入了解Three.js库,介绍构建Three.js应用的基本组件。通过本章你将了解以下内容:
- 在Three.js应用中使用的主要组件。
- THREE.Scene对象的作用。
- 几何图形和网格是如何关联的。
- 正交投影摄像机和透视投影摄像机的区别。
我们首先来介绍如何创建场景并添加对象。
2.1 创建场景
在第1章中我们已经创建了一个THREE.Scene,想必你已经了解了Three.js库的基础知识。我们可以看到,一个场景想要显示任何东西,需要表2.1所示三种类型的组件。
THREE.Scene对象是所有不同对象的容器,但这个对象本身没有那么多的选项和函数。
THREE.Scene对象有时被称为场景图,可以用来保存所有图形场景的必要信息。在Three.js中,这意味着THREE.Scene保存所有对象、光源和渲染所需的其他对象。有趣的是,场景图,顾名思义,不仅仅是一个对象数组,还包含了场景图树形结构中的所有节点。每个你添加到Three.js场景的对象,甚至包括THREE.Scene本身,都是继承自一个名为THREE.Object3D的对象。一个THREE.Object3D对象也可以有自己的子对象,你也可以使用它的子对象来创建一个Three.js能解释和渲染的对象树。
2.1.1 场景的基本功能
了解一个场景功能的最好方法就是看示例。在本章的源代码中,你可以找到一个名为 01-basic-scene.html的例子。我将使用这个例子来解释一个场景所拥有的各种方法和选项。当我们在浏览器中打开这个示例的时候,其效果大致如图2.1所示。请记住除了鼠标之外,键盘上的A、S和D键也可用于在渲染场景中转向、缩放和平移。
这跟我们在第1章中看到的例子非常像。尽管这个场景看上去有点儿空荡荡,但其实它已经包含了好几个对象。通过下面的源代码可以看到,我们使用THREE.Scene对象的scene.add(object)方法添加了一个THREE.Mesh对象(你看到的平面)、一个THREE.SpotLight对象(聚光灯光源)和一个THREE.AmbientLight对象(环境光)。渲染场景的时候,THREE.Camera对象会自动地被Three.js添加进来。但是我们手动添加它会是一个更好的实践,尤其是当你需要处理多个摄像机的时候。部分源码如下:
在深入了解THREE.Scene对象之前,先说明一下你可以在这个示例中做些什么,然后我们再来看代码。在浏览器中打开01-basic-scene.html示例,看看右上角的那些控件。如图2.2所示。
通过这些控件,你可以往场景中添加方块、移除最后一个添加到场景中的方块以及在浏览器控制台中显示当前场景中的所有对象。控件区的最后一项显示了当前场景中所有对象的数量。你可能会发现场景在启动的时候就已经包含了4个对象,它们是地面、环境光、点光源和我们之前提到的摄像机。让我们从最简单的addCube方法开始逐一了解控件区的所有方法。
现在这段代码应该很容易读懂,因为这里没有引入很多新的概念。当你点击addCube按钮的时候,一个新的THREE.BoxGeometry对象就会被创建,它的长、宽和高都是一个从1到3之间的随机数。除了尺寸是随机的,这个方块的颜色和位置也是随机的。
这段代码里的新东西是我们使用name属性为整个方块指定了一个名字。方块的名字是在cube后面加上当前场景中对象的数量(即scene.children.length)。给对象命名在调试的时候是很有用的,而且还可以直接通过名字来获取场景中的对象。如果使用Three.Scene.getObjectByName(name)方法,可以直接获取场景中名为name的对象,然后可以执行一些比如改变位置的操作。你或许想知道最后一行代码的作用,我们的控制界面就是使用numberOfObjects变量来显示场景中对象数量的。所以,无论什么时候添加或者删除对象,我们都要将该变量设置为更新后的数量。
在控制界面中能够调用的另一个方法是removeCube()。顾名思义,点击removeCube按钮,将会移除最后一个添加到场景中的方块。代码实现如下所示:
在场景中添加对象使用的是add()方法,而从场景中移除对象就要使用remove()方法。由于Three.js将子对象保存在数组中(最新的对象保存在数组的最后),所以我们可以使用THREE.Scene对象的children属性来获取最后一个添加到场景中的对象,children属性将场景中的所有对象存储为数组。在移除对象时,我们还需要检查该对象是不是THREE.Mesh对象,这样做的原因是避免移除摄像机和光源。当我们移除对象之后,需要再次更新控制界面中表示场景中对象数量的属性numberOfObjects。
控制界面上的最后一个按钮的标签是outputObjects。你或许已经发现在点击该按钮后什么都没有发生。因为这个按钮的功能是在浏览器的控制台中打印出场景中的所有对象信息,如图2.3所示。
我们使用的是内置的console对象在浏览器控制台日志中输出对象信息,代码如下所示:
这样做对于代码调试是非常有用的,尤其是当你为对象命名时。它对查找某个特定对象相关的问题是非常有用的。例如,对象cube-17(如果你已经知道对象的名字,就可以使用console.log(scene.getObjectByName("cube-17"))来输出对应的信息。输出结果如图2.4所示。
目前为止,我们已经学习了如下一些和场景相关的方法:
- THREE.Scene.Add:用于向场景中添加对象
- THREE.Scene.Remove:用于移除场景中的对象
- THREE.Scene.children:用于获取场景中所有的子对象列表
- THREE.Scene.getObjectByName:利用name属性,用于获取场景中特定的对象
这些方法是和场景相关的重要方法,通常情况下这些方法就可以满足大部分需求了。但是,还有几个辅助方法可能会被用到,请看下面的示例代码片段。
如你在第1章中所看到的,我们使用了render循环来渲染场景。该循环代码如下所示:
在这里,我们使用了THREE.Scene.traverse()方法。我们可以将一个方法作为参数传递给traverse()方法,这个传递来的方法将会在每一个子对象上执行。由于THREE.Scene对象存储的是对象树,所以如果子对象本身还有子对象,traverse()方法会在所有的子对象上执行,直到遍历完场景树中的所有对象为止。
我们使用render()方法来更新每个方块的旋转状态(我们特意忽略了表示地面的plane对象)。我们还可以使用for循环或者forEach来遍历children属性数组来达到同样的目的,因为只向THREE.Scene增加对象且没有创建嵌套结构。
在我们深入讨论THREE.Mesh和THREE.Geometry对象之前,先来介绍THREE.Scene对象的两个属性:fog(雾化)和overrideMaterial(材质覆盖)。
2.1.2 给场景添加雾化效果
使用fog属性就可以为整个场景添加雾化效果。雾化效果是:场景中的物体离摄像机越远就会变得越模糊。如图2.5所示。
为了更好地观察雾化效果,可以利用鼠标推进或拉远摄像机,这样就可以观察物体是如何受雾化效果的影响。在Three.js中为场景添加雾化效果是很简单的,在定义完场景后只要添加如下代码即可:
我们在这里定义一个白色雾化效果(0xffffff)。后面的两个参数是用来调节雾的显示,0.015是near(近处)属性的值,100是far(远处)属性的值。通过这两个属性可以决定雾化开始和结束的地方,以及加深的程度。使用THREE.Fog创建的对象,雾的浓度是线性增长的,除此之外还有另外一种添加雾化效果的方法,定义如下:
在这个方法中不再指定near和far属性,只需要设置雾的颜色(0xffffff)和浓度(0.01)即可。需要注意的是,该方法中雾的浓度不再是线性增长的,而是随着距离呈指数增长。
2.1.3 使用overrideMaterial属性
我们要介绍的最后一个场景属性是overrideMaterial。当设置了overrideMaterial属性后,场景中所有的物体都会使用该属性指向的材质,即使物体本身也设置了材质。当某一个场景中所有物体都共享同一个材质时,使用该属性可以通过减少Three.js管理的材质数量来提高运行效率,但是实际应用中,该属性通常并不非常实用。该属性的使用方法如下所示:
使用了上述代码中显示的overrideMaterial属性之后,场景的渲染结果如图2.6所示。
从图中可以看出,所有的立方体都使用相同的材质和颜色进行渲染。在这个示例中,我们用的材质是THREE.MeshLambertMaterial,而且使用该材质类型,还能够创建出不发光但是可以对场景中的光源产生反应的物体。在第4章中你将会学到更多关于这种材质的内容。
在本节中我们介绍了Three.js中最核心的概念:场景。关于场景我们需要记住的是:它是在渲染时你想使用的所有物体、光源的容器。表2.2列出了THREE.Scene中最常用的方法和属性。
在下一节中,我们将会对可以添加到场景中的物体作详细介绍。
2.2 几何体和网格
在前面的例子中我们已经使用了几何体和网格。比如在向场景中添加球体时,代码如下所示:
我们使用THREE.SphereGeometry定义了物体的形状、使用THREE.MeshBasicMaterial定义了物体的外观和材质,并将它们合并成能够添加到场景中的网格(THREE.Mesh)。在这一节中将进一步介绍什么是几何体和网格。
2.2.1 几何体的属性和方法
Three.js提供了很多可以在三维场景中使用的几何体。图2.7是04-geometries示例的截图,该图展示了Three.js库中可用的标准几何体。
在第5章和第6章中我们将会深入讨论Three.js提供的所有基本几何体和高级几何体。在这一节中,我们主要介绍什么是几何体。
像其他大多数三维库一样,在Three.js中几何体基本上是三维空间中的点集(也被称作顶点)和将这些点连接起来的面。以立方体为例:
- 一个立方体有8个角。每个角都可以用x、y和z坐标点来定义,所以每个立方体在三维空间中都有8个点。在Three.js中,这些点称为顶点。
- 一个立方体有6个面,每个角有一个顶点。在Three.js中,每个面都是包含3个顶点的三角形。所以,立方体的每个面都是由两个三角形面组成的。
当你使用Three.js库提供的几何体时,不需要自己定义几何体的所有顶点和面。对于立方体来说,你只要定义长、宽、高即可。Three.js会基于这些信息在正确的位置创建一个拥有8个顶点和12个三角形面的立方体。尽管可以使用Three.js提供的几何体,但是你仍然可以通过定义顶点和面来自定义创建几何体。创建几何体的方法如下代码所示:
上述代码展示了如何创建简单的立方体。在vertices数组中保存了构成几何体的顶点,在faces数组中保存了由这些顶点连接起来创建的三角形面。如new THREE.Face3(0,2,1)就是使用vertices数组中的点0、2和1创建而成的三角形面。需要注意的是创建面的顶点时的创建顺序,因为顶点顺序决定了某个面是面向摄像机还是背向摄像机的。如果你想创建面向摄像机的面,那么顶点的顺序是顺时针的,反之顶点的顺序是逆时针的。
在这个示例中,我们使用THREE.Face3元素定义立方体的6个面,也就是说每个面都是由两个三角形面组成的。在Three.js以前的版本中,可以使用四边形来定义面。到底是使用四边形还是三角形来创建面,在三维建模领域里一直存在比较大的争议。基本上,大家都习惯于用四边形来创建面,因为它比三角形更容易增强和平滑。但是对于渲染器和游戏引擎来说,使用三角形更加容易,因为三角形渲染起来效率更高。
有了这些顶点和面,我们就可以创建一个新的THREE.Geometry的实例对象,然后将vertices数组赋值给vertices属性,将faces数组赋值给faces属性。最后我们需要做的就是在创建的几何体上执行computeFaceNormals()方法,当该方法执行时,Three.js会决定每个面的法向量,法向量用于决定不同光源下的颜色。
基于几何体,我们就可以创建网格了。我已经创建了一个例子,你可以尝试修改顶点的位置来体验一下。在示例05-custom-geometry.html中,你可以修改立方体的每个顶点并能够看到相应的面是如何变化的。如图2.8所示(按H键来隐藏GUI)。
这个示例和其他示例一样,都有一个render循环。无论何时修改了顶点的属性,立方体都会基于修改后的值重新进行渲染。出于性能方面的考虑,Three.js认为组成网格的几何体在整个生命周期内是不会改变的,而且对于大部分的几何体而言,这个假设是成立的。为了使我们的示例工作,我们还需要在render循环中添加如下的代码:
在循环中的第一行,我们将组成网格的几何体的vertices属性值指向一个更新后的顶点数组。如果顶点数组vertices没有更新,不需要重新配置这些面,因为它们仍然连接到原来的顶点。如果顶点被更新了,我们还需要告诉几何体顶点需要更新,在代码中是将verticesNeedUpdate属性设置为true来实现这一点的,最后需要调用computeFaceNormals()方法来重新计算每个面,从而完成整个模型的更新。
我们将要介绍的最后一个关于几何体的函数是clone()。我们说过几何体可以定义物体的形状,添加相应的材质后就可以创建出能够添加到场景中并由Three.js渲染的物体。通过clone()方法我们可以创建出几何体对象的拷贝。为这些拷贝对象添加不同的材质,我们就可以创建出不同的网格对象。在示例05-custom-geometry.html里,你可以在控制界面的顶端看到一个clone按钮。如图2.9所示。
如果点击clone按钮就可以按照几何体当前的状态创建出一个拷贝,而且这个新对象被赋予了新的材质并被添加到场景中。实现这个功能的代码是非常简单的,但是由于我使用的材质导致代码看起来有点复杂。首先我们先看一下绿色材质的实现代码:
如你所看到的,我们使用的不是一个材质,而是由两个材质构成的数组。这样做的原因是,除了显示绿色透明的立方体外,我还想显示一个线框。因为使用线框可以很容易地找出顶点和面的位置。当然,Three.js支持使用多种材质来创建网格。你可以使用SceneUtils.createMulti-MaterialObject()方法来达到这个目的。代码如下所示:
这个方法创建的并不是一个THREE.Mesh对象实例,而是为materials数组中每个指定的材质创建一个实例,并把这些实例存放在一个组里(THREE.Object3D对象)。你可以像使用场景中的对象那样使用这个组,如添加网格、按名称获取对象等。如果要为这个组中所有的子对象添加阴影,我们可以这样做:
现在,我们继续讨论clone()函数:
点击clone按钮,这段JavaScript代码就会被调用。这里我们复制立方体的第一个子对象。请记住,mesh变量包含两个THREE.Mesh子对象:基于两个不同材质创建的。通过这个复制的几何体我们创建了一个新的网格,并命名为mesh2。使用translate()方法移动这个新创建的网格,删除之前的副本(如果存在)并把这个副本添加到场景中。
在前面的章节中,我们使用THREE.SceneUtils对象的createMultiMaterialObject()方法为几何体添加了线框。在Three.js中还可以使用THREE.WireframeGeometry来添加线框。假设有一个几何体对象名为geom,可以通过下面代码基于geom创建一个线框对象:
var wireframe = new THREE.WireframeGeometry(geom);
然后,基于新建的线框对象创建一个Three.LineSegments对象并将它添加到场景中:
var line = new THREE.LineSegments(wireframe);
scene.add(line);
最后便可以利用它来绘制线框了,并且还可以像下面代码那样设置线框的宽度:
line.material.linewidth = 2;
关于Three.js中几何体的知识,我们暂时就学习到这里。
2.2.2 网格对象的属性和方法
我们已经知道,创建一个网格需要一个几何体,以及一个或多个材质。当网格创建好之后,我们就可以将它添加到场景中并进行渲染。网格对象提供了几个属性用于改变它在场景中的位置和显示效果。下面我们来看下网格对象提供的属性和方法,具体见表2.3。
我们同样准备了一个示例,你可以修改这些属性的值来感受下效果。当你在浏览器中打开示例06-mesh-properties.html时,可以看到一个下拉菜单。通过该下拉菜单你就可以修改属性的值,并立即看到修改后的效果,如图2.10所示。
下面来逐一讲解这些属性和方法,先从position属性开始。通过这个属性你可以设置对象在x、y和z轴的坐标。对象的位置是相对于它的父对象来说的,通常父对象就是添加该对象的场景,但有的时候可能是THREE.Object3D对象或其他THREE.Mesh对象。在第5章讨论对象组合时我们再来讨论这个问题。有三种方式用于设置对象的位置。第一种是直接设置坐标,代码如下所示:
也可以一次性地设置x、y和z坐标的值:
还有第三种方式。position属性是一个THREE.Vector3对象,这意味着我们可以像下面这样设置该对象:
效果图如图2.11所示。
但是如果现在移动该对象组,会发现其位移值保持不变。在第5章中我们还会深入讨论这种父子关系,以及对象组是如何影响变换(如缩放、旋转和平移)的。
下一个我们将要介绍的是rotation(旋转)属性。在本章和前一章中我们已经多次使用这个属性了,通过这个属性可以设置对象绕轴的旋转弧度。我们可以像设置position属性那样来设置rotation属性。在数学上物体旋转一周的弧度值为2,所以可以用如下三种方式设置旋转:
如果想使用度数(0到360)来设置旋转,那么需要对度数做如下转换:
你可以通过示例06-mesh-properties.html来体验该属性。
属性列表中还没有讨论的属性是scale(缩放)。从名字我们就已经知道该属性是用来做什么了。该属性让我们可以沿指定轴缩放对象。如果设置的缩放值小于1,那么物体就会缩小,如图2.12所示。
如果设置的缩放值大于1,那么物体就会变大,如图2.13所示。
在本章中将要继续讨论的是网格的translate()方法。使用translate()方法你可以改变对象的位置,但是该方法设置的不是物体的绝对位置,而是物体相对于当前位置的平移距离。假设在场景中有个球体,其位置为(1,2,3)。如果我们想让这个对象相对于x轴平移4个单位:translateX(4),那么物体的位置就会变为(5,2,3)。如果我们想重置物体的位置为原来的位置,可以调用translateX(-4)。在示例06-mesh-properties.html中有个translate菜单项,通过这个菜单项你可以体验这个功能。只要设置沿x、y和z轴方向的平移距离,然后点击translate按钮,你就可以看到物体依照这三个值平移到一个新的位置。
最后一个可以使用的是菜单项右上角的visible属性。当你点击visible菜单项时,会发现立方体消失了,如图2.14所示。
当你再次点击visible按钮时,立方体又再次出现了。在第5章和第7章中,我们还会进一步介绍网格和几何体,以及如何使用这些对象。
2.3 选择合适的摄像机
Three.js库提供了两种不同的摄像机:正交投影摄像机和透视投影摄像机。在第3章中将会详细介绍如何使用这些摄像机,所以在本章中只会介绍一些比较基础的内容。值得注意的是,Three.js还提供了一些非常特殊的摄像机用于支持3D眼镜和VR设备。由于它们与本章将要讲述的基础摄像机的工作方式类似,因此本书不会深入介绍这些特殊摄像机。
如果你只需要简单VR摄像机(即标准的立体视觉效果),可以使用THREE.StereoCamera将左右眼画面并排渲染,或者也可以使用其他特殊摄像机渲染视差屏障式的(例如3DS提供的设备)3D图像,或者是传统的红蓝重叠式的3D图像。
此外,Three.js还实验性地支持WebVR这一被浏览器广泛支持的标准(更多信息请参考https://webvr. info/developers/ )。为了启用这一支持,只需要设置renderer.vr.enabled = true;,后续工作Three.js会为你做好。在Three.js的官方网站可以找到有关这一属性值以及其他WebVR相关特性的示例:https:/ /threejs.org/examples/。
下面将会使用几个示例来解释正交投影摄像机和透视投影摄像机的不同之处。
2.3.1 正交投影摄像机和透视投影摄像机
在本章的示例中有个名为07-both-cameras.html的例子。当你打开该示例时,会看到如图2.15所示的结果。
这就是透视视图,也是最自然的视图。正如你所看到的,这些立方体距离摄像机越远,它们就会被渲染得越小。如果我们使用另外一种摄像机—正交投影摄像机,对于同一个场景你将会看到如图2.16所示的效果。
使用正交投影摄像机的话,所有的立方体被渲染出来的尺寸都是一样的,因为对象相对于摄像机的距离对渲染的结果是没有影响的。这种摄像机通常被用于二维游戏中,比如《模拟城市4》和早期版本的《文明》。如图2.17所示。
在我们的示例中,大部分使用的是透视投影摄像机,因为这种摄像机的效果更贴近真实世界。在Three.js中改变摄像机是很简单的,当你点击07-both-cameras.html示例中的switchCamera按钮时,下面的代码将会被执行:
THREE.PerspectiveCamera和THREE.OrthographicCamera的创建方法是不一样的。首先我们先来看下THREE.PerspectiveCamera,该方法接受的参数如表2.4所示。
这些属性结合到一起影响你所看到的景象,如图2.18所示。
摄像机的fov属性决定了横向视场。基于aspect属性,纵向视场也就相应地确定了。near属性决定了近面距离,far属性决定了远面距离。近面距离和远面距离之间的区域将会被渲染。
如果要配置正交投影摄像机,我们需要使用其他的一些属性。由于正交投影摄像机渲染出的物体大小都是一样的,所以它并不关心使用什么长宽比,或者以什么样的视角来观察场景。当使用正交投影摄像机时,你要定义的是一个需要被渲染的方块区域。表2.5列出了正交投影摄像机相应的属性。
所有这些属性可以总结为图2.19。
2.3.2 将摄像机聚焦在指定点上
到目前为止,我们已经介绍了如何创建摄像机,以及各个参数的含义。在前面一章中,我们也讲过摄像机需要放置在场景中的某个位置,以及摄像机能够看到的区域将会被渲染。通常来说,摄像机会指向场景的中心,用坐标来表示就是position(0,0,0)。但是我们可以很容易地改变摄像机所指向的位置,代码如下所示:
在我们的示例中,摄像机是可以移动的,而且它所指向的位置标记一个红点,如图2.20所示。
如果你打开示例08-cameras-lookat.html,就会发现场景正从左向右移动。其实场景并没有移动,而是摄像机从不同点(屏幕*的红点)拍摄场景,其带来的效果就是场景从左向右移动。在该示例中,你还可以切换到正交投影摄像机;你会看到改变摄像机拍摄位置所带来的效果和使用透视投影摄像机的效果是相同的。有意思的是,不管摄像机拍摄的位置如何改变,正交投影摄像机拍摄出来的所有立方体大小都是一样的。如图2.21所示。
当使用lookAt()方法时,可以在某个特定的位置设置摄像机。使用该方法还可以让摄像机追随场景中的某个物体。由于THREE.Mesh对象的位置都是THREE.Vector3对象,所以可以使用lookAt()方法使摄像机指向场景中特定的某个网格。你所需要做的就是输入如下代码:camera.lookAt(mesh.position)。如果在render循环中执行该代码,你所看到的效果就是摄像机随着物体的移动而移动。
2.4 总结
在本章中我们介绍了THREE.Scene的所有属性和方法,并解释了如何使用这些属性来配置主场景。我们还展示了如何使用THREE.Geometry对象或者使用Three.js内置的几何体来创建几何体。最后我们介绍了如何配置Three.js提供的两种摄像机:透视投影摄像机使用接近真实世界的视角来渲染场景,而正交投影摄像机提供了一种在游戏中被广泛采用的伪三维效果。我们还介绍了Three.js中的几何体是如何工作的。现在你就可以很简单地创建你自己的几何体了。
在下一章中,我们将会介绍Three.js库提供的各种不同光源。你将会学到各种不同光源的行为,如何创建和配置这些光源,以及它们对特定材质的影响。