Skip to main content
GDN8 PATIO

TSL / NodeMaterialについて

TSL(Three.js Shading Language)はJavaScriptでシェーダーの処理を組み立てるためのThree.jsの仕組みです。GLSLやWGSLを文字列として直接書く代わりに値や計算をNodeとして接続し、そのグラフからレンダラーがシェーダーを生成します。

NodeMaterialはそのNodeをThree.jsのMaterialへ接続する場所です。TSLが計算を記述する方法だとすればNodeMaterialは計算結果を色や頂点位置、粗さなどへ割り当てるインターフェースと言えます。

2026年7月17日時点の位置づけ

WebGPURendererNodeMaterialは新規案件でも条件付きで実務利用できる段階にあります。ただしThree.js公式はWebGPURendererを現在もexperimentalと位置づけており純粋なWebGL2案件では引き続きWebGLRendererの方が安全な可能性があります。WebGPURendererはWebGPUを利用できない環境でWebGL2へフォールバックします。また実務で採用する場合はWebGPU側だけでなくforceWebGL: trueを指定したWebGL側も実機で確認する必要があります。

既存のGLSL資産が多い案件、幅広い端末で厳密な表示一致が必要な案件ではWebGLRendererの方が安全です。一方で新規実装でTSL、Compute、MRT、新しいポストプロセスを使う理由があり、バックエンドごとの検証時間を確保できる場合は採用候補になります。本稿はNodeMaterialの構造を説明するものであり、すべての案件でWebGPURendererへの移行を推奨するものではありません。

TSLとNodeMaterialの関係

NodeMaterialはすべてのNode Materialの基底クラスです。実装では基底クラスをそのまま使うより目的に近いMaterialを選びます。

  • MeshBasicNodeMaterial:ライトの影響を受けない面
  • MeshStandardNodeMaterialroughnessmetalnessを使うPBRの面
  • MeshPhysicalNodeMaterialtransmissionclearcoatなどを含む面
  • SpriteNodeMaterialPointsNodeMaterial、各種LineのNode Material:描画プリミティブに固有の処理

たとえばライトと環境マップを受ける物体ならMeshStandardNodeMaterialから始めます。独自表現だからといって最初から頂点シェーダーとフラグメントシェーダーをすべて置き換える必要はありません。既存のライティングモデルを残したまま必要な部分だけをNodeへ差し替えられることがNodeMaterialの大きな利点です。

import * as THREE from 'three/webgpu';
import { color } from 'three/tsl';

const material = new THREE.MeshStandardNodeMaterial( {
	color: 0xffffff,
	roughness: 0.55,
	metalness: 0.1
} );

material.colorNode = color( 0x5d72ff );

この場合に置き換わるのは面の基本色です。MeshStandardNodeMaterialが持つライトや環境マップへの反応はそのまま残ります。

Materialのどこへ接続するか

NodeMaterialには処理の役割ごとに接続先が用意されています。よく使うものは次の通りです。

プロパティ 置き換えるもの
colorNode Materialの色とカラーマップ
opacityNode 不透明度とアルファマップ
positionNode ローカル座標の頂点位置
normalNode ライティングに使う法線
emissiveNode 発光色
roughnessNode MeshStandardNodeMaterialの粗さ
metalnessNode MeshStandardNodeMaterialの金属性
fragmentNode 組み込みのフラグメント処理全体
vertexNode 組み込みの頂点処理全体

colorNoderoughnessNodeのように役割が限定された接続先を使うほど標準Materialの処理を保ちやすくなります。fragmentNodevertexNodeは自由度が高い一方で組み込みの処理を置き換えます。まずは目的に最も近い接続先を選び、全体の置き換えは本当に必要な場合に限るのが扱いやすい順序です。

色と質感を同じNodeからつくる

TSLではuv()timeもNodeです。計算をメソッドチェーンで接続し、その結果を複数のMaterialプロパティへ利用できます。

import * as THREE from 'three/webgpu';
import { color, mix, time, uv } from 'three/tsl';

const material = new THREE.MeshStandardNodeMaterial();

const band = uv().y
	.mul( 16 )
	.add( time.mul( 2 ) )
	.sin()
	.mul( 0.5 )
	.add( 0.5 );

material.colorNode = mix(
	color( 0x171a20 ),
	color( 0xff7657 ),
	band
);

material.roughnessNode = band.oneMinus().mul( 0.7 ).add( 0.15 );

bandは0から1の間を往復する縞模様です。同じ値を色の補間と粗さへ渡しているため明るい部分と反射の状態が別々に動きません。一つの関係をNodeとして定義し、色や質感へ展開できるのはグラフとしてMaterialを組む利点です。

colorNodeを指定すると元のmaterial.color * material.mapは置き換わります。既存の色やテクスチャを土台として加工したい場合はmaterialColorを参照します。

import { materialColor } from 'three/tsl';

material.colorNode = materialColor.mul( band.mul( 0.3 ).add( 0.7 ) );

この書き方ならMaterialに設定済みのcolormapを残したまま明暗だけを加えられます。Node用プロパティは既存値への追加ではなく置き換えであることを先に意識すると意図しない二重計算を避けられます。

ノイズなどのNode関数をimportする

TSLではシェーダー用の処理もJavaScriptの関数としてimportできます。たとえばThree.js本体には3Dノイズを生成するtriNoise3Dがあります。

import {
	color,
	mix,
	positionLocal,
	time,
	triNoise3D
} from 'three/tsl';

const noise = triNoise3D(
	positionLocal.mul( 2 ),
	0.25,
	time
).saturate();

material.colorNode = mix(
	color( 0x16181d ),
	color( 0xb9ff66 ),
	noise
);

triNoise3D()の結果も一つのNodeです。そのまま色の補間へ使うほかpositionNodeへ渡して頂点を動かしたり、roughnessNodeへ渡して表面の反射を変えたりできます。

ShaderMaterialでノイズを使う場合はGLSLのノイズ関数をシェーダー文字列へ含め、引数と戻り値を自分で接続します。TSLでは実装済みのNode関数をimportし、ほかのNodeと同じ方法で合成できます。ノイズに限らずUV変換、色調整、ブレンド、フォグ、スクリーン座標なども同じ考え方です。

外部にもtsl-texturesのようにプロシージャルテクスチャをNode関数として提供するライブラリがあります。必要な機能をMaterial単位ではなく関数単位で持ち込めるため、複数の表現へ再利用しやすくなります。

ただしimportが簡単でもGPU上の計算が無料になるわけではありません。ノイズの重ね合わせ回数や評価する頂点数、ピクセル数によって負荷は変わります。外部ライブラリを使う場合はThree.jsとの対応バージョンも確認します。

positionNodeで頂点を動かす

頂点変形も同じ考え方で記述できます。positionLocalはMaterialの処理を通ったローカル座標の頂点位置です。元の形を保って変形を加える場合はこのNodeを起点にします。

import { normalLocal, positionLocal, time } from 'three/tsl';

const wave = positionLocal.x
	.mul( 4 )
	.add( time.mul( 1.5 ) )
	.sin()
	.mul( 0.08 );

material.positionNode = positionLocal.add( normalLocal.mul( wave ) );

各頂点はローカル法線の方向へ動きます。ShaderMaterialで同じ処理を書く場合に必要だったモデル・ビュー・プロジェクション変換を自分で組み直さず、頂点位置の変化だけを記述できます。

ただしGPU上で頂点を大きく動かしてもGeometryが持つ法線やバウンディング情報は自動では作り直されません。変形量が大きい場合はnormalNodeで法線を補正し、フラスタムカリングに使われるbounding sphereや影の見え方も確認する必要があります。

動的な値はuniform()で渡す

スクロール位置やポインター座標のようにJavaScript側から更新する値にはuniform()を使います。Nodeの構造は保ったままGPUへ渡す値だけを変えられます。

import { color, mix, smoothstep, uniform, uv } from 'three/tsl';

const progress = uniform( 0 );
const edge = smoothstep(
	progress.sub( 0.08 ),
	progress.add( 0.08 ),
	uv().x
);

material.colorNode = mix(
	color( 0x111318 ),
	color( 0xd8ff4f ),
	edge
);

function update( scrollProgress ) {
	progress.value = scrollProgress;
}

毎フレーム新しいNodeやMaterialを作るのではなくprogress.valueを更新します。同じuniformを複数のMaterialやポストプロセスで共有することもできます。見た目を構成するグラフとアプリケーションから変わる値を分けておくと動きの調整範囲が明確になります。

ShaderMaterial / RawShaderMaterialとの違い

ShaderMaterialはGLSLで頂点処理とフラグメント処理を直接記述する方法です。描画の全体を制御できますがライティングやフォグ、シャドウ、トーンマッピングとの接続も実装側で管理する範囲が増えます。組み込みMaterialをonBeforeCompile()で書き換える方法も挿入位置がThree.js内部のシェーダーチャンクに依存します。

RawShaderMaterialもGLSLを直接記述しますが、Three.jsによる組み込みuniformattributeの定義がシェーダーの先頭へ自動追加されません。必要な宣言を含めてソース全体を管理したい場合のMaterialです。

NodeMaterialは色、位置、法線、粗さといった意味の単位で処理を差し替えます。Node Systemは接続されたグラフを解析しWebGPUではWGSL、WebGL 2バックエンドではGLSLを生成します。WebGPUとWebGL 2で性能や結果が必ず同一になるという意味ではありませんが、Materialのロジックをバックエンドごとの文字列から切り離せます。

同じグラデーションを二つの方法で書く

UVの下端を黒、上端を白にするだけの例で比べます。ライトの計算を含まない同じ条件にするためNodeMaterial側ではMeshBasicNodeMaterialを使います。

ShaderMaterialでは頂点シェーダーからフラグメントシェーダーへUVを渡し、GLSLのmix()で色を補間します。この例はWebGLRendererでの使用を前提としています。

import * as THREE from 'three';

const material = new THREE.ShaderMaterial( {
	vertexShader: /* glsl */`
		varying vec2 vUv;

		void main() {
			vUv = uv;
			gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
		}
	`,
	fragmentShader: /* glsl */`
		varying vec2 vUv;

		void main() {
			vec3 color = mix( vec3( 0.0 ), vec3( 1.0 ), vUv.y );

			gl_FragColor = vec4( color, 1.0 );
		}
	`
} );

NodeMaterialではuv().yをそのまま色の補間へ接続します。モデル・ビュー・プロジェクション変換やUVの受け渡しはNode Systemが組み立てます。この例はWebGPURendererでの使用を前提としています。

import * as THREE from 'three/webgpu';
import { color, mix, uv } from 'three/tsl';

const material = new THREE.MeshBasicNodeMaterial();

material.colorNode = mix(
	color( 0x000000 ),
	color( 0xffffff ),
	uv().y
);

二つのコードが行っている計算は同じです。ShaderMaterialはシェーダーステージ全体を記述し、NodeMaterialは変更したい色だけを記述しています。

ここへライトの影響を加える場合も違いが現れます。ShaderMaterialではライトの情報を受け取り反射モデルを実装します。NodeMaterialではMeshBasicNodeMaterialMeshStandardNodeMaterialへ変更し、同じcolorNodeを接続したまま既存のPBRライティングを利用できます。

同じ頂点変形を二つの方法で書く

次は頂点をローカル法線の方向へ波形に動かす例です。変形を確認するには頂点数のあるGeometryを使います。

ShaderMaterialではpositionnormalから変形後の座標をつくり、最後にモデル・ビュー・プロジェクション変換を行います。フラグメント側は比較のため白を出すだけにしています。

import * as THREE from 'three';

const material = new THREE.ShaderMaterial( {
	vertexShader: /* glsl */`
		void main() {
			float wave = sin( position.x * 4.0 ) * 0.1;
			vec3 transformed = position + normal * wave;

			gl_Position = projectionMatrix
				* modelViewMatrix
				* vec4( transformed, 1.0 );
		}
	`,
	fragmentShader: /* glsl */`
		void main() {
			gl_FragColor = vec4( 1.0 );
		}
	`
} );

NodeMaterialでは変形後のローカル座標をpositionNodeへ接続します。座標変換はNode Systemが補います。

import * as THREE from 'three/webgpu';
import { normalLocal, positionLocal } from 'three/tsl';

const material = new THREE.MeshBasicNodeMaterial();
const wave = positionLocal.x.mul( 4 ).sin().mul( 0.1 );

material.positionNode = positionLocal.add( normalLocal.mul( wave ) );

ここでも計算は同じです。ShaderMaterialでは頂点シェーダー全体を記述し、NodeMaterialでは変更後の頂点位置だけを記述します。時間で動かす場合は両方へtimeを加えますが、ShaderMaterialではuniformの宣言と更新、NodeMaterialではthree/tsltimeをNodeへ接続する違いがあります。

RawShaderMaterialでは何が増えるか

前のShaderMaterialではpositionnormalmodelViewMatrixprojectionMatrixを宣言せずに使えました。これらの定義をThree.jsがシェーダーの先頭へ追加するためです。

RawShaderMaterialでは同じ頂点変形に必要なprecisionattributeuniformをすべて記述します。

import * as THREE from 'three';

const material = new THREE.RawShaderMaterial( {
	vertexShader: /* glsl */`
		precision highp float;

		attribute vec3 position;
		attribute vec3 normal;

		uniform mat4 modelViewMatrix;
		uniform mat4 projectionMatrix;

		void main() {
			float wave = sin( position.x * 4.0 ) * 0.1;
			vec3 transformed = position + normal * wave;

			gl_Position = projectionMatrix
				* modelViewMatrix
				* vec4( transformed, 1.0 );
		}
	`,
	fragmentShader: /* glsl */`
		precision highp float;

		void main() {
			gl_FragColor = vec4( 1.0 );
		}
	`
} );

Three.jsはGeometryattributeやカメラとObjectの行列をGPUへ渡しますが、GLSL側でどの型と名前を使うかは自分で宣言します。ShaderMaterialより記述量は増える一方で、Three.jsが追加するシェーダー定義に頼らずGLSLの入口を明示できます。

RawShaderMaterialが向いているのは既存のGLSLを宣言も含めて管理したい場合や、シェーダーへ自動挿入されるコードを最小限にしたい場合です。Three.jsの標準的な行列やattributeを使うだけならShaderMaterialの方が簡潔です。どちらもWebGLRenderer専用でWebGPURendererでは使用できません。

メリットとデメリット

NodeMaterial

メリット:

  • 色や頂点位置など変更する部分だけを書ける
  • 標準Materialのライト、環境マップ、シャドウなどを残しやすい
  • Nodeやuniformを別のMaterial、ポストプロセスと共有できる
  • ノイズやUV変換などのNode関数をimportし、色や変形へ直接接続できる
  • JavaScriptのimportと関数で処理を分割でき、シェーダー文字列の置換が不要になる
  • 同じグラフからWebGPU向けのWGSLとWebGL 2向けのGLSLを生成できる

デメリット:

  • GLSLをそのまま貼り付けられず処理をNodeへ分解して移植する必要がある
  • 生成されたシェーダーが間に入るためGLSLを直接書く場合より処理の全体を追いにくい
  • JavaScriptの通常の計算とGPU上で評価されるNodeの区別に慣れが必要になる
  • WebGPUとWebGL 2では利用できる機能や性能が異なるため両方のバックエンドで確認が必要になる
  • WebGPURendererとTSLは更新が続いておりThree.jsのバージョン変更でAPIを追う必要がある

Nodeで短く書けることとGPU処理が軽いことは別です。接続が簡潔でもテクスチャ参照や分岐、頂点数が同じなら負荷も残ります。最終的な性能は生成後の処理と実機で判断します。

ShaderMaterial

メリット:

  • 頂点シェーダーとフラグメントシェーダーをGLSLで直接制御できる
  • 既存のGLSLコードやWebGL向けの知見を利用しやすい
  • GPUで実行するコードとシェーダーステージ間の受け渡しがソース上で明示される

デメリット:

  • 座標変換、varyinguniformなど表現以外の記述も必要になる
  • ライティングやフォグ、シャドウを使う場合はThree.jsの処理との接続範囲が増える
  • シェーダーが文字列になるためJavaScriptのモジュールとして分割、合成しにくい
  • WebGPURendererでは使用できない

RawShaderMaterial

メリット:

  • attributeuniformprecisionを含むGLSLの入口を自分で管理できる
  • Three.jsが自動追加する組み込み定義へ依存せずシェーダーを構成できる
  • 宣言を含む既存のGLSLを移植するときに構造を保ちやすい

デメリット:

  • ShaderMaterialなら省略できる宣言も必要になりコードが長くなる
  • 行列やattributeの宣言を間違えるとコンパイルエラーや描画不良になる
  • Three.jsの標準機能へ接続する範囲も自分で管理する必要がある
  • ShaderMaterialと同じくWebGLRendererでしか使用できない

WebGPURendererではShaderMaterialRawShaderMaterialonBeforeCompile()による組み込みMaterialの変更はサポートされていません。既存のWebGL表現を移行する場合はGLSLをそのままTSLの記法へ置換するより、まず処理を色、位置、法線、ポストプロセスなどの役割へ分ける方がNodeMaterialの構造に合わせやすくなります。

NodeMaterialを使う基準

NodeMaterialはGLSLを短く書き直すためだけの仕組みではありません。標準Materialが持つ処理を残しながら必要な部分を交換し、その計算を別のMaterialやポストプロセスでも再利用するための構造です。

実装では次の順序で考えると整理しやすくなります。

  1. 表現に最も近いNode Materialを選ぶ
  2. colorNodepositionNodeなど最小の接続先を決める
  3. 繰り返す計算をNodeとして共有する
  4. 外部から変わる値だけをuniformにする
  5. 標準処理では足りない部分に限ってfragmentNodevertexNodeを検討する

NodeMaterialで重要なのは何を書けるかより、Materialのどの段階を変更するかを選べることです。色と光、形と動きを別々のシェーダー文字列として管理せず、一つの関係として組み直せる点にTSLを使う意味があります。

参考

  • ※ 記載内容とコードは2026年7月17日時点のThree.js公式ドキュメントを基準にしています。