"Raytraced primitives in unity3d (part 5): distance fields"

Posted on 2016-11-08 in shaders

After a long pause, I decided to give distance fields a try. Here's result: scene

This is a CSG-based shape. Sphere minus torus minus cube. Positions of the primitives are hardcoded.

Here's the code:

DistanceFields.shader:

Shader "Unlit/RaycastDistanceFields"{
    Properties  {
        _MainTex ("Texture", 2D) = "white" {}
        _Color ("Color", Color) = (1, 1, 1, 1)
        _SpecularColor ("Specular Color", Color) = (1, 1, 1, 1)
        _SpecularPower ("Specular power", Range(1.0, 100.0)) = 25.0
    }
    SubShader   {
        Tags { "RenderType"="Opaque" }
        LOD 100

        //basepass
        Pass{
            Tags {"LightMode" = "ForwardBase"}
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fwdbase_fullshadows

            #define RAYCAST_USE_AMBIENT
            #define RAYCAST_USE_LIGHT
            #define RAYCAST_USE_SPECULAR
            #define RAYCAST_USE_ATTENUATION
            #include "DistanceFields.cginc"
            ENDCG
        }

        //light pass
        Pass{
            Tags {"LightMode" = "ForwardAdd"}
            Blend One One
            ZWrite Off
            ZTest LEqual
            CGPROGRAM
            #pragma multi_compile_fwdadd_fullshadows
            #pragma vertex vert
            #pragma fragment frag

            #define RAYCAST_USE_LIGHT
            #define RAYCAST_USE_SPECULAR
            #define RAYCAST_USE_ATTENUATION
            #include "DistanceFields.cginc"
            ENDCG
        }

        Pass{
            Tags {"LightMode" = "Shadowcaster"}
            ZWrite On
            ZTest LEqual
            CGPROGRAM
            #pragma multi_compile_shadowcaster
            #pragma vertex vert
            #pragma fragment frag

            #define RAYCAST_NO_TEXTURE
            #include "DistanceFields.cginc"
            ENDCG
        }
    }
}

DistanceFields.cginc:

#include "UnityCG.cginc"

#include "Raycast.cginc"

struct appdata{
    float4 vertex : POSITION;
};

struct v2f{
    float4 vertex : SV_POSITION;
    float3 rayDir: TEXCOORD0;
    float3 rayPos: TEXCOORD1;
};

sampler2D _MainTex;
float4 _MainTex_ST;
float4 _Color;
float4 _SpecularColor;
float _SpecularPower;

v2f vert (appdata v){
    v2f o;
    o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
    float3 worldPos = mul(_Object2World, v.vertex);
    o.rayPos = worldPos;
    o.rayDir = getViewRay(worldPos);
    return o;
}

struct DistanceFieldParams{
    float3 spherePos;
    float3 sphereRadius;
    float4 sphereColor;
    float3 aabbCenter;
    float3 aabbSize;
    float4 aabbColor;
    float torusR1;
    float torusR2;
};

struct DistanceFieldOutput{
    float4 c;
    float3 n;
    float t;
};

float getSphereDistance(float3 queryPoint, float3 spherePos, float sphereRadius){
    return length(queryPoint - spherePos) - sphereRadius;
}

float getAabbDistance(float3 queryPoint, float3 center, float3 size){
    //return queryPoint.x;
    float3 diff = abs(queryPoint - center) - size*0.5;
    return min(max(diff.x, max(diff.y, diff.z)), 0.0) + length(max(diff, 0.0));
}

float distanceFieldUnion(float field1, float field2){
    return min(field1, field2);
}

float distanceFieldIntersect(float field1, float field2){
    return max(field1, field2);
}

float distanceFieldSubtract(float field1, float field2){
    return max(field1, -field2);
}

float torusXyDistanceField(float3 queryPos, float r1, float r2){
    float2 torusCoord = float2(length(queryPos.xy) - r1, queryPos.z);
    return length(torusCoord) - r2;
}

float getDistanceField(float3 queryPos, DistanceFieldParams params){
    return 
    distanceFieldSubtract(
    distanceFieldSubtract(
        getSphereDistance(queryPos, params.spherePos, params.sphereRadius),
        torusXyDistanceField(queryPos, params.torusR1, params.torusR2)),
    getAabbDistance(queryPos, params.aabbCenter, params.aabbSize));

    //return torusXyDistanceField(queryPos, params.torusR1, params.torusR2);
    /*
    return distanceFieldIntersect(
        ,
        getSphereDistance(queryPos, params.spherePos, params.sphereRadius)
    );*/
}

float3 getDistanceFieldNormal(float3 pos, DistanceFieldParams params, float delta){
    float3 result;
    result.x = getDistanceField(pos + float3(delta, 0, 0), params) - getDistanceField(pos - float3(delta, 0, 0), params);
    result.y = getDistanceField(pos + float3(0, delta, 0), params) - getDistanceField(pos - float3(0, delta, 0), params);
    result.z = getDistanceField(pos + float3(0, 0, delta), params) - getDistanceField(pos - float3(0, 0, delta), params);
    return normalize(result);   
}

DistanceFieldOutput raymarchDistanceField(float3 rayPos, float3 rayDir, DistanceFieldParams params){
    DistanceFieldOutput result;
    result.t = -1.0f;
    result.c = float4(1, 1, 1, 1);
    result.n = float3(0, 1, 0);

    float lastDist = getDistanceField(rayPos, params);
    if (lastDist < 0)
        return result;


    const float normalDelta = 0.0001;
    const float minStep = 0.0001;
    float lastT = 0.0f;
    for(int i = 0; i < 64; i++){
        //float distanceStep = max(lastDist, 0.1);
        float t = lastT + max(lastDist, minStep);
        float dist = getDistanceField(rayPos + t * rayDir, params);
        if (dist < 0){
            result.t = lerp(lastT, t, lastDist / (lastDist - dist));
            result.n = getDistanceFieldNormal(rayPos + rayDir * result.t, params, normalDelta);         
            return result;
        }
        else{
            lastT = t;
            lastDist = dist;
        }
    }

    return result;
}

RaytracedFragOut frag (v2f i){
    float3 rayDir = normalize(i.rayDir);
    float3 rayPos = i.rayPos;

    DistanceFieldParams params;
    params.spherePos = float3(0.0, 0.5, 0.0);
    params.sphereRadius = 1.0;
    params.aabbCenter = float3(0.0, 1.0, 0.6);
    params.aabbSize = float3(5.0, 2.0, 1);
    params.torusR1 = 1.0;
    params.torusR2 = 0.25;

    DistanceFieldOutput fieldData = raymarchDistanceField(rayPos, rayDir, params);
    clip(fieldData.t);
    float3 worldPos = rayPos + rayDir*fieldData.t;

    float4 col = 0.0;

    float4 baseColor = fieldData.c;

#ifdef RAYCAST_USE_AMBIENT
    float4 ambColor = UNITY_LIGHTMODEL_AMBIENT;
    col = col + baseColor * ambColor;
#endif
#ifdef RAYCAST_USE_LIGHT
    float3 lightDir = _WorldSpaceLightPos0.xyz - worldPos*_WorldSpaceLightPos0.w;
    lightDir = normalize(lightDir);

    float shadowAtten = getShadowAttenuation(worldPos);
    float lightAtten = getLightAttenuation(worldPos);
    float lightDot = dot(lightDir, fieldData.n);
    float lightFactor = max(0.0, lightDot);
    col = col + baseColor * lightFactor * _LightColor0 * shadowAtten * lightAtten;
#ifdef RAYCAST_USE_SPECULAR
    float3 reflectedLight = reflect(lightDir, fieldData.n);
    float specFactor = max(dot(reflectedLight, rayDir), 0.0);
    specFactor = pow(specFactor, _SpecularPower);
    col = col + _LightColor0 * lightAtten * _SpecularColor * specFactor * shadowAtten;
#endif
#endif
#ifdef RAYCAST_USE_UNLIT_TEXTURE
    col = baseColor;
#endif

    return computeOutputFragment(worldPos, col);
}

Other files remained unchanged sincce previous post

The interesting thing about distance fields is that transformation, scaling and csg geometry is incredibly easy to implement. You can define any kind of shape easily via substractions, unions and differences... which are all based around simple min/max functions. Computing normals is easy too, you can just sample the field data based on +- xyz coordinate from original point.

However, surprisingly distance fields have some disadvantage compared to normal primitive-based rayccasting: because shape of the figure we are raymarching is not known, distance field tracing algorithm slows down and beccomes more computationally expensive when ray runs in parallel to some surface.

See, in case of mathematical primtive we can determine whether we hit it with our ray or not in one query. Distance field can only tell us how far is the closest surface from a point, but not in which direction this surface exists. Meaning, we can adjust query position more or less safely by moving the point by that amount, but then we'll need to run another query. And if our ray is running in parllel (or almost in parllel) to a infinite plane, Raymarching alogrithm will keep sampling the field again and again and again.

This can be avoided by defining minimum step of a distance field, but in this case it is possible that the raymarcher will "shoot through" some thin object, and edges of some objects will become blured.