Skip to content

Shader Graph

A shader graph is a visual node graph that compiles to the same Material and WGSL hooks a hand-written shader produces, so you can author surfaces by wiring nodes instead of typing shader code.

How it fits

A shader graph is an authoring form of a Material asset. You wire value, texture, and math nodes into a single Surface output node, and the compiler walks that graph into the same four WGSL hooks (vertexPosition, surfaceNormal, surfaceColor) and parameter list a written shader would produce. The compiled result is a Material, indistinguishable at render time from one authored as raw WGSL.

The panel, the create flow (+ New Shader Graph), the live mirroring (no Save button), the preview, and the Params table all live on the Shader Editor page. This page documents the node canvas itself: the Surface output, how you work in the graph, every node in the registry, subgraphs, and how the graph compiles.

The node type keys are generic (multiply, sampleTexture, lerp), never a source engine's node names. One registry drives hand-authored graphs and graphs imported from Unity, Unreal, or Godot alike.

The Surface output

The Surface node is permanent: it always exists, cannot be deleted, and every graph ends at it. Wire a node's output into one of its input ports to drive that channel. A port left unwired keeps the engine default for that channel. If nothing is wired to any port, the graph fails to compile with the message nothing wired to the master.

PortDrivesStageNotes
baseColorAlbedo (albedo = rgb)FragmentThe lit surface colour.
emissionEmissive (emissive = rgb)FragmentAdded light. With emission wired but no baseColor, albedo is forced to black.
alphaOutput alpha (outAlpha)FragmentRead only when the surface blend is alpha or additive; ignored on opaque surfaces.
normalShading normal (N = normalize(...))FragmentReplaces the interpolated normal used for lighting.
metallicMetalness, clamped to 0-1FragmentFeeds the PBR model. See PBR lighting.
roughnessRoughness, clamped to 0.04-1FragmentLower bound 0.04 avoids a zero-roughness singularity.
occlusionAmbient occlusion, clamped to 0-1FragmentMultiplies indirect lighting.
vertexOffsetWorld displacement (world.xyz += offset)VertexAdded to the world position in the vertex stage - the one port evaluated in the vertex stage.

Because vertexOffset runs in the vertex stage, its subtree can only use nodes that are valid there. Fragment-only nodes (see the catalog below) cannot feed vertexOffset.

The surface's blend mode (opaque / alpha / additive), unlit flag, and opacity are surface settings, edited alongside the graph. Transparent surfaces render unlit by default; a graph opts a transparent surface back into lighting with the surface lit flag (for water or glass specular). Opaque surfaces are always lit.

Working in the graph

The canvas is editable when you own the shader and read-only when you are inspecting an imported one. Editing controls:

  • Palette - the + Add Node ▾ button opens a searchable menu grouped by the registry category. Type to filter by label or type key; press Enter to add the first match. Any subgraphs in the project appear in a Subgraphs group at the bottom.
  • Wire - drag from a node's output port (right side) to an input port (left side, or a Surface port). Each input holds one wire; wiring a port that already has a wire replaces it.
  • Delete - click a wire to cut it. Select a node and press Delete / Backspace, or click the ✕ on the node, to remove it. The Surface node is permanent and never selected for deletion.
  • Multi-select - shift-click to toggle nodes into the selection; shift-drag on empty canvas to box-select. ⌘A / Ctrl A selects every node.
  • Copy / paste / duplicate - ⌘C / ⌘V copy and paste the selection (with the wires internal to it); ⌘D duplicates in place. The clipboard survives switching between shaders in one session.
  • Group → Subgraph - with a selection, the ⧉ Group → Subgraph button collapses those nodes into a reusable subgraph (see Subgraphs).
  • Inline edits - a Constant node has a number field; a Property node has a dropdown of the graph's properties; a Split node has a channel dropdown (.x/.y/.z/.w); Blend, Comparison, Static Switch, and Custom WGSL each expose their own inline control. Any unwired float input port also shows a small number box, so a one-off constant needs no separate Constant node.

The header above the canvas carries Tidy, Fit, and a zoom slider - documented on the Shader Editor page. Tidy re-lays the whole graph left to right (sources on the left, Surface on the right) as one undo step.

📸 Screenshot - save as img/editor-shader-graph-canvas.png

The node canvas with a Sample Texture 2D feeding a Multiply into the Surface baseColor port, the + Add Node palette open and filtered, and one node's inline number box visible.

Tracing a Surface output

Click a wired Surface port to back-trace what produces it: the node feeding that port and every ancestor up the chain light amber, and every node and wire that does not contribute dims. It answers "which nodes actually affect this channel" on a dense graph without reading every wire by hand. Click the same port again to toggle it off, or press the ✕ Clear highlight button (top-left) or Esc. Tracing changes nothing in the graph and works in a read-only imported shader too.

📸 Screenshot - save as img/editor-shader-graph-trace.png

The baseColor Surface port clicked: its contributing nodes and wires highlighted amber, the unrelated nodes dimmed, and the ✕ Clear highlight button showing top-left.

Variadic math ports

Add, Subtract, Multiply, and Divide start with two input ports (A, B) and grow. Once both base ports are used, a trailing spare port appears (C, then D, up to H), and the node folds every filled port left to right (A op B op C ...). A spare port shows its no-op identity (1 for Multiply/Divide, 0 for Add/Subtract) so an untouched extra reads as ignored. This collapses a chain of two-input operations into one node; capped at ports C-H.

Node catalog

Every entry below is one node in the NODE_REGISTRY. Inputs list the port name and type; the output is a single value. Notes:

  • † fragment-only - the node emits WGSL that only exists in the fragment stage (a texture sample, N/V, or the scene-depth prepass). Wiring it into the vertexOffset chain (or a Vertex Interpolator's vertex-stage input) fails to compile with a clear message.
  • ‡ variadic - grows extra input ports C-H (see above).
  • Several Input and Geometry nodes are stage-aware: they read a pre-interpolation vertex variable in a vertex chain and the interpolated fragment value otherwise.

Input

NodeInputsOutputDescription
Constant(none)floatA literal number typed inline (params.value).
Vector 2X (float), Y (float)vec2Packs two floats into a vec2.
Vector 3X (float), Y (float), Z (float)vec3Packs three floats into a vec3.
Vector 4X (float), Y (float), Z (float), W (float)vec4Packs four floats into a vec4.
Property(none)matches propertyReads an exposed graph property (params.<name>); rank follows the property type.
UV(none)vec2Mesh UV coordinates. Stage-aware (uv in vertex, in.uv in fragment).
Vertex Color(none)vec3Per-vertex colour attribute. Stage-aware.
Time(none)floatAnimated time, globals.time * globals.anim.
Sample Texture 2D †Texture (texture), UV (vec2)vec4Samples a texture. The Texture port binds a Texture2D property (or the node's own embedded texture id); UV defaults to the mesh UV.

Geometry

NodeInputsOutputDescription
Position(none)vec3World-space position (world.xyz in vertex, in.worldPos in fragment).
Object Position(none)vec3The object origin (pivot) in world space. Vertex-only; degrades to zero in the fragment stage.
Normal Vector(none)vec3World-space surface normal (model-transformed in vertex, N in fragment).
View Direction(none)vec3View direction; V in fragment, a forward constant in vertex.

Math

NodeInputsOutputDescription
Add ‡A (float), B (float)floatA + B (folds C-H).
Subtract ‡A (float), B (float)floatA - B (folds C-H).
Multiply ‡A (float), B (float)floatA * B (folds C-H).
Divide ‡A (float=1), B (float=1)floatA / B (folds C-H).
PowerA (float), B (float=1)floatpow(A, B).
One MinusIn (float)float1 - In.
AbsoluteIn (float)floatabs(In).
NegateIn (float)float-In.
NormalizeIn (vec3)vec3normalize(In).
Dot ProductA (vec3), B (vec3)floatdot(A, B).
SaturateIn (float)floatclamp(In, 0, 1).
ClampIn (float), Min (float=0), Max (float=1)floatclamp(In, Min, Max).
LerpA (float), B (float), T (float)floatmix(A, B, T).
Fresnel Effect †Power (float=1)floatpow(1 - max(dot(N, V), 0), Power).
SineIn (float)floatsin(In).
CosineIn (float)floatcos(In).
LengthIn (vec3)floatlength(In).
DistanceA (vec3), B (vec3)floatdistance(A, B).
ReflectIn (vec3), Normal (vec3)vec3reflect(In, Normal).
SmoothstepEdge0 (float=0), Edge1 (float=1), In (float)floatsmoothstep(Edge0, Edge1, In).
FractionIn (float)floatfract(In).
FloorIn (float)floatfloor(In).
CeilingIn (float)floatceil(In).
RoundIn (float)floatround(In).
Square RootIn (float)floatsqrt(In).
ExponentialIn (float)floatexp(In).
LogIn (float=1)floatlog(In).
SignIn (float)floatsign(In).
StepEdge (float), In (float)floatstep(Edge, In).
ModuloA (float), B (float=1)floatA - B * floor(A / B).
MinimumA (float), B (float)floatmin(A, B).
MaximumA (float), B (float)floatmax(A, B).
Arctangent2A (float), B (float=1)floatatan2(A, B).
Cross ProductA (vec3), B (vec3)vec3cross(A, B).
RemapIn (float), InMin (float=-1), InMax (float=1), OutMin (float=0), OutMax (float=1)floatRescales In from the In range to the Out range.
Rotate About AxisIn (vec3), Axis (vec3=0,1,0), Angle (float)vec3Rotates a vector about an axis by an angle.

UV

NodeInputsOutputDescription
Tiling And OffsetUV (vec2), Tiling (vec2=1,1), Offset (vec2=0,0)vec2UV * Tiling + Offset.
PannerUV (vec2), Speed (vec2=0.1,0)vec2Scrolls UVs over time, UV + time * Speed.
RotatorUV (vec2), Center (vec2=0.5,0.5), Rotation (float)vec2Rotates UVs about a centre point.
Polar CoordinatesUV (vec2), Center (vec2=0.5,0.5), RadialScale (float=1), LengthScale (float=1)vec2Converts UVs to polar (radius, angle) coordinates.
FlipbookUV (vec2), Width (float=1), Height (float=1), Tile (float=0)vec2Remaps UVs to one cell of a sprite sheet.

Procedural

NodeInputsOutputDescription
HashSeed (vec3)floatA stable 0-1 hash of a position (same input, same value).
Gradient NoiseUV (vec2), Scale (float=1)floatPerlin-style gradient noise.
VoronoiUV (vec2), AngleOffset (float=2), CellDensity (float=5)floatVoronoi cell noise.
CheckerboardUV (vec2), Frequency (float=4)floatA 0/1 checker pattern at the given frequency.

Channel

NodeInputsOutputDescription
SplitIn (vec4)floatExtracts one channel (.x/.y/.z/.w, chosen inline).

Environment

NodeInputsOutputDescription
Sky ColorDir (vec3, default view dir)vec3Samples the procedural sky colour in a direction. Wire a reflected view direction for a reflection.
Scene Depth †(none)floatDistance from this fragment to the opaque geometry behind it, from the scene-position prepass; a large sentinel when no prepass ran.

Artistic

NodeInputsOutputDescription
HueIn (vec3), Offset (float)vec3Shifts hue by the offset.
SaturationIn (vec3), Saturation (float=1)vec3Scales colour saturation.
ContrastIn (vec3), Contrast (float=1)vec3Scales contrast about 0.5.
Invert ColorsIn (vec3)vec31 - In.
BlendA (vec3), B (vec3), Opacity (float=1)vec3Photoshop-style blend (multiply, screen, overlay, add, lighten, darken - chosen inline) mixed by Opacity.

Logic

NodeInputsOutputDescription
ComparisonA (float), B (float)floatReturns 1 or 0 for A op B (op chosen inline: >, <, >=, <=, ==, !=).
BranchPredicate (float), True (float), False (float)floatRuntime select: Predicate > 0.5 ? True : False. Both branches evaluate.
Static SwitchOn (float), Off (float)floatCompile-time select: only the chosen branch is compiled; the other subtree emits no WGSL.

Vertex

NodeInputsOutputDescription
Vertex InterpolatorIn (vec4)vec4Computes its input once per vertex and reads it back interpolated in the fragment stage. Up to 8 per graph. In a vertex chain it is a pass-through.

Utility

NodeInputsOutputDescription
Custom WGSL(none)float (rank set inline)An escape hatch: a raw WGSL expression you type (referencing in.*, params.*, N, V), with its output type chosen inline (f / v2 / v3 / v4).

Subgraphs

A subgraph is a reusable, single-output graph fragment - the same idea as a Unity Sub Graph or an Unreal Material Function. A parent graph references it through a subgraph node whose input ports mirror the subgraph's declared inputs; inside the subgraph, an input node stands for a value the parent wires in.

Create one with ⧉ Group → Subgraph: select a set of nodes and the editor lifts them into a new subgraph, replacing them with a single subgraph node. Wires crossing into the selection become the subgraph's inputs; the one wire (or master port) leaving the selection becomes its single output. The selection must have exactly one output boundary - zero or more than one is rejected, since subgraphs are single-output.

The same subgraph can be dropped into many graphs from the palette's Subgraphs group. Before compilation, expandSubgraphs inlines every subgraph node into the parent graph - prefixing internal ids, rewiring each input node to the parent's wired source, and splicing the subgraph's output in place of the node. Nested subgraphs expand recursively (guarded against a subgraph that references itself). The compiler itself stays subgraph-unaware, so a flattened graph compiles identically whether or not subgraphs were used.

How it compiles

Compilation is a single-static-assignment (SSA) topological walk that starts at the wired Surface ports and pulls their subtrees backward. Only nodes reachable from a wired Surface port emit code; anything disconnected is skipped entirely.

Details:

  • Per-port walk. Each Surface port is walked into its hook buffer at its stage: normal into surfaceNormal (fragment, walked first), then baseColor / emission / metallic / roughness / occlusion and (for transparent surfaces) alpha into surfaceColor (fragment), then vertexOffset into vertexPosition (vertex).
  • Each node's emit produces a WGSL value expression using the shared value helpers (the same splat / bin / asVec3 helpers the Unity transpiler uses), so a hand-built graph and an imported one compile to identical WGSL.
  • Memoisation and CSE. Node outputs are memoised per stage, and two nodes that emit the same WGSL collapse to one let (common-subexpression elimination), keyed per stage. A value's expression is written once as an SSA line and referenced by id.
  • Stage awareness. Position, UV, Normal, and similar nodes are keyed by stage, because they read a different variable in the vertex hook than in the fragment hook. A Vertex Interpolator computes its input in the vertex stage, writes it to a awakenVsg varying (max 8), and reads the interpolated value in the fragment stage.
  • Errors throw SGUnsupported with a readable message, and the editor keeps the last good compile. Cases: an unknown node type, a cycle (cycle at node X), an edge to a missing node, a fragmentOnly node reached in the vertex stage, a value read of a texture or bool property, more than four textures (MAX_MATERIAL_TEXTURES), or nothing wired to the Surface.
  • Engine-agnostic. One registry compiles hand-authored graphs and graphs imported from Unity ShaderGraph, Unreal, or Godot - every source maps its nodes onto these generic types, and every path shares this compiler.

The output is a Material (hooks, packed params, and resolved texture bindings) ready to register with the renderer - the same artifact a written WGSL shader produces.

See also

Awaken — browser-native WebGPU game engine.