DEV Community

Cover image for Ray marching part 1: the unappreciated cousin of ray tracing
Rami-Slicer
Rami-Slicer

Posted on • Updated on

Ray marching part 1: the unappreciated cousin of ray tracing

Ray tracing

We all know about ray tracing, where you shoot out a ray for each pixel and use fancy math to recursively bounce it around and color the pixel. If you repeat this process for every pixel, you can get a lovely rendering of your scene that, if made by a talented artist, can look almost indistinguishable from real life.

Ray trace diagram.svg

Unfortunately, ray tracing has some downsides, namely that it can be insanely slow. A single frame can take hours to render, which may be fine for animated movies since their creator most likely has very powerful computers dedicated to rendering, however it's quite a nuisance for hobbyists since they usually don't have gigantic renderfarms all to themselves.

However, there is another.

Ray marching

Enter ray marching: the unappreciated cousin of ray tracing.
Ray marching is quite similar to ray tracing, however it has some interesting quirks that make it an interesting option for real-time graphics demos and games.
First, I should note that ray marching is not a replacement for rasterization nor ray tracing, it's just a cool alternative.
Alright! Let's get started.

How it works

Ray marching is, as I said previously, quite similar to ray tracing. The difference is that ray marching "marches" rays along their vectors and then uses distance equations to decide how far to march the ray, or if it has intersected with an object. We can then return stuff from the marching function like the position it intersected at, how many times it marched, or how far it marched.

How to implement it

Now that you know roughly how it works, let's actually make a ray marcher. We'll use GLSL, so you'll need to set up OpenGL or use something like Shadertoy. If you don't feel like following along, here's the code on Shadertoy

First of all, we need some stuff at the top of the shader:

#version 420
// you can change #version if you want to
// the resolution, set to the window's dimensions.
uniform vec2 resolution;
// uncomment if you are going to implement a movement system
//uniform vec3 camPos;

Let's start with the simplest part, a distance estimator. Specifically, a sphere's DE:

float sphereDE(vec3 pos, vec3 spherePos, float size) {
    return length(pos - spherePos) - size;
}

This one is super simple and pretty self explanatory so I won't really explain it, however keep in mind that other DEs are more complex than this. If you don't feel like spheres are for you than here's a big list of other shapes.

Now we can write the marching function which does the actual marching!

vec3 march(vec3 origin, vec3 direction) {
    float rayDist = 0.0;
    const int NUM_STEPS = 32; // doesn't matter much right now
    const float MIN_DIST = 0.001; // threshold for intersection
    const float MAX_DIST = 1000.0; // oops we went into space

    for(int i = 0; i < NUM_STEPS; i++) {
        vec3 current_pos = origin + rayDist*direction;

        // Use our distance estimator to find the distance
        float _distance = sphereDE(current_pos, vec3(0.0), 1.0);

        if(_distance < MIN_DIST) {
            // We hit an object! This just adds a subtle shading effect.
            return vec3(rayDist/float(NUM_STEPS)*4.0);
        }

        if(rayDist > MAX_DIST) {
            // We have gone too far
            break;
        }

        // Add the marched distance to total
        rayDist += _distance;
    }
    // The ray didn't hit anything so return a color (black, for now)
    return vec3(0.0);
}

Now we have all we need to raymarch except a main function.
Let's write it:

void main( out vec4 fragColor, in vec2 fragCoord ) {
    // normalized coordinates, resolution should be the window/screens pixel dimensions.
    vec2 uv = fragCoord / resolution.xy * 2.0 - 1.0;
    uv = uv / vec2(0.6, 1.0);

    // camPos can be a uniform if you want to move around
    vec3 camPos = vec3(0.0, 0.0, -3.0);
    vec3 rayDir = vec3(uv, 1.0);

    // march!
    vec3 shaded_color = march(camPos, rayDir);

    // set the color!
    fragColor = vec4(shaded_color, 1.0);
}

And we are done! Once you've put these together, you should be able to see a circle that subtly gets lighter towards the edges.

Improvements

While what we just made was cool and all, it's not that exciting. It's monochrome, dull, and there's not much to it other than a sphere.
We cold make a struct for a ray, and then we'd be able to return all sorts of useful values:

struct Ray {float distance; vec3 endPos; float minDist; bool hit;};

So here is my final version:

const int NUM_STEPS = 32; // doesn't matter much right now
const float MIN_DIST = 0.001; // threshold for intersection
const float MAX_DIST = 1000.0; // oops we went into space

struct Ray {float totalDist; float minDist; vec3 endPos; bool hit;};

// The distance estimator for a sphere
float sphereDE(vec3 pos, vec3 spherePos, float size) {
    return length(pos - spherePos) - size;
}

Ray march(vec3 origin, vec3 direction) {
    float rayDist = 0.0;
    float minDist = MAX_DIST;

    for(int i = 0; i < NUM_STEPS; i++) {
        vec3 current_pos = origin + rayDist * direction;

        // Use our distance estimator to find the distance
        float _distance = sphereDE(current_pos, vec3(0.0), 1.0);

        minDist = _distance < minDist ? _distance : minDist;

        if(_distance < MIN_DIST) {
            // We hit an object!
            return Ray(rayDist, minDist, current_pos, true);
        }

        if(rayDist > MAX_DIST) {
            // We have gone too far
            break;
        }

        // Add the marched distance to total
        rayDist += _distance;
    }
    return Ray(MAX_DIST, minDist, origin + rayDist * direction, false);
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates (from 0 to 1)
    vec2 uv = fragCoord / iResolution.xy * 2.0 - 1.0;

    // scale the ra
    uv = uv / vec2(0.6, 1.0);

    vec3 camPos = vec3(0.0, 0.0, -3.0);
    vec3 rayDir = vec3(uv, 1.0);

    Ray marched = march(camPos, rayDir);

    vec3 color = marched.hit ? 
        vec3(marched.totalDist/pow(float(NUM_STEPS), 0.8)*4.0) : // shading
        vec3(0.0) + vec3(pow(clamp(-1.0 * marched.minDist + 1.0, 0.0, 1.0), 4.0) / 2.0); // glow

    fragColor = vec4(color,1.0);
}

Here's how it looks!
There are infinity other additions you can make like colors, CSG (boolean operations between DEs), or more shapes to play around with. However my fingers are getting really tired so I'm going to stop here.

I also highly recommend watching CodeParade's intro video about ray marching as well as his video about marble marcher, a game made with ray marching and pretty fractals.

Part 2

Discussion (3)

Collapse
th2 profile image
TH

Also have a look at thefuselab.io which is a raymarching beast! You can dynamically compose SDFs and raymarch them with PBR materials, all visually in real time, you don't have to stop the application to edit code...

Collapse
thatguyjk profile image
ThatGuyJK🕵🏾‍

Glad to see ray marching stuff on the site! An small improvement to your initial uv coordinate that centers the uv as well as fixes the aspect ratio would be.

vec2 uv = (fragCoord-.5*iResolution.xy)/iResolution.y; //

Collapse
ramislicer profile image
Rami-Slicer Author

Side note, does anyone know how can I embed a Shadertoy shader in the post? I tried using HTML but it didn't work.