GLTF 导入与导出
本文深入探讨 GLTFLoader 和 GLTFExporter 类,它们实现了 glTF 2.0 格式与 Three.js 场景表示之间的双向转换。GLTFLoader 将 .gltf(JSON)和 .glb(二进制)文件解析为 Three.js 对象,而 GLTFExporter 则将 Three.js 场景转换回 glTF 格式。
有关其他模型加载器的信息,请参阅 附加格式加载器。有关通过 JSON 进行场景序列化的信息,请参阅 场景序列化。
概述
glTF 导入/导出系统对 Khronos glTF 2.0 规范 提供全面支持,这是一种免版税格式,针对 3D 内容的高效传输和加载进行了优化。GLTFLoader 和 GLTFExporter 均利用可扩展的插件架构来支持核心规范及 17+ 官方 glTF 扩展。
关键能力:
- 通过
GLTFParser解析 glTF JSON(.gltf)和二进制(.glb)格式 - 使用
GLTFWriter导出 Three.js 场景为 glTF/GLB,保留场景层次结构 - 使用
register()回调的基于插件的扩展系统 - 通过
DRACOLoader和MeshoptDecoder支持压缩网格 - 通过
KTX2Loader(Basis Universal)支持压缩纹理 - 完整的 PBR 材质工作流映射到
MeshStandardMaterial/MeshPhysicalMaterial - 动画支持:骨骼(
Skeleton)、变形目标、属性关键帧(AnimationClip) - 完整场景图:
Object3D节点、Camera、Light对象、层次结构
支持的 glTF 扩展:
| 扩展 | 加载器 | 导出器 | 用途 |
|---|---|---|---|
| KHR_draco_mesh_compression | ✓ | — | 几何体压缩 |
| KHR_materials_clearcoat | ✓ | ✓ | 清漆层 |
| KHR_materials_dispersion | ✓ | ✓ | 色散 |
| KHR_materials_emissive_strength | ✓ | ✓ | HDR 自发光 |
| KHR_materials_ior | ✓ | ✓ | 折射率 |
| KHR_materials_iridescence | ✓ | ✓ | 薄膜干涉 |
| KHR_materials_sheen | ✓ | ✓ | 织物外观 |
| KHR_materials_specular | ✓ | ✓ | 镜面工作流 |
| KHR_materials_transmission | ✓ | ✓ | 玻璃/透明 |
| KHR_materials_unlit | ✓ | ✓ | 无光照材质 |
| KHR_materials_volume | ✓ | ✓ | 体积材质 |
| KHR_materials_anisotropy | ✓ | ✓ | 各向异性反射 |
| KHR_lights_punctual | ✓ | ✓ | 点/聚光/方向光 |
| KHR_mesh_quantization | ✓ | ✓ | 量化顶点属性 |
| KHR_texture_basisu | ✓ | — | Basis Universal 纹理 |
| KHR_texture_transform | ✓ | ✓ | 纹理变换 |
| EXT_materials_bump | ✓ | ✓ | 凹凸贴图 |
| EXT_texture_webp | ✓ | — | WebP 纹理 |
| EXT_texture_avif | ✓ | — | AVIF 纹理 |
| EXT_meshopt_compression | ✓ | — | Meshopt 缓冲区压缩 |
| EXT_mesh_gpu_instancing | ✓ | ✓ | GPU 实例化 |
性能考量
加载器优化
- 缓存:
GLTFRegistry缓存已解析的对象以避免重复处理 examples/jsm/loaders/GLTFLoader.js565-597 - ImageBitmapLoader: 默认在支持的平台上使用,以加快图像解码 examples/jsm/loaders/GLTFLoader.js80-82
- 异步解析: 所有繁重操作返回 Promise,避免主线程阻塞
- 扩展懒加载: 扩展仅在需要时处理数据
导出器优化
- 资源去重:
GLTFWriter.cache防止共享几何体、材质和纹理的重复处理 examples/jsm/exporters/GLTFExporter.js612-619 - 缓冲区合并: 所有二进制数据合并为单一缓冲区以减少 HTTP 请求
- 纹理压缩: 支持导出可用时的压缩纹理
- 几何体共享: 引用同一几何体的多个 Mesh 仅导出一次几何体
最佳实践
加载最佳实践
1. 为生产资源使用压缩
始终为生产环境配置压缩加载器。这可以减少 90%+ 的文件大小 examples/misc_exporter_gltf.html486-492:
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/examples/jsm/libs/draco/');
const ktx2Loader = new KTX2Loader()
.setTranscoderPath('jsm/libs/basis/')
.detectSupport(renderer);
loader.setDRACOLoader(dracoLoader);
loader.setKTX2Loader(ktx2Loader);
loader.setMeshoptDecoder(MeshoptDecoder);2. 清理资源
切换场景或模型时释放加载器资源 examples/jsm/loaders/GLTFLoader.js80-82:
// ImageBitmap 资源需要显式清理
gltf.scene.traverse((node) => {
if (node.isMesh) {
node.geometry.dispose();
if (node.material.map?.image instanceof ImageBitmap) {
node.material.map.image.close();
}
node.material.dispose();
}
});3. 使用 LoadingManager 跟踪进度
跨多个资源跟踪加载进度:
const manager = new THREE.LoadingManager();
manager.onProgress = (url, loaded, total) => {
console.log(`加载中: ${(loaded/total * 100)}%`);
};
const loader = new GLTFLoader(manager);4. 优雅处理错误
始终提供错误处理器 examples/jsm/loaders/GLTFLoader.js253-327:
loader.load('model.gltf', onLoad, onProgress, (error) => {
console.error('加载失败:', error);
// 回退到占位模型
scene.add(createPlaceholder());
});5. 优先使用 Async/Await 获得更清晰代码
使用 loadAsync() 实现现代基于 Promise 的工作流:
try {
const gltf = await loader.loadAsync('model.gltf');
scene.add(gltf.scene);
} catch (error) {
console.error('加载错误:', error);
}导出最佳实践
1. 为生产使用二进制格式
二进制 .glb 文件比带有独立资源的 .gltf 更高效 examples/misc_exporter_gltf.html34-69:
const options = { binary: true };
const glb = await exporter.parseAsync(scene, options);2. 为有动画的内容启用 TRS
导出动画时,使用 trs: true 以保持动画兼容性 examples/jsm/exporters/GLTFExporter.js656-660:
const options = {
binary: true,
trs: true, // 动画必需
animations: mixer.clipAction.getClip()
};3. 优化纹理尺寸
限制纹理尺寸为合理值 examples/misc_exporter_gltf.html34-69:
const options = {
binary: true,
maxTextureSize: 2048 // 防止导出 4K+ 纹理
};4. 设置纹理解压缩
导出带有压缩纹理的场景时需要 examples/jsm/exporters/GLTFExporter.js1048-1058:
import * as WebGLTextureUtils from 'three/addons/utils/WebGLTextureUtils.js';
exporter.setTextureUtils(WebGLTextureUtils);5. 使用 MeshStandardMaterial 获得最佳结果
MeshStandardMaterial 和 MeshPhysicalMaterial 可干净映射到 glTF PBR 工作流 examples/jsm/exporters/GLTFExporter.js1571-1586:
// 良好 - 可干净导出
const material = new THREE.MeshStandardMaterial({
color: 0xff0000,
metalness: 0.5,
roughness: 0.7
});
// 避免 - ShaderMaterial 不受支持
const shaderMat = new THREE.ShaderMaterial({...}); // 将被跳过6. 确保 UV 通道一致性
金属度和粗糙度贴图必须使用相同的 UV 通道 examples/jsm/exporters/GLTFExporter.js1035-1038:
// 两个贴图必须使用相同 UV 通道
material.metalnessMap.channel = 0;
material.roughnessMap.channel = 0; // 必须匹配7. 仅导出需要的内容
使用 onlyVisible 跳过隐藏的调试对象 examples/misc_exporter_gltf.html34-69:
const options = {
onlyVisible: true, // 跳过 object.visible === false
binary: true
};材质工作流最佳实践
1. 分离金属度和粗糙度贴图
导出器会自动合并它们,但为了保持灵活性请分开源文件 examples/jsm/exporters/GLTFExporter.js939-1045:
material.metalnessMap = metalnessTexture; // 蓝色通道
material.roughnessMap = roughnessTexture; // 绿色通道
// 导出器合并为单个纹理2. 对数据纹理使用线性色彩空间
非色彩数据(法线、金属度、粗糙度)应使用 NoColorSpace:
normalMap.colorSpace = THREE.NoColorSpace;
metalnessMap.colorSpace = THREE.NoColorSpace;
roughnessMap.colorSpace = THREE.NoColorSpace;3. 验证法线归一化
确保法线为单位长度以避免导出警告 examples/jsm/exporters/GLTFExporter.js1808-1815:
geometry.computeVertexNormals();
// 导出器会在需要时归一化,但预先归一化更好局限性与约束
系统架构
GLTFLoader 和 GLTFExporter 架构
GLTFLoader
核心 API
GLTFLoader 类提供加载 glTF 资源的主要接口:
| 方法 | 参数 | 返回类型 | 用途 |
|---|---|---|---|
load() | url, onLoad, onProgress, onError | void | 从 URL 异步加载 |
parse() | data, path, onLoad, onError | void | 解析原始 glTF 数据 |
parseAsync() | data, path | Promise | parse() 的异步版本 |
setDRACOLoader() | dracoLoader | GLTFLoader | 配置 Draco 解码器 |
setKTX2Loader() | ktx2Loader | GLTFLoader | 配置 KTX2 解码器 |
setMeshoptDecoder() | meshoptDecoder | GLTFLoader | 配置 Meshopt 解码器 |
register() | callback | GLTFLoader | 注册扩展插件 |
unregister() | callback | GLTFLoader | 注销扩展插件 |
加载结果对象(LoadObject):
{
scene: Group, // 主场景层次结构
scenes: Array<Group>, // 文件中所有场景
cameras: Array<Camera>, // 所有相机
animations: Array<AnimationClip>, // 所有动画
asset: Object, // glTF 资产元数据
parser: GLTFParser, // 解析器实例
userData: Object // 自定义数据
}加载管线
GLTFLoader 加载管线
GLTFParser 中的关键方法:
| 方法 | 用途 | 返回值 |
|---|---|---|
parse(onLoad, onError) | 主解析入口 | void |
getDependency(type, index) | 按类型/索引加载资源 | Promise<any> |
loadBuffer(bufferIndex) | 加载二进制缓冲区 | Promise<ArrayBuffer> |
loadBufferView(bufferViewIndex) | 加载缓冲区视图,可选解压缩 | Promise<ArrayBuffer> |
loadAccessor(accessorIndex) | 从访问器创建 BufferAttribute | Promise<BufferAttribute> |
loadTexture(textureIndex) | 从纹理定义创建 Texture | Promise<Texture> |
loadImage(imageIndex) | 从 URI 或 bufferView 加载图像 | Promise<Image> |
assignTexture(materialParams, mapName, mapDef) | 将纹理分配给材质参数 | Promise<Texture> |
loadMaterial(materialIndex) | 从材质定义创建 Material | Promise<Material> |
loadGeometry(primitiveIndex) | 从图元创建 BufferGeometry | Promise<BufferGeometry> |
loadMesh(meshIndex) | 从网格定义创建 Mesh 或 Group | Promise<Group> |
loadCamera(cameraIndex) | 从相机定义创建 Camera | Promise<Camera> |
loadNode(nodeIndex) | 从节点定义创建 Object3D | Promise<Object3D> |
loadScene(sceneIndex) | 从场景定义构建完整场景 | Promise<Group> |
扩展系统
GLTFLoader 使用插件架构,每个 glTF 扩展实现为单独的类别。扩展在加载器构造期间通过 register() 注册,并由 GLTFParser 在解析管线的特定点调用。
扩展注册流程:
GLTFParser 扩展方法:
GLTFParser 为扩展提供这些挂钩点:
| 方法 | 调用时机 | 返回类型 | 用途 |
|---|---|---|---|
getMaterialType(materialIndex) | 创建材质前 | Material 类 | 覆盖默认材质类型(例如返回 MeshPhysicalMaterial) |
extendMaterialParams(materialIndex, materialParams) | 材质创建期间 | Promise | 向 materialParams 添加扩展属性 |
createNodeMesh(nodeIndex) | 节点解析期间 | Promise<Mesh> | 创建自定义 Mesh(例如 InstancedMesh) |
createNodeAttachment(nodeIndex) | 节点解析期间 | Promise<Object3D> | 附加额外对象(例如 Light) |
loadTexture(textureIndex) | 纹理加载期间 | Promise<Texture> | 自定义纹理加载(例如 KTX2) |
loadBufferView(index) | 缓冲区加载期间 | Promise<ArrayBuffer> | 自定义缓冲区解压缩(例如 Draco、Meshopt) |
getDependency(type, index) | 依赖解析 | Promise<any> | 提供自定义依赖 |
_markDefs() | 解析开始前 | void | 标记引用的定义以进行依赖跟踪 |
内置扩展类:
| 扩展 | 类名 | 关键方法 |
|---|---|---|
| KHR_draco_mesh_compression | GLTFDracoMeshCompressionExtension | decodePrimitive() |
| KHR_lights_punctual | GLTFLightsExtension | _loadLight(), createNodeAttachment(), _markDefs() |
| KHR_materials_clearcoat | GLTFMaterialsClearcoatExtension | getMaterialType(), extendMaterialParams() |
| KHR_materials_transmission | GLTFMaterialsTransmissionExtension | getMaterialType(), extendMaterialParams() |
| KHR_materials_unlit | GLTFMaterialsUnlitExtension | getMaterialType(), extendParams() |
| KHR_texture_basisu | GLTFTextureBasisUExtension | loadTexture() |
| EXT_meshopt_compression | GLTFMeshoptCompression | loadBufferView() |
| EXT_mesh_gpu_instancing | GLTFMeshGpuInstancing | createNodeMesh() |
扩展构造模式:
扩展接收 GLTFParser 实例并声明其名称 examples/jsm/loaders/GLTFLoader.js643-653:
class GLTFLightsExtension {
constructor(parser) {
this.parser = parser;
this.name = EXTENSIONS.KHR_LIGHTS_PUNCTUAL;
this.cache = { refs: {}, uses: {} };
}
// ... 扩展方法
}扩展示例:KHR_lights_punctual
GLTFLightsExtension 演示了添加自定义场景对象的扩展模式:
GLTFLightsExtension 方法流程
实现细节:
依赖跟踪:
_markDefs()扫描json.nodes[]查找光源引用 examples/jsm/loaders/GLTFLoader.js655-673:_markDefs() { const nodeDefs = this.parser.json.nodes || []; for (let nodeIndex = 0; nodeIndex < nodeDefs.length; nodeIndex++) { const nodeDef = nodeDefs[nodeIndex]; if (nodeDef.extensions?.[this.name]?.light !== undefined) { this.parser._addNodeRef(this.cache, nodeDef.extensions[this.name].light); } } }光源创建:
_loadLight()创建适当的Light子类 examples/jsm/loaders/GLTFLoader.js676-741:_loadLight(lightIndex) { const lightDef = extensions.lights[lightIndex]; switch (lightDef.type) { case 'directional': lightNode = new DirectionalLight(color); break; case 'point': lightNode = new PointLight(color); lightNode.distance = range; break; case 'spot': lightNode = new SpotLight(color); lightNode.angle = lightDef.spot.outerConeAngle; lightNode.penumbra = 1.0 - lightDef.spot.innerConeAngle / outerConeAngle; break; } }节点附加:
createNodeAttachment()返回节点附加的 Promise examples/jsm/loaders/GLTFLoader.js753-770:createNodeAttachment(nodeIndex) { const lightIndex = nodeDef.extensions[this.name].light; return this._loadLight(lightIndex).then((light) => { return this.parser._getNodeRef(this.cache, lightIndex, light); }); }
材质扩展模式
材质扩展遵循扩展 PBR 属性的通用模式:
材质扩展执行流程
GLTFMaterialsClearcoatExtension 示例:
此扩展向 MeshPhysicalMaterial 添加清漆属性 examples/jsm/loaders/GLTFLoader.js877-954:
class GLTFMaterialsClearcoatExtension {
constructor(parser) {
this.parser = parser;
this.name = EXTENSIONS.KHR_MATERIALS_CLEARCOAT;
}
getMaterialType(materialIndex) {
const materialDef = this.parser.json.materials[materialIndex];
if (!materialDef.extensions?.[this.name]) return null;
return MeshPhysicalMaterial;
}
extendMaterialParams(materialIndex, materialParams) {
const materialDef = this.parser.json.materials[materialIndex];
const extension = materialDef.extensions?.[this.name];
if (!extension) return Promise.resolve();
const pending = [];
// 标量属性
if (extension.clearcoatFactor !== undefined) {
materialParams.clearcoat = extension.clearcoatFactor;
}
if (extension.clearcoatRoughnessFactor !== undefined) {
materialParams.clearcoatRoughness = extension.clearcoatRoughnessFactor;
}
// 纹理加载
if (extension.clearcoatTexture !== undefined) {
pending.push(this.parser.assignTexture(
materialParams, 'clearcoatMap', extension.clearcoatTexture
));
}
if (extension.clearcoatRoughnessTexture !== undefined) {
pending.push(this.parser.assignTexture(
materialParams, 'clearcoatRoughnessMap', extension.clearcoatRoughnessTexture
));
}
if (extension.clearcoatNormalTexture !== undefined) {
pending.push(this.parser.assignTexture(
materialParams, 'clearcoatNormalMap', extension.clearcoatNormalTexture
));
}
return Promise.all(pending);
}
}常见材质扩展属性:
| 扩展 | 材质类型 | 设置的属性 | 加载的纹理 |
|---|---|---|---|
| KHR_materials_clearcoat | MeshPhysicalMaterial | clearcoat, clearcoatRoughness | clearcoatMap, clearcoatRoughnessMap, clearcoatNormalMap |
| KHR_materials_transmission | MeshPhysicalMaterial | transmission | transmissionMap |
| KHR_materials_volume | MeshPhysicalMaterial | thickness, attenuationDistance, attenuationColor | thicknessMap |
| KHR_materials_ior | MeshPhysicalMaterial | ior | — |
| KHR_materials_sheen | MeshPhysicalMaterial | sheenColor, sheenRoughness, sheen | sheenColorMap, sheenRoughnessMap |
| KHR_materials_unlit | MeshBasicMaterial | color, opacity | map |
压缩资源支持
GLTFLoader 通过外部解码器库支持多种压缩格式:
Draco 网格压缩:
用法 examples/misc_exporter_gltf.html486-492:
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/examples/jsm/libs/draco/');
loader.setDRACOLoader(dracoLoader);KTX2 纹理压缩:
const ktx2Loader = new KTX2Loader()
.setTranscoderPath('jsm/libs/basis/')
.detectSupport(renderer);
loader.setKTX2Loader(ktx2Loader);Meshopt 缓冲区压缩:
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
loader.setMeshoptDecoder(MeshoptDecoder);GLTFExporter
核心 API
GLTFExporter 类将 Three.js 场景转换为 glTF 格式:
| 方法 | 参数 | 返回类型 | 用途 |
|---|---|---|---|
parse() | input, onDone, onError, options | void | 导出为 glTF |
parseAsync() | input, options | Promise | 异步版本 |
register() | callback | GLTFExporter | 注册扩展插件 |
unregister() | callback | GLTFExporter | 注销扩展插件 |
setTextureUtils() | utils | GLTFExporter | 配置纹理解压缩 |
导出选项:
{
binary: false, // 输出 GLB 与 GLTF
trs: false, // 使用平移/旋转/缩放而非矩阵
onlyVisible: true, // 跳过隐藏对象
maxTextureSize: Infinity, // 调整纹理尺寸
animations: [], // 要导出的 AnimationClip
includeCustomExtensions: false // 导出 userData.gltfExtensions
}支持的输入类型:
Scene- 单一场景Object3D- 任何场景图节点Array<Scene|Object3D>- 多个场景/对象
输出格式:
- 二进制模式(
binary: true):返回ArrayBuffer(.glb 文件) - JSON 模式(
binary: false):返回嵌入 base64 缓冲区的 glTF JSON 对象
导出管线
GLTFExporter 导出管线
导出管线中的关键 GLTFWriter 方法:
| 方法 | 调用者 | 用途 | 输出 |
|---|---|---|---|
writeAsync(input, onDone, options) | GLTFExporter.parse() | 主导出编排 | 用结果调用 onDone() |
processInputAsync(input) | writeAsync() | 遍历输入场景/对象 | 填充 json 结构 |
processNode(object) | processInputAsync() | 将 Object3D 转换为 glTF 节点 | json.nodes[] 条目 |
processMeshAsync(mesh) | processNode() | 将 Mesh 转换为 glTF 网格 | json.meshes[] 条目 |
processMaterialAsync(material) | processMeshAsync() | 将 Material 转换为 glTF 材质 | json.materials[] 条目 |
processTextureAsync(texture) | processMaterialAsync() | 将 Texture 转换为 glTF 纹理 | json.textures[] 条目 |
processImage(image, format, flipY) | processTextureAsync() | 将图像编码为 PNG/JPEG | json.images[] 条目 |
processSampler(map) | processTextureAsync() | 转换纹理参数 | json.samplers[] 条目 |
processAccessor(attribute, geometry) | processMeshAsync() | 将 BufferAttribute 转换为访问器 | json.accessors[] 条目 |
processBufferView(attribute, componentType) | processAccessor() | 从属性数据创建 bufferView | json.bufferViews[] 条目 |
processBuffer(buffer) | processBufferView() | 将二进制数据添加到合并列表 | 追加到 buffers[] |
GLTFWriter 内部结构
GLTFWriter 编排导出过程,维护所有 glTF 资源的状态:
GLTFWriter 状态结构:
class GLTFWriter {
constructor() {
this.plugins = []; // 扩展插件实例
this.options = {}; // parseAsync() 中的导出选项
this.pending = []; // 异步操作(Promise[])
this.buffers = []; // 二进制数据块(ArrayBuffer[])
this.byteOffset = 0; // 当前缓冲区写入位置
this.nodeMap = new Map(); // Object3D -> 节点索引映射
this.skins = []; // 蒙皮定义
this.extensionsUsed = {}; // 导出中使用的扩展
this.extensionsRequired = {}; // 必需的扩展
this.uids = new Map(); // BufferAttribute -> UID 映射
this.uid = 0; // UID 计数器
this.cache = { // 资源去重
meshes: new Map(), // mesh geometry+material 键 -> 索引
attributes: new Map(), // BufferAttribute UID -> 访问器索引
attributesNormalized: new Map(), // 归一化属性缓存
materials: new Map(), // Material -> 材质索引
textures: new Map(), // Texture -> 纹理索引
images: new Map() // Image -> { mimeType:flipY -> 图像索引 }
};
this.json = { // 输出 glTF JSON 结构
asset: {
version: '2.0',
generator: 'THREE.GLTFExporter r' + REVISION
}
};
this.textureUtils = null; // WebGLTextureUtils 或 WebGPUTextureUtils
}
}核心处理方法:
| 方法 | 输入 | 输出 | 用途 |
|---|---|---|---|
processBuffer(buffer) | ArrayBuffer | 0(缓冲区索引) | 将缓冲区添加到 buffers[] 以进行合并 |
processBufferView(attribute, componentType, start, count, target) | BufferAttribute, 组件类型, 范围, 目标 | {id, byteLength} | 在 json.bufferViews[] 中创建条目 |
processBufferViewImage(blob) | Blob | Promise<number> | 为图像数据创建 bufferView |
processAccessor(attribute, geometry, start, count) | BufferAttribute, BufferGeometry, 范围 | number(访问器索引) | 在 json.accessors[] 中创建条目并含最小/最大值 |
processImage(image, format, flipY, mimeType) | Image, 格式, 翻转标志, MIME 类型 | number(图像索引) | 将图像编码为 PNG/JPEG,添加到 json.images[] |
processSampler(map) | Texture | number(采样器索引) | 将滤镜/环绕转换为 glTF 常量 |
processTextureAsync(map) | Texture | Promise<number> | 处理纹理,调用 processImage() 和 processSampler() |
processMaterialAsync(material) | Material | Promise<number> | 转换为 PBR 材质定义 |
processMeshAsync(mesh) | Mesh | Promise<number> | 将几何体和材质转换为 glTF 网格 |
getUID(attribute, isRelativeCopy) | BufferAttribute, 标志 | number | 返回属性去重的唯一 ID |
serializeUserData(object, objectDef) | object, definition | void | 将 userData 添加到 objectDef.extras |
applyTextureTransform(mapDef, texture) | texture definition, Texture | void | 如需要则添加 KHR_texture_transform |
缓冲区数据布局
GLTFExporter 使用 glTF 数据对齐要求,其中 bufferViews 必须对齐到 4 字节边界:
GLTFWriter 缓冲区和访问器布局
填充函数: examples/jsm/exporters/GLTFExporter.js491-530
function getPaddedBufferSize(bufferSize) {
return Math.ceil(bufferSize / 4) * 4;
}
function getPaddedArrayBuffer(arrayBuffer, paddingByte = 0) {
const paddedLength = getPaddedBufferSize(arrayBuffer.byteLength);
if (paddedLength !== arrayBuffer.byteLength) {
const array = new Uint8Array(paddedLength);
array.set(new Uint8Array(arrayBuffer));
// 填充填充字节
for (let i = arrayBuffer.byteLength; i < paddedLength; i++) {
array[i] = paddingByte;
}
return array.buffer;
}
return arrayBuffer;
}材质导出
材质导出将 Three.js 材质转换为 glTF 的 PBR 金属粗糙度工作流:
属性映射:
| Three.js 属性 | glTF 属性 | 说明 |
|---|---|---|
color, opacity | baseColorFactor | RGBA 数组 $$r, g, b, opacity$$ |
metalness | metallicFactor | 0.0 到 1.0 |
roughness | roughnessFactor | 0.0 到 1.0 |
map | baseColorTexture | 带 texCoord 通道 |
metalnessMap, roughnessMap | metallicRoughnessTexture | 合并为单个纹理(B=金属度, G=粗糙度) |
normalMap | normalTexture | 带缩放因子 |
emissive, emissiveMap | emissiveFactor, emissiveTexture | 通过扩展实现 HDR 自发光 |
aoMap | occlusionTexture | 带强度 |
transparent | alphaMode: "BLEND" | 对比 OPAQUE 或 MASK |
alphaTest | alphaMode: "MASK", alphaCutoff | 阈值 |
side: DoubleSide | doubleSided: true | 布尔标志 |
金属粗糙度纹理合并:
当材质具有独立的 metalnessMap 和 roughnessMap 时,导出器将它们合并为 glTF 要求的单个纹理 examples/jsm/exporters/GLTFExporter.js939-1045:
// 绿色通道 = 粗糙度, 蓝色通道 = 金属度
async buildMetalRoughTextureAsync(metalnessMap, roughnessMap) {
// 创建最大尺寸的画布
const width = Math.max(metalness?.width || 0, roughness?.width || 0);
const height = Math.max(metalness?.height || 0, roughness?.height || 0);
const canvas = getCanvas();
canvas.width = width;
canvas.height = height;
// 绘制并提取通道数据
// 蓝色通道 = 金属度, 绿色通道 = 粗糙度
for (let i = 2; i < data.length; i += 4) {
composite.data[i] = metalnessValue; // 蓝色
}
for (let i = 1; i < data.length; i += 4) {
composite.data[i] = roughnessValue; // 绿色
}
return mergedTexture;
}纹理处理
纹理导出处理图像编码、压缩纹理的解压缩和 mipmap 扁平化:
纹理解压缩:
导出压缩纹理(例如来自 CompressedTexture)时,导出器需要纹理工具来解压缩它们 examples/jsm/exporters/GLTFExporter.js1048-1058:
// 必须在导出压缩纹理前调用
import * as WebGLTextureUtils from 'three/addons/utils/WebGLTextureUtils.js';
exporter.setTextureUtils(WebGLTextureUtils);
// 或对于 WebGPU:
import * as WebGPUTextureUtils from 'three/addons/utils/WebGPUTextureUtils.js';
exporter.setTextureUtils(WebGPUTextureUtils);图像编码:
使用画布将图像编码为 PNG 或 JPEG examples/jsm/exporters/GLTFExporter.js1377-1489:
processImage(image, format, flipY, mimeType = 'image/png') {
const canvas = getCanvas();
canvas.width = Math.min(image.width, options.maxTextureSize);
canvas.height = Math.min(image.height, options.maxTextureSize);
const ctx = canvas.getContext('2d');
if (flipY) {
ctx.translate(0, canvas.height);
ctx.scale(1, -1);
}
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
// 二进制模式:存储为 bufferView
// JSON 模式:编码为 data URI
if (options.binary) {
const blob = await getToBlobPromise(canvas, mimeType);
imageDef.bufferView = await processBufferViewImage(blob);
} else {
imageDef.uri = ImageUtils.getDataURL(canvas, mimeType);
}
}扩展导出
导出器的扩展插件遵循与加载器类似的模式:
导出器扩展方法:
{
name: string, // 扩展标识符
// 对每个材质调用
writeMaterialAsync(material, materialDef),
// 对每个纹理调用
writeTexture(texture, textureDef),
// 对每个节点调用
writeNode(object, nodeDef),
// 对每个网格调用
writeMesh(mesh, meshDef)
}内置导出扩展:
| 扩展 | 类 | 用途 |
|---|---|---|
KHR_lights_punctual | GLTFLightExtension | 导出 DirectionalLight、PointLight、SpotLight |
KHR_materials_unlit | GLTFMaterialsUnlitExtension | 将 MeshBasicMaterial 导出为无光照 |
KHR_materials_transmission | GLTFMaterialsTransmissionExtension | 导出透射属性 |
KHR_materials_volume | GLTFMaterialsVolumeExtension | 导出体积属性 |
KHR_materials_ior | GLTFMaterialsIorExtension | 导出折射率属性 |
KHR_materials_specular | GLTFMaterialsSpecularExtension | 导出镜面工作流 |
KHR_materials_clearcoat | GLTFMaterialsClearcoatExtension | 导出清漆属性 |
KHR_materials_dispersion | GLTFMaterialsDispersionExtension | 导出色散 |
KHR_materials_iridescence | GLTFMaterialsIridescenceExtension | 导出彩虹色 |
KHR_materials_sheen | GLTFMaterialsSheenExtension | 导出光泽 |
KHR_materials_anisotropy | GLTFMaterialsAnisotropyExtension | 导出各向异性 |
KHR_materials_emissive_strength | GLTFMaterialsEmissiveStrengthExtension | 导出 HDR 自发光 |
EXT_materials_bump | GLTFMaterialsBumpExtension | 导出凹凸贴图 |
EXT_mesh_gpu_instancing | GLTFMeshGpuInstancing | 导出 InstancedMesh |
插件调用:
在特定点通过 _invokeAllAsync() 调用扩展 examples/jsm/exporters/GLTFExporter.js1723-1727:
await this._invokeAllAsync(async function(ext) {
ext.writeMaterialAsync && await ext.writeMaterialAsync(material, materialDef);
});使用示例
基本加载
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader();
// 回调风格
loader.load('model.gltf', (gltf) => {
scene.add(gltf.scene);
// 访问动画
if (gltf.animations.length) {
const mixer = new THREE.AnimationMixer(gltf.scene);
gltf.animations.forEach(clip => mixer.clipAction(clip).play());
}
}, undefined, (error) => {
console.error('加载错误:', error);
});
// 异步风格
const gltf = await loader.loadAsync('model.gltf');
scene.add(gltf.scene);带压缩的加载
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
// 配置 Draco 解码器
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/jsm/libs/draco/');
// 配置 KTX2 解码器
const ktx2Loader = new KTX2Loader()
.setTranscoderPath('/jsm/libs/basis/')
.detectSupport(renderer);
const loader = new GLTFLoader();
loader.setDRACOLoader(dracoLoader);
loader.setKTX2Loader(ktx2Loader);
loader.setMeshoptDecoder(MeshoptDecoder);
const gltf = await loader.loadAsync('compressed-model.glb');
scene.add(gltf.scene);基本导出
import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';
const exporter = new GLTFExporter();
// 导出为 JSON (.gltf)
exporter.parse(scene, (gltf) => {
const output = JSON.stringify(gltf, null, 2);
downloadJSON(output, 'scene.gltf');
}, (error) => {
console.error('导出错误:', error);
}, {
binary: false,
trs: false,
onlyVisible: true
});
// 导出为二进制 (.glb)
exporter.parse(scene, (glb) => {
downloadBinary(glb, 'scene.glb');
}, undefined, {
binary: true
});带选项的导出
import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';
import * as WebGLTextureUtils from 'three/addons/utils/WebGLTextureUtils.js';
const exporter = new GLTFExporter();
// 压缩纹理导出所需
exporter.setTextureUtils(WebGLTextureUtils);
const options = {
binary: true, // 输出 GLB 格式
trs: true, // 使用 TRS 而非矩阵(动画必需)
onlyVisible: true, // 跳过隐藏对象
maxTextureSize: 2048, // 调整纹理尺寸
animations: [clip1, clip2] // 包含特定动画
};
const glb = await exporter.parseAsync(scene, options);
downloadBinary(glb, 'scene.glb');自定义扩展注册
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
// 自定义加载器扩展
class MyCustomExtension {
constructor(parser) {
this.parser = parser;
this.name = 'MY_custom_extension';
}
extendMaterialParams(materialIndex, materialParams) {
const materialDef = this.parser.json.materials[materialIndex];
const extension = materialDef.extensions?.[this.name];
if (!extension) return Promise.resolve();
// 应用自定义属性
materialParams.userData.customData = extension.customData;
return Promise.resolve();
}
}
const loader = new GLTFLoader();
loader.register((parser) => new MyCustomExtension(parser));
const gltf = await loader.loadAsync('model.gltf');GLB 二进制格式
.glb 二进制格式将 JSON 和二进制数据打包成单一文件:
常量: examples/jsm/exporters/GLTFExporter.js370-376
const GLB_HEADER_BYTES = 12;
const GLB_HEADER_MAGIC = 0x46546C67; // 'glTF'
const GLB_VERSION = 2;
const GLB_CHUNK_PREFIX_BYTES = 8;
const GLB_CHUNK_TYPE_JSON = 0x4E4F534A; // 'JSON'
const GLB_CHUNK_TYPE_BIN = 0x004E4942; // 'BIN\0'GLB 构造: examples/jsm/exporters/GLTFExporter.js688-734
// 二进制块
const binaryChunk = getPaddedArrayBuffer(bufferData, 0);
const binaryChunkPrefix = new DataView(new ArrayBuffer(8));
binaryChunkPrefix.setUint32(0, binaryChunk.byteLength, true);
binaryChunkPrefix.setUint32(4, GLB_CHUNK_TYPE_BIN, true);
// JSON 块(空格填充)
const jsonChunk = getPaddedArrayBuffer(stringToArrayBuffer(JSON.stringify(json)), 0x20);
const jsonChunkPrefix = new DataView(new ArrayBuffer(8));
jsonChunkPrefix.setUint32(0, jsonChunk.byteLength, true);
jsonChunkPrefix.setUint32(4, GLB_CHUNK_TYPE_JSON, true);
// 头部
const header = new ArrayBuffer(12);
const headerView = new DataView(header);
headerView.setUint32(0, GLB_HEADER_MAGIC, true);
headerView.setUint32(4, GLB_VERSION, true);
const totalByteLength = 12 + 8 + jsonChunk.byteLength + 8 + binaryChunk.byteLength;
headerView.setUint32(8, totalByteLength, true);
// 连接所有块
const glbBlob = new Blob([header, jsonChunkPrefix, jsonChunk, binaryChunkPrefix, binaryChunk]);性能考量
加载器优化
- 缓存:
GLTFRegistry缓存已解析的对象以避免重复处理 examples/jsm/loaders/GLTFLoader.js565-597 - ImageBitmapLoader: 默认在支持的平台上使用,以加快图像解码 examples/jsm/loaders/GLTFLoader.js80-82
- 异步解析: 所有繁重操作返回 Promise,避免主线程阻塞
- 扩展懒加载: 扩展仅在需要时处理数据
导出器优化
- 资源去重:
GLTFWriter.cache防止共享几何体、材质和纹理的重复处理 examples/jsm/exporters/GLTFExporter.js612-619 - 缓冲区合并: 所有二进制数据合并为单一缓冲区以减少 HTTP 请求
- 纹理压缩: 支持导出可用时的压缩纹理
- 几何体共享: 引用同一几何体的多个 Mesh 仅导出一次几何体
局限性与约束
加载器局限性
- 仅 glTF 2.0: 不支持版本 1.0 文件 examples/jsm/loaders/GLTFLoader.js460-464
- ImageBitmap 处置: Image bitmap 需要手动垃圾回收和特殊处置处理 examples/jsm/loaders/GLTFLoader.js80-82
- 扩展依赖: 某些扩展需要外部解码器库(Draco、KTX2、Meshopt)
- Shader 材质: ShaderMaterial 无法可靠地从 glTF 导入
导出器局限性
- ShaderMaterial: 不支持导出 examples/jsm/exporters/GLTFExporter.js1571-1576
- 材质类型: 使用
MeshStandardMaterial和MeshBasicMaterial效果最佳 examples/jsm/exporters/GLTFExporter.js1583-1586 - 纹理通道:
metalnessMap和roughnessMap必须使用相同 UV 通道 examples/jsm/exporters/GLTFExporter.js1035-1038 - 动画要求: 动画需要
trs: true选项(使用平移/旋转/缩放而非矩阵) examples/jsm/exporters/GLTFExporter.js656-660 - 缓冲区类型: 仅支持
Float32Array、Uint32Array、Int32Array、Uint16Array、Int16Array、Uint8Array、Int8Arrayexamples/jsm/exporters/GLTFExporter.js1297-1328 - 法线验证: 非归一化法线触发归一化副本的创建 examples/jsm/exporters/GLTFExporter.js1808-1815