🛎 本文为《和我一起学 Three.js 系列》「初级篇」的第六篇文章,文中的示例代码基于往期文章的代码进行扩展,在动手实践前,请确保您已经在本地创建好了引入 three.js 的现代前端开发环境。
时间过的真快!今天(2023.3.27),我终于按照当初每周更新一篇文章的计划,完成了《和我一起学 Three.js 系列》「初级篇」的 6 篇「无条件发布」的文章。每篇文章我都花了大量的时间搜集资料,从 0 开始编写代码(以保障您操作的结果和我相同)以及思考如何让文章更易于理解,截止目前,您应该已经可以在 3D 场景中添加各种立体物品并与之交互,您还应该了解到纹理的不同种类,以及它是让简单的 3D 模型变为现实物品的一把关键钥匙 🔑。
而本章节,我们不仅会让上一章所学没有白费,还会更进一步,向您展示 3D 世界又一个激动人心的概念:「材质(Materail)」,通过学习本章节的内容,您将有能力串联起来之前所有的知识,打造一个充满趣味的 3D 世界!
话不多说,让我们开始吧 🤜 !
0. 系列文章合集
- 《👋 和我一起学【Three.js】「初级篇」:0. 总论》
- 《👋 和我一起学【Three.js】「初级篇」:1. 搭建 3D 场景》
- 《👋 和我一起学【Three.js】「初级篇」:2. 掌握几何体》
- 《👋 和我一起学【Three.js】「初级篇」:3. 掌握摄影机》
- 《👋 和我一起学【Three.js】「初级篇」:4. 掌握纹理》
- 您当前在这里 👉 《👋 和我一起学【Three.js】「初级篇」:5. 掌握材质》
- 🚧《👋 和我一起学【Three.js】「初级篇」:6. 掌握光照》(🚨 本篇文章将于 2023.4.3 更新并支持公众号内付费观看,将在全系列文章总赞数 >= 500 时解锁发布)
- 🚧《👋 和我一起学【Three.js】「初级篇」:7. 掌握阴影》(🚨 本篇文章将于 2023.4.10 更新并支持公众号内付费观看,将在全系列文章总赞数 >= 1000 时解锁发布)
- 🚧《👋 和我一起学【Three.js】「初级篇」:8. 融会贯通,神功小成》(🚨 本篇文章将于 2023.4.17 更新并支持公众号内付费观看,将在第 7,8 章节解锁后发布)
1. 什么是材质(material)?
在 Three.js 中,「材质(material)」是用来定义物体外观的属性。它包含如何渲染物体的信息,如颜色,光照,反射等等。材质可以被赋予不同的属性,以便实现各种不同的外观效果。
我们上一章所提到的「纹理(Texture)」是材质的一种属性,它可以被用来在物体表面添加图像,图案,颜色等。纹理可以用来模拟各种不同的表面特性,例如木头、金属、石头等等。
为了区别「材质」和「纹理」的概念,我们可以想象一件木质家具。该家具的「材质」是木头,但它的外观可能会因为在木表面上应用不同的纹理而产生不同的效果。例如,一个家具制造商可能会在木表面上应用一层光滑的漆,使其看起来更加光滑;另一个家具制造商则可能会在木表面上应用一个粗糙的纹理,使其看起来更加天然。同样地,在 Three.js 中,我们可以通过应用不同的纹理来改变物体的外观,以实现我们想要的效果。
💡 在 Three.js 中,材质被用于为几何体的每个可见像素着色,这一过程的算法实现被称为「着色器(shader)」(一种在 GPU 上运行的程序,它用于计算几何体的每个像素的颜色和外观,可以对材质进行定制化的着色和光照计算,以及其他各种效果)。
至此,我们可以将之前一张描述 Three.js 中创建 3D 对象的类关系图更新为下面这样:
2. 材质的通用属性
不知道您是否还记得,我们在《👋 和我一起学【Three.js】「初级篇」:1. 搭建 3D 场景》一文中提到过使用材质的方法:实例化一个材质类型对象,并将实例化后的对象作为第二个参数传递给网格(Mesh)对象。
const geometry = new THREE.BoxGeometry()
const material = new THREE.MeshBasicMaterial()
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)
💡 我们可以创建一个材质后重复利用于多个几何体,这将有利于提升应用性能。
但材质的用法可不止这些,本章我们将逐步解锁更多材质的使用方法。我们将先来看看材质的一些通用且常用的属性,然后在介绍材质的种类时,再针对特定材质介绍一些特殊的属性。
2.1 纹理配置属性
我们上一次用了一整章内容介绍了纹理的概念,但并没有向您详细介绍该如何使用它。现在是时候了,答案也很简单,使用材质上的一系列「纹理 map
」 属性:
const geometry = new THREE.SphereGeometry(0.5, 64, 64);
const material = new THREE.MeshStandardMaterial({
map: colorTexture,
});
material.metalness = 0.8;
material.roughness = 0.8;
geometry.setAttribute(
"uv2",
new THREE.BufferAttribute(geometry.attributes.uv.array, 2)
);
material.displacementMap = heightTexture;
material.displacementScale = 0.03;
material.alphaMap = opacityTexture;
material.transparent = true;
material.opacity = 0.75;
material.normalMap = normalTexture;
material.aoMap = aoTexture;
material.aoMapIntensity = 2;
material.roughnessMap = roughnessTexture;
material.metalnessMap = metalTexture;
下面是 Three.js 中几乎所有 map
属性所定义纹理及用途的信息:
2.1.1 map
属性(_非常常用_)
用于添加「颜色纹理(Color Texture
)」(也称为反照率纹理(Albedo Texture
)),给物体表面添加图案,细节或者其他颜色纹理。
2.1.2 specularMap
(比较常用)
用于添加「镜面高光纹理(Specular Texture
)」,调整物体的反射和高光效果,增强物体的光照效果。
2.1.3 normalMap
(比较常用)
用于添加「法线纹理(Normal Texture
)」,给物体表面添加细节和深度感。
2.1.4 displacementMap
(_不常用_)
用于添加「高度纹理(Height Texture
)」,使物体表面产生凸起或凹陷的效果。
2.1.5 alphaMap
(比较常用)
用于添加「透明度纹理(Alpha Texture
)」实现透明效果。
2.1.6 emissiveMap
(比较常用)
用于添加「自发光纹理(Emissive Texture
)」,实现物体表面的自发光效果,使物体更具有光泽感。
2.1.7 envMap
(比较常用)
用于添加「环境纹理(Environment Texture
)」,实现反射和折射效果。
2.1.8 aoMap
(比较常用)
用于添加「环境光遮蔽纹理(Ambient Occlusion Texture
)」,调整物体表面的阴影和遮蔽效果。
2.1.9 roughnessMap
(比较常用)
用于添加「粗糙度纹理(Roughness Texture
)」,调整物体表面的光滑度。
2.1.10 metalnessMap
(比较常用)
用于添加「金属度纹理(Metalness Texture
)」调整物体表面的金属度。
⚠️ 要注意,并不是所有的材质都拥有以上属性(因为不同的材质类型需要实现不同的外观效果,而不同的纹理贴图可以用于实现不同的效果。),因此虽然我们已经了解了所有的纹理类型,但是不同的纹理类型,只作用于对应的几种材质。了解材质和纹理之间的对应关系将是本文稍后将介绍的一大主题。
2.2 color
属性
很显然,color
属性用于指定物体的颜色,但是需要注意,与直觉不同,您需要通过实例化 THREE.Color
对象才能成功配置:
material.color = new THREE.Color("red")
2.3 wireframe
属性
我们之前提到过,WebGL 只会绘制三角形,而 wireframe
属性则是一个开关,用来表示一个物体是否仅使用 1px
的线勾勒三角形。将我们上一篇文章中的物体开启该属性,将会有这样的效果:
material.wireframe = true
2.4 opacity
属性
很显然,opacity
属性用来控制物体的透明度,它的取值范围在 0
到 1
之间。若要想 opacity
值生效,您需要同时设置 transparent
属性:
materail.transparent = true
material.opacity = 0.5
如先前所说,当要使用「透明度纹理」时,您需要首先确保 transparent
属性开启。
2.5 side
属性
假如我们旋转一个「平面(Plane)」,在一定角度后(如下图所示),我们会发现平面消失了。
这是因为,默认情况下,Three.js 仅渲染物体「前面(THREE.FrontSide
)」,如果您需要让物体的背面也能在物体移动中看到,那么您需要手动使用 side
属性定义物体的两面都要渲染:
material.side = THREE.DoubleSide
🚨 有时候,虽然您可以这么做,但并不意味着您「应该」这么做,因为渲染物体的两面就意味着至少双倍的计算资源,您需要谨慎考虑这种开销是否值得。
3. 常用材质的种类
介绍完了材质的基本属性,现在是时候看看 Three.js 提供的多种材质了,截止目前(2023.3.27),官方共提供了 17
种材质供您选择,您可以通过查阅官方文档获取更详细的信息。
👋 本文并不会向您介绍所有的 17 种材质类型,而只选择介绍其中最常用的 9 种。在这些材质中,使用 💡 符合开头的材质说明要想成功显示物体,需要场景中有光源。
3.1 基础材质(MeshBasicMaterial
)
「基础材质(MeshBasicMaterial
)」是 Three.js 中最基础,最简单的一种材质类型,没有高级的渲染效果,只能实现基本的平面渲染。它不会产生阴影、反射或其他高级效果,只能显示一个单一的颜色或纹理。
通常,基础网格材质被用于测试、简单展示和快速原型设计等场景。
它支持的纹理属性有:
-
map
; -
specularMap
; -
alphaMap
; -
envMap
; -
aoMap
; -
lightMap
;
⚠️ 您可能会好奇,如果设置了不支持的属性会发生什么,答案是这需要分情况讨论,对于
roughnessMap
这类属性,什么都不会发生,程序会自动忽略掉这条指令,但是如果是normalMap
或displacementMap
属性,程序则会报错(因为这些属性需要通过「顶点着色器」(vertex shader
)或「片段着色器」(fragment shader
)来计算每个顶点或像素的偏移或法线方向,而MeshBasicMaterial
只有一个基础着色器(basic shader
),不能执行这些高级的计算)。因此,您应该始终确定只在材质上设置其支持的纹理属性。
下面的图像是我们上一篇内容生成的球体以及替换为基础材质后的效果:
可以明显看到,使用基础材质的球体丧失了诸如金属光泽,透明度等细节。
3.2 深度材质(MeshDepthMaterial
)
「深度网格材质(MeshDepthMaterial
)」用于渲染模型的深度信息。它的特点是不考虑光照和颜色等因素,仅仅根据模型的深度信息进行渲染。我们通常将其用于深度测试,深度图像渲染等场景,也可以使用它创建一些特殊效果,例如 3D 深度图像渲染,水下效果,立体显示效果等。
它支持的纹理属性有:
-
map
; -
alphaMap
; -
displacementMap
;
下图是将我们的球体使用深度材质替换后的效果:
为什么我们的球体变得黑乎乎的?这是因为深度网格材质会根据对象距离摄影机的距离判断物体的明亮程度。当物体的位置接近摄影机的「近值」时,物体会被用白色着色,当物体接近摄影机的「远值」时,则使用黑色着色。
👋 如果您恰好忘记了关于「近值」和「远值」的概念,也许这是个合适的时机通过本系列第四篇文章《👋 和我一起学【Three.js】「初级篇」:3. 掌握摄影机》进行回顾。
现在,让我们把之前我们设置的摄影机近值从 0.1
调整为 1
看看效果:
不出意外,物体变的更亮了。
3.3 捕捉材质(MeshMatcapMaterial
)
「捕捉材质(MeshMatcapMaterial
)」是 Three.js 中的一种特殊材质,它的渲染效果基于预先计算好的 Matcap(Material Capture)纹理,纹理中包含了表面的光照和颜色信息。它可以实现高效、轻量级的渲染效果,因为它不依赖于实时计算光照(这意味它不响应灯光,但它将在接收阴影的对象上投射阴影(并且阴影剪切有效),但它不会自我遮挡或接收阴影)。
👋 「光照」和「阴影」将是下两章我们的主题,您现在不必深究,有一个大致的印象即可。
它支持的纹理属性有:
-
map
; -
alphaMap
; -
displacementMap
; -
normalMap
; -
bumpMap
;
Matcap 纹理通常是一个球形的图像,其中每个像素代表了一个特定的表面法线方向和对应的颜色。在渲染过程中,物体表面的法线信息被用于从 Matcap 纹理中查找对应的颜色。下面是一张 Matcap 纹理:
下图是在我们的球体上应用该纹理后的样子,即使我们关掉所有光源的设置,物体依然会有明暗的光影信息:
3.4 法线材质(MeshNormalMaterial
)
「法线材质(MeshNormalMaterial
)」会将顶点法线向量映射到 RGB 颜色空间中,通过将 X、Y、Z 分量映射到 R、G、B 通道中来表示法线方向。换句话说,它会根据物体表面的法向量信息,将每个像素的法向量转换为颜色值,然后将这个颜色值作为像素的最终颜色。
💡 法向量(normal vector)是指垂直于曲面或几何体表面的向量。在三维图形学中,法向量通常用于表示几何体表面的朝向和曲面的方向,是渲染器计算阴影、反射、光照等高级渲染效果的重要基础。
法线材质通常用于调试和可视化几何模型的法线信息,例如调试模型的表面是否正常、判断模型是否对光源反射正确等,也可以用于创建一些抽象的艺术效果,例如模拟海洋、云彩等场景。您可以在这个网站浏览更多使用法线网格材质创建的数字艺术图像。
法线网格材质支持的纹理属性有:
-
displacementMap
; -
normalMap
; -
map
;
下面的图像是将球体替换为法线材质后的效果:
法线材质有一个额外值得注意的属性 flatShading
,它用于控制材质的平滑程度。当 flatShading
属性值为 true
时,表示使用「平面着色」(flat shading
);为 false
时,表示使用「光滑着色」(smooth shading
)。
💡「平面着色」是一种简单的着色方式,它将一个面上所有像素的颜色值设为该面法向量的颜色值,不考虑像素之间的平滑过渡。这种着色方式在模型表面包含较多平坦区域时比较适用,可以减少不必要的计算,提高渲染效率。
💡「光滑着色」是一种复杂的着色方式,它会在模型表面像素之间进行插值计算,使得渲染结果更加真实和逼真,但也会增加计算量,影响渲染效率。这种着色方式适用于模型表面包含大量细节和曲面的场景。
flatShading
属性的应用场景取决于模型表面的几何形状。如果模型表面包含大量平坦区域,使用平面着色可以提高渲染效率;如果模型表面包含大量细节和曲面,使用光滑着色可以获得更加真实和逼真的渲染效果。
该属性不仅可以应用于法线材质,还可以应用于我们稍后会介绍的标准材质,兰伯特材质等高级材质中。
3.5 💡 兰伯特材质(MeshLambertMaterial
)(💡 需要光照!)
「兰伯特材质(MeshLambertMaterial
)」是一个可以响应光线的材质(🚨 这意味着当您使用该材质时,如果场景中没有光源,您将无法看到物体!),一种用于非光亮表面的材料,因为它不存在镜面高光。
💡 Lambertian 模型基于表面法向量和光线方向的角度来计算光照效果。它是一种简化的光照模型,不涉及镜面反射,因此更加高效且易于计算。
兰伯特材质经常用于性能有限的设备上,模拟磨砂表面的物体。
它支持的纹理属性有:
-
map
; -
specularMap
; -
alphaMap
; -
aoMap
; -
displacementMap
; -
envMap
; -
normalMap
; -
emissiveMap
; -
lightMap
; -
bumpMap
;
下面的图像是我们上一篇内容生成的球体替换为基础网格材质后的效果:
👋 为了让您得到和我一样的效果,我需要想您提前剧透一下我对光源的设置:
const ambientLight = new THREE.AmbientLight(0xffffff, 10);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight("blue", 1);
directionalLight.position.set(1, 2, 0);
scene.add(directionalLight);
3.6 💡 冯氏材质(MeshPhongMaterial
)
「冯氏材质(MeshPhongMaterial
)」和之前介绍的兰伯特材质十分相似,但却支持了兰伯特材质所缺失的镜面反光效果。基于冯氏光照模型进行渲染的冯氏材质结合了漫反射、镜面反射和环境光三种光照效果,可以实现更真实的光照表现。但这一方面也意味着更大的性能开销。
💡 冯氏光照模型由冯·卡尼(Bui Tuong Phong)提出。
在需要真实感渲染的场景中,冯氏材质是一个不错的选择。
冯氏材质支持的纹理与兰伯特材质完全相同。
从下方图片可以看到,使用冯氏材质的球体效果虽然和我们上一节使用标准材质的效果有些区别,但已经比之前的简单材质有明显改善:
冯氏材质还有两个特殊的属性:
-
shininess
用于控制物体表面材质的反光效果,该值越高,表面越亮; -
specular
用于控制镜面反射的颜色;
material.shininess = 100
material.specular = new THREE.Color("green")
3.7 💡 标准材质(MeshStandardMaterial
)
💡 这是上一篇文章中我们使用的材质,它具有广泛的纹理支持度。
「标准材质(MeshStandardMaterial
)」是一种 PBR 的材质。它根据现实世界的物理光照原理对物体表面进行渲染,以实现更真实,自然的光照效果。它还可以模拟各种金属和非金属材质。
使用标准材质渲染的物体将会比之前提到的兰伯特材质和冯氏材质拥有更加逼真的效果,让然,这也意味着更多的性能消耗。
标准材质可以应用我们目前介绍的所有纹理类别 🆒!
💡 PBR(Physically Based Rendering,基于物理的渲染)是一种计算机图形学中的渲染技术,它根据现实世界的物理光照原理对物体表面进行渲染。PBR 的目标是实现更真实,自然的光照效果,使渲染结果在各种光照条件下都具有一致性和可预测性。
关于 PBR 技术的更多内容,可以参考:
- Basic Theory of Physically Based Rendering
- Physically Based Rendering and You Can Too
3.8 💡 物理材质(MeshPhysicalMaterial
)
「物理材质(MeshPhysicalMaterial
)」是对标准材质的一种扩展,可以理解为「标准材质 Pro」,这意味着它同样基于 PBR 技术,有更好的显示效果以及需要消耗比标准材质还要多的计算资源。
除此之外,它还提供了包含透明度的众多属性,用于给物体添加更多细节,例如:
-
clearcoat
属性:一些材料(如汽车漆、碳纤维和湿润表面)需要在另一层不规则或粗糙的表面上覆盖一个透明反射层。clearcoat
近似这种效果,无需单独使用透明表面; -
transmission
属性:使用.opacity
的一个限制是高度透明材料反射较少。而通过transmission
属性则能为玻璃等薄而透明的表面提供更真实的效果; -
reflectivity
属性:可以使非金属材料更灵活地反射光线; -
sheen
属性: 可用于表示布和织物材料;
让我们将这些属性统统添加于我们的球体之上:
material.clearcoat = 1;
material.transmission = 0.8;
material.reflectivity = 0.8;
material.sheen = 0.8;
我们会得到下面这个更加精致的球体!(我专门使用了动图来表现,这是它应得的!)
3.9 💡 卡通材质(MeshToonMaterial
)
卡通材质(MeshToonMaterial
)如它的名字一样,是一种具有卡通风格材质类型,在渲染效果上,它类似于兰伯特材质的渲染效果。默认情况下,他只会对「阴影」和「光照」两个部分着色。它支持的纹理与标准材质基本相同,只是不支持 envMap
纹理。
如果使用卡通材质渲染我们熟悉的球体,会得到下面的图像:
终于,我们介绍完了绝大多数常用的材质类型,如您所见,不同类型的材质有不同的适用场景,而材质的灵活度在于其支持的纹理属性,以及其他属性的多少。很多时候,我们需要根据具体情况进行取舍,究竟是要「更好的性能」还是要「更好的渲染效果」?只有能自己能给出答案。
最后,我们用一张图来总结不同材质的纹理支持情况:
由这张图中您应该可以得出以下两点结论:
- 所有材质都有
map
属性; - 需要光照的材质都有较为广泛的纹理支持度,其中,标准材质和物理材质的纹理支持度最高;
4. 总结
在本篇文章中,我向您介绍了创建 Web 3D 物体的最后一个关键概念:「材质(Material)」,罗列了它的通用属性,并展示了不同材质的渲染效果,相信您能感受到它和我们上一篇文章中介绍的「纹理(Texture)」概念有多么紧密的联系。相信您也注意到了,在我们介绍的 9 种材质中,有 5 种都依赖场景中的光源才能被我们「看到」。那么在 Three.js 中,如何添加不同类别的光源呢?这将是我们下一章介绍的主题,敬请期待!
🚨 下一期文章将于 2023.4.3 更新,将在全系列文章总赞数 >= 500 时解锁发布,届时也将支持公众号内付费观看。如果您喜欢本系列文章,并想要尽早看到更新后的文章,请转发给更多朋友观看,点赞,留言。
5. 参考资料
- Three.js 官网;
6. 使用到的工具
- 截屏:Xnip;
- 屏幕录制:QuickTime Player;
- GIF 图片:GIPHY CAPTURE;
7. 💰 支持创作
您有很多方式可以表达您喜欢这篇文章,并愿意支持我持续创作,例如:
- 点击各类平台「喜欢」按钮;
- 将文章转发在各类您喜欢的平台,并为它写一份简短的推荐语;
- 在评论区留言;
- 关注我的个人公众号「前端乱步」;
- …
无论您选择哪一项,我都会因为您的欣赏而感到愉悦。
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net