デュアルレンダラーアーキテクチャ:WebGLRenderer と新しいバックエンドシステム
前提知識
- ›第1・2回の記事
- ›グラフィックスパイプラインの基本的な理解(頂点シェーダー/フラグメントシェーダー、レンダーターゲット、デプスバッファー)
- ›WebGL または WebGPU API の基礎知識
デュアルレンダラーアーキテクチャ:WebGLRenderer と新しいバックエンドシステム
Three.js は今、歴史的なアーキテクチャの転換期を迎えています。10年以上にわたって、WebGLRenderer は30を超える内部ヘルパーモジュールを通じて WebGL の状態を直接管理する、単一のモノリシッククラスでした。現在は新しい3層構造が導入され、関心事が明確に分離されています。API に依存しない Renderer オーケストレーター、抽象 Backend インターフェース、そして WebGPU と WebGL 2 向けの具体的なバックエンド実装という構成です。レガシーレンダラーは今でも広く使われており、新システムはすべての活発な開発の場となっています。両方のアーキテクチャをしっかり理解しておくことが重要です。
アーキテクチャの分岐:モノリシックからモジュール式へ
WebGLRenderer は30を超える WebGL 専用の内部モジュールを直接インポートしています。代表的なものとしては以下があります。
WebGLAttributes、WebGLBindingStates、WebGLBufferRenderer、WebGLCapabilities、WebGLClipping、WebGLExtensions などです。WebGLGeometries、WebGLInfo、WebGLMorphtargets、WebGLObjects、WebGLPrograms、WebGLProperties もあります。さらに WebGLRenderLists、WebGLRenderStates、WebGLShadowMap、WebGLState、WebGLTextures、WebGLUniforms なども含まれます。
これらはすべて 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、シェーダーのコンパイルエラーならNodeManagerとPipelines、描画順の不具合なら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(GPUDevice、GPUCommandEncoder、GPURenderPassEncoder)を使ってこれらのメソッドを実装しています。WebGLBackend(約2,800行)は WebGL 2 の呼び出し(gl.bindTexture、gl.drawElements など)を使って同じインターフェースを実装しており、WebGPU に対応していないブラウザ向けのフォールバックとして機能します。
data WeakMap は設計上の重要なポイントです。Three.js のオブジェクトにバックエンド固有のプロパティを直接付与すると抽象が漏れてしまいますが、この WeakMap を使うことで各バックエンドは GPU ハンドル(WebGL テクスチャ、WebGPU バッファー、パイプラインオブジェクト)をソースオブジェクトをキーとして保存できます。Three.js のオブジェクトがガベージコレクションされると、対応する GPU データも自動的に回収可能になります。
WebGPURenderer:107行のオーケストレーター
WebGPURenderer は驚くほどシンプルで、JSDoc を含めてもわずか107行です。53〜103行目のコンストラクターは3つのことだけを行います。
- バックエンドの選択:
parameters.forceWebGLが true の場合はWebGLBackendを直接使用し、そうでなければWebGPUBackendを作成してフォールバック関数をセットアップします。
parameters.getFallback = () => {
warn( 'WebGPURenderer: WebGPU is not available, running under WebGL2 backend.' );
return new WebGLBackend( parameters );
};
-
バックエンドを Renderer に渡す:
super( backend, parameters )。 -
StandardNodeLibrary のインストール:
this.library = new StandardNodeLibrary()— これにより、従来の Three.js マテリアルとライトがノードベースの対応物にマッピングされ、後方互換性が確保されます(詳細は第4回で扱います)。
フォールバックの仕組みはエレガントです。プライマリバックエンドの初期化が失敗した場合、Renderer は init() の中で 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 が使われ、groupOrder、renderOrder、z の順でソートされます(前から後ろ — 近いオブジェクトが先)。透明オブジェクトには 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 をトラバースして実際のシェーダーコードを生成する方法について詳しく見ていきます。