This was an experimental project to learn more about raymarching and unity's compute shader
therefore the code isn't my cleanest nor properly optimized, and is more a project made for the fun of it.
and you'll also see some code from me first testing out unity's compute shader.
- About raymarching
- Sphere tracing snippet
- Shading and normals
- Constructive solid geometry
- My result
- Sources
(Click to open/close)
To run this project you can simply open the assets folder in the unity engine.
Note: when opening the project you might get a notification saying there are error's in the project
these errors are simply because of some project settings and is fairly easy to fix.
In unity to get the size of struct using "sizeof" you have to mark the code as unsafe like so:
struct ShapeData
{
public static unsafe int GetSize()
{
return sizeof(ShapeData);
}
}
and that exact piece of code is what causes the error when opening the file for the first time.
You can easily fix this by allowing unsafe code for that project like so:
After that all errors will be gone and
I ensure you none of the code is actually "unsafe" ;)
When I started this project I was happily surprised to find out it had a lot of similarities to raytracing and having made a raytracer in the past made this project go a lot easier.
Just as in raytracing, we have a position for the camera, put a grid in front of it, send rays from that camera position through each point in the grid, with each grid point corresponding to a pixel in the output image.
Now the big difference with raytracing is how we're finding the intersection between the view ray and the scene.
With raytracing, we usually have some primitive geometries (like triangles, spheres, and quats) and do a series of intersection tests seeing when and where the ray intersects with the geometry, if at all.
In raymarching, we use signed distance functions to find intersections between the ray and geometry, just like in raytracing we do this for every pixel but here instead of simply getting a boolean that says that you intersected with the geometry you get a value with the distance from the shape and by increasing the size of the ray you will get closer and closer to the geometry until the value gets really small and thus you have hit the geometry.
For increasing the size of this ray you could simply increment it by small steps until the distance gets really small, but luckily some clever people have come up with a better and way more optimized way of doing this; called “sphere tracing”. Instead of incrementing by a tiny step every time, we take the maximum step we know is safe without going through the surface; in other words, we step by the distance to the surface, which the SDF provides us! (see picture below)
Interestingly GPU Gems 2 shows how this can be used in distance mapping and also compaires it to raytracing
Wich in my code looks something like this: (slightly simplyfied (removed code for the color of the pixel but you can find all code here))
//returns the shortest distance from the eyepoint that the ray has to travel
//to get really close to the object, but if nothing had been found within the view range we just return max
float ClosestPointToSurface(float3 eye, float3 marchingDirection)
{
float depth = MinDst;
for (int i = 0; i < MaxMarchingSteps; i++)
{
float result = SceneSDF(eye + depth * marchingDirection);
if (result <= Epsilon)
{
return depth;
}
depth += result;
if (depth >= MaxDst)
{
return MaxDst;
}
}
return MaxDst;
}
Raymarching also has an interesting way of finding surface normals using signed distance functions, for finding the normals something called the gradient is used, in simple terms this is a function that tells you at a given point in what direction to move to most rapidly increase the distance from the geometry. Using signed distance functions, gives us a pretty good estimation of our surface normal.
Which again in my code looks like this:
float3 EstimateNormal(float3 p)
{
return normalize(float3(
SceneSDF(float3(p.x + Epsilon, p.y, p.z)).w - SceneSDF(float3(p.x - Epsilon, p.y, p.z)),
SceneSDF(float3(p.x, p.y + Epsilon, p.z)).w - SceneSDF(float3(p.x, p.y - Epsilon, p.z)),
SceneSDF(float3(p.x, p.y, p.z + Epsilon)).w - SceneSDF(float3(p.x, p.y, p.z - Epsilon))
));
}
Shape operations as I call them or Constructive solid geometry as a more technical term is a way of creating complex geometry using boolean operations on wimple shapes simple shapes.
I think this image from Wikipedia is a nice visual explanation of this.
And this is actually really simple to acheave using ray marching
// None
if (operation == 0)
{
if (dstB < dstA)
{
dst = dstB;
colour = colourB;
}
}
// Blend
else if (operation == 1)
{
float4 blend = Blend(dstA, dstB, colourA, colourB, blendStrength);
dst = blend.w;
colour = blend.xyz;
}
// Cut
else if (operation == 2)
{
// max(a,-b)
if (-dstB > dst)
{
dst = -dstB;
colour = colourB;
}
}
// Mask
else if (operation == 3)
{
// max(a,b)
if (dstB > dst)
{
dst = dstB;
colour = colourB;
}
}
Only the blend has a bit more complex code to both create a nice blend and have a smooth transition between colours here's my code snippet but for more information on how it works I recommend this site that gives a very nice explanation of this.
float4 Blend(float a, float b, float3 colA, float3 colB, float k)
{
//see mix factor https://www.iquilezles.org/www/articles/smin/smin.htm
const float h = max(k - abs(a - b), 0.0) / k;
const float m = h * h * 0.5;
const float s = m * k * (1.0 / 2.0);
float2 result = (a < b) ? float2(a - s, m) : float2(b - s, 1.0 - m);
float blendDst = result.x;
float3 blendCol = lerp(colA, colB, result.y);
return float4(blendCol, blendDst);
}
https://jamie-wong.com/2016/07/15/ray-marching-signed-distance-functions/#the-raymarching-algorithm
https://www.iquilezles.org/www/articles/smin/smin.htm
https://en.wikipedia.org/wiki/Gradient
https://en.wikipedia.org/wiki/Constructive_solid_geometry
http://iquilezles.org/www/articles/distfunctions/distfunctions.htm
https://developer.nvidia.com/gpugems/gpugems2/part-i-geometric-complexity/chapter-8-pixel-displacement-mapping-distance-functions
https://www.youtube.com/watch?v=BrZ4pWwkpto