Skip to content

A rather small unity project where I experimented with raymarching and unity's compute shader

Notifications You must be signed in to change notification settings

Slowlor1ss/Raymarching

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

29 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Raymarching

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.

How to open

(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.

EnterSafeMode-png

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" ;)


About raymarching

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;
}

Shading and normals

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

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.

csg-wiki

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);
}

Result

Blending-balls Testing-Scene


Sources


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


Back to top

About

A rather small unity project where I experimented with raymarching and unity's compute shader

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages