Read OSS

Three.js の内部構造:アーキテクチャ概要とコードナビゲーションガイド

中級

前提知識

  • JavaScript の基礎知識と ES module の構文
  • npm パッケージ構造への理解

Three.js の内部構造:アーキテクチャ概要とコードナビゲーションガイド

Three.js は Web 向け 3D グラフィックスライブラリの中でも最も広く使われており、10 年以上の歴史と数百名のコントリビューターを誇ります。しかし、400 を超えるソースファイルと数千のエクスポートを持つコードベースを理解しようとすると、どこから手を付ければよいか迷ってしまいます。この記事では、旅を始める前に地図を渡します。ビルドの仕組み、ファイルの在処、そしてコードベース全体に繰り返し現れる設計パターンを解説します。

本記事はコミット 582a03b74d843d2c99082aa84589a228b480d262(バージョン 0.183.1)を対象としています。参照するすべての内容はこのスナップショットに固定されています。

マルチエントリーポイントシステム

Three.js は単一のモノリシックな bundle としては提供されていません。用途に応じた 4 つの異なるエントリーポイントが用意されており、それぞれは階層的な関係を持ちます。上位のエントリーポイントは、下位のエントリーポイントのすべてを再エクスポートする仕組みになっています。

flowchart TB
    Core["Three.Core.js<br/>Renderer-agnostic types<br/>(~165 re-exports)"]
    Classic["Three.js<br/>Core + WebGLRenderer<br/>(+8 WebGL exports)"]
    WebGPU["Three.WebGPU.js<br/>Core + Node system + WebGPU/WebGL backends<br/>(+35 exports)"]
    TSL["Three.TSL.js<br/>Standalone TSL re-exports<br/>(600+ symbols)"]

    Core --> Classic
    Core --> WebGPU
    WebGPU --> TSL

    style Core fill:#e8f4f8
    style Classic fill:#fff3cd
    style WebGPU fill:#d4edda
    style TSL fill:#f8d7da

src/Three.js にある classic エントリーポイントは非常にシンプルです。Core のすべてを再エクスポートし、WebGLRendererShaderLibUniformsLib といった WebGL 固有のユーティリティをいくつか追加しているだけです。

src/Three.Core.js はライブラリの核となるエントリーポイントです。シーン、オブジェクト、マテリアル、ジオメトリ、カメラ、ライト、ローダー、数学、アニメーション、オーディオ、ヘルパー、エクストラにわたる約 165 の型を再エクスポートしており、renderer を除くすべてが揃っています。

src/Three.WebGPU.js は新しい renderer アーキテクチャを追加したエントリーポイントです。WebGPURendererWebGPUBackend、フォールバック用の WebGLBackend、ノードマテリアルシステム全体、そして TSL(Three Shading Language)が含まれています。これが将来の標準となるエントリーポイントです。

src/Three.TSL.js は、600 を超える TSL シンボルを再エクスポートするスタンドアロンのファイルです。WebGPU bundle 全体を引き込むことなく import { float, vec3, mul } from 'three/tsl' という形でインポートできるように、独立したファイルとして用意されています。

package.json のエクスポートマップ

package.jsonexports フィールド は、import 指定子をビルド成果物にマッピングするルーティングテーブルです。

import 指定子 解決先
import * from 'three' build/three.module.js
require('three') build/three.cjs
import * from 'three/webgpu' build/three.webgpu.js
import * from 'three/tsl' build/three.tsl.js
import * from 'three/addons' examples/jsm/Addons.js
import * from 'three/addons/*' examples/jsm/*
import * from 'three/src/*' src/* (direct source access)

ヒント: 新しいプロジェクトを始める際は、エントリーポイントとして 'three/webgpu' を選びましょう。WebGL 2 フォールバックが内蔵されているため、ブラウザ互換性を犠牲にすることなくモダンなノードベースのマテリアルシステムを利用できます。

package.json#L25-L27sideEffects フィールドにも注目してください。src/nodes/**/* のみがサイドエフェクトありとして指定されており、それ以外はすべて tree-shake 可能であることを bundler に伝えています。ライブラリの規模を考えると、これは非常に重要な設定です。

ディレクトリ構造とモジュール構成

src/ ディレクトリはシンプルな機能分割に沿った構成になっています。各サブディレクトリは概念的なドメインに対応しています。

ディレクトリ 役割 主要な型
core/ 基底クラスとデータ構造 Object3D, BufferGeometry, BufferAttribute, EventDispatcher, Raycaster
math/ 線形代数のプリミティブ Vector2/3/4, Matrix3/4, Quaternion, Euler, Color, Frustum
scenes/ シーンコンテナ Scene, Fog, FogExp2
cameras/ 視野投影 Camera, PerspectiveCamera, OrthographicCamera
objects/ 描画可能なシーンオブジェクト Mesh, InstancedMesh, BatchedMesh, Line, Points, Sprite
materials/ サーフェスの外観 Material, MeshStandardMaterial, NodeMaterial (in materials/nodes/)
geometries/ プロシージャルシェイプ BoxGeometry, SphereGeometry, etc. (22 types)
lights/ 光源 Light, DirectionalLight, PointLight, SpotLight
loaders/ アセットの読み込み Loader, FileLoader, TextureLoader, ObjectLoader
textures/ 画像データのラッパー Texture, DataTexture, CubeTexture
animation/ キーフレームアニメーションシステム AnimationMixer, AnimationClip, KeyframeTrack
renderers/ GPU レンダリングバックエンド WebGLRenderer, Renderer, WebGPUBackend, WebGLBackend
nodes/ ノードベースのシェーダーグラフシステム Node, NodeBuilder, TSL, MaterialNode
extras/ ユーティリティとヘルパー Controls, Curves, PMREMGenerator

src/ のルートにある 2 つのファイルは、共通インフラとして機能します。src/constants.js はライブラリ全体のすべての enum 定数を定義しています——カリングモードやブレンディングタイプから、テクスチャフォーマットや色空間まで網羅されています。このファイルの先頭にある REVISION 文字列(このスナップショットでは '184dev')は、ビルド時に各出力ファイルに埋め込まれます。

graph TD
    subgraph "src/"
        Constants["constants.js"]
        Utils["utils.js"]
        subgraph "core/"
            ED[EventDispatcher]
            O3D[Object3D]
            BG[BufferGeometry]
        end
        subgraph "math/"
            V3[Vector3]
            M4[Matrix4]
            Q[Quaternion]
        end
        subgraph "renderers/"
            WGL[WebGLRenderer]
            R[Renderer]
            BE[Backend]
        end
        subgraph "nodes/"
            N[Node]
            NB[NodeBuilder]
            TSL[TSLCore]
        end
    end

    Constants --> O3D
    Constants --> WGL
    Utils --> O3D
    ED --> O3D
    ED --> BG
    ED --> N
    V3 --> O3D
    M4 --> O3D
    Q --> O3D

EventDispatcher:すべての基底クラス

Three.js の主要クラスのほぼすべては EventDispatcher を継承しています——Object3D、BufferGeometry、Material、Texture、Node がいずれもこれを拡張しています。src/core/EventDispatcher.js の実装はわずか 106 行で、addEventListenerhasEventListenerremoveEventListenerdispatchEvent を提供しています。

ここでの重要な設計上の選択は遅延初期化です。_listeners マップは最初の addEventListener 呼び出し時(33 行目)に初めて生成されます。多くのオブジェクトはリスナーをひとつも持たないまま生成されるため、コンストラクタのたびに空のオブジェクトを確保せずに済みます。数千のジオメトリを持つシーンでは、この差は無視できません。

classDiagram
    class EventDispatcher {
        +addEventListener(type, listener)
        +hasEventListener(type, listener)
        +removeEventListener(type, listener)
        +dispatchEvent(event)
        -_listeners: Object
    }

    class Object3D {
        +isObject3D: boolean
        +id: number
        +uuid: string
    }

    class BufferGeometry {
        +isBufferGeometry: boolean
    }

    class Material {
        +isMaterial: boolean
    }

    class Node {
        +isNode: boolean
    }

    EventDispatcher <|-- Object3D
    EventDispatcher <|-- BufferGeometry
    EventDispatcher <|-- Material
    EventDispatcher <|-- Node

もうひとつ見落としがちな点があります。dispatchEvent はイテレーションの前にリスナー配列をコピーしています(114 行目)。これにより、イベント発火中にリスナーが追加・削除されても、コールバックのスキップや重複実行が起きません——反復処理の安全性を確保するための古典的なパターンです。

コードベースに貫かれる設計パターン

次の 3 つのパターンはコードベース全体に一貫して登場するため、すぐに見分けられるようにしておきましょう。

型判定のための Boolean is* フラグ

Three.js では instanceof の代わりに、isObject3DisMeshisBufferGeometry といった boolean フラグで型を判定します。src/core/Object3D.js#L80this.isObject3D = true がその典型例です。この設計により、複数バージョンの Three.js が同時に読み込まれたり、オブジェクトが iframe をまたいだりする場合のクロスレルム問題を回避できます。また、ホットパスでの実行速度もわずかに向上します。

自動インクリメント ID と UUID

Object3D、BufferGeometry、Material のすべてには、モジュールスコープのカウンターによる数値の id と、文字列の uuid の両方が付与されます。src/core/Object3D.js#L11-L89 にはその両方のパターンが示されています。_object3DId はクロージャスコープのカウンター、src/math/MathUtils.js#L17-L33generateUUID() は事前計算済みの 16 進数ルックアップテーブルを使って v4 UUID を高速に生成します。数値 ID はランタイムでの高速なソートと比較に、UUID はシリアライズとアセット識別に使われます。

モジュールレベルの /*@__PURE__*/ スクラッチオブジェクト

Object3D.js、Mesh.js など数十のファイルの先頭には、次のような宣言が見られます。

const _v1 = /*@__PURE__*/ new Vector3();
const _q1 = /*@__PURE__*/ new Quaternion();
const _m1 = /*@__PURE__*/ new Matrix4();

これらはメソッド呼び出しをまたいで再利用されるプールされたスクラッチオブジェクトです。タイトなループ内で一時オブジェクトをアロケートするコストを避けるための工夫です。/*@__PURE__*/ アノテーションは、モジュールが未使用の場合にこれらのアロケーションを省略してよいと tree-shaking ツールに伝えます。典型的な例は src/core/Object3D.js#L13-L24 で確認できます。

ヒント: Three.js にコードを追加する際は、フレームごとに実行されるメソッド内で new Vector3() を直接アロケートしないでください。代わりに、アンダースコアをプレフィックスとしたモジュールレベルのスクラッチオブジェクトを使いましょう。

ビルドシステムと GLSL の最小化

ビルドは Rollup によって駆動されており、設定ファイルは utils/build/rollup.config.js にあります。このファイルには 7 つのビルド設定が定義されており、通常版と最小化版の ESM bundle、そして単一の CJS bundle が生成されます。

ビルドを支える 2 つのカスタム plugin があります。

glsl()4〜36 行目)は .glsl.js で終わるファイルを対象とします。/* glsl */`...` でラップされたタグ付きテンプレートリテラルを検索し、コメントの除去と空白の削除によって内部の GLSL を最小化します。これにより、開発中はコメント付きの読みやすい GLSL としてシェーダーソースを管理しつつ、最小化された状態で配布できます。

header()38〜61 行目)はすべての出力チャンクに MIT ライセンスバナーを付加します。

flowchart LR
    A["Source Entry<br/>src/Three.*.js"] --> B["glsl() Plugin<br/>Minify .glsl.js"]
    B --> C["Rollup Tree-shake<br/>& Bundle"]
    C --> D["header() Plugin<br/>Add license"]
    D --> E["build/*.js<br/>ESM Output"]
    C --> F["terser() Plugin<br/>(minified builds)"]
    F --> D

TSL ビルドには注目すべき点があります。122 行目three/webgpuexternal として宣言されており、TSL bundle は WebGPU エントリーを bundle に含めず、peer dependency として扱います。これにより three.tsl.js は再エクスポートだけの小さなファイルに保たれます。

コアとアドオン:examples/jsm の位置付け

examples/jsm/ ディレクトリは Three.js の公式アドオンエコシステムで、three/addons という import パスで公開されています。"examples" という名称からサンプルコードを連想するかもしれませんが、このディレクトリにはプロダクション品質のコードが揃っています。GLTFLoader や DRACOLoader といったローダー、OrbitControls などのコントロール、ポストプロセスエフェクト、特殊なジオメトリなどが含まれます。

examples/jsm/Addons.js のバレルファイルはすべてを一括で再エクスポートしていますが、tree-shaking のためには個別にインポートするのがベストプラクティスです。

// ✅ Good — tree-shakeable
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// ❌ Bad — pulls in everything
import { OrbitControls } from 'three/addons';

コアとアドオンの分離には明確な原則があります。コアには renderer が必要とするもの(シーングラフ、マテリアル、ジオメトリ、カメラ、ライト、数学、組み込みフォーマット用ローダー)を、アドオンにはユーザーが必要とするもの(フォーマット固有のローダー、インタラクションコントロール、視覚エフェクト、特殊なジオメトリ)を置くという考え方です。

flowchart TB
    subgraph "Core (src/)"
        direction TB
        SceneGraph["Scene Graph<br/>Object3D, Scene, Camera"]
        Rendering["Renderers<br/>WebGLRenderer, WebGPURenderer"]
        DataTypes["Data Types<br/>Materials, Geometry, Textures"]
        Math["Math<br/>Vector3, Matrix4, Color"]
    end

    subgraph "Addons (examples/jsm/)"
        direction TB
        Loaders["Loaders<br/>GLTFLoader, FBXLoader, DRACOLoader"]
        Controls["Controls<br/>OrbitControls, FlyControls"]
        Effects["Effects<br/>EffectComposer, Bloom, SSAO"]
        Extra["Extras<br/>CSM, NURBS, CSG"]
    end

    Core --> Addons

次のステップ

コードベースの全体像を掴んだところで、次はいよいよシーングラフへと踏み込みます。あらゆる Three.js アプリケーションの中核をなすデータ構造です。次の記事では、Object3D における Euler/Quaternion 回転の双方向同期、マトリックス更新の伝播フロー、そして BufferGeometry と Material が Mesh の中でどのように組み合わさってレンダリングの基本契約を形成するかを詳しく見ていきます。