4.2 简单材质

在本节中,我们将介绍几种简单的材质:MeshBasicMaterialMeshDepthMaterialMeshNormalMaterial。我们先从 MeshBasicMaterial 开始。

在深入研究这些材质的属性之前,先简要说明一下如何传入属性来配置材质。有两种选择:

你可以在构造函数中作为参数对象传入属性,如下所示:

var material = new THREE.MeshBasicMaterial({
    color: 0xff0000,
    name: 'material-1',
    opacity: 0.5,
    transparent: true
    // ...
});

或者你也可以创建实例并单独设置属性,如下所示:

var material = new THREE.MeshBasicMaterial();
material.color = new THREE.Color(0xff0000);
material.name = 'material-1';
material.opacity = 0.5;
material.transparent = true;

通常,如果我们在创建材质时知道所有属性的值,最好的方式是使用构造函数。这两种风格中使用的参数格式相同。唯一的例外是 color 属性。在第一种风格中,我们可以直接传入十六进制值,Three.js 会自己创建一个 THREE.Color 对象。在第二种风格中,我们必须显式创建一个 THREE.Color 对象。在本书中,我们将同时使用这两种风格。

THREE.MeshBasicMaterial

MeshBasicMaterial 是一种非常简单的材质,不考虑场景中可用的光照。使用这种材质的网格将作为简单的平面多边形渲染,你还可以选择显示几何体的线框。除了我们在前面章节中看到的该材质的公共属性外,我们还可以设置以下属性:

名称 描述
color 此属性允许你设置材质的颜色。
wireframe 此属性允许你将材质渲染为线框。这对于调试非常有用。
wireframeLineWidth 如果启用了线框,此属性定义线框中线条的宽度。
wireframeLinecap 此属性定义线框模式下线条端点的样式。可能的值为 buttroundsquare。默认值为 round。实际上,更改此属性的效果很难看到。此属性在 WebGLRenderer 上不支持。
wireframeLinejoin 此属性定义线条连接点的可视化方式。可能的值为 roundbevelmiter。默认值为 round。如果你仔细观察,在示例中使用低不透明度和非常大的 wireframeLineWidth 值可以看到此效果。此属性在 WebGLRenderer 上不支持。

在之前的章节中,我们看到了如何创建材质并将其分配给对象。对于 THREE.MeshBasicMaterial,我们这样做:

var meshMaterial = new THREE.MeshBasicMaterial({color: 0x7777ff});

这将创建一个新的 THREE.MeshBasicMaterial 并将 color 属性初始化为 0x7777ff(紫色)。

你可以选择 THREE.CanvasRenderer 来试验 THREE.MeshBasicMaterial 的属性,如下所示:

在此示例中你可以设置的一个属性是 side 属性。使用此属性,你可以定义材质应用于 THREE.Geometry 的哪一面。当你选择平面网格时,可以测试此属性如何工作。由于材质通常仅应用于材质的正面,旋转的平面有一半时间会不可见(当背面朝向您时)。如果将 side 属性设置为 double,则平面将始终可见,因为材质应用于几何体的两面。但请注意,当 side 属性设置为 double 时,渲染器需要做更多工作,因此这可能影响场景的性能。

THREE.MeshDepthMaterial

列表中的下一个材质是 THREE.MeshDepthMaterial。使用这种材质,对象的外观不是由灯光或特定的材质属性定义的,而是由对象到相机的距离决定的。你可以将其与其他材质结合,轻松创建淡入淡出效果。这种材质唯一相关的属性是以下两个控制是否显示线框的属性:

名称 描述
wireframe 决定是否显示线框。
wireframeLineWidth 决定线框的宽度(这仅适用于 THREE.CanvasRenderer)。

下面的示例中可以点击 addCube 按钮在场景中添加立方体:

此截图显示立方体根据到相机的距离进行着色渲染。尽管这种材质没有太多额外属性来控制对象的渲染方式,但我们仍然可以控制对象颜色淡出的速度。在此示例中,我们暴露了相机的 nearfar 属性。这两个属性设置了相机的可视区域。任何比 near 属性更靠近相机的对象都不会显示,任何比 far 属性更远的对象也落在相机的可视区域之外。

相机的 nearfar 属性之间的距离定义了亮度和对象淡出的速率。如果距离非常大,对象在远离相机时只会略微淡出。如果距离很小,淡出效果会更加明显

在此示例中,立方体在相机的 nearfar 属性之间距离较小时渲染。创建 THREE.MeshDepthMaterial 非常简单,该对象不需要任何参数。在此示例中,我们使用了 scene.overrideMaterial 属性来确保场景中的所有对象都使用这种材质,而无需为每个 THREE.Mesh 对象显式指定:

var scene = new THREE.Scene();
scene.overrideMaterial = new THREE.MeshDepthMaterial();

组合材质

如果你查看 THREE.MeshDepthMaterial 的属性,会发现没有设置立方体颜色的选项。一切都是由材质的默认属性决定的。然而,Three.js 提供了组合材质以创建新效果的选项(这也是混合发挥作用的地方)。以下代码展示了如何组合材质:

var cubeMaterial = new THREE.MeshDepthMaterial();
var colorMaterial = new THREE.MeshBasicMaterial({
    color: 0x00ff00,
    transparent: true,
    blending: THREE.MultiplyBlending
});
var cube = new THREE.SceneUtils.createMultiMaterialObject(
    cubeGeometry,
    [colorMaterial, cubeMaterial]
);
cube.children[1].scale.set(0.99, 0.99, 0.99);

我们得到如下绿色立方体,它们使用了 THREE.MeshDepthMaterial 的亮度和 THREE.MeshBasicMaterial 的颜色。以下为示例:

让我们看一下获得此特定结果所需的步骤。首先,我们需要创建两种材质。对于 THREE.MeshDepthMaterial,我们不做任何特殊处理;对于 THREE.MeshBasicMaterial,我们将 transparent 设置为 true 并定义混合模式。因为 Three.js 无法混合不同的材质,如果不将 transparent 属性设置为 true,则只会得到纯绿色的对象。当 transparent 设置为 true 时,Three.js 将检查 blending 属性,以确定绿色的 THREE.MeshBasicMaterial 应如何与背景交互。在此例中,背景是使用 THREE.MeshDepthMaterial 渲染的立方体。

对于此示例,我们使用了 THREE.MultiplyBlending。这种混合模式将前景色与背景色相乘,得到所需的效果。此代码片段中的最后一行也很重要。当我们使用 THREE.SceneUtils.createMultiMaterialObject() 函数创建网格时,几何体会被复制,并在一个组中返回两个完全相同的网格。如果我们在没有最后一行的情况下渲染它们,经常会看到闪烁效果。当一个对象渲染在另一个对象之上,并且其中之一是透明的时,有时会发生这种情况。通过缩放使用 THREE.MeshDepthMaterial 创建的网格,我们可以避免这种情况。为此,使用以下代码:

cube.children[1].scale.set(0.99, 0.99, 0.99);

THREE.MeshNormalMaterial

THREE.MeshNormalMaterial 是一种根据每个面的法线向量为其着色的材质。面由三个顶点组成。法线向量是垂直于该面的向量。THREE.MeshNormalMaterial 计算这些法线向量的方向,并根据该方向对面进行着色。示例如下:

如你所见,网格的每个面都以略有不同的颜色渲染,即使球体旋转,颜色也基本保持在相同位置。这是因为每个面的颜色基于从该面指向外的法线。法线是垂直于面的向量。法线向量在 Three.js 的许多不同部分中使用。它用于确定光的反射,有助于将纹理映射到 3D 模型,并提供有关如何照亮、着色和着色表面像素的信息。Three.js 负责计算这些向量并在内部使用它们,因此你无需自己计算或处理它们。

此法线指向的方向决定了使用 THREE.MeshNormalMaterial 时面获得的颜色。由于球体各面的法线指向不同的方向,因此我们得到如示例中所示的彩色球体。附带说明一下,要添加这些法线箭头,你可以像这样使用 THREE.ArrowHelper

for (var f = 0, fl = sphere.geometry.faces.length; f < fl; f++) {
    var face = sphere.geometry.faces[f];
    var centroid = new THREE.Vector3(0, 0, 0);
    centroid.add(sphere.geometry.vertices[face.a]);
    centroid.add(sphere.geometry.vertices[face.b]);
    centroid.add(sphere.geometry.vertices[face.c]);
    centroid.divideScalar(3);
    var arrow = new THREE.ArrowHelper(
        face.normal,
        centroid,
        2,
        0x3333FF,
        0.5,
        0.5
    );
    sphere.add(arrow);
}

在此代码片段中,我们遍历 THREE.SphereGeometry 的所有面。对于每个 THREE.Face3 对象,我们通过添加构成该面的顶点并将结果除以 3 来计算中心点(质心)。我们使用此质心以及面的法线向量来绘制箭头。THREE.ArrowHelper 接受以下参数:方向、原点、长度、颜色、箭头头部长度和箭头头部宽度。

通过 shading 属性,我们可以告诉 Three.js 如何渲染对象。如果你使用 THREE.FlatShading,每个面将按原样渲染,或者你可以使用 THREE.SmoothShading,它会使对象的面平滑化。

单个网格的多种材质

在创建 THREE.Mesh 时,到目前为止我们只使用了一种材质。也可以为几何体的每个面定义特定的材质。例如,如果你有一个立方体,它有 12 个面(记住,Three.js 使用三角形),你可以为立方体的每一面分配不同的材质(例如,具有不同颜色)。这样做非常简单,如以下代码所示:

var matArray = [];
matArray.push(new THREE.MeshBasicMaterial({ color: 0x009e60 }));
matArray.push(new THREE.MeshBasicMaterial({ color: 0x0051ba }));
matArray.push(new THREE.MeshBasicMaterial({ color: 0xffd500 }));
matArray.push(new THREE.MeshBasicMaterial({ color: 0xff5800 }));
matArray.push(new THREE.MeshBasicMaterial({ color: 0xC41E3A }));
matArray.push(new THREE.MeshBasicMaterial({ color: 0xffffff }));

var cubeGeom = new THREE.BoxGeometry(3, 3, 3);
var cube = new THREE.Mesh(cubeGeom, matArray);

我们首先创建一个名为 matArray 的数组来保存所有材质,并使用该数组创建 THREE.Mesh。你可能注意到我们只创建了 6 种材质,尽管我们有 12 个面。要理解这是如何工作的,我们必须看看 Three.js 如何将材质分配给面。为此,Three.js使用构成对象的面的 materialIndex 属性。如果我们查看 THREE.CubeGeometry 各面的 materialIndex 属性,会看到以下内容(你可以从浏览器的调试控制台执行此操作):

> cubeGeom.faces.forEach((p,i) => console.log("face " + i + ": " + p.materialIndex))
face 0: 0
face 1: 0
face 2: 1
face 3: 1
face 4: 2
face 5: 2
face 6: 3
face 7: 3
face 8: 4
face 9: 4
face 10: 5
face 11: 5

如你所见,Three.js 已经将立方体的面分组。因此,我们不必提供 12 种不同的材质,只需提供 6 种,每种对应立方体的一面。让我们更深入地研究代码,看看你需要做什么来重新创建以下示例:一个简单的 3D 魔方。示例如下所示:

这个魔方由许多小立方体组成:沿 x 轴三个,沿 y 轴三个,沿 z 轴三个。以下是实现方式:

var group = new THREE.Mesh();
// 添加所有魔方元素
var mats = [];
mats.push(new THREE.MeshBasicMaterial({ color: 0x009e60 }));
mats.push(new THREE.MeshBasicMaterial({ color: 0x0051ba }));
mats.push(new THREE.MeshBasicMaterial({ color: 0xffd500 }));
mats.push(new THREE.MeshBasicMaterial({ color: 0xff5800 }));
mats.push(new THREE.MeshBasicMaterial({ color: 0xC41E3A }));
mats.push(new THREE.MeshBasicMaterial({ color: 0xffffff }));

for (var x = 0; x < 3; x++) {
    for (var y = 0; y < 3; y++) {
        for (var z = 0; z < 3; z++) {
            var cubeGeom = new THREE.BoxGeometry(2.9, 2.9, 2.9);
            var cube = new THREE.Mesh(cubeGeom, mats);
            cube.position.set(x * 3 - 3, y * 3 - 3, z * 3 - 3);
            group.add(cube);
        }
    }
}

在这段代码中,我们首先创建 THREE.Mesh,它将容纳所有单个立方体(group);接下来,我们为每个面创建材质并将其推送到 mats 数组。然后,我们创建三个循环以确保创建正确数量的立方体。在循环中,我们创建每个单独的立方体,分配材质,定位它们,并将它们添加到组中。你应该记住的是,立方体的位置是相对于该组的位置的。如果我们移动或旋转该组,所有立方体将随之移动和旋转,这样就可以看到整个魔方在旋转,整个组围绕其中心点(0,0,0)旋转:

group.rotation.y = step += controls.rotationSpeed;
group.rotation.z = step -= controls.rotationSpeed;
group.rotation.x = step += controls.rotationSpeed;

发布时间:2026/8/26 19:13:38  阅读次数:49