Skip to main content

加载和使用纹理

在第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');

在这个代码示例中,我们使用了THREE.TextureLoader的实例从特定位置加载图像文件。 使用此加载器, 您可以使用PNGGIFJPEG图像作为纹理的输入(在本章后面, 我们将向您展示如何加载其他纹理格式)。 请注意,纹理是异步加载的:如果是大型纹理,并且在纹理完全加载之前渲染场景, 您将在短时间内看到未应用纹理的网格。 如果您希望等待纹理加载完成,可以将回调提供给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的幂的正方形纹理可以获得最佳效果。 因此,边长如256 x 256512 x 5121,024 x 1,024等的维度效果最佳。 如果纹理不是2的幂,则Three.js将缩小图像以最接近2的幂的值。

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

纹理的像素(也称为纹素)通常不是一对一地映射到面的像素上。 如果摄像机非常靠近,我们需要放大纹理,如果缩小了,我们可能需要缩小纹理。 为此,WebGL 和 Three.js 提供了一些不同的选项来调整此图像的大小。 这是通过 magFilterminFilter 属性完成的:

  • THREE.NearestFilter:此滤镜使用它能找到的最近纹素的颜色。 用于放大时,这将导致粗糙,用于缩小时,结果将失去很多细节。

  • THREE.LinearFilter:此滤镜更高级; 它使用四个相邻纹素的颜色值来确定正确的颜色。 在缩小中仍会失去很多细节,但放大会更平滑,且不那么粗糙。

除了这些基本值,我们还可以使用 MIP 映射。 MIP 映射是一组纹理图像,每个图像的大小是前一个图像的一半。 这些是在加载纹理时创建的,可以实现更平滑的过滤。 因此,当您有一个正方形纹理(作为2的幂)时, 可以使用一些附加方法来获得更好的过滤效果。 属性可以使用以下值进行设置:

  • THREE.NearestMipMapNearestFilter:此属性选择最能映射所需分辨率的 MIP 映射,并应用我们在上一个列表中讨论的最近过滤原理。放大仍然很粗糙,但缩小效果要好得多。

  • THREE.NearestMipMapLinearFilter:此属性不仅选择单个 MIP 映射,而且选择两个最接近的 MIP 映射级别。在这两个级别上都应用最近过滤,以获得两个中间结果。这两个结果通过线性滤镜传递以获得最终结果。

  • THREE.LinearMipMapNearestFilter:此属性选择最能映射所需分辨率的 MIP 映射,并应用线性过滤原理,这在前一个列表中讨论过。

  • THREE.LinearMipMapLinearFilter:此属性选择的不是单个 MIP 映射,而是两个最接近的 MIP 映射级别。 在这两个级别上都应用线性滤镜,以获得两个中间结果。 这两个结果通过线性滤镜传递以获得最终结果。

如果未显式指定 magFilterminFilter 属性, Three.js 将 magFilter 属性的默认值设为 THREE.LinearFilterminFilter 属性的默认值设为 THREE.LinearMipMapLinearFilter

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

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

在这个例子中,您可以看到纹理很好地包裹在形状周围。 在Three.js中创建几何体时,确保任何使用的纹理都得到正确应用。 这是通过一种称为 UV 映射的技术完成的。通过 UV 映射, 我们可以告诉渲染器纹理的哪个部分应用到特定的面上。 我们将在第13章《使用Blender和Three.js》中详细介绍UV映射, 届时我们将向您展示如何在Three.js中使用Blender轻松创建自定义UV映射。

除了使用 THREE.TextureLoader 加载的标准图像格式外, Three.js 还提供了一些自定义加载器,您可以使用这些加载器加载不同格式的纹理。 如果有特定的图像格式, 您可以查看Three.js分发版中的加载器文件夹(https://github.com/mrdoob/three.js/tree/dev/examples/jsm/loaders) 以查看Three.js是否可以直接加载该图像格式,或者您是否需要手动转换。

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

加载HDR图像作为纹理

HDR图像捕捉比标准图像更高范围的亮度级别,可以更接近人眼所看到的。 Three.js支持EXR和RGBE格式。 如果您有HDR图像,可以微调Three.js渲染HDR图像的方式, 因为HDR图像包含比显示器上显示的亮度信息更多。 这可以通过在THREE.WebGLRenderer中设置以下属性来实现:

  • toneMapping:此属性定义如何将HDR图像的颜色映射到显示器上。 Three.js提供以下选项: THREE.NoToneMappingTHREE.LinearToneMappingTHREE.ReinhardToneMappingTHREE.Uncharted2ToneMappingTHREE.CineonToneMapping。默认值为THREE.LinearToneMapping
  • toneMappingExposure:这是toneMapping的曝光级别。 这可用于微调渲染纹理的颜色。
  • toneMappingWhitePoint:这是用于toneMapping的白点。 这也可用于微调渲染纹理的颜色。 如果要加载EXRRGBE图像并将其用作纹理, 可以使用THREE.EXRLoaderTHREE.RGBELoader。 这与我们在THREE.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示例中,我们展示了如何使用纹理将颜色应用于网格。 在下一节中,我们将看看如何使用纹理通过将虚拟高度信息应用于网格来使模型看起来更详细。

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

凹凸贴图用于增加材质的深度。 您可以通过打开texture-bump-map.html来看到其效果: 在这个例子中,您可以看到模型看起来更加详细,似乎具有更多深度。 这是通过在材质上设置一个额外的纹理,即所谓的凹凸贴图,实现的:

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

在这段代码中,除了设置map属性外,我们还将bumpMap属性设置为一个纹理。 此外,通过前面例子中的菜单提供的bumpScale属性, 我们可以设置凹凸的高度(如果设置为负值,则为深度)。

这个例子中使用的纹理如下所示:

凹凸贴图是一张灰度图像,但您也可以使用彩色图像。 像素的强度定义了凹凸的高度。凹凸贴图只包含像素的相对高度。 它并不表明坡度的方向。 因此,使用凹凸贴图可以达到的细节水平和深度感知是有限的。 要获得更多细节,您可以使用法线贴图。

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

在法线贴图中,不存储高度(位移),而是存储每个像素法线的方向。 不详细讨论,使用法线贴图,您可以创建外观非常详细的模型,而只使用少量顶点和面。 例如,看一下texture-normal-map.html的例子:

在上面的截图中,您可以看到一个外观非常详细的模型。 而且,当模型移动时,您可以看到纹理响应它接收到的光线。 这提供了一个非常逼真的模型,只需要一个非常简单的模型和几个纹理。 以下代码片段显示了如何在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轴进行缩放。不过,最好的方法是保持这些值相同。在这个例子中,您可以尝试调整这些值。

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

然而,法线贴图的问题在于它们不太容易创建。 您需要使用专业工具,如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

在上述代码片段中,我们加载了一个位移贴图,其外观如下:

颜色越亮,顶点的位移越大。 当运行texture-displacement.html示例时, 您将看到位移贴图的结果是一个模型,其形状基于纹理的信息而改变:

除了设置displacementMap纹理外,我们还可以使用 displacementScaledisplacementOffset来控制位移的程度。 关于使用位移贴图的最后一件事是,它只有在您的网格包含大量顶点时才能产生良好的效果。 如果不是这样,位移将不会看起来像提供的纹理,因为顶点太少,无法表示所需的位移。

使用环境光遮蔽贴图添加微妙的阴影

在前面的章节中,您了解了如何在Three.js中使用阴影。 如果设置了正确网格的castShadowreceiveShadow属性, 添加了一些灯光,并正确配置了灯光的阴影相机,Three.js将呈现阴影。

然而,渲染阴影是一个相当昂贵的操作,它会在每个渲染循环中重复进行。 如果有一些灯光或对象在移动,这是必要的,但通常,一些灯光或模型是固定的, 因此如果我们能够计算阴影一次,然后重复使用就会很好。 为了实现这一点,Three.js提供了两种不同的贴图:环境光遮蔽贴图和光照贴图。 在本节中,我们将看一下环境光遮蔽贴图,而在下一节中,我们将看一下光照贴图。

环境光遮蔽是一种技术,用于确定模型的每个部分在场景中受到多少环境光的照射。 在诸如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加载纹理并将其分配给材质的正确属性。 与许多其他纹理一样,我们还可以通过设置aoMapIntenisty属性来调整地图对模型照明的影响程度。 在这个例子中,您还可以看到我们需要将aoMapflipY属性设置为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的模型, 应用了aoMaptexture-ao-map-model.html): 您可以使用右侧的菜单设置aoMapIntensity。 这个值越高,你会从加载的aoMap纹理中看到更多的阴影。 正如您所看到的,拥有环境光遮蔽贴图非常有用, 因为它为模型提供了出色的细节,使其看起来更加逼真。 本章中已经介绍过的一些纹理还提供了可以使用的附加aoMap。 如果打开texture-ao-map.html,您将获得一个简单的类似砖块的纹理, 但这次也加入了aoMap

虽然环境光遮蔽贴图改变了模型的某些部分接收的光线量, Three.js还支持光照贴图,通过指定将额外光照添加到模型的某些部分(大致上)。

制作虚假照明使用光照贴图

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

在这个示例中,我们将使用的光照贴图如图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提供一个名为uv2的额外的uv值集(在代码中未显示), 并且必须使用THREE.TextureLoader加载纹理 - 在这种情况下, 一个用于地板颜色的简单纹理和在Blender中为此示例创建的光照贴图。 结果如下(texture-light-map.html):

如果查看前面的例子,您将看到光照贴图的信息被用于创建一个非常漂亮的阴影, 似乎是由模型投射的。 重要的是要记住,在静态场景中使用静态对象进行烘焙的阴影、光照和环境光遮蔽效果非常好。 一旦对象或光源发生变化或开始移动,您将不得不实时计算阴影。

金属度和粗糙度贴图

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

在这个例子中,我们稍微提前并且还使用了一个环境贴图, 这允许我们在对象的顶部渲染环境的反射。 金属性较高的对象反射更多,而粗糙度较高的对象使反射更加散射。 对于这个模型,我们使用了metalnessMap; 您可以看到对象本身在metalness属性从纹理中较高的地方是闪亮的, 而在metalness属性从纹理中较低的地方是粗糙的。 查看roughnessMap时,我们可以看到基本上相同但是反过来的效果:

如您所见,基于提供的纹理,模型的某些部分比其他部分更粗糙或更有划痕。 对于metalnessMap,材料的值乘以材料的metalness属性; 对于roughnessMap,同样适用,但在这种情况下,该值乘以roughness属性。

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

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

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

使用α贴图创建透明模型

α贴图是控制表面不透明度的一种方式。 如果贴图的值为黑色,则模型的该部分将完全透明,如果为白色,则将完全不透明。 在查看纹理及其应用方法之前, 我们首先来看一个例子(texture-alpha-map.html):

在这个例子中,我们渲染了一个立方体并设置了材质的alphaMap属性。 如果打开这个例子,请确保将材质的transparency属性设置为true。 您可能会注意到,您只能看到立方体的正面部分,与前面的屏幕截图不同, 在那里您可以透过立方体看到另一侧。 原因是,默认情况下,使用的材质的side属性设置为THREE.FrontSide。 为了渲染通常隐藏的一侧,我们必须将材质的side属性设置为THREE.DoubleSide; 您将看到立方体的渲染如前面的屏幕截图所示。

我们在这个例子中使用的纹理非常简单:

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

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

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

使用自发光贴图使模型发光

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

然而,仔细观察,您可能会注意到,虽然对象似乎在发光,但对象本身并不发光。 这意味着您可以使用此功能增强对象,但对象本身并不会对场景的照明产生影响。 对于此示例,我们使用的自发光贴图如下所示:

要加载和使用自发光贴图, 我们可以使用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的旧版本中, 您必须使用THREE.MeshPhongMaterial来制作有光泽的材质, 以及THREE.MeshLambertMaterial用于非有光泽的材质。 本节中使用的镜面反射贴图只能与THREE.MeshPhongMaterial一起使用。 使用镜面反射贴图,您可以定义模型的哪些部分应该是有光泽的, 哪些部分应该是粗糙的(类似于前面看到的metalnessMaproughnessMap) 在texture-specular-map.html示例中, 我们渲染了地球,并使用了镜面反射贴图使海洋比陆地更有光泽:

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

在此贴图中,黑色表示贴图的这些部分的光泽为0%,而白色部分的光泽为100%

要使用镜面反射贴图,我们必须使用THREE.TextureLoader加载贴图, 并将其分配给THREE.MeshPhongMaterialspecularMap属性:

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,如下所示的截图):

在上面的截图中,您可以看到球体反射了环境。 如果您移动鼠标,还会看到反射与摄像机角度相对应,关于您所看到的环境。 为了创建这个示例,执行以下步骤:

  1. 创建一个CubeTexture对象。CubeTexture是一组可应用于立方体各面的六个纹理。
  2. 设置天空盒。当我们有一个CubeTexture时,我们可以将其设置为场景的背景。如果这样做,我们实际上创建了一个非常大的盒子,在盒子内摆放摄像机和物体,这样当我们移动摄像机时,场景的背景也会正确变化。或者,我们还可以创建一个非常大的立方体,应用CubeTexture并将其添加到场景中。
  3. CubeTexture对象设置为材质的cubeMap属性。用于模拟环境的CubeTexture对象应该用作网格的纹理。Three.js会确保它看起来像是环境的反射。

创建CubeTexture相当简单,一旦有了源材料。 您需要的是六个图像,它们共同构成完整的环境。 因此,您将需要以下图片:

  • 正视(posz
  • 背面(negz
  • 向上(posy
  • 向下(negy
  • 向右(posx
  • 向左(negx

Three.js会将这些图像拼接在一起,以创建无缝的环境贴图。 有几个网站可以下载全景图像,但它们通常以球形等距投影的格式呈现,如下所示:

您可以使用这些地图的两种方式。 首先,您可以将其转换为由六个独立文件组成的立方体贴图格式。 您可以使用以下网站在线转换: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.CubeReflectionMappingTHREE.CubeRefractionMapping。 第一个将使对象显示基于加载的cubeMap的反射, 而第二个将使模型变成更透明的玻璃状对象,再次基于cubeMap的信息。

我们还可以将这个cubeMap设置为场景的背景,如下所示:

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.EquirectangularRefractionMappingTHREE.EquirectangularReflectionMapping

这两种方法的结果是一个场景,看起来我们站在一个

宽阔的户外环境中,其中网格反映了环境。侧边菜单允许您设置材质的属性:

// 除了反射外,Three.js还允许您为折射(类似玻璃的对象)使用CubeMap对象。以下是此效果的截图(您可以使用右侧的菜单自行测试):
// 为了获得这个效果,我们只需要将`cubeMap`的`mapping`属性设置为`THREE.CubeRefractionMapping`(默认为反射,也可以通过手动指定`THREE.CubeReflectionMapping`来设置):
cubeMap.mapping = THREE.CubeRefractionMapping

在这个例子中,我们为网格使用了一个静态的环境贴图。 换句话说,我们只看到环境的反射,而不是环境中的其他网格。 在以下截图中,您可以看到,通过一些工作,我们还可以显示其他对象的反射:

// 为了还显示场景中其他对象的反射,我们需要使用一些其他的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来拍摄渲染了所有对象的场景快照, 并使用它设置cubeMap。前两个参数定义了摄像机的近和远属性。 因此,在这种情况下,摄像机仅渲染从0.1到1.0可见的内容。 最后一个属性是我们要将纹理渲染到的目标。 为此,我们创建了一个THREE.WebGLCubeRenderTarget实例。 第一个参数是渲染目标的大小。值越大,反射看起来就越详细。 其他两个属性用于确定在缩放时如何放大和缩小纹理。

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

现在,我们正确设置了CubeCamera, 我们需要确保CubeCamera看到的内容作为纹理应用于我们示例中的立方体。 为此,我们必须将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);
....
}

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

重复包装

当您将纹理应用于由Three.js创建的几何体时,Three.js会尽可能优化地应用纹理。 例如,对于立方体,这意味着每一面将显示完整的纹理,而对于球体, 完整的纹理将包裹在球体周围。 然而,有些情况下, 您可能不希望纹理在整个面或整个几何体上传播,而是希望纹理重复自身。 Three.js提供了允许您控制这一点的功能。 可以在texture-repeat-mapping.html提供的一个示例中调整重复属性。 以下截图显示了这个示例:

在此属性产生期望效果之前,您需要确保将纹理的包装设置为THREE.RepeatWrapping, 如以下代码片段所示:

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

wrapS属性定义了纹理沿其X轴如何包裹,而wrapT属性定义了纹理沿其Y轴如何包裹。 Three.js提供了三个选项,如下所示:

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

在这个例子中,您可以尝试各种重复设置以及wrapSwrapT选项。 一旦选择了包装类型,我们就可以设置repeat属性,如下代码片段所示:

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

repeatX变量定义了纹理沿其X轴重复的次数, 而repeatY变量为Y轴定义了相同的次数。如果这些值设置为1,纹理将不会重复自身; 如果设置为更高的值,您将看到纹理开始重复。您还可以使用小于1的值。 在这种情况下,您将放大纹理。如果将重复值设置为负值,纹理将被镜像。

当更改repeat属性时,Three.js会自动更新纹理并以新的设置进行渲染。 如果从THREE.RepeatWrapping更改为THREE.ClampToEdgeWrapping, 您将不得不显式使用mesh.material.map.needsUpdate = true;更新纹理:

到目前为止,我们只使用了静态图像作为纹理。 然而,Three.js还具有使用HTML5画布作为纹理的选项。

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

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

使用画布作为颜色贴图

在第一个例子中,我们将在HTML画布元素上渲染一个分形图,并将其用作网格的颜色贴图。 以下截图显示了这个例子(texture-canvas-as-color-map.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内创建一个canvas元素。前面的代码将渲染分形,正如您在前面的截图中所看到的。接下来,我们需要将这个canvas分配给我们材质的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从canvas元素获取最新信息,然后我们将看到它应用到网格上。

当然,我们可以使用这个想法来制作到目前为止我们所见过的所有不同类型的贴图。 在下一个例子中,我们将使用画布作为凹凸贴图。

使用画布作为凹凸贴图

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

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

这方面的方法基本上与我们在前一个画布示例中看到的相同。 我们需要创建一个画布元素,并用一些噪声填充该画布。 为此,我们必须使用Perlin噪声。 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函数基于画布元素的x和y坐标创建一个值,该值在0到1之间。这个值用于在画布元素上绘制一个像素。对所有像素执行此操作将创建您在前面截图左上角看到的随机地图。然后可以将此地图用作凹凸贴图:

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

使用THREE.DataTexture进行动态纹理

在此示例中,我们使用HTML画布元素渲染了Perlin噪声。 Three.js还提供了一种动态创建纹理的替代方法: 您可以创建一个THREE.DataTexture纹理,其中可以传递一个Uint8Array, 您可以直接设置RGB值。

有关如何使用THREE.DataTexture的更多信息, 请参阅:https://threejs.org/docs/#api/en/textures/DataTexture

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

使用视频输出作为纹理

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

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

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

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

总结

通过这一章,我们已经完成了关于纹理的讨论。 正如您所看到的,Three.js中提供了许多不同用途的纹理。 您可以使用PNG、JPG、GIF、TGA、DDS、PVR、TGA、KTX、EXR或RGBE格式的任何图像作为纹理。 加载这些图像是异步进行的,因此请记住在加载纹理时使用渲染循环或添加回调函数。 使用不同类型的纹理,您可以从低多边形模型创建出色的对象。

使用Three.js,使用HTML5画布元素或视频元素创建动态纹理也很容易 - 只需将这些元素定义为输入的纹理, 并在希望更新纹理时将needsUpdate属性设置为true

通过本章的学习,我们基本上涵盖了Three.js的所有重要概念。 然而,我们还没有研究Three.js提供的一个有趣功能:后期处理。 通过后期处理,您可以在场景渲染后添加效果。 例如,您可以模糊或着色场景,或使用扫描线添加类似电视的效果。 在第11章“渲染后期处理”中,我们将学习后期处理以及如何将其应用于场景。