本文介绍如何用Three.js程序化生成亿万个篱笆墙3D模型。

1、问题

假设我们有一种产品要卖给客户,并且该产品有多种选择。 如何向我们的客户解释所有这些选项的含义? 如果客户选择选项 A 或选项 B,产品的外观将如何。

最简单的方法是为每个选项使用单独的图像。 客户选择 A 并将看到与其关联的图像。 你需要准备多少张图片? 这取决于选项结构。 如果这是一个线性结构,假设产品有一个参数 X,这个参数有选项 A、B、C 和 D,这意味着你需要 4 张图像。 但是考虑下,如果产品有三个参数会怎么样:

P2: [A, B, C],
P3: [A, B]

如何计算组合的数量? 对于此类问题,组合数学中有一条特殊的规则,它非常直观,称为“乘积法则”或“乘法法则”。 数学很简单,我们只需将每个参数中的选项数相乘,就可以得到所需的数字。 对于此示例,它将是 4x3x2 = 24 个变体。

这听起来不太可怕,我们可以估计 8 个参数的变化数量吗? 假设每个参数至少有 2 个选项。 那时会发生什么? 我们可以说变化的数量至少为 2⁸ = 256。但如果我们记住前两个参数分别有4个和 3 个选项,它将是 384 个变化。 让我们尝试为我们的变化空间绘制 20 个参数的估计图:

这是基于每个参数只有 2 个选项的想法的估计。 在 19 个参数之后,它已经超过 100 万种变化。 还是可行的。 但是,如果某些参数有超过 2 个选项怎么办? 空间快速增长到超过数万亿种变化,很快我们预定义的图像集将需要比我们在宇宙可见部分中拥有的更多的原子。

在特定项目中,我们有一组 28 个参数以及以下选项:

[16, 9, 5, 12, 3, 6, 7, 10, 6, 8, 4, 2, 13, 12, 9, 11, 4, 3, 3, 7, 4, 8, 5, 3, 11, 8, 4, 3]

它为我们提供了约 8.24²¹ 的变化空间。 或者如果遵循大数符号的名称:8 Sextillions 变体。

2、解决方案

我们可以通过为每个用户的选择实时创建图像的程序来解决可视化问题。 它将允许我们不存储千兆字节的图像,并且仍然提供所选选项的可视化表示。

我们将产品的配置定义为一组值:config = [A, C, A, B, D...N]; 顺序很重要。 每个值都与相关数据相关联,并且可以转换为我们称为 modelState 的对象:

{
'A': {code: string, slug: string, title: string, size: number[],'C': {code, slug, …
}

此对象包含有关可用于在 3D 模型构建过程中做出构建决策的大小和代码信息。

我们使用工厂模式来隐藏复杂性并提供更方便的界面。 所以我们的 ProductFactory 应该接受 modelState 作为参数并构建产品:

new ProductFactory(modelState).buildProductA();

3、工厂

在 Factory 内部,我们为不同类型的产品定义了多个 Builder。 Builder 是我们开始与变化的复杂性作斗争的地方。 为了解决复杂性,我们将决策分成几个大块。

第一个是测量。 而这个类允许我们基于modelState进行计算,而不需要任何关于3d空间的知识。

第二块是放置器/提供者。 抽象 Placer 类可以访问相关的 Provider,并且它只能做一件事来将来自 Provider 的部件放置在 3d 空间中。

我们用具体的 Placers 扩展 Placer 类,例如 Picket Placer,它与具体的提供者一起使用。 它们一起封装了我们模型这一部分的逻辑。 提供者进行测量,创建所需的部件,并将它们放入全局模型 PartsPool。 然后贴片机可以找到需要的零件,并按照既定的计划将它们一一放置:

所有 Placer 都按顺序工作。 顺序在此过程中很重要,因为在放置器内部,我们应该获得有关先前放置的零件的位置或尺寸的信息。 这类似于人们在现实世界中构建对象的方式。 例如,首先需要放置地基,然后添加一些柱子,然后安装栏杆等。

但是如何创建具有正确纹理的程序 3d 对象呢?

4、几何和纹理

three.js 的乐趣就此开始。 这个框架为我们提供了一个很好的3 维空间和一堆实用程序来处理纹理和矢量。 为了简化使用对象的工作,我们可以为 PartInstance 定义一个抽象类,其中包含一组常用方法,例如材料、几何、材料选项、名称等。可以使用其他类扩展此类,以实现特定的几何类型。 在我们的例子中,我们定义了一组 Box 类和 Cylinder 类:

为盒子创建几何体是微不足道的——我们可以像这样用给定的参数创建一个盒子几何体:

new THREE.BoxBufferGeometry(x, y, z);

但是,如果在任何图形编辑器中使用 3d 对象,你可能会知道在对象上包裹纹理并不是那么容易。 包裹过程需要有关特定几何面的知识。 你需要知道每个面的坐标以及要环绕对象的纹理的尺寸。

为了在 three.js 中为一个盒子实现这一点,我们应该定义 6 个几何组:

geometry.addGroup(0, 6, Side.Right);
geometry.addGroup(6, 6, Side.Left);
…
geometry.addGroup(30, 6, Side.Back);

然后我们必须计算每个组中的一组 UV 坐标。 并将其保存为几何属性:

geometry.setAttribute(‘uv’, new THREE.BufferAttribute(uvs, 2));

Three.js 提供了一个用于创建 Mesh 的类,它基本上是 Geometry 和 Material 的组合。 它有助于将材料分配给表面。 但它没有进行正确的纹理包装。

材质可以是单个 THREE.Material 或材质数组。 如果你尝试将一组材质与没有组的几何体组合在一起,则网格类会将每个纹理分配给一个单独的多边形,其纵横比为 1:1。 这就是为什么正确计算组中的 UV 坐标很重要的原因。 通过这种方式,我们可以为每个 Box Polygons 设置正确的纵横比。

5、性能问题

THREE.Material什么?这是一个描述对象外观的类。在该类的每个实例中,它都有一个 TextureMap,可以简单地表示为 Bitmap 或 Image。每个材质都包含多个贴图,例如法线、置换、自发光等。正如你已经知道的,我们为每个 Box 网格定义了 6 种材质。这意味着对于每个盒子,我们将在内存中创建一组单独的位图实体。如果你的模型包含数百个盒子怎么办?它将在几秒钟内消耗 10Gb 的内存。

如果你有单一的 MaterialFactory,它将允许你不为每个请求创建一个新的 Material,而是重用现有的一个。听起来不错,但有一个问题。如果材料相同,它们看起来总是相同的。

在这里,我们的几何组再次提供帮助。当 Mesh 类将 Material 包裹在 Geometry 周围时,它会检查 UV 属性,如果我们在 Texture 空间中定义偏移量,我们可以模拟每个新 Box 实例的随机外观。

从技术上讲,我们在内存中只有一个 Bitmap 实例。

你可以在这里实时试用我们的 Fence Builder:

6、使用规则减少变化空间

当然,大多数模型变体永远不会被构建。 而且我们不应该允许用户使用任何参数来构建模型,其中一些参数没有任何意义。 为了避免错误的组合,我们建立了两个系统:限制系统和规则系统。 但这是一个单独的大话题。


原文链接:Procedural generation of 3d objects with three.js

BimAnt翻译整理,转载请标明出处