Skip to main content

创建和加载高级网格和几何体

在本章中,我们将探讨一些创建和加载高级和复杂几何体和网格的不同方法。 在第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.MeshTHREE.Scene的基类,但本身不包含任何内容, 也不会导致任何内容被渲染。 在这个例子中,我们使用add函数向这个场景中添加了大量的立方体。 对于这个例子,我们添加了一个菜单,您可以使用该菜单更改网格的位置。 每当您使用此菜单更改属性时,THREE.Group对象的相关属性也会更改。 例如,在下一个例子中,您可以看到当我们缩放这个THREE.Group对象时, 所有嵌套的立方体也会被缩放:

如果您想更多地尝试使用THREE.Group对象, 一个很好的练习是修改示例,以便THREE.Group实例本身在x轴上旋转, 而各个立方体在它们的y轴上旋转。

note

使用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) 函数将它们合并在一起。 结果是一个单一的大型几何体,我们可以将其添加到场景中。 最大的缺点是您失去了对个别立方体的控制,因为它们都被合并成一个大型几何体。 如果要移动、旋转或缩放单个立方体, 您将无法实现(除非搜索正确的面和顶点并分别定位它们)。

note

通过构造实体几何体创建新几何体

除了像本章中所见的合并几何体的方式外, 我们还可以使用构造实体几何体(CSG)来创建几何体。 使用CSG,您可以对两个几何体应用操作(通常是加法、减法、差异和交集), 以将它们组合在一起。这些库将基于所选操作创建一个新的几何体。 例如,使用CSG,可以非常容易地在一个侧面上创建具有球状凹陷的实心立方体。 您可以使用Three.js的three-bvh-csghttps://github.com/gkjohnson/three-bvh-csg)和 Three.csghttps://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扫描。可以在此处找到大量信息和示例。
  • OBJMTLOBJ是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支持的格式。 在这一节中,我们将快速浏览一下其中一些格式的示例。

OBJMTL格式

OBJMTL是配套格式,通常一起使用。 OBJ文件定义了几何形状,而MTL文件定义了使用的材质。 OBJMTL都是基于文本的格式。 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

OBJMTL格式在Three.js中得到很好的支持,因此如果您想要交换3D模型,这是一个不错的选择。 Three.js有两个不同的加载器可供使用。 如果只想加载几何体,可以使用OBJLoader。我们在我们的示例(load-obj.html)中使用了此加载器。 以下截图显示了此示例:

从外部文件加载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异步加载模型。 这返回一个JavaScript promise,当解析时,将包含网格。 加载模型后,我们进行一些微调,并确保模型既投射阴影又接收阴影。 除了loadAsync,每个加载器还提供了一个load函数,该函数与回调一起工作,而不是使用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)使用OBJLoaderMTLLoader一起加载模型并直接分配材质。 以下截图显示了此示例:

除了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文件进行引用,而不是绝对路径。 代码本身与我们为THREE.ObjLoader看到的代码并没有太大区别。 我们首先使用THREE.MTLLoader对象加载MTL文件, 加载的材质通过setMaterials函数设置给THREE.ObjLoader

我们用作示例的模型在这种情况下比较复杂。 因此,在回调中设置了一些特定的属性,以解决一些渲染问题, 如下所示:

  • 我们需要合并模型中的顶点,以便其呈现为平滑的模型。 为此,我们首先需要从加载的模型中删除已定义的法线向量, 以便使用BufferGeometryUtils.mergeVerticescomputeVertexNormals函数向Three.js提供正确渲染模型的信息。
  • 源文件中的不透明度设置不正确,导致翅膀不可见。 因此,为了修复这个问题,我们自己设置了不透明度和透明属性。
  • 默认情况下,Three.js只渲染对象的一侧。 由于我们从两侧看翅膀,我们需要将side属性设置为THREE.DoubleSide
  • 翅膀在彼此之上渲染时引起了一些不希望的伪影。 我们通过设置alphaTest属性来解决这个问题。

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

加载gLTF模型

我们已经提到,在Three.js中导入数据时,glTF是一个很好的格式。 为了向您展示即使是导入和显示复杂场景也有多么简单,我们添加了一个示例, 我们只是从https://sketchfab.com/3d-models/sea-house-bc4782005e9646fb9e6e18df61bfd28d中获取了一个模型: 正如您从前面的截图中所看到的, 这不是一个简单的场景,而是一个复杂的场景, 有许多模型、纹理、阴影和其他元素。 要在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-ST-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);
});

结果看起来真的很棒: 正如您所看到的,它展示了一个完整的乐高套装的结构。有许多不同的模型可供使用: 如果您想探索更多模型,可以从LDraw存储库下载它们:https://omr.ldraw.org/

加载基于体素的模型

创建3D模型的另一种有趣方法是使用体素。这使您可以使用小立方体构建模型,并使用Three.js渲染它们。例如,您可以使用这样的工具在Minecraft之外创建Minecraft结构,并在以后导入它们到Minecraft中。一个用于尝试体素的免费工具是MagicaVoxel(https://ephtracy.github.io/)。该工具允许您创建像这样的体素模型:

有趣的部分是您可以使用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);
});

models文件夹中,您可以找到一些vox模型。以下截图显示了在Three.js中加载的模型的样子:

下一个加载器是另一个非常特定的加载器。我们将看看如何从PDB格式渲染蛋白质。

展示PDB中的蛋白质

PDB网站(www.rcsb.org)包含许多不同分子和蛋白质的详细信息。 除了这些蛋白质的解释外,它还提供了以PDB格式下载这些分子结构的方式。 Three.js提供了用于加载PDB格式文件的加载器。 在本节中,我们将演示如何解析PDB文件并在Three.js中进行可视化。 通过包含此加载器,我们将创建以下分子描述的3D模型(请参阅load-pdb.html示例):

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

new 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对象, 并传入我们想要加载的模型文件,一旦模型加载完成,我们就对其进行处理。 在本例中,该模型由两个属性组成:geometryAtomsgeometryBondsgeometryAtoms的position属性包含各个原子的位置,而color属性可用于给原子着色。 连接原子的部分使用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下载许多模型。例如,以下截图显示了钻石的结构:

从PLY模型加载点云

与其他格式相比,使用PLY格式并没有太大的区别。 您只需包含加载器并处理加载的模型。 然而,在这个最后的例子中,我们将尝试一些不同的方法。 我们将使用此模型的信息创建一个粒子系统, 而不是将模型呈现为网格(请参阅以下截图中的load-ply.html示例):

渲染上述截图的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加载模型, 并将此几何体用作THREE.Points的输入。 我们使用的材质与第7章最后一个示例中使用的相同,即点和精灵。 正如您所见,使用Three.js,将来自各种来源的模型组合并以不同方式呈现它们非常容易,只需几行代码。

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

其他加载器

在本章的开头,在“从外部资源加载几何体”一节中, 我们向您展示了Three.js提供的所有不同加载器的列表。 我们在第8章的源代码中提供了所有这些加载器的示例:

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

总结

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

在使用外部模型或使用分组和合并创建模型时,有几件事情需要记住。 您需要记住的第一件事是,当您将对象分组时,它们仍然作为单独的对象可用。 应用于父对象的变换也会影响子对象,但您仍然可以单独转换子对象。 除了分组,还可以将几何体合并在一起。 使用这种方法,您会失去单独的几何体,并得到一个新的单一几何体。 当您需要渲染数千个几何体并遇到性能问题时,这尤其有用。 如果要控制相同几何体的大量网格, 则最终方法是使用THREE.InstancedMesh对象或THREE.InstancedBufferGeometry对象, 它允许您定位和转换单个网格,但仍然获得出色的性能。

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

如果要重用自己的场景或模型,您可以简单地通过调用asJson函数导出它们, 然后使用ObjectLoader重新加载它们。

在本章和前几章中使用的模型主要是静态模型。 它们没有动画,不会移动,也不会改变形状。 在第9章,您将学习如何对模型进行动画处理,使它们栩栩如生。 除了动画之外,下一章还将解释Three.js提供的各种摄像机控制。 通过摄像机控制,您可以在场景中移动、平移和旋转相机。