Skip to content
Rendering · Pipeline stagesIntermediate

Per-sample operations

The fixed tests and merges between the fragment shader's output and the pixel that actually changes.

Live demo coming soon

How it works

When the fragment shader returns a color, nothing on screen has changed yet. The fragment still goes through a row of fixed-function steps that decide whether it lands at all and how it combines with what the pixel already holds. The OpenGL ES specification calls them per-fragment operations; they work on each sample of the pixel, which matters once multisampling is on.

None of them is written in GLSL. The host code switches and configures them (gl.enable, gl.depthFunc, gl.blendFunc and friends) before the draw call, and they apply to everything that draw call produces.

The order

The OpenGL ES 3.0 specification lists them in this order (section 4.1):

  1. Pixel ownership: the pixel belongs to this context (a covered window, for example, may not).
  2. Scissor test: drop fragments outside a rectangle, set with gl.scissor(). Cheap clipping for UI panels and split screens.
  3. Multisample operations: with MSAA, decide which samples of the pixel the fragment covers. With gl.SAMPLE_ALPHA_TO_COVERAGE enabled, the output alpha also removes samples, turning alpha into soft-edged coverage.
  4. Stencil test: compare the pixel’s value in the stencil buffer, a small integer per sample, with a reference, and update it. It is a mask the scene draws for itself: portals, mirrors, outlines drawn only outside an object.
  5. Depth test: compare the fragment’s depth with the stored one and keep only the nearer (see the depth buffer).
  6. Blending: combine the fragment’s color with the color already stored, instead of replacing it (see blending).
  7. sRGB conversion and dithering: encode the color for an sRGB target if there is one, and optionally dither it down to the target’s bit depth.

Then the result is written, through the write masks: gl.colorMask() can block single channels, and gl.depthMask(false) keeps the depth test running while leaving the stored depth untouched. A fragment that fails any test, or that the shader discarded, writes nothing.

Setting them up

A typical frame turns these on and off between draw calls. Opaque geometry first, depth tested and written; then transparent geometry, depth tested but not written, blended over it:

// opaque pass
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LESS);
gl.depthMask(true);
gl.disable(gl.BLEND);
drawOpaque();

// transparent pass: hidden by walls in front, but doesn't hide what's behind it
gl.depthMask(false);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
drawTransparent();

Per sample, and early

With MSAA, each pixel holds several samples, and the fragment shader still runs once per pixel for each triangle. The tests and the write happen per sample, using the coverage the rasterizer found, so a triangle edge cuts a pixel partly; at the end the samples are averaged (resolved) into one color. That is how MSAA smooths edges without shading every sample.

The specification describes the depth and stencil tests as running after the shader, but running a shader whose result will be thrown away wastes time, so GPUs test early whenever the result can’t differ. Two things in the shader prevent that: writing gl_FragDepth, because the depth isn’t known until the shader finishes, and discard on many GPUs, because the fragment may vanish after the test. Keep both out of shaders that cover a lot of the screen unless you need them.

In Godot and on this site

Godot sets these from the material and the shader’s render_mode instead of host code: blend_mix, blend_add, blend_sub, blend_mul and blend_premul_alpha for blending; depth_draw_opaque, depth_draw_always and depth_draw_never for the depth write; depth_test_disabled to skip the test; alpha_to_coverage for MSAA coverage. Writing ALPHA moves the material to the transparent pipeline, drawn after the opaque one.

On this site, a pass’s blend and alphaClip in render.json pick a combination: "opaque" is tested and written, "alpha" and "additive" are tested and blended but leave the depth alone, and alphaClip discards the fragments under a threshold.

Further reading