Essentially, a node is a placeholder for a unit of GPU computation, a representation of anything from a basic arithmetic operation, a lighting algorithm, a struct, a line of code, a shader, or a series of post-process passes. The specifics of how any given node works aren’t really important to understand. Nonetheless, they are the core building blocks used to write shaders and achieve a wide variety of effects within the WebGPU paradigm of the Three.js API.
Nodes are written and modified either inline or within TSL code blocks. TSL stands for Three Shading Language, an intermediary shader format that translates Three.js nodes into WGSL or GLSL code, depending on whether the WebGPURenderer deploys its WebGPU or WebGL backend.
Wait, the WebGPURenderer has a WebGL backend? Yes, it does! If the renderer detects that the device doesn’t support WebGPU, it will automatically fall back to WebGL, ensuring that projects built with WebGPU in mind can still run on a broader range of devices. TSL isn’t just a simple compatibility layer; it also abstracts much of the setup and syntax needed to deploy shaders. Consequently, unless you need to use specific features not yet supported by the node system, TSL is the recommended and way to write shaders in Three.js.
So how do we write a shader in TSL? Let’s start off with the simplest possible shader we can write, a fragment shader that outputs texture UVs to the surface of a mesh. To modify our mesh’s material using TSL shaders, we’ll need to change it from a MeshBasicMaterial to a MeshBasicNodeMaterial. As you can imagine, the MeshBasicNodeMaterial is just an extension of the feature set provided by its namesake class, allowing its properties to be define by nodes rather than traditional means. Accordingly, this change will not yet alter the visual output of your scene.
// Old version:
// const material = new THREE.MeshBasicMaterial( { map: texture } );
// New version:
const material = new THREE.MeshBasicNodeMaterial( { map: texture } );
With this new material type, we can manipulate the fragment values the mesh material outputs by modifying the NodeMaterial’s fragmentNode property. First, import the uv() function from the ‘three/tsl’ directory to access a generic UV range. Then, write a TSL function which returns the value of uv(). Finally, assign that TSL function as the value of our material’s fragmentNode property. By assigning this function to fragmentNode, we apply our function as the new fragment shader of the material. Pay special attention to some of the syntax of how a TSL function is created, including the need to call the function as it is defined in order for the shader to properly execute.
import { tslFn, uv } from 'three/tsl'
const material = new THREE.MeshBasicNodeMaterial( { map: texture } );
// TSL code blocks are created within a call to the tslFn() or Fn() function
const returnUV = tslFn( () => {
return uv();
} );
material.fragmentNode = returnUV();
With shorter shaders like these, we can actually simplify our syntax and return a value without explicit function brackets.
// Change from this code.
const returnUV = Fn( () => {
return uv();
} );
material.fragmentNode = returnUV();
// ...to this code.
material.fragmentNode = uv();
Whenever possible, try to inline node operations to make them concise and readable.
// When possible, prefer this...
material.fragmentNode = uv().distance(vec2(0.5, 0)).oneMinus().mul(3);
// ...over this.
const fragmentShaderTSL = Fn(() => {
const uvNode = uv();
const uvDistance = uvNode.distance(vec2(0.5, 0));
const scaledDistance = distance.oneMinus().mul(3);
})
material.fragmentNode = FragmentShaderTSL();