0
\$\begingroup\$

I've been learning about raymarching and signed distance functions, and I implemented a raymarching fragment shader in Unity. It works fine for geometric shapes such as cubes and spheres, but when I started trying to build terrains with it I started running into issues.

I think I've narrowed down the problem to my signed distance function and not the raymarching implementation. To simplify things I've reproduced the problem using a sin wave, which seems to have the same result I'm seeing with noise functions.

The first issue is I'm getting strange holes in the surface - at least I think they are holes. At many points it's rendering inside the volume instead of the surface: Dark/black holes

Here is a modified color output where I color each pixel based on the signed distance value.

color = 0 - signedDistance

  • Red: surface not hit
  • Black: At or very near surface
  • White: Inside the volume

White areas indicating the point being rendered is inside the volume

I would expect to not see any white at all in this image. The results should be either black (surface found) or red (surface not found, ray marching loop stopped).

The other problem, which I believe is closely related to the first, is I'm getting inaccurate surface normal values at relatively large distances resulting in a wavy appearance. However, I'm fairly certain this is caused by the normal calculation using the same signed distance function which is returning inside the volume (which would provide incorrect, probably inverted, distance results), so I'm focusing on that issue first.

I've reproduced the issue I'm having on Shadertoy. While it's not exactly the same code, the basic algorithm is the same on both the GLSL Shadertoy sample and my Unity HLSL shader.

First, here is the most basic signed distance function I've used successfully. It just renders a flat plane at y=0:

float map(float3 position) {
    float sdist = position.y;
    return sdist;
}

Now I add a sine wave on top of this flat plane:

float map(float3 position) {
    float sdist = position.y + sin(x);
    return sdist;
}

What I expect is to see the plane move up and down as a sine wave along the x axis. I do see that, but I also see those odd holes (in the images above) through the surface. It's as if the ray marching portion goes right through past the surface and then stops inside of the volume.

I can somewhat mitigate this by stepping in much smaller steps than the calculated signed distance. Here is an example with the step size at 0.05:

Step size is 0.05

But this decreases performance since each raycast / marching step now traverses much smaller distances.

Is stepping shorter distance than the calculated distance function result the way to fix this?

Just for completeness, here is a stripped down version of the ray marching function:

// March forward multiplier
float stepSize = 1.0;
// maximum number of times to march forward
float maxRayCasts = 128;
// The minimum distance considered to be the surface
float precis = 0.01;  

vec3 castRay( in vec3 ro, in vec3 rd ) {
    float tmin = 0.01;
    float tmax = 600.0;

    vec2 res;

    float t = tmin;
    float m = -1.0;
    for( int i=0; i<maxRayCasts; i++ ) {
        res = map( ro+rd*t );
        if( res.x<precis || t>tmax ) break;
        t += res.x*stepSize;
        m = res.y;
    }

    if( t>tmax ) m=-1.0;
    return vec3( t, m, res.x );
}
\$\endgroup\$
  • \$\begingroup\$ If you want to share tips on solving the problem, post them in an Answer, not as an update tacked-onto the Question. \$\endgroup\$ – DMGregory Mar 10 at 15:54
1
\$\begingroup\$

After messing with the Shadertoy link (thank you for that, by the way), I have discovered that most of your problem comes from the line if( res.x<precis || t>tmax ) break;. You need to replace res.x with abs(res.x). This way, if the ray overshoots, when the signed distance function returns a negative value, the ray extends negatively, or to put it another way, retracts.

\$\endgroup\$
0
\$\begingroup\$

Alex F's solution is correct; I'd like to just explain why it's necessary in this case.

The way you're raymarching assumes that map is a true distance function, or at least a conservative one: because you try to step forward by the distance returned by map in each iteration, that return value must always be less than or equal to the true distance to your surface.

But your addition of sine breaks that rule.

Let's look at the curve map(p) == 1.0 (in blue), the level set of points your map function claims are 1.0 units from the surface (the level set map(p) == 0.0 in black):

Diagram of stacked sine waves

Because map(p) increases straight vertically, not perpendicular to the surface, it gives the correct distance at the peaks, but massively over-estimates the distance inside the troughs.

This means that a ray heading close to perpendicularly into the side of a trough will be given an estimate larger than its true distance to the surface, and on the next step it will tunnel through.

I'm not aware of an analytical function for computing the distance to a sine wave, so correcting the estimate itself may not be viable here.

Instead, you can modify your raymarcher to understand that it might sometimes get a bad estimate and overshoot. It can detect this if it lands in a region where map(p) returns a negative value, corresponding to a distance "inside" the shape. You can then backtrack based on this estimate. Your ray will then do something similar to a binary search in regions with inaccurate estimates, hopping between points on the near and far side of the surface, a little closer each time, until it zeroes-in on something close enough.

This is what Alex F's proposed modification accomplishes.

Note that for this to work, your step size needs to be small enough that you don't tunnel through the surface and out the other side in one step - otherwise your raymarcher won't see the negative value and detect the overshoot. When it does overshoot, it needs to move back again by less than its last step distance - otherwise it could oscillate endlessly or diverge away from the true intersection point.

Tuning your step size multiplier can help achieve this in trouble spots, though it does make your raymarching slower overall since even areas with well-behaved estimates will start taking more iterations.

\$\endgroup\$

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.