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:
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
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:
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 );
}