【Three.js】day108-model-import

作者:mario 发布时间: 2026-09-07 阅读量:8 评论数:0

Day 108 · 模型导入 — GLTF 与 GLB:让真实设备进车间

前六天设备都是"几何拼装"的——能看,但"一眼假"。真实工业项目里,设备来自 3D 建模(Blender/SolidWorks/Revit 导出),前端要做的是导入。今天的主题:3D 行业的通用格式 GLTF/GLBGLTFLoader 怎么用、以及模型里藏着的坑(材质丢失、单位、方向、动画)。做完,你的车间里会有第一台"真实设备模型"——这是从"玩 Three.js"到"做数字孪生"的关键一步。


目录


一、为什么用模型:从拼装到导入

1.1 拼装 vs 模型

维度

几何拼装(Day 102/106)

真实模型(今天起)

来源

代码里 new BoxGeometry

建模软件导出(Blender 等)

质感

简单形状,需贴图补救

自带几何细节 + UV + 材质

成本

便宜,适合原型

更真实,但引入加载与资产管理

定位

兜底方案(无网络时)

正式资产(有网络时)

Day 111 会把两者整合成"降级策略":有模型用模型,没模型(断网/失败)回退拼装。今天先把"导入"打通

1.2 工业 3D 的资产来源

Blender(开源)      → 简单设备/示意模型
SolidWorks / Revit   → 真实工业设备(导出为 GLTF/GLB 常用)
Three.js 官方示例库  → 学习用免费模型(KhronosGroup glTF-Sample-Models)

本周用免费示例模型练手即可(如 glTF-Sample-Models 里的工业设备),不要纠结"自己建模"——那是美术的活,前端工程师要会的是导入、优化、管理


二、GLTF vs GLB:3D 界的"MP3 与 WAV"

格式

是什么

特点

用哪个

GLTF(.gltf)

JSON + 外部资源(.bin / .png 等)

可读、便于调试/版本管理

开发期、要看细节

GLB(.glb)

所有资源打包成一个二进制文件

体积小、加载快、单文件

生产推荐

GLTF = 3D 界的"JPEG"——它是 Khronos 集团制定的开放标准,被 Three.js、Babylon、Unity、Unreal 等全生态支持。选它不选私有格式,是"可移植性"的工程决策

记忆钩子:GLTF 是"3D 行业的通用语言"。你不需要造格式,只要会导入——这与"不引 CDN、用构建管理"是同一类纪律(资产也要可管理可替换)。


三、GLTFLoader 基本用法

# GLTFLoader 在 three/examples 里(不是核心包),已随 three 安装
# import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";

// 1. 创建加载器
const loader = new GLTFLoader();

// 2. 加载(异步):onLoad 拿到解析后的 gltf 对象
loader.load(
  "/assets/models/pump.glb",      // 资源路径(放 /public/assets/models)
  (gltf) => {
    const model = gltf.scene;      // ← 真正的场景对象(Group 的超级版)
    scene.add(model);
  },
  (xhr) => {
    // 3. 进度回调(Day 111 做加载进度条)
    const pct = (xhr.loaded / xhr.total) * 100;
    console.log(`加载中 ${pct.toFixed(0)}%`);
  },
  (err) => {
    console.error("模型加载失败:", err);   // 4. 失败回调(Day 111 降级)
  }
);

// 推荐:loadAsync(配合 await/async,代码更清晰,Day 111 用)
// const gltf = await loader.loadAsync("/assets/models/pump.glb");

加载器的三个回调

回调

时机

用途

onLoad

加载并解析成功

拿到 gltf.scene,加入场景

onProgress

下载过程中反复触发

做进度条(Day 111)

onError

失败

降级处理(Day 111)


四、加载结果的结构:gltf.scene 与内部组织

4.1 GLTF 加载后拿到什么

const gltf = await loader.loadAsync("/assets/models/pump.glb");
// gltf 里主要有:
//   gltf.scene        —— 场景根(一个 Group,挂所有模型内容)
//   gltf.animations   —— 模型自带的关键帧动画(Day 110 用)
//   gltf.cameras      —— 建模软件里留的相机(一般不用)

const model = gltf.scene;
// model 本身是个 Object3D(类似 Group),内部可能是多层嵌套:
//   pump.glb
//   └── pump (Group)
//       ├── body (Mesh)      ← 真正画出来的几何
//       ├── impeller (Mesh)  ← 叶轮
//       └── base (Mesh)

4.2 给模型"上户口"(延续 userData 纪律)

// 把设备 id 挂到模型根上(Day 104 拾取契约的模型版)
model.userData.deviceId = "pump-01";
// 交给引擎的可拾取清单(Day 104 pickables)
pickables.push(model);

记忆钩子:模型导入后就是一个普通的 Object3D——之前学的 position/rotation/scale、Group 层级、userData、拾取,全部照常适用。模型没有改变你的边界纪律,只是替换了"拼装几何"


五、模型自带的坑:材质/单位/方向/动画

模型不是"导出来就能用",四件事必须检查:

5.1 材质丢失/显示异常

现象:模型黑色 / 反光怪异 / 颜色不对
原因:① 模型贴图路径在导出时没嵌入(外部引用失效)
      ② 模型材质用了 Three 不认识的属性
修法:① 用 GLB(内嵌资源,不丢贴图)② 加载后遍历替换材质
// 模型材质兜底:遍历模型,把"没材质的 Mesh"补一个默认材质
model.traverse((child) => {
  if (child instanceof THREE.Mesh && !child.material) {
    child.material = new THREE.MeshStandardMaterial({ color: 0x16233a });
  }
});

5.2 单位不一致(最常见!)

现象:模型"巨大/极小"(一盏灯占满整个车间,或看不见)
原因:建模软件单位(cm/mm)≠ Three 单位(米)
修法:导入后统一 scale(cm→m 除以 100;mm→m 除以 1000)
model.scale.setScalar(0.01);   // 如果模型是 cm 导出的,统一缩到米

5.3 方向不一致

现象:设备"躺着/倒着"进场景
原因:建模坐标系(z 向上)≠ Three(y 向上)
修法:旋转矫正 model.rotation.x = Math.PI / 2,或建模时就导出正确方向

5.4 动画(Day 110 预告)

模型可能自带动画(如叶轮旋转)——存在 gltf.animations 里
Day 110 用 AnimationMixer 播放,今天只要知道"它在哪"

五步检查表(贴到笔记):① 材质有吗 ② 单位对吗 ③ 方向正吗 ④ 有动画吗 ⑤ 名字/层级乱不乱。每个模型进项目都过一遍——这是资产工程的"体检"。


六、资产加载管理:缓存与进度

6.1 用 LoadingManager 统一管理

Three 的 LoadingManager 可以"一次注册,所有加载器共享"进度:

import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";

// 1. 全局加载管理器:统一进度与状态
const manager = new THREE.LoadingManager();
let total = 0, loaded = 0;

manager.onStart = (url) => { total++; };                 // 新资源开始
manager.onLoad = () => { console.log("全部资源加载完成"); };  // 全部完成
manager.onProgress = (url, item, all) => {                // 单资源进度
  loaded = all;                                           // 完成的资源数
  updateProgressBar(loaded / total);                      // 更新 UI(Day 111)
};
manager.onError = (url) => { console.error("资源失败:", url); };

// 2. 让所有加载器共享 manager
const textureLoader = new THREE.TextureLoader(manager);
const gltfLoader = new GLTFLoader(manager);

6.2 缓存:同一资源只加载一次

// 模型缓存:多台相同设备只加载一次(Day 112 的 6 台同型泵)
const modelCache = new Map<string, THREE.Object3D>();

async function getModel(url: string): Promise<THREE.Object3D> {
  if (modelCache.has(url)) {
    return modelCache.get(url)!.clone();   // 已加载:克隆一个副本(不重复下载)
  }
  const gltf = await new GLTFLoader().loadAsync(url);
  modelCache.set(url, gltf.scene);
  return gltf.scene.clone();
}

clone() 是模型复用的关键:同一份模型加载一次,需要多台时克隆——省带宽、省解析、省显存。这是"设备上量"的资产侧答案(配合 Day 106 的实例化,是性能的左右手)。


七、实战:把设备模型放进车间

整合今天内容——按布局配置加载模型,替换 Day 105 的拼装设备:

// src/core/three/load-workshop-models.ts
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { LoadingManager } from "three";
import type { DeviceLayout } from "../config/device-layout";

/**
 * 按布局配置加载车间模型(GLB)
 * 返回:
 *   models     —— 已就位的模型(带 userData.deviceId,可拾取)
 *   onProgress —— 供调用方接进度条(Day 111)
 */
export async function loadWorkshopModels(
  layout: DeviceLayout[],
  manager: LoadingManager
): Promise<THREE.Group[]> {
  const loader = new GLTFLoader(manager);
  const models: THREE.Group[] = [];

  for (const cfg of layout) {
    // 1. 按类型取模型路径(本周用同一台泵模型示范,Day 112 可扩展映射表)
    const url = MODEL_URLS[cfg.type];
    const gltf = await loader.loadAsync(url);

    // 2. 五步体检:单位/方向/材质兜底(第五节)
    const model = gltf.scene;
    model.scale.setScalar(0.01);            // cm → m
    model.traverse((c) => {
      if (c instanceof THREE.Mesh && !c.material) {
        c.material = new THREE.MeshStandardMaterial({ color: 0x16233a });
      }
    });

    // 3. 就位 + 上户口
    model.position.set(cfg.position[0], cfg.position[1], cfg.position[2]);
    model.userData.deviceId = cfg.id;
    models.push(model);
  }
  return models;
}

本周用单台泵模型示范(MODEL_URLS 各类型先指向同一资源);Day 112 若拿到多类型模型,只需扩展这个映射表——类型→URL 的映射是"资产配置",与代码解耦


八、对照表第 6 行:模型加载

环节

手写 WebGL

Three.js

渲染循环

rAF + 手动矩阵 + draw

renderer.render(scene, camera)

光照

手写着色器

new DirectionalLight(...)

几何体

手动顶点数组

new BoxGeometry(...)

几何数据

手动 attribute

构造函数内置

纹理

手动纹理单元 + 采样

TextureLoader + map

模型加载

手写 glTF 解析器(JSON 解析 + 缓冲区绑定 + 材质还原 + 骨架动画)

GLTFLoader.loadAsync() 一行

结论:模型加载是"手写成本最高的单项之一"——一个完整 glTF 解析器是几千行代码(含骨骼、动画、材质)。Three.js 把它封装成一页 API。你要掌握的是:加载流程 + 五步体检 + 缓存克隆,而不是重写解析器。


九、常见坑点

坑 1:模型加载了但看不见

排查:① 加载是异步的——渲染可能先跑了(等加载完成再渲染/加入场景)② 单位太大/太小(缩小到看不见)③ 相机 near/far 没覆盖模型距离。

坑 2:模型全黑

原因:材质缺贴图/模型带不认识材质。修法:GLB(内嵌资源)+ traverse 材质兜底(第五节)。

坑 3:模型巨大/极小

原因:单位不一致。修法:scale.setScalar(0.01)(cm→m)——先查建模单位。

坑 4:模型躺着/倒着

原因:坐标系方向不同。修法:model.rotation.x = Math.PI/2 矫正,或让建模导出正确方向。

坑 5:多台同型设备重复下载

原因:没缓存。修法:modelCache + clone()(第六节)——网络、解析、显存全省。


十、自测挑战

T1 · 导入第一台模型(70 分钟)

下载一个免费 GLB 工业设备(KhronosGroup glTF-Sample-Models 或 Sketchfab),完成第三/四节:导入 → 五步体检 → 就位 → 挂 deviceId → 加入拾取。点击它应能触发事件(复用 Day 104 引擎)。

T2 · 单位/方向实验(30 分钟)

故意把 scale 设为 1(不缩小),观察模型大小;再故意旋转 90°,观察朝向。亲手制造这两个坑,记住"体检"的必要性

T3 · 缓存与克隆(40 分钟)

实现第六节的 getModel + 缓存:加载一次,克隆 6 台摆进车间。用 Network 面板确认只有 1 次模型请求——克隆不重复下载。

T4 · 五步体检表(20 分钟)

给"资产工程"写一份模型体检 checklist(第五节),作为后续所有模型入库的必经流程。写下来才算真正掌握


十一、总结

环节

要点

为什么导入

拼装是兜底,模型才是正式资产

GLTF/GLB

GLTF 开放标准、GLB 生产首选(打包单文件)

GLTFLoader

loadAsync + 进度/失败回调

结构

gltf.scene 是普通 Object3D——旧纪律全部适用

五步体检

材质/单位/方向/动画/命名——每个模型入库必查

缓存克隆

同型设备只加载一次,clone() 复用

对照表

第 6 行:glTF 解析器是"手写成本最高"的单项

车间里终于有了"真实设备"。明天给它加氛围——阴影与描边高亮。


明日预告:Day 109 阴影与描边——DirectionalLight 阴影(castShadow/receiveShadow 两端开关)、EffectComposer 后处理管线、以及 OutlinePass 描边高亮(替换 Day 104 的"换材质"方案)。

评论