Shaders Explained: From Pixels to Visual Effects
All of these are shaders.
Water, holograms, dissolves, toon shading — all four panels above are just small GLSL programs running on your GPU right now. By the end of this post you’ll understand how each one works, and you’ll have written and edited them yourself.
1. What is a shader?
A shader is a small program that runs on the GPU and controls how something is drawn.
Every pixel of your game, every wobble of water, every glow on a sword — it’s a shader deciding “for this specific pixel of this specific surface, what color comes out?“
2. CPU vs GPU
Gameplay code runs on the CPU. Shaders run on the GPU. The two chips are built for completely different jobs.
- → Branching, if/else, decisions
- → Reads files, talks to network
- → Runs gameplay logic, AI
- → Same math on millions of items
- → No branching, just compute
- → Runs once per vertex / pixel
A 1080p screen has over 2 million pixels. The fragment shader runs once for every one of them, every frame. That parallelism is why effects that would be impossibly slow on a CPU — animated noise, real-time lighting, blur — feel instant on a GPU.
3. The rendering pipeline
When the engine draws a mesh, the GPU walks it through a fixed sequence of stages. You don’t write all of them — only the two stages in green are user-programmable.
Two programmable stages. Two jobs. That’s it. Every game engine — Unity, Unreal, Godot, custom — exposes those two stages under different names but they do the same work.
4. Vertex shader: moves points
The vertex shader runs once per vertex of your mesh. Its job is to decide where each point ends up — and along the way it can move, bend, inflate, or animate the geometry.
Below: a sphere where every vertex gets pushed along its normal by a sine wave that varies across the surface. Open the vertex.glsl tab and tweak the math.
Typical jobs for the vertex shader in a game:
- Water and ocean surfaces
- Grass and foliage swaying in wind
- Character squash-and-stretch
- Skinning bones to skinned meshes
- Banner flags rippling
5. Fragment shader: colors pixels
The fragment shader runs once per pixel the rasterizer produced. Its job is to output a color — and that’s where most of the visual variety lives.
Below: a fragment shader that paints each pixel by the direction its surface faces. The cube has six normals, so it gets six colors. Try replacing vNormal with vec3(vUv, 0.5) to see what UV-based coloring looks like.
Typical jobs for the fragment shader:
- Sampling textures (albedo, normal, roughness…)
- Lighting and shading models (Lambert, Blinn-Phong, PBR)
- Glows, holograms, dissolves
- Patterns, stripes, gradients
- Outlines, toon shading
- Heat haze, glitches, screen-space FX
Vertex shaders change shape. Fragment shaders change color. They run as a pair for every draw call.
6. Five concepts you have to know
Every shader you’ll ever write or read is built on the same handful of ideas:
- UVs — where on the surface this pixel is
- Normals — which way the surface is facing
- Textures — images sampled inside the shader
- Time — a uniform that increments each frame, the source of all animation
- Uniforms — values your app sends in (color, speed, threshold, anything)
Let’s hit the ones that come up most often.
7. UVs and textures
vUv is a vec2 that goes from (0, 0) at the bottom-left of the surface to (1, 1) at the top-right. It’s how the shader knows where on the surface this pixel is.
Map it straight into RGB and you instantly see what the GPU sees — each face of the cube has its own 0..1 UV range, so each face gets its own gradient:
Try replacing vUv.y with 1.0 - vUv.y and watch the picture flip. The shader doesn’t have any concept of “up” — it just runs your math.
Textures
In a real game, you almost never compute colors from scratch — you sample a texture (an image baked by an artist) at the same vUv:
uniform sampler2D uTexture;
varying vec2 vUv;
void main() {
vec3 col = texture2D(uTexture, vUv).rgb;
gl_FragColor = vec4(col, 1.0);
}
That single texture2D call is the workhorse of game graphics. Diffuse maps, normal maps, roughness maps, masks, lookup tables — all just texture2D(map, uv).
The playground below doesn’t load a real PNG (we’re in a browser), but it computes a checkerboard pattern directly from vUv — exactly the way a texture lookup would feel. Edit uTiles or swap the math to see the UV→pattern mapping:
In production, you’d replace the procedural math with one texture2D call. The shader doesn’t know or care where the colors come from — image, math, screen capture, render target — same vec4 out the other end.
8. Time
Shaders don’t have a scene graph or keyframes. Animation comes from one uniform: uTime (seconds since start). Feed it into sin() and you get a smooth oscillation between -1 and 1. Remap that to 0..1 and you can drive anything — color, position, glow strength.
The sliders are uniforms — values pushed from the CPU into the shader each frame. Notice they update the visual without recompiling; only editing the code triggers a rebuild.
9. Noise
Most “magical-looking” effects — lava, dissolve, smoke, water, clouds — are noise plus a threshold.
The shader below uses value noise plus FBM (Fractional Brownian Motion — summing several octaves of noise at increasing frequencies). The single-octave version on its own looks blocky; FBM is what gives it that organic, cloudy quality. The same pattern is how Perlin and Simplex noise are used in production — different base function, same multi-octave trick on top.
Drag the dissolve slider from 0 to 1 and watch the cube eat itself face by face:
The pattern is universal: noise → compare to threshold → discard or color. That’s enemy death effects, teleports, portal fades, magic shields — all the same recipe with different noise and different colors.
10. Vertex animation: a bigger example
Vertex shaders aren’t just for waves. Stack a few sines together and a smooth sphere becomes a breathing alien blob:
Click the vertex.glsl tab and look at the key line:
vec3 displaced = position + normal * wave * uAmp;
That’s water. Or flags, or grass, or jelly. Same idea, different mesh, different amplitude.
11. In Unity: Shader Graph
Unity ships two ways to write shaders. The high-level one is Shader Graph — a node-based editor where you wire boxes together instead of typing code. The same dissolve effect from §9 looks like this:
Properties exposed to the inspector (so designers can tune the effect without touching the graph):
DissolveAmount— float, animated by the Animator or VFX GraphEdgeColor— color of the glowing rimEdgeWidth— how soft the edge isNoiseScale— bigger = finer pattern
In a game, you’d assign this material to your enemy mesh, then drive DissolveAmount from a death state machine (0 alive → animate to 1 over 0.6s on hit-kill). The dissolve itself is just Unity’s Alpha Clip Threshold doing the discard work. Shader Graph is fantastic for designers and rapid iteration; you give up some performance control compared to hand-written HLSL.
12. In Unity: hand-written HLSL
For tight performance work or things Shader Graph can’t express, you drop into ShaderLab + HLSL. Looks intimidating, but it’s all chrome around two functions — vert and frag:
Shader "Custom/PulsingEmissive" {
Properties {
_ColorA ("Color A", Color) = (0, 1, 1, 1)
_ColorB ("Color B", Color) = (1, 0, 1, 1)
_Speed ("Speed", Float) = 2
}
SubShader {
Tags { "RenderType" = "Opaque" }
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
fixed4 _ColorA, _ColorB;
float _Speed;
struct appdata { float4 vertex : POSITION; };
struct v2f { float4 vertex : SV_POSITION; };
v2f vert(appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
fixed4 frag(v2f i) : SV_Target {
float pulse = sin(_Time.y * _Speed) * 0.5 + 0.5;
return lerp(_ColorA, _ColorB, pulse);
}
ENDCG
}
}
}
A few Unity-specific bits to know:
Propertiesdeclares what shows up in the material inspector. Designers tune these in the editor without touching code._Time.yis Unity’s per-frame time uniform — same asuTimeeverywhere else.UnityObjectToClipPosis the standard MVP transform helper.fixed4is a low-precision color type for mobile/console performance.
Save this as .shader, assign it to a Material, drop the material on any mesh. Pulses cyan→magenta forever. That’s an entire visual effect in 30 lines.
For vertex animation (waves, wind, etc.), you’d return a modified o.vertex from vert() — same recipe as the playgrounds above, just in HLSL.
13. In Blender: shader nodes
Blender uses shader nodes too, but for offline rendering rather than runtime games. A procedural lava material is just:
Suggested values to get convincing lava:
- Noise scale: 8–15
- Noise detail: 10
- ColorRamp dark stop: black / dark red
- ColorRamp bright stop: orange / yellow
- Emission strength: 1.5–4
If you ever export pre-rendered VFX (cutscenes, key art, marketing renders), Blender’s the cheapest way to do it. Same mental model as Shader Graph; different target.
14. Same idea, different toolchains
| Tool | How you write | Best for |
|---|---|---|
| Unity Shader Graph | Visual nodes | Designer-friendly, rapid iteration |
| Unity HLSL | Code in ShaderLab | Performance, anything graph can’t do |
| Unreal Material Editor | Visual nodes | Same role as Shader Graph |
| Godot Shader / Visual Shader | GLSL-ish code or nodes | Open-source, simpler material model |
| Blender | Material nodes | Offline rendering, look-dev |
Different syntax, identical mental model. UVs in, math in the middle, color out. Once you’ve internalized the recipe, switching engines is mostly switching dialects.
15. Where to go next
Once you’re comfortable with the five ingredients — UVs, time, noise, textures, normals — most “advanced” shader effects start looking like remixes of the same handful of tricks:
- Fresnel rim light:
pow(1.0 - dot(normal, viewDir), 3.0)→ hologram, energy shields. - Scrolling UVs:
texture2D(tex, vUv + vec2(uTime * 0.1, 0.0))→ conveyor belts, waterfalls, lava flow. - Layered noise (FBM): sum multiple noise octaves at increasing frequencies → realistic clouds, terrain, smoke.
- Screen-space effects: read the previous frame’s pixel and distort UVs → heat haze, glitch, CRT.
16. Further reading
Pipeline & fundamentals
- Khronos Wiki — Rendering Pipeline Overview — authoritative reference for how the stages connect.
- Khronos Wiki — Vertex Shader and Fragment Shader — exactly what each programmable stage does.
- Stanford EE267 — Graphics Pipeline (PDF) — clean academic walk-through with the canonical pipeline diagrams.
- LearnOpenGL — Shaders chapter — best free progressive intro, OpenGL/GLSL focused.
Noise & procedural patterns
- The Book of Shaders — Noise and Fractal Brownian Motion — the chapters that the dissolve in §9 is built on. Patricio Gonzalez Vivo’s free interactive book.
- Inigo Quilez — articles — the bible for procedural shader tricks: raymarching, signed distance functions, noise variants, FBM, lighting math.
- Shadertoy — live gallery of GLSL shaders with editable source. Reading other people’s code is how you level up.
Game engines
- Unity ShaderLab — Reference — full syntax around the HLSL in §12.
- Unity HDRP — Alpha Clipping — the mechanism behind the dissolve from §11.
- Blender Manual — Shader Nodes — every node in §13’s lava graph, with parameters.
Three.js (what the playgrounds run on)
- Three.js — ShaderMaterial — the JS↔GLSL bridge used by every interactive panel above.
- Three.js documentation — the rest of the library if you want to recreate the playgrounds yourself.
17. The takeaway
A shader is not magic. It’s code that runs for vertices or pixels.
Once you internalize that, the rest is recipe collecting. UVs, time, noise, textures, color math, lighting — combine four of them creatively and you can build almost anything you’ve ever seen in a game.
The playgrounds above are real GLSL talking to real WebGL — no sandbox, no transpilation. The same lines of code work, character-for-character, in Godot’s GLSL shaders and translate one-for-one into Unity HLSL or Unreal HLSL. Break things. That’s how you learn what each line is actually doing.