Skip to content
认识 glTF:一种为 Web 而生的 3D 格式
glTF(GL Transmission Format)是一种面向 Web 传输与展示的 3D 资产格式,由 Khronos Group 制定。类似于 JPEG 在图像领域的角色,glTF 的设计目标是成为 Web 端通用的 3D 模型格式。目前主流的版本是 glTF 2.0,three.js 的 GLTFLoader 也主要面向该版本。
与 OBJ、FBX 等格式相比,glTF 具备两个显著特征:一是使用 JSON 描述场景结构,搭配二进制数据块(.bin 文件)存储顶点、动画等数据,解析过程无需逐行处理文本格式,加载速度快;二是内置 PBR(Physically Based Rendering)材质模型,导出的材质在不同渲染器中能保持较高的一致性。对于 three.js,glTF 的场景树可以直接映射为 Object3D/Mesh 层级,纹理、动画、骨骼等信息会由 GLTFLoader 一次性完成解析与绑定。
加载第一个 glTF 模型
GLTFLoader 位于 examples/jsm/loaders/GLTFLoader.js。
js
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';实例化时可传入一个 LoadingManager 实例(后文介绍),如果不需要可以直接无参调用。
js
const loader = new GLTFLoader();GLTFLoader 提供两种异步加载方式:回调风格的 load() 和 Promise 风格的 loadAsync()。两种方式均返回相同的解析结果对象——通常命名为 gltf,其中最重要的属性是 gltf.scene,即模型的根节点。
load 方法
load 方法的完整签名为:
load(url: string, onLoad: (gltf: GLTF) => void, onProgress?: (event: ProgressEvent) => void, onError?: (error: ErrorEvent | Error) => void): voidurl:模型文件路径,可以是.gltf或.glb。onLoad:加载并解析成功后的回调,接收gltf对象。onProgress:可选,加载过程中触发,可利用event.loaded / event.total计算进度百分比。onError:可选,加载或解析失败时触发。
js
loader.load(
'./model.glb',
(gltf) => {
scene.add(gltf.scene);
},
(xhr) => {
console.log((xhr.loaded / xhr.total * 100).toFixed(2) + '% loaded');
},
(error) => {
console.error('Load failed:', error);
}
);loadAsync 方法
loadAsync 的签名:
loadAsync(url: string, onProgress?: (event: ProgressEvent) => void): Promise<GLTF>返回一个 Promise,在成功时得到 gltf,失败时可通过 .catch() 或 try/catch 捕获错误。
js
loader.loadAsync('./model.glb', (xhr) => {
console.log((xhr.loaded / xhr.total * 100).toFixed(2) + '% loaded');
}).then((gltf) => {
scene.add(gltf.scene);
}).catch((error) => {
console.error('Load failed:', error);
});注意:
loadAsync内部通过load实现,只是附加了一层 Promise 包装。因此即使不关心进度,loadAsync也支持仅传入一个 URL 参数。
处理加载进度与错误反馈
进度回调中的 xhr.loaded 与 xhr.total 仅在服务器返回 Content-Length 响应头时才有准确值。若缺少该头,xhr.total 可能为 0,此时无法计算百分比。这种情况下更实用的做法是直接展示已下载字节数,或使用不确定的加载指示器。
错误处理方面,onError(或 Promise 的 rejected)可能捕获到网络错误、解析错误或格式不兼容。基本的容错策略是为关键模型准备备选资源。
js
loader.loadAsync('./model.glb', onProgress)
.then((gltf) => scene.add(gltf.scene))
.catch((error) => {
console.warn('Failed to load model, trying fallback...');
return loader.loadAsync('./fallback.glb').then(gltf => scene.add(gltf.scene));
});当同时加载多个模型,各自的进度和错误处理逻辑会分散,这时可以使用 LoadingManager 统一管理。
使用 LoadingManager 管理资源加载流程
LoadingManager 能将多个加载器的进度汇聚到一起,统一触发 onStart、onProgress、onLoad、onError 回调。
js
import { LoadingManager } from 'three';
const manager = new LoadingManager();
manager.onStart = (url, loaded, total) => {
console.log(`Started loading: ${url}, ${loaded}/${total}`);
};
manager.onProgress = (url, loaded, total) => {
console.log(`Loading: ${url}, ${loaded}/${total}`);
};
manager.onLoad = () => {
console.log('All resources loaded.');
};
manager.onError = (url) => {
console.error('Error loading:', url);
};将同一个 manager 实例传给 GLTFLoader 或其他加载器(如 TextureLoader),这些加载器的进度都会归集到 manager 的回调中。
js
const textureLoader = new TextureLoader(manager);
const gltfLoader = new GLTFLoader(manager);
gltfLoader.loadAsync('./scene.glb').then(gltf => scene.add(gltf.scene));
textureLoader.loadAsync('./env.hdr').then(texture => { /* set environment */ });需要留意 onLoad 的触发时机:它在所有已启动的加载项结束后触发,无论成功与否。如果某个资源加载失败,onError 会先触发,其他资源加载继续,最终 onLoad 仍会触发。如果业务需要精准判断“全部成功”,应自行维护计数器。
模型整合与场景适配
加载得到的 gltf.scene 是一个 Group,内部可能包含多层嵌套的 Mesh、SkinnedMesh、Light 等。直接 scene.add(gltf.scene) 可将整个树加入场景。在此之前,通常需要对节点做一些适配工作。
遍历节点、设置变换与阴影
导入的模型尺寸、位置、朝向往往与场景不匹配。可以先通过包围盒计算合适的缩放比例,再统一设置阴影属性。
js
const model = gltf.scene;
// 计算包围盒,根据最大维度缩放至期望尺寸
const box = new THREE.Box3().setFromObject(model);
const size = box.getSize(new THREE.Vector3());
const maxDim = Math.max(size.x, size.y, size.z);
const desiredSize = 2;
const scaleFactor = desiredSize / maxDim;
model.scale.setScalar(scaleFactor);
// 为所有 Mesh 开启阴影投射与接收
model.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
scene.add(model);上述代码通过 Box3 计算整个模型的包围盒,按最大维度等比例缩放,使模型在场景中的视觉大小一致。随后遍历所有子节点,为每个 Mesh 开启阴影投射与接收。castShadow 与 receiveShadow 只有在渲染器和光源已启用阴影时才会生效。
如果模型使用了 MeshBasicMaterial 这类不参与光照计算的材质,则无法显示阴影。遇到这种情况可根据需要替换为标准材质,或接受这种视觉效果。
资源释放与内存管理
当模型不再需要时,仅执行 scene.remove(model) 会移除场景引用,但 GPU 端的几何体、纹理等资源不会自动释放。three.js 中,几何体、材质、纹理都占用显存,必须显式调用 dispose() 回收。
释放一个完整模型的推荐流程是先将其从场景中移除,再递归遍历所有子节点,释放各自的资源。
js
scene.remove(model);
model.traverse((child) => {
if (child.geometry) {
child.geometry.dispose();
}
if (child.material) {
const materials = Array.isArray(child.material) ? child.material : [child.material];
materials.forEach((mat) => {
// 释放材质引用的纹理
for (const key of Object.keys(mat)) {
const value = mat[key];
if (value && value.isTexture) {
value.dispose();
}
}
mat.dispose();
});
}
});需要注意以下几点:
- 如果多个对象共享同一个材质或纹理,调用一次
dispose()将导致其他引用端渲染异常(贴图丢失等)。释放前应确保没有其他模型仍在使用该资源,或采用引用计数等方式管理。 dispose()仅释放 GPU 端内存,JavaScript 对象仍需等待垃圾回收。闭包、事件监听、动画循环中的残留引用都可能阻止回收,因此释放后应主动解除外部引用,例如将变量置为null。- 若模型包含动画(AnimationMixer),应在释放前停止动画并清理 mixer。
扩展:支持 DRACO 压缩的 glTF
glTF 文件中的二进制顶点数据可使用 DRACO 算法进行压缩,显著减小文件体积。加载这类模型需要额外配置 DRACOLoader。
js
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/');
const loader = new GLTFLoader();
loader.setDRACOLoader(dracoLoader);
loader.loadAsync('./compressed.glb').then(gltf => scene.add(gltf.scene));setDecoderPath 需要指向包含 draco_decoder.js 和 draco_wasm_wrapper.js(或 wasm 文件)的目录。官方 CDN 路径中的版本号可能随 three.js 版本变化,可到 three.js 源码的 examples/js/libs/draco/ 目录查看配套版本。也可以将这些解码器文件部署到本地,避免外部服务不可用导致模型加载失败。
setDRACOLoader 只需在 GLTFLoader 实例上调用一次,后续该 loader 加载的所有包含 DRACO 扩展的 glTF 都会自动解码。如果未配置 DRACOLoader 却尝试加载压缩模型,加载过程会抛出错误。
常见问题
路径映射
glTF 文件通常会引用同目录下的 .bin 文件和纹理文件。GLTFLoader 会自动根据模型自身的 URL 拼接这些相对路径。如果在资源路径上使用了构建工具的别名或重定向,需确保相对资源同样可访问。常见现象是模型加载成功但表面呈白色或缺少纹理,检查浏览器 Network 面板即可发现部分纹理请求返回 404。
跨域
纹理和 bin 文件作为独立请求发出。若静态资源与服务页不同源,服务端必须设置 Access-Control-Allow-Origin 响应头。three.js 内部使用的 ImageLoader 会将 <img> 的 crossOrigin 设置为 'anonymous',如果服务器未返回合适的 CORS 头,纹理加载会失败,模型呈现黑色或贴图丢失。本地使用 file:// 协议调试时也容易出现类似问题,建议始终通过本地服务器进行开发。
模型尺度
不同建模工具导出的 glTF 没有统一的世界单位。模型可能实际尺寸极大或极小,加载后几乎不可见或完全遮挡视野。除了利用 Box3 计算包围盒并等比缩放外,也可在导出时约定单位(如 1 单位 = 1 米),并在加载时不做缩放,通过场景尺寸匹配。对于来源多样的模型,统一缩放至合理范围是一个稳妥策略。
