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
@locationoutput count or format not matchingpipeline.fragment.targets. WHY it fails: WebGPU validates the shader IO against the pipeline state at pipeline creation. A missing target, an extra@location, or avec3<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
discardfrom a@vertexor@computeentry point. WHY it fails:discardis 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
@locationoutput set differ from the pipelinefragment.targetsin 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
discardoutside a@fragmententry point. - NEVER assume
@builtin(front_facing)depends on winding alone; it depends on the pipelineprimitive.frontFace. - NEVER read
@builtin(position)in the fragment stage as clip space; it is the framebuffer-space pixel coordinate. - NEVER call
textureSample,dpdx,dpdy, orfwidthin non-uniform control flow; they are fragment-stage builtins that need uniform control flow. Seewebgpu-wgsl-uniformity.
Reference Files
references/methods.md: the@fragmententry point form, fragment builtins, MRT outputs, sample-rate shading, and thediscardstatement.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 fortextureSampleand derivatives.webgpu-syntax-render-pipeline:fragment.targets,primitive.frontFace,multisamplestate.webgpu-impl-render-targets: color attachments, MRT, depth attachments.