LoginSignup
4
2

TSL(Three.js Shader Language)入門

Last updated at Posted at 2024-06-15

はじめに

この記事は、Three.jsで導入されるThree.js Shader Language(TSL)について、その概要と基本的な使い方を記録、共有するためのものです。

対象とする読者

  • Three.jsの基本的な使い方を理解している
  • シェーダーに興味がある
  • GLSLの基本的な知識がある

対象環境

この記事を読む前に

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というページで公開されています。

参考Pull Request

Three.js NodeEditor

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/examples/jsm/nodes/Nodes.js';

Three.js r165は、package.jsonのexportsフィールドでサブパスを指定しています。公式ドキュメントではこのサブパスでサンプルコードが記述されています。

TypeScriptを使う場合、tsconfig.jsonのmoduleResolutionフィールドがnodeだとexportsフィールドが解決できません。

  • サブパスを使わずthree/examples/jsm/nodes/Nodes.jsを直接importする
  • tsconfig.jsonのmoduleResolutionbundlerもしくはnode16にする

この問題を解決するためには、上記のいずれかの方法を選択してください。

レンダリング結果を変更する

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に転送されます。

可変長配列にはuniform"s"

可変長配列をuniformにしたい場合はuniformsノードを使います。sがついただけで読み間違えやすく、まだwikiにも記載されていないのでサンプルコードを読む際には注意が必要です。

import { Mesh, PlaneGeometry, Vector4 } from "three";
import { MeshBasicNodeMaterial, uniforms } from 'three/nodes'; // uniformsをインポート

const vectors = uniforms( [
	new THREE.Vector4( 1, 0, 1, 1 ),
	new THREE.Vector4( 0, 1, 0, 1 ),
	new THREE.Vector4( 0, 0, 1, 1 )
] ); //uniformsには配列を渡す

const material = new MeshBasicNodeMaterial();
const mesh = new Mesh(new PlaneGeometry(10, 10), material);
material.colorNode = vectors.element(0);
  //uniformsには[]ではなく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への移行がスムーズになりそうです。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2