Skip to content

GLTF 导入与导出

本文深入探讨 GLTFLoaderGLTFExporter 类,它们实现了 glTF 2.0 格式与 Three.js 场景表示之间的双向转换。GLTFLoader.gltf(JSON)和 .glb(二进制)文件解析为 Three.js 对象,而 GLTFExporter 则将 Three.js 场景转换回 glTF 格式。

有关其他模型加载器的信息,请参阅 附加格式加载器。有关通过 JSON 进行场景序列化的信息,请参阅 场景序列化

概述

glTF 导入/导出系统对 Khronos glTF 2.0 规范 提供全面支持,这是一种免版税格式,针对 3D 内容的高效传输和加载进行了优化。GLTFLoaderGLTFExporter 均利用可扩展的插件架构来支持核心规范及 17+ 官方 glTF 扩展。

关键能力:

  • 通过 GLTFParser 解析 glTF JSON(.gltf)和二进制(.glb)格式
  • 使用 GLTFWriter 导出 Three.js 场景为 glTF/GLB,保留场景层次结构
  • 使用 register() 回调的基于插件的扩展系统
  • 通过 DRACOLoaderMeshoptDecoder 支持压缩网格
  • 通过 KTX2Loader(Basis Universal)支持压缩纹理
  • 完整的 PBR 材质工作流映射到 MeshStandardMaterial/MeshPhysicalMaterial
  • 动画支持:骨骼(Skeleton)、变形目标、属性关键帧(AnimationClip
  • 完整场景图:Object3D 节点、CameraLight 对象、层次结构

支持的 glTF 扩展:

扩展加载器导出器用途
KHR_draco_mesh_compression几何体压缩
KHR_materials_clearcoat清漆层
KHR_materials_dispersion色散
KHR_materials_emissive_strengthHDR 自发光
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_basisuBasis Universal 纹理
KHR_texture_transform纹理变换
EXT_materials_bump凹凸贴图
EXT_texture_webpWebP 纹理
EXT_texture_avifAVIF 纹理
EXT_meshopt_compressionMeshopt 缓冲区压缩
EXT_mesh_gpu_instancingGPU 实例化

性能考量

加载器优化

导出器优化

  • 资源去重: 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 获得最佳结果

MeshStandardMaterialMeshPhysicalMaterial 可干净映射到 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 架构

SVG
100%

GLTFLoader

核心 API

GLTFLoader 类提供加载 glTF 资源的主要接口:

方法参数返回类型用途
load()url, onLoad, onProgress, onErrorvoid从 URL 异步加载
parse()data, path, onLoad, onErrorvoid解析原始 glTF 数据
parseAsync()data, pathPromiseparse() 的异步版本
setDRACOLoader()dracoLoaderGLTFLoader配置 Draco 解码器
setKTX2Loader()ktx2LoaderGLTFLoader配置 KTX2 解码器
setMeshoptDecoder()meshoptDecoderGLTFLoader配置 Meshopt 解码器
register()callbackGLTFLoader注册扩展插件
unregister()callbackGLTFLoader注销扩展插件

加载结果对象(LoadObject):

{
  scene: Group,           // 主场景层次结构
  scenes: Array<Group>,   // 文件中所有场景
  cameras: Array<Camera>, // 所有相机
  animations: Array<AnimationClip>, // 所有动画
  asset: Object,         // glTF 资产元数据
  parser: GLTFParser,    // 解析器实例
  userData: Object       // 自定义数据
}

加载管线

GLTFLoader 加载管线

SVG
100%

GLTFParser 中的关键方法:

方法用途返回值
parse(onLoad, onError)主解析入口void
getDependency(type, index)按类型/索引加载资源Promise<any>
loadBuffer(bufferIndex)加载二进制缓冲区Promise<ArrayBuffer>
loadBufferView(bufferViewIndex)加载缓冲区视图,可选解压缩Promise<ArrayBuffer>
loadAccessor(accessorIndex)从访问器创建 BufferAttributePromise<BufferAttribute>
loadTexture(textureIndex)从纹理定义创建 TexturePromise<Texture>
loadImage(imageIndex)从 URI 或 bufferView 加载图像Promise<Image>
assignTexture(materialParams, mapName, mapDef)将纹理分配给材质参数Promise<Texture>
loadMaterial(materialIndex)从材质定义创建 MaterialPromise<Material>
loadGeometry(primitiveIndex)从图元创建 BufferGeometryPromise<BufferGeometry>
loadMesh(meshIndex)从网格定义创建 MeshGroupPromise<Group>
loadCamera(cameraIndex)从相机定义创建 CameraPromise<Camera>
loadNode(nodeIndex)从节点定义创建 Object3DPromise<Object3D>
loadScene(sceneIndex)从场景定义构建完整场景Promise<Group>

扩展系统

GLTFLoader 使用插件架构,每个 glTF 扩展实现为单独的类别。扩展在加载器构造期间通过 register() 注册,并由 GLTFParser 在解析管线的特定点调用。

扩展注册流程:

SVG
100%

GLTFParser 扩展方法:

GLTFParser 为扩展提供这些挂钩点:

方法调用时机返回类型用途
getMaterialType(materialIndex)创建材质前Material覆盖默认材质类型(例如返回 MeshPhysicalMaterial
extendMaterialParams(materialIndex, materialParams)材质创建期间PromisematerialParams 添加扩展属性
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_compressionGLTFDracoMeshCompressionExtensiondecodePrimitive()
KHR_lights_punctualGLTFLightsExtension_loadLight(), createNodeAttachment(), _markDefs()
KHR_materials_clearcoatGLTFMaterialsClearcoatExtensiongetMaterialType(), extendMaterialParams()
KHR_materials_transmissionGLTFMaterialsTransmissionExtensiongetMaterialType(), extendMaterialParams()
KHR_materials_unlitGLTFMaterialsUnlitExtensiongetMaterialType(), extendParams()
KHR_texture_basisuGLTFTextureBasisUExtensionloadTexture()
EXT_meshopt_compressionGLTFMeshoptCompressionloadBufferView()
EXT_mesh_gpu_instancingGLTFMeshGpuInstancingcreateNodeMesh()

扩展构造模式:

扩展接收 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 方法流程

SVG
100%

实现细节:

  1. 依赖跟踪: _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);
        }
      }
    }
  2. 光源创建: _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;
      }
    }
  3. 节点附加: 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 属性的通用模式:

材质扩展执行流程

SVG
100%

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_clearcoatMeshPhysicalMaterialclearcoat, clearcoatRoughnessclearcoatMap, clearcoatRoughnessMap, clearcoatNormalMap
KHR_materials_transmissionMeshPhysicalMaterialtransmissiontransmissionMap
KHR_materials_volumeMeshPhysicalMaterialthickness, attenuationDistance, attenuationColorthicknessMap
KHR_materials_iorMeshPhysicalMaterialior
KHR_materials_sheenMeshPhysicalMaterialsheenColor, sheenRoughness, sheensheenColorMap, sheenRoughnessMap
KHR_materials_unlitMeshBasicMaterialcolor, opacitymap

压缩资源支持

GLTFLoader 通过外部解码器库支持多种压缩格式:

Draco 网格压缩:

SVG
100%

用法 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, optionsvoid导出为 glTF
parseAsync()input, optionsPromise异步版本
register()callbackGLTFExporter注册扩展插件
unregister()callbackGLTFExporter注销扩展插件
setTextureUtils()utilsGLTFExporter配置纹理解压缩

导出选项:

{
  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 导出管线

SVG
100%

导出管线中的关键 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/JPEGjson.images[] 条目
processSampler(map)processTextureAsync()转换纹理参数json.samplers[] 条目
processAccessor(attribute, geometry)processMeshAsync()BufferAttribute 转换为访问器json.accessors[] 条目
processBufferView(attribute, componentType)processAccessor()从属性数据创建 bufferViewjson.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)ArrayBuffer0(缓冲区索引)将缓冲区添加到 buffers[] 以进行合并
processBufferView(attribute, componentType, start, count, target)BufferAttribute, 组件类型, 范围, 目标{id, byteLength}json.bufferViews[] 中创建条目
processBufferViewImage(blob)BlobPromise<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)Texturenumber(采样器索引)将滤镜/环绕转换为 glTF 常量
processTextureAsync(map)TexturePromise<number>处理纹理,调用 processImage()processSampler()
processMaterialAsync(material)MaterialPromise<number>转换为 PBR 材质定义
processMeshAsync(mesh)MeshPromise<number>将几何体和材质转换为 glTF 网格
getUID(attribute, isRelativeCopy)BufferAttribute, 标志number返回属性去重的唯一 ID
serializeUserData(object, objectDef)object, definitionvoiduserData 添加到 objectDef.extras
applyTextureTransform(mapDef, texture)texture definition, Texturevoid如需要则添加 KHR_texture_transform

缓冲区数据布局

GLTFExporter 使用 glTF 数据对齐要求,其中 bufferViews 必须对齐到 4 字节边界:

GLTFWriter 缓冲区和访问器布局

SVG
100%

填充函数: 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, opacitybaseColorFactorRGBA 数组 $$r, g, b, opacity$$
metalnessmetallicFactor0.0 到 1.0
roughnessroughnessFactor0.0 到 1.0
mapbaseColorTexture带 texCoord 通道
metalnessMap, roughnessMapmetallicRoughnessTexture合并为单个纹理(B=金属度, G=粗糙度)
normalMapnormalTexture带缩放因子
emissive, emissiveMapemissiveFactor, emissiveTexture通过扩展实现 HDR 自发光
aoMapocclusionTexture带强度
transparentalphaMode: "BLEND"对比 OPAQUE 或 MASK
alphaTestalphaMode: "MASK", alphaCutoff阈值
side: DoubleSidedoubleSided: true布尔标志

金属粗糙度纹理合并:

当材质具有独立的 metalnessMaproughnessMap 时,导出器将它们合并为 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 扁平化:

SVG
100%

纹理解压缩:

导出压缩纹理(例如来自 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_punctualGLTFLightExtension导出 DirectionalLight、PointLight、SpotLight
KHR_materials_unlitGLTFMaterialsUnlitExtension将 MeshBasicMaterial 导出为无光照
KHR_materials_transmissionGLTFMaterialsTransmissionExtension导出透射属性
KHR_materials_volumeGLTFMaterialsVolumeExtension导出体积属性
KHR_materials_iorGLTFMaterialsIorExtension导出折射率属性
KHR_materials_specularGLTFMaterialsSpecularExtension导出镜面工作流
KHR_materials_clearcoatGLTFMaterialsClearcoatExtension导出清漆属性
KHR_materials_dispersionGLTFMaterialsDispersionExtension导出色散
KHR_materials_iridescenceGLTFMaterialsIridescenceExtension导出彩虹色
KHR_materials_sheenGLTFMaterialsSheenExtension导出光泽
KHR_materials_anisotropyGLTFMaterialsAnisotropyExtension导出各向异性
KHR_materials_emissive_strengthGLTFMaterialsEmissiveStrengthExtension导出 HDR 自发光
EXT_materials_bumpGLTFMaterialsBumpExtension导出凹凸贴图
EXT_mesh_gpu_instancingGLTFMeshGpuInstancing导出 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 和二进制数据打包成单一文件:

SVG
100%

常量: 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]);

性能考量

加载器优化

导出器优化

  • 资源去重: GLTFWriter.cache 防止共享几何体、材质和纹理的重复处理 examples/jsm/exporters/GLTFExporter.js612-619
  • 缓冲区合并: 所有二进制数据合并为单一缓冲区以减少 HTTP 请求
  • 纹理压缩: 支持导出可用时的压缩纹理
  • 几何体共享: 引用同一几何体的多个 Mesh 仅导出一次几何体

局限性与约束

加载器局限性

导出器局限性