Read OSS

数学プリミティブ、ライティング、カメラシステム

中級

前提知識

  • 第1〜2回の記事
  • 基礎的な線形代数(ベクトル、行列、内積・外積)
  • 座標空間(ローカル、ワールド、ビュー、クリップ)の理解

数学プリミティブ、ライティング、カメラシステム

Three.js には、完全な線形代数ライブラリ、高度なカラーマネジメントシステム、投影行列を計算するカメラ階層、そしてレガシーレンダラーとノードベースレンダラーの両方に対応したライトシステムが搭載されています。これらのサブシステムはレンダリングパイプラインの根幹を成しており、数学プリミティブはほぼすべてのソースファイルで使われています。カメラとライトの仕組みを理解することは、レンダリングの問題をデバッグする上でも不可欠です。この記事では各コンポーネントを詳しく見ながら、それらがどのように連携しているかを解説します。

数学ライブラリ:インプレース変更 API

Vector2Vector3Vector4Matrix3Matrix4QuaternionEulerColor といった数学クラスは、すべて一貫した API 設計方針に従っています。すべての演算がインスタンスを直接変更し、this を返すというものです。これにより、一時的なオブジェクト生成によるガベージコレクションの負荷を抑えながら、メソッドチェーンを実現しています。

約 1,263 行からなる Vector3 がこのパターンをよく示しています。

// インプレース チェーン — アロケーションなし
direction.copy( target ).sub( origin ).normalize();

// 以下と等価:
direction.copy( target );
direction.sub( origin );
direction.normalize();

イミュータブルな API では演算のたびに新しいベクトルを返すため、呼び出しごとに 3 つの Vector3 インスタンスが生成されます。60fps で動作するレンダラーで数千のオブジェクトを処理する場合、そのGC 負荷は無視できないレベルになります。

イミュータブルなセマンティクスが必要な場面では、clone() でコピーを作成するか、copy(source) で既存インスタンスに値を上書きしましょう。

const original = new Vector3( 1, 2, 3 );
const copy = original.clone();      // New Vector3(1,2,3)
const other = new Vector3();
other.copy( original );              // other is now (1,2,3)
classDiagram
    class Vector3 {
        +x: number
        +y: number
        +z: number
        +set(x, y, z): this
        +add(v): this
        +sub(v): this
        +multiplyScalar(s): this
        +normalize(): this
        +dot(v): number
        +cross(v): this
        +clone(): Vector3
        +copy(v): this
    }

    class Matrix4 {
        +elements: Float32Array[16]
        +compose(pos, quat, scale): this
        +decompose(pos, quat, scale): this
        +multiply(m): this
        +invert(): this
        +makePerspective(): this
    }

    class Quaternion {
        +x: number
        +y: number
        +z: number
        +w: number
        +setFromEuler(e): this
        +setFromAxisAngle(axis, angle): this
        +slerp(q, t): this
        +_onChange(callback)
    }

Matrix4 は約 1,314 行あり、トランスフォームの中核を担うクラスです。compose(position, quaternion, scale) は TRS(移動・回転・スケール)行列を構築し、decompose(position, quaternion, scale) は逆に各成分を取り出します。第 2 回で見たように、Object3D の updateMatrix()applyMatrix4() でも活用されています。

src/math/MathUtils.jsMathUtils 名前空間には、clamplerpsmoothstepmapLineardegToRadisPowerOfTwo などのユーティリティ関数が揃っています。また、3 行目で定義された16進数のルックアップテーブルを使い、高速に UUID を生成する generateUUID 関数も含まれています。

Tips: Three.js は WebGL/WebGPU の慣例に合わせ、列優先(column-major)の行列ストレージを採用しています。elements[12]elements[13]elements[14] が平行移動成分です。elements 配列を直接読んでトランスフォームをデバッグする際は、このメモリレイアウトを念頭に置いてください。

Color と ColorManagement

ColorManagement は、カラー演算が正しい色空間で行われるようにするための仕組みです。中心となる概念が**作業色空間(working color space)**で、デフォルトは LinearSRGBColorSpace に設定されています。

物理的に正確な結果を得るために、ライティング・ブレンディング・テクスチャサンプリングといったカラー演算はすべてリニア空間で行う必要があります。ColorManagement はその変換パイプラインを管理します。

flowchart LR
    Input["sRGB Input<br/>(textures, CSS colors)"] -->|"EOTF (gamma decode)"| Linear["Linear Working Space<br/>(all math here)"]
    Linear -->|"Lighting, blending,<br/>tone mapping"| Linear
    Linear -->|"OETF (gamma encode)"| Output["sRGB Output<br/>(canvas display)"]

convert() メソッドは色空間変換の中間表現として CIE XYZ を使用しており、ファイル冒頭(5〜15行目)に Rec.709(sRGB)原色用の 3×3 行列が事前計算されています。

const LINEAR_REC709_TO_XYZ = new Matrix3().set(
    0.4123908, 0.3575843, 0.1804808,
    0.2126390, 0.7151687, 0.0721923,
    0.0193308, 0.1191948, 0.9505322
);

ColorManagement.enabledtrue(デフォルト)の場合、16進数値(0xff0000)や CSS 文字列('red')で指定した色は、使用前に自動的に sRGB からリニア空間へ変換されます。つまり new Color(1, 0, 0) は sRGB の赤ではなく、リニアの赤(フル輝度)です。

カメラ階層と投影

カメラシステムはシンプルな継承チェーンで設計されています。Camera は Object3D を拡張し、3 つの行列を追加します。

  • matrixWorldInverse: ビュー行列。カメラのワールドトランスフォームの逆行列です。updateMatrixWorld()(112行目)をオーバーライドして自動的に計算されます。
  • projectionMatrix: パースペクティブまたは正投影のために、サブクラスが設定します。
  • projectionMatrixInverse: 逆投影行列。スクリーン座標からワールド座標への逆変換(アンプロジェクション)に使います。

また、Camera は getWorldDirection() をオーバーライドして結果を反転しています(108行目)。これはカメラが慣例としてローカル Z 軸の方向を向いているためです。

classDiagram
    class Object3D {
        +matrixWorld: Matrix4
    }

    class Camera {
        +matrixWorldInverse: Matrix4
        +projectionMatrix: Matrix4
        +projectionMatrixInverse: Matrix4
        +coordinateSystem: number
    }

    class PerspectiveCamera {
        +fov: number
        +aspect: number
        +near: number
        +far: number
        +updateProjectionMatrix()
    }

    class OrthographicCamera {
        +left: number
        +right: number
        +top: number
        +bottom: number
        +updateProjectionMatrix()
    }

    Object3D <|-- Camera
    Camera <|-- PerspectiveCamera
    Camera <|-- OrthographicCamera

PerspectiveCamerafov(垂直画角、度数法)、aspectnearfar を引数に取ります。updateProjectionMatrix() はこれらのパラメータに加え、zoomfilmGaugefilmOffset、そしてタイルレンダリング用のサブフラスタム view を考慮してパースペクティブ投影行列を計算します。

118行目にある Camera の updateMatrixWorld() には細かい配慮があります。glTF 準拠のため、ビュー行列からスケールを除外しています。カメラに非一様スケールが設定されている(稀ですが起こり得ます)場合、matrixWorldInverse の計算時にそのスケールが取り除かれます。

ライトの種類と階層

Light は Object3D を拡張し、color(Color)と intensity(number)の 2 つのプロパティを追加します。リソースのクリーンアップ用に 'dispose' イベントをディスパッチする機能も持っています。各ライト型はそれぞれ固有のプロパティを持っています。

ライト型 プロパティ シーングラフ上の挙動
AmbientLight color, intensity 位置は不要
DirectionalLight color, intensity, target 位置と target で方向を定義
PointLight color, intensity, distance, decay 位置が光源の原点
SpotLight color, intensity, distance, angle, penumbra, decay, target 位置と target でコーンを定義
HemisphereLight color, groundColor, intensity 向きから方向を決定
RectAreaLight color, intensity, width, height 位置と向きでエリアを定義
LightProbe sh (SphericalHarmonics3) 照度プローブ

DirectionalLight や SpotLight のように方向が必要なライトは、target プロパティに別の Object3D を指定します。方向ベクトルを直接保持するのではなく、ライトのワールド座標と target のワールド座標から方向を導き出す設計です。これにより、両者が通常のシーングラフのトランスフォームシステムに自然に参加できます。

classDiagram
    class Object3D
    class Light {
        +color: Color
        +intensity: number
        +isLight: boolean
    }

    class AmbientLight {
        +isAmbientLight: boolean
    }

    class DirectionalLight {
        +target: Object3D
        +shadow: DirectionalLightShadow
    }

    class PointLight {
        +distance: number
        +decay: number
    }

    class SpotLight {
        +distance: number
        +angle: number
        +penumbra: number
        +decay: number
        +target: Object3D
    }

    Object3D <|-- Light
    Light <|-- AmbientLight
    Light <|-- DirectionalLight
    Light <|-- PointLight
    Light <|-- SpotLight

ノードとしてのライト:AnalyticLightNode と LightsNode

新しいレンダラーでは、ライトは uniform 配列や #ifdef ブロックを通じてシェーディングに直接関与するのではなく、シェーダーグラフのノードとして機能します。第 4 回で触れたように、StandardNodeLibrary は各ライト型を対応するノードクラスにマッピングしています(例:PointLightPointLightNodeSpotLightSpotLightNode)。

LightsNode はすべてのライトの寄与をまとめるアグリゲーターです。出力型 'vec3' を持つ Node を拡張し、すべてのライトの拡散・鏡面反射成分を蓄積する totalDiffuseNodetotalSpecularNode を管理します。セットアップ時にはシーン内の各ライトを順に処理し、対応する AnalyticLightNode サブクラスを生成または取得して、拡散・鏡面反射の寄与を積み上げていきます。

graph TD
    LN["LightsNode"] --> DL["DirectionalLightNode"]
    LN --> PL["PointLightNode"]
    LN --> SL["SpotLightNode"]
    LN --> AL["AmbientLightNode"]

    DL --> Diff["totalDiffuseNode (vec3)"]
    PL --> Diff
    SL --> Diff
    AL --> Diff

    DL --> Spec["totalSpecularNode (vec3)"]
    PL --> Spec
    SL --> Spec

    Diff --> LC["LightingContextNode"]
    Spec --> LC
    LC --> Material["NodeMaterial output"]

DirectionalLightNodePointLightNode などの AnalyticLightNode サブクラスは、それぞれのライトの寄与計算を実装しています。方向の算出、距離減衰、コーン角度のフォールオフ(スポットライト)、シャドウ評価などがすべて TSL 式で記述されているため、WGSL または GLSL に自動的にコンパイルされます。

Tips: カスタムの光減衰や独自のフォールオフカーブを実装したい場合は、AnalyticLightNode をサブクラス化して該当メソッドをオーバーライドしましょう。ノードシステムがその結果をライティングパイプラインに組み込んでくれるため、他のライトのコードに手を加える必要はありません。

シャドウマッピングとフラスタムカリング

新しいレンダラーのシャドウマッピングは ShadowNodeShadowBaseNode が担います。ライトの視点からシーンをレンダリングしてシャドウマップを生成し、メインのライティングパスでそれをサンプリングして各フラグメントが影の中にあるかどうかを判定します。ベーシック、PCF、PCF ソフト、VSM といった異なるフィルタリング戦略が、それぞれ個別の TSL 関数として実装されています。

フラスタムカリングはレンダリングループ内で描画コマンドを発行する前に実行されます。src/math/Frustum.jsFrustum クラスは、カメラのビューボリュームを 6 つの Plane オブジェクトで表現します。シーンの投影処理中に、各オブジェクトのバウンディングスフィアがこれらの平面に対してテストされます。

flowchart TD
    Cam["Camera"] -->|"projection × view"| ProjMat["projScreenMatrix"]
    ProjMat -->|"setFromProjectionMatrix()"| Frust["Frustum (6 planes)"]

    Obj["Object3D"] --> BS["boundingSphere"]
    BS --> Test{"frustum.intersectsObject()"}
    Frust --> Test

    Test -->|"Inside"| Add["Add to RenderList"]
    Test -->|"Outside"| Skip["Skip object"]

第 2 回で見たように、Object3D に frustumCulled = false を設定したオブジェクトはこのテストを完全にスキップします。カメラ位置に関わらず常にレンダリングすべきスカイボックスなどに便利な設定です。

数学プリミティブによるトランスフォームの計算、カメラによる投影行列の生成、フラスタムによるオブジェクトのカリングがあります。さらに、ライトによるシェーダーノードの生成、シャドウマップによるオクルージョンのテストも加わります。これらのシステムが連携することで、毎フレームの描画を支えるレンダリングパイプラインのバックボーンが形成されています。

次回予告

最終回では、このレンダリングシステムにデータを供給するアセットパイプラインを取り上げます。ローダーアーキテクチャとキャッシュシステム、重要な GLTFLoader アドオン、新しい RenderPipeline クラスによるポストプロセッシング、ユーザーインタラクション用のコントロールシステム、そしてテストインフラの活用とプロジェクトへのコントリビュート方法について解説します。