购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.2 几何体和网格的关系

在到目前为止的每个示例中,我们都使用了几何体和网格。我们可以使用以下代码创建一个球体并将其添加到场景中:

以上代码定义了一个几何体(THREE.SphereGeometry),几何体是一个对象的形状。然后定义了一个材质(THREE.MeshBasicMaterial)。我们将这两者合并在一个网格(THREE.Mesh)中,该网格可以被添加到场景中。本节我们将更详细地了解几何体和网格。我们将从几何体开始。

2.2.1 几何体的属性和函数

Three.js内置了大量的、可以直接用于你3D场景里面的几何体。你可以使用这些内置的几何体,只需要添加一个材质,创建一个网格,就能完成大部分工作了。图2.7中的chapter-2/geometries示例的截图展示了Three.js中的一些标准几何体。

图2.7 场景中可用的一些标准几何体

我们将在第5章和第6章中探讨Three.js提供的所有基本和高级几何体。现在我们先详细地讲述一下几何体的实际定义。

在Three.js以及大部分其他3D库中,几何体基本上是三维空间中一系列点加上将这些点连接起来的面的集合(这些点也称为顶点)。以立方体为例:

❑ 立方体有八个角。这些角的每一个都可以定义为一个x、y和z坐标。因此,每一个立方体在三维空间中有八个顶点。

❑ 立方体有六个面,每个角落都对应一个顶点。在Three.js中,一个面总是由三个顶点组成,这三个顶点连接成一个三角形。所以立方体的每个面由两个三角形组成,以形成完整的面。通过图2.7中的立方体,你可以直观地看到立方体每个面的构成情况。

当你使用Three.js提供的几何体时,你不需要自己定义所有的顶点和面。例如使用一个立方体,你只需定义宽度、高度和深度。Three.js将使用这些信息创建一个包含正确位置和正确数目的面的几何体(对于立方体来说是12个面,每个面由两个三角形组成)。虽然通常我们会使用Three.js提供的几何体或者自动生成它们,但我们也可以完全手动地使用顶点和面来创建几何体,尽管这样做很快就会变得很复杂,就像以下代码一样:

以上代码展示了如何创建一个简单的立方体。我们在v数组中定义了构成该立方体的点(顶点)。然后,我们可以创建一个面。在Three.js中,我们需要将所有的面信息存储在一个大的Float32Array数组中。如前所述,一个面由三个顶点组成。所以,对于每个面,我们需要定义九个值:每个顶点的x、y和z。由于每个面有三个顶点,所以有九个值。为了使代码更易读,我们使用了JavaScript中的...(展开)运算符来将每个顶点的值添加到数组中。也就是说,...v[0],...v[2],...v[1]这段代码将让数组包含以下值:1, 3, 1, 1,-1, 1, 1, 3, 1。

这意味着你必须注意用于定义面的顶点的顺序。它们的顺序决定了Three.js是否将其视为一个正面朝向(朝向相机)或一个背面朝向的面。也就是说,如果你创建面,则应该使用顺时针顺序创建正面朝向的面,如果要创建背面朝向的面,则应该使用逆时针顺序。

在我们的示例中,我们使用了多个顶点来定义立方体的六个面,每个面由两个三角形组成。在Three.js的早期版本中,你可以使用四边形(quad)而不是三角形来定义面。四边形使用四个顶点而不是三个来定义面。使用四边形还是三角形更好在3D建模界是一个激烈的辩论话题,目前尚没有明确的答案。在建模过程中,通常更倾向于使用四边形,因为与三角形相比,四边形更容易进行增强和平滑处理。然而,在渲染和游戏引擎中,使用三角形通常更为简单,因为任何形状都可以通过三角形非常高效地进行渲染。

也就是说我们可以使用这些顶点和面来创建一个THREE.BufferGeometry的实例,并将顶点坐标数据分配给该实例的position属性。在创建几何体之后,最后一步是在我们创建的几何体上调用computeVertexNormals()函数。当我们调用这个函数时,Three.js会确定每个顶点和面的法线向量(normal vector)。Three.js使用这些信息来确定如何根据场景中的各种光源来着色面的颜色(如果你使用THREE.MeshNormalMaterial,则可以很容易地看到法线向量对渲染效果的影响)。

现在我们已经有了几何体,可以像前面那样创建一个网格。我们有一个示例可供你调整顶点位置并实时显示每个面,从而直观地观察顶点位置变化对几何体形状的影响,来帮助你更好地理解几何体的构建方式。这个示例是chapter-2/custom-geometry,你可以更改立方体所有顶点的位置,然后实时观察面的形状变化。具体效果如图2.8所示。

这个示例使用了和其他示例相同的设置,并包含一个渲染循环。每当你更改下拉控制框中的属性时,立方体会实时重新渲染以反映这个顶点的位置变化。这个功能并非默认就有的。出于性能考虑,Three.js假设在运行时网格的几何体是不会发生变化的。对于大多数几何体和用例来说,这是一个非常合理的假设。意味着如果你改变了后面的数组(在本例中是const faces=new Float32Array([...])数组),那么你需要告诉Three.js有些内容发生了改变。你可以通过将相关属性的needsUpdate属性设置为true来实现这一点。具体类似代码如下:

图2.8 移动顶点以改变形状

注意,你还需要重新计算法线向量以确保材质也被正确渲染。关于法线向量以及它为什么重要的更多信息我们将在第10章详细讲述。

chapter-2/custom-geometry示例菜单中还有一个按钮我们尚未解释。在右侧菜单中,有一个 clone 按钮。我们提到几何体定义了对象的形状,再结合材质,我们可以创建一个可以添加到场景中由Three.js渲染的对象。关于clone()函数,顾名思义,我们可以使用它复制几何体,然后使用这个复制出来的几何体来创建一个具有不同材质的新网格。还是回到示例chapter-2/custom-geometry,可以在控制GUI的顶部看到一个 clone 按钮,如图2.9所示。

如果你单击该按钮,将会以当前状态克隆一个几何体。然后你可以使用不同的材质创建一个新对象,并将该对象添加到场景中。具体代码如下:

图2.9 克隆几何体

在以上代码中,我们使用clone()函数来克隆bufferGeometry。在克隆之后,我们将更新每个顶点的x值,使克隆体与原始体的位置不同(我们也可以使用translateX,我们会在2.2.2节对其进行解释)。然后我们创建一个THREE.Mesh,删除现存克隆网格(如果有的话),将新创建的克隆网格添加到场景中。为了创建新的网格,我们使用了一个名为meshFromGeometry的自定义函数。这里我们简单插入一下这个函数的具体代码:

如果你回头看一下我们的示例,你会看到一个透明的立方体以及构成几何体的线条(即边)。要实现这个示例,我们需要创建一个由多个材质组成的网格。也就是说,我们需在一个网格中使用两种不同的材质。对此,Three.js有一个很好的辅助函数createMultiMaterialObject,顾名思义,它就是用来创建一个使用多个材质的网格对象。该函数可以基于一个几何体和一个材质列表创建一个我们可以添加到场景中的对象。不过,对于createMultiMaterialObject函数的结果,你需要注意一点:你得到的结果并不是一个网格,而是一个THREE.Group,它是一个容器对象,这个容器对象包含了多个THREE.Mesh(其中每个THREE.Mesh对象使用了材质列表中的一个材质)。因此,当渲染这个网格时,它看起来像一个单一的对象,但实际上是由多个叠在一起的THREE.Mesh对象组成的。这也意味着如果我们想要有阴影,那么我们需要为组内的每个网格启用阴影(对应mesh.children.forEach(function(e))这行代码)。

在前面的代码中,我们使用了THREE.SceneUtils对象的createMultiMaterial-Object函数来为创建的几何体添加了一个线框。除了createMultiMaterialObject之外,Three.js还有另一种添加线框的方法——THREE.WireframeGeometry。假设你有一个名为geom的几何体,你可以从这个几何体创建一个线框几何体:const wireframe=new THREE.WireframeGeometry(geom);。然后你可以使用Three.LineSegments对象绘制该几何体的线条,首先创建一个const line=new THREE.LineSegments(wireframe)对象,然后将其添加到场景中:scene.add(line)。这个辅助类实际上是一个THREE.Line对象,因此你可以通过设置这个对象的一些属性来控制线框的外观样式。例如,要设置线框线条的宽度,可以使用line.material.linewidth=2;。

我们已经简要了解了THREE.Mesh对象的一些内容。接下来我们将更深入地了解你可以使用它做什么。

2.2.2 网格的函数和属性

我们已经了解到,要创建一个网格,我们需要一个几何体和一个或多个材质。现在我们有了一个网格,并且已经将其添加到场景并渲染出来。我们可以通过该网格的一些属性来更改该网格在场景中的位置和方式。在第一个示例中,我们将了解以下一组属性和函数:

❑ position:确定了对象相对于其父对象的位置。一个网格对象的父对象在通常情况下是一个THREE.Scene对象或一个THREE.Group对象。

❑ rotation:你可以通过该属性设置对象绕其自身任意轴旋转的角度。Three.js还提供了专门用于绕单个轴旋转的函数:rotateX()、rotateY()和rotateZ()。

❑ scale:通过该属性我们可以控制对象相对于其自身的x、y、z轴进行缩放。

❑ translateX()、translateY()和translateZ():这些函数用于指定沿着相应的轴移动对象的距离。

❑ lookAt():用于将对象指向空间中的一个特定位置。使用lookAt()来控制对象的朝向更简单直观,不需要手动设置旋转。

❑ visible:用于控制网格对象是否应该被渲染到场景中。

❑ castShadow:用于确定当光源照射到网格时,该网格是否产生阴影。默认情况下,网格不会产生阴影。

当我们旋转一个对象时,我们是围绕一个轴旋转的。在一个3D场景中,一个对象可以在不同的空间内进行旋转,每种空间都有自己的旋转轴。rotateN()函数会在局部空间内围绕轴进行旋转,也就是说旋转轴与其父对象相关。当你将一个对象添加到场景中时,rotateN()函数将围绕场景的主轴旋转该对象。当对象嵌套在组中时,rotateN()函数将使对象围绕其父对象的轴旋转,这通常是你希望的行为。对此,Three.js还有一个rotateOnWorldAxis函数,它允许你围绕主THREE.Scene的轴旋转对象,而不需要管对象的实际父对象是什么。最后,你还可以通过调用rotateOnAxis函数来强制对象围绕自己的轴(这称为对象空间)旋转。一如既往,我们已经为你准备了一个示例供你实验这些属性。

在浏览器中打开chapter-2/mesh-properties,你将看到有一个下拉菜单可以更改所有这些属性,并实时看到结果,如图2.10所示。

现在让我一一为你介绍这些属性,先从position属性开始。

使用position属性设置网格的位置

我们已经多次见过这个属性,所以我们就不过多介绍。你可以使用这个属性设置对象相对于其父对象的x、y和z坐标。我们会在第5章介绍分组对象时再次讲解该属性。我们有三种方法设置一个对象的position属性。我们可以直接设置每个坐标:

图2.10 网格的属性

我们也可以一次设置它们的值:

还有第三种方法。position属性是一个THREE.Vector3对象。也就是说我们也可以使用以下方式设置:

接下来要介绍的是rotation属性。在本章前面内容和第1章,你已经见过几次使用rotation属性的场景。

使用rotation属性定义网格的旋转

使用该属性你可以设置对象绕其自身任意轴旋转的角度。你可以像设置position一样设置该值。你可能还记得在数学课上学过,一个完整的旋转是2π。你可以用以下几种方式设置它:

在Three.js中,对象的旋转(rotation)属性使用弧度(radians)而不是角度(degrees)来表示,如果需要使用角度值(0到360度),就需要将角度值转换为弧度值。转换也很容易:

在以上代码中,我们自己手动进行了角度到弧度的转换。对此Three.js提供了MathUtils类,该类有许多有用的转换函数,使用它们我们就不需要手动计算来转换了。你可以通过chapter-2/mesh-properties示例来实验该属性。

下一个属性是我们还没有讲述过的:scale(缩放)。其名称已经非常直观地表达了其功能,即可以沿着特定轴缩放对象。如果将scale属性的值设置小于1,则对象会缩小,如图2.11所示。

图2.11 使用scale来缩小网格

如果将scale属性的值设置大于1,则对象会放大,如图2.12所示。

我们将要探讨的下一个网格属性是translate属性。

使用translate属性改变对象的位置

你还可以使用translate改变对象的位置,但是与直接设置position属性不同,translate属性通过定义对象相对于当前位置应该移动的距离来实现。假设我们向场景中添加了一个球体,并且将其位置设置为(1, 2, 3)。然后我们沿着它的x轴平移对象:translateX(4)。它的位置将会变为(5, 2, 3)。如果我们想要将对象恢复到其初始位置,那么可以使用translateX(-4)。在chapter-2/mesh-properties示例中,有一个 translate 菜单选项。你可以通过它实验这个属性。只需设置x、y和z的translate值,然后单击 translate 按钮。你将看到对象根据这三个值移动到一个新的位置。

图2.12 使用scale来放大网格

最后要讲的两个属性:通过将visible属性设置为false来完全删除对象,通过将castShadow属性设置为false来禁用此对象是否投射阴影。当你单击这些按钮时,你将看到立方体变为不可见和可见,并且你可以禁止它投射阴影。

有关网格、几何体以及你可以对这些对象执行的操作的更多信息,请查看第5章以及第7章。

到目前为止,我们已经讲述过THREE.Scene,它是保存所有需要渲染的对象的主要对象,同时也详细讲述了THREE.Mesh,以及如何创建THREE.Mesh并将其放置在场景中。在之前的章节中,我们已经使用了相机来决定要渲染THREE.Scene的哪一部分,但还没有详细解释如何配置相机。在接下来的章节中,我们将深入探讨这些细节。 qHuhCinGzVzRLH/TDNPcI7UQboKntkk0XimGxd4YxJ6Uguel65dluTQJzwALryIV

点击中间区域
呼出菜单
上一章
目录
下一章
×