30 3D模型材质与纹理优化

D模型材质与纹理优化

关联:索引

要解决的问题

本讲定位(与上一讲衔接,避免重复)

本讲边界说明(承接上一讲的扩展内容)

章节内容(本讲核心)

环境与先修(默认沿用上一讲工程)

先修要求:

如你需要补装依赖(仅在未安装时执行):

# 安装 Three.js 运行时依赖(包含 TextureLoader、MeshStandardMaterial、GLTFLoader 等能力)
npm i three

解释:

public/
└─ models/
   ├─ robot_arm.glb
   └─ textures/
      ├─ robot-arm-basecolor.jpg
      └─ robot-arm-normal.jpg

解释:


上一讲解决的是“模型文件能不能稳定进入场景”,本讲解决的是“模型进入场景以后,看起来是否像一个可用的工业对象”。

你要记住的工程结论:

  1. 模型加载解决的是“有没有”。
  2. 材质与纹理解决的是“像不像、清不清、稳不稳”。
  3. 优化解决的是“能不能长期跑、多人机上能不能都跑得动”。

和上一讲的衔接关系:

1) 先建立最小认知:材质属于 Mesh,不直接属于 Scene

在 Three.js 里,材质通常挂在 Mesh 上,而不是直接挂在整个场景根节点上。因此你要修改模型材质,第一步通常不是改 gltf.scene 本身,而是遍历它的子对象,找到真正可渲染的 Mesh

最小读取方式:

import * as THREE from 'three';

function inspectModelMaterials(root: THREE.Object3D) {
  // traverse 会递归遍历 root 下面的所有子节点(Mesh/Group/Bone/...)
  root.traverse((obj) => {
    // obj 的真实类型是 THREE.Object3D 的各种子类;这里按 Mesh 的视角访问 geometry/material
    const mesh = obj as THREE.Mesh;
    // isMesh 是 Three.js 的运行时类型标记:只对真正可渲染的网格做材质检查
    if (!mesh.isMesh) return;

    // name 便于你在控制台里定位是哪一个部件(例如“base”“joint_1”“cover”等)
    console.log('mesh name:', mesh.name);
    // material 可能是单个材质,也可能是材质数组(multi-material)
    console.log('material:', mesh.material);
  });
}

解释:

2) 为什么运行时还要“再改一遍材质”

虽然 GLB / GLTF 文件里通常已经自带材质,但在工业项目中,运行时调材质仍然很常见,原因有 3 个:

1) 为什么这一组参数最重要

如果模型使用的是 MeshStandardMaterial 或与之兼容的 PBR 材质,最常调的是:

工业场景经验:

运行时修改示例:

import * as THREE from 'three';

function tuneIndustrialMaterial(root: THREE.Object3D) {
  root.traverse((obj) => {
    const mesh = obj as THREE.Mesh;
    if (!mesh.isMesh) return;

    const material = mesh.material;
    // 如果一个 mesh 绑定多个材质(材质数组),入门阶段先跳过,避免同时处理 groups 与多材质细节
    if (Array.isArray(material)) return;
    // 只对 MeshStandardMaterial 调参:它是 glTF PBR 的常见映射材质,支持 roughness/metalness
    if (!(material instanceof THREE.MeshStandardMaterial)) return;

    // roughness 越大,高光越“散”,更像喷涂外壳/磨砂金属;越小越像镜面
    material.roughness = 0.72;
    // metalness 越大越“金属”,但太高会显得发假(尤其是光照不足的课堂场景)
    material.metalness = 0.28;
    // opacity 是否生效取决于 transparent:opacity < 1 通常都需要开启透明
    material.transparent = true;
    material.opacity = 0.96;
    // 告诉 Three.js 材质的渲染状态发生变化,需要刷新(贴图/透明等变化更需要)
    material.needsUpdate = true;
  });
}

解释:

2) 常见误区

1) 先分清两个最常见贴图

一句话记忆:

map 决定“表面长什么颜色”,normalMap 决定“表面看起来怎么凹凸”。

2) 给已加载模型应用贴图(可直接复制)

import * as THREE from 'three';

function applyTexturesToModel(root: THREE.Object3D) {
  // TextureLoader 负责加载 jpg/png 等贴图;模型本体由 GLTFLoader 负责加载
  const textureLoader = new THREE.TextureLoader();

  // 漫反射贴图(baseColor/albedo):决定“表面颜色长什么样”
  const baseColorMap = textureLoader.load(
    `${import.meta.env.BASE_URL}models/textures/robot-arm-basecolor.jpg`,
  );
  // 法线贴图:决定“表面看起来怎么凹凸”(不显著增加面数)
  const normalMap = textureLoader.load(
    `${import.meta.env.BASE_URL}models/textures/robot-arm-normal.jpg`,
  );

  // 颜色贴图通常是 sRGB 颜色空间;不设置会常见“发灰/偏暗”
  baseColorMap.colorSpace = THREE.SRGBColorSpace;

  root.traverse((obj) => {
    const mesh = obj as THREE.Mesh;
    if (!mesh.isMesh) return;

    const material = mesh.material;
    if (Array.isArray(material)) return;
    if (!(material instanceof THREE.MeshStandardMaterial)) return;

    // 将贴图挂载到材质上:map / normalMap 是 MeshStandardMaterial 的标准字段
    material.map = baseColorMap;
    material.normalMap = normalMap;
    // 通常贴图挂上去以后,还需要配合 roughness/metalness 微调质感
    material.roughness = 0.68;
    material.metalness = 0.22;
    material.needsUpdate = true;
  });
}

解释:

3) 为什么法线贴图很适合工业场景

工业模型里很多细节如果全靠增加真实面数来做,会很快把模型变重;法线贴图的优势是:

只要这个效果会明显拖慢加载和渲染,它就不是一个合格的工程方案。

最常见的三类性能压力:

  1. 纹理太大
  1. 模型面数太高
  1. 材质和贴图种类过多
  1. 先减纹理尺寸
  2. 再统一和压缩贴图
  3. 再处理面数与冗余顶点
  4. 最后才考虑更复杂的高阶压缩链路

为什么这样排:

2) 纹理优化要点

3) 模型简化要点

这一节的目标不是做“专业性能分析平台”,而是让学生形成最基础、最可落地的测试意识。

1) 记录加载耗时

const startAt = performance.now();
const gltf = await loader.loadAsync(`${import.meta.env.BASE_URL}models/robot_arm.glb`);
const durationMs = performance.now() - startAt;

// durationMs 是“这一次 loadAsync() 花了多久”,可作为优化前后的量化对比指标
console.log('load duration:', durationMs.toFixed(0), 'ms');
scene.add(gltf.scene);

解释:

2) 记录帧率的最小思路

let frameCount = 0;
let lastTime = performance.now();

function animate() {
  if (!renderer || !scene || !camera) return;

  // 统计“渲染了多少帧”:每执行一次 animate() 就意味着渲染一次画面
  frameCount += 1;
  const now = performance.now();
  const delta = now - lastTime;

  // 每累计约 1 秒输出一次 FPS,课堂对比足够用
  if (delta >= 1000) {
    const fps = Math.round((frameCount * 1000) / delta);
    console.log('fps:', fps);
    frameCount = 0;
    lastTime = now;
  }

  renderer.render(scene, camera);
  requestAnimationFrame(animate);
}

解释:

3) 建议学生至少记录 4 个维度

补充提醒:

2) Blender 基础操作路径(入门版)

  1. 导入 GLB / GLTF 模型
  2. 进入编辑模式,检查是否存在内部不可见面或明显冗余结构
  3. 执行“按距离合并顶点”(Merge by Distance)一类的去重点处理
  4. 对远景不重要部件做适度简化
  5. 重新导出为 GLB
  6. 回到 Three.js 项目里重新测试加载时间和画面效果

说明:

课后作业

参考与延伸

自检清单(教师备课用)