Day 108 · 模型导入 — GLTF 与 GLB:让真实设备进车间
前六天设备都是"几何拼装"的——能看,但"一眼假"。真实工业项目里,设备来自 3D 建模(Blender/SolidWorks/Revit 导出),前端要做的是导入。今天的主题:3D 行业的通用格式 GLTF/GLB、
GLTFLoader怎么用、以及模型里藏着的坑(材质丢失、单位、方向、动画)。做完,你的车间里会有第一台"真实设备模型"——这是从"玩 Three.js"到"做数字孪生"的关键一步。
目录
一、为什么用模型:从拼装到导入
1.1 拼装 vs 模型
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 = 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");
加载器的三个回调
四、加载结果的结构: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 行:模型加载
结论:模型加载是"手写成本最高的单项之一"——一个完整 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(第五节),作为后续所有模型入库的必经流程。写下来才算真正掌握。
十一、总结
车间里终于有了"真实设备"。明天给它加氛围——阴影与描边高亮。
明日预告:Day 109 阴影与描边——DirectionalLight 阴影(castShadow/receiveShadow 两端开关)、EffectComposer 后处理管线、以及 OutlinePass 描边高亮(替换 Day 104 的"换材质"方案)。