第八章创建和加载高级网格和几何图形

在本章中,我们将介绍创建和加载高级复杂几何体和网格的几种不同方法。在第5章“学习使用几何图形”和第6章“探索高级几何图形”中,我们向您展示了如何使用Three.js中的内置对象创建一些高级几何图形。在本章中,我们将使用以下两种方法创建高级几何图形和网格:

  • 几何图形分组和合并
  • 正在从外部资源中加载几何图形

我们从“分组合并”方法开始。使用这种方法,我们使用标准的Three.js分组(Three.Group)和BufferGeometryUtils.mergeBufferGeometrys() 函数来创建新对象。

几何图形分组和合并

在本节中,我们将介绍Three.js的两个基本特性:将对象分组在一起,并将多个几何图形合并为单个几何图形。我们将从分组开始。

将对象分组合并

在前面的一些章节中,您已经了解了如何在使用多个材质时对对象进行分组。使用多个材质从几何体创建网格时,Three.js会创建一个组。几何图形的多个副本将添加到此组中,每个副本都有自己的特定材质。该组将返回,因此它看起来像一个使用多个材质的网格。然而,事实上,它是一个包含多个网格的组。

创建小组非常容易。创建的每个网格都可以包含子元素,这些子元素可以使用add函数添加。将子对象添加到组的效果是,可以移动、缩放、旋转和平移父对象,所有子对象也将受到影响。使用组时,仍然可以参照、修改和定位各个几何图形。唯一需要记住的是,所有位置、旋转和平移都是相对于父对象进行的。

让我们来看看下面的屏幕截图中的一个示例(grouping.html):

image-20230524143048891

图8.1–使用THREE.Group对象将对象分组在一起

在本例中,您将看到大量立方体,这些立方体作为单个组添加到场景中。在我们查看控件和使用组的效果之前,让我们快速了解一下我们是如何创建此网格的:

 const size = 1
 const amount = 5000
 const range = 20
 const group = new THREE.Group()
 const mat = new THREE.MeshNormalMaterial()
 mat.blending = THREE.NormalBlending
 mat.opacity = 0.1
 mat.transparent = true
 for (let i = 0; i < amount; i++) {
 const x = Math.random() * range - range / 2
 const y = Math.random() * range - range / 2
 const z = Math.random() * range - range / 2
 const g = new THREE.BoxGeometry(size, size, size)
 const m = new THREE.Mesh(g, mat)
 m.position.set(x, y, z)
 group.add(m)
 }

在这个代码片段中,您可以看到我们创建了一个THREE.Group实例。该对象几乎与THRE.Object3D相同,THRE.OObject3D是THREE.Mesh和THREE.Scene的基类,但就其本身而言,它不包含任何内容,也不导致任何内容被渲染。在本例中,我们使用add函数将大量立方体添加到此场景中。对于本例,我们添加了可用于更改网格位置的控件。每当您使用此菜单更改属性时,THREE.Group对象的相关属性都会更改。例如,在下一个例子中,您可以看到,当我们缩放这个THREE.Group对象时,所有嵌套的多维数据集也会被缩放:

image-20230524144750649

图8.2-正在缩放一个组

如果你想对THREE.Group对象进行更多的实验,一个很好的练习是改变示例,使THREE.Group实例本身在x轴上旋转,而单个立方体在y轴上旋转。

使用 THREE.Group 对性能的影响

在我们进入下一节讨论合并之前,先简要介绍一下性能。当您使用THREE.Group时,该组中的所有单个网格都被视为单个对象,THREE.js需要管理和渲染这些对象。如果场景中有大量对象,则会看到性能明显下降。如果您查看图8.2的左上角,您可以看到屏幕上有5000个立方体,我们可以获得大约56帧/秒(FPS)。还不错,但通常我们会以120帧/秒左右的速度运行。

Three.js提供了一种额外的方法,我们仍然可以控制各个网格,但可以获得更好的性能。这是通过THREE.InstancedMesh完成的。如果您想渲染大量具有相同几何体但具有不同变换(例如,旋转、缩放、颜色或任何其他矩阵变换)的对象,则此对象非常有效。

我们已经创建了一个名为instanced-mesh.html的示例,它展示了这是如何工作的。在本例中,我们渲染了250000个立方体,并且仍然具有出色的性能:

image-20230524145305369

图8.3-使用实例网格对象进行分组

要使用THREE.InstancedMesh对象,我们创建它的方式与创建THREE.Group实例的方式类似:

const size = 1
 const amount = 250000
 const range = 20
 const mat = new THREE.MeshNormalMaterial()
 mat.opacity = 0.1
 mat.transparent = true
 mat.blending = THREE.NormalBlending
 const g = new THREE.BoxGeometry(size, size, size)
 const mesh = new THREE.InstancedMesh(g, mat, amount)
 for (let i = 0; i < amount; i++) {
 const x = Math.random() * range - range / 2
 const y = Math.random() * range - range / 2
 const z = Math.random() * range - range / 2
 const matrix = new THREE.Matrix4()
 matrix.makeTranslation(x, y, z)
 mesh.setMatrixAt(i, matrix)
 }

与THREE.Group相比,创建THREE.InstancedMesh对象的主要区别在于,我们需要预先定义要使用的材质和几何体,以及要创建该几何体的多少实例。要定位或旋转我们的一个实例,我们需要使用THRE.Matrix4实例提供转换。幸运的是,我们不需要讨论矩阵背后的数学,因为Three.js在THRE.Matrix4实例上为我们提供了几个辅助函数,用于定义旋转、平移和其他一些转换。在本例中,我们只是将每个实例定位在一个随机位置。

因此,如果您使用的是少量网格(或使用不同几何体的网格),如果您想将它们分组在一起,则应该使用THREE.Group对象。如果要处理大量共享几何体和材质的网格,可以使用THREE.InstancedMesh对象或THREE.InstancedBufferGeometry对象来大幅提高性能。

合并几何图形

在大多数情况下,使用组可以轻松地操作和管理大量网格。然而,当您处理大量对象时,性能将成为一个问题,因为Three.js必须单独处理组中的所有子对象。使用BufferGeometryUtils.mergeBufferGeometrys,您可以将几何体合并在一起并创建一个组合的几何体,因此Three.js只需管理这一单个几何体。在图8.4中,您可以看到这是如何工作的,以及它对性能的影响。如果打开merge.html示例,您将再次看到一个场景,其中包含相同的一组随机分布的半透明立方体,我们将其合并为一个THRE.BufferGeometry对象:

image-20230524145853874

图8.4-500000个几何合并成单一几何

正如您所看到的,我们可以轻松地呈现50,000个立方体,而性能不会下降。为此,我们使用了以下几行代码:

const size = 1
 const amount = 500000
 const range = 20
 const mat = new THREE.MeshNormalMaterial()
 mat.blending = THREE.NormalBlending
 mat.opacity = 0.1
 mat.transparent = true
 const geoms = []
 for (let i = 0; i < amount; i++) {
 const x = Math.random() * range - range / 2
 const y = Math.random() * range - range / 2
 const z = Math.random() * range - range / 2
 const g = new THREE.BoxGeometry(size, size, size)
 g.translate(x, y, z)
 geoms.push(g)
 }
 const merged = BufferGeometryUtils.
 mergeBufferGeometries(geoms)
 const mesh = new THREE.Mesh(merged, mat)

在这个代码片段中,我们创建了大量的THRE.BoxGeometry对象,并使用BufferGeometryUtils.mergeBufferGeometry(geoms)函数将这些对象合并在一起。结果是一个单独的大型几何体,我们可以将其添加到场景中。最大的缺点是失去了对单个立方体的控制,因为它们都合并到一个大的几何体中。如果要移动、旋转或缩放单个立方体,则不能(除非搜索正确的面和顶点并单独定位它们)。

通过构造实体几何创建新的几何图形

除了按照我们在本章中看到的方式合并几何图形外,我们还可以使用构造实体几何(CSG)创建几何图形。使用CSG,可以应用运算(通常是加法、减法、差分和交集)来组合两个几何图形。这些库将根据选定的操作创建一个新的几何图形。例如,使用CSG,可以很容易地创建一侧具有球形压痕的实心立方体。您可以在Three.js中使用的两个库是three-bvh-csg(https://github.com/gkjohnson/three-bvh-csg)和Three.csg(https://github.com/looeee/threejs-csg).

通过分组和合并方法,您可以使用Three.js提供的基本几何图形来创建大型和复杂的几何图形。如果您想创建更高级的几何图形,那么使用Three.jsp提供的编程方法并不总是最好和最简单的选择。幸运的是,Three.js提供了其他几个创建几何图形的选项。在下一节中,我们将研究如何从外部资源加载几何图形和网格。

正在从外部资源中加载几何图形

Three.js可以读取大量三维文件格式,并导入这些文件中定义的几何图形和网格。这里需要注意的是,并非这些格式的所有功能都始终受支持。因此,有时可能会出现纹理问题,或者材质设置不正确。交换模型和纹理的新标准是glTF,所以如果你想加载外部创建的模型,将这些模型导出为glTF格式通常会在Three.js中获得最佳结果。

在本节中,我们将更深入地了解一些由Three.js支持的格式,但我们不会向您展示所有的加载器。下面的列表显示了Three.js所支持的格式的概述:

  • AMF:AMF是另一种3D打印标准,但目前尚未开发。以下维基百科页面提供了有关该标准的更多信息:https://www.sculpteo.com/en/glossary/amf-definition/.
  • 3DM:3DM是犀牛使用的格式,是一种创建3D模型的工具。有关犀牛的更多信息,请点击此处:https://www.rhino3d.com/.
  • 3MF:3MF是3D打印中使用的标准之一。有关此格式的信息,请访问3MF Consortium主页:https://3mf.io.
  • 协调活动设计活动 (COLLADA):COLLADA是一种以基于XML的格式定义数字资产的格式。这是一种广泛使用的格式,几乎所有的3D应用程序和渲染引擎都支持它。
  • Draco:Draco是一种以非常有效的方式存储几何图形和点云的文件格式。它指定如何最好地压缩和解压缩这些元素。关于Draco如何工作的详细信息,可以在其GitHub页面上找到:https://github.com/google/draco.
  • GCode:GCode是一种与3D打印机或CNC机器通话的标准方式。打印模型时,控制3D打印机的方法之一是向其发送GCode命令。本标准的详细内容在以下文件中进行了描述:https://www.nist.gov/publications/nist-rs274ngc-interpreter-version-3?pub_id=823374.
  • glTF:这是一个规范,定义了不同应用程序和工具如何交换和加载3D场景和模型,并正在成为网络上交换模型的标准格式。它们有一种扩展名为.glb的二进制格式和扩展名为/gltf的基于文本的格式。有关此标准的更多信息,请点击此处:https://www.khronos.org/gltf/.
  • 行业基础类(IFC):这是一种由建筑信息建模(BIM)工具使用的开放文件格式。它包含一个建筑模型和许多关于所用材料的附加信息。有关此标准的更多信息,请点击此处:https://www.buildingsmart.org/standards/bsi-standards/industry-foundation-classes/
  • JSON:Three.js有自己的JSON格式,您可以使用它来声明性地定义几何体或场景。尽管这不是一种官方格式,但它非常容易使用,并且在您想要重用复杂的几何体或场景时非常方便。
  • KMZ:这是谷歌地球上用于3D资产的格式。更多信息可在此处找到:https://developers.google.com/kml/documentation/kmzarchives.
  • LDraw:LDraw是一个开放的标准,可以用来创建虚拟乐高模型和场景。有关更多信息,请访问LDraw主页:https://ldraw.org.
  • LWO:这是LightWave 3D使用的文件格式。有关LightWave 3D的更多信息,请点击此处:https://www.lightwave3d.com/.
  • NRRD:NRRD是一种用于可视化体积数据的文件格式。例如,它可以用于渲染CT扫描。在这里可以找到很多信息和样本:http://teem.sourceforge.net/nrrd/.
  • OBJ和MTL:OBJ是Wavefront Technologies首次开发的一种简单的3D格式。它是最广泛采用的三维文件格式之一,用于定义对象的几何体。MTL是OBJ的配套格式。在MTL文件中,指定了OBJ文件中对象的材质。Three.js还有一个自定义的OBJ导出器,称为OBJExporter,如果您想从Three.js中将模型导出到OBJ。
  • PDB:这是一种非常专业的格式,由蛋白质数据库(PDB)创建,用于指定蛋白质的外观。Three.js可以加载并可视化以这种格式指定的蛋白质。
  • 多边形文件格式(PLY):最常用于存储来自3D扫描仪的信息。
  • 压缩原始WebGL模型(PRWM):这是另一种专注于3D几何图形的高效存储和解析的格式。有关此标准以及如何使用此标准的更多信息,请参见此处:https://github.com/kchapelier/PRWM.
  • STereo光刻(STL):这被广泛用于快速原型制作。例如,3D打印机的模型通常被定义为STL文件。Three.js还有一个自定义的STL导出器,称为STLExporter.js,如果您想从Three.js中将模型导出到STL。
  • SVG:SVG是定义矢量图形的标准方式。此加载程序允许您加载SVG文件并返回一组THREE.Path元素,这些元素可用于在2D中进行挤出或渲染。
  • 3DS:Autodesk 3DS格式。更多信息请访问https://www.autodesk.com/。
  • TILT:TILT是TILT Brush使用的格式,这是一种VR工具,允许您在VR中绘画。更多信息请点击此处:https://www.tiltbrush.com/。
  • VOX:MagicaVoxel使用的格式,这是一个免费的工具,可以用来创建体素艺术。更多信息请访问MagicaVoXE:https://ephtracy.github.io/。
  • 虚拟现实建模语言(VRML):这是一种基于文本的格式,允许您指定3D对象和世界。它已被X3D文件格式所取代。Three.js不支持加载X3D模型,但这些模型可以很容易地转换为其他格式。更多信息请访问http://www.x3dom.org/?page_id=532#。
  • 可视化工具包(VTK):这是由定义的文件格式,用于指定顶点和面。有两种可用的格式:二进制格式和基于文本的ASCII格式。Three.js仅支持基于ASCII的格式。
  • XYZ:这是一种非常简单的文件格式,用于描述三维空间中的点。更多信息请点击此处:https://people.math.sc.edu/Burkardt/data/xyz/xyz.html。

在第9章,动画和移动照相机中,我们将在查看动画时重新讨论其中的一些格式(并查看一些其他的格式)。

从这个列表中可以看到,Three.js支持大量的3D文件格式。我们不会描述所有的东西,只是最有趣的那些。我们将从JSON加载器开始,因为它提供了一种存储和检索您自己创建的场景的好方法。

保存和加载在3.js JSON格式

您可以在Three.js中为两种不同的场景使用Three.js JSON 格式。您可以使用它来保存和加载单个 Three.Object3D 对象(这意味着您也可以使用它导出Three.Scene对象)。

为了演示保存和加载,我们创建了一个基于THRE.TorusKnotGeometry的简单示例。使用此示例,您可以创建一个圆环结,就像我们在第5章中所做的那样,并且使用保存/加载菜单中的保存按钮,您可以保存当前几何体。对于这个示例,我们使用HTML5本地存储API进行保存。此API允许我们轻松地将持久信息存储在客户端的浏览器中,并在以后检索它(即使在浏览器关闭并重新启动后):

image-20230524153109103

图8.5-显示加载和当前网格

在前面的屏幕截图中,您可以看到两个网格——红色的是我们加载的,黄色的是原始的。如果您自己打开此示例并单击保存按钮,则将存储网格的当前状态。现在,您可以刷新浏览器并单击加载,所保存的状态将显示为红色。

从Three.js导出JSON非常简单,并且不需要您包含任何额外的库。你唯一需要做的事情就是导出 THREE.Mesh 设置为JSON,并将其存储在浏览器的本地存储中,如下所示:

const asJson = mesh.toJSON()
localStorage.setItem('json', JSON.stringify(asJson))

在保存之前,我们首先使用JSON.stringify函数将toJSON函数(一个JavaScript对象)的结果转换为字符串。要使用HTML5本地存储API保存这些信息,我们所要做的就是调用localStorage.setItem函数。第一个参数是键值(json),我们稍后可以使用它来检索作为第二个参数传入的信息。

这个JSON字符串看起来是这样的:

{
 "metadata": {
 "version": 4.5,
 "type": "Object",
 "generator": "Object3D.toJSON"
 },
 "geometries": [
 {
 "uuid": "15a98944-91a8-45e0-b974-0d505fcd12a8",
 "type": "TorusKnotGeometry",
 "radius": 1,
 "tube": 0.1,
 "tubularSegments": 200,
 "radialSegments": 10,
 "p": 6,
 "q": 7
 }
 ],
 "materials": [
 {
 "uuid": "38e11bca-36f1-4b91-b3a5-0b2104c58029",
 "type": "MeshStandardMaterial",
 "color": 16770655,
 // left out some material properties 
 "stencilFuncMask": 255,
 "stencilFail": 7680,
 "stencilZFail": 7680,
 "stencilZPass": 7680
 }
 ],
 "object": {
 "uuid": "373db2c3-496d-461d-9e7e-48f4d58a507d",
     "type": "Mesh",
 "castShadow": true,
 "layers": 1,
 "matrix": [
 0.5,
 ...
 1
 ],
 "geometry": "15a98944-91a8-45e0-b974-0d505fcd12a8",
 "material": "38e11bca-36f1-4b91-b3a5-0b2104c58029"
 }
}

正如您所看到的,Three.js保存了关于Three.Mesh对象的所有信息。将THREE.Mesh重新加载到Three.js中也只需要几行代码,如下所示:

const fromStorage = localStorage.getItem('json')
if (fromStorage) {
 const structure = JSON.parse(fromStorage)
 const loader = new THREE.ObjectLoader()
 const mesh = loader.parse(structure)
 mesh.material.color = new THREE.Color(0xff0000)
 scene.add(mesh)
}

在这里,我们首先使用保存JSON的名称(在本例中为JSON)从本地存储中获取JSON。为此,我们使用HTML5本地存储API提供的localStorage.getItem函数。接下来,我们需要将字符串转换回JavaScript对象(JSON.parse),并将JSON对象转换回THRE.Mesh。THREE.js提供了一个名为THRE.ObjectLoader的帮助对象,您可以使用它将JSON转换为THRE.Mash。在本例中,我们使用加载器上的parse方法直接解析JSON字符串。加载器还提供了一个加载函数,您可以在其中将URL传递给包含JSON定义的文件。

正如你在这里看到的,我们只保存了一个THREE.Mesh对象,所以我们失去了其他所有东西。如果要保存完整的场景,包括灯光和摄影机,可以使用相同的方法导出场景:

const asJson = scene.toJSON()
localStorage.setItem('scene', JSON.stringify(asJson))

其结果是在JSON中的一个完整的场景描述

image-20230524153706802

图8.6-将一个场景导出到JSON

这可以与我们已经展示的THREE.Mesh对象相同的方式加载。当您只在Three.js中工作时,将当前场景和对象存储在JSON中非常方便,但这不是一种可以与其他工具和程序轻松交换或创建的格式。在下一节中,我们将更深入地了解Three.js支持的一些3D格式

从3D文件格式导入

在本章的开头,我们列出了Three.js支持的许多格式。在本节中,我们将快速介绍这些格式的一些示例。

OBJ和MTL格式

OBJ和MTL是配套格式,经常一起使用。OBJ文件定义几何图形,而MTL文件定义所使用的材质。OBJ和MTL都是基于文本的格式。OBJ文件的一部分如下所示:

v -0.032442 0.010796 0.025935
v -0.028519 0.013697 0.026201
v -0.029086 0.014533 0.021409
usemtl Material 
s 1 
f 2731 2735 2736 2732
f 2732 2736 3043 3044

MTL文件定义了材料,如下:

newmtl Material
Ns 56.862745 
Ka 0.000000 0.000000 0.000000
Kd 0.360725 0.227524 0.127497
Ks 0.010000 0.010000 0.010000
Ni 1.000000 
d 1.000000
illum 2

Three.js很好地支持OBJ和MTL格式,所以如果你想交换3D模型,这是一个很好的选择格式选择。Three.js上可以使用两种不同的加载器。如果您只想加载几何图形,请使用OBJLoader。我们使用了这个加载器作为我们的示例(load-obj.html)。下面的屏幕截图显示了此示例:

image-20230524154511887

图8.7-仅定义几何模型的OBJ模型

从外部文件加载OBJ模型是这样完成的:

import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
new OBJLoader().loadAsync('/assets/models/baymax/Bigmax_White_OBJ.obj').then((model) => {
 model.scale.set(0.05, 0.05, 0.05)
 model.translateY(-1)
 visitChildren(model, (child) => {
 child.receiveShadow = true
 child.castShadow = true
 })
 return model
})

在这段代码中,我们使用OBJLoader从URL异步加载模型。这将返回一个JavaScript promises,解析后该 promises 将包含网格。加载模型后,我们会进行一些微调,并确保模型投射阴影并接收阴影。除了sidesladAsync之外,每个加载程序还提供了一个加载函数,该函数不使用promise,而是使用回调。同样的代码看起来是这样的:

const model = new OBJLoader().load('/assets/models/baymax/Bigmax_White_OBJ.obj', (model) => {
 model.scale.set(0.05, 0.05, 0.05)
 model.translateY(-1)
 visitChildren(model, (child) => {
 child.receiveShadow = true
 child.castShadow = true
 })
 // 用这个模型做些什么
 scene.add(model)
})

在本章中,我们将使用基于Promise的loadAsync方法,因为这避免了嵌套回调,并使将这些类型的调用链接在一起变得更容易。下一个示例(load-obj-mtl.html)使用OBJLoader和MTLLoader来加载模型并直接指定材质。以下屏幕截图显示了此示例:

image-20230524155011546

图8.8-OBJ.MTL模型与模型和材料

在OBJ文件之外使用MTL文件遵循我们在本节前面看到的相同原则:

const model = mtlLoader.loadAsync('/assets/models/butterfly/butterfly.mtl').then((materials) => {
 objLoader.setMaterials(materials)
 return objLoader.loadAsync('/assets/models/butterfly/butterfly.obj').then((model) => {
 model.scale.set(30, 30, 30)
 visitChildren(model, (child) => {
 // 如果已经有法线向量 ,我们就不能合并顶点
 child.geometry.deleteAttribute('normal')
 child.geometry = BufferGeometryUtils.mergeVertices(child.geometry)
 child.geometry.computeVertexNormals()
 child.material.opacity = 0.1
 child.castShadow = true
 })
 const wing1 = model.children[4]
 const wing2 = model.children[5]
 [0, 2, 4, 6].forEach(function (i) { 
 model.children[i].rotation.z = 0.3 * Math.PI })
 [1, 3, 5, 7].forEach(function (i) { 
 model.children[i].rotation.z = -0.3 * Math.PI })
 wing1.material.opacity = 0.9
 wing1.material.transparent = true
 wing1.material.alphaTest = 0.1
 wing1.material.side = THREE.DoubleSide
 wing2.material.opacity = 0.9
 wing2.material.depthTest = false
 wing2.material.transparent = true
 wing2.material.alphaTest = 0.1
 wing2.material.side = THREE.DoubleSide
 return model
 })
})

在我们查看代码之前,首先要提到的是,如果您收到OBJ文件、MTL文件和所需的纹理文件,则必须检查MTL文件是如何引用纹理的。这些应相对于MTL文件进行引用,而不是作为绝对路径。代码本身与我们在THRE.OjLoader中看到的代码没有太大区别。我们要做的第一件事是用THRE.MTL Loader对象加载MTL文件,加载的材料通过setMaterials函数在THRE.ojLoader上设置。

在本例中,我们使用的模型是复杂的。因此,我们在回调中设置了一些特定的属性,以解决一些渲染问题,如下所示:

  • 我们需要合并模型中的顶点,以便将其渲染为平滑模型。为此,我们首先需要从加载的模型中删除已经定义的法线向量,以便使用BufferGeometryUtils.mergeVertices和computeVertexNormals函数为Three.js提供正确渲染模型的信息。
  • 源文件中的不透明度设置不正确,导致机翼不可见。因此,为了解决这个问题,我们自己设置了不透明度和透明属性。
  • 默认情况下,Three.js只渲染对象的一侧。由于我们从两侧观察机翼,因此需要将side属性设置为THRE.DoubleSide值。
  • 当机翼需要在一个机翼上渲染时,机翼会造成一些不需要的伪影。我们通过设置alphaTest属性解决了这个问题。

但是正如您所看到的,您可以很容易地将复杂的模型直接加载到Three.js中,并在浏览器中实时渲染它们。不过,您可能需要微调各种材料属性。

加载gLTF模型

我们已经提到,在Three.js中导入数据时,glTF是一种很好的格式。为了向您展示导入和显示复杂场景是多么容易,我们添加了一个示例,其中我们刚刚从https://sketchfab.com/3d-models/sea-house-bc4782005e9646fb9e6e18df61bfd28d

image-20230524155801706

图8.9-使用Three.js的glTF加载的复杂3D场景

正如您从前面的截图中看到的,这不是一个简单的场景,而是一个复杂的场景,有许多模型、纹理、阴影和其他元素。为了在Three.js中得到这个,我们所要做的就是这样做:

const loader = new GLTFLoader()
return loader.loadAsync('/assets/models/sea_house/scene.gltf').then((structure) => {
 structure.scene.scale.setScalar(0.2, 0.2, 0.2)
 visitChildren(structure.scene, (child) => {
 if (child.material) {
 child.material.depthWrite = true
 }
 })
 scene.add(structure.scene)
})

您已经熟悉异步加载程序,我们唯一需要解决的问题是确保正确设置了材料的depthWrite属性(这似乎是一些glTF模型的常见问题)。就这样,它确实有效。glTF还允许我们定义动画,这是我们将在下一章中进一步研究的内容。

显示完整的乐高模型

除了3D模型(其中模型定义顶点、材质、灯光等)之外,还有各种文件格式,它们没有明确定义几何图形,但有更具体的用途。我们将在本节中介绍LDrawLoader加载程序,它是为了以3D方式渲染乐高模型而创建的。使用此加载程序的工作方式与我们已经多次看到的相同:

loader.loadAsync('/assets/models/lego/10174-1-ImperialAT-STUCS.mpd_Packed.mpd').'/assets/models/lego/10174-1-ImperialATST-UCS.mpd_Packed.mpd'.then((model) => {
 model.scale.set(0.015, 0.015, 0.015)
 model.rotateZ(Math.PI)
 model.rotateY(Math.PI)
 model.translateY(1)
 visitChildren(model, (child) => {
 child.castShadow = true
 child.receiveShadow = true
 })
 scene.add(model))
}

结果看起来非常棒:

image-20230524160114222

图8.10-乐高帝国式的AT-ST模型

image-20230524160114222

图8.11-乐高X型机翼战斗机

正如你所看到的,它显示了一套乐高套装的完整结构。你可以使用很多不同的型号:

如果您想探索更多的模型,您可以从LDraw存储库中下载它们:https://omr.ldraw.org/

加载基于像素的模型

创建3D模型的另一种有趣方法是使用体素。这允许你使用小立方体构建模型,并使用Three.js进行渲染。例如,你可以使用这样的工具在Minecraft之外创建Minecraft结构,并在以后将其导入Minecraft。一个免费的体素实验工具是MagicaVoxel(https://ephtracy.github.io/). 此工具允许您创建体素模型,例如:

image-20230524160715674

图8.12-使用MagicaVoxel创建的示例模型

有趣的是,您可以使用VOXLoader加载器轻松地在Three.js中导入这些模型,例如:

new VOXLoader().loadAsync('/assets/models/vox/monu9.vox').
then((chunks) => {
 const group = new THREE.Group()
 for (let i = 0; i < chunks.length; i++) {
 const chunk = chunks[i]
 const mesh = new VOXMesh(chunk)
 mesh.castShadow = true
 mesh.receiveShadow = true
 group.add(mesh)
 }
 group.scale.setScalar(0.1)
 scene.add(group)
}

在模型文件夹中,您可以找到几个vox模型。下面的屏幕截图显示了用Three.js加载的情况:

image-20230524160715674

图8.13-使用Three.js的Vox模型加载

下一个加载器是另一个非常具体的一个。我们将研究如何从PDB格式中渲染蛋白质。

显示来自PDB的蛋白质

image-20230524161510621

图8.14-使用Three.js和PDBLoader可视化蛋白质

加载PDB文件的方式与以前的格式相同,如下所示:

PDBLoader().loadAsync('/assets/models/molecules/caffeine.pdb').
then((geometries) => {
 const group = new THREE.Object3D()
 // 创造原子
 const geometryAtoms = geometries.geometryAtoms
 for (let i = 0; i < geometryAtoms.attributes.
 position.count; i++) {
 let startPosition = new THREE.Vector3()
 startPosition.x = geometryAtoms.attributes.
 position.getX(i)
 startPosition.y = geometryAtoms.attributes.
 position.getY(i)
 startPosition.z = geometryAtoms.attributes.position.getZ(i)
 let color = new THREE.Color()
 color.r = geometryAtoms.attributes.color.getX(i)
 color.g = geometryAtoms.attributes.color.getY(i)
 color.b = geometryAtoms.attributes.color.getZ(i)
 let material = new THREE.MeshPhongMaterial({
 color: color
 })
 let sphere = new THREE.SphereGeometry(0.2)
 let mesh = new THREE.Mesh(sphere, material)
 mesh.position.copy(startPosition)
 group.add(mesh)
 }
 // 创建绑定
 const geometryBonds = geometries.geometryBonds
 for (let j = 0; j < 
 geometryBonds.attributes.position.count; j += 2) {
 let startPosition = new THREE.Vector3()
 startPosition.x = geometryBonds.attributes.
 position.getX(j)
 startPosition.y = geometryBonds.attributes.position.
 getY(j)
 startPosition.z = geometryBonds.attributes.position.
 getZ(j)
 let endPosition = new THREE.Vector3()
 endPosition.x = geometryBonds.attributes.position.
 getX(j + 1)
 endPosition.y = geometryBonds.attributes.position.
 getY(j + 1)
 endPosition.z = geometryBonds.attributes.position.
 getZ(j + 1)
 // 使用起点和结束来创建曲线,并使用曲线进行绘制
 // 一根连接原子的管
 let path = new THREE.CatmullRomCurve3([startPosition, 
 endPosition])
 let tube = new THREE.TubeGeometry(path, 1, 0.04)
 let material = new THREE.MeshPhongMaterial({
 color: 0xcccccc
 })
 let mesh = new THREE.Mesh(tube, material)
 group.add(mesh)
 }
 group.scale.set(0.5, 0.5, 0.5)
 scene.add(group)
}

正如你从这个示例代码中看到的,我们实例化了一个THREE.PDBLoader对象,并传入我们想要加载的模型文件,一旦加载了模型,我们就会对其进行处理。在这种情况下,模型由两个属性组成:geometryAtoms和geometryBonds。来自的位置属性几何原子包含单个原子的位置,颜色属性可用于为单个原子着色。对于原子之间的联系,使用了几何键。

根据位置和颜色,我们创建一个THREE.Mesh对象并将其添加到一个组中:

let sphere = new THREE.SphereGeometry(0.2)
 let mesh = new THREE.Mesh(sphere, material)
 mesh.position.copy(startPosition)
 group.add(mesh)

关于原子之间的联系,我们也遵循同样的方法。我们得到了连接的起始位置和结束位置,并使用这些位置来绘制连接:

let path = new THREE.CatmullRomCurve3([startPosition, 
 endPosition])
let tube = new THREE.TubeGeometry(path, 1, 0.04)
let material = new THREE.MeshPhongMaterial({
 color: 0xcccccc
})
let mesh = new THREE.Mesh(tube, material)
group.add(mesh)

对于连接,我们首先使用THREE.CatmullRomCurve3创建一个3D路径。该路径用作THREE.TubeGeometry的输入,用于在原子之间创建连接。所有的连接和原子都被添加到一个组中,并且该组被添加到场景中。您可以从PDB下载许多型号。例如,下面的屏幕截图显示了钻石的结构:

image-20230524162146162

图8.15:金刚石的结构

在下一节中,我们将介绍Three.js对PLY模型的支持,它可以用于加载点云数据。

从PLY模型中加载点云

使用PLY格式与其他格式没有太大区别。您包括加载器并处理加载的模型。然而,对于最后一个例子,我们将做一些不同的事情。我们将使用该模型中的信息来创建粒子系统,而不是将模型渲染为网格(请参见以下屏幕截图中的load-ply.html示例):

image-20230524162347976

图8.16-从PLY模型加载的点云

渲染前面的屏幕截图的JavaScript代码实际上非常简单;它看起来是这样的:

const texture = new THREE.TextureLoader().load('/assets/textures/particles/glow.png')
const material = new THREE.PointsMaterial({
 size: 0.15,
 vertexColors: false,
 color: 0xffffff,
 map: texture,
 depthWrite: false,
 opacity: 0.1,
 transparent: true,
 blending: THREE.AdditiveBlending
})
return new PLYLoader().loadAsync('/assets/models/carcloud/carcloud.ply').then((model) => {
 const points = new THREE.Points(model, material)
 points.scale.set(0.7, 0.7, 0.7)
 scene.add(points)
})

正如你所看到的,我们使用THREE.PLYLoader来加载模型,并使用这个几何体作为THRE.Points的输入。我们使用的材料与第7章“点和精灵”中最后一个例子中使用的材料相同。正如您所看到的,使用Three.js,可以很容易地组合来自不同来源的模型,并以不同的方式呈现它们,所有这些都只需几行代码。

其他加载器

在本章的开头,在“从外部资源加载几何图形”部分中,我们展示了由Three.js提供的所有不同加载器的列表。我们在第8章的资料来源中提供了所有这些的例子:

image-20230524162609061

图8.17-目录显示了所有加载器的例子

所有这些加载器的源代码都遵循与我们在本章中解释的加载器相同的模式。只需加载模型,确定要显示已加载模型的哪个部分,确保缩放和位置正确,并将其添加到场景中。

总结

在Three.js中使用来自外部来源的模型并不难,特别是对于简单的模型——您只需要采取几个简单的步骤。

当使用外部模型时,或者使用分组和合并来创建它们时,最好记住几件事。你需要记住的第一件事是,当你对对象进行分组时,它们仍然可以作为单独的对象使用。应用于父对象的变换也会影响子对象,但您仍然可以单独变换子对象。除了分组之外,还可以将几何图形合并在一起。使用这种方法,您将丢失单个几何图形,并获得一个新的几何图形。当您处理需要渲染的数千个几何体并且遇到性能问题时,这一点尤其有用。如果要控制同一几何体的大量网格,最后一种方法是使用THREE.InstancedMesh对象或THREE.InstancedBufferGeometry对象,这样可以定位和变换单个网格,但仍能获得良好的性能。

Three.js支持大量的外部格式。当使用这些格式加载程序时,最好查看源代码并添加console.log语句,以确定加载的数据的真实外观。这将帮助您了解获得正确网格所需的步骤,并将其设置为正确的位置和比例。通常,当模型没有正确显示时,这是由其材质设置引起的。可能是使用了不兼容的纹理格式,不透明度定义不正确,或者该格式包含到纹理图像的不正确链接。通常最好使用测试材料来确定模型本身是否正确加载,并将加载的材料记录到JavaScript控制台以检查是否有意外值。

如果您想重用自己的场景或模型,只需调用asJson函数并使用ObjectLoader再次加载即可导出这些场景或模型

您在本章和前几章中使用的模型大多是静态模型。它们没有动画,不会四处移动,也不会改变形状。在第9章中,您将学习如何为模型设置动画,使其栩栩如生。除了动画,下一章还将解释Three.js提供的各种相机控件。使用相机控件,您可以围绕场景移动、平移和旋转相机。