Course outline
TSL Basics
In one line
TSL, the Three.js Shading Language, builds shader logic from JavaScript functions: Fn, uniform, time, uv, mix, sin. One program becomes WGSL on WebGPU and GLSL on WebGL 2. Its documentation is thin, so you learn to read the source.
Think of it like this
Sheet music is written once, in notes, and any musician plays it on their own instrument: a violin or a piano. TSL is the sheet music for shaders. You write the effect once, in JavaScript, and three.js plays it on the instrument that is there: WGSL for WebGPU, GLSL for WebGL 2.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- TSL
- Three.js Shading Language: shader logic written as JavaScript calls that build node graphs.
- WGSL
- WebGPU Shading Language: the shader language of WebGPU.
- GLSL
- OpenGL Shading Language: the shader language of WebGL 2 (Topic 12.1).
- Fn
- A TSL helper that wraps a function so it becomes a reusable piece of shader.
- swizzle
- Picking or reordering the parts of a vector by letters:
p.xxymakes a new vector from p's x, x and y. - linear light
- The colour space the shader does its maths in. Hex colours are converted into it, then converted back for the screen.
Step by step
01One line of TSL
mix(color("#2a9df4"), color("#ff6b2c"), step(0.5, fract(uv().x.mul(8)))) reads from the inside out: take the horizontal uv, make it run 0 to 8, keep the fractional part (so 0 to 1 eight times), turn it into 0 or 1 at the half, and use that to choose blue or orange. Eight stripes.
mix(
color("#2a9df4"),
color("#ff6b2c"),
step(0.5, fract(uv().x.mul(8))),
)02Read what it became
The first example asks the renderer for the shader it generated. The line printed on WebGPU and on WebGL 2 is the same maths, in each language's spelling. See the two code blocks below it.
03Reuse with Fn and change with uniform
The second example builds pulse once and uses it twice, for brightness and for size. speed is a uniform, so one assignment changes the animation. It also checks prefers-reduced-motion and sets the speed to 0 for visitors who asked for less movement.
04Find a function in the source
When the guide has no page for a function, search the source. The terminal below finds where smoothstep is defined and shows that its doc comment lists its three parameters, in the order low, high, x.
Try it yourself
- 1
Turn the stripes
In the first example change
uv().xtouv().y. The stripes become horizontal. Change the8to3and there are three. The printed line changes its8.0to3.0, andnodeVarying4.xto.y. - 2
Stand still
In the pulse example add
speed.value = 0;after theuniformline. The ball stops:sin(0)is 0, so the pulse is exactly 1 and the colour and size stay at their base values. This is what the reduced-motion branch does.
Code & diagrams
Prints WGSL on WebGPU and GLSL on WebGL 2, so no `output` is set. The stripes are drawn either way.
Console
Run the scene to see its console.Same maths. WGSL spells vector types `vec3<f32>`, GLSL spells them `vec3`. The colours are already in linear light.
// WebGPU (WGSL), from the example above:
DiffuseColor = vec4<f32>( mix( vec3<f32>( 0.023153366173251363, 0.3371636150376657, 0.9046611743890203 ), vec3<f32>( 1.0, 0.14702726648767014, 0.025186859622305935 ), step( 0.5, fract( ( nodeVarying4.x * 8.0 ) ) ) ), 1.0 );
// WebGL 2 (GLSL), same example:
DiffuseColor = vec4( mix( vec3( 0.023153366173251363, 0.3371636150376657, 0.9046611743890203 ), vec3( 1.0, 0.14702726648767014, 0.025186859622305935 ), step( 0.5, fract( ( nodeVarying4.x * 8.0 ) ) ) ), 1.0 );The ball pulses in brightness and breathes in size. In a browser set to reduce motion, `speed` is 0 and it stands still. No `output` because of the fallback warning.
Console
Run the scene to see its console.`forceWebGL` keeps the output the same everywhere. The last line is a statement of what the picture shows: the left half is blue, the right half orange.
Console
Run the scene to see its console.Expected console output
JS a + b: [object Object][object Object]
TSL a.add(b): a node: a recipe, not a number
JS if (node): branch taken, whatever the GPU will compute
drawn: blue on the left half, orange on the rightBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Use Math.sin on a node
In the pulse example change sin(time.mul(speed)) to Math.sin(time.mul(speed)).
Break #2
Leave out an argument
In the pulse example change pulse(speed, 0.08) to pulse(speed).
Myth vs fact
Myth
TSL is just a nicer way to write GLSL.
Fact
It is a graph builder. The same program becomes WGSL or GLSL, and three.js can also change the graph for you, for example to add fog, tone mapping or shadows.
Myth
I can use normal JavaScript maths on nodes.
Fact
Only the node functions and methods. JavaScript operators and Math.* calls do not build nodes.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Because the output depends on the backend, test a TSL effect on both:
forceWebGL: trueon a machine with WebGPU is the quickest way. A difference points at a driver, an unsupported node, or float precision. - ▸
In the generated shader the colour literals are already linear. If you read colours off the printed shader and compare them with your hex values, the numbers will not match: that is colour management, not a bug.
- ▸
getShaderAsync()is a debug tool, not for production: it compiles the object first to get the text. Use it while developing, and log the result only when something looks wrong.
Remember this
- 1
The problem this solves: GLSL (WebGL) and WGSL (WebGPU) are two different languages. Writing every effect twice, and keeping both in step, is a recipe for bugs. TSL gives one source that works on both backends, and it is plain JavaScript, so you get functions, imports and editor help for free.
- 2
The pieces. Everything comes from
three/tsl. Values:float(1),vec3(1, 0.4, 0.2),color("#ff6b2c"),uniform(3). Inputs the renderer fills in:uv(),time(seconds),positionLocal,normalView. Maths:sin,mix,step,fract,smoothstep,dot,normalize. Operators are methods:a.add(b),a.mul(b),a.sub(b),a.pow(2). Swizzles work as in GLSL:p.xxy,v.yxz. - 3
Fn makes your own function.
const pulse = Fn(([speed, depth]) => sin(time.mul(speed)).mul(depth).add(1))is a reusable piece of shader. Its arguments come as an array, and callingpulse(speed, 0.3)returns a node. InsideFn,.toVar()makes a variable you can change with.assign()and.addAssign(), andIf(...)is a branch that runs on the GPU. - 4
Two JavaScript habits that bite. A JavaScript operator on nodes does not add them:
a + bgives the text[object Object][object Object]. And a JavaScriptif (node)is always true, because the node is an object: useIf()orselect(). The third example shows both.Math.sin(node)is wrong too: it returnsNaN, and then.mulfails (see the break-it below). - 5
Colours are converted.
color("#2a9df4")is an sRGB hex. The shader works in linear light, so three.js converts it: the generated shader contains0.0231, 0.3372, 0.9047, not the 42, 157, 244 of the hex. If you pass numbers tovec3, they are used as they are, in linear. - 6
Documentation is thin, so read the source. The TSL guide is short and changes between releases. The truth is in
node_modules/three/src/nodes/and the list of everything exported isnode_modules/three/src/Three.TSL.js. Every export has a doc comment with its parameters. Andrenderer.debug.getShaderAsync()shows the shader text TSL became, which is how you debug a graph.
Explain it without notes
Why can one TSL effect run on both WebGPU and WebGL 2?
A teammate writes material.colorNode = a + b with two nodes. What do they get and how do they fix it?
Practice
Write a Fn called stripes that takes a count and returns 0 or 1 across the uv.x, and use it for a plane's colorNode.
Print the vertex shader of a mesh with a positionNode and find the line that sets positionLocal.
Done when you can
Write a colour effect in TSL with
uv(),mixandstepMake a reusable
Fnand drive it with auniformSpot the JavaScript habits that do not build nodes
Read the generated WGSL or GLSL with
getShaderAsyncFind a TSL function's definition in
node_modules/three/src/nodes/