🛎 本文为《和我一起学 Three.js 系列》初级篇的第五篇文章,文中的示例代码基于上一篇文章中的代码进行相应的扩展,请确保您已经阅读上一篇文章,并和我一起使用 vite 搭建了现代前端开发环境。
感谢您一路跟随我来到这里!截止目前为止,我们应该有能力搭建一个 3D 场景,在其中添加各种官方提供的几何体,并通过使用控制器,调整摄影机位置与几何体交互。这一切看起来都还不错,但未免有些单调。所幸本章节以及下一节的内容将让我们的 3D 世界变得丰富多彩。
本章节我们会谈及 Web 3D 世界一个非常常见的概念:「纹理」(Texture),它将会和下一章节的「材质」(Materials)概念一起使我们简单的几何体变成真实世界中我们熟悉的物体!让我们一起展开这趟旅途吧!
1. 什么是纹理(Textures)
也许使用纹理(Textures)的另一个名称能更加直观的反映其本质:「贴图」。没错,纹理本身就是一些图片,用来覆盖在我们的几何体或模型的表面,从而使物体具备拟真的效果。
虽然听起来简单,但是别忘了,我们不只是想让物体「看起来」像是某个现实物体,我们还希望它符合物体「在现实中的样子」,我是指在面对不同的光照环境时,物体需要有不同的反应,以及物体需要有其对应的「质感」。这就不是一张图片可以解决的问题了,为此,我们需要有不同类型的图片表达物体的不同属性(例如:哪里透明,哪里应该有金属光泽等等)。还需要了解在 Three.js 的世界中,如何为一个对象加载多张纹理贴图。
让我们先从认识不同的纹理开始:
2. 纹理的种类
🚨 在本章节中,我并不会向您介绍具体「贴图」的方法,因为这涉及「材质」和「光照」等稍后我们要提及的内容。本章节的用意在于让您明白纹理有哪些种类,它们长什么样子,以及在物体上会呈现什么样的效果。
如果您能够成功访问:https://3dtextures.me/ 这个网站,并下载其提供的免费纹理资源,您会发现,其中的一些图片会令人不明所以,它们是不同的纹理类别,有特定的用途:
🔗 图片来源于:https://drive.google.com/drive/folders/1tLmUWv9WocFh88XMqsexdTkMDE6R2ABg
本章节我会使用该纹理进行演示,后续将不再额外标明资源出处。
2.1 反照率纹理(Albedo Texture)
反照率纹理(Albedo Texture)又称「颜色(Color)纹理」,是一种基本的纹理类型,用于模拟物体表面的颜色和反射特性。它是最常用的纹理类型之一,也是创建逼真 3D 场景必不可少的一部分。
💡 在 3D 渲染中,反照率是指物体表面对于不同颜色光线的反射率,通常用一张 RGB 图片来表示。反照率纹理中的每个像素都对应着物体表面上的一个点,这个点的颜色和反射特性可以由纹理像素的颜色值来确定。例如,纹理中的白色像素表示该点表面对所有颜色的光线反射率都很高,黑色像素表示该点表面对所有颜色的光线反射率都很低。
👇 下面的图片是一张反照率纹理:
将反照率纹理应用于一个球体时,会获得这样的效果:
💡 为了演示方便,我设置了强度为
2
的白色环境光以及一束蓝色直射光,并且将粗糙度与金属度设置为0.8
,我想您透露这些只是为了将来您能够实现和我一样的效果,但现在您并不需要知道如何设置。
2.2 高度纹理(Height Texture)
高度纹理(Height Texture)又称为「深度纹理」。在该图像中,每个像素的灰度值被用于表示相应位置的高度或深度。它通常被用于创建几何体的表面细节,例如山峰、岩石、河流等。
通过使用高度纹理,可以在几何体表面添加细节,并模拟光的反射和折射等视觉效果。
👇 下面的图片是一张高度纹理:
在添加高度纹理后,我们的物体将会有明显的凹凸效果:
2.3 透明度纹理(Alpha Texture)
透明度纹理(Alpha Texture)也称为「Alpha 通道纹理」,它是一种特殊的纹理,其中包含了用于控制材质透明度的 Alpha 通道数据。
💡 Alpha 通道是图像中的第四个通道,它表示每个像素的透明度值。在 Alpha 纹理中,Alpha 通道的值被用于控制每个像素的透明度。使用 Alpha 纹理,您可以轻松地创建透明的材质效果。例如,您可以使用 Alpha 纹理来控制几何体表面的透明区域,以实现类似玻璃、水、烟雾等材质的效果。
👇 下面的图片是一张透明度纹理:
但当我们使用透明度纹理并开启透明配置时,我们会得到这样的立方体:
让我们放大球体的一部分,可以看到更明显的透视效果:
2.3.1 🤔 思考题
- 如果您恰好先设置了透明度纹理后设置高度纹理,您可能会发现物体并没有透明效果?您知道这是为什么吗?
欢迎在评论区留言和我讨论。
2.4 法线纹理(Normal Texture)
法线纹理(Normal Texture)是一种常用的纹理映射技术,用于在三维场景中模拟表面细节和几何体形状的变化。它通常是一个 RGB 颜色纹理,其中每个像素的颜色值编码了该像素对应的表面法线方向。
💡 在计算机图形学中,法线(normal)是一个向量,表示平面或曲面在某个点的垂直方向。
当使用法线纹理时,它可以模拟出几何体表面的微小细节,例如凹陷、凸起、皱褶等等。在渲染过程中,根据法线纹理中的像素颜色值计算每个像素的法线方向,从而在表面绘制时应用正确的光照和阴影。与其他纹理映射技术相比,法线纹理可以更加高效地模拟出复杂的细节效果,并且对于性能要求较高的场景,它通常是更好的选择。
在实际应用中,法线纹理通常用于增加几何体表面的真实感和细节,例如在建筑物、地形、人物等场景中。同时,法线纹理也可以和其他纹理映射技术如漫反射贴图、高光贴图、环境贴图等结合使用,从而创造出更加逼真的效果。
👇 下面的图片是一张高度纹理:
当我们将法线纹理添加至我们的球体中时,我们会发现更加细腻的效果:
感觉已经像模像样了?但其实我们目前为止才只走了一半,剩下的三种纹理还可以从不同侧面(主要是如何反馈光线)增强物体的真实感。
2.5 环境光遮蔽纹理(Ambient Occlusion Texture)
环境光遮蔽纹理(Ambient Occlusion Texture)又称 AO 贴图,是一种用于计算光照阴影效果的纹理贴图。
在三维图形渲染中,环境光遮蔽(Ambient Occlusion,简称 AO)是一种近似于全局光照的技术,用于模拟光线在不同物体之间的传播和遮挡效果。通过计算光线在物体表面处的遮蔽程度,可以增强场景的真实感和细节。
环境光遮蔽贴图通常使用灰度图像来表示光线的遮蔽程度,颜色越暗表示遮蔽程度越高,颜色越亮表示遮蔽程度越低。
👇 下面的图片是一张环境光遮蔽纹理:
让我们继续增强我们的物体,得到下面的效果:
请注意看我们立方体的暗部,它比之前有更加立体的效果。
2.6 粗糙度纹理(Roughness Texture)
粗糙度纹理(Roughness Texture)是一种用于模拟物体表面粗糙程度的纹理贴图。
在三维图形渲染中,表面的粗糙度会影响光线在其表面的反射和散射,从而影响物体的光照效果。粗糙度纹理通常使用灰度图像来表示表面的粗糙度,颜色越暗表示表面越光滑,颜色越亮表示表面越粗糙。
👇 下面的图片是一张粗糙度纹理:
在我们的物体上继续添加粗糙度纹理,看一看到物体显得更加细腻:
2.6 金属度纹理(Metalness Texture)
金属度纹理(Metalness Texture)是一种用于模拟表面金属质感的纹理贴图。
💡 在三维图形渲染中,金属表面的光照反应与非金属表面有很大的差异。金属表面的反射主要来自于镜面反射,而非金属表面的反射则包括漫反射和镜面反射。因此,在渲染金属表面时需要指定其金属度属性,以便正确地计算其光照反应。
金属度贴图通常使用灰度图像来表示表面的金属度,颜色越黑表示表面越非金属,颜色越白表示表面越金属。通过调整金属度贴图,可以使物体的表面更真实地反射光线,从而增强渲染效果。
👇 下面的图片是一张金属度纹理:
让我们看看物体的完成形态:
我们的金属骨架显得非常真实,或许通过动图能够更好的观察这一点:
终于,我们了解了所有的纹理类型,并感受到纹理的力量!它让我们的 3D 物体变得更加丰富!但是在进入下一章之前,让我们再了解有关纹理的一个重要知识点:「PBR 标准」。
2.7 PBR 标准
PBR(Physically Based Rendering)是一种基于物理的渲染技术,它是一组标准和约定,用于描述和模拟真实世界中材质和照明的行为。 PBR 渲染器会使用这些标准来模拟物体表面上的反射、折射、阴影和其他光线互动效果。
PBR 的基本思想是使用真实世界中材质的物理特性来模拟材质的外观。这包括使用高光贴图、环境贴图、法线贴图等来描述材质的不同属性。
我们刚才使用的纹理都符合 PBR 标准,您可以从官网的说明中证实这一点:
通常,我们可以通过观察材质的属性来判断它是否符合 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】「初级篇」: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 🤔 思考题
- 建议您使用之前谈及的技巧,亲手实验各种过滤器,并在评论区总结您的观察结果。
4.2 优化纹理资源
和一切优化手段类似,最终我们要回到资源本身,即始终选择满足需求的情况下,更小的资源或将资源压缩到足够小。除此之外,还需要注意由于 mipmapping 技术会二分的切割纹理,因此我们应该始终保障纹理的宽高是「偶数」!
5. 总结
终于结束了!真是一趟漫长的旅途 🚄!希望您觉得这是值得的。在本篇文章中,我向您介绍了 Three.js 里关于纹理的一切信息,包括纹理的类别,加载,操作方式以及一些优化手段,希望那些例子能让您印象深刻,也希望您能够亲自动手实践。在下一篇文章中,我们将探讨一个更高级的概念「材质」,届时我们终将有能力创造出真实,令人惊叹的物体!请您保持好奇,和我继续探索,下周见 👋。
6. 参考资料
- Three.js 官网:threejs.org/;
- three.js journey:threejs-journey.com/;
- PBR 标准:https://marmoset.co/posts/basic-theory-of-physically-based-re…
- 3D 材质:http://3dtextures.me/
7. 使用到的工具
- 截屏:Xnip;
- 屏幕录制:QuickTime Player;
- GIF 图片:GIPHY CAPTURE;
8. 💰 支持创作
您有很多方式可以表达您喜欢这篇文章,并愿意支持我持续创作,例如:
- 点击各类平台「喜欢」按钮;
- 将文章转发在各类您喜欢的平台,并为它写一份简短的推荐语;
- 在评论区留言;
- 关注我的个人公众号「前端乱步」;
- …
无论您选择哪一项,我都会因为您的欣赏而感到愉悦。
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net