"Raytraced primitives in unity3d (part 2): lit sphere"

Posted on 2016-03-25 in shaders

So. Here's what I got at the moment:

lit sphere with several lights

It is the next iteration of shader from before. Here's the mesh geometry of this object:

lit sphere with several lights and wireframe

So, good news:

I got lighting. Attenuation isn't suported (yet), but it interacts with unity's point and directional lights, reads their colors, and also grabs ambient value of the scene.
Oh, right. Also, I plugged in a texture. (earth texture courtesy of NASA).

Now, not so god news:
There's a very ugly seam, which appears to be going through meridian 0. Seam going through africa

Not sure if it is result of texture being downscaled, or it is a result of method I used for texture coordinate calculations, or both. It doesn't matter now, I'll look at this later. Probably.

I haven't yet plugged in unity shadows. Mostly because those shadows heavily rely on macro, and macro expects that I'm passing in original mesh, not a geometry with depth calculated on per-fragment basis. As a result, shadows are being cast over original geometry, not over raytraced geometry, which sorta defeats the point. Shadow error

I'm not exactly sure if I should pursue the "way of unity shadows", or just make shadows raytraced too. Raytraced shadows would require, of course, that entire scene is raytraced.

Also, on multiple occasions I noticed noise. Apparently in forward rendering mode per-light passes somehow end up calculating depth differently and that results in noise, and black/white specks over the model. That one is actually a bit disappointing. I experienced those specks few times, then they magically disappeared.

Either way, here are the shaders. I split shaders into three parts:

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

            #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 vertex vert
            #pragma fragment frag

            #define RAYCAST_USE_LIGHT
            #define RAYCAST_USE_SPECULAR
            //#define RAYCAST_USE_ATTENUATION
            #include "SphereRender.cginc"
            ENDCG
        }
    }
    //FallBack "Diffuse"
}

Raycast.cging:

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

SphereRender.cginc:

#include "UnityCG.cginc"
#include "Raycast.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"


struct appdata{
    float4 vertex : POSITION;
};

struct v2f{
    float4 vertex : SV_POSITION;
    float3 rayDir: TEXCOORD0;
    float3 rayPos: TEXCOORD1;
#ifdef RAYCAST_USE_ATTENUATION
    LIGHTING_COORDS(2, 3)
#endif
};

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;
#ifdef RAYCAST_USE_ATTENUATION
    TRANSFER_VERTEX_TO_FRAGMENT(o);
#endif
    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;

    float4 uv = calculateSphereTexcoordsFromNormal(sphereContact.n);
    float4 texColor = tex2D(_MainTex, uv.xy);

    FragOut result;
    float4 col = 0.0;
    float4 baseColor = texColor;

#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);
    //float3 lightDir = normalize(i.lightDir);
    float lightAtten = 1.0;
#ifdef RAYCAST_USE_ATTENUATION
    {
        UNITY_LIGHT_ATTENUATION(tmpAtten, i, worldPos);
        lightAtten = tmpAtten;
    }
#endif
    float lightDot = dot(lightDir, sphereContact.n);
    float lightFactor = max(0.0, lightDot);
    col = col + baseColor * lightFactor * _LightColor0 * lightAtten;
    //float attenuation = LIGHT_ATTENUATION(i);
#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;
#endif
#endif
#ifdef RAYCAST_USE_UNLIT_TEXTURE
    col = baseColor;
#endif

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

    return result;
}

Some portions of the code are disabled, so watch out for those.

I split shader into three parts, because this time I'm using multi-pass lighting, and I do not want to copy large body of code between different passes. That's why I put bulk of the code into separate file and commented out relevant parts with ifdefs.

Now, unity actually have keywords, which could be used instead of ifdefs, except that, there's limit on total number of those, and I don't exactly need toggleable features in the material. Only different portions of code added/removed when I need them. By the way, situations like this make me wish common lisp was used instead of c-style languages for the purposes of shader production. Oh, defmacro, how I miss thee...
Anyway.

I've used old boring phong lighting model ( n dot L for diffuse, pow(e dot r, specPower) for specular ), and decided not to bother with bumpmaps, normalmaps, glossmaps, specularmaps, or anything like that. The material has one specular color, diffuse color, diffuse texture, and one specular power value per object. That's good enough for testing purposes.

When you need multi-pass forward lighting in unity, you need to define "ForwardBase" pass for the base pass and the very first directional light, "ForwardAdd" pass that will be used for every single light after that. Secondary pass does not write to depth buffer and is rendered with additive blending. For shadows, you need to add "ShadowCaster" pass, except unity can do that automatically, if you add "Fallback "Diffuse"" to your shader... which of course, is not suitable in this situation for our shader, because our sphere is secretly a box.

I moved different portion of lighting calculations into separate blocks, so it would be easier to switch them off. I could probably use less blocks (merge specular block with light block), but whatever.

Texture coordinates are calculated using basic trigonometry subroutines - acos and atan2. They also had to be flipped.

Now that's all for today. I wonder if I should continue tinkering with unity shadows, or just move onto next primitives, so I can finally do something interesting, like this:

Only with lights, shadows and raytraced reflections.
On other hand it would be fun to seamlessly integrate raytraced object with unity shading. Decisions, decisions....