
#pragma BLENDER_REQUIRE(common_view_lib.glsl)
#pragma BLENDER_REQUIRE(common_math_lib.glsl)
#pragma BLENDER_REQUIRE(common_uniforms_lib.glsl)

/**
 * Screen-Space Raytracing functions.
 */

uniform sampler2D maxzBuffer;
uniform sampler2DArray planarDepth;

struct Ray {
  vec3 origin;
  /* Ray direction premultiplied by its maximum length. */
  vec3 direction;
};

/* Inputs expected to be in viewspace. */
void raytrace_clip_ray_to_near_plane(inout Ray ray)
{
  float near_dist = get_view_z_from_depth(0.0);
  if ((ray.origin.z + ray.direction.z) > near_dist) {
    ray.direction *= abs((near_dist - ray.origin.z) / ray.direction.z);
  }
}

/* Screenspace ray ([0..1] "uv" range) where direction is normalize to be as small as one
 * full-resolution pixel. The ray is also clipped to all frustum sides.
 */
struct ScreenSpaceRay {
  vec4 origin;
  vec4 direction;
  float max_time;
};

void raytrace_screenspace_ray_finalize(inout ScreenSpaceRay ray)
{
  /* Constant bias (due to depth buffer precision). Helps with self intersection. */
  /* Magic numbers for 24bits of precision.
   * From http://terathon.com/gdc07_lengyel.pdf (slide 26) */
  const float bias = -2.4e-7 * 2.0;
  ray.origin.zw += bias;
  ray.direction.zw += bias;

  ray.direction -= ray.origin;
  /* If the line is degenerate, make it cover at least one pixel
   * to not have to handle zero-pixel extent as a special case later */
  if (len_squared(ray.direction.xy) < 0.00001) {
    ray.direction.xy = vec2(0.0, 0.00001);
  }
  float ray_len_sqr = len_squared(ray.direction.xyz);
  /* Make ray.direction cover one pixel. */
  bool is_more_vertical = abs(ray.direction.x / ssrPixelSize.x) <
                          abs(ray.direction.y / ssrPixelSize.y);
  ray.direction /= (is_more_vertical) ? abs(ray.direction.y) : abs(ray.direction.x);
  ray.direction *= (is_more_vertical) ? ssrPixelSize.y : ssrPixelSize.x;
  /* Clip to segment's end. */
  ray.max_time = sqrt(ray_len_sqr * safe_rcp(len_squared(ray.direction.xyz)));
  /* Clipping to frustum sides. */
  float clip_dist = line_unit_box_intersect_dist_safe(ray.origin.xyz, ray.direction.xyz);
  ray.max_time = min(ray.max_time, clip_dist);
  /* Convert to texture coords [0..1] range. */
  ray.origin = ray.origin * 0.5 + 0.5;
  ray.direction *= 0.5;
}

ScreenSpaceRay raytrace_screenspace_ray_create(Ray ray)
{
  ScreenSpaceRay ssray;
  ssray.origin.xyz = project_point(ProjectionMatrix, ray.origin);
  ssray.direction.xyz = project_point(ProjectionMatrix, ray.origin + ray.direction);

  raytrace_screenspace_ray_finalize(ssray);
  return ssray;
}

ScreenSpaceRay raytrace_screenspace_ray_create(Ray ray, float thickness)
{
  ScreenSpaceRay ssray;
  ssray.origin.xyz = project_point(ProjectionMatrix, ray.origin);
  ssray.direction.xyz = project_point(ProjectionMatrix, ray.origin + ray.direction);
  /* Interpolate thickness in screen space.
   * Calculate thickness further away to avoid near plane clipping issues. */
  ssray.origin.w = get_depth_from_view_z(ray.origin.z - thickness) * 2.0 - 1.0;
  ssray.direction.w = get_depth_from_view_z(ray.origin.z + ray.direction.z - thickness) * 2.0 -
                      1.0;

  raytrace_screenspace_ray_finalize(ssray);
  return ssray;
}

struct RayTraceParameters {
  /** ViewSpace thickness the objects  */
  float thickness;
  /** Jitter along the ray to avoid banding artifact when steps are too large. */
  float jitter;
  /** Determine how fast the sample steps are getting bigger. */
  float trace_quality;
  /** Determine how we can use lower depth mipmaps to make the tracing faster. */
  float roughness;
};

/* Returns true on hit. */
/* TODO fclem remove the backface check and do it the SSR resolve code. */
bool raytrace(Ray ray,
              RayTraceParameters params,
              const bool discard_backface,
              const bool allow_self_intersection,
              out vec3 hit_position)
{
  /* Clip to near plane for perspective view where there is a singularity at the camera origin. */
  if (ProjectionMatrix[3][3] == 0.0) {
    raytrace_clip_ray_to_near_plane(ray);
  }

  ScreenSpaceRay ssray = raytrace_screenspace_ray_create(ray, params.thickness);
  /* Avoid no iteration. */
  if (!allow_self_intersection && ssray.max_time < 1.1) {
    hit_position = ssray.origin.xyz + ssray.direction.xyz;
    return false;
  }

  ssray.max_time = max(1.1, ssray.max_time);

  float prev_delta = 0.0, prev_time = 0.0;
  float depth_sample = get_depth_from_view_z(ray.origin.z);
  float delta = depth_sample - ssray.origin.z;

  float lod_fac = saturate(fast_sqrt(params.roughness) * 2.0 - 0.4);

  /* Cross at least one pixel. */
  float t = 1.001, time = 1.001;
  bool hit = false;
  const float max_steps = 255.0;
  for (float iter = 1.0; !hit && (time < ssray.max_time) && (iter < max_steps); iter++) {
    float stride = 1.0 + iter * params.trace_quality;
    float lod = log2(stride) * lod_fac;

    prev_time = time;
    prev_delta = delta;

    time = min(t + stride * params.jitter, ssray.max_time);
    t += stride;

    vec4 ss_p = ssray.origin + ssray.direction * time;
    depth_sample = textureLod(maxzBuffer, ss_p.xy * hizUvScale.xy, floor(lod)).r;

    delta = depth_sample - ss_p.z;
    /* Check if the ray is below the surface ... */
    hit = (delta < 0.0);
    /* ... and above it with the added thickness. */
    hit = hit && (delta > ss_p.z - ss_p.w || abs(delta) < abs(ssray.direction.z * stride * 2.0));
  }
  /* Discard backface hits. */
  hit = hit && !(discard_backface && prev_delta < 0.0);
  /* Reject hit if background. */
  hit = hit && (depth_sample != 1.0);
  /* Refine hit using intersection between the sampled heightfield and the ray.
   * This simplifies nicely to this single line. */
  time = mix(prev_time, time, saturate(prev_delta / (prev_delta - delta)));

  hit_position = ssray.origin.xyz + ssray.direction.xyz * time;

  return hit;
}

bool raytrace_planar(Ray ray, RayTraceParameters params, int planar_ref_id, out vec3 hit_position)
{
  /* Clip to near plane for perspective view where there is a singularity at the camera origin. */
  if (ProjectionMatrix[3][3] == 0.0) {
    raytrace_clip_ray_to_near_plane(ray);
  }

  ScreenSpaceRay ssray = raytrace_screenspace_ray_create(ray);

  /* Planar Reflections have X mirrored. */
  ssray.origin.x = 1.0 - ssray.origin.x;
  ssray.direction.x = -ssray.direction.x;

  float prev_delta = 0.0, prev_time = 0.0;
  float depth_sample = texture(planarDepth, vec3(ssray.origin.xy, planar_ref_id)).r;
  float delta = depth_sample - ssray.origin.z;

  float t = 0.0, time = 0.0;
  /* On very sharp reflections, the ray can be perfectly aligned with the view direction
   * making the tracing useless. Bypass tracing in this case. */
  bool hit = false;
  const float max_steps = 255.0;
  for (float iter = 1.0; !hit && (time < ssray.max_time) && (iter < max_steps); iter++) {
    float stride = 1.0 + iter * params.trace_quality;

    prev_time = time;
    prev_delta = delta;

    time = min(t + stride * params.jitter, ssray.max_time);
    t += stride;

    vec4 ss_ray = ssray.origin + ssray.direction * time;

    depth_sample = texture(planarDepth, vec3(ss_ray.xy, planar_ref_id)).r;

    delta = depth_sample - ss_ray.z;
    /* Check if the ray is below the surface. */
    hit = (delta < 0.0);
  }
  /* Reject hit if background. */
  hit = hit && (depth_sample != 1.0);
  /* Refine hit using intersection between the sampled heightfield and the ray.
   * This simplifies nicely to this single line. */
  time = mix(prev_time, time, saturate(prev_delta / (prev_delta - delta)));

  hit_position = ssray.origin.xyz + ssray.direction.xyz * time;
  /* Planar Reflections have X mirrored. */
  hit_position.x = 1.0 - hit_position.x;

  return hit;
}

float screen_border_mask(vec2 hit_co)
{
  const float margin = 0.003;
  float atten = ssrBorderFac + margin; /* Screen percentage */
  hit_co = smoothstep(0.0, atten, hit_co) * (1.0 - smoothstep(1.0 - atten, 1.0, hit_co));

  float screenfade = hit_co.x * hit_co.y;

  return screenfade;
}
