在这个教程中,我们将学习如何在 three.js 中制作自己的自定义程序几何,以及如何利用程序几何来制作吸引人的低多边形地形。

学习本教程需要你具备以下基本技能:

  • 基本熟悉three.js应用的结构
  • 基础编程知识
  • 3d几何的基本知识(顶点,uvs,法线等)
  • 任何具有 webgl 兼容网络浏览器的机器
  • 关于如何运行 javascript 代码的知识(本地或使用类似jsfiddle的东西)

如果你阅读了我之前的教程,会看到three.js 提供了许多不同的内置基本几何类型,再加上你可以轻松地将自己的3D 模型导入到three.js 中以及为什么需要制作你自己的几何?好吧,一个答案当然是为了好玩!如果你问我这就是你需要的唯一答案。

但是,对于更务实的读者来说,如果你想构建每次运行程序时都独一无二的几何图形,那么生成自己的几何图形也是有益的。或者,如果几何图形需要适应你事先不知道的某些约束,并且使其不适合存储。

1、基本几何体

首先,我们从一个基本Geometry实例开始。在上一个教程中,我们查看了许多不同类型的几何图形,例如SphereGeometryCylinderGeometry,但出于我们的目的,我们需要一个普通的空Geometry对象。

var geometry = new THREE.Geometry();
var material = new THREE.MeshBasicMaterial();
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

现在,我们开始制作实际的几何图形。Three.js 至少需要两件东西才能使用Geometry。首先它需要vertices,如你所知,3D 图形是通过光栅化三角形绘制的。所以我们至少需要一个三角形来绘制一些东西(严格来说这不是真的,但就我们的目的而言,这是一个公平的假设)。我们需要 3D 空间中的 3 个点。其次,我们需要告诉 three.js 如何使用 three.js   将这些点连接在一起。Face3需要 3个点并将它们连接在一起以绘制一个三角形。现在让我们来看看。首先,让我们定义一个简单的三角形。

geometry.vertices.push(new THREE.Vector3( 1, -1, 0));
geometry.vertices.push(new THREE.Vector3( 0,  1, 0));
geometry.vertices.push(new THREE.Vector3(-1, -1, 0));

geometry.faces.push(new THREE.Face3(0, 1, 2));

请注意直接访问顶点数组和面数组是多么容易?这是three.js中Geometry类的好处。如果我们使用 BufferGeometry这将更加复杂。但是BufferGeometry速度稍快,因此你面临权衡取舍。

在我们继续之前还有几点。注意我们的面是如何取三个整数的。这些是我们三个顶点中每个顶点的数组索引。我们在事务中定义它们的顺序。在多个面中引用同一个顶点并没有错。事实上,这允许我们重用一些顶点并节省一些空间。让我们看看它现在是如何工作的。

geometry.vertices.push(new THREE.Vector3( 1, -1, 0));
geometry.vertices.push(new THREE.Vector3(-1,  1, 0));
geometry.vertices.push(new THREE.Vector3(-1, -1, 0));
geometry.vertices.push(new THREE.Vector3( 1,  1, 0));

geometry.faces.push(new THREE.Face3(0, 1, 2));
geometry.faces.push(new THREE.Face3(0, 3, 1));

正如在此处看到的,我们现在已经定义了两个三角形,但只添加了一个顶点。这是在不占用顶点数组中太多额外空间的情况下定义对象的好方法。

这是在 three.js 中创建自定义几何图形的基本方法,我们可以以任何我们想要的方式扩展它。可以使用可以设计的任何方法添加顶点和面。但是还有一些有用的考虑因素。为了充分利用 three.js 的特性,我们希望我们的几何体具有法线、顶点颜色和 uv。

2、法线

法线是我们最容易添加的属性。一旦我们定义了一个顶点数组,就可以通过调用computeVertexNormalscomputeFlatNormals来来为我们计算法线。让我们看看两者:

geometry.computeVertexNormals();
geometry.normalsNeedUpdate = true;

顶点法线计算在三角形面上插值的每个顶点法线。顶点法线通常用于高保真外观模型。这是因为插值使法线看起来像是在三角形面上发生了变化,这使你可以计算更准确的三角形面上的光照。

geometry.computeFaceNormals();
geometry.normalsNeedUpdate = true;

对于面法线,整个三角形的法线保持不变。当计算照明时,我们将能够非常清楚地看到三角形本身。当做出特定的风格选择来展示三角形本身时,面法线很好。

两者都很容易调用。为了说明差异,让我们稍微弯曲一下我们的四边形,使三角形面向不同的方向,如下所示:

geometry.vertices.push(new THREE.Vector3( 1, -1, 0));
geometry.vertices.push(new THREE.Vector3(-1,  1, 0));
geometry.vertices.push(new THREE.Vector3(-1, -1, -1));
geometry.vertices.push(new THREE.Vector3( 1,  1, -1));

geometry.faces.push(new THREE.Face3(0, 1, 2));
geometry.faces.push(new THREE.Face3(0, 3, 1));

还有一件事。让我们将材质更改为 MeshNormalMaterial,以便我们可以说明法线的样子。

现在我们可以看到,当我们使用顶点法线时,它看起来像这样:

注意远角的颜色不同,而中间的两个顶点颜色相同?

现在让我们看一下带有面法线的弯曲四边形:

每个三角形都突出并具有不同的颜色。

3、UV

与法线密切相关的是 uv。将 2D 纹理映射到 3D 对象时使用 Uv。不幸的是,给定顶点数组,three.js 不会为我们计算 uv。因此,在构建几何体时,我们需要在顶点数组旁边构建一个 uv 数组。

three.js 中的 uv 数组是在GeometryfaceVertexUvs属性下访问的。faceVertexUvs是一个 uv 数组的数组。它们又是长度等于 faces 数组的数组,包含三个 uv,一个对应面中的每个顶点。现在这听起来很复杂,但实际上它非常简单。我们将顶点定义为法线,然后当我们定义我们的面时,我们会多做一步。

geometry.faces.push(new THREE.Face3(0, 1, 2));
var uvs1 = [new THREE.Vector2(1, 0), new THREE.Vector2(0, 1), new THREE.Vector2(0, 0)];
geometry.faceVertexUvs[0].push(uvs1); //remember faceVertexUvs is an array of arrays
geometry.faces.push(new THREE.Face3(0, 3, 1));
var uvs2 = [new THREE.Vector2(1, 0), new THREE.Vector2(1, 1), new THREE.Vector2(0, 1)];
geometry.faceVertexUvs[0].push(uvs2);

这样做可以让我们Geometry纹理化,我们可以轻松地在three.js 中应用纹理,我们只需将Texture添加到map材质中的属性。

var material = new THREE.MeshBasicMaterial({map: texture});

我们通过从磁盘加载一个纹理来获得一个纹理(虽然我们也可以创建自己的,但这对于本教程来说太高级了)。

var texture = new THREE.TextureLoader().load('uv_texture.jpg');

我们得到的是:

4、顶点颜色

几何的最后一个共同属性是顶点颜色。有时我们不想使用纹理或复杂的着色器。有时我们想提前存储一个顶点的颜色,然后简单地从中读取我们的材料。幸运的是,three.js 为我们提供了一种简单的方法来提前提供顶点颜色。

我们可以通过两种方式设置它们。第一种是将单一颜色传递给Color我们的Face3定义,从而为整个三角形提供单一颜色。或者我们可以传入一个颜色数组,以便每个顶点都有自己的颜色,该颜色被插在面上。

//Face3 takes three vertex indices, a normal and then a color
//We aren't worried about normals right now so we pass null in instead
geometry.faces.push(new THREE.Face3(0, 1, 2, null, new THREE.Color(0.9, 0.7, 0.75)));
geometry.faces.push(new THREE.Face3(0, 3, 1, null, new THREE.Color(0.6, 0.85, 0.7)));

如你所见,每个三角形都有自己的颜色。现在,如果我们希望每个顶点都有自己的颜色,我们只需创建一个包含三种颜色的数组并将其传入而不是单一的Color

请注意,可以为每个面使用不同的数组,我只是选择不这样做。

你可以看到颜色在顶点面上混合。如果想在简单对象上平滑过渡颜色,这将变得特别有用。

5、Low-Poly地形

为了利用我们在上面学到的一切,我们将编写一个小脚本来生成一个不规则的平面,该平面被Perlin噪声抵消,从而产生一些漂亮的 Low-Poly 风格的地形。

上面没有提到的制作自己Geometry的额外好处之一是你只需要处理自己绝对需要的数据量。如果Geometry不需要法线,请不要浪费时间计算它们。如果不想添加纹理,请不要在 uvs 上浪费空间。

我们将绘制一小块 Low-Poly 风格的地形,因此我们不需要 uvs 或顶点颜色,我们将只使用每个面的法线并将单一颜色传递到材质中。

6、制作自己的平面

当 three.js 提供了如此简单的方法来制作我们自己的平面时,这似乎是一种浪费。但我们的平面将有一个显着的不同。我们不会统一传播我们的点。通常,平面由点网格定义,就像在 three.js 中一样。但是我们将把我们的点分散一点,这样当我们把平面变成地形时,它看起来就不会被锁定在网格中。

首先,让我们来一张平面图。我们将从一个常规的网格式平面开始,然后随着我们的进步移动到不同的东西。你已经了解了如何定义单个四边形。现在我们将把它扩展到更大的东西。毕竟,平面只是一堆排列在一起的四边形。让我们从一段代码开始,然后我们将它分开。

makeTile = function(size, res) {
  geometry = new THREE.Geometry();
  for (var i = 0; i <= res; i++) {
    for (var j = 0; j <= res; j++) {
      var z = j * size;
      var x = i * size;
      var position = new THREE.Vector3(x, 0, z);
      var addFace = (i > 0) && (j > 0);
      this.makeQuad(geometry, position, addFace, res + 1);
    }
  }
  geometry.computeFaceNormals();
  geometry.normalsNeedUpdate = true;
          
  return geometry;
};

该函数makeTile有两个参数sizeressize定义构建平面的每个四边形的大小,res定义平面的宽度。很容易。然后我们有一个嵌套循环,我们在二维中计数 0 到res。很简单,你现在可以看到网格形成的开始。

魔法发生在循环内。我们将变量xz设置为我们的顶点位置,我们通过将四边形位置乘以四边形i, j的大小来计算。我们将其保存到Vector3.

但是addFace呢。这实际上是一个聪明的小技巧。你看,我们的第一行顶点不足以形成四边形。所以在我们平面的边缘,我们告诉我们的脚本不要形成三角形,但是一旦我们进入第二行,我们就可以开始组合顶点来形成四边形和三角形。

然后我们调用makeQuad并将它传递给我们在这个循环中计算的所有信息。简单的!还有一件事,一旦我们完成循环,我们告诉three.js 为我们计算法线。然后我们返回新的几何对象以在程序的主要部分中使用。太棒了!现在让我们看看makeQuad function.

makeQuad = function(geometry, position, addFace, verts) {
  geometry.vertices.push(position);
    
  if (addFace) {
    var index1 = geometry.vertices.length - 1;
    var index2 = index1 - 1;
    var index3 = index1 - verts;
    var index4 = index1 - verts - 1;
    
    geometry.faces.push(new THREE.Face3(index2, index3, index1));
    geometry.faces.push(new THREE.Face3(index2, index4, index3));
  }
};

好的,首先我们将顶点添加到顶点数组中。这很容易理解。如果addFaceFalse,那么我们停止。否则,我们计算周围顶点的位置并形成两个面,就像我们上面所做的那样。只是现在我们必须计算索引位置。这就是它的全部!使用这两个函数,你最终应该得到如下结果:

7、为平面添加一些噪声

为了让事情变得有趣,我们将向顶点添加一些噪声以稍微改变高度。如果你需要复习在 javascript 中使用噪声,请查看教程。

var position = new THREE.Vector3(x, noise.perlin2(x, z)*size, z);

我将噪声乘以大小,以便它与你的平面大小很好地缩放。如果你使用 MeshNormalMaterial你的平面现在看起来像这样:

其实有点无聊。你可以看到它只是一个顶点被上下推的网格。让我们通过更多地分解顶点位置使其更有趣。我们将通过调用 Math.random 随机偏移x和位置来做到这一点。

var z = j * size + (Math.random() - 0.5) * size;
var x = i * size + (Math.random() - 0.5) * size;

现在,如果将高度重置为 0,你可以看到三角形的变化程度。

如果你再次添加高度。

这就是创建自己的小地形所需要做的一切!尝试使用不同的设置,特别是尝试使四边形非常小并且分辨率大。x然后尝试通过乘以不同大小的数字来稍微混合噪声z以缩放噪声。你应该能够创建所有不同类型的 Low-Poly 地形。对于示例,这里是使用MeshPhongMaterial.

8、结束语

我知道上面有很多东西。程序几何是一个非常深奥的话题。但是现在你有了开始在three.js 中进一步探索它的工具!希望你已经学会:

  • 如何使用 three.js 的Geometry类来制作自己的自定义几何图形,包括:
  • 如何定义自己的uvs,
  • 如何定义自己的顶点颜色,
  • 以及如何使用 three.js 为您计算法线
  • 如何自动创建抖动平面

原文链接:Learning 3D Graphics With Three.js | Procedural Geometry

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