ノードシステムとThree Shading Language:グラフからシェーダーを構築する仕組み
前提知識
- ›記事1〜3
- ›DAG/グラフデータ構造の理解
- ›基本的なシェーダープログラミングの知識(vertex/fragmentステージ、uniform、varying)
ノードシステムとThree Shading Language:グラフからシェーダーを構築する仕組み
ノードシステムは、現代のThree.jsにおける最も野心的なエンジニアリングの成果です。マテリアル・ライト・エフェクトの組み合わせごとに個別のGLSLシェーダーテンプレートを管理するWebGLRendererのアプローチとは異なり、新しいレンダラーはシェーダーをNodeオブジェクトの有向非巡回グラフ(DAG)として構築します。これらのグラフはNodeBuilderによって三段階のパイプラインで走査され、WebGPU向けのWGSLまたはWebGLフォールバック向けのGLSLを出力します。さらに、TSL(Three Shading Language)は流暢なJavaScript APIを提供し、カスタムシェーダーのロジックをグラフデータ構造の管理ではなく数式を書く感覚で記述できるようにします。
Nodeベースクラスと型システム
NodeはEventDispatcherを継承し、すべてのシェーダーグラフノードの基底クラスとして機能します。コンストラクタはノードの出力型を表すnodeType文字列を受け取ります。使用できる値は'float'、'vec2'、'vec3'、'vec4'、'mat4'、'bool'などで、src/nodes/core/constants.jsで定義されています:
export const NodeType = {
BOOLEAN: 'bool',
INTEGER: 'int',
FLOAT: 'float',
VECTOR2: 'vec2',
VECTOR3: 'vec3',
VECTOR4: 'vec4',
MATRIX2: 'mat2',
MATRIX3: 'mat3',
MATRIX4: 'mat4'
};
ノードのupdate()メソッドが呼ばれるタイミングは、NodeUpdateTypeで定義された三つのライフサイクルプロパティによって制御されます。NONE(呼ばれない)、FRAME(フレームごとに一度)、RENDER(レンダーコールごとに一度——シャドウや反射など一フレーム内に複数のレンダーが発生することがある)、そしてOBJECT(このノードを使用するオブジェクトごとに一度)の四種類です。
ビルドプロセスは66行目で宣言された三つのフェーズで構成されています:
export const defaultBuildStages = [ 'setup', 'analyze', 'generate' ];
classDiagram
class Node {
+nodeType: string
+updateType: string
+version: number
+global: boolean
+setup(builder): Node
+analyze(builder): void
+generate(builder): string
+build(builder): string
}
class ConstNode {
+value: any
+generate(): string
}
class OperatorNode {
+op: string
+aNode: Node
+bNode: Node
}
class MathNode {
+method: string
+generate(): string
}
class MaterialNode {
+scope: string
}
Node <|-- ConstNode
Node <|-- OperatorNode
Node <|-- MathNode
Node <|-- MaterialNode
重要な実装の詳細として、10行目にある_parentBuildStageマップがステージの順序を定義しています。analyzeの親はsetup、generateの親はanalyzeです。これにより、子ノードが親から参照される前に正しいステージで処理されることが保証されます。
ノードのカテゴリ:Accessor、Math、Lighting、Display
src/nodes/ディレクトリはノードを機能ごとに整理しています:
| カテゴリ | ディレクトリ | 用途 | 例 |
|---|---|---|---|
| Core | core/ |
基底クラスとインフラ | Node, NodeBuilder, ConstNode, StackNode |
| Accessors | accessors/ |
シーン/オブジェクトのデータを読み取る | MaterialNode, CameraNode, PositionNode, NormalNode |
| Math | math/ |
数学演算 | MathNode, OperatorNode, CondNode |
| Lighting | lighting/ |
ライト計算 | LightsNode, AnalyticLightNode, ShadowNode |
| Display | display/ |
出力処理 | ToneMappingNode, ColorSpaceNode, ViewportDepthNode |
| Code | code/ |
生のシェーダーコードの注入 | FunctionNode, ExpressionNode |
| TSL | tsl/ |
Three Shading Languageランタイム | TSLCore, TSLBase |
| Functions | functions/ |
再利用可能なシェーダー関数 | BRDF_GGX, getAlphaHashThreshold |
Accessorノードはシーングラフのデータをシェーダーに取り込みます。MaterialNodeはマテリアルプロパティ(color、roughness、metalness)を読み取り、CameraNodeはカメラのuniform(位置、プロジェクション行列)を読み取ります。PositionNodeは各種座標空間(ローカル、ワールド、ビュー)での頂点位置を提供します。
Mathノードは演算を担当します。OperatorNodeは二項演算子(+、−、×、÷)を処理し、MathNodeは関数(sin、cos、normalize、mix、smoothstep)を処理します。CondNodeは三項選択を実装します。
graph TD
subgraph "Accessor Nodes"
MC["materialColor"] --> MulOp
T["texture(map)"] --> MulOp
end
subgraph "Math Nodes"
MulOp["OperatorNode(*)"] --> AddOp["OperatorNode(+)"]
E["materialEmissive"] --> AddOp
end
subgraph "Display Nodes"
AddOp --> TM["ToneMappingNode"]
TM --> CS["ColorSpaceNode"]
CS --> Output["output"]
end
TSL:流暢なシェーダー記述のためのランタイムメタプログラミング
TSLは、ノードグラフの構築をJavaScriptでシェーダーの数式を書くような感覚で行えるようにする開発者向けAPIです。その仕組みの核心は、TSLCore.js内のaddMethodChaining()にあります:
export function addMethodChaining( name, nodeElement ) {
if ( NodeElements.has( name ) ) {
warn( `TSL: Redefinition of method chaining '${ name }'.` );
return;
}
NodeElements.set( name, nodeElement );
if ( name !== 'assign' ) {
Node.prototype[ name ] = function ( ...params ) {
return this.isStackNode
? this.addToStack( nodeElement( ...params ) )
: nodeElement( this, ...params );
};
Node.prototype[ name + 'Assign' ] = function ( ...params ) {
return this.isStackNode
? this.assign( params[ 0 ], nodeElement( ...params ) )
: this.assign( nodeElement( this, ...params ) );
};
}
}
この関数は二つのことを同時に行います。ノードのファクトリー関数をNodeElementsマップに登録し、Node.prototypeにメソッドをパッチしてチェーン可能な呼び出しとして追加します。positionLocal.mul(2.0)と書いたとき、実際には次のことが起きています:
positionLocalはNode(具体的にはPositionNode).mulはaddMethodChaining('mul', mulFunction)によってNode.prototypeに追加されたメソッド- プロトタイプメソッドが
mulFunction(this, 2.0)を呼び出し、新しいOperatorNode('*', positionLocal, float(2.0))を生成して返す
これは純粋なランタイムメタプログラミングです——コンパイルステップもbabelプラグインも必要ありません。TSLの関数呼び出しはすべてノードを構築して返すため、式は自然に合成できます:
// This JavaScript expression:
positionLocal.mul( 2.0 ).add( offset ).normalize()
// Produces this node DAG:
// MathNode('normalize',
// OperatorNode('+',
// OperatorNode('*', positionLocal, float(2.0)),
// offset
// )
// )
ヒント: TSLの式は遅延評価です——グラフを構築するだけで、シェーダーコードを実行するわけではありません。グラフがシェーダーコードになるのは、マテリアルのコンパイル時にNodeBuilderがグラフを走査したときです。つまり、TSLの式を変数として保存して再利用することができます。
NodeBuilder:DAGからシェーダーへのコンパイラ
NodeBuilderは、ノードグラフをシェーダーのソースコードに変換するコンパイラの基底クラスです。3Dオブジェクト、レンダラー、パーサーを引数に取り、変数の宣言、uniformのバインディング、varyingの管理、コードの出力といったインフラストラクチャを提供します。
三つのビルドフェーズは次のように機能します:
-
Setup:ノードが子ノードを返し、DAGの構造を確立します。ノードは自己変換することもあります——たとえば、マテリアルにカラーマップがある場合、
MaterialNodeのsetupはTextureNodeを返すことがあります。 -
Analyze:ビルダーがDAGを走査し、各ノードが属するシェーダーステージ(vertexまたはfragment)を決定し、varyingにする必要がある共有ノードを検出し、uniformの宣言を収集します。
-
Generate:各ノードがシェーダーコードの文字列を出力します。
OperatorNodeは(a * b)を、MathNodeはnormalize(x)を、accessorノードはuniformの読み取りや属性のルックアップを生成します。
flowchart LR
subgraph "Phase 1: Setup"
S1["Material slots → Node DAG"]
end
subgraph "Phase 2: Analyze"
S2["Stage assignment<br/>Varying detection<br/>Uniform collection"]
end
subgraph "Phase 3: Generate"
S3["Node → shader code string<br/>Variable declarations<br/>Code assembly"]
end
S1 --> S2 --> S3
S3 --> WGSL["WGSLNodeBuilder<br/>→ WGSL code"]
S3 --> GLSL["GLSLNodeBuilder<br/>→ GLSL code"]
NodeBuilderは二つの具体的な実装にサブクラス化されています:
WGSLNodeBuilder(約2,523行):WebGPU向けのWGSLを生成GLSLNodeBuilder(約1,676行):WebGLフォールバック向けのGLSLを生成
どちらも同じマテリアルから同じノードグラフを受け取ります——DAGはバックエンドに依存せず、コード生成のみがバックエンド固有です。
NodeMaterial:ノードとマテリアルの接点
NodeMaterialはベースのMaterialクラス(Part 2で解説)を継承し、スロットシステムを追加します。スロットシステムとは、マテリアルパイプラインのあらゆる段階を制御するnull許容のノードプロパティの集合です:
| スロット | 用途 | デフォルト |
|---|---|---|
colorNode |
ディフューズカラー | マテリアルのcolorプロパティ |
normalNode |
法線の摂動 | ジオメトリの法線 |
opacityNode |
アルファ値 | マテリアルのopacity |
emissiveNode |
自己発光 | マテリアルのemissiveカラー |
roughnessNode |
PBRのroughness | マテリアルのroughness |
metalnessNode |
PBRのmetalness | マテリアルのmetalness |
outputNode |
最終的なfragment出力 | 計算済みのライティング結果 |
positionNode |
頂点位置のオーバーライド | ジオメトリの位置 |
各スロットは、期待される型に解決できるノードであれば何でも受け付けます。material.colorNode = texture(myTexture).mul(color(0xff0000))と設定すると、デフォルトのカラーロジックが、テクスチャをサンプリングして赤く色づけするTSLの式に置き換えられます。
MeshStandardNodeMaterialなどの具体的なサブクラスは、PBR固有のスロットのデフォルト値を設定し、roughnessマップ、metalnessマップ、ノーマルマップ、環境マップのサンプリング用に追加のスロットを提供します。
flowchart TB
subgraph "NodeMaterial Slots"
PN["positionNode"] --> VS["Vertex Shader"]
CN["colorNode"] --> FS["Fragment Shader"]
NN["normalNode"] --> FS
ON["opacityNode"] --> FS
EN["emissiveNode"] --> FS
RN["roughnessNode"] --> FS
MN["metalnessNode"] --> FS
end
VS --> Rasterizer["Rasterization"]
Rasterizer --> FS
FS --> OutputN["outputNode"] --> Final["Final Color"]
StandardNodeLibrary:後方互換性の要
StandardNodeLibraryは、WebGLRenderer向けに書かれたシーンと新しいWebGPURendererの後方互換性を実現する仕組みです。すべてのクラシックなマテリアルとライトの型をノードベースの対応物にマッピングします:
this.addMaterial( MeshStandardNodeMaterial, 'MeshStandardMaterial' );
this.addMaterial( MeshPhysicalNodeMaterial, 'MeshPhysicalMaterial' );
this.addMaterial( MeshBasicNodeMaterial, 'MeshBasicMaterial' );
// ... 10 more material mappings
this.addLight( PointLightNode, PointLight );
this.addLight( DirectionalLightNode, DirectionalLight );
this.addLight( SpotLightNode, SpotLight );
// ... 6 more light mappings
this.addToneMapping( acesFilmicToneMapping, ACESFilmicToneMapping );
// ... 5 more tone mapping mappings
WebGPURendererがシーン内のMeshStandardMaterialに遭遇すると、NodeManagerがStandardNodeLibraryを参照します。対応するMeshStandardNodeMaterialを見つけ、ノードベースの等価物を透過的に生成します。ユーザーのコードは一切変更不要です。ライブラリがその変換を処理します。
ヒント: WebGLRendererからWebGPURendererへの移行時に、マテリアルを書き直す必要はありません。StandardNodeLibraryが自動的に変換を処理します。ただし、ノードシステムの真の力(カスタムシェーダーエフェクト、compute shader、プロシージャルジオメトリ)を最大限に活用したい場合は、
NodeMaterialとTSLを直接使用しましょう。
ウォークスルー:TSL式からシェーダーコードへ
シンプルなTSLの式がシェーダーコードになるまでの過程を追ってみましょう。次のコードを例にします:
material.colorNode = materialColor.mul( texture( map ) );
ステップ1:グラフの構築。 materialColorはマテリアルのcolorプロパティを読み取るMaterialNodeです。texture(map)はTextureNodeを生成します。.mul()はOperatorNode('*', materialColorNode, textureNode)を生成します。
ステップ2:Setupフェーズ。 NodeBuilderがルートノードのsetup()を呼び出します。OperatorNodeは自身を返し、子ノードを再帰的にセットアップします。TextureNodeは依存関係としてUVアクセサーノードを生成することがあります。
ステップ3:Analyzeフェーズ。 ビルダーがテクスチャのサンプリングはfragmentステージで行う必要があると判断します。vertexステージからの入力がある場合、それらの接続をvaryingとしてマークします。
ステップ4:Generateフェーズ。 各ノードがコードを出力します。WGSLの場合、次のような出力が得られます:
let nodeColor = nodeUniform.color * textureSample(nodeTexture, nodeSampler, nodeVarying_uv);
GLSLでは、同じグラフから次のコードが生成されます:
vec4 nodeColor = nodeUniform_color * texture(nodeTexture, nodeVarying_uv);
このアーキテクチャの強みは、「マテリアルカラーとテクスチャサンプルを乗算する」という同一のセマンティクスグラフが、どのターゲット言語に対しても正しいコードを生成できる点にあります。uniform、sampler、varying、座標変換といった配管作業はすべて自動的に処理されます。
次のステップ
レンダラーとノードシステムをカバーしたところで、次はサポートインフラストラクチャに目を向けます。すべてのトランスフォームとシェーディング計算を支える数学ライブラリ、カメラとライトの階層構造、カラーマネジメント、そしてシェーダーベースのライティング計算においてライトがノードシステムとどのように統合されているかを解説します。