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

 

Procedural Textures

What does it mean to be procedural?

Procedurally generated content is any sort of content that was created using an algorithm by a computer, as opposed to a person by hand. There are many things that can be procedurally generated but here I will only be talking about textures.

Starting simple

If you just play around with some made up algorithms in a fragment shader you can get some pretty interesting results really easily. Here are a few examples with the output color vector listed below (texCoord is a standard OpenGL texture coordinate with a value between (0,0) and (1,1)):
lines

vec4(floor(mod((texCoord.x – texCoord.y) * 10.0, 2.0)), floor(mod((texCoord.x + texCoord.y) * 10.0, 2.0)), 0.0, 1.0)

werid stuff

vec4(0.0, 0.0, floor(mod((pow((1.0-texCoord.x), texCoord.y)) * 10.0, 2.0)) – floor(mod((texCoord.x * texCoord.y) * 10.0, 2.0)), 1.0)

These are both neat examples but they aren’t very useful. To generate more interesting content we will need to look into noise.

Noise textures

Noise textures are textures generated using random numbers, these are one of the most commonly used things in procedural generation. There are many types of noise textures, but the most basic one is grain textures. A grain texture is a texture with uninterpolated random values at each pixel. To generate any type of noise you need a random number function. To generate grain textures on the CPU you can use any type of random number function but for the GPU and more advanced types of noise, you will need a pseudo-random number generator. A pseudo-random number generator will always generate the same value from the input but has to still seem unrelated. This is the number generator I use with noise in GLSL:

float r(float n) {
return fract(cos(n * 89.421) * 343.436);
}

If you are generating noise on the CPU then you can use the built-in random generator of whatever language you are using. A lot of the code I show will be written in GLSL so you may have to adjust it to work with pixel coordinates if you want to use it. Here is a basic grain texture example:noise

vec4(vec3(1.0,1.0,1.0) * r(texCoord.x * 23.323 + texCoord.y * 249.2412), 1.0)

This is already starting to become more useful, for example, some shadow mapping methods require noise to be used in the shaders. You could also overlay it on a texture to make it look grainy, or adjust the colors and use it as a sand texture.

Grain textures are great for all of that, however, if you want to procedurally generate stuff like height maps, or just don’t want something so coarse this won’t work. The next step is to do interpolated noise, for this, there are two main options: value noise and perlin noise. They both look really similar but I generally start with value noise because it can be easier to implement and I will use it here because it is easier to understand.

Value noise

First I will define a few helper methods, a noise method that takes in a vec2 and a function that does cosine interpolation. Cosine interpolation is a lot slower than regular interpolation but is commonly used for value noise because it creates rounder edges which is good for realistic heightmaps.

float r(vec2 p) {
return r(p.x * 23.323 + p.y * 249.2412);
}

float cerp(float a, float b, float blend)
{
float theta = blend * 3.14159;
float f = (1.0 – cos(theta)) * 0.5;
return a * (1.0 – f) + b * f;
}

After that, the basic value noise function is pretty easy to define. It takes in a point and the square root of the total amount of noise cells that are visible. For example, if you want the texture to have 16×16 random values then the function will take in 16. If you set the value as the square size in pixels then it will look identical to the grain texture at this stage. To calculate the interpolated value the position is clipped to the noise grid and the fractional part is stored, then the 4 surrounding noise values are calculated and interpolated using the fractional part of the coordinate and finally, we do * 2.0 – 1.0 to allow the value to go below 0.

float value_noise(vec2 p, int c) {
vec2 scp = p * c;
vec2 flr = floor(scp);
vec2 fra = scp – flr;
float v0 = r(flr);
float v1 = r(flr + vec2(1.0, 0.0));
float v2 = r(flr + vec2(0.0, 1.0));
float v3 = r(flr + vec2(1.0, 1.0));
return cerp(cerp(v0, v1, fra.x), cerp(v2, v3, fra.x), fra.y) * 2.0 – 1.0;
}

A grid of 16×16 interpolated noise cells looks like this (adjusted to show values as 0.0-1.0):

interpolated_noise

vec4(vec3(1.0,1.0,1.0) * (value_noise(texCoord, 16) + 1.0) + 0.5, 1.0)

Most of the time this would be considered too noisy for these types of textures, so we will use a function to smooth it out.

Smooth noise

We can smooth the noise out by taking the average of the center cell and the 8 surrounding cells. You get slightly different results if you change the weights of the surrounding values so you may want to change them.

float smooth_noise(vec2 p) {
float corners = (r(p + vec2(-1.0, -1.0)) + r(p + vec2(1.0, -1.0)) + r(p + vec2(-1.0, 1.0)) + r(p + vec2(1.0, 1.0))) / 16.0;
float sides = (r(p + vec2(-1.0, 0.0)) + r(p + vec2(1.0, 0.0)) + r(p + vec2(0.0 -1.0)) + r(p + vec2(0.0, 1.0))) / 8.0;
float center = r(p) / 4.0;
return corners + sides + center;
}

Now that we have this we can replace the calls in value_noise to r() with smooth_noise().

float value_noise(vec2 p, int c) {
vec2 scp = p * c;
vec2 flr = floor(scp);
vec2 fra = scp – flr;
float v0 = smooth_noise(flr);
float v1 = smooth_noise(flr + vec2(1.0, 0.0));
float v2 = smooth_noise(flr + vec2(0.0, 1.0));
float v3 = smooth_noise(flr + vec2(1.0, 1.0));
return cerp(cerp(v0, v1, fra.x), cerp(v2, v3, fra.x), fra.y) * 2.0 – 1.0;
}

After this, the noise looks a bit smoother, here is an image with the new method using a 16×16 noise cell grid.

smooth_noise

vec4(vec3(1.0,1.0,1.0) * (value_noise(texCoord, 16) + 1.0) * 0.5, 1.0)

This is a lot better than the first grain texture, however, it wouldn’t look very interesting when applied to terrain. A common way of fixing this is by applying multiple layers of noise over each other using a method called fractal Brownian motion aka fBm.

Fractal Brownian motion

The fBm function takes in the same parameters as value_noise() but the number of layers calculated is prefined as OCTAVES = 6, but you could also take in the octaves as a parameter.

#define OCTAVES 6
float fract_noise(vec2 p, int c) {
float value = 0.0;
float amplitude = 0.5;
float frequency = 0.0;
for (int i = 0; i < OCTAVES; i++)
{
value += amplitude * value_noise(p, c);
p *= 2.0;
amplitude *= 0.5;
}
return value;
}

Here is a fBm texture generated using a base grid size of 16.
fract_noise

vec4(vec3(1.0,1.0,1.0) * (fract_noise(texCoord, 16) + 1.0) * 0.5 / 2.0, 1.0)

If you plug this into a terrain generator you can see that we are starting to get pretty interesting results.

fractal_landscape

Some more noise

At this point, there is a number of small things we can do to get very interesting results. For example, if we offset the input position by noise then we get domain warped noise.

domain_noise

vec4(vec3(1.0,1.0,1.0) * (fract_noise(texCoord + fract_noise(texCoord, 16), 16) + 1.0) * 0.5 / 2.0, 1.0)

We could also do 1.0-abs(value_noise()) in fract_noise() to get ridged noise.

ridged_noise

We can see where it got its name if we look at just one layer of ridged value noise.

single_ridge_noise

You can really apply any operation you can think of to the noise and it will produce interesting results. For example, if you divided the elements in fBm instead of adding them you would get this.

div_noise

There are many more ways of generating procedural textures and more types of noise and I will discuss some of those in future posts.