Skip to content

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

  1. In the asset browser, create a TSL Shader item (or pick a template such as Lit material, Background gradient, Vertex wind, or Water).
  2. Open shader.tsl in the code editor.
  3. 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, positionLocal for mesh-local shading; use screenCoordinate for 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