Ray Marching

Ray marching

Ray marching is one of the ray-based methods like ray tracing and path tracing. In ray tracing, you cast rays then check what object their path intersects. In ray marching, you have an origin point and a direction just like ray tracing but you travel at increments checking if the point lays inside of any objects. As you could have guessed, ray marching can be a lot slower in some cases but it can do a lot of things that ray tracing can’t, or are just very difficult to do in other methods. For example, it is very easy to render terrain using ray marching, you just have to check if the point is below the heightmap data. If you want to ray trace terrain you would have to construct a mesh out of the heightmap data then do an intersection check for each triangle. Ray marching can also render certain 3D fractals fairly easily.

Marching a sphere

First, we need to find out what position and direction to use for each pixel. You can use another coordinate system, but I use the same one as OpenGL so I can easily get the direction vectors from the coordinates of a quad mesh. You will want to make an out vector in your vertex shader.

out vec3 pass_Direction;

I will make the direction away from the camera be positive Z and do:

pass_Direction = vec3(in_Position.xy, 1.0);

Make sure to take in the direction in the fragment shader. The starting position is just the camera position, I just hard code this as vec3(0.0, 0.0, 0.0) at first.

Here I am not going to be using signed distance fields so all of the steps will be the same size. In this case, the ray march function is easy to define. DISTANCE is the maximum distance a ray will travel. After every step we move the position along the ray according to the direction. Right now this will always produce a black screen because we aren’t marching anything yet.

#define STEPS 128
#define DISTANCE 2.0
vec3 ray_march(vec3 pos, vec3 direction) {
    ­­­­­­­float stepSize = 1.0 / STEPS * DISTANCE;
    for (int i = 0; i < STEPS; i++) {
        //intersections will be calculated here
        pos += direction * stepSize;
    }
    return vec3(0.0, 0.0, 0.0);
}

All of the intersection functions will return a color if the point is inside whatever it is testing for, and if it doesn’t it will return NOTHING. I define NOTHING as a vec3 that wouldn’t be a valid color so none of the functions return an identical color on accident.

#define NOTHING vec3(-1.0, -1.0, –1.0)

Checking for a sphere intersection is super easy, all you have to do is check if the distance from the center of the sphere is smaller than the radius. I represent a sphere with a vec4 following the format of vec4(x, y, z, radius).

vec3 sphere_intersect(vec3 pos, vec4 sphere) {
float dist = distance(pos, sphere.xyz);
if (dist < sphere.w) {
return vec3(1.0, 0.0, 0.0);
}
return NOTHING;
}

After this, we can check for a sphere intersection with our ray_march() function. I put the sphere 2 units back in the z direction so it won’t cover the whole screen.

vec3 ray_march(vec3 pos, vec3 direction) {
float stepSize = 1.0 / STEPS * DISTANCE;
for (int i = 0; i < STEPS; i++) {
vec3 color = sphere_intersect(pos, vec4(0.0, 0.0, 2.0, 1.0));
if (color != NOTHING) {
return color;
}
pos += direction * stepSize;
}
return vec3(0.0, 0.0, 0.0);
}

Now that we have an object we can output the result.

out_Color = vec4(ray_march(vec3(0.0, 0.0, 0.0), pass_Direction * rotationMatrix), 1.0);

This is what it looks like:

sphere

Moving the camera

Moving the camera position is very simple, all we have to do is load in a uniform, I call mine rm_Cam_Pos. You could create the view rotation matrix on the CPU and this would be more efficient but here I just do it in the shader and load in the camera pitch and yaw as a vec2 called rm_Cam_Rot. Here is the function that creates the rotation matrix:

mat3 rotation_matrix(vec2 angle) {
vec2 c = cos(angle);
vec2 s = sin(angle);
return mat3(
c.y, 0.0, s.y,
s.y * s.x, c.x, c.y * s.x,
s.y * c.x, s.x, c.y * c.x
);
}

After we have this we can adjust the output to take into account these new variables.

mat3 rotationMatrix = rotation_matrix(rm_Cam_Rot);
out_Color = vec4(ray_march(rm_Cam_Pos, pass_Direction * rotationMatrix), 1.0);

You can change these variables any way you want on the CPU, but I just set them to be sin() and cos() of a variable that changes over time. It looks like this:

moving_sphere

Marching procedural terrain

It isn’t too hard to march terrain, like I said at the start all you have to do is check if the point is below a heightmap value. It would be much more efficient to load in a heightmap as a sampler but here I will use the fract_nosie() function described in my procedural textures post. You could have the terrain be infinite but I check if it’s within the bounds of a box first then check if the value is below the noise located and the x and z coordinates. If it is I output the height of the noise as the color.

vec3 terrain_intersect(vec3 pos, vec3 cubePos, vec3 cubeSize) {
if (
pos.x > cubePos.x cubeSize.x && pos.x < cubePos.x + cubeSize.x
&& pos.y > cubePos.y cubeSize.y && pos.y < cubePos.y + cubeSize.y
&& pos.z > cubePos.z cubeSize.z && pos.z < cubePos.z + cubeSize.z
) {
float height = cubePos.y + abs(fract_noise(pos.xz, 4)) / 2.0;
if (pos.y < height) {
return vec3(1.0, 1.0, 1.0) * height;
}
}
return NOTHING;
}

We can adjust the ray_march() function to use this instead, you could also render both this and the sphere, or even multiple spheres.

vec3 ray_march(vec3 pos, vec3 direction) {
float stepSize = 1.0 / STEPS * DISTANCE;
for (int i = 0; i < STEPS; i++) {
vec3 color = terrain_intersect(pos, vec3(0.0, 0.0, 2.0), vec3(1.0, 0.5, 1.0));
if (color != NOTHING) {
return color;
}
pos += direction * stepSize;
}
return vec3(0.0, 0.0, 0.0);
}

This makes some pretty cool looking terrain:

terrain

I also wrote a function that uses domain warped noise and a domain warped texture. Here is the function:

vec3 terrain_intersect(vec3 pos, vec3 cubePos, vec3 cubeSize) {
if (
pos.x > cubePos.x cubeSize.x && pos.x < cubePos.x + cubeSize.x
&& pos.y > cubePos.y cubeSize.y && pos.y < cubePos.y + cubeSize.y
&& pos.z > cubePos.z cubeSize.z && pos.z < cubePos.z + cubeSize.z
) {
float warp = fract_noise(pos.xz, 4);
float height = cubePos.y + abs(fract_noise(pos.xz + warp, 4)) / 2.0;
if (pos.y < height) {
float intensity = clamp(height + 0.2, 0.2, 1.0);
return texture(diffuse, pos.xz * 2.0 + warp).xyz * intensity;
}
}
return NOTHING;
}

Here is the texture:

red

And here is the result:

red_terrain

Things to try

  • Multiple objects
  • Shaded spheres
  • More shapes
  • Infinite terrain
  • Sky shading

 

Leave a comment