创建和加载高级网格和几何体
在本章中,我们将探讨一些创建和加载高级和复杂几何体和网格的不同方法。 在第5章“学习使用几何体”和第6章“探索高级几何体”中, 我们向您展示了如何使用Three.js的内置对象创建一些高级几何体。 在本章中,我们将使用以下两种方法来创建高级几何体和网格:
- 几何体分组和合并
- 从外部资源加载几何体
我们从“分组和合并”方法开始。
使用此方法,我们使用标准的Three.js分组(THREE.Group)和BufferGeometryUtils.mergeBufferGeometries()函数来创建新对象。
几何体分组和合并
在本节中,我们将查看Three.js的两个基本功能: 将对象分组在一起和将多个几何体合并 为单个几何体。 我们将从分组对象开始。
将对象分组在一起
在先前的一些章节中,您已经看到在使用多个材质时如何将对象分组。 当使用多个材质从几何体创建网格时,Three.js会创建一个组。 您的几何体的多个副本被添加到此组中,每个副本都有自己的特定材质。 这个组被返回,因此它看起来像一个使用多个材质的网格。 但实际上,它是一个包含许多网格的组。
创建组非常简单。
您创建的每个网格都可以包含子元素,可以使用add函数添加子元素。
将子对象添加到组中的效果是您可以移动、缩放、旋转和转换父对象,
所有子对象也将受到影响。在使用组时,您仍然可以引用、修改和定位各个几何体。
您唯一需要记住的是所有位置、旋转和平移都是相对于父对象完成的。
让我们看一个示例(grouping.html)如下截图:
在这个例子中,您看到了大量的立方体,它们作为单个组添加到场景中。 在查看控件和使用组的效果之前,让我们快速看一下我们如何创建这个网格:
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实例。
该对象几乎与THREE.Object3D相同,
它是THREE.Mesh和THREE.Scene的基类,但本身不包含任何内容,
也不会导致任何内容被渲染。
在这个例子中,我们使用add函数向这个场景中添加了大量的立方体。
对于这个例子,我们添加了一个菜单,您可以使用该菜单更改网格的位置。
每当您使用此菜单更改属性时,THREE.Group对象的相关属性也会更改。
例如,在下一个例子中,您可以看到当我们缩放这个THREE.Group对象时,
所有嵌套的立方体也会被缩放:
如果您想更多地尝试使用THREE.Group对象,
一个很好的练习是修改示例,以便THREE.Group实例本身在x轴上旋转,
而各个立方体在它们的y轴上旋转。
使用THREE.Group的性能影响
在我们转到下一节,看看合并的地方,关于性能的一个快速说明。
当您使用THREE.Group时,此组内的所有单独网格都被视为单独的对象,
Three.js需要管理和渲染这些对象。如果场景中有大量的对象,您会看到性能明显下降。
如果您查看图8.2左上角,您会看到屏幕上有5000个立方体时,
我们的帧速率(FPS)大约为56。
并不算太糟糕,但通常我们的运行速度会在120 FPS左右。
Three.js提供了另一种方式,我们仍然可以控制单独的网格,但获得更好的性能。
这是通过THREE.InstancedMesh实现的。
如果要渲染具有相同几何体但具有不同变换(例如旋转、缩放、颜色或任何其他矩阵变换)的大量对象,
这个对象非常适用。
我们创建了一个名为instanced-mesh.html的示例,展示了它是如何工作的。
在这个例子中,我们渲染了250,000个立方体,仍然保持了良好的性能:
要使用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.Inst
ancedMesh(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.InstancedMesh对象与THREE.Group的主要区别在于,
我们需要预先定义要使用的材质和几何体,以及我们要创建多少个此几何体的实例。
要定位或旋转我们的实例之一,我们需要使用THREE.Matrix4实例提供变换。
幸运的是,我们不需要深入矩阵背后的数学,
因为Three.js在THREE.Matrix4实例上提供了一些辅助函数,
用于定义旋转、 平移和其他一些变换。
在这个例子中,我们只是将每个实例放在一个随机位置。
因此,如果您正在处理少量网格(或使用不同几何体的网格),
如果要将它们组合在一起,应使用THREE.Group对象。
如果处理共享几何体和材质的大量网格,
可以使用THREE.InstancedMesh对象或
THREE.InstancedBufferGeometry对象以获得显著的性能提升。
在下一节中,我们将看一下合并,在那里您将合并多个单独的几何体,
最终得到一个单独的THREE.Geometry对象。
合并几何体
在大多数情况下,使用组可以轻松操作和管理大量的网格。
然而,当您处理非常大量的对象时,性能将成为一个问题,
因为Three.js必须单独处理组的所有子元素。
通过BufferGeometryUtils.mergeBufferGeometries,
您可以将几何体合并在一起,创建一个组合的几何体,
因此Three.js只需要管理这个单一的几何体。
在图8.4中,您可以看到这是如何工作的以及它对性能的影响。
如果您打开merging.html示例,您会再次看到一个场景,
其中有相同分布的半透明立方体,
但我们将它们合并成了一个THREE.BufferGeometry对象:
正如您所看到的,我们可以轻松渲染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);
在这段代码片段中,我们创建了大量的THREE.BoxGeometry对象,
然后使用BufferGeometryUtils.mergeBufferGeometries(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.js提供的编程方法并非总是最佳和最简便的选项。 幸运的是,Three.js提供了其他几种选项来创建几何体。 在下一节中,我们将看看如何从外部资源加载几何体和网格。
从外部资源加载几何体
Three.js可以读取许多3D文件格式,并导入在这些文件中定义的几何体和网格。
需要注意的是,并非始终支持这些格式的所有功能。
因此,有时可能会出现纹理问题,或者材质可能没有正确设置的情况。
用于交换模型和纹理的新事实上的标准是glTF,因此,如果要加载外部创建的模型,
将这些模型导出到glTF格式通常会在Three.js中获得最佳结果。
在本节中,我们将更深入地了解一些由Three.js支持的格式,但不会向您展示所有加载器。 以下列表概述了Three.js支持的格式:
AMF:AMF是另一种3D打印标准,但不再处于积极开发中。 有关此标准的更多信息,请参见维基百科页面。3DM:3DM是Rhinoceros使用的格式,Rhinoceros是创建3D模型的工具。 有关Rhinoceros的更多信息,请查看此处。3MF:3MF是3D打印中使用的标准之一。 有关此格式的信息,请访问3MF联盟主页。COLLAborative Design Activity (COLLADA):COLLADA是一种使用基于XML的格式定义数字资产的格式。 这是一种广泛使用的格式,几乎所有3D应用程序和渲染引擎都支持。Draco:Draco是一种以非常高效的方式存储几何体和点云的文件格式。 它规定了这些元素最佳的压缩和解压缩方式。 有关Draco的 详细信息,请参见其GitHub页面:https://github.com/google/draco。GCode:GCode是与3D打印机或数控机床交流的标准方式。 在打印模型时,3D打印机可以通过发送GCode命令来进行控制。 此标准的详细信息在此论文中有描述。glTF:这是一种规范,定义了3D场景和模型如何在不同应用程序和工具之间交换和加载, 并且正在成为在Web上交换模型的标准格式。 它们以二进制格式(.glb扩展名)和基于文本的格式(.gltf扩展名)提供。 有关此标准的更多信息,请参见这里。Industry Foundation Classes (IFC):这是建筑信息建模(BIM)工具使用的开放文件格式。 它包含建筑物的模型以及有关使用的材料的大量附加信息。 有关此标准的更多信息,请参见此处。JSON:Three.js有自己的JSON格式,您可以使用它来声明性地定义几何体或场景。 尽管这不是官方格式,但在想要重用复杂几何体或场景时,它非常易于使用且非常方便。KMZ:这是Google Earth上用于3D资产的格式。 有关更多信息,请访问此处。LDraw:LDraw是一种开放标准,可用于创建虚拟LEGO模型和场景。 有关更多信息,请查看LDraw主页。LWO:这是LightWave 3D使用的文件格式。 有关LightWave 3D的更多信息,请查看此处。NRRD:NRRD是用于可视化体积数据的文件格式。 例如,它可以用于渲染CT扫描。可以在此处找到大量信息和示例。OBJ和MTL:OBJ是Wavefront Technologies首次开发的简单3D格式。 它是最广泛采用的3D文件格式之一,用于定义对象的几何形状。MTL是OBJ的伴随格式,在MTL文件中指定了OBJ文件中对象的材质。 Three.js还有一个自定义的OBJ导出器,称为OBJExporter, 如果您想要从Three.js导出模型到OBJ,可以使用它。PCD:这是描述点云的开放格式。 有关更多信息,请访问此处。PDB:这是由蛋白质数据银行(PDB)创建的非常专业化的格式,用于指定蛋白质的外观。 Three.js可以加载和可视化以此格式指定的蛋白质。Polygon File Format (PLY):这通常用于存储来自3D扫描仪的信息。Packed Raw WebGL Model (PRWM):这是另一种专注于高效存储和解析3D几何体的格式。 有关此标准以及如何使用它的更多信息,请参见此处。STereoLithography (STL):这在快速原型制作中被广泛使用。 例如,3D打印机的模型通常以STL文件定义。 Three.js还有一个自定义的STL导出器,称为STLExporter.js, 如果您想要从Three.js导出模型到STL,可以使用它。SVG:SVG是定义矢量图形的标准方法。 此加载器允许您加载SVG文件并返回一组THREE.Path元素,您可以用于挤压或在2D中渲染。3DS:Autodesk 3DS格式。有关更多信息,请查看此处。TILT:TILT是Tilt Brush使用的格式,Tilt Brush是一种允许您在虚拟现实中绘画的工具。 有关更多信息,请访问此处。VOX:MagicaVoxel使用的格式,MagicaVoxel是一种可以用来创建体素艺术的免费工具。 有关更多信息,请查看MagicaVoxel的主页:https://ephtracy.github.io/。Virtual Reality Modeling Language (VRML):这是一种文本格式,允许您指定3D对象和世界。 它已被X3D文件格式取代。Three.js不支持加载X3D模型,但可以将这些模型轻松转换为其他格式。 有关更多信息,请访问X3D主页。Visualization Toolkit (VTK):这是由VTK定义并用于指定顶点和面的文件格式。 有两种可用的格式:一种是二进制格式,另一种是基于文本的ASCII格式。Three.js仅支持基于ASCII的格式。XYZ:这是用于描述三维空间中点的非常简单的文件格式。 有关更多信息,请访问此处。
在第9章《动画和摄像机移动》中,当我们查看动画时,我们将重新访问其中的一些格式(并查看一些附 加的格式)。
正如您从这个列表中所看到的,Three.js支持非常多的3D文件格式。 我们不会描述其中的所有格式,只描述一些最有趣的。 我们将从JSON加载器开始,因为它提供了一种很好的方式来存储和检索您自己创建的场景。
在Three.js中保存和加载JSON格式
在Three.js中,您可以使用JSON格式执行两种不同的场景。
您可以使用它来保存和加载单个THREE.Object3D对象(这意味着您也可以使用它来导出THREE.Scene对象)。
为了演示保存和加载,我们创建了一个基于THREE.TorusKnotGeometry的简单示例。
通过这个示例,您可以创建一个环结,
就像我们在第5章中所做的那样,并且可以使用保存/加载菜单中的保存按钮保存当前几何体。
对于此示例,我们使用HTML5本地存储API进行保存。
此API允许我们轻松地将持久信息存储在客户端浏览器中,
并在以后的某个时间检索它(甚至在浏览器已关闭并重新启动后):
在上面的截图中,您可以看到两个网格——红色的是我们加载的网格,黄色的是原始网格。 如果您自己打开此示例并单击保存按钮,则将存储网格的当前状态。 现在,您可以刷新浏览器,单击加载,保存的状态将以红色显示。
在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,
// 省略了一些材质属性
"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。
为此,我们使用HTML5本地存储API提供的localStorage.getItem函数。
接下来,我们需要将字符串转换回JavaScript对象(JSON.parse),
并将JSON对象转换回THREE.Mesh。
Three.js提供了一个称为THREE.ObjectLoader的辅助对象,
您可以使用它将JSON转换为THREE.Mesh。
在这个例子中,我们在加载器上使用了parse方法直接解析JSON字符串。
加载器还提供了一个load函数,您可以将URL传递给包含JSON定义的文件。
正如您在这里看到的,我们只保存了一个THREE.Mesh对象,因此失去了其他一切。
如果要保存包括灯光和摄像机在内的完整场景,您可以使用相同的方法导出场景:
const asJson = scene.toJSON();
localStorage.setItem('scene', JSON.stringify(asJson));
这的结果是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
OBJ