第十章 加载和处理纹理

在第4章“使用Three.js材质”中,我们向您介绍了Three.js中可用的各种材质。然而,我们没有讨论将纹理应用于创建网格时使用的材质。在本章中,我们将研究这个主题。具体而言,我们将讨论以下主题:

  • 在Three.js中加载纹理并将其应用于网格
  • 使用凹凸、法线和置换贴图将深度和细节应用于网格
  • 使用光照贴图和环境遮挡贴图创建假阴影
  • 使用镜面反射、金属度和粗糙度贴图来设置网格特定部分的光泽度
  • 为对象的部分透明度应用alpha贴图
  • 使用环境贴图向材质添加详细反射
  • 使用HTML5画布和视频元素作为纹理的输入

让我们从一个基本的例子开始,其中我们将向您展示如何加载和应用纹理。

在材质中使用纹理

在Three.js中可以使用不同的纹理。您可以使用它们来定义网格的颜色,但也可以使用它们定义光泽度、凹凸和反射。不过,我们将看到的第一个例子是非常基本的,其中我们将使用纹理来定义网格的各个像素的颜色。这通常被称为颜色贴图或漫反射贴图。

加载纹理并将其应用到网格中

纹理最基本的用法是当将其设置为材质上的映射时。使用此材质创建网格时,网格将根据提供的纹理着色。可以通过以下方式加载纹理并在网格上使用它:

const textureLoader = new THREE.TextureLoader(); 
const texture = textureLoader.load
 ('/assets/textures/ground/ground_0036_color_1k.jpg')

在这个代码示例中,我们使用THRE.TextureLoader的一个实例来加载图像文件来自特定位置。使用此加载程序,您可以使用PNG、GIF或JPEG图像作为纹理的输入(在本章的后面,我们将向您展示如何加载其他纹理格式)。请注意,已加载纹理异步:如果它是一个大纹理,并且在纹理完全加载之前渲染场景,您将在短时间内看到没有应用纹理的网格。如果要等到纹理已经加载,您可以为textureLoader.load()函数提供回调:

Const textureLoader = new THREE.TextureLoader(); 
const texture = textureLoader.load
 ('/assets/textures/ground/ground_0036_color_1k.jpg',
 onLoadFunction, 
 onProgressFunction,
 onErrorFunction)

正如您所看到的,load函数采用三个额外的函数作为参数:onLoadFunction在加载纹理时调用,onProgressFunction可用于跟踪加载了多少纹理,onErrorFunction在加载或 解析纹理。现在纹理已经加载,我们可以将其添加到网格中:

const material = new THREE.MeshPhongMaterial({ color: 
 0xffffff })
material.map = texture

请注意,加载程序还提供了一个loadAsync函数,它会返回Promise,就像我们在上一章加载模型时看到的那样。

你可以使用几乎任何你想要的图像作为纹理。但是,通过使用一个尺寸为2次幂的正方形纹理,您将得到最好的结果。所以,诸如256x256 ,512x512,11024x1024等的尺寸是最好的。如果纹理不是2的幂,Three.js将把图像缩小到最接近2值的幂。

我们将在本章的示例中使用的其中一个纹理如下所示:

image-20230508171201407

图10.1-砖墙的颜色纹理

纹理的像素(也称为纹素)通常不会一一映射到人脸的像素上。如果相机离得很近,我们需要放大纹理,如果我们被缩小,我们可能需要缩小纹理。为此,WebGL和Three.js提供了几个不同的调整大小选项这张图片。这是通过magFilter和minFilter属性完成的:

  • THREE.NearestFilter:这个过滤器使用它能找到的最近纹素的颜色。当用于放大时,这将导致块状,而当用于缩小时,结果将丢失大量细节。
  • THRE.LinearFilter:该过滤器更先进;它使用四个相邻纹素的颜色值来确定正确的颜色。在缩小过程中,你仍然会丢失很多细节,但放大会更平滑,也不会那么块状。除了这些基本值之外,我们还可以使用MIP映射。MIP映射是一组纹理图像,每个纹理图像的大小是前一个纹理图像的一半。这些是在加载纹理时创建的,可以实现更平滑的过滤。所以,当你有一个正方形的纹理(作为2的幂)时,你可以使用一些额外的方法来更好地过滤。可以使用以下值来设置这些属性:
  • THREE.NearestMipMapNearestFilter:此属性选择最佳的MIP映射映射所需的分辨率并应用最近滤波器原理,我们在上一个列表。放大仍然是块状的,但缩小看起来好多了。
  • THRE.NearestMipMapLinearFilter:此属性不仅选择单个MIP映射而是两个最近的MIP映射级别。在这两个级别上,应用最近的过滤器,以获得两个中间结果。这两个结果通过线性滤波器得到最终结果。
  • THRE.LinearMipMapNearestFilter:此属性选择最佳的MIP映射映射所需的分辨率并应用线性滤波器原理,该原理在上一个列表。
  • THRE.LinearMipMapLinearFilter:此属性不选择单个MIP映射,而是两个最近的MIP映射级别。在这两个级别上,都应用了线性滤波器,以获得两个中间结果。这两个结果通过线性滤波器得到最终结果

如果未明确指定magFilter和minFilter属性,请选择Three.js使用THRE.LinearFilter作为magFilter属性的默认值,使用THRE_LinearMipMapLinearFilter作为minFilter属性的缺省值。

在我们的示例中,我们将只使用默认的纹理属性。在映射中,这种基本纹理的例子可以在texture-basics.html中找到。下面的屏幕截图显示了此示例:

image-20230508172309258

图10.2-具有简单的木材纹理的模型

在本例中,您可以更改模型,并从右侧的菜单中选择一些纹理。您还可以更改默认的材质特性,以查看材质与颜色贴图结合时如何受到不同设置的影响。

在本例中,您可以看到纹理很好地围绕着形状。在Three.js中创建几何图形时,它将确保正确应用所使用的任何纹理。这是通过所谓UV映射来完成的。通过UV映射,我们可以告诉渲染器纹理的哪一部分应该应用于特定的面。我们将在第13章中详细介绍UV映射,使用 Blender和 Three.js,其中我们将向您展示如何轻松地使用 Blender 为 Three.js 创建自定义UV映射。

除了标准的图像格式之外,我们还可以加载三种图像格式。纹理加载器, Three.js 还提供了一些自定义加载器,您可以用来加载以不同格式提供的纹理。如果你有一个特定的图像格式,你可以从 Three.js 分布的(https://github.com/mrdoob/three.js/tree/dev/examples/jsm/加载器)中查看加载器文件夹,看看图像格式是否可以直接通过 Three.js 加载,或者你是否需要手动转换它。

除了这些普通图像外,Three.js还支持HDR图像

加载HDR图像作为纹理

HDR图像比标准图像捕捉到更高范围的亮度水平,并且可以更紧密地匹配我们用人眼看到的东西。Three.js支持EXR和RGBE格式。如果你有HDR图像,你可以微调Three.js渲染HDR图像的方式,因为HDR图像包含的亮度信息比显示器上显示的更多。这可以通过在THRE.WebGLRenderer中设置以下属性来实现:

  • toneMapping 色调映射:此属性定义如何将HDR图像中的颜色映射到显示器。Three.js提供了以下选项:THREE.NoToneMMapping, THREE.LinearToneMapping,THREE.ReinhardToneMappling,THREE.Uncharted2ToneMapping和THREE.CineonToneMMapping。默认值为THREE.LinearToneMapping。
  • toneMappingExposure 色调映射曝光:这是色调映射的曝光级别。这可以使用以微调渲染纹理的颜色。
  • toneMappingWhitePoint 色调映射白点:这是用于色调映射的白点。这也可以用于微调渲染纹理的颜色。

如果你想加载EXR或RGBE图像并将其用作纹理,你可以使用THRE.EXRLoader或THRE.RGBELLoader。这与我们在THRE.TextureLoader中看到的工作方式相同:

const loader = new THREE.EXRLoader(); 
exrTextureLoader.load('/assets/textures/exr/Rec709.exr')
...
const hdrTextureLoader = new THREE.RGBELoader(); 
hdrTextureLoader.load('/assets/textures/hdr/
 dani_cathedral_oBBC.hdr')

在texture-basics.html示例中,我们向您展示了如何使用纹理来向网格应用颜色。在下一节中,我们将介绍如何通过向网格上应用假高度信息来使模型看起来更详细。

使用凹凸贴图为网格提供额外的细节

凹凸贴图用于为材质添加更多的深度。你可以通过打开texturebump-map.html来看到这个动作:

image-20230508174326572

图10.3-带有凹凸贴图的模型

在本例中,您可以看到该模型看起来更详细,而且似乎也更有深度。这是通过在材质上设置一个额外的纹理,一个所谓的凹凸贴图来实现的:

const exrLoader = new EXRLoader()
const colorMap = exrLoader.load('/assets/textures/brick-wall/
brick_wall_001_diffuse_2k.exr', (texture) => {
 texture.wrapS = THREE.RepeatWrapping
 texture.wrapT = THREE.RepeatWrapping
 texture.repeat.set(4, 4)
})
const bumpMap = new THREE.TextureLoader().load(
 '/assets/textures/brick-wall/brick_wall_001_displacement_2k.
png',
 (texture) => {
 texture.wrapS = THREE.RepeatWrapping
 texture.wrapT = THREE.RepeatWrapping
     texture.repeat.set(4, 4)
 }
)
const material = new THREE.MeshPhongMaterial({ color: 
 0xffffff })
material.map = colorMap
material.bumpMap = bumpMap

在这段代码中,您可以看到,除了设置贴图属性外,我们还将bumpMap属性设置为纹理。此外,通过上一示例中的菜单提供的bumpScale属性,我们可以设置凸起的高度(或深度,如果设置为负值)。此示例中使用的纹理如下所示:

image-20230508174521199

图10.4-凹凸贴图所使用的纹理

凹凸贴图是一个灰度图像,但您也可以使用彩色图像。像素的强度定义了凹凸点的高度。凹凸贴图仅包含一个像素的相对高度。它并没有提到斜坡的方向。因此,你通过凹凸贴图可以达到的细节水平和深度感知是有限的。要了解更多细节,您可以使用法线贴图

使用法线贴图实现更详细的凹凸

在法线贴图中,不存储高度(位移),而是存储每个像素的法线方向。在不涉及太多细节的情况下,使用法线贴图,可以创建仅使用少量顶点和面的外观非常详细的模型。例如,看看texture-normal-map.html示例:

image-20230606101313029

图10.5–使用法线贴图的模型

在前面的屏幕截图中,您可以看到一个非常详细的模型。当模型四处移动时,可以看到纹理对其接收到的光做出了响应。这提供了一个看起来非常逼真的模型,并且只需要一个非常简单的模型和几个纹理。以下代码片段显示了如何在Three.js中使用法线映射:

const colorMap = new THREE.TextureLoader().load('/assets/textures/red-bricks/red_bricks_04_diff_1k.jpg', (texture) => {
 texture.wrapS = THREE.RepeatWrapping
 texture.wrapT = THREE.RepeatWrapping
 texture.repeat.set(4, 4)
})
const normalMap = new THREE.TextureLoader().load('/assets/textures/red-bricks/red_bricks_04_nor_gl_1k.jpg',
 (texture) => {
 texture.wrapS = THREE.RepeatWrapping
 texture.wrapT = THREE.RepeatWrapping
     texture.repeat.set(4, 4)
 }
)
const material = new THREE.MeshPhongMaterial({ color: 
 0xffffff })
material.map = colorMap
material.normalMap = normalMap

这涉及到与我们用于凹凸贴图的方法相同的方法。不过,这一次,我们将normalMap属性设置为法线纹理。我们还可以通过设置normalScale属性(mat.normalScale.set(1,1))来定义凸起的明显程度。使用此属性,您可以沿X轴和Y轴进行缩放。然而,最好的方法是保持这些值不变。在本例中,您可以使用这些值。

下图显示了我们在这里使用的法线贴图的样子:

image-20230606101529709

图10.6–正常纹理

然而,法线贴图的问题在于,它们不太容易创建。您需要使用专门的工具,如Blender或Photoshop。这些程序可以使用高分辨率渲染或纹理作为输入,并可以从中创建法线贴图。

使用法线贴图或凹凸贴图时,不会更改模型的形状;所有顶点都位于同一位置。这些贴图只是使用场景中的灯光来创建虚假的深度和细节。然而,Three.js提供了第三种方法,您可以使用它来使用贴图向模型添加细节,这确实会更改顶点的位置。这是通过位移贴图完成的

使用移位贴图更改顶点的位置

Three.js还提供了一个纹理,可用于更改模型顶点的位置。虽然凹凸贴图和法线贴图会产生深度错觉,但使用移位贴图时,我们会根据纹理中的信息更改模型的形状。我们可以像使用其他贴图一样使用移位贴图:

const colorMap = new THREE.TextureLoader().load('/assets/textures/displacement/w_c.jpg', (texture) => {
 texture.wrapS = THREE.RepeatWrapping
 texture.wrapT = THREE.RepeatWrapping
})
const displacementMap = new THREE.TextureLoader().load('/assets/textures/displacement/w_d.png', (texture) => {
 texture.wrapS = THREE.RepeatWrapping
 texture.wrapT = THREE.RepeatWrapping
})
const material = new THREE.MeshPhongMaterial({ color: 
 0xffffff })
material.map = colorMap
material.displacementMap = displacementMap

在前面的代码片段中,我们加载了一个移位图像,如下所示:

image-20230606102430359

图10.7-移位图像

颜色越亮,顶点的位移就越多。运行texture-displacement.html示例时,您将看到置换贴图的结果是一个模型,其中模型的形状将根据贴图中的信息进行更改:

图10.8–使用位移的模型

图10.8–使用位移的模型

除了设置displacementMap纹理外,我们还可以使用displacementScale和displacementOffset来控制位移的明显程度。关于使用位移贴图,最后要提到的一件事是,只有当网格包含大量顶点时,它才会产生好的结果。如果不是,则置换看起来不会像提供的纹理,因为顶点太少,无法表示所需的位移。

使用环境遮挡贴图添加细微阴影

在前几章中,您学习了如何在Three.js中使用阴影。如果设置castShadowwand-receiveShadow属性,添加几个灯光,并正确配置灯光的阴影摄影机,Three.jss将渲染阴影。

但是,渲染阴影是一项相当昂贵的操作,每次渲染循环都会重复此操作。如果你有四处移动的灯光或对象,这是必要的,但通常情况下,一些灯光或模型是固定的,所以如果我们可以计算一次阴影,然后重用它们,那就太好了。为了实现这一点,Three.js提供了两种不同的贴图:环境光遮挡贴图和光照贴图。在本节中,我们将查看环境光遮挡贴图,在下一节中,将查看光照贴图。

环境光遮挡是一种用于确定模型的每个部分在场景中暴露于环境光的程度的技术。在Blender等工具中,环境光通常通过半球光或平行光(如太阳)进行建模。虽然模型的大多数部分将接收一些环境光,但并非所有部分都接收相同的环境光。例如,如果你为一个人建模,那么头顶将比手臂底部接收更多的环境光。这种照明的差异——阴影——可以渲染(烘焙,如下面的屏幕截图所示)到纹理中,然后我们可以将该纹理应用于我们的模型,为它们提供阴影,而不必每次都计算阴影:

image-20230606105115770

图10.9–在Blender中烘焙的环境光遮挡贴图

一旦有了环境光遮挡贴图,就可以将其指定给材质的aoMap属性,并且Three.js在应用和计算场景中的灯光应应用于模型的特定部分时会考虑到这些信息。下面的代码片段显示了如何设置aoMap属性:

const aoMap = new THREE.TextureLoader().load('/assets/gltf/material_ball_in_3d-coat/aoMap.png')
const material = new THREE.MeshPhongMaterial({ color:  0xffffff })
material.aoMap = aoMap
material.aoMap.flipY = false

就像其他类型的纹理贴图一样,我们只需使用THREE.TextureLoader来加载纹理并将其指定给材质的正确属性。与许多其他贴图一样,我们也可以通过设置aoMapIntensityProperty来调整贴图对模型照明的影响程度。在这个例子中,您还可以看到我们需要将aoMap的flipY属性设置为false。有时,外部程序将材质存储在与Three.js预期的纹理略有不同的纹理中。使用此属性,我们将翻转纹理的方向。这通常是你在使用模型时反复尝试会注意到的。

要使环境遮挡贴图工作,我们(通常)需要一个额外的步骤。我们已经提到了UV映射(存储在UV属性中)。这些定义了纹理的哪个部分映射到模型的特定面。对于环境光遮挡贴图,以及以下示例中的光照贴图,Three.js使用一组单独的UV贴图(存储在uv2属性中),因为通常需要以不同于阴影和光照贴图纹理的方式应用其他纹理。对于我们的示例,我们只是从模型中复制UV映射;请记住,当我们使用aoMap属性或lightMap属性时,Three.js将使用uv2属性的值,而不是uv属性。如果加载的模型中不存在该属性,通常情况下,只复制uv贴图属性也可以,因为我们没有做任何事情来优化环境遮挡贴图,这可能需要一组不同的uv:

const k = mesh.geometry
const uv1 = k.getAttribute('uv')
const uv2 = uv1.clone()
k.setAttribute('uv2', uv2)

我们将提供两个使用环境遮挡贴图的示例。在第一个例子中,我们展示了图10.9中应用aoMap的模型(texture ao-map-model.html):

图10.10–在Blender中烘焙并应用于模型的环境遮挡贴图

图10.10–在Blender中烘焙并应用于模型的环境遮挡贴图

可以使用右侧的菜单设置aoMapIntensity。该值越高,从加载的aoMap纹理中看到的阴影就越多。正如您所看到的,拥有环境遮挡贴图非常有用,因为它为模型提供了很好的细节,并使其看起来更加逼真。我们在本章中看到的一些纹理也提供了一个可以使用的额外aoMap。如果你打开texture-ao-map.html,你会得到一个简单的砖状纹理,但这次也添加了aoMap:

图10.11-环境遮挡贴图与颜色贴图和法线贴图相结合

图10.11-环境遮挡贴图与颜色贴图和法线贴图相结合

虽然环境光遮挡贴图会更改模型某些部分接收到的光量,但Three.js也支持光照贴图,通过指定一个为模型某些部分添加额外照明的贴图,光照贴图的作用大致相反。

使用光照贴图创建假光源

在本节中,我们将使用光照贴图。光照贴图是一种纹理,包含有关场景中的灯光对模型的影响程度的信息。换句话说,灯光的效果被烘焙到纹理中。光照贴图在三维软件(如Blender)中烘焙,并包含模型每个部分的光照值

image-20230606110015425

图10.12–在Blender中烘焙的Lightmap

我们将在本例中使用的光照贴图如图10.12所示。编辑窗口的右侧部分显示地平面的烘焙光照贴图。您可以看到,整个地平面都被白光照亮,其中的一部分接收到的光更少,因为场景中还有一个模型。使用光照贴图的代码与环境光遮挡贴图的代码类似:

Const textureLoader = new THREE.TextureLoader()
const colorMap = textureLoader.load('/assets/textures/wood/abstract-antique-backdrop-164005.jpg')
const lightMap = textureLoader.load('/assets/gltf/material_ball_in_3d-coat/lightMap.png')
const material = new THREE.MeshBasicMaterial({ color:  0xffffff })
material.map = colorMap
material.lightMap = lightMap
material.lightMap.flipY = false

再次,我们需要为Three.js提供一组额外的uv值,称为uv2(代码中未显示),并且我们必须使用Three.TextureLoader来加载纹理——在这种情况下,一个简单的纹理用于Blender中为本例创建的地板和光照贴图的颜色。结果如下所示(texture-light-map.html):

图10.13-使用光照贴图制作假阴影

图10.13-使用光照贴图制作假阴影

如果您查看前面的示例,您将看到光照贴图中的信息用于创建一个看起来非常漂亮的阴影,该阴影似乎是由模型投射的。重要的是要记住,烘焙阴影、灯光和环境遮挡在具有静态对象的静态场景中非常有效。一旦对象或光源发生变化或开始移动,就必须实时计算阴影。

金属度和粗糙度贴图

在讨论Three.js中可用的材质时,我们提到要使用的默认材质是Three.MeshStandardMaterial。您可以使用它来创建有光泽的类似金属的材质,但也可以应用粗糙度,使网格看起来更像木材或塑料。通过使用材料的金属性和粗糙度特性,我们可以配置材料来表示我们想要的材料。除了这两个属性之外,还可以通过使用纹理来配置这些属性。因此,如果我们有一个粗糙的对象,并且我们想指定该对象的某个部分是有光泽的,我们可以设置THREE.MeshStandardMaterial的metalnessMap属性,如果我们想指示网格的某些部分应该被视为有划痕或更粗糙,我们可以设定roughnessMap属性。使用这些贴图时,模型特定部分的纹理值将乘以粗糙度特性或金属度特性,从而决定应如何渲染该特定像素。首先,我们将查看texture-metalness-map.html中的金属性属性:

图10.14-应用于模型的Metaness纹理

图10.14-应用于模型的Metaness纹理

在这个例子中,我们稍微跳过了一点,还使用了一个环境贴图,它允许我们在对象顶部渲染来自环境的反射。具有高金属度的物体反射得更多,而具有高粗糙度的物体散射反射得更多。对于这个模型,我们使用了metalnessMap;您可以看到,在纹理的金属度属性较高的地方,对象本身是有光泽的,而在纹理的合金度属性较低的地方,某些部分是粗糙的。当查看roughnessMap时,我们可以看到大致相同但相反的情况:

图10.15——应用于模型的粗糙度纹理

图10.15—应用于模型的粗糙度纹理

正如您所看到的,根据提供的纹理,模型的某些部分比其他部分更粗糙或更粗糙。对于metalnessMap,材质的值乘以材质的metalnessproperty;对于roughnessMap,同样适用,但在这种情况下,该值将乘以粗糙度属性。

加载这些纹理并将其设置为材质可以这样做:

const metalnessTexture = new THREE.TextureLoader().load('/assets/textures/engraved/Engraved_Metal_003_ROUGH.jpg',
 (texture) => {
 texture.wrapS = THREE.RepeatWrapping
 texture.wrapT = THREE.RepeatWrapping
 texture.repeat.set(4, 4)
 }
)
const material = new THREE.MeshStandardMaterial({ color: 
 0xffffff })
material.metalnessMap = metalnessTexture
...
const roughnessTexture = new THREE.TextureLoader().load('/assets/textures/marble/marble_0008_roughness_2k.jpg',
 (texture) => {
 texture.wrapS = THREE.RepeatWrapping
 texture.wrapT = THREE.RepeatWrapping
 texture.repeat.set(2, 2)
 }
)
const material = new THREE.MeshStandardMaterial({ color: 
 0xffffff })
material.roughnessMap = roughnessTexture

接下来是alpha贴图。使用alpha贴图,我们可以使用纹理来更改模型各部分的透明度。

使用alpha贴图创建透明模型

alpha贴图是控制曲面不透明度的一种方法。如果贴图的值为黑色,则模型的该部分将完全透明;如果贴图为白色,则模型将完全不透明。在我们研究纹理以及如何应用它之前,我们将首先看看示例(texture-alpha-map.html):

图10.16-用于提供部分透明度的Alpha图

图10.16-用于提供部分透明度的Alpha图

在本例中,我们渲染了一个立方体,并设置了材质的alphaMap属性。如果打开此示例,请确保将材质的透明度特性设置为true。你可能会注意到,你只能看到立方体的正面部分,而不像前面的屏幕截图,你可以透过立方体看到另一面。原因是,默认情况下,使用的材质的side属性设置为THRE.FrontSide。要渲染通常隐藏的边,我们必须将材质的side特性设置为THEE.DoubleSide;您将看到立方体的渲染如前面的屏幕截图所示。

我们在本例中使用的纹理非常简单:

图10.17-用于创建透明模型的纹理

图10.17-用于创建透明模型的纹理

要加载它,我们必须使用与其他纹理相同的方法:

const alphaMap = new THREE.TextureLoader().load('/assets/textures/alpha/partial-transparency.png', (texture) => {
 texture.wrapS = THREE.RepeatWrapping
 texture.wrapT = THREE.RepeatWrapping
 texture.repeat.set(4, 4)
})
const material = new THREE.MeshPhongMaterial({ color: 
 0xffffff })
material.alphaMap = alphaMap
material.transparent = true

在这个代码片段中,您还可以看到我们已经设置了纹理的wrapS、wrapT和repeat属性。我们将在本章后面更详细地解释这些特性,但这些特性可用于确定我们希望在网格上重复纹理的频率。如果设置为(1,1),则应用于网格时不会重复整个纹理;如果设置为更高的值,纹理将收缩并重复多次。在这种情况下,我们在两个方向上重复了四次。

为发光的模型使用放射贴图

放射贴图是一种纹理,可用于使模型的某些部分发光,就像放射特性对整个模型所做的那样。就像放射特性一样,使用放射贴图并不意味着该对象正在放射光——它只是使应用该纹理的模型部分看起来发光。这通过看一个例子更容易理解。如果您在浏览器中打开 texture-emissive-map.html示例,您将看到一个类似熔岩的对象:

图10.18-使用放射贴图的熔岩状物体

图10.18-使用放射贴图的熔岩状物体

然而,当你仔细观察时,你可能会发现,虽然物体看起来发光,但物体本身并不发光。这意味着您可以使用它来增强对象,但对象本身不会对场景的照明产生影响。对于本例,我们使用了一个发射贴图,如下所示:

image-20230606112608669

图10.19–熔岩纹理

要加载和使用发射贴图,我们可以使用THREE.TextureLoader加载一个,并将其分配给emissiveMap属性(与其他一些贴图一起获得如图10.18所示的模型):

const emissiveMap = new THREE.TextureLoader().load('/assets/textures/lava/lava.png', (texture) => {
 texture.wrapS = THREE.RepeatWrapping
 texture.wrapT = THREE.RepeatWrapping
 texture.repeat.set(4, 4)
})
const roughnessMap = new THREE.TextureLoader().load('/assets/textures/lava/lava-smoothness.png', (texture) => {
 texture.wrapS = THREE.RepeatWrapping
 texture.wrapT = THREE.RepeatWrapping
 texture.repeat.set(4, 4)
})
const normalMap = new THREE.TextureLoader().load('/assets/textures/lava/lava-normals.png', (texture) => {
 texture.wrapS = THREE.RepeatWrapping
 texture.wrapT = THREE.RepeatWrapping
 texture.repeat.set(4, 4)
})
const material = new THREE.MeshPhongMaterial({ color: 
 0xffffff })
material.normalMap = normalMap
material.roughnessMap = roughnessMap
material.emissiveMap = emissiveMap
material.emissive = new THREE.Color(0xffffff)
material.color = new THREE.Color(0x000000)

由于emissiveMap中的颜色是用emissive特性调制的,请确保将材质的emissive属性设置为黑色以外的其他颜色。

使用镜面反射贴图来确定亮度

在前面的示例中,我们主要使用THREE.MeshStandardMaterial以及该材质支持的不同贴图。THREE.MeshStandardMaterial通常是您需要材质的最佳选择,因为它可以很容易地配置为表示大量不同类型的真实世界材质。在旧版本的Three.js中,对于有光泽的材质必须使用THRE.MeshPhongMaterial,对于无光泽的材质则必须使用THEE.MeshLambertMaterial。本节中使用的镜面反射贴图只能与THRE.MeshPhongMaterial一起使用。使用镜面反射贴图,您可以定义模型的哪些部分应该有光泽,哪些部分应该是粗糙的(类似于我们前面看到的metalnessMap和roughnessMap)。在texture-specular-map.html示例中,我们渲染了地球,并使用镜面反射贴图使海洋比陆地更明亮:

图10.20–显示反射海洋的镜面反射图

图10.20–显示反射海洋的镜面反射图

通过使用右上角的菜单,可以使用镜面反射颜色和光泽度。正如你所看到的,这两种特性会影响海洋反射光线的方式,但它们不会改变陆地的光泽。这是因为我们使用了以下镜面反射贴图:

图10.21–镜面反射贴图纹理

图10.21–镜面反射贴图纹理

在该地图中,黑色表示地图的这些部分具有0%的光泽度,而白色部分具有100%的光泽度。

要使用镜面反射贴图,我们必须使用THREE.TextureLoader加载贴图,并将其指定给THREE.MathPhongMaterial的镜面反射贴图属性:

const colorMap = new THREE.TextureLoader().load('/assets/textures/specular/Earth.png')
const specularMap = new THREE.TextureLoader().load('/assets/textures/specular/EarthSpec.png')
const normalMap = new THREE.TextureLoader().load('/assets/textures/specular/EarthNormal.png')
const material = new THREE.MeshPhongMaterial({ color:  0xffffff })
material.map = colorMap
material.specularMap = specularMap
material.normalMap = normalMap

对于镜面反射贴图,我们已经讨论了大多数基本纹理,这些纹理可以用于为模型添加深度、颜色、透明度或其他灯光效果。在接下来的两个部分中,我们将看到另一种类型的贴图,它将允许您将环境反射添加到模型中。

使用环境贴图创建假反射

计算环境反射非常耗费CPU,并且通常需要光线跟踪器方法。如果你想在Three.js中使用反射,你仍然可以这样做,但你必须伪造它。你可以通过创建对象所在环境的纹理并将其应用于特定对象来做到这一点。首先,我们将向您展示我们的目标结果(请参阅texture-environment-map.html,如以下屏幕截图所示):

图10.22–显示汽车内部的环境图

图10.22–显示汽车内部的环境图

在前面的屏幕截图中,您可以看到球体反映了环境。如果你四处移动鼠标,你还会看到反射与相机角度相对应,与你看到的环境有关。要创建此示例,请执行以下步骤:

  1. 创建一个CubeTexture对象。立方体纹理是一组六个纹理,可以应用于立方体的每一侧。
  2. 设置skybox。当我们有一个立方体纹理时,我们可以将其设置为场景的背景。如果我们这样做,我们可以有效地创建一个非常大的盒子,里面有相机和物体放置,这样当我们四处移动相机时,场景的背景也会正确更改。或者,我们也可以创建一个非常大的立方体,应用CubeTexture,并自己将其添加到场景中。
  3. 将CubeTexture对象设置为材质的cubeMap属性的纹理。我们用来模拟环境的CubeTexture对象应该用作网格上的纹理。Three.js将确保它看起来像是环境的反映。

一旦有了源材质,创建立方体纹理就非常容易了。你需要的是六张图片,它们合在一起,构成一个完整的环境。因此,您需要以下图片:

  • 向前看 (posz)
  • 向后看 (negz)
  • 向上看 (posy)
  • 向下看 (negy)
  • 向右看 (posx)
  • 向左看 (negx)

Three.js将把这些补丁拼凑在一起,创建一个无缝的环境地图。有几个网站可以下载全景图像,但它们通常是球形等矩形格式,如下所示:

图10.23——等矩形格式立方体图

图10.23—全景立方体贴图

有两种方法可以使用这些类型的地图。首先,您可以将其转换为由六个独立文件组成的多维数据集映射格式。您可以使用以下网站在线转换:https://jaxry.github.io/panorama-to-cubemap/.

或者,您可以使用不同的方式将此纹理加载到Three.js中,我们将在本节稍后部分中显示。

要从六个独立的文件加载CubeTexture,我们可以使用THREE.CubeTextureLoader,如下所示:

const cubeMapFlowers = new THREE.CubeTextureLoader().load([
 '/assets/textures/cubemap/flowers/right.png',
 '/assets/textures/cubemap/flowers/left.png',
 '/assets/textures/cubemap/flowers/top.png',
 '/assets/textures/cubemap/flowers/bottom.png',
 '/assets/textures/cubemap/flowers/front.png',
 '/assets/textures/cubemap/flowers/back.png'
])
const material = new THREE.MeshPhongMaterial({ color:  0x777777 }
material.envMap = cubeMapFlowers
material.mapping = THREE.CubeReflectionMapping

在这里,您可以看到我们已经加载了一个由几个不同图像组成的cubeMap。加载后,我们将纹理指定给材质的envMap属性。最后,我们必须通知Three.js我们要使用哪种映射。如果使用THREE.CubeTextureLoader加载纹理,则可以使用THREE.CubeReflectionMapping或THREE.CobeRefractMapping。第一个将使对象显示基于加载的立方体贴图的反射,而第二个将使模型变成更半透明的玻璃状对象,再次基于立方体贴图中的信息对灯光进行轻微折射。

我们也可以将这个立方体贴图设置为场景的背景,如下所示:

scene.background = cubeMapFlowers

当你有一个单一的图像,过程没有太大的不同:

const cubeMapEqui = new THREE.TextureLoader().load('/assets/equi.jpeg')
const material = new THREE.MeshPhongMaterial({ color:  0x777777 }
material.envMap = cubeMapEqui
material.mapping = THREE.EquirectangularReflectionMapping
scene.background = cubeMapFlowers

这一次,我们使用了普通纹理加载程序,但通过指定不同的贴图,我们可以通知Three.js如何渲染此纹理。使用此方法时,可以将映射设置为THREE.EquarectangularRefractMapping或THREE.EquirectangularReflectionMapping。

这两种方法的结果是,我们站在一个广阔的户外环境中,网格反映了环境。侧面的菜单允许您设置材质的特性:

图10.24-使用折射创建类似玻璃的物体

图10.24-使用折射创建类似玻璃的物体

除了反射,Three.js还允许使用cubeMap对象进行折射(类似玻璃的对象)。下面的屏幕截图显示了这一点(您可以使用右侧的菜单自行测试):

图10.25-使用折射创建类似玻璃的物体

图10.25-使用折射创建类似玻璃的物体

要获得这种效果,我们只需要将cubeMap的映射属性设置为THREE.CubeRefractMapping(默认为反射,也可以通过指定THREE.CubeReflectionMapping手动设置):

 cubeMap.mapping = THREE.CubeRefractionMapping

在本例中,我们为网格使用了一个静态环境贴图。换句话说,我们只看到了环境的反射,而没有看到环境中的其他网格。在下面的屏幕截图中,您可以看到,只要做一点工作,我们也可以显示其他对象的反射:

图10.26-使用立方体相机创建动态反射

图10.26-使用立方体相机创建动态反射

为了显示场景中其他对象的反射,我们需要使用一些其他Three.js组件。其中第一个是一个名为THREE.CubeCamera的附加相机:

const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(128, {
 generateMipmaps: true,
 minFilter: THREE.LinearMipmapLinearFilter
})
const cubeCamera = new THREE.CubeCamera(0.1, 10, cubeRenderTarget)
cubeCamera.position.copy(mesh.position); 
scene.add(cubeCamera);

我们将使用THREE.CubeCamera拍摄所有渲染对象的场景快照,并使用该快照设置立方体贴图。前两个参数定义了摄影机的近属性和远属性。因此,在这种情况下,摄影机仅渲染其在0.1到1.0之间可以看到的内容。最后一个属性是我们要将纹理渲染到的目标。为此,我们创建了一个THREE.WebGLCubeRenderTarget的实例。第一个参数是渲染目标的大小。该值越高,反射看起来就越详细。其他两个属性用于确定放大时纹理的放大和缩小方式。

您需要确保将此相机定位在要显示动态反射的THREE.Mesh的确切位置。在本例中,我们从网格中复制了位置,以便正确定位相机。

现在我们已经正确设置了CubeCamera,我们需要确保在我们的示例中,CubeCamerases作为纹理应用于立方体。要做到这一点,我们必须将envMap属性设置为cubeCamera.renderTarget:

cubeMaterial.envMap = cubeRenderTarget.texture;

现在,我们必须确保cubeCamera渲染场景,以便我们可以使用该输出作为立方体的输入。为此,我们必须按如下方式更新渲染循环(或者,如果场景没有更改,我们可以只调用一次):

const render = () => {
...
mesh.visible = false; 
cubeCamera.update(renderer, scene); 
mesh.visible = true;
requestAnimationFrame(render); 
renderer.render(scene, camera);
....
}

如您所见,首先,我们禁用网格的可见性。我们这样做是因为我们只想看到其他物体的反射。接下来,我们通过调用update函数,使用cubeCamera渲染场景。之后,我们使网格再次可见,并将场景渲染为正常。结果是,在网格的反射中,您可以看到我们添加的立方体。对于本例,每次单击updateCubeCamera按钮时,网格的envMap属性都会更新。

重复包装材料

将纹理应用于Three.js创建的几何体时,Three.js将尝试以尽可能优化的方式应用纹理。例如,对于立方体,这意味着每一侧都将显示完整的纹理,而对于球体,完整的纹理将包裹在球体周围。但是,在某些情况下,您不希望纹理围绕完整的面或完整的几何体展开,而是希望纹理重复本身。Three.js提供了允许您控制此操作的功能。texture-repeat-mapping.html 中提供了一个可以使用repeat属性的示例。下面的屏幕截图显示了这个示例:

图10.27–在球体上重复包裹

图10.27–在球体上重复包裹

在该属性具有所需效果之前,需要确保将纹理的环绕设置为THREE.ReatWrapping,如以下代码片段所示:

mesh.material.map.wrapS = THREE.RepeatWrapping; 
mesh.material.map.wrapT = THREE.RepeatWrapping;

wrapS属性定义希望纹理如何沿其X轴缠绕,wrapTproperty定义纹理应如何沿其Y轴缠绕。Three.js为此提供了三个选项,如下所示:

  • THREE.RepeatWrapping允许纹理自身重复
  • THREE.MirroredRepeatWrapping允许纹理自身重复,但每次重复都是镜像的
  • THREE.ClampToEdgeWrapping是纹理不会作为一个整体重复的默认设置;只重复边缘的像素

在本例中,您可以使用各种重复设置以及wrapS和wrapT选项。一旦选择了包装类型,我们就可以设置repeat属性,如以下代码片段所示:

mesh.material.map.repeat.set(repeatX, repeatY);

repeatX变量定义纹理沿其X轴重复的频率,repeatYvariable为Y轴定义相同的频率。如果将这些值设置为1,则纹理不会自行重复;如果将它们设置为更高的值,您将看到纹理将开始重复。也可以使用小于1的值。在这种情况下,您将放大纹理。如果将重复值设置为负值,则纹理将被镜像。

更改repeat属性时,Three.js将自动更新纹理并使用此新设置进行渲染。如果从THREE.RepeatWrapping更改为THREE.ClampToEdgeWrapping,则必须使用mesh.material.map.needsUpdate=true显式更新纹理:

图10.28–夹在球体上的边缘包裹

图10.28–夹在球体上的边缘包裹

到目前为止,我们只使用静态图像作为纹理。然而,Three.js也可以选择使用HTML5画布作为纹理。

渲染到画布并将其用作纹理

在本节中,我们将看两个不同的例子。首先,我们将了解如何使用画布创建简单纹理并将其应用于网格;之后,我们将更进一步,使用随机生成的图案创建一个可以用作凹凸贴图的画布。

将画布用作颜色贴图

在第一个示例中,我们将向HTML Canvas元素渲染分形,并将其用作网格的颜色映射。下面的屏幕截图显示了这个例子(texture-canvas-as-color-map.html):

图10.29-使用HTML画布作为纹理

图10.29-使用HTML画布作为纹理

首先,我们将查看渲染分形所需的代码:

import Mandelbrot from 'mandelbrot-canvas'
...
const div = document.createElement('div')
div.id = 'mandelbrot'
div.style = 'position: absolute'
document.body.append(div)
const mandelbrot = new Mandelbrot(document.
getElementById('mandelbrot'), {
 height: 300,
 width: 300,
 magnification: 100
})
mandelbrot.render()

我们不想谈太多细节,但这个库需要一个div元素作为输入,并将在该div中创建一个画布元素。前面的代码将呈现分形,正如您在前面的屏幕截图中看到的那样。接下来,我们需要将此画布指定给材质的map属性:

const material = new THREE.MeshPhongMaterial({
 color: 0xffffff,
 map: new THREE.Texture(document.querySelector
 ('#mandelbrot canvas'))
})
material.map.needsUpdate = true

在这里,我们只创建一个新的THREE.Texture,并传入对canvas元素的引用。我们唯一需要做的就是将material.map.needsUpdate设置为true,这将触发Three.js从画布元素中获取最新信息,此时我们将看到它应用于网格。

当然,我们可以对迄今为止看到的所有不同类型的地图使用相同的想法。在下一个示例中,我们将使用画布作为凹凸贴图。

将画布用作凹凸贴图

正如您在本章前面所看到的,我们可以使用凹凸贴图将高度添加到模型中。该图中像素的强度越高,褶皱就越高。由于凹凸贴图只是一个简单的黑白图像,因此没有什么能阻止我们在画布上创建它,并将该画布用作凹凸贴图的输入。

在以下示例中,我们将使用画布生成基于Perlin噪声的灰度图像,并将该图像用作应用于立方体的凹凸贴图的输入。请参见texture-canvas-as-bump-map.html示例。以下屏幕截图显示了此示例:

图10.30–使用HTML画布作为凹凸贴图

图10.30–使用HTML画布作为凹凸贴图

这种方法与我们在前面的画布示例中看到的方法基本相同。我们需要创建一个画布元素,并在画布中填充一些噪声。要做到这一点,我们必须使用佩林噪声。Perlin噪波生成了一个看起来非常自然的纹理,正如您在前面的屏幕截图中看到的那样。有关Perlin噪声和其他噪声发生器的更多信息,请点击此处:https://thebookofshaders.com/11/.实现这一点的代码如下所示:

import generator from 'perlin'
var canvas = document.createElement('canvas')
canvas.className = 'myClass'
const size = 512
canvas.style = 'position:absolute;'
canvas.width = size
canvas.height = size
document.body.append(canvas)
const ctx = canvas.getContext('2d')
for (var x = 0; x < size; x++) {
 for (var y = 0; y < size; y++) {
 var base = new THREE.Color(0xffffff)
 var value = (generator.noise.perlin2(x / 8, y / 8) + 1) / 2
 base.multiplyScalar(value)
 ctx.fillStyle = '#' + base.getHexString()
 ctx.fillRect(x, y, 1, 1)
 }
}

我们使用generator.noise.perlin2函数根据canvas元素的x和y坐标创建一个从0到1的值。该值用于在画布元素上绘制单个像素。对所有像素执行此操作将创建一个随机贴图,您可以在前面屏幕截图的左上角看到该贴图。然后,此贴图可以用作凹凸贴图:

const material = new THREE.MeshPhongMaterial({
 color: 0xffffff,
 bumpMap: new THREE.Texture(canvas)
})
material.bumpMap.needsUpdate = true

将THREE.DataTexture用于动态纹理

在本例中,我们使用HTML画布元素渲染了Perlin噪声。Three.js还提供了一种动态创建纹理的替代方法:您可以创建一个Three.DataTexturetexture纹理,在那里您可以传入一个Uint8Array,在那里可以直接设置RGB值。有关如何使用THREE.DataTexture的更多信息,请点击此处:https://threejs.org/docs/#api/en/textures/DataTexture.

我们用于纹理的最后一个输入是另一个HTML元素:HTML5视频元素。

使用视频输出作为纹理

如果您阅读了前面关于渲染到画布的部分,您可能已经考虑过将视频渲染到画布,并将其用作纹理的输入。这是一种方法,但Three.js已经直接支持使用HTML5视频元素(通过WebGL)。查看texture-canvas-as-video-map.html:

image-20230606134801324

图10.31-使用HTML视频作为纹理

使用视频作为纹理的输入很容易,就像使用画布元素一样。首先,我们需要一个视频元素来播放视频:

const videoString = `
<video
 id="video"
 src="/assets/movies/Big_Buck_Bunny_small.ogv"
 controls="true"
</video>
`
const div = document.createElement('div')
div.style = 'position: absolute'
document.body.append(div)
div.innerHTML = videoString

这创建了一个基本的HTML5视频元素,方法是将HTML字符串直接设置为div元素的innerHTML属性。虽然这对测试非常有效,但框架和库通常会为此提供更好的选择。接下来,我们可以将Three.js配置为使用视频作为纹理的输入,如下所示:

const video = document.getElementById('video')
const texture = new THREE.VideoTexture(video)
const material = new THREE.MeshStandardMaterial({
 color: 0xffffff,
 map: texture
})

结果可以在texture-canvas-video-map.html示例中看到。

总结

至此,我们完成了关于纹理的这一章。正如您所看到的,Three.js中有很多纹理,每种纹理都有不同的用途。您可以使用PNG、JPG、GIF、TGA、DDS、PVR、TGA,KTX、EXR或RGBE格式的任何图像作为纹理。加载这些图像是异步完成的,所以请记住在加载纹理时使用渲染循环或添加回调。使用不同类型的纹理,可以从低多边形模型中创建外观良好的对象。

使用Three.js,使用HTML5画布元素或视频元素也可以很容易地创建动态纹理——只需定义一个以这些元素为输入的纹理,并在需要更新纹理时将needsUpdateproperty设置为true。

在这一章结束后,我们几乎涵盖了Three.js的所有重要概念。然而,我们还没有看到Three.jss提供的一个有趣的功能:后处理。使用后处理,可以在场景渲染后将效果添加到场景中。例如,您可以对场景进行模糊或着色处理,或者使用扫描线添加类似电视的效果。在第11章“渲染后处理”中,我们将介绍后处理以及如何将其应用于场景。