GLTF Import & Export
This document provides a deep dive into the GLTFLoader and GLTFExporter classes, which enable bidirectional conversion between the glTF 2.0 format and three.js scene representations. GLTFLoader parses .gltf (JSON) and .glb (binary) files into three.js objects, while GLTFExporter converts three.js scenes back into glTF format.
For information about other model loaders, see Additional Format Loaders. For scene serialization via JSON, see Scene Serialization.
Overview
The glTF import/export system implements comprehensive support for the Khronos glTF 2.0 specification a royalty-free format optimized for efficient transmission and loading of 3D content. Both GLTFLoader and GLTFExporter leverage an extensible plugin architecture to support the core specification plus 17+ official glTF extensions.
Key Capabilities:
- Parse glTF JSON (
.gltf) and binary (.glb) formats viaGLTFParser - Export three.js scenes to glTF/GLB with
GLTFWriterpreserving scene hierarchy - Plugin-based extension system using
register()callbacks - Compressed mesh support via
DRACOLoaderandMeshoptDecoder - Compressed texture support via
KTX2Loader(Basis Universal) - Full PBR material workflow mapping to
MeshStandardMaterial/MeshPhysicalMaterial - Animation support: skeletal (
Skeleton), morph targets, property keyframes (AnimationClip) - Complete scene graph:
Object3Dnodes,Camera,Lightobjects, hierarchies
Supported glTF Extensions:
| Extension | Loader | Exporter | Purpose |
|---|---|---|---|
| KHR_draco_mesh_compression | ✓ | — | Geometry compression |
| KHR_materials_clearcoat | ✓ | ✓ | Clear coat layer |
| KHR_materials_dispersion | ✓ | ✓ | Chromatic dispersion |
| KHR_materials_emissive_strength | ✓ | ✓ | HDR emissive |
| KHR_materials_ior | ✓ | ✓ | Index of refraction |
| KHR_materials_iridescence | ✓ | ✓ | Thin-film interference |
| KHR_materials_sheen | ✓ | ✓ | Fabric appearance |
| KHR_materials_specular | ✓ | ✓ | Specular workflow |
| KHR_materials_transmission | ✓ | ✓ | Glass/transparency |
| KHR_materials_unlit | ✓ | ✓ | Unlit materials |
| KHR_materials_volume | ✓ | ✓ | Volumetric materials |
| KHR_materials_anisotropy | ✓ | ✓ | Anisotropic reflections |
| KHR_lights_punctual | ✓ | ✓ | Point/Spot/Directional lights |
| KHR_mesh_quantization | ✓ | ✓ | Quantized vertex attributes |
| KHR_texture_basisu | ✓ | — | Basis Universal textures |
| KHR_texture_transform | ✓ | ✓ | Texture transforms |
| EXT_materials_bump | ✓ | ✓ | Bump mapping |
| EXT_texture_webp | ✓ | — | WebP textures |
| EXT_texture_avif | ✓ | — | AVIF textures |
| EXT_meshopt_compression | ✓ | — | Meshopt buffer compression |
| EXT_mesh_gpu_instancing | ✓ | ✓ | GPU instancing |
Performance Considerations
Loader Optimizations
- Caching:
GLTFRegistrycaches parsed objects to avoid duplicate processing examples/jsm/loaders/GLTFLoader.js565-597 - ImageBitmapLoader: Used by default for faster image decoding on supported platforms examples/jsm/loaders/GLTFLoader.js80-82
- Async parsing: All heavy operations return promises, avoiding main thread blocking
- Extension lazy loading: Extensions only process data when needed
Exporter Optimizations
- Resource deduplication:
GLTFWriter.cacheprevents duplicate processing of shared geometries, materials, and textures examples/jsm/exporters/GLTFExporter.js612-619 - Buffer merging: All binary data merged into single buffer to reduce HTTP requests
- Texture compression: Supports exporting compressed textures when available
- Geometry sharing: Multiple meshes referencing same geometry only export geometry once
Best Practices
Loading Best Practices
1. Use Compression for Production Assets
Always configure compression loaders for production. This can reduce file sizes by 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. Clean Up Resources
Dispose of loader resources when switching scenes or models examples/jsm/loaders/GLTFLoader.js80-82:
// ImageBitmap resources require explicit cleanup
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. Use LoadingManager for Progress Tracking
Track loading progress across multiple assets:
const manager = new THREE.LoadingManager();
manager.onProgress = (url, loaded, total) => {
console.log(`Loading: ${(loaded/total * 100)}%`);
};
const loader = new GLTFLoader(manager);4. Handle Errors Gracefully
Always provide error handlers examples/jsm/loaders/GLTFLoader.js253-327:
loader.load('model.gltf', onLoad, onProgress, (error) => {
console.error('Loading failed:', error);
// Fallback to placeholder model
scene.add(createPlaceholder());
});5. Prefer Async/Await for Cleaner Code
Use loadAsync() for modern promise-based workflows:
try {
const gltf = await loader.loadAsync('model.gltf');
scene.add(gltf.scene);
} catch (error) {
console.error('Loading error:', error);
}Export Best Practices
1. Use Binary Format for Production
Binary .glb files are more efficient than .gltf with separate resources examples/misc_exporter_gltf.html34-69:
const options = { binary: true };
const glb = await exporter.parseAsync(scene, options);2. Enable TRS for Animated Content
When exporting animations, use trs: true to preserve animation compatibility examples/jsm/exporters/GLTFExporter.js656-660:
const options = {
binary: true,
trs: true, // Required for animations
animations: mixer.clipAction.getClip()
};3. Optimize Texture Sizes
Limit texture dimensions to reasonable values examples/misc_exporter_gltf.html34-69:
const options = {
binary: true,
maxTextureSize: 2048 // Prevent exporting 4K+ textures
};4. Set Up Texture Decompression
Required when exporting scenes with compressed textures examples/jsm/exporters/GLTFExporter.js1048-1058:
import * as WebGLTextureUtils from 'three/addons/utils/WebGLTextureUtils.js';
exporter.setTextureUtils(WebGLTextureUtils);5. Use MeshStandardMaterial for Best Results
MeshStandardMaterial and MeshPhysicalMaterial map cleanly to glTF PBR workflow examples/jsm/exporters/GLTFExporter.js1571-1586:
// Good - exports cleanly
const material = new THREE.MeshStandardMaterial({
color: 0xff0000,
metalness: 0.5,
roughness: 0.7
});
// Avoid - ShaderMaterial not supported
const shaderMat = new THREE.ShaderMaterial({...}); // Will be skipped6. Ensure UV Channel Consistency
Metalness and roughness maps must use the same UV channel examples/jsm/exporters/GLTFExporter.js1035-1038:
// Both maps must use same UV channel
material.metalnessMap.channel = 0;
material.roughnessMap.channel = 0; // Must match7. Export Only What's Needed
Use onlyVisible to skip hidden debug objects examples/misc_exporter_gltf.html34-69:
const options = {
onlyVisible: true, // Skip object.visible === false
binary: true
};Material Workflow Best Practices
1. Separate Metalness and Roughness Maps
The exporter automatically merges them, but keep sources separate for flexibility examples/jsm/exporters/GLTFExporter.js939-1045:
material.metalnessMap = metalnessTexture; // Blue channel
material.roughnessMap = roughnessTexture; // Green channel
// Exporter merges into single texture2. Use Linear Color Space for Data Textures
Non-color data (normal, metalness, roughness) should use NoColorSpace:
normalMap.colorSpace = THREE.NoColorSpace;
metalnessMap.colorSpace = THREE.NoColorSpace;
roughnessMap.colorSpace = THREE.NoColorSpace;3. Validate Normal Normalization
Ensure normals are unit length to avoid export warnings examples/jsm/exporters/GLTFExporter.js1808-1815:
geometry.computeVertexNormals();
// Exporter will normalize if needed, but pre-normalized is betterLimitations and Constraints
System Architecture
GLTFLoader and GLTFExporter Architecture
GLTFLoader
Core API
The GLTFLoader class provides the main interface for loading glTF assets:
| Method | Parameters | Return Type | Purpose |
|---|---|---|---|
load() | url, onLoad, onProgress, onError | void | Asynchronously load from URL |
parse() | data, path, onLoad, onError | void | Parse raw glTF data |
parseAsync() | data, path | Promise | Async version of parse() |
setDRACOLoader() | dracoLoader | GLTFLoader | Configure Draco decoder |
setKTX2Loader() | ktx2Loader | GLTFLoader | Configure KTX2 decoder |
setMeshoptDecoder() | meshoptDecoder | GLTFLoader | Configure Meshopt decoder |
register() | callback | GLTFLoader | Register extension plugin |
unregister() | callback | GLTFLoader | Unregister extension plugin |
Load Result Object (LoadObject):
{
scene: Group, // Main scene hierarchy
scenes: Array<Group>, // All scenes in file
cameras: Array<Camera>, // All cameras
animations: Array<AnimationClip>, // All animations
asset: Object, // glTF asset metadata
parser: GLTFParser, // Parser instance
userData: Object // Custom data
}Loading Pipeline
GLTFLoader Loading Pipeline
Key Methods in GLTFParser:
| Method | Purpose | Returns |
|---|---|---|
parse(onLoad, onError) | Main parsing entry point | void |
getDependency(type, index) | Load resource by type/index | Promise<any> |
loadBuffer(bufferIndex) | Load binary buffer | Promise<ArrayBuffer> |
loadBufferView(bufferViewIndex) | Load buffer view with optional decompression | Promise<ArrayBuffer> |
loadAccessor(accessorIndex) | Create BufferAttribute from accessor | Promise<BufferAttribute> |
loadTexture(textureIndex) | Create Texture from texture definition | Promise<Texture> |
loadImage(imageIndex) | Load image from URI or bufferView | `Promise<Image |
assignTexture(materialParams, mapName, mapDef) | Assign texture to material params | Promise<Texture> |
loadMaterial(materialIndex) | Create Material from material definition | Promise<Material> |
loadGeometry(primitiveIndex) | Create BufferGeometry from primitive | Promise<BufferGeometry> |
loadMesh(meshIndex) | Create Mesh or Group from mesh definition | `Promise<Group |
loadCamera(cameraIndex) | Create Camera from camera definition | Promise<Camera> |
loadNode(nodeIndex) | Create Object3D from node definition | Promise<Object3D> |
loadScene(sceneIndex) | Build complete scene from scene definition | Promise<Group> |
Extension System
GLTFLoader uses a plugin architecture where each glTF extension is implemented as a separate class. Extensions are registered during loader construction via register() and invoked by GLTFParser at specific points in the parsing pipeline.
Extension Registration Flow:
GLTFParser Extension Methods:
GLTFParser provides these hook points for extensions:
| Method | When Called | Return Type | Purpose |
|---|---|---|---|
getMaterialType(materialIndex) | Before creating material | Material class | Override default material type (e.g., return MeshPhysicalMaterial) |
extendMaterialParams(materialIndex, materialParams) | During material creation | Promise | Add extension properties to materialParams |
createNodeMesh(nodeIndex) | During node parsing | Promise<Mesh> | Create custom mesh (e.g., InstancedMesh) |
createNodeAttachment(nodeIndex) | During node parsing | Promise<Object3D> | Attach additional objects (e.g., Light) |
loadTexture(textureIndex) | During texture loading | Promise<Texture> | Custom texture loading (e.g., KTX2) |
loadBufferView(index) | During buffer loading | Promise<ArrayBuffer> | Custom buffer decompression (e.g., Draco, Meshopt) |
getDependency(type, index) | Dependency resolution | Promise<any> | Provide custom dependencies |
_markDefs() | Before parsing starts | void | Mark referenced definitions for dependency tracking |
Built-in Extension Classes:
| Extension | Class Name | Key Methods |
|---|---|---|
| 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() |
Extension Constructor Pattern:
Extensions receive a GLTFParser instance and declare their name examples/jsm/loaders/GLTFLoader.js643-653:
class GLTFLightsExtension {
constructor(parser) {
this.parser = parser;
this.name = EXTENSIONS.KHR_LIGHTS_PUNCTUAL;
this.cache = { refs: {}, uses: {} };
}
// ... extension methods
}Extension Example: KHR_lights_punctual
GLTFLightsExtension demonstrates the extension pattern for adding custom scene objects:
GLTFLightsExtension Method Flow
Implementation Details:
Dependency Tracking:
_markDefs()scansjson.nodes[]for light references 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); } } }Light Creation:
_loadLight()creates appropriateLightsubclass 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; } }Node Attachment:
createNodeAttachment()returns promise for node to attach 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); }); }
Material Extension Pattern
Material extensions follow a common pattern for extending PBR properties:
Material Extension Execution Flow
GLTFMaterialsClearcoatExtension Example:
This extension adds clear coat properties to 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 = [];
// Scalar properties
if (extension.clearcoatFactor !== undefined) {
materialParams.clearcoat = extension.clearcoatFactor;
}
if (extension.clearcoatRoughnessFactor !== undefined) {
materialParams.clearcoatRoughness = extension.clearcoatRoughnessFactor;
}
// Texture loading
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);
}
}Common Material Extension Properties:
| Extension | Material Type | Properties Set | Textures Loaded |
|---|---|---|---|
| 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 |
Compressed Asset Support
GLTFLoader supports multiple compression formats through external decoder libraries:
Draco Mesh Compression:
Usage examples/misc_exporter_gltf.html486-492:
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/examples/jsm/libs/draco/');
loader.setDRACOLoader(dracoLoader);KTX2 Texture Compression:
const ktx2Loader = new KTX2Loader()
.setTranscoderPath('jsm/libs/basis/')
.detectSupport(renderer);
loader.setKTX2Loader(ktx2Loader);Meshopt Buffer Compression:
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
loader.setMeshoptDecoder(MeshoptDecoder);GLTFExporter
Core API
The GLTFExporter class converts three.js scenes to glTF format:
| Method | Parameters | Return Type | Purpose |
|---|---|---|---|
parse() | input, onDone, onError, options | void | Export to glTF |
parseAsync() | input, options | Promise | Async version |
register() | callback | GLTFExporter | Register extension plugin |
unregister() | callback | GLTFExporter | Unregister extension plugin |
setTextureUtils() | utils | GLTFExporter | Configure texture decompression |
Export Options:
{
binary: false, // Output GLB vs GLTF
trs: false, // Use translation/rotation/scale vs matrices
onlyVisible: true, // Skip hidden objects
maxTextureSize: Infinity, // Resize textures
animations: [], // AnimationClips to export
includeCustomExtensions: false // Export userData.gltfExtensions
}Supported Input Types:
Scene- Single sceneObject3D- Any scene graph nodeArray<Scene|Object3D>- Multiple scenes/objects
Output Format:
- Binary mode (
binary: true): ReturnsArrayBuffer(.glb file) - JSON mode (
binary: false): Returns glTF JSON object with embedded base64 buffer
Export Pipeline
GLTFExporter Export Pipeline
Key GLTFWriter Methods in Export Pipeline:
| Method | Called By | Purpose | Output |
|---|---|---|---|
writeAsync(input, onDone, options) | GLTFExporter.parse() | Main export orchestration | Calls onDone() with result |
processInputAsync(input) | writeAsync() | Traverse input scenes/objects | Populates json structure |
processNode(object) | processInputAsync() | Convert Object3D to glTF node | json.nodes[] entry |
processMeshAsync(mesh) | processNode() | Convert Mesh to glTF mesh | json.meshes[] entry |
processMaterialAsync(material) | processMeshAsync() | Convert Material to glTF material | json.materials[] entry |
processTextureAsync(texture) | processMaterialAsync() | Convert Texture to glTF texture | json.textures[] entry |
processImage(image, format, flipY) | processTextureAsync() | Encode image to PNG/JPEG | json.images[] entry |
processSampler(map) | processTextureAsync() | Convert texture parameters | json.samplers[] entry |
processAccessor(attribute, geometry) | processMeshAsync() | Convert BufferAttribute to accessor | json.accessors[] entry |
processBufferView(attribute, componentType) | processAccessor() | Create bufferView from attribute data | json.bufferViews[] entry |
processBuffer(buffer) | processBufferView() | Add binary data to merge list | Appends to buffers[] |
GLTFWriter Internal Structure
GLTFWriter orchestrates the export process, maintaining state for all glTF resources:
GLTFWriter State Structure:
class GLTFWriter {
constructor() {
this.plugins = []; // Extension plugin instances
this.options = {}; // Export options from parseAsync()
this.pending = []; // Async operations (Promise[])
this.buffers = []; // Binary data chunks (ArrayBuffer[])
this.byteOffset = 0; // Current buffer write position
this.nodeMap = new Map(); // Object3D -> node index mapping
this.skins = []; // Skin definitions
this.extensionsUsed = {}; // Extensions used in export
this.extensionsRequired = {}; // Required extensions
this.uids = new Map(); // BufferAttribute -> UID mapping
this.uid = 0; // UID counter
this.cache = { // Resource deduplication
meshes: new Map(), // mesh geometry+material key -> index
attributes: new Map(), // BufferAttribute UID -> accessor index
attributesNormalized: new Map(), // normalized attribute cache
materials: new Map(), // Material -> material index
textures: new Map(), // Texture -> texture index
images: new Map() // Image -> { mimeType:flipY -> image index }
};
this.json = { // Output glTF JSON structure
asset: {
version: '2.0',
generator: 'THREE.GLTFExporter r' + REVISION
}
};
this.textureUtils = null; // WebGLTextureUtils or WebGPUTextureUtils
}
}Core Processing Methods:
| Method | Input | Output | Purpose |
|---|---|---|---|
processBuffer(buffer) | ArrayBuffer | 0 (buffer index) | Adds buffer to buffers[] for merging |
processBufferView(attribute, componentType, start, count, target) | BufferAttribute, component type, range, target | {id, byteLength} | Creates entry in json.bufferViews[] |
processBufferViewImage(blob) | Blob | Promise<number> | Creates bufferView for image data |
processAccessor(attribute, geometry, start, count) | BufferAttribute, BufferGeometry, range | number (accessor index) | Creates entry in json.accessors[] with min/max |
processImage(image, format, flipY, mimeType) | Image, format, flip flag, mime type | number (image index) | Encodes image to PNG/JPEG, adds to json.images[] |
processSampler(map) | Texture | number (sampler index) | Converts filter/wrap to glTF constants |
processTextureAsync(map) | Texture | Promise<number> | Processes texture, calls processImage() and processSampler() |
processMaterialAsync(material) | Material | Promise<number> | Converts to PBR material definition |
processMeshAsync(mesh) | Mesh | Promise<number> | Converts geometry and material to glTF mesh |
getUID(attribute, isRelativeCopy) | BufferAttribute, flag | number | Returns unique ID for attribute deduplication |
serializeUserData(object, objectDef) | object, definition | void | Adds userData to objectDef.extras |
applyTextureTransform(mapDef, texture) | texture definition, Texture | void | Adds KHR_texture_transform if needed |
Buffer Data Layout
GLTFExporter uses the glTF data alignment requirements where bufferViews must be aligned to 4-byte boundaries:
GLTFWriter Buffer and Accessor Layout
Padding Function: 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));
// Fill padding bytes
for (let i = arrayBuffer.byteLength; i < paddedLength; i++) {
array[i] = paddingByte;
}
return array.buffer;
}
return arrayBuffer;
}Material Export
Material export converts three.js materials to glTF's PBR metallic-roughness workflow:
Property Mapping:
| Three.js Property | glTF Property | Notes |
|---|---|---|
color, opacity | baseColorFactor | RGBA array [r, g, b, opacity] |
metalness | metallicFactor | 0.0 to 1.0 |
roughness | roughnessFactor | 0.0 to 1.0 |
map | baseColorTexture | With texCoord channel |
metalnessMap, roughnessMap | metallicRoughnessTexture | Merged into single texture (B=metalness, G=roughness) |
normalMap | normalTexture | With scale factor |
emissive, emissiveMap | emissiveFactor, emissiveTexture | HDR emissive via extension |
aoMap | occlusionTexture | With strength |
transparent | alphaMode: "BLEND" | vs OPAQUE or MASK |
alphaTest | alphaMode: "MASK", alphaCutoff | Threshold value |
side: DoubleSide | doubleSided: true | Boolean flag |
Metallic-Roughness Texture Merging:
When a material has separate metalnessMap and roughnessMap, the exporter merges them into a single texture as required by glTF examples/jsm/exporters/GLTFExporter.js939-1045:
// Green channel = roughness, Blue channel = metalness
async buildMetalRoughTextureAsync(metalnessMap, roughnessMap) {
// Create canvas with max dimensions
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;
// Draw and extract channel data
// Blue channel = metalness, Green channel = roughness
for (let i = 2; i < data.length; i += 4) {
composite.data[i] = metalnessValue; // Blue
}
for (let i = 1; i < data.length; i += 4) {
composite.data[i] = roughnessValue; // Green
}
return mergedTexture;
}Texture Processing
Texture export handles image encoding, decompression of compressed textures, and mipmap flattening:
Texture Decompression:
When exporting compressed textures (e.g., from CompressedTexture), the exporter requires texture utilities to decompress them examples/jsm/exporters/GLTFExporter.js1048-1058:
// Must be called before exporting compressed textures
import * as WebGLTextureUtils from 'three/addons/utils/WebGLTextureUtils.js';
exporter.setTextureUtils(WebGLTextureUtils);
// Or for WebGPU:
import * as WebGPUTextureUtils from 'three/addons/utils/WebGPUTextureUtils.js';
exporter.setTextureUtils(WebGPUTextureUtils);Image Encoding:
Images are encoded to PNG or JPEG using canvas 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);
// Binary mode: store as bufferView
// JSON mode: encode as data URI
if (options.binary) {
const blob = await getToBlobPromise(canvas, mimeType);
imageDef.bufferView = await processBufferViewImage(blob);
} else {
imageDef.uri = ImageUtils.getDataURL(canvas, mimeType);
}
}Extension Export
Extension plugins for the exporter follow a pattern similar to the loader:
Exporter Extension Methods:
{
name: string, // Extension identifier
// Called for each material
writeMaterialAsync(material, materialDef),
// Called for each texture
writeTexture(texture, textureDef),
// Called for each node
writeNode(object, nodeDef),
// Called for each mesh
writeMesh(mesh, meshDef)
}Built-in Export Extensions:
| Extension | Class | Purpose |
|---|---|---|
KHR_lights_punctual | GLTFLightExtension | Export DirectionalLight, PointLight, SpotLight |
KHR_materials_unlit | GLTFMaterialsUnlitExtension | Export MeshBasicMaterial as unlit |
KHR_materials_transmission | GLTFMaterialsTransmissionExtension | Export transmission property |
KHR_materials_volume | GLTFMaterialsVolumeExtension | Export volume properties |
KHR_materials_ior | GLTFMaterialsIorExtension | Export IOR property |
KHR_materials_specular | GLTFMaterialsSpecularExtension | Export specular workflow |
KHR_materials_clearcoat | GLTFMaterialsClearcoatExtension | Export clearcoat properties |
KHR_materials_dispersion | GLTFMaterialsDispersionExtension | Export dispersion |
KHR_materials_iridescence | GLTFMaterialsIridescenceExtension | Export iridescence |
KHR_materials_sheen | GLTFMaterialsSheenExtension | Export sheen |
KHR_materials_anisotropy | GLTFMaterialsAnisotropyExtension | Export anisotropy |
KHR_materials_emissive_strength | GLTFMaterialsEmissiveStrengthExtension | Export HDR emissive |
EXT_materials_bump | GLTFMaterialsBumpExtension | Export bump maps |
EXT_mesh_gpu_instancing | GLTFMeshGpuInstancing | Export InstancedMesh |
Plugin Invocation:
Extensions are invoked via _invokeAllAsync() at specific points examples/jsm/exporters/GLTFExporter.js1723-1727:
await this._invokeAllAsync(async function(ext) {
ext.writeMaterialAsync && await ext.writeMaterialAsync(material, materialDef);
});Usage Examples
Basic Loading
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader();
// Callback style
loader.load('model.gltf', (gltf) => {
scene.add(gltf.scene);
// Access animations
if (gltf.animations.length) {
const mixer = new THREE.AnimationMixer(gltf.scene);
gltf.animations.forEach(clip => mixer.clipAction(clip).play());
}
}, undefined, (error) => {
console.error('Loading error:', error);
});
// Async style
const gltf = await loader.loadAsync('model.gltf');
scene.add(gltf.scene);Loading with Compression
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';
// Configure Draco decoder
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/jsm/libs/draco/');
// Configure KTX2 decoder
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);Basic Export
import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';
const exporter = new GLTFExporter();
// Export to JSON (.gltf)
exporter.parse(scene, (gltf) => {
const output = JSON.stringify(gltf, null, 2);
downloadJSON(output, 'scene.gltf');
}, (error) => {
console.error('Export error:', error);
}, {
binary: false,
trs: false,
onlyVisible: true
});
// Export to binary (.glb)
exporter.parse(scene, (glb) => {
downloadBinary(glb, 'scene.glb');
}, undefined, {
binary: true
});Export with Options
import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';
import * as WebGLTextureUtils from 'three/addons/utils/WebGLTextureUtils.js';
const exporter = new GLTFExporter();
// Required for compressed texture export
exporter.setTextureUtils(WebGLTextureUtils);
const options = {
binary: true, // Output GLB format
trs: true, // Use TRS instead of matrix (required for animation)
onlyVisible: true, // Skip hidden objects
maxTextureSize: 2048, // Resize textures
animations: [clip1, clip2] // Include specific animations
};
const glb = await exporter.parseAsync(scene, options);
downloadBinary(glb, 'scene.glb');Custom Extension Registration
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
// Custom loader extension
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();
// Apply custom properties
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 Binary Format
The .glb binary format packages JSON and binary data into a single file:
Constants: 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 Construction: examples/jsm/exporters/GLTFExporter.js688-734
// Binary chunk
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 chunk (padded with spaces)
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);
// Header
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);
// Concatenate all chunks
const glbBlob = new Blob([header, jsonChunkPrefix, jsonChunk, binaryChunkPrefix, binaryChunk]);Performance Considerations
Loader Optimizations
- Caching:
GLTFRegistrycaches parsed objects to avoid duplicate processing examples/jsm/loaders/GLTFLoader.js565-597 - ImageBitmapLoader: Used by default for faster image decoding on supported platforms examples/jsm/loaders/GLTFLoader.js80-82
- Async parsing: All heavy operations return promises, avoiding main thread blocking
- Extension lazy loading: Extensions only process data when needed
Exporter Optimizations
- Resource deduplication:
GLTFWriter.cacheprevents duplicate processing of shared geometries, materials, and textures examples/jsm/exporters/GLTFExporter.js612-619 - Buffer merging: All binary data merged into single buffer to reduce HTTP requests
- Texture compression: Supports exporting compressed textures when available
- Geometry sharing: Multiple meshes referencing same geometry only export geometry once
Limitations and Constraints
Loader Limitations
- glTF 2.0 only: Version 1.0 files are not supported examples/jsm/loaders/GLTFLoader.js460-464
- ImageBitmap disposal: Image bitmaps require manual garbage collection and special disposal handling examples/jsm/loaders/GLTFLoader.js80-82
- Extension dependencies: Some extensions require external decoder libraries (Draco, KTX2, Meshopt)
- Shader materials: ShaderMaterial cannot be reliably imported from glTF
Exporter Limitations
- ShaderMaterial: Not supported for export examples/jsm/exporters/GLTFExporter.js1571-1576
- Material types: Best results with
MeshStandardMaterialandMeshBasicMaterialexamples/jsm/exporters/GLTFExporter.js1583-1586 - Texture channels:
metalnessMapandroughnessMapmust use same UV channel examples/jsm/exporters/GLTFExporter.js1035-1038 - Animation requirement: Animations require
trs: trueoption (uses translation/rotation/scale instead of matrices) examples/jsm/exporters/GLTFExporter.js656-660 - Buffer types: Only
Float32Array,Uint32Array,Int32Array,Uint16Array,Int16Array,Uint8Array,Int8Arraysupported examples/jsm/exporters/GLTFExporter.js1297-1328 - Normal validation: Non-normalized normals trigger creation of normalized copies examples/jsm/exporters/GLTFExporter.js1808-1815