2
$\begingroup$

I have been experiencing issues over the past few days with implementing SSR on water into my engine. Looking straight forward, it appears to be alright. However, when I look up or down, the reflections become shrunken or stretched. (Ignore colors being incorrect, and the sides of blocks looking messed up- that's not the problem)

This is the water looking decent.

For example, this is what happens when I look down. The reflections become increasingly stretched on the Y-axis. The reflections become increasingly stretched and distorted the further down we look.

Additionally, when I look down further, the water begins to reflect terrain that is underneath it in a very distorted manner (when clearly it shouldn't, as the water is 100% flat and all the normals are (0, 1, 0)). It's like the rays are being cast down instead of up. Water looking severely unwater-like when looking low.k

Similarly, when I look up, the reflections become increasingly squashed and "disappear". Reflections being smashed when looking up.

My fragment shader code is primarily based on this post, but uses a binary search algorithm from here to refine the results. Here is the relevant fragment shader code:

flat in vec3 camNormal; // view space normal
uniform mat4 inv_projection;
uniform sampler2D ssr_depth; // straight-up depth
uniform sampler2D ssr_albedoSpec; // used solely for color information here

// calculate view position of current fragment
vec3 calcViewPosition(in vec2 texCoord)
{
  // Combine UV & depth into XY & Z (NDC)
  vec3 rawPosition = vec3(texCoord, gl_FragCoord.z);

  // Convert from (0, 1) range to (-1, 1)
  vec4 ScreenSpacePosition = vec4(rawPosition * 2. - 1., 1.);

  // Undo Perspective transformation to bring into view space
  vec4 ViewPosition = inv_projection * ScreenSpacePosition;

  // Perform perspective divide and return
  return ViewPosition.xyz / ViewPosition.w;
}

// calculate view position with depth buffer
vec3 calcViewPositionDepthTex(in vec2 texCoord)
{
  vec3 rawPosition = vec3(texCoord, texture(ssr_depth, texCoord).r);
  vec4 ScreenSpacePosition = vec4(rawPosition * 2. - 1., 1.);
  vec4 ViewPosition = inv_projection * ScreenSpacePosition;
  return ViewPosition.xyz / ViewPosition.w;
}

vec2 binarySearch(inout vec3 dir, inout vec3 hitCoord, inout float dDepth)
{
  float depth;

  vec4 projectedCoord;

  for(int i = 0; i < 20; i++)
  {
    projectedCoord = u_proj * vec4(hitCoord, 1.0);
    projectedCoord.xy /= projectedCoord.w;
    projectedCoord.xy = projectedCoord.xy * 0.5 + 0.5;

    depth = calcViewPositionDepthTex(projectedCoord.xy).z;

    dDepth = hitCoord.z - depth;

    dir *= 0.5;
    if(dDepth > 0.0)
      hitCoord += dir;
    else
      hitCoord -= dir;    
  }

  projectedCoord = u_proj * vec4(hitCoord, 1.0);
  projectedCoord.xy /= projectedCoord.w;
  projectedCoord.xy = projectedCoord.xy * 0.5 + 0.5;

  return vec2(projectedCoord.xy);
}

vec2 rayCast(vec3 dir, inout vec3 hitCoord, out float dDepth)
{
  dir *= .3;

  for (int i = 0; i < 40; i++)
  {
    hitCoord += dir; 

    vec4 projectedCoord = u_proj * vec4(hitCoord, 1.0);
    projectedCoord.xy /= projectedCoord.w;
    projectedCoord.xy = projectedCoord.xy * 0.5 + 0.5; 

    float depth = calcViewPositionDepthTex(projectedCoord.xy).z;
    dDepth = hitCoord.z - depth;

    if(dDepth < 0.0)
      return binarySearch(dir, hitCoord, dDepth);
  }

  return vec2(-1.0f);
}

vec3 ssr()
{
  // compute texture coordinates
  ssTexCoords.x = gl_FragCoord.x / textureSize(ssr_positions, 0).x;
  ssTexCoords.y = gl_FragCoord.y / textureSize(ssr_positions, 0).y;
  vec2 texCoord = ssTexCoords;

  // normal at initial ray position
  vec3 normal = camNormal;
  vec3 fViewPos = calcViewPosition(texCoord);

  // Reflection vector
  vec3 reflected = normalize(reflect(normalize(fViewPos), normalize(normal)));

  // Ray cast
  vec3 hitPos = fViewPos;
  float dDepth;
  float minRayStep = .05f;
  vec2 coords = rayCast(reflected * max(minRayStep, -fViewPos.z), hitPos, dDepth);
  if (coords != vec2(-1.0))
    return mix(vColor.rgb, texture(ssr_albedoSpec, coords).rgb / 2, .5005);
  else
    return vColor.ggg; // ray failed to intersect (use sky color in the future)
}

The color returned from ssr() is currently 100% equal to the final fragment color to view the results most directly.

The primary deviation I make in this implementation is that instead of getting the depth and normal of the initial ray from stored textures, I am getting it from the values passed to the current fragment. This is because the water is drawn in a separate forward-pass, and is never drawn to the depth or normal maps. Therefore I am using gl_FragCoord.z for the depth, and view-space normal passed from the vertex shader as the normal. I understand it is less expandable this way, but I only intend for water to be reflective.

I initially suspected that the problem was related to the normals of the water, but displaying the water normals (shifted from -1-1 range to 0-1) showed it to correctly be green. This has led me to believe the issue either has to do with A) the reflected angle being incorrect, which would mean calcViewPos is incorrect; or B) some of the information being used that I assumed to be correct to use in this context was not.

I have very little experience with ray-tracing (and graphics programming in general), so my problem might be something really basic that I overlooked.

$\endgroup$

Your Answer

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

Browse other questions tagged or ask your own question.