Read OSS

シーングラフ:Object3D、トランスフォーム、そしてジオメトリとマテリアルの契約

中級

前提知識

  • 線形代数の基礎(ベクトル、行列、クォータニオン)
  • 第1回:アーキテクチャとナビゲーションガイド

シーングラフ:Object3D、トランスフォーム、そしてジオメトリとマテリアルの契約

Three.js のアプリケーションは、突き詰めると Object3D ノードのツリーに過ぎません。カメラはシーンを見つめ、シーンはメッシュを含み、メッシュはジオメトリとマテリアルを持ち、レンダラーはそのツリーを走査してピクセルを生成します。Object3D がトランスフォームをどう管理するか、ツリー全体に行列の更新がどう伝播するか、そして Mesh においてジオメトリとマテリアルの契約がどう機能するかを理解することは、「チュートリアルをコピーするだけ」を卒業して Three.js を使いこなすうえで欠かせません。

Object3D:万能なシーンノード

Object3D は src/core/Object3D.js で定義されており、第1回で取り上げた pub/sub システムを持つ EventDispatcher を継承しています。コンストラクタでは、自動インクリメントの数値 iduuid によってオブジェクトの同一性を確立し、親子関係のフィールド(parentchildren)を用意したうえで、最も重要なトランスフォームプロパティを初期化します。

トランスフォームの状態は4つのプロパティで管理されます。position(Vector3)、rotation(Euler)、quaternion(Quaternion)、scale(Vector3)です。これらは 160〜226行目Object.defineProperties によって紐付けられており、プロパティ自体の置き換えはできませんが値は変更可能です。さらに2つの行列も定義されています。modelViewMatrix(レンダラーが計算するビュー空間のトランスフォーム)と、法線の正しい変換に使う normalMatrix です。

Euler ↔ Quaternion の同期

これはコードベースの中でも特に巧妙な仕組みのひとつです。Three.js は rotation(Euler 角)と quaternion の両方をプロパティとして公開しており、_onChange コールバックを通じて双方向に同期が保たれます。

// src/core/Object3D.js, lines 145-158
function onRotationChange() {
    quaternion.setFromEuler( rotation, false );
}

function onQuaternionChange() {
    rotation.setFromQuaternion( quaternion, undefined, false );
}

rotation._onChange( onRotationChange );
quaternion._onChange( onQuaternionChange );

各変換で渡している false は、相手側の変更コールバックを抑制するためのフラグです。これにより、無限ループに陥ることなく同期が成立します。たとえば object.rotation.y = Math.PI とセットすると、Euler の _onChange が発火してクォータニオンが静かに更新されます。逆に object.quaternion.setFromAxisAngle(axis, angle) を呼べば、クォータニオンの _onChange が発火して Euler が静かに更新されます。

sequenceDiagram
    participant User
    participant Euler as rotation (Euler)
    participant Quaternion as quaternion

    User->>Euler: rotation.y = π
    Euler->>Euler: _onChange fires
    Euler->>Quaternion: setFromEuler(rotation, false)
    Note right of Quaternion: false = don't fire _onChange

    User->>Quaternion: quaternion.setFromAxisAngle(...)
    Quaternion->>Quaternion: _onChange fires
    Quaternion->>Euler: setFromQuaternion(quat, order, false)
    Note right of Euler: false = don't fire _onChange

ヒント: 回転をアニメーションさせたい場合は、クォータニオンを直接使いましょう。Euler 角はジンバルロックの問題を抱えており、補間時に意図しない挙動が生じることがあります。Three.js は内部的に、どちらのプロパティをセットしたかに関わらず、行列の合成にはクォータニオンを使っています。

トランスフォーム行列のパイプライン

Object3D は matrix(ローカルトランスフォーム)と matrixWorld(ワールドトランスフォーム)の2つの行列を保持しています。これらのライフサイクルを管理する3つのメソッドがあり、それぞれの役割と関係性を理解することが重要です。

updateMatrix()1133行目)は、position・quaternion・scale から matrix.compose() でローカル行列を計算します。その後、オプショナルなピボットポイントの調整を適用し、matrixWorldNeedsUpdate = true にフラグを立てます。

updateMatrixWorld(force)1165行目)は、レンダラーから呼び出される中心的なメソッドです。matrixAutoUpdate が true であれば最初に updateMatrix() を呼んでからワールド行列を合成します。ルートオブジェクトの場合は matrixWorldmatrix をそのままコピーし、子オブジェクトの場合は parent.matrixWorld × matrix の積になります。そして重要なのは、すべての子に対して再帰的に処理が走る点です——これがトップダウンの伝播であり、すべての子孫のワールド行列が最新の状態に保たれる仕組みです。

updateWorldMatrix(updateParents, updateChildren)1212行目)は、より細かな制御を可能にします。サブツリー全体に手を入れることなく特定のノードのワールド行列だけを更新したり、先に祖先側を上向きに更新したりできます。レンダーループの外でオブジェクトのワールド座標を取得したい場合に重宝するメソッドです。

flowchart TD
    A["position, quaternion, scale"] -->|"compose()"| B["matrix (local)"]
    B -->|"parent.matrixWorld × matrix"| C["matrixWorld (world)"]

    subgraph "updateMatrixWorld(force)"
        D["For each child:"] --> E["child.updateMatrixWorld(force)"]
    end

    C --> D

    style A fill:#e8f4f8
    style B fill:#fff3cd
    style C fill:#d4edda

親子ツリーの操作

add() メソッド(746行目)は、いくつかの安全チェックを経て子オブジェクトをアタッチします。自身を自分の親にしようとするケースの防止、isObject3D による型チェック、そして removeFromParent() による以前の親からの自動デタッチがその内容です。アタッチが成功すると、子には 'added' イベントが、親には 'childadded' イベントがそれぞれ発火します。

remove() メソッド(798行目)はその対称となるメソッドで、'removed''childremoved' イベントを発火します。どちらのメソッドも可変長引数を受け取るため、複数オブジェクトの一括操作が可能です。

ツリーの走査には、用途に応じた3つのメソッドが用意されています。

メソッド 動作
traverse(callback) 深さ優先ですべての子孫を訪問する
traverseVisible(callback) 同じく深さ優先だが、visible === false のオブジェクトはスキップする
traverseAncestors(callback) parent チェーンをルートまで遡る

レンダラーはシーンの投影処理に traverseVisible を使い、Raycaster は交差テストに traverse を使っています。

graph TD
    Scene["Scene"] --> Camera["Camera"]
    Scene --> Group["Group"]
    Scene --> Light["DirectionalLight"]
    Group --> MeshA["Mesh A"]
    Group --> MeshB["Mesh B"]
    MeshA --> ChildMesh["Child Mesh"]

    style Scene fill:#d4edda
    style Group fill:#e8f4f8
    style MeshA fill:#fff3cd
    style MeshB fill:#fff3cd
    style ChildMesh fill:#fff3cd

BufferGeometry と BufferAttribute

BufferGeometry は EventDispatcher を継承しています(Object3D ではありません——ジオメトリは空間上に位置を持たないためです)。Object3D と同じ ID + UUID のパターンに従い、頂点データを名前付きの BufferAttribute オブジェクトとして管理します。

データモデルは setAttribute(name, attribute)getAttribute(name) でアクセスする辞書形式になっています。標準的な属性名は 'position''normal''uv''color''tangent' ですが、ノードマテリアルやカスタムシェーダー向けにカスタム属性も定義できます。各 BufferAttribute は型付き配列(Float32Array など)と、1頂点分のデータが何要素で構成されるかを示す itemSize をラップしたものです。

BufferGeometry はさらに次のものも管理します。

  • インデックスバッファ:共有頂点を使ったインデックス描画のためのオプショナルな BufferAttribute
  • グループ:異なるマテリアルを適用できるジオメトリ内のサブレンジ
  • バウンディングボリュームboundingBox(Box3)と boundingSphere(Sphere)——必要時に computeBoundingBox()computeBoundingSphere() で計算される

BufferGeometry.js の先頭にはモジュールレベルのスクラッチオブジェクト(_m1_obj_offset_box_vector)が定義されています。第1回で取り上げたオブジェクトプーリングのパターンが、applyMatrix4()computeBoundingSphere() といったメソッドでもそのまま活用されているわけです。

Three.js には src/geometries/ 以下に22種類の手続き型ジオメトリが用意されています。BoxGeometry、SphereGeometry、CylinderGeometry などがあり、いずれも BufferGeometry を継承して頂点データをあらかじめ計算した状態で提供します。

Material 基底クラスとプロパティ駆動のアプローチ

Material は、すべてのサーフェス外観定義のための抽象基底クラスです。BufferGeometry と同様に EventDispatcher を継承し、ID + UUID のパターンを踏襲しています。

Material が採るのは純粋なプロパティ駆動アプローチです。状態変化を起こすためのセッターメソッドではなく、blendingsidedepthTesttransparentopacityvisible といったプロパティを直接セットするだけで済みます。レンダラーはレンダーループ中にこれらのプロパティを読み取り、GPU の状態を適切に設定します。唯一の「トリガー」的な仕組みが needsUpdate で、これを true にするとレンダラーはそのマテリアルのシェーダープログラムを強制的に再コンパイルします。

基底クラスにはブレンドモード、デプステスト、ステンシル操作、ポリゴンオフセット、アルファテストの閾値など、30を超えるプロパティが定義されています。MeshStandardMaterial のような具体的なサブクラスでは、PBR プロパティ(color、roughness、metalness、マップ類)が追加され、新しい NodeMaterial 系ではプログラマブルなシェーダーノードが加わります。

ヒント: material.transparent = true を設定しただけでは、オブジェクトは透明になりません。material.opacity < 1.0 にするか、アルファチャンネルを持つテクスチャを指定する必要があります。transparent フラグが制御するのはソート順であり、透明オブジェクトは不透明オブジェクトの後に奥から手前の順でレンダリングされます。

Mesh、InstancedMesh、BatchedMesh

Mesh は最も基本的なレンダリング対象オブジェクトです。Object3D を継承し、geometrymaterial という2つのプロパティを追加します。このジオメトリとマテリアルの組み合わせがレンダリングの契約です——レンダラーはシーン走査中に Mesh を見つけると、ジオメトリから頂点データを、マテリアルから外観とシェーダープログラムを取得して描画できることを知っています。

classDiagram
    class Object3D {
        +position: Vector3
        +quaternion: Quaternion
        +scale: Vector3
        +matrixWorld: Matrix4
    }

    class Mesh {
        +geometry: BufferGeometry
        +material: Material
        +isMesh: boolean
        +raycast()
    }

    class InstancedMesh {
        +instanceMatrix: InstancedBufferAttribute
        +instanceColor: InstancedBufferAttribute
        +count: number
    }

    class BatchedMesh {
        +addGeometry()
        +addInstance()
        +setMatrixAt()
    }

    Object3D <|-- Mesh
    Mesh <|-- InstancedMesh
    Mesh <|-- BatchedMesh

Mesh は raycast() も実装しており、Raycaster の交差テストシステムと連携できます。このメソッドはまずバウンディングスフィアをチェックして高速な棄却判定を行い、その後に個々の三角形をレイに対してテストします。

InstancedMesh は GPU のハードウェアインスタンシングを活用します。単一のジオメトリとマテリアル、各インスタンスの 4×4 行列を格納した instanceMatrix、そしてオプションの instanceColor を渡すと、GPU は1回のドローコールですべてのインスタンスを描画します。森や群衆、パーティクルシステムなど大量の同一オブジェクトを描画する場面に最適です。

BatchedMesh はドローコール削減に向けた別のアプローチを取ります。複数の異なるジオメトリを単一の頂点バッファにまとめ、インスタンスとジオメトリのマッピングを内部で管理します。InstancedMesh(全インスタンスが同一ジオメトリを共有する必要がある)より柔軟ですが、セットアップは複雑になります。

Scene と Raycaster

Scene は165行と意外なほどコンパクトです。Object3D を継承し、追加するのはわずかなプロパティのみです。background(スカイボックス用の Color または Texture)、environment(IBL 用テクスチャ)、fog(Fog または FogExp2)、overrideMaterial、そして背景・環境のぼかし量・強度・回転の制御プロパティです。Scene にレンダリングロジックは一切なく、純粋なデータコンテナとして機能しています。

Raycaster はシーングラフに対する交差テストを担います。コアメソッドの intersectObjects() はシーングラフを走査し、各オブジェクトの raycast() メソッドを呼び出します。これはビジターパターンの典型的な実装です——Raycaster がレイを提供し、各オブジェクト型が自身の交差ロジックを実装します。Mesh は三角形、Sprite は平面、Line は線分、Points は各点を囲む球を対象にテストを行います。

flowchart LR
    RC["Raycaster"] -->|"intersectObjects()"| Scene["Scene.traverse()"]
    Scene --> M1["Mesh.raycast()"]
    Scene --> M2["Sprite.raycast()"]
    Scene --> M3["Line.raycast()"]
    M1 -->|"Hit?"| Results["Sorted Intersections[]"]
    M2 -->|"Hit?"| Results
    M3 -->|"Hit?"| Results

結果の配列は距離でソートされており、各交差情報には distancepoint(ワールド空間のヒット座標)、face(ヒットした三角形)、faceIndexobject(交差した Object3D)、そしてテクスチャ座標の参照に使う uv/uv1 が含まれます。

次回に向けて

ここまで、あらゆる Three.js アプリケーションが操作するデータ構造を一通り見てきました。Object3D のシーンツリー、ジオメトリとマテリアルの契約、そして階層を通じたトランスフォームの伝播です。次回は、レンダラーがこのデータを実際にどう描画するかに踏み込みます。レガシーなモノリシック WebGLRenderer から、WebGPU 時代を支える新しいモジュラーな Renderer + Backend システムまで、デュアルレンダラーアーキテクチャの全貌を探っていきましょう。