references/effects.md
# Shader effects and GLSL↔HLSL mapping
Full versions of the effects sketched in `SKILL.md`, plus the dialect mapping you
need to port a shader between engines. Examples are GLSL-style; the mapping table
shows the HLSL form.
## GLSL ↔ HLSL quick reference
| GLSL | HLSL | Note |
|---|---|---|
| `vec2/3/4`, `mat3/4` | `float2/3/4`, `float3x3/4x4` | vectors and matrices |
| `mix(a, b, t)` | `lerp(a, b, t)` | linear interpolation |
| `fract(x)` | `frac(x)` | fractional part |
| `mod(a, b)` | `a - b * floor(a / b)` | HLSL `fmod` truncates, so it differs for negatives |
| `texture(samp, uv)` | `tex.Sample(samp, uv)` | sampler is a separate object in HLSL |
| `inversesqrt(x)` | `rsqrt(x)` | |
| `atan(y, x)` | `atan2(y, x)` | two-argument arctangent |
| `M * v` | `mul(M, v)` | same maths; `v * M` ↔ `mul(v, M)` |
| `gl_Position` / `out` color | `SV_Position` / `SV_Target` | stage outputs via semantics |
| `dFdx/dFdy` | `ddx/ddy` | screen-space derivatives |
Matrix order is about how the engine builds and uploads its matrices, not the
language: Unity's SRP shader library writes
`mul(GetObjectToWorldMatrix(), float4(positionOS, 1.0))`, the same order as
GLSL's `M * v`. When porting, keep the multiply order the source engine used and
check its constant-buffer packing (`row_major` / `column_major`) rather than
swapping the operands by reflex.
Also watch: clip-space depth range (OpenGL `-1..1`; Direct3D, Vulkan, Metal and
WebGPU `0..1`) and Y orientation (Vulkan's clip-space Y points down; texture and
framebuffer origins differ between APIs) — engines usually normalize this for
you, but it surfaces when porting raw shaders.
## 2D sprite outline (sample the alpha neighborhood)
```glsl
// Draw an outline where a transparent pixel is adjacent to an opaque one.
uniform sampler2D tex;
uniform vec2 texel_size; // (1/width, 1/height) of the texture
uniform vec4 outline_color;
in vec2 uv;
out vec4 frag;
void main() {
vec4 c = texture(tex, uv);
if (c.a > 0.5) { frag = c; return; } // inside the sprite: unchanged
// sample 4 neighbors; if any is opaque, this empty pixel is on the border
float a = max(max(texture(tex, uv + vec2( texel_size.x, 0)).a,
texture(tex, uv + vec2(-texel_size.x, 0)).a),
max(texture(tex, uv + vec2(0, texel_size.y)).a,
texture(tex, uv + vec2(0, -texel_size.y)).a));
frag = (a > 0.5) ? outline_color : vec4(0.0); // border pixel -> outline
}
```
For thicker or smoother outlines, sample 8 neighbors (include diagonals) or do a
small distance-field pass. Two traps: the outline can only be drawn inside the
sprite's quad, so the texture needs a transparent margin at least as wide as the
outline (or the engine must pad the quad); and on an atlas, neighbour samples at
the frame edge read the next sprite, so clamp the UV to the frame. In Godot,
`TEXTURE_PIXEL_SIZE` gives `texel_size` directly; in a PixiJS filter, clamp with
the provided `uInputClamp`. In 3D, outlines are usually done differently: render
back-faces scaled outward along normals, or detect edges from depth/normal
discontinuities in a post-process.
## Vignette (darken the screen edges) — post-process
```glsl
// Darken pixels by distance from screen center. screen_uv is 0..1 across the view.
uniform sampler2D screen_tex;
uniform float strength = 0.6; // 0 = none, 1 = strong
uniform float radius = 0.75; // full darkness at this distance
in vec2 screen_uv;
out vec4 frag;
void main() {
vec3 c = texture(screen_tex, screen_uv).rgb;
float d = distance(screen_uv, vec2(0.5)); // 0 center .. ~0.707 corner
float v = 1.0 - smoothstep(radius * 0.5, radius, d); // 1 in center, ->0 at edges
frag = vec4(c * mix(1.0 - strength, 1.0, v), 1.0);
}
// smoothstep needs edge0 < edge1; smoothstep(radius, radius * 0.5, d) is undefined.
```
## Color grading via a curve (brightness/contrast/saturation)
```glsl
// Order matters: contrast around 0.5, then saturation, then brightness.
uniform sampler2D screen_tex;
uniform float brightness = 0.0; // additive
uniform float contrast = 1.0; // multiplicative around mid-gray
uniform float saturation = 1.0;
in vec2 screen_uv;
out vec4 frag;
void main() {
vec3 c = texture(screen_tex, screen_uv).rgb;
c = (c - 0.5) * contrast + 0.5; // contrast
float luma = dot(c, vec3(0.2126, 0.7152, 0.0722)); // Rec.709 luminance
c = mix(vec3(luma), c, saturation); // saturation
c += brightness; // brightness
frag = vec4(clamp(c, 0.0, 1.0), 1.0);
}
```
The luminance weights `(0.2126, 0.7152, 0.0722)` are the Rec.709 coefficients —
green dominates perceived brightness. For LUT-based grading, sample a color
lookup texture indexed by the pixel's RGB instead.
## Per-engine notes
- **Godot (4.x) — `gdshader`.** Godot's shading language is GLSL-like with a
`shader_type` (`canvas_item` for 2D, `spatial` for 3D). It provides built-ins:
`UV`, `COLOR`, `TIME`, `TEXTURE`, `TEXTURE_PIXEL_SIZE` (2D), `NORMAL`, `VIEW`
(3D). The screen is not a built-in since 4.0 (`SCREEN_TEXTURE` was removed):
declare `uniform sampler2D screen_tex : hint_screen_texture, filter_linear_mipmap;`
and sample it with `SCREEN_UV`. Write `fragment()` / `vertex()` functions. Map
the GLSL examples here onto those built-ins. See `godot-shaders`.
- **Unity 6.3 — ShaderLab + HLSL.** Shaders live in `.shader` files (ShaderLab
blocks) or Shader Graph. Code is HLSL: `float4`, `lerp`, `SV_Target`. In URP,
include `Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl`,
declare `TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap);`, sample with
`SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv)` and put every material
property in one `CBUFFER_START(UnityPerMaterial)` block so the SRP Batcher can
batch it. `tex2D`/`sampler2D` belong to Built-in Render Pipeline shaders.
- **Unreal 5.8 — Material Editor (node graph) + HLSL.** Most authoring is visual
nodes; Custom nodes embed HLSL. Concepts (UVs, fresnel via a Fresnel node,
panner for UV scroll) map directly to the patterns here.
- **three.js r186.** `WebGLRenderer` runs GLSL through `ShaderMaterial`
(three.js prepends its uniforms and attributes such as `projectionMatrix`,
`position`, `uv`; compiled as GLSL ES 3.00 on WebGL 2) or `RawShaderMaterial`
(you declare everything). `WebGPURenderer` (`three/webgpu`) only runs node
materials: write the effect in TSL (`three/tsl`), which it compiles to WGSL on
WebGPU and to GLSL on its WebGL 2 fallback.
- **PixiJS 8 filters.** `Filter.from({ gl: { fragment }, resources })` takes a
GLSL ES 3.00 fragment shader (`in vec2 vTextureCoord; uniform sampler2D
uTexture; out vec4 finalColor;`) for the WebGL renderer; the WebGPU renderer
needs a WGSL program passed as `gpu`. `vTextureCoord` is **not** 0..1 over
the sprite: it covers the filter's input texture, so for sprite-relative
effects (vignette, dissolve by UV) compute
`vTextureCoord * uInputSize.xy / uOutputFrame.zw` (declare both as
`uniform vec4`; the filter system fills them).
- **WGSL / WebGPU in browsers (gpuweb status, Oct 2026).** Default-on in Chrome
and Edge 113+ on Windows, macOS and ChromeOS, Chrome 121+ on Android 12+
(Qualcomm, ARM, Intel GPUs), Safari 26 on macOS, iOS, iPadOS and visionOS,
and Firefox 141+ on Windows and 147+ on Apple Silicon Macs. Chrome on Linux
enables it only for some GPUs; Firefox on Linux and Android does not yet.
Keep a WebGL 2 path for a web game.
## Performance checklist
- Minimize texture samples; they are the dominant cost. Reuse a sample instead of
re-reading.
- Prefer math (`smoothstep`, `mix`) to branches and `discard`.
- Compute heavy values per vertex (then interpolate) when per-pixel precision
isn't required.
- Use appropriate precision on mobile (`mediump` for color, higher for UV/time).
- Avoid dependent texture reads (sampling using a value read from another texture)
on low-end GPUs where possible.
SKILL.md
---
name: shader-programming
description: >
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.
---
# 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
1. **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.
2. **Track coordinate spaces.** Positions move model → world → view → clip space;
normals belong in world or view space. Mixing spaces is the most common bug.
3. **Drive effects with UVs and time.** UVs are `0..1` texture coordinates;
offset, scale, or distort them, and animate with a `time` uniform.
4. **Work per pixel, branch-light.** Prefer `mix`, `step`, `smoothstep`, and
`clamp` over `if` where possible; GPUs run pixels in lockstep, so a branch
whose condition differs between neighbouring pixels runs both sides. A branch
on a uniform (same for the whole draw) is cheap.
5. **Pass data via uniforms** (constant per draw) and **varyings** (interpolated
vertex→fragment). Keep texture samples few; they dominate cost.
6. **Validate source before visual testing when the tools are already available.** For
GLSL, run `glslangValidator path/to/shader.frag` with the matching stage suffix;
for WGSL, run `naga path/to/shader.wgsl`. Check Godot `.gdshader` files by
opening the project in Godot, which has no standalone validator for them.
7. **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 fragment snippets for desktop `#version 330` (each passes
`glslangValidator` with that header; close to Godot's `canvas_item`/`spatial`
shaders). For GLSL ES 3.00 — WebGL2, three.js `ShaderMaterial`, PixiJS filters —
add `precision highp float;` and set uniform defaults from code: ES rejects
`uniform float edge = 0.05;` with "cannot initialize this type of qualifier"
(Godot's shading language accepts it). See `references/effects.md` for the HLSL
equivalents and the full outline/fresnel/vignette shaders.
### 1. Fragment basics: sample, tint, and combine
```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
}
```
### 2. Scrolling UVs (animated texture) — frame-rate independent
```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.
```
### 3. Dissolve (threshold a noise map, glow the edge)
```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;
float cut = amount * (1.0 + edge) - edge; // edge band starts below 0 -> none at amount 0
if (n <= cut) discard; // cut away dissolved pixels (all of them at 1)
float e = smoothstep(cut, cut + edge, n); // 0 at the edge -> 1 inside; keep edge > 0
frag = mix(edge_color, c, e); // HLSL: lerp(edge_color, c, e)
}
// WRONG: thresholding at `amount` directly — at amount 0 every pixel with
// n < edge is still tinted, so the "fully visible" sprite shows glowing specks.
```
### 4. Fresnel rim light (3D) — brighten glancing angles
```glsl
// Rim = 1 where the surface is edge-on to 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.
- **Reversed `smoothstep` edges.** The GLSL spec leaves `smoothstep(e0, e1, x)`
undefined when `e0 >= e1`, so `smoothstep(1.0, 0.5, d)` may work on one GPU
and not another. Write `1.0 - smoothstep(0.5, 1.0, d)`.
- **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`, `M * v`↔`mul(M, v)`, `mod`↔`fmod` (different for negatives).
See the reference mapping.
- **A GLSL `ShaderMaterial` that renders nothing in three.js** under
`WebGPURenderer`: that renderer only runs node materials (TSL), on its WebGPU
and WebGL 2 backends alike. Keep `WebGLRenderer` for raw GLSL or port to TSL.
## 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.