Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Write game shaders from cross-engine fundamentals — the vertex→fragment pipeline, coordinate spaces, UV math, and common 2D/3D effects (tint, UV scroll, dissolve, outline, fresnel rim, vignette) in GLSL with HLSL equivalents. Use when the user mentions shaders, fragment/pixel shader, vertex shader, UV, GLSL, HLSL, or effects like dissolve, outline, or rim light.
.claude/skills/gamedev-skills-shader-programming/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-13 | ✓→✗ | ▼ Worse | 41% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 49% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 38% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 60% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 113% | 0% |
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.
coordinate spaces, and the GPU pipeline.
outlines, fresnel/rim light, vignette, color grading.
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.
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.
normals belong in world or view space. Mixing spaces is the most common bug.
0..1 texture coordinates;offset, scale, or distort them, and animate with a time uniform.
mix, step, smoothstep, andclamp over if where possible; GPUs run pixels in lockstep and dislike divergent branches.
vertex→fragment). Keep texture samples few; they dominate cost.
can break on mobile (precision, missing features). Test where it ships.
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.
glsl// 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 }
glsl// 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.
glsl// 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) }
glsl// 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.
view-space light) yields subtly wrong shading. Pick one space and convert everything into it.
shortens vectors, so dot() results drift. normalize() in the fragment stage.
origin); a texture may appear upside-down. Know your engine's convention.
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.
highp vs mediump matters; large UVs or time valuesin low precision shimmer. Use adequate precision for coordinates and time.
mix↔lerp, fract↔frac, texture()↔.Sample(),vec2↔float2, column- vs row-major matrices. See the reference mapping.
references/effects.md — full outline (2D sprite + 3D), vignette, and colorgrading shaders; the GLSL↔HLSL function/type mapping table; per-engine notes (Godot canvas_item/spatial, Unity ShaderLab/HLSL, Unreal material nodes).
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.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 8,640 | 6,151 | -29% | 1 | 1 | 0% | 1,701 | 2,903 | +71% | 0 | 0 | — |
case-02 | pass→pass | 16,500 | 14,228 | -14% | 1 | 1 | 0% | 2,887 | 4,308 | +49% | 0 | 0 | — |
case-03 | pass→pass | 15,705 | 9,229 | -41% | 1 | 1 | 0% | 2,369 | 3,276 | +38% | 0 | 0 | — |
case-04 | pass→pass | 12,928 | 11,785 | -9% | 1 | 1 | 0% | 2,382 | 3,822 | +60% | 0 | 0 | — |
case-05 | pass→pass | 6,972 | 3,817 | -45% | 1 | 1 | 0% | 1,154 | 2,454 | +113% | 0 | 0 | — |
case-06 | pass→pass | 9,222 | 5,897 | -36% | 1 | 1 | 0% | 1,796 | 2,900 | +61% | 0 | 0 | — |
case-07 | pass→pass | 6,630 | 5,371 | -19% | 1 | 1 | 0% | 1,187 | 2,727 | +130% | 0 | 0 | — |
case-08 | pass→pass | 9,114 | 5,552 | -39% | 1 | 1 | 0% | 1,582 | 2,647 | +67% | 0 | 0 | — |
case-09 | pass→pass | 11,859 | 6,008 | -49% | 1 | 1 | 0% | 2,186 | 2,896 | +32% | 0 | 0 | — |
case-10 | pass→pass | 11,421 | 10,431 | -9% | 1 | 1 | 0% | 2,232 | 3,555 | +59% | 0 | 0 | — |
case-11 | pass→pass | 15,164 | 15,309 | +1% | 1 | 1 | 0% | 2,456 | 4,258 | +73% | 0 | 0 | — |
case-12 | pass→pass | 8,138 | 6,775 | -17% | 1 | 1 | 0% | 1,366 | 2,881 | +111% | 0 | 0 | — |
case-13 | pass→fail | 16,414 | 13,149 | -20% | 1 | 1 | 0% | 3,074 | 4,326 | +41% | 0 | 0 | — |
case-14 | pass→pass | 2,836 | 2,606 | -8% | 1 | 1 | 0% | 517 | 2,198 | +325% | 0 | 0 | — |
case-15 | pass→pass | 3,266 | 2,070 | -37% | 1 | 1 | 0% | 535 | 2,115 | +295% | 0 | 0 | — |
case-16 | pass→pass | 8,399 | 4,445 | -47% | 1 | 1 | 0% | 1,378 | 2,589 | +88% | 0 | 0 | — |
case-17 | pass→pass | 7,841 | 4,482 | -43% | 1 | 1 | 0% | 1,354 | 2,517 | +86% | 0 | 0 | — |
case-18 | pass→pass | 10,899 | 9,006 | -17% | 1 | 1 | 0% | 1,573 | 3,274 | +108% | 0 | 0 | — |
case-19 | pass→pass | 3,064 | 2,284 | -25% | 1 | 1 | 0% | 468 | 2,105 | +350% | 0 | 0 | — |
case-20 | pass→pass | 7,214 | 4,117 | -43% | 1 | 1 | 0% | 1,348 | 2,368 | +76% | 0 | 0 | — |
case-21 | pass→pass | 12,229 | 9,232 | -25% | 1 | 1 | 0% | 2,394 | 3,525 | +47% | 0 | 0 | — |
case-22 | pass→pass | 10,618 | 8,368 | -21% | 1 | 1 | 0% | 2,032 | 3,367 | +66% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of -100 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Other measured skills in the registry, with their headline benchmark lift.