| name | webgpu-wgsl-fragment-shaders |
| description | Use when writing a WGSL fragment shader: color outputs, multiple render targets, depth output, sample-rate shading, or discard. Prevents target / output mismatches and the lost-early-Z bug from writing frag_depth. Covers the @fragment entry point, @builtin position front_facing frag_depth sample_index sample_mask, MRT @location outputs, sample-rate shading, and discard. Keywords: WGSL fragment shader, @fragment, @builtin(frag_depth), @builtin(front_facing), MRT, multiple render targets, discard, sample_index, sample mask, how do I write a fragment shader, pixel shader.
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires WebGPU 1.0-stable. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
WGSL Fragment Shaders
Write a WGSL @fragment entry point that produces color, depth, and coverage
outputs that match the render pipeline, without target mismatches or losing early-Z.
Quick Reference
WGSL is the shading language of WebGPU 1.0-stable (Chrome 113+, Safari 26+,
Firefox 141+). A fragment shader runs once per rasterized fragment. It is marked
@fragment and may not be called from other WGSL code.
Fragment IO
| Slot | Direction | @builtin / @location | Type | Notes |
|---|
| Pixel coordinate | input | @builtin(position) | vec4<f32> | Framebuffer-space pixel coord, not clip space |
| Facing | input | @builtin(front_facing) | bool | Depends on pipeline primitive.frontFace |
| Sample index | input | @builtin(sample_index) | u32 | Reading it triggers sample-rate shading |
| Sample mask | input | @builtin(sample_mask) | u32 | Coverage mask delivered to the shader |
| Varyings | input | @location(n) | scalar/vec | Interpolated from the vertex stage |
| Color target | output | @location(n) | scalar/vec | One per pipeline fragment.targets entry |
| Depth | output | @builtin(frag_depth) | f32 | Overrides rasterizer depth, disables early-Z |
| Sample mask | output | @builtin(sample_mask) | u32 | ANDs with coverage to drop samples |
@builtin(sample_mask) is the one builtin that is both input and output.
The interpolated varying struct is described in webgpu-wgsl-vertex-shaders.
Entry point form
@fragment fn fs(in : VSOut) -> @location(0) vec4<f32> { ... }
The fragment input struct is the SAME struct the vertex stage returns. Each
@location color output type and component count must match the format of the
matching GPUColorTargetState in pipeline.fragment.targets.
Decision Tree
What does the fragment shader produce?
├── One color, no depth override
│ └── return @location(0) vec4<f32> (single target)
├── Multiple color attachments (G-buffer, deferred shading)
│ └── return a struct with @location(0), @location(1), ...
│ one @location per colorAttachment, formats must match
├── Needs to override depth (raymarch, impostor, soft particles)
│ └── add @builtin(frag_depth) : f32 to the output struct
│ NOTE: this disables early depth testing for the whole draw
├── Needs to drop the fragment entirely (alpha cutout, clipping)
│ └── call discard; (NEVER from @vertex or @compute)
└── Needs per-sample shading (MSAA edge AA on shaded result)
└── read @builtin(sample_index) OR declare @interpolate(perspective, sample)
Core Patterns
ALWAYS match @location output count and format to the pipeline targets
Each @location(n) color output maps to pipeline.fragment.targets[n]. The output
type and component count must agree with that target's format (a vec4<f32>
output for an rgba8unorm target, a vec4<u32> output for an rgba32uint
target). A count or format mismatch fails pipeline creation.
@fragment fn fs(in : VSOut) -> @location(0) vec4<f32> {
return vec4<f32>(in.uv, 0.0, 1.0); // target 0 must be a 4-component color format
}
ALWAYS return a struct with one @location per attachment for MRT
Multiple render targets write several color attachments in one pass. The fragment
output struct holds one @location(n) member per attachment, in order.
struct GBuffer {
@location(0) albedo : vec4<f32>,
@location(1) normal : vec4<f32>,
}
@fragment fn fs(in : VSOut) -> GBuffer { ... }
NEVER write @builtin(frag_depth) when you still want early depth testing
Writing @builtin(frag_depth) overrides the rasterizer-computed depth. The GPU
cannot know the final depth before running the shader, so it disables the early
depth test for the whole draw. Omit frag_depth unless the shader genuinely
computes a non-default depth.
struct FSOut {
@location(0) color : vec4<f32>,
@builtin(frag_depth) depth : f32, // early-Z is now off for this draw
}
ALWAYS treat front_facing as defined by the pipeline frontFace setting
@builtin(front_facing) is true when the primitive faces the viewer per the
pipeline's primitive.frontFace ("ccw" or "cw"). It is NOT a fixed function
of triangle winding alone. Use it to flip normals for double-sided materials.
@fragment fn fs(in : VSOut, @builtin(front_facing) ff : bool) -> @location(0) vec4<f32> {
let n = select(-in.normal, in.normal, ff);
return vec4<f32>(n * 0.5 + 0.5, 1.0);
}
ALWAYS use discard to drop a fragment, knowing the invocation keeps running
discard demotes the invocation: no further memory writes and no color or depth
output occur. The invocation KEEPS running so neighboring derivatives stay valid.
discard is a fragment-stage statement only.
@fragment fn fs(in : VSOut) -> @location(0) vec4<f32> {
let texel = textureSample(tex, samp, in.uv);
if (texel.a < 0.5) { discard; } // derivatives still valid for non-discarded lanes
return texel;
}
ALWAYS trigger sample-rate shading by reading sample_index or @interpolate(..., sample)
Sample-rate shading runs the fragment shader once per MSAA sample instead of once
per pixel. It is triggered when the shader reads @builtin(sample_index) or
declares a varying with @interpolate(perspective, sample). Use it only when
per-sample shading is needed, since it multiplies fragment cost by the sample count.
Common Anti-Patterns
-
@location output count or format not matching pipeline.fragment.targets.
WHY it fails: WebGPU validates the shader IO against the pipeline state at
pipeline creation. A missing target, an extra @location, or a vec3<f32>
output for a 4-component format makes pipeline creation throw a validation
error.
-
Writing @builtin(frag_depth) and expecting early depth-test optimization.
WHY it fails: explicit depth output forces the depth test after the fragment
shader runs, so early-Z is disabled and overdrawn fragments still pay full
shading cost.
-
Calling discard from a @vertex or @compute entry point. WHY it fails:
discard is defined only for the fragment stage. There is no fragment to
demote elsewhere, so it is a shader-creation error.
Critical Warnings
- NEVER let the
@location output set differ from the pipeline fragment.targets
in count, order, or format.
- NEVER write
@builtin(frag_depth) unless the depth-stencil state writes depth
and you accept that early depth testing is disabled.
- NEVER call
discard outside a @fragment entry point.
- NEVER assume
@builtin(front_facing) depends on winding alone; it depends on
the pipeline primitive.frontFace.
- NEVER read
@builtin(position) in the fragment stage as clip space; it is the
framebuffer-space pixel coordinate.
- NEVER call
textureSample, dpdx, dpdy, or fwidth in non-uniform control
flow; they are fragment-stage builtins that need uniform control flow. See
webgpu-wgsl-uniformity.
Reference Files
references/methods.md : the @fragment entry point form, fragment builtins,
MRT outputs, sample-rate shading, and the discard statement.
references/examples.md : verified WGSL for a single-color fragment shader,
an MRT G-buffer shader, and a depth-output shader.
references/anti-patterns.md : fragment-shader mistakes with WHY-it-fails
explanations.
Related Skills
webgpu-wgsl-vertex-shaders : the vertex stage that produces the varying struct.
webgpu-wgsl-uniformity : uniform control flow for textureSample and
derivatives.
webgpu-syntax-render-pipeline : fragment.targets, primitive.frontFace,
multisample state.
webgpu-impl-render-targets : color attachments, MRT, depth attachments.