Overview

Shader Graph is Unity’s visual shader-authoring tool. It lets teams build shaders through node graphs rather than writing everything by hand in shader code. Unity’s docs position it as a package-based workflow for current Scriptable Render Pipeline projects, especially URP and HDRP. Unity’s more recent URP guidance treats it as part of a wider rendering stack for current projects rather than an isolated shader toy. (Unity Documentation, Shader Graph, see source-shader-graph; Unity, Introduction to URP for advanced creators, see source-unity-urp-advanced-creators)

Which render pipelines? URP and HDRP are the main targets. “As of Unity version 2021.2, you can also use Shader Graph with the Built-In Render Pipeline”, but that support “is for compatibility purposes only”: it receives bug fixes, not new features, and Unity recommends using Shader Graph with the Scriptable Render Pipelines (Unity Technologies, Shader Graph 12.1 Manual: Getting started). Unity 6’s Graph Target page adds that the Built-In Render Pipeline itself “is deprecated and will be made obsolete in a future release”, and that “not all blocks are compatible with all targets”: a block that goes inactive when you choose a target is not supported there (Unity Technologies, Shader Graph 17.5 Manual: Graph Target). For new Unity 6 projects, use URP.

Setup

Typical workflow:

  1. Use a compatible render pipeline such as URP or HDRP.
  2. Create a Shader Graph asset.
  3. Build the shader visually with nodes.
  4. Create a material from the graph.
  5. Assign the material to objects in the scene.

Usage

Do you need HLSL? No. You can build complete shaders, including all the maths, from nodes, and you never have to write HLSL to make Shader Graph work. HLSL becomes useful in three situations: when an effect needs a Custom Function node (loops, data the nodes cannot read, custom lighting; see “When you need a Custom Function node or HLSL” below), when you read View Generated Shader to debug what the graph produced, and when you need something Shader Graph cannot do at all, such as extra passes or custom render state, which means a hand-written ShaderLab shader.

Shader Graph is strongest when:

  • artists or technical designers need to iterate quickly
  • the project wants stylised effects without deep shader-code expertise
  • the teaching goal is understanding shader logic before full HLSL work

It does not replace shader fundamentals (see shader-fundamentals), but it gives a more approachable route into them.

In current Unity workflows it is most often paired with unity-urp-overview and VFX tooling. That makes it especially useful for stylised rendering, material iteration, and gameplay-facing effects where artists or technical designers need to work without dropping straight into hand-written shader code. (Unity, Create popular shaders and visual effects with URP, see source-unity-urp-shaders-vfx)

How nodes translate into HLSL

Shader Graph is visual, but it is still producing shader code underneath. Unity’s URP shader/VFX material explicitly points students to View Generated Shader in the inspector when they need to inspect the produced HLSL. That means the graph is not magic: nodes, properties, and connections are being compiled into a concrete shader file (Unity, Create Popular Shaders and Visual Effects with URP, see source-unity-urp-shaders-vfx).

Practical workflow:

  1. Build the graph visually.
  2. Save the graph.
  3. Select the Shader Graph asset.
  4. Use View Generated Shader in the inspector if you need to inspect the compiled result.

In that file each node appears as a small HLSL function (the Sine node, for example, becomes a function whose body is Out = sin(In);), each wire becomes a typed variable passed from one function call to the next, and the calls are split between a vertex function and a fragment function according to which Master Stack context they feed (Unity Technologies, Shader Graph Manual: Sine Node and Master Stack pages). shader-fundamentals walks through this in detail, together with the vertex and fragment stages, precision and data types.

Recipe: pulsing glow

A glow that breathes in and out needs no HLSL: the graph does the maths with four nodes. The idea is glow = remap(sin(time × speed), −1..1 → min..max) × HDR colour, fed into Emission.

Blackboard properties (suggested defaults):

PropertyTypeDefaultMeaning
_GlowColourColor, Mode: HDRcyan, Intensity about +2colour and brightness of the glow; HDR mode “sets its intensity from -10 to 10 exposure stops” (Unity Technologies, Shader Graph Manual: Property Types)
_PulseSpeedFloat4radians per second; pulses per second = speed ÷ 2π, so 4 is about 0.64 Hz
_GlowMinFloat0.2glow strength at the dimmest point
_GlowMaxFloat1glow strength at the brightest point

Node steps:

  1. Time node (Time output, seconds) → Multiply by _PulseSpeed. (The Time node’s ready-made Sine Time output is sin(time) with no speed control, so multiply first.)
  2. → Sine, which gives a value swinging between −1 and 1.
  3. → Remap with In Min Max = (−1, 1) and Out Min Max = (_GlowMin, _GlowMax). Remap “converts a value from one range to another” (Unity Technologies, Shader Graph Manual: Remap node). To drive Out Min Max from the two properties, combine them with a Vector 2 node.
  4. → Multiply with _GlowColour.
  5. → the Emission block of a Lit graph. In an Unlit graph, which has no Emission block, Add the result to Base Color instead.
  6. In the scene, enable HDR on the URP Asset and add a Bloom override to a Volume, otherwise the glow brightens the surface but casts no halo. The steps are in “Glow: emissive material or particles?” on vfx-value-and-colour.

The same maths in HLSL, which is roughly what View Generated Shader shows for these nodes:

float pulse = sin(time * pulseSpeed);                 // −1..1
float glow  = glowMin + (pulse + 1) * (glowMax - glowMin) / 2; // Remap −1..1 → min..max
float3 emission = glowColour.rgb * glow;              // HDR colour, can exceed 1

Variations: add a per-object Float _Phase to the Time value so that many objects do not pulse in sync; put an Absolute node after the Sine (and set Remap’s In Min Max to (0, 1)) for a sharper, bouncing pulse at twice the rate; or replace Sine with a Sample Texture 2D of a gradient ramp for a hand-authored pulse shape. To start or stop the pulse from gameplay, set _GlowMax (or the colour’s intensity) from C# with Material.SetFloat or SetColor.

Finding the “ripple frequency” control

In a ripple or wave effect, “frequency” can mean two different things, and a typical graph controls them separately:

  • Spatial frequency (ring spacing): a Multiply node that scales the distance or UV value before it enters the Sine node. A larger value packs more rings into the same area, so the rings sit closer together.
  • Temporal speed: a Multiply node that scales the Time node’s output before it is added to (or subtracted from) that scaled distance. A larger value makes the rings travel outwards faster without changing their spacing.
  • The Sine (or repeated UV offset) result then feeds the final connection into UV distortion, normal movement, or vertex displacement.

In other words, a ripple is commonly built as sin(distance × frequency − time × speed). If a student asks which node controls wave frequency, the honest answer is “the Multiply on the distance or UV input for ring spacing, and the Multiply on Time for speed”, not one universal magic node. Unity’s URP examples repeatedly build effects from small subgraphs that move textures, offset UVs, or drive surface displacement over time (Unity, Create Popular Shaders and Visual Effects with URP, see source-unity-urp-shaders-vfx).

One subtlety: in that form, the rings’ outward travel speed (in UV units per second) is speed ÷ frequency, so tightening the ring spacing also slows the rings down. If you want the two controls fully independent, subtract first and multiply afterwards: sin((distance − time × speed) × frequency). Now speed is the travel speed in UV units per second and frequency only sets the spacing (one ring every 2π ÷ frequency UV units). The recipe below uses this form.

Recipe: radial ripple with decay

This recipe answers “which maths makes a ripple fade or slow down over time?“. The short answer: multiply the wave by a decay term that falls from 1 towards 0 as time passes, usually exp(−k·t) (the Exponential node) or 1 − saturate(t ÷ duration) (Divide → Saturate → One Minus), and by a second term that falls off with distance. Here t is the time since the impact, not the time since the game started. If “slow down” means the rings themselves should decelerate, use a decaying function for the distance the wave has travelled as well (step 9).

Blackboard properties (suggested defaults, UV units):

PropertyTypeDefaultMeaning
_CentreVector 2(0.5, 0.5)impact point in UV space
_ImpactTimeFloat−1000time of the impact; a large negative default keeps the ripple dead until triggered
_RingFrequencyFloat60ring spacing (spatial frequency)
_WaveSpeedFloat0.25outward travel speed, UV units per second
_AmplitudeFloat0.02wave height or distortion strength
_DampingFloat2k in exp(−k·t); larger fades faster
_MaxRadiusFloat0.5distance at which the ripple has faded out

Node steps:

  1. UV node (UV0) and _Centre → Distance → d, the distance of this pixel from the impact point. Distance returns “the euclidean distance between the values of the inputs A and B” (Unity Technologies, Shader Graph Manual: Distance Node), and because it is measured from a centre the bands form circles.
  2. Time node (Time output) → Subtract _ImpactTime → Maximum with 0 → t, the seconds since impact (never negative).
  3. t → Multiply by _WaveSpeed → r, how far the wavefront has travelled.
  4. d → Subtract r → Multiply by _RingFrequency → Sine → wave, between −1 and 1. This is sin((d − r) × frequency), so ring spacing and travel speed stay separate.
  5. Time decay: t → Multiply by −_Damping (Negate the property first) → Exponential (Base E) → timeFade. The Exponential node “Returns e to the power of input In” (Unity Technologies, Shader Graph Manual: Exponential Node), so timeFade starts at 1 and halves roughly every 0.69 ÷ k seconds without ever quite reaching 0. Alternative: t → Divide by a _Duration property → Saturate → One Minus gives a straight-line fade that reaches exactly 0 at _Duration, which is handy if a script needs to know when the effect has finished.
  6. Distance decay: d → Divide by _MaxRadius → Saturate → One Minus → distFade, so outer rings are weaker. (exp(−m·d) is a softer alternative.)
  7. Wavefront mask: Step with Edge = d and In = r → front. Step returns 1 “if the value of input In is greater than or equal to the value of input Edge” (Unity Technologies, Shader Graph Manual: Step Node), so only pixels the wave has already reached ripple. Use Smoothstep instead for a soft leading edge.
  8. wave × _Amplitude × timeFade × distFade × front (chain of Multiply nodes) → height.
  9. Optional deceleration: to make the rings slow down as well as fade, replace step 3 with r = _MaxRadius × (1 − exp(−k·t)): t → Multiply by −k → Exponential → One Minus → Multiply by _MaxRadius. The front then starts fast and eases to a stop at _MaxRadius.
  10. Use height in one of three ways:
    • UV distortion (fragment): Subtract _Centre from UV → Normalize → Multiply by height → Add to UV → UV input of Sample Texture 2D → Base Color.
    • Lighting only (fragment): height → Normal From Height → the Normal (Tangent Space) block, so the surface looks rippled without moving.
    • Real displacement (vertex): Normal Vector × height → Add to Position (Object) → the Position block in the Vertex context. This runs once per vertex, so it needs a finely subdivided mesh; on a plain quad nothing visible happens. See shader-fundamentals for why.

The same maths in HLSL, for checking the graph or for a Custom Function node:

float d = distance(uv, centre);
float t = max(time - impactTime, 0);
float r = t * waveSpeed;                       // wavefront radius
float wave = sin((d - r) * ringFrequency);     // spacing and speed kept separate
float timeFade = exp(-damping * t);            // or 1 - saturate(t / duration)
float distFade = 1 - saturate(d / maxRadius);
float front = step(d, r);                      // 1 where the wave has arrived
float height = wave * amplitude * timeFade * distFade * front;

Triggering it from C#. The graph only knows the impact time if a script sends it. In URP, the shader’s time “matches Time.time” (Unity Technologies, Unity Manual: Built-in shader variables reference), so the script stores Time.time at the moment of impact:

using UnityEngine;
 
public class RippleImpact : MonoBehaviour
{
    static readonly int ImpactTimeId = Shader.PropertyToID("_ImpactTime");
    static readonly int CentreId = Shader.PropertyToID("_Centre");
    Material rippleMaterial;
 
    void Awake()
    {
        rippleMaterial = GetComponent<Renderer>().material; // per-object instance
    }
 
    // uvCentre: the hit point in UV space, e.g. RaycastHit.textureCoord.
    public void Trigger(Vector2 uvCentre)
    {
        rippleMaterial.SetVector(CentreId, uvCentre);
        rippleMaterial.SetFloat(ImpactTimeId, Time.time);
    }
}

The property names must match the graph’s Reference names on the Blackboard (not just the display names). RaycastHit.textureCoord only works when the hit collider is a MeshCollider; for any other collider it returns Vector2.zero (Unity Technologies, Scripting API: RaycastHit.textureCoord). This version supports one ripple at a time; overlapping ripples need an array of impacts or a render-texture simulation, which is beyond a first recipe. The array version is the worked example in the next section.

When you need a Custom Function node or HLSL

Quick test. If you can describe the effect as a fixed chain of maths on values the node library can already read (UVs, time, positions, normals, textures, vertex colour, material properties), build it from nodes. If it needs a loop, data the nodes cannot read, or lighting the provided targets do not compute, write that part in HLSL inside a Custom Function node. If it needs to change how the shader is drawn (passes, render state, vertex structures), even a Custom Function node is not enough and you need a hand-written ShaderLab shader.

NeedWhy nodes fall shortUse
A loop with many or variable steps: raymarching, a many-sample blur, summing several impactsShader Graph has no loop node; copying nodes only works for a small, fixed countCustom Function. Check the node library first: some looped effects already exist as nodes, such as Parallax Occlusion Mapping
Custom lighting across the main light and all additional lights (toon ramps, custom specular)The Lit target gives Unity’s own PBR lighting; an Unlit graph has no node that loops over every lightCustom Function calling URP’s lighting functions. Unity’s Shader Graph package includes a Custom Lighting sample built this way, and the URP manual notes that under Forward+ the additional-light loop must be written differently (Unity Technologies, Unity Manual: Introduction to custom lighting in URP)
Arrays or other data the Blackboard cannot holdBlackboard properties have no array typeCustom Function with a global array set from C# (example below)
An existing HLSL function to port, or a function shared by many graphsRebuilding tested code as nodes is slow and error-proneCustom Function in File mode, so every graph includes the same .hlsl file. For reuse of node logic alone, a Sub Graph is enough
Extra passes, render state such as ColorMask or stencil, or changes to the vertex input and output structuresCustom Function nodes cannot “define extra shader passes”, “define extra render state commands such as ColorMask” or “change or add attributes in the input and output structures” (Unity Technologies, Unity Manual: Introduction to custom lighting in URP)A hand-written ShaderLab shader. For a stencil mask in URP, a Render Objects Renderer Feature with a stencil override is often enough (see unity-urp-lighting-and-render-features)
Tessellation or a geometry stageURP Shader Graph has neither (HDRP’s Shader Graph offers tessellation)A hand-written shader, or a different technique such as a denser mesh or vertex displacement

How the Custom Function node works (Unity Technologies, Shader Graph Manual: Custom Function Node, Shader Graph 17):

  1. Add a Custom Function node and open the Graph Inspector (Node Settings). Define its Inputs and Outputs there; the names you type become the variable names in your code, and the function’s arguments must match these ports in order.
  2. Choose a Type:
    • String mode: “Directly write functions in the node.” You type only the function body, and Shader Graph generates the signature from the Name and ports. A node named MyFunction with inputs A, B and output Out generates void MyFunction_float(float3 A, float B, out float3 Out). Write $precision instead of float or half inside the body if it should follow the node’s precision. Good for a few lines.
    • File mode: “Reference an external file that contains the functions.” Point the node at a .hlsl file anywhere in the project. Good for longer code and anything shared between graphs.
  3. Name. Type the function name without the precision suffix. In File mode the file must contain the function with the suffix: MyFunction_float for full precision, and MyFunction_half as well if the node runs at half precision. Outputs are out parameters on a void function.
  4. Include guards. A File-mode file needs an #ifndef / #define / #endif guard with an ID unique to that file.
  5. Globals. A variable declared outside the function becomes a global uniform: it can only be set globally from C# (for example with Shader.SetGlobalVectorArray), not per material.
  6. Previews. The node previews cannot reach the render pipeline’s libraries, so code that calls URP lighting functions needs an #ifdef SHADERGRAPH_PREVIEW branch that returns default values. Unity also warns that “the code in libraries might change over time from one Unity version to another”, so retest such functions after upgrading.

Worked example: many ripples at once (a loop plus an array). The single-impact ripple above becomes a Custom Function in File mode with Name MultiRipple, inputs UV (Vector 2), Time, Frequency, Speed, Damping (Floats) and output Height (Float):

#ifndef MULTI_RIPPLE_INCLUDED
#define MULTI_RIPPLE_INCLUDED
float4 _Impacts[8];   // global: xy = UV centre, z = impact time; set from C#
float _ImpactCount;
 
void MultiRipple_float(float2 UV, float Time, float Frequency, float Speed,
                       float Damping, out float Height)
{
    Height = 0;
    for (int i = 0; i < (int)_ImpactCount; i++)
    {
        float d = distance(UV, _Impacts[i].xy);
        float t = max(Time - _Impacts[i].z, 0);
        float r = t * Speed;
        Height += sin((d - r) * Frequency) * exp(-Damping * t) * step(d, r);
    }
}
#endif

From C#, keep a Vector4[8] of recent impacts and call Shader.SetGlobalVectorArray("_Impacts", impacts) and Shader.SetGlobalFloat("_ImpactCount", count). Always pass an array of the full declared length (8), and set the node’s Precision to Single (or add a MultiRipple_half version). The loop and the array are exactly the two things the node library cannot express, which is why this effect needs HLSL while the single ripple did not. The HLSL was syntax-checked outside Unity (with glslangValidator’s HLSL front end) but not tested in a Unity project, so check the Console for shader errors when you first use it.

Gotchas

  • Shader Graph support depends on the render pipeline: URP and HDRP are the main targets, and the Built-In target exists for compatibility only, with some blocks unavailable.
  • Visual authoring can hide complexity; node graphs can still become difficult to maintain.
  • A graph can look simple while generating a large amount of shader code underneath.
  • Some effects need HLSL in a Custom Function node, and some need a hand-written ShaderLab shader; see “When you need a Custom Function node or HLSL” above.
  • A Custom Function node that calls URP lighting or other pipeline functions breaks the node previews unless it has an #ifdef SHADERGRAPH_PREVIEW branch with default values.

shader-fundamentals · blender-overview · game-feel · unity-urp-overview · unity-urp-material-performance · vfx-value-and-colour · unity-urp-lighting-and-render-features · source-shader-graph · source-unity-urp-shaders-vfx · unity-reflection-probes-and-specular · evidence-based-learning