Blending & transparency
How a fragment's color combines with what is already on screen, and the alpha, additive and premultiplied modes built from it.
How it works
Without blending, a fragment that passes its tests replaces the pixel’s color. Glass, smoke, fire, a sprite’s soft
edge and a UI fade all need something else: a mix of the new color with the one already there. Blending is that mix.
It runs after the fragment shaderThe GPU program that runs once for every pixel a triangle covers and decides its color. Fragment shader The GPU program that runs once for every pixel a triangle covers and decides its color. Per-sample operations The fixed steps after the fragment shader that decide whether its color lands and how. Scissor, stencil and depth tests, then blending and the write masks, set from the host rather than in GLSL.
The blend equation
Blending works on two colors: the source, what the fragment shader just output, and the destination, what the pixel holds. Each is multiplied by a factor, and the two products are combined:
result = source × srcFactor + destination × dstFactor
The host picks the factors with gl.blendFunc(srcFactor, dstFactor) and the operation with gl.blendEquation():
add (the default), subtract, reverse subtract, and in WebGL2 also min and max. gl.blendFuncSeparate() sets
different factors for the color and the alpha channels.
Three modes cover most effects
- Alpha (“over”):
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA). The result issource × α + destination × (1 − α): alpha 1 replaces, 0 leaves the pixel alone, 0.5 is halfway. Glass, sprites, fading UI. - Additive:
gl.blendFunc(gl.ONE, gl.ONE), orgl.SRC_ALPHA, gl.ONEto scale it by alpha. The source is added to what is there, so it can only brighten. Fire, sparks, glows, lasers: things that give off light. Because addition doesn’t care about order, additive layers never need sorting; on an 8-bit target, though, they flatten to white where they pile up. - Multiply:
gl.blendFunc(gl.DST_COLOR, gl.ZERO). The result issource × destination, so it can only darken. Shadows painted as decals, tinted glass.
Premultiplied alpha
In “straight” alpha, the color and its opacity are stored apart: a half-transparent red is (1, 0, 0, 0.5). In
premultiplied alpha the color is already multiplied by its alpha: the same red is (0.5, 0, 0, 0.5), and it blends
with gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA).
vec3 color = vec3(1.0, 0.45, 0.1);
float alpha = 0.6;
fragColor = vec4(color * alpha, alpha); // premultiplied: pairs with blendFunc(ONE, ONE_MINUS_SRC_ALPHA)
It looks like a detail, but it solves real problems:
- Filtering is correct. When a texture is filtered or mipmapped, a fully transparent texel still has a color (often black or white) that bleeds into its neighbors in straight alpha, leaving dark or light fringes around sprites. Premultiplied, a transparent texel is (0, 0, 0, 0) and adds nothing.
- One mode does both. An output with color but alpha 0 adds light without covering anything, so the same blend state draws smoke (covering) and embers (adding) in one draw call.
- Layers combine. Two premultiplied layers can be merged into one first and blended later with the same result, which is what compositing passes rely on.
The browser composites a WebGL canvas into the page as premultiplied by default (the premultipliedAlpha context
attribute). A shader that writes straight alpha into a transparent canvas gets bright, wrong edges where the page
shows through.
Order matters
Opaque surfaces can be drawn in any order because the depth bufferAn image the size of the framebuffer that holds, for each pixel, how far away the nearest surface drawn so far is. The depth test uses it to hide what is behind. Depth buffer An image the size of the framebuffer that holds, for each pixel, how far away the nearest surface drawn so far is. The depth test uses it to hide what is behind.
- draw every opaque object, depth tested and written;
- sort the transparent objects from back to front;
- draw them with blending on, depth tested but not written (
gl.depthMask(false)).
Sorting is per object, so objects that cross each other, or a single mesh that overlaps itself, can still come out wrong. When the edge can be hard, an alpha cut-out avoids the problem entirely: the shader discards fragments below a threshold and the surface stays opaque, depth-written and order-free, at the price of jagged edges (or MSAA alpha-to-coverage to soften them).
In Godot and on this site
In Godot, a spatial shader picks its blend with render_mode blend_mix (the default), blend_add, blend_sub,
blend_mul or blend_premul_alpha. Blending only happens for transparent materials: writing ALPHA moves the
material to the transparent pipeline, which Godot sorts back to front and, by default, draws without writing depth.
Writing ALPHA_SCISSOR_THRESHOLD makes a cut-out instead. A canvas_item (2D) shader always blends, with mix by
default.
On this site, a pass’s blend in render.json is "opaque", "alpha" (source × α + destination × (1 − α)) or
"additive" (destination + source × α); both blended modes are depth tested but not written. Sprites always use
"alpha"; glowing cards like the Hologram use "additive".