An Introduction to TSL

TSL (the Three.js Shading Language) lets you build shaders with TypeScript function calls. You combine nodes such as vec3, normalView and mix, and Three.js compiles them for WebGL or WebGPU.

This gives us autocompletion, type checking and ordinary functions for sharing shader code. Reusing GLSL often involves manipulating strings, so this is a useful improvement.

Let's build up from a flat colour to a torus knot with a bright rim. The examples use WebGPURenderer from three/webgpu with React Three Fiber.

A flat colour

First, a flat colour. meshBasicNodeMaterial uses colorNode to set the surface colour. Here that node returns a constant vec3:

import { vec3 } from 'three/tsl'

const colorNode = vec3(0.2, 0.55, 0.9)

// ...
<mesh>
  <sphereGeometry args={[1.2, 64, 64]} />
  <meshBasicNodeMaterial colorNode={colorNode} />
</mesh>

Note that vec3(0.2, 0.55, 0.9) is a node rather than a runtime value. TSL compiles it into the shader as a constant.

Visualising position and normal

These two icosahedrons use positionLocal and normalView to colour their surfaces. positionLocal gives each point's position in the shape's own coordinates.

normalView is the surface normal, a unit vector perpendicular to the surface, expressed in camera coordinates. Its Z component tells us how directly the surface faces the camera.

Rotate the shapes to compare them. The position colours stay attached to the shape; the normal colours change as each face turns towards or away from the camera.

Fresnel lighting

The Fresnel effect describes how reflection changes with viewing angle. A surface generally reflects more light at a glancing angle than when viewed straight on.

You can see this on a lake: looking down into the water, you can see beneath the surface; looking across it, the reflections are much stronger.

We can use a cheap approximation in real-time graphics:

fresnel = (1 - cos(theta))^k

where theta is the angle between the view vector and the surface normal, and k is a sharpness exponent.

With a view direction of (0, 0, 1), we can use 1 - |normalView.z| as a simple Fresnel mask. It is zero for a surface facing the camera and one for a surface viewed edge-on.

import { normalView, oneMinus, abs, vec3 } from 'three/tsl'

const fresnel = oneMinus(abs(normalView.z))
const colorNode = vec3(fresnel, fresnel, fresnel)

oneMinus(x) is the TSL equivalent of 1.0 - x, and abs(x) is its absolute value.

Putting it together

Now let's use the mask to blend two colours. There are three steps:

  1. Wrap the logic in Fn(() => ...) so it becomes a reusable TSL function.
  2. Sharpen the fresnel falloff with pow(..., 2.5). A higher exponent pushes the bright band closer to the silhouette and makes the glow tighter.
  3. mix between a deep 'core' colour (where the surface faces the camera) and a bright 'rim' colour (at glancing angles).

We will use a torus knot for a more interesting result.

import {
  Fn,
  normalView,
  oneMinus,
  abs,
  pow,
  mix,
  vec3,
} from 'three/tsl'

const fresnelGlow = Fn(() => {
  const core = vec3(0.02, 0.18, 0.35)
  const rim = vec3(0.5, 0.95, 0.8)
  const fresnel = pow(oneMinus(abs(normalView.z)), 2.5)
  return mix(core, rim, fresnel)
})

const knotColorNode = fresnelGlow()

// ...
<mesh>
  <torusKnotGeometry args={[1, 0.32, 220, 32]} />
  <meshBasicNodeMaterial colorNode={knotColorNode} />
</mesh>

Where next?

TSL also has positionNode and normalNode for changing geometry and normals, plus nodes for reading vertex attributes. You can organise reusable helpers with Fn in ordinary TypeScript modules.

Try changing the colours and exponent in this example. A small change to the rim can give the same shape quite a different appearance.

At the time I wrote this, React Three Fiber 10 was in early alpha, with better built-in TSL support expected.

Alternatives

I've also tried TypeGPU. In my experiments it felt less mature and didn't work well with Next.js.

TypeGPU only supports WebGPU. TSL supports WebGL too, though it comes with the larger Three.js dependency.

Learn More

  • I've been working on a fairly comprehensive book of TSL examples, with around 100 progressively complex examples covering fragment shaders, vertex shaders and techniques like SDFs.
  • The official TSL documentation is the best reference.
  • The Three.js examples include many node-material demos that are worth reading.