はじめに
この記事は、Three.jsで導入されるThree.js Shader Language(TSL)について、その概要と基本的な使い方を記録、共有するためのものです。
対象とする読者
- Three.jsの基本的な使い方を理解している
- シェーダーに興味がある
- GLSLの基本的な知識がある
対象環境
- Three.js r165
- WebGPUが利用可能なブラウザー
- TypeScript v5.4.5
- Node.js v20.14.0
この記事を読む前に
TSLの仕様は現在ドラフト版と位置付けられています。ユーザーからのフィードバックを受け付けている段階で、将来的には仕様が大きく変更されるかもしれません。Three.jsとブラウザーのバージョンを確認してからお読みください。
この記事では、JavaScriptの開発環境やThree.jsのインストールガイドは取り扱いません。Three.jsの基本的な使い方については、公式ドキュメントを参照してください。
TSLとは
Three.js Shader Language(TSL)は、Three.jsが新たに推進するシェーダー言語です。2024/05/29にThree.jsから最初の仕様がアナウンスされました。
TSLに関連する用語
まず、TSLに関連する用語を整理します。
-
Node
: 値の変更を検知し、演算を行い、複数のNodeを合成するオブジェクト -
NodeMaterial
: 複数のNodeをメンバー変数に持つマテリアル。Nodeを入れ替えることでレンダリング結果が変わる -
WebGPURenderer
: NodeMaterialをオンザフライでWGSL(WebGPU)やGLSL(WebGL2)に変換するレンダラー
これらの要素を総合してTSLと呼びます。
なぜ新しいシェーダー言語が必要なのか
現在、Three.jsはWebGPUへの段階的移行と、シェーダー作成の難易度を下げるという2つの目標を掲げています。この目標を達成するために、TSLが導入されました。
WebGPUへの移行
Three.jsはWebGLコンテンツをJavaScriptで記述するためのライブラリです。
WebGLでは最新のGPUを最大限に活用できません。この問題を解決するためにWebGPUが開発されています。
WebGPUはシェーダー言語にWGSL(WebGPU Shading Language)を採用しています。この言語はWebGLで使われてきたGLSL(OpenGL Shading Language)とは互換性がありません。
Three.jsは1つのソースからWGSLとGLSLの両方を出力する中間言語としてTSLを導入しました。
Shader Chunkの複雑化
現状のThree.jsは、シェーダーの処理を共通化するためにShaderChunkという仕組みを採用しています。これはGLSLコードを文字列として扱い、レンダリング直前に置き換えるというものです。
Three.js内蔵のShaderChunkに依存するシェーダーは、その変更によって動作しなくなります。たとえばShaderChunk内の変数名が1つ変わっただけで、そのChunkに依存する後続処理が失敗します。
TSLはJavaScriptで記述されるため、WGSLへ変換する際にTree Shakingが適用されます。ShaderChunkと違い、シェーダーの処理は入出力以外が隠蔽され、変更による影響範囲が限られます。
GLSLとTSLの関係性の整理
従来のGLSLで記述されたシェーダーマテリアルとTSLの関係性を整理します。
- WebGLRenderer → ShaderMaterial → GLSL
- WebGPURenderer → NodeMaterial →(変換)→ WGSL / GLSL
NodeMaterialにNodeを渡すと、WebGPURendererがNodeをWGSL(WebGPU)またはGLSL(WebGL2)に変換します。
WebGLRendererではNodeMaterialはサポートされません。NodeMaterialはWebGPURendererでのみサポートされます(関連issue)。
TSLの目標
TSLが実現されると、以下のような利点があります。
WebGL2とWebGPUで動作するシェーダーマテリアル
Three.jsは以下の手順でWebGPUへの移行を進めています。
- レンダラーをWebGLRendererからWebGPURendererへ移行する
- WebGPUが動作しない環境では、WebGPURendererがWebGL2にフォールバックする
カスタマイズしたGLSLに依存しないコンテンツは、WebGLRendererをWebGPURendererに入れ替えるだけで移行が完了します。GLSLで記述したシェーダーをTSLに移行すれば、WebGPUがサポートされていない環境でもコンテンツが動作するようになります。
シェーダーの再利用性向上
TSLはシェーダーの処理をノードとして切り出せます。
- 基本となるノードマテリアルを安全に継承し、カスタマイズできる
- 頻繁に利用するシェーダーをノードに切り出すことで、複数のマテリアルに再利用できる
ノードエディター
NodeMaterialはシェーダーの処理がNodeに切り出されており、ビジュアルシェーダーエディターが実現可能です。将来的には、Three.jsでもUnityのShader Graphのようなエディターが利用できるかもしれません。
ブラウザー上で動作するThree.js用のノードエディターは、Three.js PlayGroundというページで公開されています。
TSLの基本
ミニマムなNodeMaterialを作成し、TSLの基本を理解しましょう。
セットアップ
Three.jsからTSLを利用するために、関連するクラスをimportします。
import { Mesh, PlaneGeometry } from "three";
import { MeshBasicNodeMaterial } from 'three/nodes';
const material = new MeshBasicNodeMaterial();
const mesh = new Mesh(new PlaneGeometry(10, 10), material);
シェーダーをカスタマイズしないNodeMaterialを作成しました。このマテリアルは、MeshBasicMaterial
と同じように動作します。
TypeScriptの場合、importに注意
import { MeshBasicNodeMaterial } from 'three/src/nodes/Nodes.js';
Three.js r167は、package.jsonのexportsフィールドでwebgpu
もしくはtsl
というサブパスを指定しています。公式ドキュメントではこのサブパスでサンプルコードが記述されています。
TypeScriptを使う場合、tsconfig.jsonのmoduleResolution
フィールドがnode
だとexportsフィールドが解決できません。
- サブパスを使わず
three/src/nodes/Nodes.js
を直接importする - tsconfig.jsonの
moduleResolution
をbundler
もしくはnode16
にして、threeのimportをすべてwebgpu
サブパスに統一する。
この問題を解決するためには、上記のいずれかの方法を選択してください。
レンダリング結果を変更する
NodeMaterialには、メンバー変数としてNodeが組み込まれています。これらのNodeを置き換えると、レンダリング結果が変わります。
import { Mesh, PlaneGeometry } from "three";
import { MeshBasicNodeMaterial, color } from 'three/nodes'; // colorノードを生成する関数をimport
const material = new MeshBasicNodeMaterial();
material.colorNode = color(0xff0000); // カラーノードを赤色に変更
const mesh = new Mesh(new PlaneGeometry(10, 10), material);
このコードでは、colorNode
に固定値を割り当てて赤色に変更しています。TSLのcolor
は、Nodeを生成するための関数です。NodeMaterialのコンストラクター内でたくさんのNodeが初期化されていますが、まずは以下の3つのNodeを覚えてください。
-
colorNode
: マテリアルに適用される色を決定するノード -
fragmentNode
: カラーノードにライトやフォグの影響を合わせたノード -
positionNode
: ジオメトリの座標を出力するノード
他にもopacity、shadow、normalなどのNodeがあります。目指す表現にあわせて、これらのNodeを入れ替えましょう。
マテリアルの設定値をNodeとして扱う
マテリアルに設定された値をNodeとして扱いたい場合は、materialColorなどのアクセサー関数を使います。
import { Mesh, PlaneGeometry, Color } from "three"; // three.jsのColorクラスをimport
import { MeshBasicNodeMaterial, materialColor } from 'three/nodes'; // TSLのuniformを生成する関数をimport
const material = new MeshBasicNodeMaterial();
const mesh = new Mesh(new PlaneGeometry(10, 10), material);
material.colorNode = materialColor; // カラーノードにマテリアルのcolor変数を割り当てる。
material.color.setHex(0x00ff00); // マテリアルの設定を変更すると、レンダリング結果に反映される。
ほかにもopacityなどのアクセサー関数が用意されています。
変数を扱う
Nodeマテリアルのレンダリングに成功しましたが、固定値を割り当てたためマテリアル設定が変更できません。次に、変数を扱う方法を解説します。
Uniform
uniform
はJavaScriptからWebGPUに変数を渡すためのNodeです。
import { Mesh, PlaneGeometry, Color } from "three"; // three.jsのColorクラスをimport
import { MeshBasicNodeMaterial, color, uniform } from 'three/nodes'; // TSLのuniformを生成する関数をimport
const uniformColor = uniform(new Color(0xff0000)); //Colorインスタンスを参照するuniformノードを作成
const material = new MeshBasicNodeMaterial();
const mesh = new Mesh(new PlaneGeometry(10, 10), material);
material.colorNode = uniformColor; // カラーノードにuniformノードを割り当てる。
uniformColor.value.setHex(0x00ff00); // uniformノードの値を変更すると、レンダリング結果に反映される。
この例では、uniformColor
というuniformノードを作成し、Color
インスタンスを割り当てました。このuniformノードをcolorNode
に割り当てることで、マテリアルの色を動的に変更できます。
uniform
ノードにはvalue
というメンバーがあり、ここでuniformに割り当てたインスタンスが格納されています。この値を変更すると、次のレンダリング時に値がGPUに転送されます。
可変長配列にはuniformArray
可変長配列をuniformにしたい場合はuniformArrayノードを使います。
import { Mesh, PlaneGeometry, Vector4 } from "three";
import { MeshBasicNodeMaterial, uniformArray } from 'three/nodes'; // uniformArrayをインポート
const vectors = uniformArray( [
new THREE.Vector4( 1, 0, 1, 1 ),
new THREE.Vector4( 0, 1, 0, 1 ),
new THREE.Vector4( 0, 0, 1, 1 )
] ); //uniformArrayには配列を渡す
const material = new MeshBasicNodeMaterial();
const mesh = new Mesh(new PlaneGeometry(10, 10), material);
material.colorNode = vectors.element(0);
//uniformArrayには[]ではなくelementでアクセスする。
//elementの引数にはNodeも渡せるので、attributeを参照したり、別のuniformを参照できる。
Attribute
attribute
はジオメトリの頂点情報をフラグメントシェーダーで受け取るためのNodeです。BufferGeometryには、デフォルトで座標や頂点カラーなどのAttributeが登録されていますが、カスタムアトリビュートを追加することで、頂点ごとに異なる値をシェーダーに渡せます。
const geometry = new PlaneGeometry(10, 10);
const customAttribute = new BufferAttribute(
new Float32Array(geometry.attributes.position.array),
3
);
customAttribute.setXYZ(0, 0, 0, 0);
customAttribute.setXYZ(1, 1, 0, 0);
customAttribute.setXYZ(2, 0, 1, 0);
customAttribute.setXYZ(3, 1, 1, 1);
geometry.setAttribute("customColorAttribute", customAttribute); //ジオメトリに"customColorAttribute"という名前でアトリビュートを追加する。
const material = new MeshBasicNodeMaterial();
material.colorNode = color(attribute("customColorAttribute")); //カラーノードからカスタムアトリビュートを参照する。
const mesh = new Mesh(geometry, material);
この例ではcolorNode内でカスタムアトリビュートを参照し、頂点ごとに異なる色を割り当てています。
オペレーター
JavaScriptでは演算子オーバーロードができません。そのため、TSLではWGSLの演算子を関数として定義しています。
import { float } from 'three/nodes';
const operated = float(1).add(2); // = 1 + 2
各関数は戻り値としてNode自身を返します。そのため、メソッドチェーンで演算を連続できます。
tslFn(TSL関数)
tslFn(TSL関数)は、複数のNodeを参照して、演算結果を返すNodeを生成する関数です。以下のような用途に利用できます。
- ノイズ関数のような、複数のマテリアルで再利用したい処理をノード化する
- 複数のノードを入力として受け取り、合成するノードを生成する
tslFnは処理に必要なNodeを引数として受け取ります。ShaderChunkのように他のChunkで設定されているはずの変数に依存することがなく、再利用性が高くなります。
個人的な感想
この記事ではTSLの背景と基本的な使い方を紹介しました。従来のShaderMaterialには再利用性の問題がありましたが、TSLではその問題が改善しそうです。TSLに慣れておくと、WebGPUへの移行がスムーズになりそうです。