第 8 篇:Three.js 材质系统

第 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 金属非常重要;
  • transparentopacity 为什么需要配合;
  • 双面材质为什么不能滥用;
  • depthTestdepthWrite 有什么区别;
  • 共享材质后修改颜色为什么会影响多个 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 复制代码
颜色
金属度
粗糙度
法线
环境反射

建立相对统一的表达方式。

这比给每个设备手动调整 shininessspecular 更适合大型项目。


21. PBR 不等于自动真实

仅仅写:

javascript 复制代码
new THREE.MeshStandardMaterial()

不会自动得到真实画面。

最终效果还取决于:

  • 模型比例;
  • 法线;
  • UV;
  • 颜色纹理;
  • 粗糙度纹理;
  • 金属度纹理;
  • 法线贴图;
  • 环境贴图;
  • 灯光;
  • 色彩空间;
  • 曝光;
  • 阴影;
  • 后处理。

PBR 是一套更加合理的材质基础,不是"一键真实"。


七、MeshPhysicalMaterial

22. MeshPhysicalMaterial 是什么

MeshPhysicalMaterialMeshStandardMaterial 的扩展。

它保留 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:

  • 支持镜面高光;
  • 使用 specularshininess
  • 比 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;
  • 转炉氧化、锈蚀、积灰和高温纹理;
  • 纹理尺寸与显存;
  • 纹理缓存与释放。

参考资料

相关推荐
烬羽4 小时前
《受控 vs 非受控:你以为用对了 useState,直到你写了那个表单》
javascript·react.js·前端框架
嘟嘟07174 小时前
React 受控组件与非受控组件:表单数据到底归谁管?
前端·javascript·react.js
嘟嘟07174 小时前
React 表单管理进阶:从单字段到带校验的完整登录表单
前端·javascript·react.js
做前端的娜娜子4 小时前
什么是浅拷贝(Shallow Copy)和深拷贝(Deep Copy)?如何手动实现一个深拷贝函数
javascript·面试·掘金·金石计划
xcLeigh4 小时前
编程语言的 AI 友好度排名:Python、JavaScript、TypeScript 谁更适合 AI 辅助
javascript·人工智能·python·ai·typescript·ai编程
宿6745 小时前
vue3-DOM树
前端·javascript·vue.js
谷哥的小弟5 小时前
TypeScript中的any
javascript·typescript
白狐_7985 小时前
408数据结构:中缀转后缀与操作符栈容量——真题精讲
前端·javascript·数据结构
谷哥的小弟5 小时前
TypeScript在Vue3中的常见写法
前端·javascript·typescript
mONESY6 小时前
用 useRef 管理 Worker 线程,让你的页面不再卡顿
javascript