1
\$\begingroup\$

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: enter image description here

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?

\$\endgroup\$
1
\$\begingroup\$

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:

Result Image

| improve this answer | |
\$\endgroup\$
  • 3
    \$\begingroup\$ 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. \$\endgroup\$ – DMGregory Nov 20 '15 at 16:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.