Raymarching on a Hi-Z buffer in GLSL

So I'm trying to implement Screen Space Reflections using a Hierarchical z-Buffer in GLSL. I'm following the approach from GPU Gems 5 and the Frostbite presentation linked here ("Stochastic Screen-Space Reflections", Slide 36+).

I believe I have a fundamental misunderstanding how the algorithm works, however there are not many information about this algorithm, besides of the resources I linked.

My current GLSL code looks like this:

vec3 trace_ray(vec3 ray_start, vec3 ray_dir)
{

ray_dir = normalize(ray_dir);
ivec2 work_size = SCREEN_SIZE_INT;
const int loop_max = 250;
int mipmap = 0;
int max_iter = loop_max;
vec3 pos = ray_start;

// Move pos by a small bias to avoid self-intersection
pos += ray_dir * 0.004;
float hit_bias = 0.002;

while (mipmap > -1 && max_iter --> 0)
{

// Check if we are out of screen bounds, if so, return
if (pos.x < 0.0 || pos.y < 0.0 || pos.x > 1.0 || pos.y > 1.0 || pos.z < 0.0 || pos.z > 1.0)
{
return vec3(0,0,0);
}

// Compute the fractional part of the coordinate (scaled by the working size)
// so the values will be between 0.0 and 1.0
vec2 fract_coord = mod(pos.xy * work_size, 1.0);

// Modify fract coord based on which direction we are stepping in.
// Fract coord now contains the percentage how far we moved already in
// the current cell in each direction.
fract_coord.x = ray_dir.x > 0.0 ? fract_coord.x : 1.0 - fract_coord.x;
fract_coord.y = ray_dir.y > 0.0 ? fract_coord.y : 1.0 - fract_coord.y;

// Compute maximum k for which the ray would still be inside of the cell.
float max_k_x = (1.0 / ray_dir.x) / work_size.x;
float max_k_y = (1.0 / ray_dir.y) / work_size.y;

// Scale the maximum k by the percentage we already processed in the current cell,
// since e.g. if we already moved 50%, we can only move another 50%.
max_k_x *= 1.0 - fract_coord.x;
max_k_y *= 1.0 - fract_coord.y;

// The maximum k is the minimum of the both sub-k's since if once maximum
// is reached, the ray is out of the cell
float max_k = min(abs(max_k_x), abs(max_k_y) );

// Fetch the current minimum cell plane height
float cell_z = textureLod(DownscaledDepth, pos.xy, mipmap).x;

// Check if the ray intersects with the cell plane. We have the following
// equation:
// pos.z + k * ray_dir.z = cell.z
// So k is:
float k = (cell_z - pos.z) / ray_dir.z;

// Check if we intersected the cell
if (k > 0.0 && k < max_k + hit_bias )
{
// Move to point of intersection
pos += k * ray_dir;

// In case we are at mipmap level 0, we found a match
if (mipmap == 0) {
return pos;
}

// If we hit anything at a higher mipmap, step up to a higher detailed
// mipmap:
mipmap -= 2;
work_size *= 4;
}

// If we hit nothing, move to the next cell, with a small bias
pos += max_k * ray_dir * 1.02;

mipmap += 1;
work_size /= 2;

}
return vec3(0);
}


And the result is:

So I'm doing something wrong, its probably not much since the result is not completely off, however, I cannot figure out what I'm doing wrong.

TL;DR: Am I either misunderstanding how the Algorithm works, or if not, what am I missing or doing wrong in my code?

Okay, I figured the error now, I thought I'd share the code I used so others can benefit from it:

vec3 trace_ray(vec3 ray_start, vec3 ray_dir)
{

if (ray_dir.z < 0.0) {
return vec3(0);
}

ray_dir = normalize(ray_dir);
ivec2 work_size = SCREEN_SIZE_INT;

const int loop_max = 150;
int mipmap = 0;
int max_iter = loop_max;

vec3 pos = ray_start;

// Move pos by a small bias
pos += ray_dir * 0.008;

float hit_bias = 0.0017;

while (mipmap > -1 && max_iter --> 0)
{

// Check if we are out of screen bounds, if so, return
if (pos.x < 0.0 || pos.y < 0.0 || pos.x > 1.0 || pos.y > 1.0 || pos.z < 0.0 || pos.z > 1.0)
{
return vec3(0,0,0);
}

// Fetch the current minimum cell plane height
float cell_z = textureLod(DownscaledDepth, pos.xy, mipmap).x;

// Compute the fractional part of the coordinate (scaled by the working size)
// so the values will be between 0.0 and 1.0
vec2 fract_coord = mod(pos.xy * work_size, 1.0);

// Modify fract coord based on which direction we are stepping in.
// Fract coord now contains the percentage how far we moved already in
// the current cell in each direction.
fract_coord.x = ray_dir.x > 0.0 ? fract_coord.x : 1.0 - fract_coord.x;
fract_coord.y = ray_dir.y > 0.0 ? fract_coord.y : 1.0 - fract_coord.y;

// Compute maximum k and minimum k for which the ray would still be
// inside of the cell.
vec2 max_k_v = (1.0 / abs(ray_dir.xy)) / work_size.xy;
vec2 min_k_v = -max_k_v * fract_coord.xy;

// Scale the maximum k by the percentage we already processed in the current cell,
// since e.g. if we already moved 50%, we can only move another 50%.
max_k_v *= 1.0 - fract_coord.xy;

// The maximum k is the minimum of the both sub-k's since if one component-maximum
// is reached, the ray is out of the cell
float max_k = min(max_k_v.x, max_k_v.y);

// Same applies to the min_k, but because min_k is negative we have to use max()
float min_k = max(min_k_v.x, min_k_v.y);

// Check if the ray intersects with the cell plane. We have the following
// equation:
// pos.z + k * ray_dir.z = cell.z
// So k is:
float k = (cell_z - pos.z) / ray_dir.z;

// Optional: Abort when ray didn't exactly intersect:
// if (k < min_k && mipmap <= 0) {
//     return vec3(0);
// }

// Check if we intersected the cell
if (k < max_k + hit_bias)
{
// Clamp k
k = max(min_k, k);

if (mipmap < 1) {
pos += k * ray_dir;
return pos;
}

// If we hit anything at a higher mipmap, step up to a higher detailed
// mipmap:
mipmap -= 2;
work_size *= 4;
} else {

// If we hit nothing, move to the next cell, with a small bias
pos += max_k * ray_dir * 1.04;
}

mipmap += 1;
work_size /= 2;
}

return vec3(0);
}


Result:

• This answer would be even better if it included a summary of what changed, from the version above & why, so readers don't need to do a line-by-line diff to follow your line of thinking. – DMGregory Nov 20 '15 at 16:20