TSLよく使うノード・関数 早見表|これだけ覚えれば書ける10個
みなさんこんにちは。フロントエンドエンジニアのしゅん(@shun_webdesign)です。
TSLとは?WebGPU時代の新しいシェーダーの書き方で「TSLはJavaScriptでシェーダーを書ける仕組みです」という話をしました。じゃあ実際に何を覚えればいいのか、というのが今回です。
先に身も蓋もないことを書くと、TSLのエクスポートは611個あります(three.tsl.js を数えました)。ぜんぶ覚えるのは無理だし、覚える必要もありません。
僕が作品を作るときに実際に使っているのは、そのうち10個くらいです。この記事では、その10個を「最小コード+実際の見た目」で並べます。全部、手元で動かして撮ったものです。

この10枚は1ページに並べて動かしています(デモ:
demo/08-tsl/)。以下のコードはすべてそこから抜き出したものです。
Table of Contents
前提:バージョンによって書き方が変わります
ここが一番大事なので最初に書きます。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/webgpu と three/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 |
下ごしらえはこれだけです。以降のコードは、この material の colorNode を差し替えているだけだと思ってください。
const renderer = new THREE.WebGPURenderer({ antialias: true });
await renderer.init(); // ← 非同期。忘れやすい
// TSLを繋ぐのは Node系マテリアル(MeshBasicNodeMaterial / MeshStandardNodeMaterial など)
const material = new THREE.MeshBasicNodeMaterial();
平面の左下が (0, 0)、右上が (1, 1) になる座標です。「今どこを塗っているか」を知る手段で、模様はだいたいここから始まります。
material.colorNode = vec4(uv(), 0, 1); // x→赤、y→緑
左下が黒、右に行くほど赤、上に行くほど緑になります。この絵が出れば環境は正しく動いています。 TSLのHello Worldとして最初に出すのがおすすめです。
経過秒数が入ったノードです。関数ではなく、ノードそのものなのがポイント(旧 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) でゆっくり。速度調整は掛け算です。
同じ処理を何度も使うときは関数にできます。ここが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だと文字列連結でやっていた部品化が、普通のモジュールの話になるわけです。
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のスライダー……外の世界の値はすべてここから入れます。
グラデーションの基本にして、たぶん一番よく使う関数です。
material.colorNode = vec4(mix(color(0xff8a3d), color(0x3d7bff), uv().x), 1);
第3引数が 0 なら1色目、1 なら2色目、0.5 なら半々。ここに uv() や sin(time) を突っ込むと、それだけで「動くグラデーション」になります。
輪郭やマスクを作るときの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 はジャギが出やすいので、輪郭をわずかにぼかすだけで一気に見栄えが良くなります。
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 が歪み、という感覚を掴むと、あとは数字遊びで無限に作れます。
ここまでは色の話(フラグメント)でしたが、形そのものも動かせます。
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つしかないので、いくら波を足しても四隅が動くだけ。「コードは合っているのに平らなまま」のときは、だいたいこれです。
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 を付けておけば、端まで行ったらループします。
最後は関数ではなく、繋ぎ先の話です。ここまでのコードが全部 material.colorNode = ... で終わっていたのは偶然ではありません。
material.colorNode = /* 色として出したいノード */;
material.positionNode = /* 頂点をどこに動かすか */;
TSLのノードは、作っただけでは何も起きません。 sin(time) と書いても、それは「式を組み立てた」だけ。マテリアルのどこかに繋いで初めて画面に出ます。
僕はここで一度ハマりました。「コードは書いたのに真っ黒」の半分はこれです。
組み合わせ例:uv + sin + mix だけで作る波
10個のうち3つだけで、それっぽい絵になります。

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 という警告が出て、画面は真っ黒。例外は投げられません。真っ黒になったらまずコンソールを見る、が鉄則です。
演算はすべてメソッドで書きます。
| GLSL | TSL |
|---|---|
a + b | a.add(b) |
a - b | a.sub(b) |
a * b | a.mul(b) |
a / b | a.div(b) |
1.0 - a | a.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/webgpu と three/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で動くものが作れる - バージョンで名前が変わる。
tslFn→Fn、timerLocal→time。記事を読むときはバージョンを確認する(この記事は r180) - import元は
three/webgpuとthree/tsl。マテリアルは Node系 - 演算子は使えない。
a + bは文字列になり、エラーも出ずに画面が真っ黒になる。.add().mul()で書く - ノードは繋いで初めて画面に出る(
colorNode/positionNode) await renderer.init()を忘れない- WebGPUが無い環境では自動でWebGLに落ちて、同じ絵が出る
早見表と言いつつ、結局は「この10個を組み合わせて遊ぶ」だけの話でした。TSLのいいところは、新しい言語を覚えるのではなく、JavaScriptのメソッドチェーンとして書けるところです。補完も効くので、uv(). と打った瞬間に候補が出てくる。この体験はGLSLの文字列には無かったものです。
- TSLって何? から読みたい方は → TSLとは?WebGPU時代の新しいシェーダーの書き方
- シェーダーの考え方そのものは → GLSL入門|ShaderMaterialで自作シェーダーを書く
- 描画エンジンの載せ替えは → Three.jsでWebGPUを使う|WebGPURenderer移行ガイド
- 全体のロードマップは → Three.js入門ガイド
📗 シェーダーを基礎からちゃんと学びたい人へ
TSLで書くにしても、
uvやmix、smoothstepの感覚はGLSLと同じです。色・UV座標・図形(SDF)・頂点アニメーション・ノイズ表現まで、「書いて・確かめて・変えてみる」体験ベースでゼロから学べるKindle本を書きました。
『Three.js × GLSL シェーダー入門 ── 画像を使わず、コードだけで光と質感をつくる』
「数式をひとつ変えたら見た目が変わる」あの感覚を、手を動かしながら掴めます。
僕もXでTSLやWebGPUの作品をちょこちょこ上げているので、よかったら@shun_webdesignを覗いてもらえると嬉しいです。それでは、よいシェーダーライフを🌊