Custom shaders

Node materials written in TSL render. ShaderMaterial and other GLSL do not. This page shows which TSL operations work and the extra nodes the SDK ships.

Use a node material

import { MeshStandardNodeMaterial } from 'three/webgpu';
import { mix, uv, vec3, sin, mul } from 'three/tsl';

const material = new MeshStandardNodeMaterial();

material.colorNode = mix(
  vec3(0.02, 0.5, 0.8),
  vec3(1.0, 0.18, 0.58),
  sin(mul(uv().x, 12.0))
);
material.roughnessNode = mul(uv().y, 0.8);

MeshStandardNodeMaterial and MeshPhysicalNodeMaterial both work. If you have a ShaderMaterial, port it to TSL or declare a material fallback.

Check a material before you render

bakery3.check() reports every node material in the scene. To look at one material on its own:

import { extractMaterialShader, isNodeMaterial } from '@oeave/bakery3';

if (isNodeMaterial(material)) {
  const extracted = extractMaterialShader(material);

  extracted.supportedSlots; // ['color', 'roughness']
  extracted.failedSlots;    // []
  extracted.issues;         // what will not render, and why
}

Node inputs you can set

These inputs render: colorNode, opacityNode, alphaTestNode, roughnessNode, metalnessNode, normalNode, emissiveNode, aoNode, clearcoatNode, clearcoatRoughnessNode, sheenNode, sheenRoughnessNode, iridescenceNode, iridescenceIORNode, iridescenceThicknessNode, transmissionNode, thicknessNode, iorNode, specularIntensityNode, specularColorNode and anisotropyNode.

Each input is checked on its own. If colorNode works and roughnessNode uses dFdx, the render uses your color graph and the material's plain roughness value. check() reports both.

These inputs do not render:

InputWhy
fragmentNodeReplaces the whole material, so there is no surface left to render.
outputNodeReplaces the material's output.
positionNodeMoves vertices in the shader. Not supported yet.
backdropNodeA screen-space effect. The renderer has no screen to read from.
depthNodeOverrides the depth buffer, which the renderer does not have.
envNodeA warning, not an error. The scene's environment is used instead.

Use textures in a node material

Textures your graph samples are uploaded with the scene. The SDK has to be able to read their pixels. It cannot read a tainted canvas, a cross-origin image without CORS, or a render target that only exists on the GPU.

✗ shader_texture_unreadable
The texture "weave" used by "fabric" has no reachable pixels, so the
shader graph was not sent.

To fix it, serve the texture with CORS headers or draw it into a canvas you own.

Operations that work

There are 98 operations. Each one is in one of three groups.

Exact. The render matches your viewport.

constuniformuvvertex_colorpositionnormalview_directioncamera_positiontexturenormal_mapaddsubmuldivmodpowminmaxabssignfloorceilroundtruncfractsqrtinverse_sqrtexpexp2loglog2sincostanasinacosatanatan2radiansdegreesnegateone_minusreciprocalsaturateclampmixstepsmoothstepremapdotcrosslengthdistancenormalizereflectrefractfaceforwardswizzlejoinconvertrotateless_thangreater_thanless_equalgreater_equalandornotselectluminancechecker

Approximate. The render looks similar, not identical. check() warns once per material and says what differs.

bump_mapequalnot_equalsrgb_to_linearlinear_to_srgbmx_noisemx_fractal_noisemx_worley_noisemx_cell_noise

Bakery-only. The extra nodes from @oeave/bakery3/tsl. Your viewport shows a preview and the render shows the real thing. check() reports them as info.

bk_noise_texturebk_voronoi_texturebk_wave_texturebk_magic_texturebk_gradient_texturebk_brick_texturebk_white_noisebk_ambient_occlusionbk_bevelbk_pointinessbk_wireframebk_fresnelbk_layer_weightbk_color_rampbk_blackbodybk_hsvbk_brightness_contrastbk_gamma

How the approximate operations differ

bump_map

three.js bump-maps with screen-space derivatives, the renderer with surface derivatives. The relief reads the same; the exact pixels differ.

equal

Float equality is compared exactly on both sides, but GPU rounding differs between the browser and the renderer, so equality on computed values may flip near the boundary.

not_equal

Float equality is compared exactly on both sides, but GPU rounding differs between the browser and the renderer, so equality on computed values may flip near the boundary.

srgb_to_linear

The renderer applies a gamma-2.2 curve rather than the piecewise sRGB transfer function; the toe below 0.04 differs slightly.

linear_to_srgb

The renderer applies a gamma-2.2 curve rather than the piecewise sRGB transfer function; the toe below 0.04 differs slightly.

mx_noise

MaterialX Perlin noise renders through the renderer's own Perlin-based noise. The character matches; the exact pattern placement does not.

mx_fractal_noise

MaterialX fractal noise renders through the renderer's own noise with matching octaves. The character matches; the exact pattern placement does not.

mx_worley_noise

MaterialX Worley noise renders through the renderer's cellular noise. Cell shapes match in character, not placement.

mx_cell_noise

MaterialX cell noise renders as a hash over the floored coordinate. Statistically identical, per-cell values differ.

Extra nodes the SDK ships

The renderer can do things a browser cannot, like traced ambient occlusion and rounded edges. We ship them as TSL nodes for creative coding.

import {
  bakeryNoiseTexture,
  bakeryColorRamp,
  bakeryPointiness,
} from '@oeave/bakery3/tsl';

material.colorNode = bakeryColorRamp(
  bakeryNoiseTexture(undefined, { scale: 12, detail: 4 }),
  [
    { position: 0.0, color: [0.04, 0.05, 0.07] },
    { position: 0.6, color: [0.2, 0.82, 1.0] },
    { position: 1.0, color: [1.0, 0.18, 0.58] },
  ]
);

material.roughnessNode = bakeryPointiness();
NodeWhat it renders
bakeryNoiseTextureFractal Perlin noise
bakeryVoronoiTextureVoronoi cells
bakeryWaveTextureBands and rings, with distortion
bakeryMagicTextureLayered interference patterns
bakeryGradientTextureLinear, radial and spherical gradients
bakeryBrickTextureBricks and mortar
bakeryWhiteNoiseA random value per coordinate
bakeryAmbientOcclusionTraced ambient occlusion
bakeryBevelRounded edges, without changing the mesh
bakeryPointinessSurface curvature, from concave to convex
bakeryWireframe1 on triangle edges, 0 elsewhere
bakeryFresnelPhysically correct fresnel
bakeryLayerWeightA facing or fresnel blend between two layers
bakeryColorRampA color ramp with positioned stops
bakeryBlackbodyA color from a temperature in kelvin
bakeryGammaGamma curve
bakeryBrightnessContrastBrightness and contrast
bakeryHueSaturationHue, saturation and value

The options are plain numbers. They cannot be driven by another node yet.

Importing @oeave/bakery3/tsl also lets check() recognize the Three.js builtins positionWorld, cameraPosition and time. Import it once if your graph uses them.

When an operation is not supported

check() names the operation, the reason and the material before you render.

✗ shader_node_unsupported
In `roughnessNode`: `dFdx()` cannot be translated. Screen-space
derivatives do not exist in a path tracer.

Location: <Product>/<Panel>/<Holo>

Write it another way, or declare a material fallback and keep your shader for the browser.

Every operation

OpFidelityNote
absExact—
acosExact—
addExact—
andExact—
asinExact—
atanExact—
atan2Exact—
bk_ambient_occlusionBakery-only—
bk_bevelBakery-only—
bk_blackbodyBakery-only—
bk_brick_textureBakery-only—
bk_brightness_contrastBakery-only—
bk_color_rampBakery-only—
bk_fresnelBakery-only—
bk_gammaBakery-only—
bk_gradient_textureBakery-only—
bk_hsvBakery-only—
bk_layer_weightBakery-only—
bk_magic_textureBakery-only—
bk_noise_textureBakery-only—
bk_pointinessBakery-only—
bk_voronoi_textureBakery-only—
bk_wave_textureBakery-only—
bk_white_noiseBakery-only—
bk_wireframeBakery-only—
bump_mapApproximatethree.js bump-maps with screen-space derivatives, the renderer with surface derivatives. The relief reads the same; the exact pixels differ.
camera_positionExact—
ceilExact—
checkerExact—
clampExact—
constExact—
convertExact—
cosExact—
crossExact—
degreesExact—
distanceExact—
divExact—
dotExact—
equalApproximateFloat equality is compared exactly on both sides, but GPU rounding differs between the browser and the renderer, so equality on computed values may flip near the boundary.
expExact—
exp2Exact—
faceforwardExact—
floorExact—
fractExact—
greater_equalExact—
greater_thanExact—
inverse_sqrtExact—
joinExact—
lengthExact—
less_equalExact—
less_thanExact—
linear_to_srgbApproximateThe renderer applies a gamma-2.2 curve rather than the piecewise sRGB transfer function; the toe below 0.04 differs slightly.
logExact—
log2Exact—
luminanceExact—
maxExact—
minExact—
mixExact—
modExact—
mulExact—
mx_cell_noiseApproximateMaterialX cell noise renders as a hash over the floored coordinate. Statistically identical, per-cell values differ.
mx_fractal_noiseApproximateMaterialX fractal noise renders through the renderer's own noise with matching octaves. The character matches; the exact pattern placement does not.
mx_noiseApproximateMaterialX Perlin noise renders through the renderer's own Perlin-based noise. The character matches; the exact pattern placement does not.
mx_worley_noiseApproximateMaterialX Worley noise renders through the renderer's cellular noise. Cell shapes match in character, not placement.
negateExact—
normalExact—
normal_mapExact—
normalizeExact—
notExact—
not_equalApproximateFloat equality is compared exactly on both sides, but GPU rounding differs between the browser and the renderer, so equality on computed values may flip near the boundary.
one_minusExact—
orExact—
positionExact—
powExact—
radiansExact—
reciprocalExact—
reflectExact—
refractExact—
remapExact—
rotateExact—
roundExact—
saturateExact—
selectExact—
signExact—
sinExact—
smoothstepExact—
sqrtExact—
srgb_to_linearApproximateThe renderer applies a gamma-2.2 curve rather than the piecewise sRGB transfer function; the toe below 0.04 differs slightly.
stepExact—
subExact—
swizzleExact—
tanExact—
textureExact—
truncExact—
uniformExact—
uvExact—
vertex_colorExact—
view_directionExact—