TSL Shaders
TSL shaders are JavaScript files that describe GPU shading with Three.js Shading Language (TSL). They are editor assets, not Lua scripts: you create and edit them in Studio, and the engine compiles them when a material or scene background links them in.
Use TSL shaders when built-in PBR materials are not enough — stylized water, animated backgrounds, vertex wind on grass cards, custom post-style color grading on a surface, and similar effects. TSL is the supported way to author custom shaders; legacy GLSL shader items cannot be created for new projects.
Creating a TSL shader
- In the asset browser, create a TSL Shader item (or pick a template such as Lit material, Background gradient, Vertex wind, or Water).
- Open
shader.tslin the code editor. - Link the shader from a Material (
Material type= Node, then TSL Shader link) or from a Scene item (Background mode = Shader, then Background TSL shader link).
The file must return an object describing node outputs and uniforms. The engine evaluates the source in a restricted scope where tsl is the only global import.
Minimal example
const { Fn, uv, vec4, uniform, float, vec2 } = tsl;
const iTime = uniform(float(0));
const iResolution = uniform(vec2());
const fragmentNode = Fn(() => {
const c = uv().flipY();
return vec4(c.x, c.y, 0.5, 1.0);
})();
return {
uniforms: { iTime, iResolution },
fragmentNode,
};
Shader graph outputs
The returned object may contain:
| Key | Purpose |
|---|---|
uniforms |
Map of TSL uniform nodes; exposed in the Inspector when used |
colorNode |
Surface color for material TSL shaders (preferred) |
positionNode |
Vertex displacement or animation |
fragmentNode |
Full-screen fragment color (background shaders) |
vertexNode |
Rare; full vertex stage override |
At least one of colorNode, positionNode, fragmentNode, or vertexNode is required.
Materials usually return colorNode and optionally positionNode. Scene backgrounds return fragmentNode.
Uniforms and the Inspector
Any uniform you list under uniforms (except built-in engine uniforms) becomes an editable field on the Material or Scene that links the shader.
Supported uniform value types and how they appear in the Inspector:
| TSL uniform | Inspector type |
|---|---|
float(...) |
Number |
vec2(...) |
Vec2 |
vec3(...) |
Vec3 / color |
vec4(...) |
Vec4 |
uniformTexture() |
Link (Image or Composition) |
Use uniformTexture() for texture samplers. Link an Image or a Composition (color output) in the material Inspector. For composition links you can choose which output slot to sample (for example color, depth, or normal).
Built-in uniforms
The engine updates these automatically when declared in uniforms:
| Name | Description |
|---|---|
iTime |
Elapsed time in seconds |
iResolution |
Render resolution (width, height) in pixels |
iRenderViewport |
Active render rectangle: xy = top-left origin, zw = size in physical pixels. Use with screenCoordinate for screen-space UVs when the 3D view is docked (not fullscreen). |
iMatrixCamera |
Camera matrix (when used) |
Declare iRenderViewport as uniform(vec4()) in your shader; the engine replaces it with a mutable Vector4 when the material loads.
Material example
Set Material type to Node, link a TSL Shader, then assign the material to a mesh.
const { Fn, vec3, vec4, uniform, float, dot, max, normalLocal } = tsl;
const baseColor = uniform(vec3(0.25, 0.55, 0.35));
const lightDir = uniform(vec3(0.4, 1.0, 0.25));
const ambient = uniform(float(0.2));
const colorNode = Fn(() => {
const N = normalLocal.normalize();
const L = lightDir.normalize();
const ndotl = max(dot(N, L), ambient);
return vec4(baseColor.mul(ndotl), 1.0);
})();
return {
uniforms: { baseColor, lightDir, ambient },
colorNode,
};
For transparent surfaces (water, glass), enable Transparent on the Material item and return alpha from colorNode. Turning off Depth write often looks better for water.
Background example
Link the shader from the Scene item when Background mode is Shader.
const { Fn, uv, vec2, vec3, vec4, uniform, float, sin, mix, length, smoothstep } = tsl;
const iTime = uniform(float(0));
const iResolution = uniform(vec2());
const fragmentNode = Fn(() => {
const coord = uv().flipY();
const t = iTime.mul(0.15);
const sky = mix(vec3(0.08, 0.12, 0.35), vec3(0.45, 0.65, 0.95), coord.y);
const sunPos = vec2(0.72).add(vec2(sin(t).mul(0.08), 0.0));
const sun = smoothstep(float(0.08), float(0.0), length(coord.sub(sunPos)));
return vec4(sky.add(vec3(1.0, 0.85, 0.5).mul(sun)), 1.0);
})();
return {
uniforms: { iTime, iResolution },
fragmentNode,
};
Use uv().flipY() for full-screen backgrounds so the pattern matches the viewport.
Vertex displacement
positionNode runs in object space before rasterization. A common pattern is grass cards or ocean waves:
const positionNode = Fn(() => {
const pos = positionLocal;
const height = computeWaveHeight(); // your TSL function
return pos.add(normalLocal.mul(height));
})();
Pair positionNode with a matching colorNode for lighting and foam.
Screen-space sampling
When a shader samples a render texture (refraction, distortion), build UVs from screenCoordinate and iRenderViewport, not from mesh UVs:
const renderUV = Fn(() =>
screenCoordinate.sub(iRenderViewport.xy).div(iRenderViewport.zw)
)();
const sceneBehind = refractionScene.sample(renderUV.add(offset));
This keeps refraction aligned when the render view is docked inside Studio. Composition cameras should use Follow viewport so the offscreen render matches what the user sees.
Security and limitations
Shader source is sandboxed. The following are not allowed:
import,export,require,eval,fetch, timers, workers, DOM access, and similar APIs.
Only the tsl module namespace is available. Errors are reported with a line number pointing into your shader.tsl source.
Tips
- Start from a Studio template (Lit material, Water, Background gradient, Vertex wind) and adapt.
- Recompile happens automatically when you save the shader item; linked materials pick up changes on the next frame.
- Use
normalLocal,uv,positionLocalfor mesh-local shading; usescreenCoordinatefor effects tied to the final image. - For water refraction, link a Composition with a camera that renders only the layers behind the water; relative resolution (for example 0.5×) is often enough for distortion.
See also
- Materials — loading and assigning materials from Lua
- Compositions — render-to-texture for shader inputs
- Shaders — overview of shader types in Lemonate
- Three.js TSL documentation