Read OSS

デュアルレンダラーアーキテクチャ:WebGLRenderer と新しいバックエンドシステム

上級

前提知識

  • 第1・2回の記事
  • グラフィックスパイプラインの基本的な理解(頂点シェーダー/フラグメントシェーダー、レンダーターゲット、デプスバッファー)
  • WebGL または WebGPU API の基礎知識

デュアルレンダラーアーキテクチャ:WebGLRenderer と新しいバックエンドシステム

Three.js は今、歴史的なアーキテクチャの転換期を迎えています。10年以上にわたって、WebGLRenderer は30を超える内部ヘルパーモジュールを通じて WebGL の状態を直接管理する、単一のモノリシッククラスでした。現在は新しい3層構造が導入され、関心事が明確に分離されています。API に依存しない Renderer オーケストレーター、抽象 Backend インターフェース、そして WebGPU と WebGL 2 向けの具体的なバックエンド実装という構成です。レガシーレンダラーは今でも広く使われており、新システムはすべての活発な開発の場となっています。両方のアーキテクチャをしっかり理解しておくことが重要です。

アーキテクチャの分岐:モノリシックからモジュール式へ

WebGLRenderer は30を超える WebGL 専用の内部モジュールを直接インポートしています。代表的なものとしては以下があります。

WebGLAttributesWebGLBindingStatesWebGLBufferRendererWebGLCapabilitiesWebGLClippingWebGLExtensions などです。WebGLGeometriesWebGLInfoWebGLMorphtargetsWebGLObjectsWebGLProgramsWebGLProperties もあります。さらに WebGLRenderListsWebGLRenderStatesWebGLShadowMapWebGLStateWebGLTexturesWebGLUniforms なども含まれます。

これらはすべて gl.* メソッドを直接呼び出しています。この密結合のために、WebGPU サポートの追加は完全な書き直しなしには不可能でした。

新しいアーキテクチャはこの結合を逆転させます。

flowchart TB
    subgraph "Legacy Architecture"
        WGL["WebGLRenderer<br/>(3,600+ lines)"]
        WGL --> WGLA["WebGLAttributes"]
        WGL --> WGLB["WebGLBindingStates"]
        WGL --> WGLP["WebGLPrograms"]
        WGL --> WGLS["WebGLState"]
        WGL --> WGLT["WebGLTextures"]
        WGL --> WGLMore["...25+ more modules"]
    end

    subgraph "New Architecture"
        R["Renderer<br/>(3,680+ lines)<br/>API-agnostic"]
        R --> RL["RenderLists"]
        R --> RO["RenderObjects"]
        R --> PP["Pipelines"]
        R --> TX["Textures"]
        R --> NM["NodeManager"]
        R --> More["...10+ more"]
        R -->|"delegates GPU calls"| BE["Backend (abstract)"]
        BE --> WGPU["WebGPUBackend<br/>(~2,600 lines)"]
        BE --> WGLBE["WebGLBackend<br/>(~2,800 lines)"]
    end

重要なのは、Renderer がレンダリングの「ロジック」全体(シーンのトラバーサル、ソート、カリング、パイプライン管理)を担い、Backend が「GPU API 呼び出し」(バッファーの作成、シェーダーのコンパイル、描画コマンドの発行)を担うという役割分担です。この分離によって、新しいバックエンド(たとえば WebNN や将来の API 向け)を追加するには Backend インターフェースを実装するだけで済み、レンダーループ全体を書き直す必要はありません。

新しい Renderer 基底クラス

Renderer は3,680行を超えるオーケストレーターです。83〜107行目のコンストラクターは Backend インスタンスとパラメーターオブジェクトを受け取り、複数の管理コンポーネントを組み立てます。

コンポーネント 役割
RenderLists 不透明/透明オブジェクト向けのソート済みレンダーリストを管理する
RenderObjects RenderObject インスタンスをキャッシュして提供する
Pipelines GPU パイプライン状態(シェーダープログラム+レンダー状態)を管理する
Bindings ユニフォームバッファーとバインドグループを管理する
Attributes GPU 属性バッファーを管理する
Geometries ジオメトリの状態とワイヤーフレーム生成を管理する
Textures GPU テクスチャの作成と更新を管理する
NodeManager ノードグラフのコンパイルとキャッシュを管理する
Background シーン背景のレンダリングを処理する
Lighting ライトノードのセットアップを管理する
RenderContexts レンダーパス用のレンダーコンテキストオブジェクトを提供する
RenderBundles コマンドバンドルの記録を管理する
Animation アニメーション/レンダーループを管理する
XRManager WebXR セッションの統合を管理する

Renderer.js の冒頭にあるインポートを見ると、その規模がよくわかります。common/ ディレクトリからの20以上のインポートに加え、内部シェーダー構築用の TSL ノードインポートが含まれています。

ヒント: 新しいシステムでレンダリングの問題をデバッグするときは、まず問題領域に対応する管理コンポーネントから調べましょう。テクスチャの問題なら Textures、シェーダーのコンパイルエラーなら NodeManagerPipelines、描画順の不具合なら RenderList を確認するのが近道です。

Backend:抽象 GPU インターフェース

Backend は GPU 操作のコントラクトを定義します。コンストラクターはシンプルで、パラメーターを保存し、オブジェクトごとの GPU データ用に WeakMap を作成し、初期化時に設定されるレンダラーと DOM 要素への参照を保持するだけです。

バックエンドが実装しなければならない抽象メソッドは、GPU ライフサイクル全体をカバーしています。

flowchart LR
    subgraph "Backend Interface"
        Init["init()"]
        Create["createTexture()<br/>createAttribute()<br/>createBindings()"]
        Update["updateTexture()<br/>updateAttribute()<br/>updateBindings()"]
        Render["beginRender()<br/>draw()<br/>endRender()"]
        Compute["beginCompute()<br/>compute()<br/>endCompute()"]
        Destroy["destroyTexture()<br/>destroyAttribute()"]
    end

    Init --> Create --> Update --> Render --> Destroy
    Create --> Compute

WebGPUBackend(約2,600行)はネイティブ WebGPU API(GPUDeviceGPUCommandEncoderGPURenderPassEncoder)を使ってこれらのメソッドを実装しています。WebGLBackend(約2,800行)は WebGL 2 の呼び出し(gl.bindTexturegl.drawElements など)を使って同じインターフェースを実装しており、WebGPU に対応していないブラウザ向けのフォールバックとして機能します。

data WeakMap は設計上の重要なポイントです。Three.js のオブジェクトにバックエンド固有のプロパティを直接付与すると抽象が漏れてしまいますが、この WeakMap を使うことで各バックエンドは GPU ハンドル(WebGL テクスチャ、WebGPU バッファー、パイプラインオブジェクト)をソースオブジェクトをキーとして保存できます。Three.js のオブジェクトがガベージコレクションされると、対応する GPU データも自動的に回収可能になります。

WebGPURenderer:107行のオーケストレーター

WebGPURenderer は驚くほどシンプルで、JSDoc を含めてもわずか107行です。53〜103行目のコンストラクターは3つのことだけを行います。

  1. バックエンドの選択parameters.forceWebGL が true の場合は WebGLBackend を直接使用し、そうでなければ WebGPUBackend を作成してフォールバック関数をセットアップします。
parameters.getFallback = () => {
    warn( 'WebGPURenderer: WebGPU is not available, running under WebGL2 backend.' );
    return new WebGLBackend( parameters );
};
  1. バックエンドを Renderer に渡すsuper( backend, parameters )

  2. StandardNodeLibrary のインストールthis.library = new StandardNodeLibrary() — これにより、従来の Three.js マテリアルとライトがノードベースの対応物にマッピングされ、後方互換性が確保されます(詳細は第4回で扱います)。

フォールバックの仕組みはエレガントです。プライマリバックエンドの初期化が失敗した場合、Rendererinit() の中で getFallback() を呼び出します。つまり new WebGPURenderer() と書くだけで、WebGPU にも WebGL 2 にも対応したブラウザで自動的に動作します。

レンダーパイプライン:_renderScene() の処理を追う

レンダーループの核心は、1424行目にある _renderScene() です。1フレームの処理を順番に追ってみましょう。

sequenceDiagram
    participant App
    participant Renderer
    participant RenderList
    participant Background
    participant Backend

    App->>Renderer: render(scene, camera)
    Renderer->>Renderer: _renderScene(scene, camera)
    Renderer->>Renderer: Update projection matrix
    Renderer->>Renderer: _projectObject(scene) → build RenderList
    Renderer->>RenderList: sort opaque (front-to-back)
    Renderer->>RenderList: sort transparent (back-to-front)
    Renderer->>Backend: beginRender(renderContext)
    Renderer->>Background: render background
    Renderer->>Renderer: _renderObjects(opaqueList)
    Renderer->>Renderer: _renderTransparents(transparentList)
    Renderer->>Backend: endRender(renderContext)

このメソッドはまず、ネストされたレンダー呼び出しをサポートするために現在のレンダー状態(レンダー ID、レンダーコンテキスト、レンダーオブジェクト関数)を保存します。シャドウマップやトランスミッションエフェクトはどちらも再帰的なレンダーを発生させるため、この保存が必要です。続いてレンダーコンテキストをセットアップし、カメラの座標系と反転デプスバッファーを設定します。

プロジェクション処理ではシーングラフを走査し、各オブジェクトをビュー錐台に対してテストして、可視オブジェクトを RenderList 内の不透明、透明、バンドルグループに分類します。ソート後(不透明オブジェクトは early-z rejection のために前から後ろ、透明オブジェクトは正しいブレンディングのために後ろから前)、背景、不透明オブジェクト、透明オブジェクトの順でレンダリングを行います。

RenderObject と RenderList

RenderObject はシーングラフと GPU 描画コマンドをつなぐ橋渡し役です。3D オブジェクト、マテリアル、シーン、カメラ、ライトノード、レンダーコンテキスト、クリッピングコンテキストへの参照を持って構築されます。バックエンドが描画コマンドを発行するために必要なすべてのもの、つまりコンパイル済みノードグラフ、パイプラインオブジェクト、バインドグループ、描画パラメーターをキャッシュします。

このキャッシュ戦略がパフォーマンスの鍵です。毎フレームシェーダープログラムを再コンパイルする代わりに、RenderObject は現在のマテリアルプロパティ、ジオメトリ属性、ライティング設定をハッシュ化してキャッシュの有効性を確認します。何かが変化したときにのみ、再コンパイルが走ります。

RenderList は2種類のソート戦略を実装しています。不透明オブジェクトには painterSortStable が使われ、groupOrderrenderOrderz の順でソートされます(前から後ろ — 近いオブジェクトが先)。透明オブジェクトには reversePainterSortStable が使われ、z の比較が逆になります(後ろから前 — 遠いオブジェクトが先)。

// Opaque: front-to-back (smaller z first → early-z rejection)
return a.z - b.z;

// Transparent: back-to-front (larger z first → correct blending)
return b.z - a.z;

どちらの関数も、決定論的な順序を保証するために最終的なタイブレーカーとして a.id - b.id を使用しています。

flowchart TD
    PO["_projectObject()"] -->|"Categorize"| Opaque["Opaque List"]
    PO -->|"Categorize"| Trans["Transparent List"]

    Opaque -->|"painterSortStable<br/>front-to-back"| SortO["Sorted Opaques"]
    Trans -->|"reversePainterSortStable<br/>back-to-front"| SortT["Sorted Transparents"]

    SortO --> RenderO["_renderObjects()"]
    SortT --> RenderT["_renderTransparents()"]

    RenderO -->|"For each item"| RO["Get/Create RenderObject"]
    RenderT -->|"For each item"| RO
    RO -->|"Backend.draw()"| GPU["GPU Draw Call"]

次回予告

新しいレンダラーアーキテクチャはノードシステムと切り離せない関係にあります。シェーダー生成パイプライン全体が、静的な GLSL テンプレートチャンクから、WGSL または GLSL にコンパイルされる動的なノードグラフへと移行しました。次回は、Node 基底クラスの仕組み、TSL がシェーダーを記述するための流暢な JavaScript API をどのように提供するか、そして NodeBuilder が DAG をトラバースして実際のシェーダーコードを生成する方法について詳しく見ていきます。