和我一起学 Three.js【初级篇】:4. 掌握纹理

欢迎关注「前端乱步」公众号,我会在此分享 Web 开发技术,前沿科技与互联网资讯。

感谢您一路跟随我来到这里!截止目前为止,我们应该有能力搭建一个 3D 场景,在其中添加各种官方提供的几何体,并通过使用控制器,调整摄影机位置与几何体交互。这一切看起来都还不错,但未免有些单调。所幸本章节以及下一节的内容将让我们的 3D 世界变得丰富多彩。

本章节我们会谈及 Web 3D 世界一个非常常见的概念:「纹理」(Texture),它将会和下一章节的「材质」(Materials)概念一起使我们简单的几何体变成真实世界中我们熟悉的物体!让我们一起展开这趟旅途吧!

0. 系列文章合集

本系列第 6,7 章节支持微信公众号内付费观看,将在全系列文章点赞数+评论数 >= 500, 1000 时分别解锁发布。
  1. 《0. 总论》
  2. 《1. 搭建 3D 场景》
  3. 《2. 掌握几何体》
  4. 《3. 掌握摄影机》
  5. 您当前在这里 《4. 掌握纹理》
  6. 《5. 掌握材质》
  7. 《6. 掌握光照》
  8. 《7. 掌握阴影》
  9. 《8. 融会贯通,神功小成》(将于 2023.4.17 更新,敬请期待)

1. 什么是纹理(Textures)

也许使用纹理(Textures)的另一个名称能更加直观的反映其本质:「贴图」。没错,纹理本身就是一些图片,用来覆盖在我们的几何体或模型的表面,从而使物体具备拟真的效果

虽然听起来简单,但是别忘了,我们不只是想让物体「看起来」像是某个现实物体,我们还希望它符合物体「在现实中的样子」,我是指在面对不同的光照环境时,物体需要有不同的反应,以及物体需要有其对应的「质感」。这就不是一张图片可以解决的问题了,为此,我们需要有不同类型的图片表达物体的不同属性(例如:哪里透明,哪里应该有金属光泽等等)。还需要了解在 Three.js 的世界中,如何为一个对象加载多张纹理贴图。

让我们先从认识不同的纹理开始:

2. 纹理的种类

在本章节中,我并不会向您介绍具体「贴图」的方法,因为这涉及「材质」和「光照」等稍后我们要提及的内容。本章节的用意在于让您明白纹理有哪些种类,它们长什么样子,以及在物体上会呈现什么样的效果。

如果您能够成功访问:https://3dtextures.me/ 这个网站,并下载其提供的免费纹理资源,您会发现,其中的一些图片会令人不明所以,它们是不同的纹理类别,有特定的用途:

和我一起学 Three.js【初级篇】:4. 掌握纹理_第1张图片

图片来源于:https://drive.google.com/drive/folders/1tLmUWv9WocFh88XMqsexdTkMDE6R2ABg

本章节我会使用该纹理进行演示,后续将不再额外标明资源出处。

2.1 反照率纹理(Albedo Texture)

反照率纹理(Albedo Texture)又称「颜色(Color)纹理」,是一种基本的纹理类型,用于模拟物体表面的颜色和反射特性。它是最常用的纹理类型之一,也是创建逼真 3D 场景必不可少的一部分。

在 3D 渲染中,反照率是指物体表面对于不同颜色光线的反射率,通常用一张 RGB 图片来表示。反照率纹理中的每个像素都对应着物体表面上的一个点,这个点的颜色和反射特性可以由纹理像素的颜色值来确定。例如,纹理中的白色像素表示该点表面对所有颜色的光线反射率都很高,黑色像素表示该点表面对所有颜色的光线反射率都很低。

下面的图片是一张反照率纹理:

和我一起学 Three.js【初级篇】:4. 掌握纹理_第2张图片

将反照率纹理应用于一个球体时,会获得这样的效果:

和我一起学 Three.js【初级篇】:4. 掌握纹理_第3张图片

为了演示方便,我设置了强度为 2 的白色环境光以及一束蓝色直射光,并且将粗糙度与金属度设置为 0.8,我想您透露这些只是为了将来您能够实现和我一样的效果,但现在您并不需要知道如何设置。

2.2 高度纹理(Height Texture)

高度纹理(Height Texture)又称为「深度纹理」。在该图像中,每个像素的灰度值被用于表示相应位置的高度或深度。它通常被用于创建几何体的表面细节,例如山峰、岩石、河流等。

通过使用高度纹理,可以在几何体表面添加细节,并模拟光的反射和折射等视觉效果。

下面的图片是一张高度纹理:

和我一起学 Three.js【初级篇】:4. 掌握纹理_第4张图片

在添加高度纹理后,我们的物体将会有明显的凹凸效果:

和我一起学 Three.js【初级篇】:4. 掌握纹理_第5张图片

2.3 透明度纹理(Alpha Texture)

透明度纹理(Alpha Texture)也称为「Alpha 通道纹理」,它是一种特殊的纹理,其中包含了用于控制材质透明度的 Alpha 通道数据。

Alpha 通道是图像中的第四个通道,它表示每个像素的透明度值。在 Alpha 纹理中,Alpha 通道的值被用于控制每个像素的透明度。使用 Alpha 纹理,您可以轻松地创建透明的材质效果。例如,您可以使用 Alpha 纹理来控制几何体表面的透明区域,以实现类似玻璃、水、烟雾等材质的效果。

下面的图片是一张透明度纹理:

和我一起学 Three.js【初级篇】:4. 掌握纹理_第6张图片

但当我们使用透明度纹理并开启透明配置时,我们会得到这样的立方体:

和我一起学 Three.js【初级篇】:4. 掌握纹理_第7张图片

让我们放大球体的一部分,可以看到更明显的透视效果:

和我一起学 Three.js【初级篇】:4. 掌握纹理_第8张图片

2.3.1 思考题

  1. 如果您恰好先设置了透明度纹理后设置高度纹理,您可能会发现物体并没有透明效果?您知道这是为什么吗?

欢迎在评论区留言和我讨论。

2.4 法线纹理(Normal Texture)

法线纹理(Normal Texture)是一种常用的纹理映射技术,用于在三维场景中模拟表面细节和几何体形状的变化。它通常是一个 RGB 颜色纹理,其中每个像素的颜色值编码了该像素对应的表面法线方向。

在计算机图形学中,法线(normal)是一个向量,表示平面或曲面在某个点的垂直方向。

当使用法线纹理时,它可以模拟出几何体表面的微小细节,例如凹陷、凸起、皱褶等等。在渲染过程中,根据法线纹理中的像素颜色值计算每个像素的法线方向,从而在表面绘制时应用正确的光照和阴影。与其他纹理映射技术相比,法线纹理可以更加高效地模拟出复杂的细节效果,并且对于性能要求较高的场景,它通常是更好的选择。

在实际应用中,法线纹理通常用于增加几何体表面的真实感和细节,例如在建筑物、地形、人物等场景中。同时,法线纹理也可以和其他纹理映射技术如漫反射贴图、高光贴图、环境贴图等结合使用,从而创造出更加逼真的效果。

下面的图片是一张高度纹理:

和我一起学 Three.js【初级篇】:4. 掌握纹理_第9张图片

当我们将法线纹理添加至我们的球体中时,我们会发现更加细腻的效果:

和我一起学 Three.js【初级篇】:4. 掌握纹理_第10张图片

感觉已经像模像样了?但其实我们目前为止才只走了一半,剩下的三种纹理还可以从不同侧面(主要是如何反馈光线)增强物体的真实感。

2.5 环境光遮蔽纹理(Ambient Occlusion Texture)

环境光遮蔽纹理(Ambient Occlusion Texture)又称 AO 贴图,是一种用于计算光照阴影效果的纹理贴图。

在三维图形渲染中,环境光遮蔽(Ambient Occlusion,简称 AO)是一种近似于全局光照的技术,用于模拟光线在不同物体之间的传播和遮挡效果。通过计算光线在物体表面处的遮蔽程度,可以增强场景的真实感和细节。

环境光遮蔽贴图通常使用灰度图像来表示光线的遮蔽程度,颜色越暗表示遮蔽程度越高,颜色越亮表示遮蔽程度越低。

下面的图片是一张环境光遮蔽纹理:

和我一起学 Three.js【初级篇】:4. 掌握纹理_第11张图片

让我们继续增强我们的物体,得到下面的效果:

和我一起学 Three.js【初级篇】:4. 掌握纹理_第12张图片

请注意看我们立方体的暗部,它比之前有更加立体的效果。

2.6 粗糙度纹理(Roughness Texture)

粗糙度纹理(Roughness Texture)是一种用于模拟物体表面粗糙程度的纹理贴图。

在三维图形渲染中,表面的粗糙度会影响光线在其表面的反射和散射,从而影响物体的光照效果。粗糙度纹理通常使用灰度图像来表示表面的粗糙度,颜色越暗表示表面越光滑,颜色越亮表示表面越粗糙。

下面的图片是一张粗糙度纹理:

和我一起学 Three.js【初级篇】:4. 掌握纹理_第13张图片

在我们的物体上继续添加粗糙度纹理,看一看到物体显得更加细腻:

和我一起学 Three.js【初级篇】:4. 掌握纹理_第14张图片

2.6 金属度纹理(Metalness Texture)

金属度纹理(Metalness Texture)是一种用于模拟表面金属质感的纹理贴图。

在三维图形渲染中,金属表面的光照反应与非金属表面有很大的差异。金属表面的反射主要来自于镜面反射,而非金属表面的反射则包括漫反射和镜面反射。因此,在渲染金属表面时需要指定其金属度属性,以便正确地计算其光照反应。

金属度贴图通常使用灰度图像来表示表面的金属度,颜色越黑表示表面越非金属,颜色越白表示表面越金属。通过调整金属度贴图,可以使物体的表面更真实地反射光线,从而增强渲染效果。

下面的图片是一张金属度纹理:

和我一起学 Three.js【初级篇】:4. 掌握纹理_第15张图片

让我们看看物体的完成形态:

和我一起学 Three.js【初级篇】:4. 掌握纹理_第16张图片

我们的金属骨架显得非常真实,或许通过动图能够更好的观察这一点:

终于,我们了解了所有的纹理类型,并感受到纹理的力量!它让我们的 3D 物体变得更加丰富!但是在进入下一章之前,让我们再了解有关纹理的一个重要知识点:「PBR 标准」。

2.7 PBR 标准

PBR(Physically Based Rendering)是一种基于物理的渲染技术,它是一组标准和约定,用于描述和模拟真实世界中材质和照明的行为。 PBR 渲染器会使用这些标准来模拟物体表面上的反射、折射、阴影和其他光线互动效果。

PBR 的基本思想是使用真实世界中材质的物理特性来模拟材质的外观。这包括使用高光贴图、环境贴图、法线贴图等来描述材质的不同属性。

我们刚才使用的纹理都符合 PBR 标准,您可以从官网的说明中证实这一点:

和我一起学 Three.js【初级篇】:4. 掌握纹理_第17张图片

通常,我们可以通过观察材质的属性来判断它是否符合 PBR 标准。符合 PBR 标准的材质通常包括漫反射、高光、法线、粗糙度、金属度等属性,而且这些属性是相对独立的,可以单独进行调整。如果材质缺少其中的某个属性,或者属性之间没有相互关联,那么它可能不符合 PBR 标准。

3. 纹理的使用方式

在掌握各种纹理类型后,接下来我们需要回到 3D 场景中,了解如何将纹理添加到几何体上。

3.1 纹理加载

3.1.1 直接加载纹理

虽然纹理就是一张图片,但是我们却不能通过图片链接直接加载,而需要实例化一个 Texture 对象。这是因为 WebGL 需要一种特殊的数据结构与 GPU 交互,例如对纹理进行一些预处理,生成纹理坐标等。

下面是在 Three.js 中正确加载一张图片的方式,看起来会稍显复杂:

const image = new Image();
const texture = new THREE.Texture(image);
image.addEventListener('load', () => {
    texture.needsUpdate = true;
});
image.src = '...';

《 和我一起学【Three.js】「初级篇」:1. 搭建 3D 场景》这篇文章中,我们介绍过在 3D 世界中创建物体的方法:

const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial();
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

不知道您是否还有印象这张图片:

和我一起学 Three.js【初级篇】:4. 掌握纹理_第18张图片

提示:出现在《 和我一起学【Three.js】「初级篇」:2. 掌握几何体》中。

您应该已经完全理解,3D 世界中的物体由「几何体(Geometry)」和「材质(Material)」构成。虽然材质将是我们下一章讨论的主题,但是现在我们可以暂时剧透一下它和纹理之间的关系:

  • 从功能角度上看:材质是更高级的概念,它会将多个纹理组合起来,以定义 3D 对象表面的复杂外观和材质属性。
  • 从代码角度上看:纹理是材质的一个参数

因此,要为物体添加纹理,我们需要通过如下代码:

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

如下方所示,您可以看到我们的立方体已不再是从前那个单调的立方体:

我使用了 3D TEXTURES 网站的 stylized fur 02 纹理,让立方体上有很多毛确实有些古怪(虽然我觉得也未尝不可),但是您可以替换成任何您喜欢的纹理!

3.1.2 使用 TextureLoader

除了上述方法外,TextureLoader 对象实例本身还有一个 .load() 方法,通过该方法,我们可以更优雅地获取图片加载状态并绑定对应的逻辑:

const textureLoader = new THREE.TextureLoader()
const texture = textureLoader.load(
    '/textures/custom/Stylized_Fur_002_basecolor.jpg',
    () =>
    {
        console.log('loading finished')
    },
    () =>
    {
        console.log('loading progressing')
    },
    () =>
    {
        console.log('loading error')
    }
)

注意三个回调函数分别表示图片「加载成功」,「加载中」与「加载失败」三种状态。

3.1.3 使用加载管理器

当同时加载多张图片时,您可能不希望一次又一次地将相同的逻辑绑定在每一张图片上(这也违背了 DRY 原则),此时,您可以使用 LoadingManager 对象,要使用它,我们同样需要初始化一个实例:

const loadingManager = new THREE.LoadingManager()

loadingManager.onStart = () => {
    console.log('loading started')
}
loadingManager.onLoad = () => {
    console.log('loading finished')
}
loadingManager.onProgress = () => {
    console.log('loading progressing')
}
loadingManager.onError = () => {
    console.log('loading error')
}

const textureLoader = new THREE.TextureLoader(loadingManager)

之后,再使用 textureLoader 时我们就可以复用绑定的逻辑。当我们需要在所有素材加载成 功后移除进度条并展示内容时,加载管理器就特别有用。

3.2 UV 展开问题

也许您会感到疑惑,如果将纹理应用于奇怪的立体图形上,会发生什么?答案是 Three.js 会帮助我们以一定的规则贴图(虽然可能不是我们想要的)。这就涉及到三维模型表面上的每个点如何映射到二维平面上的问题。这一类问题我们称之为「UV 展开问题」。

要解决这个问题我们需要根据模型的几何形状和拓扑关系,对模型表面进行切割和展开,以便将每个点对应的纹理坐标计算出来。对于一些几何体而言,这个过程会非常复杂,因此我们在这里只是大概了解这个概念,并不会深入介绍。

在计算机图形学中,UV 通常指的是二维纹理坐标,用于将纹理映射到三维模型表面上。它通常用 (u, v) 表示,它们的取值范围是 [0, 1]。在 UV 坐标空间中,(0, 0) 表示纹理图像的左下角,(1, 1) 表示纹理图像的右上角。通过调整 UV 坐标的取值,可以控制纹理在模型表面上的映射方式,实现不同的纹理效果。

3.3 操作纹理

就像在 CSS 中操作背景图片一样,我们也可以通过类似的方式设置纹理是否重复,旋转多少度以及是否需要一些位移,下面我们将分别看看如何实现这些功能:

3.3.1 重复

在 Three.js 中,纹理默认会被拉伸至整个材质,如果希望重复纹理贴图,那么就需要额外告知 Three.js 如何处理纹理边缘:

texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.RepeatWrapping
当设置为 THREE.MirroredRepeatWrapping 时代表镜像重复。

上面的代码分别设置了横向和纵向的重复模式,之后,就可以通过纹理实例对象上的 repeat 属性重复使用材质:

const texture = textureLoader.load('...')
texture.repeat.x = 2
texture.repeat.y = 3

3.3.2 旋转

通过 rotation 属性可以旋转纹理:

texture.rotation = Math.PI * 0.25

默认情况下,纹理会围绕 UV 坐标 (0, 0) 点进行旋转,如果要围绕纹理中心旋转需要使用 center 属性:

texture.center.x = 0.2
texture.center.y = 0.3

3.3.3 位移

改变纹理位移非常简单,使用 offset 属性即可:

texture.offset.x = 0.4
texture.offset.y = 0.6

4. 纹理优化技术

这里的纹理优化是指,如何在最大化减少计算资源的情况下,获得最好的视觉效果。为此,我们可以分别从两个角度出发:「算法」和「资源」。

4.1 使用过滤器和 Mipmapping

Three.js 会使用一种名为 mipmapping 的纹理优化技术,它通过预先生成一系列不同大小的纹理贴图(也称为 mipmap),来减少在渲染过程中的计算和内存消耗。

在使用 mipmapping 技术时,WebGL 将纹理图像分解成一系列递减的尺寸,从原始尺寸开始,每次缩小到原来的一半,直到缩小到一个像素。这些不同大小的纹理贴图被存储在 GPU 内存中,随着渲染距离的变远,GPU 会自动选择更小的贴图来显示,从而减少了纹理贴图在远处的像素数提高性能。

使用 mipmapping 技术可以减少因纹理采样而导致的失真和锯齿,提高纹理质量。同时,由于更小的纹理贴图需要更少的内存,因此也可以减少内存占用。

而 mipmapping 技术对于开发者的意义在于,当纹理图像的分辨率大于或小于模型的分辨率时,Three.js 让开发者能够通过 API 选择合适的过滤算法以取得计算速度与渲染效果之间的平衡。

4.1.1 缩小过滤器

通过纹理上的 minFilter 属性可以配置纹理的缩小过滤器,以应对纹理图像分辨率大于物体分辨率,需要缩小时的效果,有如下可选值:

  • THREE.LinearMipmapLinearFilter(默认):使用 MipMap 和线性插值,效果比较平滑,但是计算速度较慢;
  • THREE.NearestFilter:使用最近邻插值,这种插值方式会产生明显的马赛克效果,但是计算速度比较快;
  • THREE.LinearFilter:使用线性插值,效果比较平滑,但是计算速度比较慢;
  • THREE.NearestMipmapNearestFilter:使用最近邻插值和 MipMap,这种插值方式会产生明显的马赛克效果,但是计算速度较快;
  • THREE.NearestMipmapLinearFilter:使用 MipMap 和最近邻插值,这种插值方式会产生明显的马赛克效果,但是计算速度较快;
  • THREE.LinearMipmapNearestFilter:使用线性插值和 MipMap,效果比较平滑,但是计算速度较慢;

您可以这样配置:

texture.minFilter = THREE.NearestFilter

4.1.2 放大过滤器

您可以通过 magFilter 属性配置放大过滤器,它的使用场景刚好和缩小过滤器相反,并且可选值也少的多,只有两个:

  • THREE.LinearFilter(默认):使用线性插值,效果比较平滑,但是计算速度比较慢;
  • THREE.NearestFilter:使用最近邻插值,这种插值方式会产生明显的马赛克效果,但是计算速度比较快;

4.3 思考题

  1. 建议您使用之前谈及的技巧,亲手实验各种过滤器,并在评论区总结您的观察结果。

4.2 优化纹理资源

和一切优化手段类似,最终我们要回到资源本身,即始终选择满足需求的情况下,更小的资源或将资源压缩到足够小。除此之外,还需要注意由于 mipmapping 技术会二分的切割纹理,因此我们应该始终保障纹理的宽高是「偶数」!

5. 总结

终于结束了!真是一趟漫长的旅途 !希望您觉得这是值得的。在本篇文章中,我向您介绍了 Three.js 里关于纹理的一切信息,包括纹理的类别,加载,操作方式以及一些优化手段,希望那些例子能让您印象深刻,也希望您能够亲自动手实践。在下一篇文章中,我们将探讨一个更高级的概念「材质」,届时我们终将有能力创造出真实,令人惊叹的物体!请您保持好奇,和我继续探索,下一篇见 。

你可能感兴趣的:(和我一起学 Three.js【初级篇】:4. 掌握纹理)