Dual Depth Peeling in GLSL Explained
Dual depth peeling is an order-independent transparency technique that cuts the required rendering passes in half compared to standard depth peeling by simultaneously extracting both the front-most and back-most geometry layers in a single pass. By maintaining two depth bounds and utilizing Multiple Render Targets (MRT) alongside specialized blending equations, a GLSL fragment shader can discard fragments that have already been processed, isolate the next closest and furthest surfaces, and accumulate their colors into separate buffers until the entire volume is resolved.
The Limitation of Standard Depth Peeling
Standard depth peeling renders geometry iteratively from front to back (or back to front). Each pass uses the depth buffer of the previous pass to peel away exactly one layer of fragments. If a scene has a maximum depth complexity of \(N\) layers, it requires \(N\) geometry passes. Dual depth peeling solves this inefficiency by peeling inward from both directions simultaneously, requiring only \(\lceil N / 2 \rceil\) passes.
Buffer Setup and Render Targets
Dual depth peeling relies on OpenGL’s Multiple Render Targets (MRT) and the ability to output to different color attachments with distinct blend operations. A typical implementation uses:
- Depth/Min-Max Texture: A two-channel 32-bit
floating-point texture (
GL_RG32F). The red channel stores the negative nearest depth (to useGL_MAXblending as a proxy for minimum depth, or standard min-max blending where supported), and the green channel stores the maximum depth. - Front Color Accumulator: Accumulates colors peeled from front to back using an "under" blending operator (\(C_{dst} = C_{src} \cdot (1 - \alpha_{dst}) + C_{dst}\)).
- Back Color Accumulator: Accumulates colors peeled from back to front using standard "over" blending (\(C_{dst} = C_{src} + C_{dst} \cdot (1 - \alpha_{src})\)).
The Initialization Pass
Before peeling begins, an initialization pass determines the initial scene boundaries:
- Depth testing is disabled, and blending is set to write the minimum and maximum depths of the entire scene into the depth target.
- The fragment shader outputs the fragment’s depth \(z\) to both channels (often storing \(-z\) in the first channel and \(z\) in the second channel if relying on
standard
GL_MAXblending). - The resulting texture contains the global nearest depth (\(z_{min}\)) and the global farthest depth (\(z_{max}\)).
The Peeling Pass in GLSL
During each peeling iteration, the geometry is rendered with depth writes disabled. The fragment shader samples the previous pass’s depth texture to retrieve the active front boundary (\(z_{front}\)) and back boundary (\(z_{back}\)).
The GLSL fragment logic operates as follows:
#version 330 core
uniform sampler2D u_PrevDepthTex;
in vec4 v_Color;
layout (location = 0) out vec2 o_Depth;
layout (location = 1) out vec4 o_FrontColor;
layout (location = 2) out vec4 o_BackColor;
void main()
{
float z = gl_FragCoord.z;
vec2 bounds = texelFetch(u_PrevDepthTex, ivec2(gl_FragCoord.xy), 0).rg;
float minDepth = -bounds.r;
float maxDepth = bounds.g;
// Discard fragments outside the current unpeeled layer range
if (z <= minDepth || z >= maxDepth) {
discard;
}
// Initialize depth outputs to default extremes
o_Depth = vec2(-1.0, 0.0);
o_FrontColor = vec4(0.0);
o_BackColor = vec4(0.0);
// Identify if the fragment belongs to the new front or back layer
if (z > minDepth && z < maxDepth) {
// Output depths for the next layer's min-max search
o_Depth = vec2(-z, z);
}
if (z == minDepth) {
o_FrontColor = vec4(v_Color.rgb * v_Color.a, v_Color.a);
} else if (z == maxDepth) {
o_BackColor = v_Color;
}
}Depth Isolation and Color Blending
Because GLSL cannot write simultaneously to the color buffers and resolve the next minimum/maximum depths in a single step without race conditions, the peeling step is typically split into two sub-passes per layer:
- Geometry Extraction Pass: Reads the previous depth
limits, discards fragments outside the interval \((z_{front}, z_{back})\), and uses
glBlendEquationSeparateset toGL_MAXon the depth target to capture the closest new depth and farthest new depth for remaining fragments. - Color Accumulation Pass: Renders the geometry again using the newly found depth bounds. Fragments matching the new \(z_{front}\) are written to the front accumulator using under-blending, while fragments matching the new \(z_{back}\) are blended into the back accumulator using over-blending.
Final Composition
Once the loop completes—either after reaching a set number of iterations or when an occlusion query indicates zero fragments were processed—a full-screen compositing shader blends the front accumulation buffer over the back accumulation buffer, producing the correctly ordered transparent image.