Binary Search Refinement in GLSL Screen Space Reflections
Screen-space reflections (SSR) calculate real-time reflections by marching a ray through the camera's depth buffer. Because large step sizes are required during linear ray marching for performance, finding the exact intersection point of the reflected ray and rendered geometry is difficult. This article explains how binary search refinement functions in GLSL, transforming a coarse intersection range into a pixel-perfect surface hit by iteratively halving ray step intervals against depth buffer samples.
The Need for Refinement
In a standard SSR shader, linear ray marching steps along a reflected view-space or screen-space vector. To maintain real-time frame rates, each step must cover multiple pixels.
This creates a dilemma:
- Large step sizes miss fine details, skip thin geometry, and cause severe stair-stepping or jitter artifacts along reflection edges.
- Small step sizes preserve detail but drastically increase texture sample operations, degrading performance.
Linear search is used only to bracket the hitâidentifying the exact segment between the last sample in front of geometry and the first sample that penetrated behind it. Once this span is found, a binary search refines the point of contact with minimal texture lookups.
The Binary Search Algorithm in Screen Space
Binary search refinement operates on the bounded 1D segment along the ray between the last unoccluded position (\(P_{\text{prev}}\)) and the first occluded position (\(P_{\text{hit}}\)).
- Calculate the Midpoint: The algorithm divides the current search vector in half and steps to the midpoint between the front and back bounds.
- Project to UV and Sample Depth: The midpoint's \(X\) and \(Y\) coordinates determine the texture UV coordinates used to sample the scene's depth buffer.
- Compare Depths: The midpoint's calculated \(Z\) depth is compared against the sampled
surface depth:
- Ray is behind geometry (\(Z_{\text{ray}} > Z_{\text{buffer}}\)): The ray penetrated too deeply. The algorithm retreats along the ray direction by half the current step size.
- Ray is in front of geometry (\(Z_{\text{ray}} < Z_{\text{buffer}}\)): The ray has not yet intersected the surface. The algorithm advances along the ray direction by half the current step size.
- Halve and Repeat: The step size is halved with every iteration (\(0.5^N\)).
After 4 to 8 iterations, the step size decreases to a fraction of a pixel, providing a precise intersection coordinate without significant computational overhead.
GLSL Implementation
Below is a standard GLSL implementation of binary search refinement executed after a linear ray march registers a hit:
vec3 BinarySearchRefinement(vec3 rayDir, vec3 hitCoord, sampler2D depthTexture, int iterations)
{
// rayDir should be the vector of the single linear step that crossed the surface
vec3 stepVector = rayDir;
vec3 currentPos = hitCoord;
for (int i = 0; i < iterations; i++)
{
// Halve the step size
stepVector *= 0.5;
// Sample scene depth at current screen-space UV
float sampledDepth = texture(depthTexture, currentPos.xy).r;
// Compare ray depth with depth buffer
if (currentPos.z > sampledDepth)
{
// Ray is inside/behind the object: step backward
currentPos -= stepVector;
}
else
{
// Ray is in front of the object: step forward
currentPos += stepVector;
}
}
return currentPos;
}Depth Thickness and Edge-Case Rejection
A simple depth comparison (\(Z_{\text{ray}} > Z_{\text{buffer}}\)) assumes all geometry extends infinitely backward. Without validation, binary refinement can lock onto false surfaces, such as the back side of thin objects or separate geometry situated far behind the occluder.
To prevent these false hits:
- Thickness Threshold: A maximum allowable penetration depth is enforced. If \(|Z_{\text{ray}} - Z_{\text{buffer}}| > \text{thicknessThreshold}\), the hit is rejected, and linear marching either continues or terminates.
- Normal Validation: Comparing the surface normal stored in a G-buffer against the reflection vector ensures the hit surface faces toward the ray origin, discarding back-facing intersections.
Efficiency and Precision
The binary search converts an exponential problem into a logarithmic one. While increasing linear marching precision by a factor of 32 requires 32 additional texture lookups, binary refinement achieves the same precision increase in just 5 iterations (\(\log_2(32) = 5\)). This efficiency makes binary search refinement the standard method for sharp, jitter-free reflections in real-time GLSL rendering pipelines.