"Raytraced primitives in unity3d (part 4.1): code cleanup"

Posted on 2016-04-13 in shaders

This is a basic update of previous example.

It is largely the same thing as before:

scene

However, I cleaned up the code.

Basically, I'm thinking about moving from mathematically defined primitives (meaning every figure is defined by a formula) to distant fields which have somewhat simpler definition..

Distant field, basically, defines distance to any nearest "anything" for every point of 3d space. If the point is already within some object, the value becomes negative, and stores depth/distance from the nearest surface. This kind of data is very useful if you're going to render voxels, also you can compute normals automatically using it.

Either way, I'll look into distant fields properly later. For now, the promised code cleanup.

RaycastObjectShader.shader:

Shader "Unlit/RaycastObjectShader"{
    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 "SphereRender.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 "SphereRender.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 "SphereRender.cginc"
            ENDCG
        }
    }
}

LightAndShadow.cginc:

#include "Lighting.cginc"
#include "AutoLight.cginc"

float getLightAttenuation(float3 worldPos){
#if defined(POINT)
    {
        unityShadowCoord3 lightCoord = mul(_LightMatrix0, float4(worldPos, 1.0)).xyz;
        float result = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
        return result;
    }   
#elif defined(SPOT)
    {
        unityShadowCoord4 lightCoord = mul(_LightMatrix0, float4(worldPos, 1.0));
        float result = (lightCoord.z > 0) * UnitySpotCookie(lightCoord) * UnitySpotAttenuate(lightCoord.xyz);
        return result;
    }
#elif defined(DIRECTIONAL)
    {
        return 1.0;
    }
#elif defined(POINT_COOKIE)
    {
        unityShadowCoord3 lightCoord = mul(_LightMatrix0, float4(worldPos, 1.0)).xyz;
        float result = tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL * texCube(_LightTexture0, lightCoord).w;
        return result;
    }
#elif defined(DIRECTIONAL_COOKIE)
    {
        unityShadowCoord2 lightCoord = mul(_LightMatrix0, float4(worldPos, 1.0)).xy;
        float result = tex2D(_LightTexture0, lightCoord).w;
        return result;
    }
#else
    return 1.0;
#endif
}

float getShadowAttenuation(float3 worldPos){
#if defined(SHADOWS_CUBE)
    {
        unityShadowCoord3 shadowCoord = worldPos - _LightPositionRange.xyz;
        float result = UnitySampleShadowmap(shadowCoord);
        return result;
    }
#elif defined(SHADOWS_SCREEN)
    {
    #ifdef UNITY_NO_SCREENSPACE_SHADOWS
        unityShadowCoord4 shadowCoord = mul( unity_World2Shadow[0], worldPos);  
    #else
        unityShadowCoord4 shadowCoord = ComputeScreenPos(mul(UNITY_MATRIX_VP, float4(worldPos, 1.0)));
    #endif
        float result = unitySampleShadow(shadowCoord);
        return result;
    }       
#elif defined(SHADOWS_DEPTH) && defined(SPOT)
    {       
        unityShadowCoord4 shadowCoord = mul(unity_World2Shadow[0], float4(worldPos, 1.0));
        float result = UnitySampleShadowmap(shadowCoord);
        return result;
    }
#else
    return 1.0;
#endif  
}

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

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

    float3 spherePos = float3(0.0, 2.0, 0.0);
    float sphereRadius = 1.0;

    float t = getDistanceToSphere(rayPos, rayDir, spherePos, sphereRadius);
    if (t < 0)
        discard;

    float3 worldPos = rayPos + rayDir*t;

    ContactInfo sphereContact = calculateSphereContact(worldPos, spherePos, sphereRadius);

    float4 col = 0.0;
#ifdef RAYCAST_NO_TEXTURE
    float4 baseColor = 1.0;
#else
    float4 uv = calculateSphereTexcoordsFromNormal(sphereContact.n);
    float4 texColor = tex2D(_MainTex, uv.xy);
    float4 baseColor = texColor;
#endif

#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, sphereContact.n);
    float lightFactor = max(0.0, lightDot);
    col = col + baseColor * lightFactor * _LightColor0 * shadowAtten * lightAtten;
#ifdef RAYCAST_USE_SPECULAR
    float3 reflectedLight = reflect(lightDir, sphereContact.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);
}

Raycast.cginc:

float getDistanceToSphere(float3 rayPos, float3 rayDir, float3 spherePos, float sphereRadius){
    float3 diff = spherePos - rayPos;

    float r2 = sphereRadius;

    float3 projectedDiff = dot(diff, rayDir)*rayDir;
    float3 perp = diff - projectedDiff;
    float perpSqLen = dot(perp, perp);
    float perpSqDepth = r2 - perpSqLen;

    if (perpSqDepth < 0)
        return -1.0;

    float dt = sqrt(r2 - perpSqLen);
    float t = sqrt(dot(projectedDiff, projectedDiff)) - dt;
    return t;
}

struct ContactInfo{
    float3 relPos;
    float3 n;
};

ContactInfo calculateSphereContact(float3 worldPos, float3 spherePos, float sphereRadius){
    ContactInfo contact;
    contact.relPos = worldPos - spherePos;
    contact.n = normalize(contact.relPos);
    return contact;
}

float4 calculateSphereTexcoordsFromNormal(float3 n){
    float4 result = 0.0;

    float u = 0.0;
    float v = 0.0;

    float pi = 3.14159265358979323846264338327;
    v = 1.0 - acos(n.y)/pi, 0.0, 1.0;
    u = 1.0 - atan2(n.x, n.z)/(2.0*pi);

    result.x = u;
    result.y = v;

    return result;
}

float4 calculateSphereTexcoords(float3 worldPos, float3 spherePos, float sphereRadius){
    float3 dir = normalize(worldPos - spherePos);
    return calculateSphereTexcoordsFromNormal(dir);
}

float calculateFragmentDepth(float3 worldPos){
    float4 depthVec = mul(UNITY_MATRIX_VP, float4(worldPos, 1.0));
    return depthVec.z/depthVec.w;
}

struct RaytracedFragOut{
    float4 col: SV_Target;
    float depth: SV_Depth;
};

float3 getRayToCamera(float3 worldPos){
    if (unity_OrthoParams.w > 0){
        return -UNITY_MATRIX_V[2].xyz;;
    }
    else
        return worldPos  - _WorldSpaceCameraPos;
}

#if !defined(RAYCAST_WITHIN_SHADOWCASTER_PASS) \
    && (defined(SHADOWS_DEPTH)||defined(SHADOWS_NATIVE)||defined(SHADOWS_CUBE)) \
    && !defined(SPOT) \
    && !defined(POINT) \
    && !defined(DIRECTIONAL) \
    && !defined(POINT_COOKIE) \
    && !defined(DIRECTIONAL_COOKIE) 

#define RAYCAST_WITHIN_SHADOWCASTER_PASS

#endif

float4 calculateCubeShadowDepth(float3 worldPos){
    float3 diff = worldPos - _LightPositionRange.xyz;
    float depth = (length(diff) + unity_LightShadowBias.x) * _LightPositionRange.w;
    return UnityEncodeCubeShadowDepth(depth);
}

float calculateShadowDepth(float3 worldPos){
    float4 projPos = mul(UNITY_MATRIX_VP, float4(worldPos, 1));
    projPos = UnityApplyLinearShadowBias(projPos);
    return projPos.z/projPos.w;
}

RaytracedFragOut computeOutputFragment(float3 worldPos, float4 col){
    RaytracedFragOut result;
#if defined(RAYCAST_WITHIN_SHADOWCASTER_PASS)
    #ifdef SHADOWS_CUBE
        float4 shadowDepth = calculateCubeShadowDepth(worldPos);
        result.depth = calculateFragmentDepth(worldPos);
        result.col = shadowDepth;
    #else
        float shadowDepth = calculateShadowDepth(worldPos);
        result.depth = shadowDepth;
        result.col = shadowDepth;
    #endif
#else
    result.depth = calculateFragmentDepth(worldPos);
    result.col = col;
#endif
    return result;
}

#include "LightAndShadow.cginc"

/*
For unknown reason, this is order-dependent. 
Moving getRayToLight before lighting functions breaks the shader.
*/

float3 getRayToLight(float3 worldPos){
    float3 result;
    if (_WorldSpaceLightPos0.w > 0){
        result = worldPos.xyz - _WorldSpaceLightPos0.xyz;
    }
    else{
#if defined(DIRECTIONAL)
        if ((UNITY_MATRIX_P[3].x == 0.0) 
                && (UNITY_MATRIX_P[3].y == 0.0) && (UNITY_MATRIX_P[3].z == 0.0)){
            result = -UNITY_MATRIX_V[2].xyz;
        }
        else{
            result = getRayToCamera(worldPos);
        }
#else
        result = getRayToCamera(worldPos);
#endif
    }
    return result;
}

float3 getViewRay(float3 worldPos){
#if defined(RAYCAST_WITHIN_SHADOWCASTER_PASS)
    return getRayToLight(worldPos);
#else
    return getRayToCamera(worldPos);
#endif
}

See? Much cleaner.

I moved bulk of the repeating routines into Raycast.cginc. That made SphereRender.cginc significantly simpler, and this will also allow easier integration of alternative figures, scenes or the distant fields. I also exterminated the need for manually specifying whether we're within shadowcaster pass or not. See RAYCAST_WITHIN_SHADOWCASTER_PASS macro.

There was an unpleasant surprise.

I found out that moving around #include's for Lighting.cginc and AutoLight.cginc (which are provided by unity) can affect the rendered result and break the shader.

Basically, a function that determines direction towards ray - getRayToLight breaks for directional lights if it is placed before those includes. I have not looked into this, but what happens is that I lose shadow bias, so as a result shadows become very noisy.


This is the end of today's post.