Though ray marching is powerful in terms of rendering various SDFs, it always falls short in its performance as the algorithm is very heavy and computationally expensive. Therefore, it is important to try and optimize it as best as possible, which is where cone marching comes along.
Since pixels are always very close to each other, bundles of them end up marching the same distance so to speak, but since we are calculating the distance equation for each pixel independently, we end up with many repeated calculations that give very similar results. Bundling a group of rays, however, can mathematically give us a cone, and marching this cone instead of all these rays independently will allow us to skip a lot of those extra calculations.
In this article, we’ll build upon the basic ray marching setup discussed in my previous article. It is, therefore, recommended that you check out that article before continuing.
Below is an example of the FPS gain we can get with this algorithm:
However, if you are interested in the code only, click here. This will take you to a CodePen example of our final cone marching result.
Before we start with the algorithm, we will use a different scene SDF to see the optimization in action. Our scene will be defined as follows:
And our scene colour is a simple orange colour:
This will give us the following SDF:
If you are curious about this SDF, it’s a fractal called mandlebox. The implementation is based on the third level of Fractal Glide: a game I made using cone-marching.
Mathematically, cone marching is very similar to ray marching with the exception being the break condition that we have in our marching loop where instead of checking if the ray has hit anything, we check if the cone has hit anything.
For a live 2D demo, you can check out this CodePen example:
There are many ways to use this algorithm and it depends on the 3D graphics library that you are using and how complex you want your code to be. Here I will explain various methods but I will implement the simplest three.js way of using this algorithm in our renderer.
Before we jump into the implementation though, let us first discuss how we can know if the cone has hit the scene:
To make things short, a cone intersects with an SDF if the distance calculated from the SDF is less than the radius of the cone.
And so, as we march, we need to constantly keep track of the radius of our cone.
We will discuss how the radius is calculated mathematically, but let’s first start with the different approaches for cone marching.
This approach is discussed in this PDF where the idea is to run multiple passes on the GPU decreasing the size of the cone we are marching every step of the way. Theoretically, this is an ideal approach where you can technically continue decreasing the size of the cone until it is less than or equal to the size of your pixel. However, you will potentially only need 3 to 4 passes (according to the PDF).
In my experience, this approach is complicated in terms of implementation and the FPS gain isn’t worth the complexity it has compared to the simple approach we will discuss soon. However, if you are interested in seeing this in action, you can take a look at my Unity Fractal renderer FractiX where I implemented this approach using compute shaders.
This is a more straightforward implementation that aligns nicely with our previous ray marching implementation where we simply need to extend our shaders to account for cone marching.
More specifically, we will cone-march in our vertex shader and then give our result to our fragment shader where we will ray-march again but with a starting distance and steps.
So, the changes to our renderer will be as follows:
When creating our ray marching plane (which we’ll now call marching plane), we need to account for how many vertices we will use.
If we have too many vertices (or segments in our plane), then the FPS increase wouldn’t be that much as the number of cones we are marching is closer to the number of pixels on the screen.
If we don’t have a lot of vertices, then we will also not see an FPS increase since after we pass our cone marching distance to the fragment shader, we will still need to ray-march for a longer distance (making the optimization obsolete and not useful for our purposes).
So a balance needs to be met, and I have found that for most scenes, 32 segment subdivisions seem enough. So in our three.js code, we create the plane geometry as follows:
let marchingPlaneGeo = new THREE.PlaneGeometry(1, 1, Math.trunc(camera.aspect*32), 32)
Moreover, the reason why we are multiplying by the aspect ratio of our camera is to keep our vertices evenly spaced (or square). This will avoid some artifacts that might occur if the screen is not 1 by 1.
There is some more information that we will need to send with our uniforms from the CPU to the GPU.
First is the tan of the camera field of view divided by two. This is needed as it is what we will need to use to find the radius of our cone as we are marching.
Second is the number of subdivisions we have for our screen. In our case, that is 32 but you can try and test other subdivision values.
Now that we have everything needed to cone-march, we just have to update our shaders to account for that. Let’s first define our cone marching function:
You can first notice that we have added a unique struct. This is because we will need to pass our distance and steps taken to our fragment shader. In order to keep our code readable, I am using a struct, though it is not needed.
Moreover, you can see that the cone marching function is almost the same as a ray marching one. But, we need to keep track of the current radius of the cone. We are also dividing the radius by how many subdivisions we are making. This is so that each cone we are marching is occupying only the vertex it's on.
Lastly, our break condition checks if the current distance is less than our current radius with some padding (unlike ray marching which checks if it is less than the epsilon value). This is the mathematical approach of finding an intersection between the cone and the scene (as mentioned above) and the padding is there to avoid some artifacts that usually occur at the edges of the SDF. Feel free to experiment here and use different padding values (according to your specific SDF).
With this, we can also change our ray marching function to account for the use of a starting distance and steps already taken.
Now, we can update our fragment and vertex shaders to use these new functions.
Our vertex shader will now cone-march and send that data to our fragment shader:
After we cone march, we send this information to our fragment shader which will run per pixel ray marching.
With our new ray marching function, we simply will need to pass our distance and steps taken from the vertex shader and essentially march less which makes us end up with fewer computations of SDFs.
And that is pretty much it! You now have a cone marching scene in three.js
If you are interested in the code only, you can check out the following CodePen:
Though ray marching is unique in its ability to render fractals and other complex objects, it falls short in terms of efficiency since we always end up marching many similar rays: leading to repeated calculations.
However, with cone marching, we can bundle these rays together, march them as a cone, and then use our resultant value to march the rays independently. This results in quite an increase in FPS which can even reach +50FPS in some cases.
I hope you found this article useful,
Feel free to contact me if you have any questions.
Cheers!