Read OSS

The Asset Pipeline: Loaders, Addons, and the Three.js Ecosystem

Intermediate

Prerequisites

  • Articles 1-3
  • Familiarity with common 3D asset formats (glTF, FBX, OBJ)

The Asset Pipeline: Loaders, Addons, and the Three.js Ecosystem

A 3D engine is only as useful as the content it can load. Three.js provides a structured loading system built on a Loader base class and LoadingManager, with core loaders for fundamental file types in the library and 50+ specialized loaders in the addon ecosystem. This final article covers the full asset pipeline from HTTP request to scene graph, the critical GLTFLoader addon, the post-processing architecture, the controls system, and the testing and contribution infrastructure.

Loader Base Class and LoadingManager

Loader is the abstract base class for all loaders. Its constructor at lines 15-70 establishes the configuration that every loader needs:

  • manager: A LoadingManager instance (defaults to DefaultLoadingManager)
  • crossOrigin: CORS setting for cross-domain resources ('anonymous' by default)
  • path: Base path prepended to URLs
  • resourcePath: Separate base path for dependent resources (textures referenced by models)
  • requestHeader: Custom HTTP headers for authenticated endpoints

The key abstract method is load(url, onLoad, onProgress, onError), which every concrete loader must implement. The class also provides loadAsync(url) which wraps load() in a Promise — a welcome modernization that makes loader usage cleaner with async/await.

LoadingManager orchestrates multiple concurrent loads. It tracks the total number of items loading, provides onStart, onLoad, onProgress, and onError callbacks, and implements a URL modification system through setURLModifier() — useful for redirecting asset paths in different deployment environments. It also integrates with the Cache module for HTTP-level response caching.

classDiagram
    class Loader {
        +manager: LoadingManager
        +crossOrigin: string
        +path: string
        +resourcePath: string
        +load(url, onLoad, onProgress, onError)*
        +loadAsync(url): Promise
        +setPath(path): this
    }

    class FileLoader {
        +responseType: string
        +mimeType: string
        +load(): XMLHttpRequest
    }

    class ImageLoader {
        +load(): HTMLImageElement
    }

    class TextureLoader {
        +load(): Texture
    }

    class ObjectLoader {
        +load(): Object3D
    }

    Loader <|-- FileLoader
    Loader <|-- ImageLoader
    Loader <|-- TextureLoader
    Loader <|-- ObjectLoader
    FileLoader <-- ImageLoader : uses
    ImageLoader <-- TextureLoader : uses

Core Loaders: FileLoader to ObjectLoader

The core loaders form a dependency chain:

FileLoader is the foundation — it handles HTTP requests via XMLHttpRequest, supports response types (text, arraybuffer, blob, json), integrates with LoadingManager for progress tracking, and uses the Cache module to avoid duplicate network requests.

ImageLoader builds on FileLoader (indirectly) to load images as HTMLImageElement or ImageBitmap objects, handling CORS and data URLs.

TextureLoader wraps ImageLoader and returns a Texture object with the loaded image data. This is the most commonly used loader for basic texture mapping.

ObjectLoader deserializes Three.js's JSON format back into a complete scene graph with geometries, materials, textures, and animation clips. It's the inverse of toJSON() methods on Object3D and its subclasses.

flowchart LR
    URL["Asset URL"] --> FL["FileLoader<br/>(HTTP request + caching)"]
    FL --> IL["ImageLoader<br/>(Image element)"]
    IL --> TL["TextureLoader<br/>(Texture object)"]

    FL -->|"JSON"| OL["ObjectLoader<br/>(Scene graph)"]

    FL -->|"ArrayBuffer"| Addons["Addon Loaders<br/>(GLTFLoader, FBXLoader, etc.)"]

Tip: Always use LoadingManager when loading multiple assets to track overall progress. Create a custom manager with new LoadingManager(onAllLoaded, onProgress, onError) rather than relying on individual loader callbacks.

The Addon Ecosystem: examples/jsm/

As we discussed in Part 1, examples/jsm/ is the official addon system published under three/addons. The Addons.js barrel file re-exports from organized subdirectories:

Directory Contents Notable Modules
loaders/ Format-specific asset loaders GLTFLoader, FBXLoader, DRACOLoader, KTX2Loader, OBJLoader
controls/ User interaction handlers OrbitControls, FlyControls, MapControls, TransformControls
effects/ Visual effects AnaglyphEffect, AsciiEffect, OutlineEffect
geometries/ Specialized geometry types TextGeometry, DecalGeometry, ParametricGeometry
csm/ Cascaded shadow maps CSM, CSMHelper
curves/ Additional curve types NURBSCurve, NURBSSurface
animation/ Animation utilities CCDIKSolver, AnimationClipCreator

The addon directory is structured for selective importing. Each module is a standalone ES module that imports from 'three' (the core library), so tree-shaking works correctly when you import individual addons:

import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

GLTFLoader Deep Dive

GLTFLoader is the most important addon loader — glTF is the de facto standard for web 3D content. The loader implements a plugin architecture for handling glTF extensions:

flowchart TD
    Input["glTF file<br/>(.gltf JSON or .glb binary)"] --> Parse["Parse JSON + binary chunk"]
    Parse --> Extensions["Register Extension Plugins"]

    Extensions --> DRC["DRACOLoader<br/>(mesh compression)"]
    Extensions --> KTX["KTX2Loader<br/>(texture compression)"]
    Extensions --> Mat["Material Extensions<br/>(transmission, sheen, etc.)"]

    Parse --> Build["Build Scene Graph"]
    Build --> Meshes["Create Meshes"]
    Build --> Mats["Create Materials"]
    Build --> Anim["Create AnimationClips"]
    Build --> Skins["Create Skeletons"]

    Meshes --> Scene["Complete Scene"]
    Mats --> Scene
    Anim --> Scene
    Skins --> Scene

The plugin system allows third-party extensions to hook into the parsing pipeline. For example, DRACOLoader decompresses geometry data, KTX2Loader decompresses GPU texture formats, and material extension plugins handle PBR extensions like KHR_materials_transmission and KHR_materials_sheen.

GLTFLoader's output is a result object containing scene (the root Object3D), scenes (all scenes in the file), cameras, animations (AnimationClip array), and asset metadata. The loader handles the complete glTF spec: meshes with multiple primitives (creating Groups), indexed and non-indexed geometry, sparse accessors, morph targets, skeletal animation, and the full PBR material model.

Post-Processing: Two Paradigms

Three.js currently supports two post-processing approaches, reflecting the broader architectural transition.

The legacy approach (WebGLRenderer) uses EffectComposer from the addons, which manages a chain of render passes. Each pass renders to a framebuffer, and subsequent passes read the previous result:

Scene → EffectComposer → RenderPass → BloomPass → FXAAPass → Screen

The new approach (WebGPURenderer) uses RenderPipeline (formerly PostProcessing, now renamed as of r183). It leverages the node system to express effects as TSL node graphs:

const renderPipeline = new RenderPipeline( renderer );
const scenePass = pass( scene, camera );
const bloomPass = bloom( scenePass, { strength: 1.5 } );
renderPipeline.outputNode = bloomPass;
flowchart LR
    subgraph "Legacy (WebGL)"
        EC["EffectComposer"] --> RP["RenderPass"]
        RP --> BP["BloomPass"]
        BP --> FP["FXAAPass"]
        FP --> Screen1["Canvas"]
    end

    subgraph "New (WebGPU/Node)"
        PP["RenderPipeline"] --> SP["pass(scene, camera)"]
        SP --> BN["bloom(pass, options)"]
        BN -->|"outputNode"| Screen2["Canvas"]
    end

The node-based approach is more composable — since effects are just nodes, they can be combined, branched, and conditionally applied using TSL's fluent API. The RenderPipeline class handles tone mapping and color space conversion automatically through its outputColorTransform property.

Note the deprecation at src/renderers/common/PostProcessing.js: PostProcessing is now just a wrapper that warns about the rename and extends RenderPipeline.

Controls and the Editor

The Controls base class lives in core (not addons) because it defines the interface that all control implementations share. It extends EventDispatcher and provides:

  • object: The Object3D being controlled (typically a camera)
  • domElement: The HTML element receiving input events
  • enabled: Master toggle
  • connect(element) / disconnect(): Attach/detach DOM event listeners
  • update(delta): Per-frame state update method
  • dispose(): Cleanup

OrbitControls, the most widely used control implementation, lives in the addons at examples/jsm/controls/OrbitControls.js. It implements orbit (left-click drag), zoom (scroll), and pan (right-click drag) around a target point. Other notable controls include FlyControls (first-person flight), MapControls (orbit with screen-space panning), and TransformControls (3D gizmo for translate/rotate/scale).

Three.js also ships a complete browser-based editor application in editor/. Built entirely on the library and its addons, it provides a visual scene editor with viewport rendering, object manipulation, material editing, and export. While not as polished as commercial tools, it serves as both a powerful example application and a practical authoring tool.

Testing and Contributing

The test infrastructure uses two frameworks:

QUnit unit tests are organized through test/unit/three.source.unit.js, which aggregates test suites for each source module. The structure mirrors src/ — there's a test file for each major class testing constructor behavior, method correctness, and edge cases. Run them with npm run test-unit.

Puppeteer E2E tests use headless Chrome to render examples and compare screenshots against reference images. This catches visual regressions that unit tests can't detect — shader compilation differences, sorting order bugs, or geometry generation errors. Run with npm run test-e2e or npm run test-e2e-webgpu for the WebGPU path.

flowchart TD
    PR["Pull Request"] --> Lint["npm run lint<br/>(ESLint on src/)"]
    Lint --> Unit["npm run test-unit<br/>(QUnit tests)"]
    Unit --> UnitAddons["npm run test-unit-addons<br/>(Addon tests)"]
    UnitAddons --> E2E["npm run test-e2e<br/>(Puppeteer screenshots)"]
    E2E --> TreeShake["npm run test-treeshake<br/>(Bundle size check)"]
    TreeShake --> Review["Code Review"]

Key conventions for contributors:

  • Code style: Use tabs, Three.js-specific spacing (spaces inside parentheses), and follow the eslint-config-mdcs rules
  • Tree-shaking: Avoid top-level side effects outside src/nodes/. Use /*@__PURE__*/ for module-level allocations
  • is* flags: Always add type-testing boolean flags on new classes (this.isMyClass = true)
  • IDs and UUIDs: Follow the dual-ID pattern for new classes that need identity
  • Scratch objects: Use module-level /*@__PURE__*/ temporaries instead of allocating in methods

Tip: Before contributing a new feature, check if it belongs in core or addons. Core changes require higher scrutiny and backward compatibility. Most new functionality (loaders, controls, effects, geometries) should go in examples/jsm/.

Series Recap

Across these six articles, we've traced the full architecture of Three.js from build tool to shader generation:

  1. Architecture and Navigation — Four entry points, directory structure, foundational patterns
  2. Scene Graph — Object3D transforms, parent-child tree, geometry-material contract
  3. Dual Renderer — WebGLRenderer monolith vs. the new Renderer + Backend architecture
  4. Node System and TSL — DAG-based shader construction and fluent shader authoring
  5. Math, Cameras, Lights — Linear algebra library, projection matrices, light-to-node pipeline
  6. Asset Pipeline — Loaders, addons, post-processing, controls, and contributing

Three.js is undergoing its most significant architectural transformation since its creation — the shift from a WebGL-only monolith to a multi-backend, node-based rendering system. Understanding both architectures positions you to work with the library as it is today and as it evolves into the WebGPU era.