A simplified approach to rendering SDFs on the web
10 min read2024-05-16
Ray marching is a rendering technique that differs from the standard pipeline in 3D graphics. It is, however, similar to traditional ray tracing with the difference being that we iteratively march the ray towards the objects to find the intersection point.
This iterative nature of ray marching allows it to handle the rendering of complex, mathematically defined surfaces like fractals or clouds: something that cannot be achieved with ray tracing or the standard rasterization approaches.
In this article, we’ll create the basic setup for a simplified 3D scene in three.js of two spheres rendered through ray marching. Below is what our final result will be.
If you are interested in the code only, clickhere. This will take you to a CodePen example.
Also, if you want to watch a video version of this article, check out this video:
The Algorithm
In short, we define a ray for each pixel, then iteratively march our ray in our scene until we find an intersection point, at which point, we calculate the colour for the pixel according to what we have hit.
For a live 2D demo, you can check out this CodePen example:
But to simplify the algorithm, we can dissect it into three functions:
The SDF equation of our scene which we will call scene(vec3 p) that will accept a vector3 variable p which represents our current position in the scene. The output will be the shortest distance to the objects in our scene.
The marching loop which we will call rayMarch(vec3 ro, vec3 rd) that will accept a ray origin vector3 and a ray direction vector3 (which we will both calculate for every pixel). This function will call our scene function at every loop to find the current distance we have to the objects in our scene and will stop the execution once we have an intersection point or if we are too far away from the scene. The final output will be the total distance covered.
The main function that will handle the calculation of the ray origin and ray direction and where the rayMarch function will be called. Once we have the total distance marched, we can calculate the intersection position and do various colour and normal calculations there.
We will make the code for these steps later in the article.
A small note about Ray marching Objects
Objects in our scene are not defined with polygonal 3D models as is usually the case with standard rasterization or ray tracing, instead, the objects are defined as a set of signed distance functions that can be compiled together to produce complex scenes.
For example, if we want to render a simple sphere on our screen, we would use the SDF equation:
Now that we have a good idea on what ray marching is, we can start on making our three.js scene. We will first start with the usual setup:
Here we created a basic three.js scene where we added a camera, a renderer, controls (to make movement in our scene easier) and a light.
Converting to a Ray marching scene
With that done, we will need to convert our viewport into a raymarching one. We can do this by making a plane with a custom fragment shader that will be placed exactly on the near plane of our camera.
The following is the code for adding this special plane:
And the following is for updating the position of our plane as the camera moves:
Moreover, in case you resize your screen, you can add the following code to resize the plane geometry as well:
At this point, you should see a red screen on your canvas. This is because we defined our shader material without a vertex and fragment shader so we will need to do that next.
The Shaders
I will assume that you are familiar with shaders in 3D graphics, but in essence, they are programs that run on the GPU that can affect the final render of our three.js objects.
Generally speaking, an object will have two shader programs attached to its material: a vertex shader that will change the position of the vertices of our model and a fragment shader that will affect the final output colour of the pixels that represent our model.
We can also pass data from our CPU to the GPU by defining uniforms that we can add to our three.js shader material.
Vertex shader
We first will need to define our vertex shader code. This will be pretty straightforward for our purposes as we won’t really do much in this shader.
Uniforms
Before we define our fragment shader, we should pass some data from our CPU to our GPU like camera position, light direction, and other things.
Fragment shader:
Now that we have everything set up, we will need to define our fragment shader for our screen plane which will hold all of our raymarching functions.
However, let’s first start by defining the uniforms inside the fragment shader. This is important as it allows us to use the data passed from the CPU inside of our shader.
One thing to note is where the vUv vec2 came from. This is a vector2 defined inside our vertex shader and passed to our fragment shader in order to identify the UV position of the pixel. In other words, it allows us to know which pixel we are at in our shader which is important for calculating the ray position and direction of each pixel.
Scene function
With that, let’s define our scene function:
Our scene will be made of two spheres moving around our scene and merging together as they get closer.
Ray March function
Now for our ray marcher, we can define it as follows:
As explained before, this is where the iterative process of ray marching is done as we first calculate where we are in the scene, if we’ve hit anything or we’re too far away, then we can just return our total distance travelled, and if not, we compound our current distance into our total distance variable and continue our loop.
Colouring functions
Before we start with our main function, we will need some other functions that we will use for colouring, these are:
A scene colour function that gives us the colour according to our hit point. Usually, you can make it mirror our scene function but instead of returning the distance, we return the color based on which object we are closer to:
A normal function that returns the normal to the scene according to our hit point. There are many ways to get this function, but Inigo Quilez’s method seems to be the most efficient:
Main Fragment function
Finally, With these functions, we can define our main fragment function:
Here we are calculating the ray origin and direction for the pixel, finding the total distance travelled by calling the ray marching function, and then with the normal and colour information of our hit point, we calculate the final colour for that pixel by using the Blinn-Phong lighting algorithm.
Adding the shaders to our Three.js Scene
To pass these shaders into the rendering plane we defined with three.js, we can simply put our shaders in strings and pass those strings into our shader material.
And that’s basically it, you now have a ray marching scene inside your three.js scene.
If you are interested in the code only, you can check out the following CodePen:
Conclusion
We can render a ray marching scene in three.js by setting up a simple plane, fixing that plane into the near plane of the camera, and adding the ray marching code into the fragment shader of that plane.
The CodePen above is an example of doing this in pure JavaScript, but if you are interested in adding this code to your React website, you can check out this live example.
I hope you enjoyed this article!
Cheers!
Raymarching
Threejs
Glsl
Rendering
If you enjoyed this article and would like to support me, consider to