glTF Transform

repository·main·Indexed 23 days ago

https://github.com/donmccurdy/gltf-transform

A glTF 2.0 SDK for JavaScript and TypeScript, compatible with Web and Node.js. It provides a programmatic scripting API and a command-line interface for reading, editing, and optimizing 3D models. Key features include geometry compression via Draco and Meshoptimizer, texture compression (WebP, KTX2/Basis Universal), and a view module for real-time synchronization with three.js scenes.

Tokens
40.1K
Snippets
80
Records
203
Agent score
83%

What's inside glTF Transform

  1. How extensions and extension properties work

    main

    Extensions enhance a glTF Document by adding new features or schema.

    1. Extension: A class extending Extension that manages the extension's presence in the document. It is added via Document.createExtension(Constructor).
    2. ExtensionProperty: A class extending ExtensionProperty used to attach data to existing glTF resources (like Node, Material, or Texture).

    Properties use an internal property-graph to manage references to glTF resources (like Accessor or Texture). This ensures that glTF Transform's graph-aware operations (like pruning unused resources) correctly account for resources used by extensions. You define which resources a property can be attached to using this.parentTypes (e.g., [PropertyType.NODE]).

  2. How properties and references work in glTF Transform

    main

    In a standard glTF file, properties like meshes, materials, and nodes are stored in top-level JSON arrays and linked via integer indices. This is efficient for loading but difficult to edit manually.

    glTF Transform preserves the conceptual model of these root-level arrays but replaces index-based pointers with a managed graph of references. Instead of working with indices, you interact with objects directly. The library manages the internal graph structure, ensuring that when you pass an object to a property, the relationship is automatically tracked.

    Key behaviors:

    • Fluidity: Positions in root-level arrays are not fixed until the file is exported. You can add, move, or remove objects without manually recalculating indices.
    • Automatic Tracking: Objects can identify their users. For example, a Mesh can list all Node instances that reference it using .listParents().
    • Lifecycle Management: To remove all uses of a Property, use .detach() or .dispose(). Note that detached but non-disposed properties may still be exported, though they will lack references from a Scene.
    // Find an existing Mesh named 'Cog'.
    const mesh = doc.getRoot()
      .listMeshes()
      .find((mesh) => mesh.getName() === 'Cog');
    
    // Instantiate a copy of the 'Cog' mesh at a new node, 'CogInstance1'.
    const cogNode = doc.createNode('CogInstance1')
      .setMesh(mesh);
    
    // Add the Node to a Scene.
    doc.listScenes()[0]
      .addChild(cogNode);
    
    // List all references to the 'Cog' mesh.
    mesh.listParents(); // → [cogNode, ...]
  3. How Transforms work in glTF Transform

    main

    Transforms are operations that apply a modification to a Document. They are executed using the Document.transform() method, which accepts one or more transform functions as arguments. This allows you to chain multiple modifications (like welding, quantizing, or deduplicating) in a single call. You can also implement custom transforms by creating a function that accepts a Document and modifies it.

    import { NodeIO } from '@gltf-transform/core';
    import { KHRONOS_EXTENSIONS } from '@gltf-transform/extensions';
    import { weld, quantize, dedup } from '@gltf-transform/functions';
    
    const io = new NodeIO().registerExtensions(KHRONOS_EXTENSIONS);
    const document = await io.read('input.glb');
    
    // Apply multiple transforms at once
    await document.transform(
    	weld(),
    	quantize(),
    	dedup(),
    
    	// Custom transform.
    	backfaceCulling({cull: true}),
    );
    
    // Custom transform implementation example: enable/disable backface culling.
    function backfaceCulling(options) {
      return (document) => {
        for (const material of document.getRoot().listMaterials()) {
          material.setDoubleSided(!options.cull);
        }
      };
    }
    
    await io.write('output.glb', document);
  4. How @gltf-transform/view works with three.js

    main

    @gltf-transform/view creates three.js objects from a glTF Transform Document and maintains a real-time link between them. As you modify the Document via the glTF Transform API, the three.js scene graph updates automatically.

    This provides a lossless workflow for editor-like web applications. Unlike THREE.GLTFExporter, features not supported by three.js are preserved in the underlying Document even if they are not rendered in the preview.

    Key Workflow

    1. Load: Use WebIO to read a glTF file into a Document.
    2. Initialize View: Create a DocumentView from the Document.
    3. Bind to Scene: Use documentView.view(sceneDef) to convert a glTF Transform Scene (or other elements) into three.js objects (like THREE.Group) and add them to your three.js scene.
    4. Edit: Modify the Document properties; the three.js scene will update automatically.

    Memory Management

    DocumentView tracks reference counts and disposes of WebGL resources (textures, geometry, materials) when the underlying glTF Transform properties are disposed.

    To manually free up GPU memory by disposing of unused resources, call documentView.gc(). Resources are re-allocated automatically if they are needed again later.

    import { Scene, WebGLRenderer, PerspectiveCamera } from 'three';
    import { WebIO } from '@gltf-transform/core';
    import { KHRONOS_EXTENSIONS } from '@gltf-transform/extensions';
    import { DocumentView } from '@gltf-transform/view';
    
    // Set up three.js scene.
    const scene = new Scene();
    // ...
    
    // Load glTF Document.
    const io = new WebIO().registerExtensions(KHRONOS_EXTENSIONS);
    const document = await io.read('path/to/input.glb');
    const documentView = new DocumentView(document);
    
    // Add glTF content to the scene (just once).
    const sceneDef = document.getRoot().getDefaultScene(); // glTF Transform Scene
    const group = documentView.view(sceneDef); // THREE.Group
    scene.add(group);
    
    // Render.
    function animate () {
    	requestAnimationFrame(animate);
    	renderer.render(scene, camera);
    }
    
    // When glTF Document is edited, scene updates automatically.
    const materialDef = document.getRoot().listMaterials()
    	.find((mat) => mat.getName() === 'MyMaterial');
    buttonEl.addEventListener('click', () => {
    	materialDef.setBaseColorHex(0xFF0000);
    });
  5. How binary data and Accessors are managed

    main

    In glTF, binary data (like vertex positions) is tightly packed into buffers, where Accessors and BufferViews define byte offsets. Manually editing this data is difficult because changing one value can shift the offsets for everything else.

    glTF Transform simplifies this by isolating the typed arrays during editing. You can resize or delete data in an Accessor without manually updating byte offsets or other BufferViews. The library handles the complex task of recalculating these offsets and generating appropriate BufferViews during the export process.

    Key Abstractions:

    • Accessor: Represents the typed array of data. It has a reference to a Buffer, but this is primarily used by the exporter to determine data placement.
    • BufferView: This concept is abstracted away from the user. glTF Transform automatically creates interleaved BufferViews for each mesh at export and generates additional views for remaining data.
    • Grouping: You can assign Accessors to specific Buffer instances to group data, which enables lazy-loading in supported clients.
  6. Install the glTF Transform scripting packages

    main

    To use the glTF Transform SDK for programmatic model manipulation in Node.js or the Web, install the core, extensions, and functions packages via npm.

    npm install --save @gltf-transform/core @gltf-transform/extensions @gltf-transform/functions
  7. Write a custom extension

    main

    To implement a custom extension, create a subclass of Extension. You should implement read(context) and write(context) to support full round-trip processing of glTF files.

    Example structure:

    class ParticleEmitter extends Extension {
    	extensionName = 'ACME_particle_emitter';
    	static EXTENSION_NAME = 'ACME_particle_emitter';
    
    	/** Creates a new Emitter property, for use on a Node. */
    	createEmitter(name = '') {
    		return new Emitter(this.document.getGraph(), name);
    	}
    
    	/** See https://github.com/donmccurdy/glTF-Transform/blob/main/packages/core/src/io/reader-context.ts */
    	read(context) {
    		throw new Error('ACME_particle_emitter: read() not implemented');
    	}
    
    	/** See https://github.com/donmccurdy/glTF-Transform/blob/main/packages/core/src/io/writer-context.ts */
    	write(context) {
    		throw new Error('ACME_particle_emitter: write() not implemented');
    	}
    }
  8. Write a custom extension property

    main

    To define data that attaches to glTF objects, extend ExtensionProperty. Use init() to define the extensionName, propertyType, and parentTypes (the glTF types this property can be attached to). Use getDefaults() to define initial values.

    Properties use methods like .get(), .set(), .addRef(), and .removeRef() (from the property-graph package) to manage data and resource references. This ensures the extension's usage of resources is tracked by the core engine.

    Example structure:

    class Emitter extends ExtensionProperty {
    	static EXTENSION_NAME = 'ACME_particle_emitter';
    
    	init() {
    		this.extensionName = 'ACME_particle_emitter';
    		this.propertyType = 'Emitter';
    		this.parentTypes = [PropertyType.NODE];
    	}
    
    	getDefaults() {
    		return Object.assign(super.getDefaults(), {
    			minVelocity: [1, 1, 1],
    			maxVelocity: [10, 10, 10],
    			meshes: []
    		});
    	}
    
    	// minVelocity
    	getMinVelocity() {
    		return this.get('minVelocity');
    	}
    	setMinVelocity(velocity) {
    		return this.set('minVelocity', velocity);
    	}
    
    	// maxVelocity
    	getMaxVelocity() {
    		return this.get('maxVelocity');
    	}
    	setMaxVelocity(velocity) {
    		return this.set('maxVelocity', velocity);
    	}
    
    	// meshes
    	listMeshes() {
    		return this.listRefs('meshes');
    	}
    	addMesh(mesh) {
    		return this.addRef('meshes', mesh);
    	}
    	removeMesh(mesh) {
    		return this.removeRef('meshes', mesh);
    	}
    }
  9. Configure the glTF Transform CLI with a configuration file

    main

    You can extend the glTF Transform CLI by providing a configuration file. This allows you to install custom commands or extensions that will be available to any command executed by the CLI. Extensions enable operations on glTF files that are otherwise unsupported, such as unofficial compression, texture formats, or materials.

    Note: This feature is currently EXPERIMENTAL and may undergo breaking changes.

    To use a configuration file, pass the --config flag followed by the path to your .mjs configuration file when running the CLI.

    gltf-transform --help --config path/to/gltf-transform.config.mjs