Read OSS

アセットパイプライン:ローダー、アドオン、そして Three.js エコシステム

中級

前提知識

  • 第1〜3回の記事
  • glTF、FBX、OBJ などの代表的な 3D アセット形式への基本的な理解

アセットパイプライン:ローダー、アドオン、そして Three.js エコシステム

3D エンジンの価値は、読み込めるコンテンツの幅によって決まります。Three.js は Loader 基底クラスと LoadingManager を中心に構造化されたローディングシステムを提供しており、ライブラリ本体には基本的なファイル形式に対応するコアローダーが、アドオンエコシステムには 50 以上の専用ローダーが揃っています。本シリーズ最終回では、HTTP リクエストからシーングラフ構築までのアセットパイプライン全体、重要な GLTFLoader アドオン、ポストプロセッシングアーキテクチャ、コントロールシステム、そしてテストとコントリビューションの仕組みを解説します。

Loader 基底クラスと LoadingManager

Loader はすべてのローダーの抽象基底クラスです。15〜70 行目のコンストラクタでは、すべてのローダーが必要とする設定を初期化しています。

  • manager: LoadingManager インスタンス(デフォルトは DefaultLoadingManager
  • crossOrigin: クロスドメインリソース向けの CORS 設定(デフォルトは 'anonymous'
  • path: URL の前に付加するベースパス
  • resourcePath: モデルから参照されるテクスチャなど、依存リソース用の別ベースパス
  • requestHeader: 認証が必要なエンドポイント向けのカスタム HTTP ヘッダー

すべての具体的なローダーが実装しなければならない抽象メソッドは load(url, onLoad, onProgress, onError) です。また loadAsync(url) も提供されており、load() を Promise でラップしているため async/await をすっきり使えます。モダンな書き方を好む開発者にとっては嬉しい選択肢でしょう。

LoadingManager は複数の並行ロードを統括します。ロード中のアイテム総数を追跡し、onStartonLoadonProgressonError のコールバックを提供するほか、setURLModifier() によって URL を書き換える仕組みも持っています。これはデプロイ環境ごとにアセットパスを切り替えたい場合に便利です。また、HTTP レベルのレスポンスキャッシュのために Cache モジュールとも連携しています。

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

コアローダー:FileLoader から ObjectLoader まで

コアローダーは依存関係の連鎖として構成されています。

FileLoader はその土台となるローダーで、XMLHttpRequest を使って HTTP リクエストを処理します。レスポンスタイプ(text、arraybuffer、blob、json)をサポートし、LoadingManager と連携して進捗を管理し、Cache モジュールを使って重複したネットワークリクエストを防ぎます。

ImageLoader は FileLoader を間接的に利用して画像を HTMLImageElement または ImageBitmap として読み込み、CORS やデータ URL にも対応します。

TextureLoaderImageLoader をラップし、読み込んだ画像データを含む Texture オブジェクトを返します。基本的なテクスチャマッピングに最もよく使われるローダーです。

ObjectLoader は Three.js の JSON 形式をデシリアライズして、ジオメトリ、マテリアル、テクスチャ、アニメーションクリップを含む完全なシーングラフに復元します。Object3D やそのサブクラスの toJSON() メソッドとは逆の操作を担います。

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.)"]

ヒント: 複数のアセットを読み込む場合は、必ず LoadingManager を使って全体の進捗を管理しましょう。個々のローダーコールバックに頼るのではなく、new LoadingManager(onAllLoaded, onProgress, onError) でカスタムマネージャーを作成することをお勧めします。

アドオンエコシステム:examples/jsm/

第 1 回で説明したとおり、examples/jsm/three/addons として公開されている公式アドオンシステムです。Addons.js のバレルファイルは、整理されたサブディレクトリから再エクスポートを行っています。

ディレクトリ 内容 主なモジュール
loaders/ フォーマット別アセットローダー GLTFLoader, FBXLoader, DRACOLoader, KTX2Loader, OBJLoader
controls/ ユーザー操作ハンドラー OrbitControls, FlyControls, MapControls, TransformControls
effects/ ビジュアルエフェクト AnaglyphEffect, AsciiEffect, OutlineEffect
geometries/ 特殊ジオメトリ型 TextGeometry, DecalGeometry, ParametricGeometry
csm/ カスケードシャドウマップ CSM, CSMHelper
curves/ 追加カーブ型 NURBSCurve, NURBSSurface
animation/ アニメーションユーティリティ CCDIKSolver, AnimationClipCreator

アドオンディレクトリは選択的なインポートを前提に設計されています。各モジュールはコアライブラリである 'three' からインポートするスタンドアロンの ES モジュールなので、個々のアドオンを個別にインポートすればツリーシェイキングが正しく機能します。

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

GLTFLoader 詳解

GLTFLoader はアドオンローダーの中でも特に重要な存在です。glTF は Web 上での 3D コンテンツのデファクトスタンダードとなっており、このローダーは glTF 拡張を処理するためのプラグインアーキテクチャを実装しています。

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

このプラグインシステムにより、サードパーティの拡張機能がパースパイプラインにフックを差し込めます。たとえば DRACOLoader はジオメトリデータを展開し、KTX2Loader は GPU テクスチャ形式を展開します。マテリアル拡張プラグインは KHR_materials_transmissionKHR_materials_sheen といった PBR 拡張を処理します。

GLTFLoader の出力は、scene(ルートの Object3D)、scenes(ファイル内のすべてのシーン)、camerasanimations(AnimationClip の配列)、asset メタデータを含む結果オブジェクトです。複数のプリミティブを持つメッシュ(Group として生成)、インデックス付きおよびインデックスなしジオメトリ、スパースアクセサー、モーフターゲット、スケルタルアニメーション、そして完全な PBR マテリアルモデルまで、glTF 仕様を網羅的に処理します。

ポストプロセッシング:2 つのアプローチ

Three.js では現在 2 種類のポストプロセッシングアプローチをサポートしており、これはより大きなアーキテクチャ移行を反映しています。

レガシーなアプローチ(WebGLRenderer)では、アドオンの EffectComposer がレンダーパスのチェーンを管理します。各パスはフレームバッファにレンダリングし、後続のパスがその結果を読み取る仕組みです。

Scene → EffectComposer → RenderPass → BloomPass → FXAAPass → Screen

新しいアプローチ(WebGPURenderer)では、RenderPipeline(旧称 PostProcessing、r183 で改名)を使います。ノードシステムを活用し、エフェクトを TSL ノードグラフとして表現します。

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

ノードベースのアプローチはより高い合成性を持ちます。エフェクトはただのノードなので、TSL のフルーエント API を使って組み合わせたり、分岐させたり、条件付きで適用したりできます。RenderPipeline クラスは outputColorTransform プロパティを通じて、トーンマッピングと色空間変換を自動的に処理します。

src/renderers/common/PostProcessing.js には非推奨の注記があります。PostProcessing は現在、名前変更の警告を出しながら RenderPipeline を継承するラッパーに過ぎません。

コントロールとエディター

Controls 基底クラスは、すべてのコントロール実装が共有するインターフェースを定義するため、アドオンではなくコアに置かれています。EventDispatcher を拡張し、以下を提供します。

  • object: 操作対象の Object3D(通常はカメラ)
  • domElement: 入力イベントを受け取る HTML 要素
  • enabled: 有効/無効を切り替えるマスタートグル
  • connect(element) / disconnect(): DOM イベントリスナーのアタッチ/デタッチ
  • update(delta): フレームごとの状態更新メソッド
  • dispose(): クリーンアップ

最もよく使われるコントロール実装である OrbitControls は、examples/jsm/controls/OrbitControls.js のアドオンにあります。target ポイントを中心に、オービット(左クリックドラッグ)、ズーム(スクロール)、パン(右クリックドラッグ)を実装しています。他にも注目すべきコントロールとして、FlyControls(一人称視点での飛行)、MapControls(スクリーン空間パンを伴うオービット)、TransformControls(平行移動/回転/スケールの 3D ギズモ)があります。

Three.js は editor/ にブラウザベースの完全なエディターアプリケーションも同梱しています。ライブラリとそのアドオンだけで構築されており、ビューポートレンダリング、オブジェクト操作、マテリアル編集、エクスポート機能を備えたビジュアルシーンエディターです。商用ツールほどの完成度はありませんが、強力なサンプルアプリケーションであると同時に、実用的なオーサリングツールとしても機能します。

テストとコントリビューション

テスト基盤は 2 つのフレームワークで構成されています。

QUnit ユニットテストtest/unit/three.source.unit.js を通じて整理されており、各ソースモジュールのテストスイートをまとめています。構造は src/ を反映しており、主要なクラスごとにコンストラクタの挙動、メソッドの正確性、エッジケースを検証するテストファイルが存在します。npm run test-unit で実行できます。

Puppeteer E2E テストはヘッドレス Chrome を使ってサンプルをレンダリングし、スクリーンショットをリファレンス画像と比較します。シェーダーのコンパイル差異、ソート順のバグ、ジオメトリ生成のエラーなど、ユニットテストでは検出できないビジュアルリグレッションを捉えるためのものです。npm run test-e2e、または WebGPU パスの場合は npm run test-e2e-webgpu で実行できます。

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"]

コントリビューターが押さえておくべき主要な規約は以下のとおりです。

  • コードスタイル: タブを使用し、Three.js 固有のスペーシング(括弧内にスペース)に従い、eslint-config-mdcs のルールを守ること
  • ツリーシェイキング: src/nodes/ 以外ではトップレベルの副作用を避けること。モジュールレベルの割り当てには /*@__PURE__*/ を使うこと
  • is* フラグ: 新しいクラスには必ず型チェック用のブール値フラグを追加すること(this.isMyClass = true
  • ID と UUID: 識別子が必要な新しいクラスには、デュアル ID パターンに従うこと
  • スクラッチオブジェクト: メソッド内で割り当てるのではなく、モジュールレベルで /*@__PURE__*/ の一時オブジェクトを使うこと

ヒント: 新機能をコントリビュートする前に、それがコアに属するのかアドオンに属するのかを確認しましょう。コアへの変更はより厳しい審査と後方互換性が求められます。ローダー、コントロール、エフェクト、ジオメトリといった新しい機能のほとんどは examples/jsm/ に置くべきです。

シリーズのまとめ

全 6 回にわたり、ビルドシステムからシェーダー生成まで、Three.js のアーキテクチャ全体を追ってきました。

  1. アーキテクチャとナビゲーション — 4 つのエントリーポイント、ディレクトリ構造、基本パターン
  2. シーングラフ — Object3D のトランスフォーム、親子ツリー、ジオメトリとマテリアルの契約
  3. デュアルレンダラー — WebGLRenderer モノリスと新しい Renderer + Backend アーキテクチャ
  4. ノードシステムと TSL — DAG ベースのシェーダー構築とフルーエントなシェーダーオーサリング
  5. 数学、カメラ、ライト — 線形代数ライブラリ、プロジェクション行列、ライトからノードへのパイプライン
  6. アセットパイプライン — ローダー、アドオン、ポストプロセッシング、コントロール、コントリビューション

Three.js は現在、その誕生以来最も大きなアーキテクチャ変革の途上にあります。WebGL 専用のモノリスから、マルチバックエンドのノードベースレンダリングシステムへの移行です。両方のアーキテクチャを理解しておけば、現在のライブラリはもちろん、WebGPU 時代へと進化していく将来の姿にも対応できるでしょう。