Summary
A shader is a small program that runs on the graphics card (GPU) rather than the CPU. Godot’s documentation puts it simply: “Shaders are a special kind of program that runs on Graphics Processing Units (GPUs)” (Godot Engine, Introduction to shaders). A shader does not run once per object like a C# Update() method. Instead the GPU runs the same shader code thousands of times in parallel: once for every vertex of a mesh, and then once for every pixel that the mesh covers on screen. Everything else on this page follows from that idea. Whether a student builds a shader visually in Shader Graph or writes HLSL by hand, the result is the same kind of program: a per-vertex function and a per-pixel function that turn mesh data, textures and material properties into a colour.
Key ideas
- The GPU pipeline, at student level. A draw call sends a mesh through three stages that matter to beginners:
- Vertex stage. A vertex function runs once for every vertex. Its main job is to move the vertex from the model’s own space into clip space (where the camera can project it), and to pass along data such as UVs and normals. Godot describes its
vertex()function as one that “runs over all the vertices in the mesh and sets their positions” (Godot Engine, Introduction to shaders). - Rasterisation. Fixed GPU hardware (not your code) works out which screen pixels each triangle covers. Microsoft’s Direct3D documentation describes this stage as converting “vector information (composed of shapes or primitives) into a raster image (composed of pixels)”, and notes that “each primitive is converted into pixels, while interpolating per-vertex values across each primitive” (Microsoft, Rasterizer Stage).
- Fragment (pixel) stage. A fragment function runs once for every covered pixel (strictly, every fragment, a candidate pixel). It receives the interpolated values from the vertex stage and returns a colour. Godot’s
fragment()“runs for every pixel covered by the mesh” (Godot Engine, Introduction to shaders).
- Vertex stage. A vertex function runs once for every vertex. Its main job is to move the vertex from the model’s own space into clip space (where the camera can project it), and to pass along data such as UVs and normals. Godot describes its
- Per vertex versus per pixel. A cube has 24 vertices in Unity, but on a 1080p screen it can easily cover hundreds of thousands of pixels. Work placed in the vertex stage therefore usually runs far fewer times than work in the fragment stage. Unity’s optimisation advice is to “Move calculations from the fragment shader to the vertex shader, so the ALU calculates once per vertex instead of once per fragment. If you need per-fragment values, let the GPU interpolate the per-vertex values” (Unity Technologies, Unity Manual: Optimize custom shaders, 6000.5). The trade-off is detail: a value computed per vertex is smoothly blended across the triangle, so it cannot show detail smaller than the triangles.
- Each invocation is isolated. One pixel’s fragment function cannot read the result of its neighbour’s, and nothing is remembered between frames. Godot’s documentation states that “Shader code runs on each vertex or pixel in isolation” and “You cannot store data between frames either” (Godot Engine, Introduction to shaders). Anything that changes over time must be computed from inputs such as the current time, or sent in from C# as a material property.
- Data types. HLSL works with small fixed-size vectors.
floatis one number;float2,float3andfloat4hold two, three and four (for example a UV, a position or normal, and a colour with alpha). Matrices such asfloat4x4transform positions between spaces. Shader Graph shows the same types as colour-coded ports: Float, Vector 2, Vector 3, Vector 4 and Matrix types (Unity Technologies, Shader Graph Manual: Data Types). - Precision:
floatversushalf. Unity’s Shader Graph manual describes Single as “a high-precision floating point value”, typically 32 bits, suited to world-space positions, texture coordinates, and trigonometry or exponentiation, and Half as “a low-precision floating point value”, typically 16 bits, suited to short vectors, directions, object-space positions and many colours (Unity Technologies, Shader Graph Manual: Precision Modes). Unity’s hand-written shader advice is the same rule from the other side: usehalf“for all variables except world space coordinates and texture coordinates” (Unity Technologies, Unity Manual: Optimize shaders, 6000.2). The benefit depends on the hardware: “By default,halfismin16floaton platforms like mobile andfloaton other platforms” (Unity Technologies, Unity Manual: Shader data types and precision, 6000.0), so on a desktop GPUhalfoften makes no difference at all. - UVs. UV coordinates are a 2D address stored on each vertex that says which point of a texture belongs at that vertex, usually from (0, 0) at one corner of the texture to (1, 1) at the opposite corner. The rasteriser interpolates them, so every pixel gets its own UV. Shader Graph’s UV node “outputs the vertex or fragment UV coordinates of a mesh”, with channels UV0 to UV7 (Unity Technologies, Shader Graph Manual: UV Node). Because a UV is just a pair of numbers, maths on it (distance from a centre, offsets, tiling) is how many procedural effects are built. Modelling-side UV unwrapping is covered in 3d-materials-and-uvs.
In practice
How Shader Graph nodes become HLSL (Unity URP)
Shader Graph is a code generator. Unity’s docs describe it as the visual alternative to writing shader code (Unity Documentation, Shader Graph, see source-shader-graph), and the Shader Graph asset inspector has a View Generated Shader button that “Opens the shader code that the shader graph generates in a text editor or an IDE”, plus Copy Shader to copy it to the clipboard (Unity Technologies, Shader Graph Manual: Shader Graph Asset; Unity, Create Popular Shaders and Visual Effects with URP, see source-unity-urp-shaders-vfx). Reading that file shows exactly how the graph becomes GPU maths:
-
Each node becomes an HLSL function or expression. Every node page in the Shader Graph manual ends with a “Generated Code Example”. The Sine node, for example, becomes:
void Unity_Sine_float4(float4 In, out float4 Out) { Out = sin(In); }The Distance node wraps HLSL’s
distance(A, B), the Multiply node wrapsA * B(ormulfor matrices), and the Time node is not a function at all but a read of a built-in variable such as_Time.y(Unity Technologies, Shader Graph Manual: Sine, Distance, Multiply and Time node pages). The_float4suffix shows the type and precision the generator chose for that copy of the function. -
Each wire becomes a typed local variable. When you connect an output port to an input port, the generator declares a variable of that port’s type for the output, passes it as the
outargument of the first node’s function, and then passes it as an input to the next node’s function. In Unity’s Shader Graph source code, these variable names are built from the node’s name, the slot (port) name, the slot ID and the type, which is why generated files are full of names like_Sine_…_Out_1_Float(Unity Technologies,AbstractMaterialNode.GetVariableNameForSlot, Unity Graphics repository on GitHub). -
Mismatched widths are converted for you. Connecting a Vector 4 to a Vector 2 input truncates it (“excess channels are simply removed”), and connecting a smaller vector to a larger input pads the missing channels with default values (Unity Technologies, Shader Graph Manual: Data Types). This is convenient, but it can silently throw away the channel you meant to use.
-
The Master Stack decides which stage each chain runs in. The Master Stack “contains two Contexts: Vertex and Fragment”. Nodes connected to Vertex blocks (such as Position) feed the vertex function; nodes connected to Fragment blocks (such as Base Color) feed the fragment function. If the same nodes feed both, “they are executed twice, once in the vertex function and then again in the fragment function” (Unity Technologies, Shader Graph Manual: Master Stack). In the generated code the two halves appear as functions named
VertexDescriptionFunctionandSurfaceDescriptionFunction, which URP’s pass code calls from its own vertex and fragment programs (Unity Technologies, Shader GraphGenerator.csand URPVaryings.hlsl, Unity Graphics repository on GitHub).
So the honest answer to “how do node connections become GPU maths?” is: the graph is flattened into an ordered list of function calls, one per node, joined by typed variables, one per wire, inside a per-vertex function and a per-pixel function. The GPU then runs those functions for every vertex and every pixel in parallel. The node preview thumbnails are the same idea in miniature: each one shows that node’s output evaluated per pixel across a preview shape.
Some nodes are only valid in one stage. Sample Texture 2D, with its default settings, “can only connect to a Block node in the Fragment Context”; to sample a texture in the vertex stage you set its Mip Sampling Mode to LOD (Unity Technologies, Shader Graph Manual: Sample Texture 2D Node). This follows from the pipeline: in its default mode the GPU picks a mipmap level by comparing UVs across neighbouring pixels, and the vertex stage has no neighbouring pixels to compare.
The same idea in hand-written HLSL (URP ShaderLab)
A hand-written URP shader makes the two stages explicit. The ShaderLab wrapper (Shader, SubShader, Pass) tells Unity how to use the program; the HLSL between HLSLPROGRAM and ENDHLSL is the program. This example follows the structure of Unity’s basic URP unlit shader (Unity Technologies, Unity Manual: Write an unlit basic shader in URP), with a UV passed from the vertex stage to the fragment stage so the interpolation is visible:
Shader "Examples/UVColour"
{
SubShader
{
Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }
Pass
{
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; };
struct Varyings { float4 positionHCS : SV_POSITION; float2 uv : TEXCOORD0; };
Varyings vert(Attributes IN) // runs once per vertex
{
Varyings OUT;
OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
OUT.uv = IN.uv; // interpolated by the rasteriser
return OUT;
}
half4 frag(Varyings IN) : SV_Target // runs once per pixel
{
return half4(IN.uv, 0, 1); // red = U, green = V
}
ENDHLSL
}
}
}Applied to a quad, this shows a smooth black-to-red-to-yellow-to-green gradient even though only four UV values were stored: the rasteriser filled in every value in between. Unity’s docs note that TransformObjectToHClip “transforms vertex positions from object space to homogenous clip space”, and that the output position “must have the SV_POSITION semantic” (Unity Technologies, Unity Manual: Write an unlit basic shader in URP).
Why branches and texture samples cost
- Branching. GPUs get their speed by running many invocations in lockstep. Unity’s manual explains that branching on a non-uniform value (one that differs per pixel) means the GPU must either “perform different operations at the same time (and therefore break parallelism)” or “flatten the branch” by “performing the operations for both branches and then discarding one result”. Branching on a uniform value (the same for the whole draw call) is “usually more efficient”. In every case “the GPU must allocate register space for the worst case”, so a branch with one expensive side wastes resources even when that side is rarely taken (Unity Technologies, Unity Manual: Introduction to static and dynamic branching, 6000.0). In Shader Graph the cost is simpler to state: the Branch node is generated as
Out = Predicate ? True : False;, and “Both sides of the branch are evaluated in the shader, and the output from the unused path is discarded” (Unity Technologies, Shader Graph Manual: Branch Node). A Branch node therefore does not save work; it chooses between two results that were both computed. Static branching (conditions resolved at compile time, for example with preprocessor directives) has “no runtime performance cost” but cannot react to values that change while the game runs (Unity Technologies, Unity Manual: Introduction to static and dynamic branching). When keywords are used to produce separate compiled versions of a shader (shader variants), each extra variant adds build time and memory instead. - Texture samples. Arithmetic is fast; fetching texture data from memory is comparatively slow, and many samples per pixel can make a shader limited by “texture bandwidth”, meaning the GPU is approaching the limit of how much texture data it can read per second. Unity recommends using maths instead of a texture where a gradient or shape can be computed, packing several greyscale maps into the channels of one texture, and enabling mipmaps (Unity Technologies, Unity Manual: Optimize custom shaders, 6000.5). URP’s own Lit shader shows the scale of the issue: Unity describes it as the most computationally heavy shading model in URP (Unity Technologies, Lit Shader, see source-unity-urp-lit-shader).
- Expensive maths. Not all maths is equal. Unity lists
sqrt,log,exp,sinandcosamong slower operations and suggestsdotandcrosswhere possible (Unity Technologies, Unity Manual: Optimize custom shaders, 6000.5). This matters at scale rather than for a single effect: onesinin a ripple shader is fine; a dozen per pixel across a full-screen effect on mobile may not be.
The practical habit is to measure rather than guess: use the unity-profiler and Frame Debugger to see whether a shader is actually the bottleneck before optimising it.
Godot (shading language)
Godot’s shading language is “based on the popular OpenGL Shading Language (GLSL) but simplified” (Godot Engine, Introduction to shaders). A shader declares its type on the first line (spatial for 3D, canvas_item for 2D, plus particles, sky and fog) and fills in processor functions: vertex() per vertex, fragment() per pixel, and light() per pixel per light. Built-ins replace Unity’s nodes and properties: UV, TIME (“Global time since the engine has started, in seconds”) and outputs such as ALBEDO (Godot Engine, Spatial shaders). The radial ring idea from shader-graph-overview looks like this:
shader_type spatial;
void fragment() {
float d = distance(UV, vec2(0.5));
ALBEDO = vec3(0.5 + 0.5 * sin(d * 40.0 - TIME * 4.0));
}Godot also has VisualShaders, “the visual alternative for creating shaders”; the graph “is converted to a script shader behind the scene”, and the generated code can be viewed from the toolbar (Godot Engine, Using VisualShaders). See godot-overview.
Unreal Engine (Material Editor)
Unreal’s Material Editor is “a node-based graph interface with which you can create Materials (also called shaders)” (Epic Games, Material Editor Guide). The model matches Shader Graph closely: “Shaders in Unreal Engine are written in High Level Shading Language (HLSL)”, “these node graphs are silently translated to HLSL”, and “a Material Expression is simply a visual representation of a snippet of HLSL code”. The generated code can be viewed, read-only, from Window > Shader Code > HLSL Code (Epic Games, Essential Unreal Engine Material Concepts). Expressions connect to the Main Material Node, which plays the role of Unity’s Master Stack.
Evidence
- Unity’s Shader Graph manual documents the generated HLSL for each node, the two Master Stack contexts, the data types and the precision modes (Unity Technologies, Shader Graph Manual, package version 17.0, accessed 2026-10-25).
- Unity’s Manual pages on branching and shader optimisation are the basis for the cost section; note that the optimisation page has changed between Unity 6.2 (Optimize shaders) and 6.5 (Optimize custom shaders), and the newer page adds explicit texture-read guidance.
- The description of rasterisation comes from Microsoft’s Direct3D documentation, which is the reference for HLSL’s pipeline model; Unity, Godot and Unreal sit on the same hardware pipeline.
- Godot’s and Epic’s documentation independently describe their visual editors as front ends that generate text shader code, which supports treating “graph = generated code” as an engine-agnostic idea rather than a Unity detail.
- Unity’s URP shader and VFX e-book points students to View Generated Shader for inspecting the HLSL behind a graph (Unity, Create Popular Shaders and Visual Effects with URP, see source-unity-urp-shaders-vfx).
Implications
- Students who understand the vertex and fragment stages can predict where an effect belongs: vertex displacement (waves on a mesh, wobble) needs enough vertices to show the shape, while colour, UV distortion and normal detail can be done per pixel on a simple mesh.
- Reading View Generated Shader once, for a three-node graph, is arguably the fastest way to demystify Shader Graph, and it makes the move to hand-written HLSL much less abrupt.
- Because a Branch node computes both sides, replacing it with a
Lerpor aStep-based mask often costs the same and reads more clearly; true savings come from keywords (static branching) or from removing work altogether. - Defaulting to
floatis safe while learning; switching graphs or Sub Graphs to Half is an optimisation step for mobile targets, best checked on the device.
Open questions
- How far should a first-year module go into the generated code, beyond recognising node functions and wire variables?
- The relative cost of
sin,expand texture samples differs across GPUs; a small profiling exercise on a lab machine and a phone would turn this page’s general advice into local evidence.
Related
shader-graph-overview · unity-urp-overview · unity-urp-lighting-and-render-features · 3d-materials-and-uvs · unity-profiler · godot-overview · material-maker · vfx-shape-and-timing · source-shader-graph · source-unity-urp-shaders-vfx · source-unity-urp-lit-shader · unity-urp-material-performance