第 8 篇:Three.js 材质系统
本文是"使用 Three.js 构建转炉数字孪生系统"系列的第 8 篇。
本篇将系统介绍 Three.js 的常用 Mesh 材质,并重点解释为什么转炉数字孪生中的设备模型通常优先使用 PBR 材质。
在上一篇中,我们建立了下面这个核心关系:
text
Geometry + Material = Mesh
其中:
text
Geometry
→ 决定物体的形状
Material
→ 决定物体表面如何被渲染
Mesh
→ 把形状和表面组合成场景中的可见对象
同一个 Geometry 使用不同 Material,可能呈现出完全不同的视觉效果。
例如,同一个转炉炉体可以显示为:
- 不受灯光影响的纯灰色;
- 没有高光的粗糙表面;
- 带有镜面高光的传统金属效果;
- 符合金属度---粗糙度工作流的 PBR 钢材;
- 带清漆层、各向异性或高级反射的物理材质;
- 半透明的设备透视状态;
- 红色报警状态;
- 线框调试状态。
因此,材质不仅决定"好不好看",还会影响:
- 工业设备的真实感;
- 光照系统的设计;
- 纹理资源的组织;
- GPU 性能;
- 透明对象排序;
- 设备状态切换;
- 材质复用和资源释放。
一、本文要解决的问题
完成本文后,应当能够回答:
MeshBasicMaterial为什么不受灯光影响;MeshLambertMaterial适合哪些粗糙表面;MeshPhongMaterial的高光与shininess是什么;MeshStandardMaterial为什么属于 PBR 材质;MeshPhysicalMaterial比 Standard 多了哪些能力;- 金属度和粗糙度应该怎样理解;
- 为什么金属并不是简单地把颜色调成灰色;
- 为什么环境贴图对 PBR 金属非常重要;
transparent和opacity为什么需要配合;- 双面材质为什么不能滥用;
depthTest与depthWrite有什么区别;- 共享材质后修改颜色为什么会影响多个 Mesh;
material.clone()复制了什么;- 转炉壳体、耐火材料、火焰、高温区域和辅助层分别适合什么材质;
- 为什么转炉模型通常应优先选择
MeshStandardMaterial。
二、Material 是什么
1. Material 定义表面外观
Three.js 中,Material 是各种具体材质的抽象基础类。
它定义了很多材质共有的能力,例如:
text
透明度
正反面
深度检测
深度写入
混合
裁剪
雾效
可见性
名称
用户数据
资源释放
克隆
但在实际项目中,通常不会直接创建:
javascript
new THREE.Material()
而是创建具体材质:
javascript
new THREE.MeshBasicMaterial()
new THREE.MeshLambertMaterial()
new THREE.MeshPhongMaterial()
new THREE.MeshStandardMaterial()
new THREE.MeshPhysicalMaterial()
2. 材质不等于颜色
很多初学者容易把 Material 理解为:
text
给模型设置一个颜色
但颜色只是材质的一项属性。
材质可能同时决定:
text
基础颜色
受不受灯光影响
表面高光
金属或非金属
表面粗糙程度
透明度
纹理
法线贴图
自发光
正反面
深度关系
混合方式
例如:
javascript
const material =
new THREE.MeshStandardMaterial({
color: 0x6b7280,
metalness: 0.75,
roughness: 0.6
});
这里不仅定义了灰色,还说明:
text
它更接近金属
表面比较粗糙
反射不会形成特别锐利的镜面
3. 常用材质的复杂度关系
Three.js 官方材质教程将常用标准材质的总体复杂度大致排列为:
text
MeshBasicMaterial
↓
MeshLambertMaterial
↓
MeshPhongMaterial
↓
MeshStandardMaterial
↓
MeshPhysicalMaterial
通常越往后:
- 光照模型更复杂;
- 可控制的表面属性更多;
- 视觉效果更接近真实世界;
- 单像素计算成本更高。
但这并不表示所有模型都应该使用最复杂的材质。
正确原则是:
根据视觉目标和性能预算,选择能够满足需求的最简单材质。
三、MeshBasicMaterial
4. MeshBasicMaterial 是什么
MeshBasicMaterial 是一种不受灯光影响的 Mesh 材质。
创建:
javascript
const material =
new THREE.MeshBasicMaterial({
color: 0xf97316
});
即使场景中没有任何灯光,它仍然能够显示颜色。
5. 为什么它不受灯光影响
MeshBasicMaterial 不使用场景灯光计算表面明暗。
因此,无论:
- 光源在哪里;
- 光源强度是多少;
- 表面朝向哪里;
它的基础颜色通常不会因为光照而产生真实明暗变化。
这使它看起来更像:
text
直接把颜色绘制到表面
而不是模拟真实材料与光线的相互作用。
6. MeshBasicMaterial 的适用场景
它适合:
- 坐标辅助器;
- 设备选中轮廓的辅助 Mesh;
- 纯色状态标记;
- 不需要光照的标签底板;
- 测点标记;
- 调试模型;
- 线框显示;
- 屏幕信息面板;
- 不希望被环境明暗影响的报警提示。
例如:
javascript
const alarmMarkerMaterial =
new THREE.MeshBasicMaterial({
color: 0xef4444
});
7. 不适合直接表现真实转炉金属
如果用 Basic 材质显示转炉壳体:
javascript
const material =
new THREE.MeshBasicMaterial({
color: 0x6b7280
});
模型可以被看见,但通常缺少:
- 受光面与背光面的变化;
- 金属反射;
- 表面粗糙感;
- 高光;
- 环境光照关系;
- 工业设备的体积感。
因此,它适合调试,不适合作为转炉主体最终材质。
8. 线框模式
javascript
const material =
new THREE.MeshBasicMaterial({
color: 0x22d3ee,
wireframe: true
});
适合检查:
- 模型三角面密度;
- 拓扑结构;
- 几何体细分;
- 设备占位体;
- 模型是否存在异常长三角形。
线框模式显示的是三角形边,不是 CAD 工程线。
四、MeshLambertMaterial
9. MeshLambertMaterial 是什么
MeshLambertMaterial 使用非物理的 Lambert 反射模型。
它适合表现:
text
没有明显镜面高光的粗糙表面
例如:
- 未上漆木材;
- 普通石材;
- 混凝土;
- 粗糙耐火材料;
- 对真实金属反射要求不高的远景模型。
创建:
javascript
const material =
new THREE.MeshLambertMaterial({
color: 0x8b8175
});
10. Lambert 材质需要灯光
与 Basic 材质不同,Lambert 材质受灯光影响。
如果场景中没有灯光:
javascript
const material =
new THREE.MeshLambertMaterial({
color: 0xffffff
});
模型可能显示得很暗,甚至近似黑色。
需要加入灯光:
javascript
const ambientLight =
new THREE.AmbientLight(
0xffffff,
1
);
scene.add(ambientLight);
const directionalLight =
new THREE.DirectionalLight(
0xffffff,
2
);
directionalLight.position.set(
5,
10,
8
);
scene.add(directionalLight);
11. Lambert 材质没有镜面高光
Lambert 材质可以表现漫反射明暗,但不能表现明显的镜面高光。
因此,它适合:
text
粗糙、无光泽、非金属或远景表面
不适合表现:
- 抛光金属;
- 油漆钢板;
- 光亮炉壳;
- 湿润表面;
- 需要明显高光的设备。
12. 在数字孪生中的应用
可以考虑用于:
- 混凝土地面;
- 墙体;
- 粗糙耐火材料占位体;
- 次要建筑结构;
- 很远的低成本场景物体;
- 低性能模式下的简化材质。
但如果项目已经采用 PBR 资产流程,通常更倾向统一使用 MeshStandardMaterial,通过低金属度和高粗糙度来表现这些非金属表面。
五、MeshPhongMaterial
13. MeshPhongMaterial 是什么
MeshPhongMaterial 使用非物理的 Blinn-Phong 反射模型。
与 Lambert 材质相比,它能够表现镜面高光。
创建:
javascript
const material =
new THREE.MeshPhongMaterial({
color: 0x6b7280,
specular: 0xffffff,
shininess: 60
});
14. color、specular 和 shininess
color
表示基础漫反射颜色:
javascript
color: 0x6b7280
specular
表示镜面高光颜色:
javascript
specular: 0xffffff
shininess
控制高光的集中程度:
javascript
shininess: 60
一般来说:
text
较低 shininess
→ 高光范围更宽、更柔和
较高 shininess
→ 高光范围更小、更集中
15. Phong 材质的优势
相对于 PBR 材质,它具有以下特点:
- 能表现高光;
- 参数容易理解;
- 传统项目中使用广泛;
- 通常比 Standard 和 Physical 材质轻;
- 在不追求物理一致性的场景中效果够用。
16. Phong 材质的局限
MeshPhongMaterial 的参数属于经验模型。
例如:
text
shininess
specular
并不直接对应现实材料制作流程中的:
text
金属度
粗糙度
因此,同一材质在不同光照环境中可能需要人工重新调整,才能保持"看起来合适"。
它适合:
- 传统高光效果;
- 简化工业设备;
- 性能预算较低的场景;
- 不使用完整 PBR 纹理工作流的项目。
但对转炉这类以钢铁、耐材、油漆和氧化表面为主的设备,PBR 通常更容易形成统一、可维护的材质体系。
六、MeshStandardMaterial
17. MeshStandardMaterial 是什么
MeshStandardMaterial 是 Three.js 中常用的标准 PBR 材质。
它采用:
text
Metallic-Roughness
金属度---粗糙度工作流
创建:
javascript
const material =
new THREE.MeshStandardMaterial({
color: 0x6b7280,
metalness: 0.75,
roughness: 0.6
});
18. 什么是 PBR
PBR 是:
text
Physically Based Rendering
基于物理的渲染
其目标不是简单地"做得更亮"或"高光更强",而是用更接近现实光学规律的方式描述材料。
传统经验材质往往需要针对某个灯光环境进行调节。
PBR 更强调:
- 材质参数具有相对稳定的物理含义;
- 同一材质在不同光照环境下保持合理反应;
- 资产可在不同 PBR 软件和引擎之间传递;
- 纹理制作流程更加统一。
19. MeshStandardMaterial 的核心参数
最核心的三个参数是:
text
color
metalness
roughness
例如:
javascript
const steelMaterial =
new THREE.MeshStandardMaterial({
color: 0x707780,
metalness: 0.8,
roughness: 0.55
});
20. 为什么它适合工业设备
工业数字孪生中常见表面包括:
- 钢板;
- 铸钢;
- 氧化金属;
- 油漆金属;
- 不锈钢;
- 粗糙耐材;
- 混凝土;
- 塑料;
- 橡胶。
这些材料可以通过:
text
颜色
金属度
粗糙度
法线
环境反射
建立相对统一的表达方式。
这比给每个设备手动调整 shininess 和 specular 更适合大型项目。
21. PBR 不等于自动真实
仅仅写:
javascript
new THREE.MeshStandardMaterial()
不会自动得到真实画面。
最终效果还取决于:
- 模型比例;
- 法线;
- UV;
- 颜色纹理;
- 粗糙度纹理;
- 金属度纹理;
- 法线贴图;
- 环境贴图;
- 灯光;
- 色彩空间;
- 曝光;
- 阴影;
- 后处理。
PBR 是一套更加合理的材质基础,不是"一键真实"。
七、MeshPhysicalMaterial
22. MeshPhysicalMaterial 是什么
MeshPhysicalMaterial 是 MeshStandardMaterial 的扩展。
它保留 Standard 材质的金属度---粗糙度体系,同时增加高级物理属性,例如:
- Clearcoat;
- Transmission;
- IOR;
- Thickness;
- Iridescence;
- Anisotropy;
- Sheen;
- Advanced reflectivity。
创建:
javascript
const material =
new THREE.MeshPhysicalMaterial({
color: 0x6b7280,
metalness: 0.7,
roughness: 0.45,
clearcoat: 0.35,
clearcoatRoughness: 0.25
});
23. Clearcoat
Clearcoat 表示材料表面额外覆盖一层透明反射层。
适合:
- 汽车漆;
- 涂层金属;
- 碳纤维;
- 湿润表面;
- 清漆表面。
例如:
javascript
const paintedMetal =
new THREE.MeshPhysicalMaterial({
color: 0x334155,
metalness: 0.45,
roughness: 0.55,
clearcoat: 0.5,
clearcoatRoughness: 0.3
});
24. Transmission
Transmission 用于表现更物理化的透光表面,例如玻璃。
javascript
const glassMaterial =
new THREE.MeshPhysicalMaterial({
color: 0xffffff,
transmission: 1,
roughness: 0.08,
metalness: 0,
thickness: 0.02,
ior: 1.5
});
这和简单设置:
javascript
transparent: true
opacity: 0.3
不是同一种效果。
Transmission 更适合:
- 玻璃观察窗;
- 透明防护罩;
- 某些透明仪表部件。
25. Anisotropy
各向异性可以表现:
- 拉丝金属;
- 被加工的金属表面;
- 某些具有方向性高光的材料。
对于精细的拉丝不锈钢设备,它可能有价值。
但如果普通转炉壳体只需要粗糙钢材,Standard 材质通常已经足够。
26. Physical 材质的成本
MeshPhysicalMaterial 增加的高级着色功能会提高每像素计算成本。
因此,不应因为"Physical 更高级"就给整个车间全部使用。
推荐原则:
text
普通金属和非金属
→ MeshStandardMaterial
确实需要高级物理特征的少数表面
→ MeshPhysicalMaterial
例如:
text
转炉炉壳
→ Standard
普通钢结构平台
→ Standard
玻璃观察窗
→ Physical
清漆控制柜面板
→ 可评估 Physical
透明防护罩
→ Physical
普通远景厂房
→ 不一定需要 Physical
八、金属度 metalness
27. metalness 的含义
metalness 表示表面在 PBR 模型中有多接近金属。
范围通常是:
text
0 到 1
javascript
material.metalness = 0;
表示非金属。
javascript
material.metalness = 1;
表示金属。
28. 常见理解
非金属
例如:
- 石材;
- 混凝土;
- 木材;
- 塑料;
- 橡胶;
- 耐火砖。
通常使用:
javascript
metalness: 0
金属
例如:
- 钢;
- 铁;
- 铝;
- 铜;
- 不锈钢。
通常接近:
javascript
metalness: 1
29. 为什么中间值要谨慎
现实中的一个均匀表面通常要么是金属,要么是非金属。
但工业表面可能存在:
- 锈蚀;
- 灰尘;
- 氧化层;
- 油漆脱落;
- 金属与非金属覆盖层混合。
这时可以通过:
metalnessMap;- 分离材质区域;
- 合理中间值;
表达混合状态。
例如简化的氧化钢材:
javascript
const oxidizedSteel =
new THREE.MeshStandardMaterial({
color: 0x6f5a4b,
metalness: 0.55,
roughness: 0.82
});
但更真实的方案是使用纹理,让不同区域拥有不同金属度。
30. 金属度不是"亮度"
错误理解:
text
metalness 越大,模型越亮
实际上金属度改变的是材料的反射模型。
一个:
text
高金属度
高粗糙度
暗环境
的表面仍然可能显得很暗。
金属需要反射周围环境。
如果环境中没有有意义的亮度变化,金属也不会自然出现丰富反射。
九、粗糙度 roughness
31. roughness 的含义
roughness 表示表面微观结构的粗糙程度。
范围通常为:
text
0 到 1
javascript
roughness: 0
表示非常光滑,反射更加清晰集中。
javascript
roughness: 1
表示非常粗糙,反射更加模糊分散。
32. 粗糙度对工业表面的影响
光滑抛光金属
javascript
metalness: 1
roughness: 0.15
表现出集中清晰的反射。
普通钢板
javascript
metalness: 0.8
roughness: 0.5
有金属反射,但不完全镜面。
氧化炉壳
javascript
metalness: 0.55
roughness: 0.8
反射更弱、更分散。
耐火材料
javascript
metalness: 0
roughness: 0.9
接近粗糙非金属。
33. 粗糙度不是 shininess 的简单反向值
可以粗略理解:
text
roughness 高
→ 表面更哑光
shininess 高
→ Phong 高光更集中
但两者属于不同光照模型,不能认为:
text
roughness = 1 - shininess
Standard 材质和 Phong 材质使用的是不同参数体系。
十、为什么 PBR 金属需要环境贴图
34. 金属主要反射环境
金属表面的视觉信息很大程度来自反射。
如果场景只有一个简单方向光,却没有环境贴图,PBR 金属可能出现:
- 大片发黑;
- 缺少环境层次;
- 看起来像深色塑料;
- 反射方向单一;
- 材质参数很难判断。
Three.js 官方文档建议,为 MeshStandardMaterial 提供环境贴图以获得更好的效果。
35. envMap
可以把环境纹理设置为场景环境:
javascript
scene.environment =
environmentTexture;
这样场景中的 PBR 材质可以使用它进行环境光照和反射。
材质还可以调节环境反射强度:
javascript
material.envMapIntensity =
1.2;
36. 背景和环境可以不同
javascript
scene.background =
backgroundTexture;
scene.environment =
environmentTexture;
background 决定用户看到的背景。
environment 主要用于材质环境光照和反射。
两者可以使用同一纹理,也可以不同。
37. 转炉项目中的环境设计
真实炼钢车间环境通常具有:
- 大面积暗色结构;
- 顶部厂房光源;
- 炉口高亮;
- 局部警示灯;
- 热辐射区域;
- 金属设备反射。
可以使用:
- 合理的环境贴图;
- 方向光;
- 区域光或局部灯光;
- 自发光纹理;
- 后处理;
共同塑造工业空间。
不要通过把所有金属材质调得极亮来替代环境设计。
十一、为什么转炉通常优先使用 PBR 材质
38. 转炉表面具有明确的材料属性
转炉系统中可能存在:
text
钢制炉壳
氧化层
耐火材料
油漆结构件
不锈钢管道
积灰区域
高温炉口
玻璃观察窗
这些表面都可以使用统一的 PBR 参数体系描述。
例如:
text
钢材
→ 高金属度、中高粗糙度
耐材
→ 零金属度、高粗糙度
油漆钢结构
→ 表层通常更接近非金属涂层,并保留适当粗糙度
玻璃
→ Physical 的 transmission
高温区域
→ Standard/Physical + emissive
39. PBR 与 GLTF 工作流一致
现代 GLTF 资产通常采用 PBR 材质工作流。
常见纹理包括:
text
Base Color
Metallic
Roughness
Normal
Occlusion
Emissive
Three.js 的 MeshStandardMaterial 与 GLTF 的金属度---粗糙度工作流具有良好对应关系。
因此,从 Blender 或其他建模工具导出的转炉模型,更容易在 Three.js 中保持接近原制作环境的表面逻辑。
40. 参数具有更稳定的业务语义
使用 Phong 材质时,可能需要讨论:
text
这个设备的 shininess 应该是 25 还是 70?
specular 应该多亮?
使用 Standard 材质时,可以围绕现实表面讨论:
text
这是金属还是非金属?
表面是抛光、普通还是严重氧化?
是否存在粗糙度纹理?
这对团队协作更容易形成统一标准。
41. 不同光照条件下更一致
数字孪生系统可能提供:
- 全厂视角;
- 转炉局部视角;
- 日间模式;
- 暗色控制室模式;
- 炉口高亮状态;
- 报警状态;
- 检修状态。
PBR 材质在合理光照和环境贴图下,通常比经验高光材质更容易在不同环境中保持一致。
42. MeshStandardMaterial 通常是默认起点
对于普通转炉设备,推荐优先从:
javascript
const converterMaterial =
new THREE.MeshStandardMaterial({
color: 0x66707a,
metalness: 0.75,
roughness: 0.62
});
开始。
然后根据实际资产加入:
map;metalnessMap;roughnessMap;normalMap;aoMap;emissiveMap;envMapIntensity。
只有确实需要高级能力时,再升级为:
javascript
MeshPhysicalMaterial
十二、透明度
43. opacity
Material 通常具有:
javascript
opacity
其范围一般为:
text
0 到 1
text
0
→ 完全透明
1
→ 完全不透明
例如:
javascript
material.opacity = 0.35;
44. transparent
只设置 opacity 通常还不够。
需要启用:
javascript
material.transparent = true;
完整写法:
javascript
const material =
new THREE.MeshStandardMaterial({
color: 0x38bdf8,
transparent: true,
opacity: 0.35
});
45. 半透明设备透视
数字孪生中可能需要把炉壳暂时设为半透明,以观察内部结构。
不推荐直接修改共享材质:
javascript
converterMaterial.transparent =
true;
converterMaterial.opacity =
0.25;
如果多个设备共享该材质,它们都会变透明。
更合理:
javascript
const transparentMaterial =
converterMaterial.clone();
transparentMaterial.transparent =
true;
transparentMaterial.opacity =
0.25;
然后只赋给目标对象。
46. 透明对象的问题
透明不是一个简单功能。
透明 Mesh 可能出现:
- 前后表面排序错误;
- 两个透明对象互相穿插;
- 内部结构显示混乱;
- 某些角度突然消失;
- 深度写入遮挡后方对象;
- 双面透明表面排序复杂;
- 性能开销增加。
因此,透明状态应作为特殊模式使用,而不是让整个厂房长期半透明。
47. alphaTest
对于铁丝网、树叶、带孔贴图等"有或无"的透明区域,可以使用:
javascript
alphaTest: 0.5
低于阈值的片元直接丢弃。
它与连续半透明不同,通常不需要复杂的透明混合排序。
适合:
- 栅栏贴图;
- 警示网;
- 带孔标牌;
- 镂空纹理。
48. alphaHash
当前 Three.js 的通用 Material 还提供 alphaHash,可作为普通透明或 alpha test 的另一种选择。
它通过随机阈值近似透明,可能产生颗粒噪声,但能减少部分排序问题。
这是更进阶的透明方案,初学阶段先掌握:
text
transparent
opacity
alphaTest
即可。
十三、双面材质
49. side
材质可以通过:
javascript
material.side
决定绘制哪一面。
可选:
javascript
THREE.FrontSide
THREE.BackSide
THREE.DoubleSide
默认通常是:
javascript
THREE.FrontSide
50. FrontSide
javascript
side: THREE.FrontSide
只绘制三角面的正面。
适合:
- 封闭转炉炉壳;
- 钢包;
- 管道;
- 厂房实体结构;
- 大多数正常模型。
它可以避免绘制封闭模型内部不可见的背面。
51. BackSide
javascript
side: THREE.BackSide
只绘制背面。
可用于:
- 从内部观察的球形天空;
- 某些内表面;
- 特殊视觉效果。
例如:
javascript
const roomMaterial =
new THREE.MeshBasicMaterial({
color: 0x111827,
side: THREE.BackSide
});
52. DoubleSide
javascript
side: THREE.DoubleSide
绘制两面。
适合:
- PlaneGeometry;
- 薄片;
- 数据面板;
- 没有厚度的警示区域;
- 确实需要从两侧观察的表面。
53. 为什么不能给全部模型设置 DoubleSide
无条件双面可能:
- 增加片元处理;
- 掩盖模型法线翻转;
- 掩盖破面;
- 让透明排序更复杂;
- 使封闭模型内部表面也参与渲染;
- 降低问题排查效率。
如果转炉部分表面从外侧看不到,应先检查:
- 顶点绕序;
- 法线方向;
- 导出设置;
- 模型是否破面。
不要首先用 DoubleSide 掩盖问题。
十四、深度检测
54. 什么是深度缓冲
渲染三维场景时,GPU 需要判断:
text
哪个像素更靠近相机
哪个像素应该被遮挡
深度缓冲保存每个像素位置已经绘制的深度信息。
材质中的两个重要属性是:
text
depthTest
depthWrite
55. depthTest
javascript
material.depthTest = true;
表示绘制当前材质时,需要与深度缓冲中的值进行比较。
默认通常为:
text
true
因此,被前方设备挡住的后方设备不会错误显示在前面。
关闭:
javascript
material.depthTest = false;
会让材质忽略正常遮挡测试。
适合少量特殊辅助对象,例如:
- 始终显示的调试坐标轴;
- 某些屏幕提示;
- 特殊高亮覆盖层。
56. depthWrite
javascript
material.depthWrite = true;
表示当前材质绘制后,是否把自己的深度写入深度缓冲。
普通不透明物体通常保持:
text
true
透明对象有时会设置:
javascript
material.depthWrite = false;
避免透明表面先写入深度后,把后面的透明或不透明对象完全挡住。
57. depthTest 与 depthWrite 的区别
可以简化理解为:
text
depthTest
→ 我是否读取并尊重已有遮挡关系?
depthWrite
→ 我是否把自己的深度写入遮挡记录?
四种组合具有不同含义。
普通不透明设备
javascript
depthTest: true
depthWrite: true
常见半透明辅助层
javascript
depthTest: true
depthWrite: false
它仍然被前方物体遮挡,但不会强力阻止后续对象绘制。
始终显示的调试对象
javascript
depthTest: false
depthWrite: false
可能始终覆盖显示。
这种设置不能滥用,否则三维空间关系会失真。
58. 透明炉壳示例
javascript
const xrayMaterial =
new THREE.MeshStandardMaterial({
color: 0x38bdf8,
metalness: 0.15,
roughness: 0.45,
transparent: true,
opacity: 0.22,
depthTest: true,
depthWrite: false,
side: THREE.DoubleSide
});
这适合临时透视模式。
但真实效果还需要根据:
- 内部模型;
- 绘制顺序;
- 双面透明;
- renderOrder;
- 后处理;
继续调整。
十五、材质克隆
59. 为什么需要 clone()
多个 Mesh 可以共享同一个 Material:
javascript
const steelMaterial =
new THREE.MeshStandardMaterial({
color: 0x64748b,
metalness: 0.75,
roughness: 0.6
});
meshA.material =
steelMaterial;
meshB.material =
steelMaterial;
如果修改:
javascript
meshA.material.color.set(
0xff0000
);
meshB 也会变红,因为它们引用的是同一个材质对象。
60. 使用 clone() 创建独立材质
javascript
meshA.material =
steelMaterial.clone();
meshA.material.color.set(
0xff0000
);
此后,meshA 使用独立材质,修改不会直接影响 meshB。
61. clone() 不等于复制全部纹理数据
材质克隆会创建新的 Material 对象并复制材质属性。
但材质引用的纹理通常仍然是共享资源。
例如:
javascript
const clonedMaterial =
originalMaterial.clone();
console.log(
clonedMaterial.map ===
originalMaterial.map
);
通常可能为:
text
true
这是合理的,因为纹理往往体积较大,复用可以减少 GPU 内存。
如果确实需要独立纹理对象:
javascript
clonedMaterial.map =
originalMaterial.map.clone();
clonedMaterial.map.needsUpdate =
true;
但不要无理由克隆纹理像素数据。
62. 材质状态切换策略
转炉可能有:
text
正常
选中
报警
离线
透明透视
检修
不推荐每次切换都创建新材质。
可以提前建立材质集合:
javascript
const converterMaterials = {
normal:
new THREE.MeshStandardMaterial({
color: 0x64748b,
metalness: 0.75,
roughness: 0.6
}),
selected:
new THREE.MeshStandardMaterial({
color: 0x38bdf8,
metalness: 0.55,
roughness: 0.45,
emissive: 0x082f49,
emissiveIntensity: 0.7
}),
alarm:
new THREE.MeshStandardMaterial({
color: 0xb91c1c,
metalness: 0.45,
roughness: 0.55,
emissive: 0x7f1d1d,
emissiveIntensity: 1.2
})
};
切换:
javascript
converterMesh.material =
converterMaterials.alarm;
但真实模型可能具有多种原始材质,不能简单把全部 Mesh 替换成一种材质。后续需要建立原材质备份和按节点状态覆盖机制。
十六、修改材质后是否需要 needsUpdate
63. 普通数值属性
修改:
javascript
material.color.set(0xff0000);
material.roughness = 0.8;
material.metalness = 0.5;
material.opacity = 0.4;
通常不需要手动:
javascript
material.needsUpdate = true;
Three.js 会在已有着色器参数中更新对应 uniform。
64. 改变着色器结构的属性
某些修改会改变着色器程序结构,例如:
- 从没有贴图变成有贴图;
- 切换某些功能开关;
- 改变部分材质定义;
- 更换某些需要重新编译的配置。
这时可能需要:
javascript
material.needsUpdate = true;
但不要每一帧设置 needsUpdate,因为它可能触发着色器重新编译和性能抖动。
十七、材质复用与性能
65. 复用材质的价值
多个对象共享相同材质可以:
- 减少 Material 对象;
- 减少着色器变体;
- 便于统一风格;
- 便于集中释放;
- 降低资源管理复杂度。
例如,所有普通钢结构使用:
javascript
const structuralSteelMaterial =
new THREE.MeshStandardMaterial({
color: 0x59636f,
metalness: 0.7,
roughness: 0.68
});
66. 材质越多,渲染状态切换越多
一个模型被拆成大量不同材质区域,可能增加:
- Draw Call;
- 着色器程序切换;
- 纹理切换;
- 材质管理成本;
- 加载体积。
因此,工业模型优化时应避免:
text
每个小零件一个独立材质
每个螺栓一种材质
颜色略有差异就创建新材质
可以通过:
- 材质合并;
- 纹理图集;
- 共享 PBR 材质;
- 顶点颜色;
- 实例化;
减少材质数量。
67. 不同质量模式
可以根据设备能力提供材质质量级别。
高质量模式
text
MeshStandardMaterial
环境贴图
法线贴图
粗糙度贴图
阴影
适当后处理
中质量模式
text
MeshStandardMaterial
减少贴图分辨率
降低环境强度复杂度
简化阴影
低质量模式
text
MeshLambertMaterial
或简化 Standard
关闭部分阴影和高成本特效
不要只通过把全部材质换成 Basic 来解决性能问题,因为这样会严重破坏场景空间感。
十八、转炉系统材质建议
68. 转炉钢制外壳
推荐起点:
javascript
const converterShellMaterial =
new THREE.MeshStandardMaterial({
color: 0x666b70,
metalness: 0.72,
roughness: 0.66
});
特点:
text
金属
长期高温和氧化
表面通常并不镜面
粗糙度较高
不要使用:
javascript
metalness: 1,
roughness: 0
否则容易像抛光镜面金属,与真实转炉不符。
69. 氧化和积灰表面
javascript
const oxidizedMaterial =
new THREE.MeshStandardMaterial({
color: 0x6f5847,
metalness: 0.45,
roughness: 0.88
});
更真实的方式是使用:
- Base Color Map;
- Roughness Map;
- Metalness Map;
- Normal Map;
- AO Map。
这样不同区域可以表现不同程度的氧化和积灰。
70. 耐火材料
javascript
const refractoryMaterial =
new THREE.MeshStandardMaterial({
color: 0x7c5f4a,
metalness: 0,
roughness: 0.95
});
耐材通常属于粗糙非金属。
需要避免把它因为"颜色偏深"就设置成高金属度。
71. 氧枪
javascript
const lanceMaterial =
new THREE.MeshStandardMaterial({
color: 0x8b949e,
metalness: 0.85,
roughness: 0.38
});
氧枪表面可能比炉壳更规则、更光滑,但仍应结合实际设备状态调节。
72. 钢结构平台
javascript
const platformMaterial =
new THREE.MeshStandardMaterial({
color: 0x4b5563,
metalness: 0.55,
roughness: 0.72
});
如果表面有非金属油漆覆盖,可降低金属度。
需要记住:
油漆本身通常是非金属表层,即使下面是钢材。
73. 高温炉口
可以使用自发光:
javascript
const hotMaterial =
new THREE.MeshStandardMaterial({
color: 0x7c2d12,
metalness: 0.15,
roughness: 0.72,
emissive: 0xff3d00,
emissiveIntensity: 2
});
自发光表示材质自身发出可见颜色,但它不一定会自动照亮周围物体。
要让周围环境产生光照,还需要:
- 实际 Light;
- 烘焙光照;
- 后处理 Bloom;
- 更高级的全局光照方案。
74. 透明观察区域
javascript
const inspectionMaterial =
new THREE.MeshPhysicalMaterial({
color: 0xbfe9ff,
metalness: 0,
roughness: 0.12,
transmission: 0.85,
thickness: 0.03,
ior: 1.5
});
只用于确实需要的玻璃或透明结构。
十九、完整材质对比示例
75. 示例目标
下面的示例将在同一场景中创建五个相同球体,分别使用:
MeshBasicMaterial;MeshLambertMaterial;MeshPhongMaterial;MeshStandardMaterial;MeshPhysicalMaterial。
这样可以直观看到不同材质对光照的反应。
76. main.js
javascript
import * as THREE from "three";
import {
OrbitControls
} from "three/addons/controls/OrbitControls.js";
import "./style.css";
const app =
document.querySelector("#app");
if (!app) {
throw new Error(
"未找到 #app 容器"
);
}
// 场景
const scene =
new THREE.Scene();
scene.background =
new THREE.Color(0x111827);
// 相机
const camera =
new THREE.PerspectiveCamera(
55,
window.innerWidth /
window.innerHeight,
0.1,
200
);
camera.position.set(
0,
7,
18
);
// 渲染器
const renderer =
new THREE.WebGLRenderer({
antialias: true
});
renderer.setPixelRatio(
Math.min(
window.devicePixelRatio,
2
)
);
renderer.setSize(
window.innerWidth,
window.innerHeight
);
app.appendChild(
renderer.domElement
);
// 控制器
const controls =
new OrbitControls(
camera,
renderer.domElement
);
controls.target.set(
0,
1,
0
);
controls.enableDamping =
true;
// 灯光
const ambientLight =
new THREE.AmbientLight(
0xffffff,
0.8
);
scene.add(
ambientLight
);
const directionalLight =
new THREE.DirectionalLight(
0xffffff,
3
);
directionalLight.position.set(
5,
10,
8
);
scene.add(
directionalLight
);
const pointLight =
new THREE.PointLight(
0xffb36b,
20,
30
);
pointLight.position.set(
-5,
5,
4
);
scene.add(
pointLight
);
// 地面
const ground =
new THREE.Mesh(
new THREE.PlaneGeometry(
30,
20
),
new THREE.MeshStandardMaterial({
color: 0x252b35,
metalness: 0,
roughness: 0.95
})
);
ground.rotation.x =
-Math.PI / 2;
scene.add(
ground
);
// 共享球体 Geometry
const geometry =
new THREE.SphereGeometry(
1.35,
48,
24
);
const materials = [
new THREE.MeshBasicMaterial({
color: 0x64748b
}),
new THREE.MeshLambertMaterial({
color: 0x64748b
}),
new THREE.MeshPhongMaterial({
color: 0x64748b,
specular: 0xffffff,
shininess: 70
}),
new THREE.MeshStandardMaterial({
color: 0x64748b,
metalness: 0.75,
roughness: 0.55
}),
new THREE.MeshPhysicalMaterial({
color: 0x64748b,
metalness: 0.7,
roughness: 0.42,
clearcoat: 0.55,
clearcoatRoughness: 0.22
})
];
const names = [
"Basic",
"Lambert",
"Phong",
"Standard",
"Physical"
];
const meshes = [];
materials.forEach(
(material, index) => {
const mesh =
new THREE.Mesh(
geometry,
material
);
mesh.position.set(
(index - 2) * 3.2,
1.5,
0
);
mesh.name =
names[index];
scene.add(mesh);
meshes.push(mesh);
}
);
// 透明辅助球
const xrayMaterial =
materials[3].clone();
xrayMaterial.color.set(
0x38bdf8
);
xrayMaterial.transparent =
true;
xrayMaterial.opacity =
0.25;
xrayMaterial.depthWrite =
false;
const xrayMesh =
new THREE.Mesh(
new THREE.SphereGeometry(
2,
48,
24
),
xrayMaterial
);
xrayMesh.position.set(
0,
1.5,
-4
);
scene.add(
xrayMesh
);
const clock =
new THREE.Clock();
function animate() {
const elapsed =
clock.getElapsedTime();
for (
let index = 0;
index < meshes.length;
index += 1
) {
meshes[index].rotation.y =
elapsed * 0.25;
}
controls.update();
renderer.render(
scene,
camera
);
}
renderer.setAnimationLoop(
animate
);
function handleResize() {
const width =
window.innerWidth;
const height =
window.innerHeight;
camera.aspect =
width / height;
camera.updateProjectionMatrix();
renderer.setSize(
width,
height
);
}
window.addEventListener(
"resize",
handleResize
);
需要注意:
没有环境贴图时,Standard 和 Physical 金属的优势可能无法完全体现。后续纹理与环境贴图文章会继续完善这个示例。
二十、材质管理器示例
77. 为什么需要统一管理
大型数字孪生中可能有:
- 数十个设备;
- 数百个 Mesh;
- 多种运行状态;
- 多套质量等级;
- 共享纹理;
- 共享 PBR 材质。
如果每个组件自行创建材质,会造成:
- 视觉风格不一致;
- 大量重复 Material;
- 资源难以释放;
- 状态切换逻辑分散;
- Shader 变体过多。
78. 创建材质库
javascript
export function createMaterialLibrary() {
const materials = {
converterShell:
new THREE.MeshStandardMaterial({
color: 0x666b70,
metalness: 0.72,
roughness: 0.66
}),
refractory:
new THREE.MeshStandardMaterial({
color: 0x7c5f4a,
metalness: 0,
roughness: 0.95
}),
oxygenLance:
new THREE.MeshStandardMaterial({
color: 0x8b949e,
metalness: 0.85,
roughness: 0.38
}),
alarm:
new THREE.MeshBasicMaterial({
color: 0xef4444
}),
xray:
new THREE.MeshStandardMaterial({
color: 0x38bdf8,
metalness: 0.1,
roughness: 0.45,
transparent: true,
opacity: 0.22,
depthWrite: false,
side: THREE.DoubleSide
})
};
return materials;
}
79. 统一释放
javascript
export function disposeMaterialLibrary(
materials
) {
const disposedTextures =
new Set();
for (
const material of
Object.values(materials)
) {
for (
const value of
Object.values(material)
) {
if (
value?.isTexture &&
!disposedTextures.has(value)
) {
value.dispose();
disposedTextures.add(value);
}
}
material.dispose();
}
}
实际项目中还需要区分:
- 外部缓存纹理;
- 全局环境贴图;
- 多个材质共享的纹理;
- 组件私有纹理。
不能只按属性遍历后无条件释放全部共享资源。
二十一、常见错误
80. 使用 Standard 材质却没有灯光和环境
结果:
- 模型发黑;
- 金属缺少层次;
- 调整颜色也不自然。
处理:
- 加入合理灯光;
- 加入环境贴图;
- 检查曝光;
- 检查法线;
- 检查色彩空间。
81. 把所有金属度都设为 1
真实设备表面可能存在:
- 油漆;
- 氧化;
- 灰尘;
- 耐材;
- 锈蚀;
- 非金属覆盖层。
不能因为设备主体是钢铁,就让每个表面都:
javascript
metalness = 1;
82. 粗糙度全部设为 0
这会让设备像镜面或塑料玩具。
转炉、钢包和厂房结构通常具有较高粗糙度。
83. 透明却没有 transparent
错误:
javascript
material.opacity =
0.3;
正确:
javascript
material.transparent =
true;
material.opacity =
0.3;
84. 所有透明对象都关闭 depthTest
javascript
material.depthTest =
false;
会让对象忽略正常遮挡,破坏三维关系。
多数半透明对象应保持:
javascript
depthTest = true;
仅根据具体排序问题考虑 depthWrite。
85. 所有材质都使用 DoubleSide
这会:
- 掩盖模型问题;
- 增加渲染成本;
- 加剧透明排序问题。
正常封闭模型应使用:
javascript
THREE.FrontSide
86. 修改共享材质导致全场变色
需要确认:
javascript
meshA.material ===
meshB.material
需要独立修改时先:
javascript
meshA.material =
meshA.material.clone();
87. 每次报警都 clone 新材质
如果报警频繁切换,持续 clone 会产生大量材质对象。
应预先准备状态材质,或保存原材质并复用有限的状态覆盖材质。
88. 过度使用 MeshPhysicalMaterial
Physical 的高级功能只有在:
- 材料确实需要;
- 光照和环境足够;
- 性能预算允许;
时才有价值。
普通钢结构使用 Standard 通常已经足够。
二十二、材质选型表
| 使用对象 | 推荐材质 | 主要原因 |
|---|---|---|
| 调试占位体 | MeshBasicMaterial | 不依赖灯光,问题容易定位 |
| 坐标标记和状态点 | MeshBasicMaterial | 颜色稳定、成本较低 |
| 粗糙远景物体 | MeshLambertMaterial 或简化 Standard | 不需要高光 |
| 传统高光物体 | MeshPhongMaterial | 具有镜面高光,性能低于 PBR |
| 转炉炉壳 | MeshStandardMaterial | 适合金属度---粗糙度工作流 |
| 氧枪和金属管道 | MeshStandardMaterial | 金属反射与粗糙度可控 |
| 耐火材料 | MeshStandardMaterial | 零金属度、高粗糙度 |
| 玻璃观察窗 | MeshPhysicalMaterial | 支持 transmission 和 IOR |
| 清漆或涂层表面 | MeshPhysicalMaterial | 支持 clearcoat |
| 透明透视辅助层 | Standard/Basic + transparent | 特殊状态显示 |
| 高温发光区域 | Standard/Physical + emissive | 保留 PBR 表面并增加自发光 |
二十三、本篇必须掌握的代码
89. Basic 材质
javascript
const material =
new THREE.MeshBasicMaterial({
color: 0xff6600
});
90. Lambert 材质
javascript
const material =
new THREE.MeshLambertMaterial({
color: 0x8b8175
});
91. Phong 材质
javascript
const material =
new THREE.MeshPhongMaterial({
color: 0x64748b,
specular: 0xffffff,
shininess: 60
});
92. Standard 材质
javascript
const material =
new THREE.MeshStandardMaterial({
color: 0x64748b,
metalness: 0.75,
roughness: 0.6
});
93. Physical 材质
javascript
const material =
new THREE.MeshPhysicalMaterial({
color: 0x64748b,
metalness: 0.7,
roughness: 0.45,
clearcoat: 0.4,
clearcoatRoughness: 0.25
});
94. 透明材质
javascript
material.transparent =
true;
material.opacity =
0.3;
material.depthWrite =
false;
95. 双面材质
javascript
material.side =
THREE.DoubleSide;
只在确实需要从两面观察时使用。
96. 克隆材质
javascript
const clonedMaterial =
originalMaterial.clone();
97. 释放材质
javascript
material.dispose();
只有确认材质不再被任何 Mesh 使用时才能释放。
二十四、本文总结
Three.js 常用 Mesh 材质可以分成三类。
不依赖灯光
text
MeshBasicMaterial
适合:
- 调试;
- 状态标记;
- 辅助对象;
- 不需要真实光照的界面元素。
传统经验光照模型
text
MeshLambertMaterial
MeshPhongMaterial
Lambert:
- 适合粗糙无高光表面;
- 成本相对较低。
Phong:
- 支持镜面高光;
- 使用
specular和shininess; - 比 PBR 更轻,但物理一致性较弱。
PBR 材质
text
MeshStandardMaterial
MeshPhysicalMaterial
Standard:
- 使用金属度---粗糙度工作流;
- 适合绝大多数工业设备;
- 与现代 GLTF 资产流程一致;
- 通常应作为转炉材质的默认起点。
Physical:
- 在 Standard 基础上增加 Clearcoat、Transmission、IOR、Anisotropy 等高级能力;
- 适合玻璃、涂层、拉丝金属等特殊表面;
- 性能成本更高。
转炉为什么优先使用 PBR
因为转炉系统主要由:
text
钢材
氧化金属
油漆结构
耐火材料
高温区域
玻璃和透明部件
组成。
PBR 可以用统一的:
text
颜色
金属度
粗糙度
法线
环境反射
自发光
表达这些材料,并与 GLTF 模型制作流程保持一致。
透明和深度
透明材质不仅需要:
text
transparent
opacity
还可能需要考虑:
text
depthTest
depthWrite
side
renderOrder
透明对象应作为特殊视觉模式谨慎使用。
材质克隆
共享材质有利于性能和统一管理,但修改共享材质会影响所有引用者。
需要单独修改时:
javascript
const material =
sharedMaterial.clone();
最终可以用一句话概括:
Basic 材质适合不受光照影响的辅助对象,Lambert 和 Phong 适合传统简化光照,Standard 是工业设备 PBR 表面的首选,Physical 则用于确实需要高级物理效果的少数材质。
二十五、下一篇学习内容
下一篇建议继续介绍:
第 9 篇:纹理、PBR 贴图与环境贴图
主要内容可以包括:
TextureLoader;- Base Color Map;
- Normal Map;
- Roughness Map;
- Metalness Map;
- AO Map;
- Emissive Map;
- Alpha Map;
- UV;
- 纹理重复和包裹;
- 色彩空间;
- HDR 环境贴图;
- PMREM;
- 转炉氧化、锈蚀、积灰和高温纹理;
- 纹理尺寸与显存;
- 纹理缓存与释放。