Shader programming (cross-engine)
Shaders are small programs that run per vertex and per pixel on the GPU.
The concepts — the pipeline, coordinate spaces, UVs, and how common effects are
built — port across engines; only the language dialect and built-in variable
names change. This skill teaches those portable fundamentals in GLSL with HLSL
equivalents; use godot-shaders (or Unity/Unreal material docs) for the exact
engine syntax and built-ins.
When to use
- Use to understand or write vertex/fragment shaders and to reason about UVs,
coordinate spaces, and the GPU pipeline.
- Use to build common effects: tint/recolor, scrolling textures, dissolve,
outlines, fresnel/rim light, vignette, color grading.
- Use to translate a shader concept between GLSL and HLSL, or between engines.
When not to use: for an engine's exact shader language and built-ins, use
godot-shaders (Godot shading language) or the engine's material docs. For full
particle VFX systems, see unreal-niagara. For post-process stacks, defer to
the engine's renderer settings.
Core workflow
- Know which stage you're in. The vertex shader transforms each vertex
into clip space and passes data (UVs, normals) onward; the fragment/pixel
shader runs per rasterized pixel and outputs a color. Most game effects live
in the fragment stage.
- Track coordinate spaces. Positions move model → world → view → clip space;
normals belong in world or view space. Mixing spaces is the most common bug.
- Drive effects with UVs and time. UVs are
0..1 texture coordinates;
offset, scale, or distort them, and animate with a time uniform.
- Work per pixel, branch-light. Prefer
mix, step, smoothstep, and
clamp over if where possible; GPUs run pixels in lockstep and dislike
divergent branches.
- Pass data via uniforms (constant per draw) and varyings (interpolated
vertex→fragment). Keep texture samples few; they dominate cost.
- Verify visually and on target hardware. Shaders that look right on desktop
can break on mobile (precision, missing features). Test where it ships.
Patterns
GLSL-style fragment snippets (close to Godot's canvas_item/spatial
shaders and OpenGL). See references/effects.md for the HLSL equivalents and
the full outline/fresnel/vignette shaders.
1. Fragment basics: sample, tint, and combine
// Per-pixel: read the texture at this UV, multiply by a color (tint), keep alpha.
uniform sampler2D tex;
uniform vec4 tint; // e.g. (1,0,0,1) reddens; multiply is non-destructive
in vec2 uv; // interpolated 0..1 texture coordinate (a "varying")
out vec4 frag;
void main() {
vec4 c = texture(tex, uv); // HLSL: tex.Sample(samp, uv)
frag = c * tint; // component-wise multiply tints without clipping
}
2. Scrolling UVs (animated texture) — frame-rate independent
// Add time * speed to the UV to scroll. fract() wraps it into 0..1 so it tiles.
uniform sampler2D tex;
uniform float time; // seconds, supplied by the engine
uniform vec2 scroll_speed; // UV units per second, e.g. (0.1, 0.0)
in vec2 uv;
out vec4 frag;
void main() {
vec2 scrolled = fract(uv + scroll_speed * time); // HLSL: frac(...)
frag = texture(tex, scrolled);
}
// Drive with a real time uniform, not a per-frame accumulator, so speed is stable.
3. Dissolve (threshold a noise map, glow the edge)
// Hide pixels where noise < threshold; tint a thin band at the boundary.
uniform sampler2D tex;
uniform sampler2D noise_tex; // grayscale noise, 0..1
uniform float amount; // 0 = fully visible, 1 = fully dissolved
uniform float edge = 0.05; // width of the glowing edge band
uniform vec4 edge_color;
in vec2 uv;
out vec4 frag;
void main() {
vec4 c = texture(tex, uv);
float n = texture(noise_tex, uv).r;
if (n < amount) discard; // cut away dissolved pixels
float e = smoothstep(amount, amount + edge, n); // 0 at the edge -> 1 inside
frag = mix(edge_color, c, e); // HLSL: lerp(edge_color, c, e)
}
4. Fresnel rim light (3D) — brighten glancing angles
// Rim = 1 where the surface faces away from the camera (silhouette glow).
in vec3 world_normal; // normalized, world space (from the vertex stage)
in vec3 view_dir; // normalized, surface -> camera, world space
uniform float power = 3.0;
uniform vec3 rim_color;
out vec4 frag;
void main() {
float f = pow(1.0 - clamp(dot(world_normal, view_dir), 0.0, 1.0), power);
frag = vec4(rim_color * f, 1.0); // add to lighting; f peaks at the silhouette
}
// Correctness: normal and view_dir MUST be in the same space and normalized.
Pitfalls
- Mixing coordinate spaces (lighting a world-space normal against a
view-space light) yields subtly wrong shading. Pick one space and convert
everything into it.
- Forgetting to normalize interpolated normals/directions: interpolation
shortens vectors, so
dot() results drift. normalize() in the fragment stage.
- UV assumptions across engines. Some engines flip V (top-left vs bottom-left
origin); a texture may appear upside-down. Know your engine's convention.
- Heavy branching / dynamic loops stall GPUs. Prefer
step/smoothstep/
mix; reserve if/discard for genuinely cheap early-outs.
discard defeats early-Z and can hurt performance on tiled mobile GPUs;
prefer alpha blending where you can.
- Precision on mobile:
highp vs mediump matters; large UVs or time values
in low precision shimmer. Use adequate precision for coordinates and time.
- Assuming GLSL == HLSL.
mix↔lerp, fract↔frac, texture()↔.Sample(),
vec2↔float2, column- vs row-major matrices. See the reference mapping.
References
references/effects.md — full outline (2D sprite + 3D), vignette, and color
grading shaders; the GLSL↔HLSL function/type mapping table; per-engine notes
(Godot canvas_item/spatial, Unity ShaderLab/HLSL, Unreal material nodes).
Related skills
godot-shaders — Godot shading language syntax, built-ins, and screen-reading.
unreal-niagara — GPU particle VFX (a different shader use).
procedural-gen — the noise that drives dissolve and procedural texturing.
1---
2name: shader-programming
3description: >
4 Write game shaders from cross-engine fundamentals — the vertex→fragment
5 pipeline, coordinate spaces, UV math, and common 2D/3D effects (tint, UV
6 scroll, dissolve, outline, fresnel rim, vignette) in GLSL with HLSL
7 equivalents. Use when the user mentions shaders, fragment/pixel shader, vertex
8 shader, UV, GLSL, HLSL, or effects like dissolve, outline, or rim light.
9---
10
11# Shader programming (cross-engine)
12
13Shaders are small programs that run **per vertex** and **per pixel** on the GPU.
14The concepts — the pipeline, coordinate spaces, UVs, and how common effects are
15built — port across engines; only the language dialect and built-in variable
16names change. This skill teaches those portable fundamentals in GLSL with HLSL
17equivalents; use `godot-shaders` (or Unity/Unreal material docs) for the exact
18engine syntax and built-ins.
19
20## When to use
21
22- Use to understand or write vertex/fragment shaders and to reason about UVs,
23 coordinate spaces, and the GPU pipeline.
24- Use to build common effects: tint/recolor, scrolling textures, dissolve,
25 outlines, fresnel/rim light, vignette, color grading.
26- Use to translate a shader concept between GLSL and HLSL, or between engines.
27
28**When *not* to use:** for an engine's exact shader language and built-ins, use
29`godot-shaders` (Godot shading language) or the engine's material docs. For full
30particle VFX systems, see `unreal-niagara`. For post-process *stacks*, defer to
31the engine's renderer settings.
32
33## Core workflow
34
351. **Know which stage you're in.** The **vertex** shader transforms each vertex
36 into clip space and passes data (UVs, normals) onward; the **fragment/pixel**
37 shader runs per rasterized pixel and outputs a color. Most game effects live
38 in the fragment stage.
392. **Track coordinate spaces.** Positions move model → world → view → clip space;
40 normals belong in world or view space. Mixing spaces is the most common bug.
413. **Drive effects with UVs and time.** UVs are `0..1` texture coordinates;
42 offset, scale, or distort them, and animate with a `time` uniform.
434. **Work per pixel, branch-light.** Prefer `mix`, `step`, `smoothstep`, and
44 `clamp` over `if` where possible; GPUs run pixels in lockstep and dislike
45 divergent branches.
465. **Pass data via uniforms** (constant per draw) and **varyings** (interpolated
47 vertex→fragment). Keep texture samples few; they dominate cost.
486. **Verify visually and on target hardware.** Shaders that look right on desktop
49 can break on mobile (precision, missing features). Test where it ships.
50
51## Patterns
52
53GLSL-style fragment snippets (close to Godot's `canvas_item`/`spatial`
54shaders and OpenGL). See `references/effects.md` for the HLSL equivalents and
55the full outline/fresnel/vignette shaders.
56
57### 1. Fragment basics: sample, tint, and combine
58
59```glsl
60// Per-pixel: read the texture at this UV, multiply by a color (tint), keep alpha.
61uniform sampler2D tex;
62uniform vec4 tint; // e.g. (1,0,0,1) reddens; multiply is non-destructive
63in vec2 uv; // interpolated 0..1 texture coordinate (a "varying")
64out vec4 frag;
65void main() {
66 vec4 c = texture(tex, uv); // HLSL: tex.Sample(samp, uv)
67 frag = c * tint; // component-wise multiply tints without clipping
68}
69```
70
71### 2. Scrolling UVs (animated texture) — frame-rate independent
72
73```glsl
74// Add time * speed to the UV to scroll. fract() wraps it into 0..1 so it tiles.
75uniform sampler2D tex;
76uniform float time; // seconds, supplied by the engine
77uniform vec2 scroll_speed; // UV units per second, e.g. (0.1, 0.0)
78in vec2 uv;
79out vec4 frag;
80void main() {
81 vec2 scrolled = fract(uv + scroll_speed * time); // HLSL: frac(...)
82 frag = texture(tex, scrolled);
83}
84// Drive with a real time uniform, not a per-frame accumulator, so speed is stable.
85```
86
87### 3. Dissolve (threshold a noise map, glow the edge)
88
89```glsl
90// Hide pixels where noise < threshold; tint a thin band at the boundary.
91uniform sampler2D tex;
92uniform sampler2D noise_tex; // grayscale noise, 0..1
93uniform float amount; // 0 = fully visible, 1 = fully dissolved
94uniform float edge = 0.05; // width of the glowing edge band
95uniform vec4 edge_color;
96in vec2 uv;
97out vec4 frag;
98void main() {
99 vec4 c = texture(tex, uv);
100 float n = texture(noise_tex, uv).r;
101 if (n < amount) discard; // cut away dissolved pixels
102 float e = smoothstep(amount, amount + edge, n); // 0 at the edge -> 1 inside
103 frag = mix(edge_color, c, e); // HLSL: lerp(edge_color, c, e)
104}
105```
106
107### 4. Fresnel rim light (3D) — brighten glancing angles
108
109```glsl
110// Rim = 1 where the surface faces away from the camera (silhouette glow).
111in vec3 world_normal; // normalized, world space (from the vertex stage)
112in vec3 view_dir; // normalized, surface -> camera, world space
113uniform float power = 3.0;
114uniform vec3 rim_color;
115out vec4 frag;
116void main() {
117 float f = pow(1.0 - clamp(dot(world_normal, view_dir), 0.0, 1.0), power);
118 frag = vec4(rim_color * f, 1.0); // add to lighting; f peaks at the silhouette
119}
120// Correctness: normal and view_dir MUST be in the same space and normalized.
121```
122
123## Pitfalls
124
125- **Mixing coordinate spaces** (lighting a world-space normal against a
126 view-space light) yields subtly wrong shading. Pick one space and convert
127 everything into it.
128- **Forgetting to normalize** interpolated normals/directions: interpolation
129 shortens vectors, so `dot()` results drift. `normalize()` in the fragment stage.
130- **UV assumptions across engines.** Some engines flip V (top-left vs bottom-left
131 origin); a texture may appear upside-down. Know your engine's convention.
132- **Heavy branching / dynamic loops** stall GPUs. Prefer `step`/`smoothstep`/
133 `mix`; reserve `if`/`discard` for genuinely cheap early-outs.
134- **`discard` defeats early-Z** and can hurt performance on tiled mobile GPUs;
135 prefer alpha blending where you can.
136- **Precision on mobile**: `highp` vs `mediump` matters; large UVs or time values
137 in low precision shimmer. Use adequate precision for coordinates and time.
138- **Assuming GLSL == HLSL.** `mix`↔`lerp`, `fract`↔`frac`, `texture()`↔`.Sample()`,
139 `vec2`↔`float2`, column- vs row-major matrices. See the reference mapping.
140
141## References
142
143- `references/effects.md` — full outline (2D sprite + 3D), vignette, and color
144 grading shaders; the GLSL↔HLSL function/type mapping table; per-engine notes
145 (Godot `canvas_item`/`spatial`, Unity ShaderLab/HLSL, Unreal material nodes).
146
147## Related skills
148
149- `godot-shaders` — Godot shading language syntax, built-ins, and screen-reading.
150- `unreal-niagara` — GPU particle VFX (a different shader use).
151- `procedural-gen` — the noise that drives dissolve and procedural texturing.