45
←→

How to learn TSL, the Three.js Shading Language

A practical path into TSL: translating GLSL you already know, getting used to chained operators, wrapping code in Fn, reading the compiled output, and writing a first compute shader.

Loading...
←WebGPU vs WebGL for creative coding
←

Ready to start learning?

What's included in the course ↓

Access to the course: master shader techniques, use workflow-enhancing utilities, learn from shader breakdowns with full code, and get downloadable R3F and vanilla projects.

One single payment. No subscription required. 30-day money-back guarantee. No questions asked.

Not sure which to pick? Most people choose Pro for the full collection and all future updates. Fundamentals is a focused, lower-cost starting point you can upgrade from any time.

“Knowing shaders is your unreasonable advantage in UI. Ben has worked super hard on this course, recommended!”
Joshua Crowley - Designer and Educator
Loading...

Fragments

Learn creative coding with shaders. For design engineers, creative coders and shader artists: techniques, tools, deep dives. Powered by ThreeJS and TSL.

New techniques, breakdowns and shader experiments — straight to your inbox.

2026 Phobon

phobon.ioartifice.shshadercraft

Pages

HomeTechniquesUtilitiesBreakdownsWorksWriting

Contact

X @thenoumenonhey@fragments.supplyOKAY DEV @phobon
All rights reserved.
1st anniversary sale: 33% off until Sept 17
Curriculum
Works171
Writing45
1st anniversary sale: 33% off until Sept 17

What TSL is

TSL, Three.js Shading Language, is a way of writing shaders in JavaScript. Instead of handing Three.js a string of GLSL or WGSL, you build the shader out of function calls: vec3(uv(), 0.0), sin(time.mul(2.0)), mix(a, b, t). Three compiles that node graph to WGSL when it's running on WebGPU and to GLSL when it falls back to WebGL. One shader, both backends. No string templating.

Every shader in Fragments is written in TSL, and I've taught a lot of people to write it over the past year. This is the path I'd give someone starting today, including the bits that trip people up.

Why it's worth learning at all

TSL will feel weird if you already write GLSL. Worth it anyway.

Reuse. A TSL function is a JavaScript function. You can import it, compose it, pass it around. The noise functions, SDF shapes and post-processing effects in Fragments are all just modules. In GLSL that reuse meant string concatenation and #include hacks.

Two backends for free. WebGPU where available, WebGL 2 where not. You don't maintain two shaders.

Compute shaders. TSL is the on-ramp to WebGPU compute in Three.js. Fn(...).compute(count). That's how you write particle systems, cellular automata and pixel sorting that were painful or impossible in WebGL.

It makes you better at GLSL and WGSL. This surprised me. Writing TSL means thinking about nodes and data flow, and I read raw shader code more fluently now than before.

The cost is a learning curve, even for people who already write shaders, and a smaller pool of tutorials than GLSL has. The official TSL guide has closed a lot of that gap in the last year; I lean on it below.

Step 0: have somewhere to type

Don't build an environment. Use one.

  • The TSL guide. threejs.org/tsl is the reference: syntax, inputs, compute, post-processing, all with runnable examples.
  • Three.js examples. The official TSL examples are runnable and you can read every one.
  • A boilerplate. Fragments boilerplate for React Three Fiber, or the Vite walkthrough for vanilla. Either gets you a WebGPURenderer, a camera and a full-screen plane, which is all a shader needs.

The plane-plus-fragment-shader setup is deliberate. Almost everything in Fragments is done on a single PlaneGeometry with a colorNode. Geometry comes later, if at all.

A lot of my own TSL time lately has gone into a creative coding tool I've been building for exactly this loop - type a Fn, see it, tweak it, chain it into something else - because I got tired of the setup tax every time I wanted to try an idea. It's not ready to talk about properly yet, but it's shaped everything in this post. More on that soon.

Step 1: rewrite something you already know

The fastest way in is to take a GLSL snippet you understand and translate it. UV gradient first:

GLSL
void main() {
  gl_FragColor = vec4(v_uv, 0.0, 1.0);
}
TSL
import { MeshBasicNodeMaterial } from 'three/webgpu'
import { uv, vec3 } from 'three/tsl'
 
const material = new MeshBasicNodeMaterial()
material.colorNode = vec3(uv(), 0.0)

Then something with time. There's no uniform to declare or update. time is a built-in node:

Animating with time
import { uv, vec3, sin, time } from 'three/tsl'
 
material.colorNode = vec3(uv(), sin(time).mul(0.5).add(0.5))

The TSL primer has a longer run of these side-by-sides, including loops and conditionals.

If you have a pile of existing GLSL, you don't have to translate it by hand. glslFn from three/tsl wraps a GLSL function so it runs inside a TSL graph on the WebGL backend, and Three's transpiler converts GLSL to TSL or WGSL. Running a Shadertoy snippet through the transpiler and reading what comes out is a very fast way to learn the idioms.

Step 2: get used to the chaining

This is the habit that bites everyone. In GLSL you write a * b + c. In TSL, operators are methods:

Operators are methods
// GLSL:  float d = length(uv - 0.5) * 2.0 - 0.3;
const d = length(uv().sub(0.5)).mul(2.0).sub(0.3)

A few rules that make this stop feeling weird:

  • Every node has .add, .sub, .mul, .div, plus comparisons like .greaterThan and swizzles like .xy or .r
  • Math functions are imported from three/tsl and take nodes or plain numbers. mix(a, b, 0.5) is fine
  • Reassignment is .assign(), accumulation is .addAssign(). You'll use these inside Fn bodies and compute shaders
  • Read chains left to right. a.mul(b).add(c) is (a * b) + c

The guide's Method Chaining, Swizzle and Operators pages are the full list.

After a couple of hours this becomes automatic. After a week you'll stop translating from GLSL in your head.

Step 3: wrap things in Fn

Fn is how you make a reusable shader function (guide). It takes a JavaScript function and returns a node factory:

A reusable circle SDF
import { Fn, length, float } from 'three/tsl'
 
export const circle = Fn(([p, radius]) => {
  return length(p).sub(radius)
})
 
// Later, anywhere:
const d = circle(uv().sub(0.5), float(0.3))

Once you have a handful of these, you import them instead of rewriting them. The SDF Shapes and SDF Operations pages are essentially a library of Fns, and the geometric shapes technique, which is free, builds a full pattern out of them step by step. That's the lesson I'd do first.

Step 4: learn to read the output

When TSL does something you didn't expect, look at what it compiled to. Attach .debug() to any node and TSL prints the generated WGSL or GLSL for that expression, with the surrounding shader for context (guide). Reading it answers most "why is this black" questions quickly. The Debugging TSL page covers the workflow, and Runtime Tweaking shows how to hook uniform() nodes up to a control panel so you can poke values live instead of editing and reloading.

This is the step people skip and then get stuck. Ten minutes learning to inspect output saves hours.

Step 5: your first compute shader

Once fragment shaders feel comfortable, do one compute shader. It's the thing TSL makes easy that WebGL never did.

The smallest useful compute shader
import { Fn, instancedArray, instanceIndex, hash } from 'three/tsl'
 
const count = 1024
const values = instancedArray(count, 'float')
 
const fill = Fn(() => {
  values.element(instanceIndex).assign(hash(instanceIndex))
})().compute(count)
 
await renderer.computeAsync(fill)

That's a GPU-side array filled with a thousand random numbers in parallel. From there, a flow field is a grid of angles, a particle buffer and two more Fns. The guide's Compute Stage and Storage pages cover workgroups and the buffer API when you need them.

Things that trip people up

  • Mixing plain numbers and nodes. Fine as arguments, not as the receiver. float(2.0).mul(x), not 2.0.mul(x).
  • Forgetting () on uv. It's uv(), a function that returns the node, not a constant.
  • Conditionals. Use If(cond, () => {...}) and select(cond, a, b), not JavaScript if. JS if runs once at graph-build time, not per pixel. If also has to live inside a Fn body; it relies on the function's execution stack to build the branch.
  • Loops. Loop(count, ({ i }) => {...}). Same reason.
  • Colour space. WebGPURenderer outputs sRGB by default. If your colours look off compared to a Shadertoy reference, the Color Space Correction utility is usually the answer.
  • Precision of hash/time. time grows forever. fract(time) or time.mod(...) when you need it bounded.

A rough order to work through

If you want a sequence rather than a pile of links:

  • Shader Vocabulary for the words
  • TSL primer for the syntax
  • the free Geometric Shapes lesson for a complete build
  • Procedural Color Palettes and Noise, because they're what make everything look good
  • Flow Field for compute

Give it two weekends. The first one is awkward. The second one, you'll be reaching for TSL by default.

ProMost popularFull access to the entire Fragments collection. Includes all techniques, utilities, breakdowns and all future updates
  • ✓ 12 long-form technique lessons
  • ✓ 7 fundamentals lessons
  • ✓ 60 workflow enhancing utilities
  • ✓ 171+ full shader breakdowns
  • ✓ Downloadable R3F and Vanilla projects
  • ✓ Access to community Discord
Get Pro →$199$133USD
FundamentalsAccess to foundational shader techniques and utilities
  • ✓ 5 foundational long-form technique lessons
  • ✓ 7 fundamentals lessons
  • ✓ 24 foundational workflow enhancing utilities
  • ✓ 71 full shader breakdowns
  • ✓ Downloadable R3F and Vanilla projects
  • ✓ Access to community Discord
Get Fundamentals →$99$66USD
You'll be redirected to our secured payment platform and get instant access.