TSLよく使うノード・関数 早見表|これだけ覚えれば書ける10個

約20分
TSLよく使うノード・関数 早見表|これだけ覚えれば書ける10個

みなさんこんにちは。フロントエンドエンジニアのしゅん(@shun_webdesign)です。

TSLとは?WebGPU時代の新しいシェーダーの書き方で「TSLはJavaScriptでシェーダーを書ける仕組みです」という話をしました。じゃあ実際に何を覚えればいいのか、というのが今回です。

先に身も蓋もないことを書くと、TSLのエクスポートは611個ありますthree.tsl.js を数えました)。ぜんぶ覚えるのは無理だし、覚える必要もありません。

僕が作品を作るときに実際に使っているのは、そのうち10個くらいです。この記事では、その10個を「最小コード+実際の見た目」で並べます。全部、手元で動かして撮ったものです。

TSLでよく使うノード10個の実行結果

この10枚は1ページに並べて動かしています(デモ: demo/08-tsl/)。以下のコードはすべてそこから抜き出したものです。

前提:バージョンによって書き方が変わります

ここが一番大事なので最初に書きます。TSLはAPIがけっこう変わります。

古い記事でよく見る書き方今(r180)
tslFn()Fn()
timerLocal()time

僕が最初にハマったのがこれで、ネットで見つけたコードを貼っても tslFn is not a function で止まる。ライブラリが壊れているわけではなく、名前が変わっただけでした。

なので、TSLの記事を読むときは「どのバージョンで書かれたか」を必ず見てください。この記事は three r180(0.180.0) で全部動かして確認しています。

npm list three
# └── three@0.180.0

読み込み先も普通のThree.jsとは別です。

import * as THREE from "three/webgpu"; // WebGPURenderer や Node系マテリアル
import { uv, time, Fn, mix } from "three/tsl"; // TSLのノードたち

three からではなく three/webgputhree/tsl から取る。ここを間違えると「関数が見つからない」になります。

早見表

ノード何に使うGLSLでいうと
uv()UV座標。ほぼ全ての模様の起点vUv
time時間。動かすなら必ず要るuTime uniform
Fn()処理を関数にまとめる関数定義
uniform()JS側から値を渡すuniform
mix()2つの色・値を混ぜるmix()
step() / smoothstep()境目を作る。輪郭・マスク同名
sin() / fract()波と繰り返し同名
positionLocal頂点そのものを動かすposition
texture()画像を読むtexture2D()
colorNode / positionNodeマテリアルへの差し込み口gl_FragColor / gl_Position

下ごしらえはこれだけです。以降のコードは、この materialcolorNode を差し替えているだけだと思ってください。

const renderer = new THREE.WebGPURenderer({ antialias: true });
await renderer.init(); // ← 非同期。忘れやすい

// TSLを繋ぐのは Node系マテリアル(MeshBasicNodeMaterial / MeshStandardNodeMaterial など)
const material = new THREE.MeshBasicNodeMaterial();

1. uv() ── 模様の起点

平面の左下が (0, 0)、右上が (1, 1) になる座標です。「今どこを塗っているか」を知る手段で、模様はだいたいここから始まります。

material.colorNode = vec4(uv(), 0, 1); // x→赤、y→緑

左下が黒、右に行くほど赤、上に行くほど緑になります。この絵が出れば環境は正しく動いています。 TSLのHello Worldとして最初に出すのがおすすめです。

2. time ── 動かすなら必ず要る

経過秒数が入ったノードです。関数ではなく、ノードそのものなのがポイント(旧 timerLocal() は呼び出し形式でした)。

const band = sin(uv().x.mul(6).sub(time.mul(2))).mul(0.5).add(0.5);
material.colorNode = vec4(mix(color(0x1b2028), color(0x4fc3f7), band), 1);

GLSLだと uTime を自分で作って、毎フレーム material.uniforms.uTime.value = clock.getElapsedTime() と流し込んでいましたよね。TSLは書くだけで勝手に進みます。 描画ループに何も足さなくていいのが地味に気持ちいいところです。

time.mul(2) で倍速、time.mul(0.3) でゆっくり。速度調整は掛け算です。

3. Fn() ── 処理を関数にまとめる(旧 tslFn

同じ処理を何度も使うときは関数にできます。ここがGLSLに対するTSLの一番の強みだと思っています。

const circle = Fn(([center, radius]) =>
  smoothstep(radius, radius.sub(0.02), uv().distance(center))
);

const a = circle(vec2(0.35, 0.5), float(0.18));
const b = circle(vec2(0.68, 0.5), float(0.12));
material.colorNode = vec4(mix(vec3(0.1, 0.12, 0.15), vec3(1, 0.55, 0.25), a.add(b)), 1);

引数は配列で受け取る([center, radius]) =>)のが独特ですが、これは「JSの関数」なので、別ファイルに切り出して import できます。GLSLだと文字列連結でやっていた部品化が、普通のモジュールの話になるわけです。

4. uniform() ── JS側から値を渡す

JSの世界とシェーダーの世界をつなぐ口です。

const level = uniform(0.5);

material.colorNode = vec4(mix(color(0x1b2028), color(0x7cf6a0), step(uv().y, level)), 1);

// 描画ループの中で
level.value = 0.5 + 0.45 * Math.sin(performance.now() / 900);

.value を書き換えれば画面に反映されます。マウス座標、スクロール量、GUIのスライダー……外の世界の値はすべてここから入れます。

5. mix() ── 2つを混ぜる

グラデーションの基本にして、たぶん一番よく使う関数です。

material.colorNode = vec4(mix(color(0xff8a3d), color(0x3d7bff), uv().x), 1);

第3引数が 0 なら1色目、1 なら2色目、0.5 なら半々。ここに uv()sin(time) を突っ込むと、それだけで「動くグラデーション」になります。

6. step() / smoothstep() ── 境目を作る

輪郭やマスクを作るときの2択です。

const d = uv().distance(vec2(0.5)); // 中心からの距離
const hard = step(d, 0.3);             // パキッと切る
const soft = smoothstep(0.3, 0.18, d); // ふわっと切る

デモの6番目のマスは、左半分が step、右半分が smoothstep です。並べると差が一目で分かります。

僕の体感だと、実際の作品で使うのはほぼ smoothstep です。step はジャギが出やすいので、輪郭をわずかにぼかすだけで一気に見栄えが良くなります。

7. sin() / fract() ── 波と繰り返し

sin() は波、fract() は「小数部だけ取る」=繰り返しです。

const stripe = fract(uv().x.mul(6).add(sin(uv().y.mul(8).add(time)).mul(0.15)));
material.colorNode = vec4(mix(vec3(0.1, 0.12, 0.15), vec3(0.95, 0.75, 0.35), stripe), 1);

fract(uv().x.mul(6)) で「0→1」が6回繰り返される。そこに sin を足すと縞がうねる。掛ける数字が模様の密度、足す sin が歪み、という感覚を掴むと、あとは数字遊びで無限に作れます。

8. positionLocal ── 頂点そのものを動かす

ここまでは色の話(フラグメント)でしたが、形そのものも動かせます。

const wave = sin(positionLocal.x.mul(12).add(time.mul(2))).mul(0.06);
material.positionNode = positionLocal.add(vec3(0, wave, 0));

差し込み先が colorNode ではなく positionNode になるのがポイントです。

そしてもう1つ、ジオメトリを分割しておかないと何も起きません。

new THREE.PlaneGeometry(1, 1, 64, 64); // ← 分割数

PlaneGeometry(1, 1) は頂点が4つしかないので、いくら波を足しても四隅が動くだけ。「コードは合っているのに平らなまま」のときは、だいたいこれです。

9. texture() ── 画像を読む

const tex = new THREE.TextureLoader().load("/brick.jpg");
material.colorNode = texture(tex, uv().add(vec2(time.mul(0.1), 0)));

第2引数にUVを渡せるので、UVをずらせば画像が流れますtex.wrapS = tex.wrapT = THREE.RepeatWrapping を付けておけば、端まで行ったらループします。

10. colorNode / positionNode ── 差し込み口

最後は関数ではなく、繋ぎ先の話です。ここまでのコードが全部 material.colorNode = ... で終わっていたのは偶然ではありません。

material.colorNode = /* 色として出したいノード */;
material.positionNode = /* 頂点をどこに動かすか */;

TSLのノードは、作っただけでは何も起きません。 sin(time) と書いても、それは「式を組み立てた」だけ。マテリアルのどこかに繋いで初めて画面に出ます。

僕はここで一度ハマりました。「コードは書いたのに真っ黒」の半分はこれです。

組み合わせ例:uv + sin + mix だけで作る波

10個のうち3つだけで、それっぽい絵になります。

uv・sin・mixだけで作った波打つグラデーション
const speed = uniform(0.6);
const amount = uniform(0.12);

// 「波の高さを返す」関数を部品にする
const wave = Fn(([p, freq, offset]) =>
  sin(p.x.mul(freq).add(time.mul(speed)).add(offset)).mul(amount)
);

// 周波数の違う波を3本足す = 自然なうねり
const h = wave(uv(), 6, 0)
  .add(wave(uv(), 11, 1.7))
  .add(wave(uv(), 17, 4.2));

const band = smoothstep(0.0, 0.35, uv().y.sub(0.5).add(h));
const base = mix(color(0x101a2b), color(0xff7a59), band);
const glow = smoothstep(0.06, 0.0, uv().y.sub(0.5).add(h).abs()); // 境界を光らせる

material.colorNode = vec4(base.add(vec3(0.9, 0.7, 0.4).mul(glow)), 1);

ポイントは周波数の違う波を3本足しているところです。1本だとただのサインカーブですが、3本重ねるだけで急に「自然なうねり」に見えます。これはGLSLでも同じテクニックです。

Fn() で部品にしてあるので、波を4本に増やすのも1行です。

つまずきポイント(全部やりました)

a + b では計算できない

これが最大の罠です。

const bad = uv().x + uv().y;  // ❌
const good = uv().x.add(uv().y); // ✅

JavaScriptには演算子オーバーロードがないので、+ で繋ぐとノード同士が文字列結合されます。実際に typeof を取ると string、中身は "[object Object][object Object]" です。

タチが悪いのは、エラーで止まらないことです。そのまま繋ぐと、生成されたWGSLにこんなものが出力されます。

DiffuseColor = vec4<f32>( vec3<f32>( [object Object][object Object], ... ), 1.0 );

コンソールに Error while parsing WGSL という警告が出て、画面は真っ黒。例外は投げられません。真っ黒になったらまずコンソールを見る、が鉄則です。

演算はすべてメソッドで書きます。

GLSLTSL
a + ba.add(b)
a - ba.sub(b)
a * ba.mul(b)
a / ba.div(b)
1.0 - aa.oneMinus()

慣れると uv().x.mul(6).add(time).sin() のように左から右へ読めるので、僕は今はこっちのほうが好きです。

await renderer.init() を忘れる

const renderer = new THREE.WebGPURenderer({ antialias: true });
await renderer.init(); // これ

WebGPUはGPUの確保が非同期なので、初期化を待つ必要があります。忘れると THREE.Renderer: .render() called before the backend is initialized. という警告が出て、やはり例外は投げられないまま何も映りません

トップレベルawaitが使えない場所なら renderer.init().then(...) か、render() の代わりに renderAsync() でも構いません。

素の MeshBasicMaterial に繋いでしまう

TSLを差せるのは Node系マテリアルMeshBasicNodeMaterial / MeshStandardNodeMaterial / MeshPhysicalNodeMaterial など)だけです。three/webgpu から import していれば揃っています。

つまずき一覧

症状原因対処
tslFn is not a functionバージョンで名前が変わったFn() を使う
timerLocal is not a function同上time(呼び出さない)
画面が真っ黒・コンソールにWGSLエラー+* で計算した.add() .mul() にする
何も映らない・init警告が出るawait renderer.init() がない初期化を待つ
ノードを作ったのに変化しないどこにも繋いでいないmaterial.colorNode = ...
頂点が動かないジオメトリの分割数が足りないPlaneGeometry(1, 1, 64, 64)
色が付かない・関数が見つからないthree から import しているthree/webgputhree/tsl から

おまけ:WebGLでも同じ絵が出ます

WebGPURenderer という名前ですが、WebGPUが使えない環境では自動でWebGL2にフォールバックします。

試しに navigator.gpu を消して同じページを開いたら、コンソールに

THREE.WebGPURenderer: WebGPU is not available, running under WebGL2 backend.

と出たうえで、10枚とも寸分違わず同じ絵が出ました。TSLの「1回書けば両方で動く」は、本当にそのまま動きます。

今どちらで描かれているかは、こう書けば取れます。

console.log(renderer.backend.isWebGPUBackend ? "WebGPU" : "WebGL");

余力があれば覚えたいもの

10個を押さえたあと、次に手が伸びるのはこのあたりです。

ノード何に使う
oneMinus()1.0 - x。マスクの反転で頻出
If() / Loop()条件分岐と繰り返し
mx_noise_float()ノイズ。雲・炎・地形の素
normalLocal / positionWorld法線・ワールド座標
screenUV画面基準のUV。ポストエフェクト向き

まとめ

  • TSLのエクスポートは611個あるが、実際に使うのは10個くらい。まず uv()mix()time で動くものが作れる
  • バージョンで名前が変わる。 tslFnFntimerLocaltime。記事を読むときはバージョンを確認する(この記事は r180)
  • import元は three/webgputhree/tsl。マテリアルは Node系
  • 演算子は使えない。 a + b は文字列になり、エラーも出ずに画面が真っ黒になる。.add() .mul() で書く
  • ノードは繋いで初めて画面に出るcolorNode / positionNode
  • await renderer.init() を忘れない
  • WebGPUが無い環境では自動でWebGLに落ちて、同じ絵が出る

早見表と言いつつ、結局は「この10個を組み合わせて遊ぶ」だけの話でした。TSLのいいところは、新しい言語を覚えるのではなく、JavaScriptのメソッドチェーンとして書けるところです。補完も効くので、uv(). と打った瞬間に候補が出てくる。この体験はGLSLの文字列には無かったものです。


📗 シェーダーを基礎からちゃんと学びたい人へ

TSLで書くにしても、uvmixsmoothstep の感覚はGLSLと同じです。色・UV座標・図形(SDF)・頂点アニメーション・ノイズ表現まで、「書いて・確かめて・変えてみる」体験ベースでゼロから学べるKindle本を書きました。
『Three.js × GLSL シェーダー入門 ── 画像を使わず、コードだけで光と質感をつくる』
「数式をひとつ変えたら見た目が変わる」あの感覚を、手を動かしながら掴めます。

👉 Amazonで読む


僕もXでTSLやWebGPUの作品をちょこちょこ上げているので、よかったら@shun_webdesignを覗いてもらえると嬉しいです。それでは、よいシェーダーライフを🌊

しゅん

しゅん

フロントエンドエンジニア / Webデザイナー。 Three.js・WebGLでのWeb表現が専門。CSS Winner / Orpetron で Site of the Day 受賞、Kindle本を2冊出しています。 音楽、映画、芸術を中心としたサブカルが大好き。 お仕事のご相談は、下記サイトの連絡先からお願いします。

← 記事一覧に戻る