1
$\begingroup$

I am currently reading the Ray Tracing in One Weekend tutorial (link), and I am dubious about their fix for shadow acne, which is to ignore ray-geometry intersections that occur at very small times.

For background, my understanding of the basic algorithm of raytracing and shadow acne is as follows:

  1. For each pixel in the image, shoot a light ray from the eye point / camera through the pixel's designated region in the image plane.
  2. To find the color of each pixel, calculate the closest intersection of the ray with the objects in the scene. Also, use multiple random rays for each pixel (anti-aliasing).
  3. Shadow acne: Now, say that we have some ray $R$ and say its closest intersection time is some floating-point number $t$. Then, $t$ may be inaccurate; if it is a little larger than the actual closest intersection time, then the calculated intersection point will be little inside the first object $R$ intersects, rather than being flush with its surface. As a result, the reflected ray will originate from inside the object, and so it will bounce off the inside surface next and continue to bounce inside the object, losing color each time and resulting in the pixel being darker than it should be (essentially, the object will shadow itself).

Now, the book suggests the following solution. Observe that if the next ray originates from inside the sphere due to $t$ being a little larger than it should have been, then the intersection time for the next ray will be very small, like $0.000001$. The book thus claims that ignoring small intersection times (such as all those below $0.001$) suffices to stop such occurrences of shadow acne.

However, I am dubious. Consider the following scenario:

  1. Say we have a sphere $S$ and a ray $R$ that intersects $S$ at two times $t_1 < t_2$.
  2. Now, say that $t_1 < 0.001$. Then $t_1$ will be ignored by the book's method, and so $t_2$ will be chosen as the correct intersection time.
  3. However, if a ray intersects a sphere twice, then the second intersection will actually be when the ray intersects the sphere from the inside! As a result, the ray will be reflected inside the sphere as well, and so then it will bounce and bounce off the interior surface theoretically forever, which has resulted in stack overflows in my code and the given code.

The main issue here is that ignoring small intersection times may cause larger intersection times, where the ray actually goes through objects, to be counted as the correct one.

How do we resolve this fundamental issue with the approach of ignoring smaller intersection times where dealing with shadow acne? Is this a known problem?

$\endgroup$
1
  • 1
    $\begingroup$ You are absolutely right, the solution that is being proposed is not fool proof and can lead to further shadow acne problems. However this solution does have the benefit of being simple, and works "well enough" in simple scenes. So is often taught as the first, simple solution to solving the shadow acne problem. $\endgroup$
    – pmw1234
    Oct 16, 2023 at 23:09

1 Answer 1

1
$\begingroup$

The book "RAY TRACING GEMS (1)" from Nvidia has a whole chapter about avoiding self-intersection. I highly recommend this book because they show different methods about all parts of problems using ray tracing and how to solve them with differently.

Lets look into chapter 6 "A Fast and Robust Method for Avoiding Self-Intersection"

I don't want to write down the hole chapter, so I'll collect the information and try to write it my own words.

In the book they say, that the self intersection problem becomes bigger in case the object is further away from the ray origine. Due to the floating point precision. So when calculating the intersection point, you'll receive the value t which becomes less accurate over distance. Therefore the new ray position which is calculated from the value t can be behind or infront of the hit surface. Like you already discribed in the question. And those displacement of the calculated position leeds to the problem, that when doing the next ray object intersection, it is not enough to exclude an intersection of $t = 0$ to avoid self intersection.

In the book they explain different methods to avoid this:

1. exclude the same primitive: The idea is very simple. When a ray intersects a triangle, you can write down the unique triangles primitive id. When shooting the reflection ray, you compare the primitive id for each intersection. In case the primitive id is the same, you have a self intersection and you can easily ignore this intersection.

Sounds good, efficient and easy to implement. But: Lets assume, the calculated hit position is behind the surface and we are extremly close to the neighboring primitive. Now the reflection direction can lead the ray being through the neighboring primitive. There we have the problem of self intersection again.

2. Limiting the ray interval: This method is what you discribe in your question. Defining an $t_{min} = \epsilon > 0$. So we shoot the reflection ray and only take care of intersections, where $t > t_{min}$.

Sounds efficient and easy to implement as well. But: $\epsilon$ is scene depending! and will fail for grazing angles, resulting in self-intersection. The second problem is that intersections with neighboring primitives can be wrongly skipped by t_{min} and the ray goes inside the geometry as you have discribed in your question.

3. Offsetting along the old ray direction: Same as number 2. But here the $t_{min}$ goes backwards the old ray direction. Again, the same problems occur than in number 2.

4. Offsetting along the normal direction of the hit surface: Here the important part is, that the offset is not fixed distance. So $\epsilon$ has not a fixed value. The offset need to be depending on the distance to the origin. Which can be calculated:

HLSL Code:

constexpr float origin()       { return 1.0f / 32.0f; }
constexpr float float_scale()  { return 1.0f / 65536.0f; }
constexpr float int_scale()    { return 1.0f / 256.0f; }

//normal points outward for rays exiting the surface, else is flipped.
float3 offset_ray(const float3 p, const float3 n)
{
    int3 of_i(int_scale()*n.x, int_scale() *n.y, int_scale()*n.z);
    float3 p_i(
        int_as_float(float_as_int(p.x)+((p.x < 0) ? -of_i.x : of_i.x)),
        int_as_float(float_as_int(p.y)+((p.y < 0) ? -of_i.y : of_i.y)),
        int_as_float(float_as_int(p.z)+((p.z < 0) ? -of_i.z : of_i.z)));
    return float3(fabsf(p.x) < origin() ? p.x+ float_scale()*n.x : p_i.x,
                  fabsf(p.y) < origin() ? p.y+ float_scale()*n.y : p_i.y,
                  fabsf(p.z) < origin() ? p.z+ float_scale()*n.z : p_i.z);
}

This source code is from the book: "RAY TRACING GEMS (1)".

This solution works with two steps: first sets an initial position as close as possible to the plane of the surface using the surface parameterization. It then shifts the intersection away from the surface by applying a scale-invariant offset to the position, along the geometric normal. The provided program code is doing the second step!

There are still cases, where problems can occur. But in the book they say these cases are extremly rare.

$\endgroup$
3
  • 1
    $\begingroup$ BTW: the link to this chapter: link.springer.com/content/pdf/10.1007/978-1-4842-4427-2_6.pdf $\endgroup$
    – Thomas
    Oct 17, 2023 at 10:02
  • 1
    $\begingroup$ Thank you for this detailed and insightful answer that not only explains why the original method is indeed problematic, but also offers an alternative solution. Thanks very much! $\endgroup$ Oct 17, 2023 at 18:50
  • $\begingroup$ You are welcome :) $\endgroup$
    – Thomas
    Oct 17, 2023 at 19:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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