TSL Implementation Guide
Important: This guide is specifically based on Three.js v0.183.0 . Due to TSL's rapid evolution, even minor version updates may render these patterns and workarounds obsolete.
Core Principle: The Single Source of Truth
TSL (Three.js Shader Language) is in a phase of rapid evolution, where APIs and patterns change frequently. In this environment, the only reliable reference for implementation is the type definitions.
- Absolute Authority of @types/three :
Treat the local type definitions as the source of truth. When implementing or debugging TSL, do not rely on internal knowledge or external summaries. Inspect node_modules/@types/three directly, tracing signatures and return types to ensure alignment with the current API state.
- Avoid Internet-Based Research:
Web-found information (blogs, tutorials) is chronically outdated for TSL. Relying on such data is a primary source of syntax errors and must be avoided.
Proactive Use of .toVar()
TSL intermediate nodes (const x = ... ) are operation references that are inlined in the generated shader code unless .toVar() is used. Missing .toVar() in required places causes critical logic errors and performance degradation.
When updating values via .assign() :
Use .toVar() for any variable that needs to be updated (reassigned) or maintain state throughout an algorithm. .assign() cannot be called on nodes that haven't been materialized with .toVar() .
// NG: const count = float(0); count.addAssign(1);
// OK: const count = float(0).toVar(); count.addAssign(1);
When a calculation is referenced two or more times:
Excluding simple literals or swizzles, always use .toVar() to store a result if it is used in multiple places. Failure to do so causes the operation graph to be duplicated and re-executed in the output shader, significantly degrading performance.
// NG: pow() is duplicated in the generated shader const color1 = baseColor1.mul(pow(d, 5.0)); const color2 = baseColor2.mul(pow(d, 5.0));
// OK: Calculated once and reused const intensity = pow(d, 5.0).toVar(); const color1 = baseColor1.mul(intensity); const color2 = baseColor2.mul(intensity);
Use .toVar() to balance between state management and code optimization. If a calculation is referenced only once and requires no updates, omit .toVar() to produce clean, inlined shader code. Otherwise, prioritize .toVar() to prevent logic errors and redundant calculations.
Node Type Definition
Import the Node type from three/webgpu :
import { type Node } from "three/webgpu";
For unions of Node types, use Node<A> | Node<B> instead of Node<A | B> . The latter often triggers compile errors (e.g., by resolving to 'never').
// NG: Fails to compile type FloatOrVector = "float" | "vec2" | "vec3" | "vec4"; Fn(([a, b]: [Node<FloatOrVector>, Node<FloatOrVector>]) => { return a.add(b); // never });
// OK: At least it compiles, though type inference is poor type FloatOrVectorNode = | Node<"float"> | Node<"vec2"> | Node<"vec3"> | Node<"vec4">; Fn(([a, b]: [FloatOrVectorNode, FloatOrVectorNode]) => { return a.add(b); // Node<"vec4"> });
Fn Function Inference
Fn 's default type inference is unreliable and often too loose. Manually define the function signature with as unknown as to enforce strict type safety and prevent loss of specific type information.
Fn(([a, b]: [FloatOrVectorNode, FloatOrVectorNode]) => { return a.add(b); }) as unknown as { <T extends FloatOrVectorNode>(a: T | number, b: T | number): T; (a: number, b: number): Node<"float">; };
Math Function Vector Issues
Important: This is a fallback workaround. Only apply this if you encounter a type error when passing a vector to a function that should support it.
Some mathematical functions are defined to accept only scalar inputs, even though they support vectors in practice. Use as any to bypass the incorrect scalar-only definition and restore the correct Node type.
const v = vec3(1, 4, 9); // eslint-disable-next-line @typescript-eslint/no-explicit-any const res = sqrt(v as any) as unknown as Node<"vec3">;