"Raytraced primitives in unity3d (part 3): lights, shadows and attenuations"

Posted on 2016-04-01 in shaders

I originally planned to finish and post this on monday. But stuff happened. So here we go.

Here's what I got at the moment: lit scene with point and spot lights

Good news: I got point and spot lights working. Raytraced sphere receives shadows from external sources. Oh, right, cookies and attenuation now works as well.
Bad news: Directional light refuses to comply. Apparently this one needs proper "shadowcaster" pass.

directional light artifact

Here are the shaders, I'll explain things later:

RaytcastObjectShader.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"}
            CGPROGRAM
            #pragma multi_compile_fwdadd_fullshadows
            #pragma vertex vert
            #pragma fragment frag

            #define RAYCAST_NO_TEXTURE
            #include "SphereRender.cginc"
            ENDCG
        }
        */
    }
    //FallBack "Diffuse"
}

SphereRender.cginc:

#include "UnityCG.cginc"
#include "Raycast.cginc"
#include "LightAndShadow.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 = worldPos - _WorldSpaceCameraPos;
    return o;
}

struct FragOut{
    float4 col: SV_Target;//COLOR0;
    float depth: SV_Depth;//DEPTH;
};

FragOut frag (v2f i){// : SV_Target{
    // sample the texture
    float3 rayDir = normalize(i.rayDir);
    float3 rayPos = i.rayPos;
    //fixed4 col = 1.0;//tex2D(_MainTex, i.uv);

    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);
    //col.xyz = sphereContact.n;

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

#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

    result.depth = calculateFragmentDepth(worldPos);
    result.col = col;

    return result;
}

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

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  
}

As you can (probably) see, Raycast.cginc is unchanged since the last time. The brand new addition is LightAndShadow.cginc. That's where shadow-casting-attenuation-computing magic happens.

Now, the explanations.

Majority of lighting-related calculations are macros from within AutoLight.cginc, which is a part of unity standard shaders. The problem is, AutoLight.cginc is heavily geared towards being used in "normal" vertex/fragment shader. Meaning, a lot of macros (it is heavily macros-based) expect v parameter to be present (input vertex), or they want to operate on on struct that has _ShadowCoord field, etc. That... doesn't really work when your primitive is raytraced and you calculate the depth dynamically. So, in the end, I got fed up and wrote a modification that handles shadows and light attenuation. However, said modification still uses some portions of AutoLight.cginc. The reason for that is because Unity is multi-platform, and even though AutoLight.cginc looks like spaghetti code to me, it containss several platform definitions for sampling shadowmaps. I don't have hardware to test my own version of those, so when possible, we'd want to hijack and reuse existing shadow-related types and subroutines. The new cginc file is (obviously) named LightAndShadow.cginc.

Types of lights and shadows:

How many light types do you think there are, hmm? Shadow types? That was a rhetorical question. After reading through AutoLight.cginc, I found out that the best way seems to be separating shadow-related functionality into separate subroutines. That may not be the best way to go about it (haven't really investigated compiled output), but at this point I'm making pretty much a prototype, where the first priority is code readability (I can always optimize it later).

So, lights and shadows.

There are 5 types of lights.

  • Directional (DIRECTIONAL)
  • Point (POINT)
  • Spot (SPOT)
  • Point with cookie (POINT_COOKIE)
  • Directional with cookie (DIRECTIONAL_COOKIE)

There are three types of shadows:

  • Screen space shadows (SHADOWS_SCREEN), which have two different implementations (controlled by UNITY_NO_SCREENSPACE_SHADOWS macro)
  • Cube shadows (SHADOWS_CUBE) which appears to be mostly used by point lights.
  • Depth shadows (SHADOWS_DEPTH) which appears to be only used by spot lights (AutoLight.cginc reads as if they could theoretically be used by point lights too, but i haven't seen this happening).

At runtime unity apparently compiles multiple versions of the shaders (using pragma_multicompile keywords), depending on the situations (light present or not, shadow present or not, etc).

Now... In LightAndShadow.cginc I split attenuation calculation into two parts. Shadow and Lights. Both subroutines require world position of a vertex/pixel/whatever, and return floating point value indicating brightness of that point. getLightAttenuation() will return factor determined by light attenuation, while getShadowAttenuation() will return factor based on light's shadows. For final result, those two values are multiplied together. Shadow and light calculations usse unityShadowCoordX types which is defined (by macro) as either floatX or halfX type, depending on the platform.

Complications: See, with 3 types of shadows and 5 types of light, we need slightly different coordinates for shadows and attenuations. For example, cube shadows require float3 vector which is pretty much, worldPosition - cubemapCenter. Spot light require projective depth texture. For projective depth texture, you need float4 vector for coordinates, and we calculate those by multiplying light matrix with float4(worldPos, 1) which is a 4 component version of world position. Directional lights are a special case, because their shadows are stored in screen space. As far as I can tell, shadows for directional lights are awfully close to just prerendered screenspace shadowmaps, where unlit regions are white and lit ones are black. Which is why directional lights is not currently working - this kind of "shadow" requires proper shadow pass, which will pretty much only work for solid objects. Which is one of the reasons why unity cannot cast shadows onto transparent objects (well, it can be done, but not with directional lights).

Compared to shadows, computing light attenuation is fairly straightforward, because most of the time it is just a plain texture lookup (_LightTexture0, typically). Directional lights do not have attenuation, point lights appear to be using precomputed 1D texture with attenuation, spot lights have builtin functions for computing attenuation (UnitySpotCookie() and unitySpotAttenuate() - both require projective 4component coordinates), and directional cookie is pretty much a 2D texture.

And that is the end of this post.

Assuming I'll concentrate on making raytraced sphere properly CAST shadows into the world, I'll be investigating UnityShadowLibrary.cginc and UnityStandardShadow.cginc next. Or perhaps I should stop deconstructing unity built-in lights and move onto more interesting raytracing-related stuff. Hmm...