"Raytraced primitives in unity3d (part 4): casting and receiving shadows"
Posted on 2016-04-12 in shaders
Alright. I expected this to take less time, but things happened.
Here's what I have right now.
Point liights,
spot lights,
and directional lights
are supported and cast shadows.
Getting there, however, was unexpectedly difficult.
Here's the code. It is in need of some refactoring, but it works.
RaycastObject.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
#define RAYCAST_SHADOW_PASS
#include "SphereRender.cginc"
ENDCG
}
}
}
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;
#if defined(RAYCAST_SHADOW_PASS)
#define RAYCAST_RAY_FROM_LIGHT_POS
#ifdef SHADOWS_CUBE
float4 calculateCubeShadowDepth(float3 worldPos){
float3 diff = worldPos - _LightPositionRange.xyz;//_WorldSpaceLightPos0.xyz;
float depth = (length(diff) + unity_LightShadowBias.x) * _LightPositionRange.w;
return UnityEncodeCubeShadowDepth(depth);
}
#else
float calculateShadowDepth(float3 worldPos){
float4 projPos = mul(UNITY_MATRIX_VP, float4(worldPos, 1));
projPos = UnityApplyLinearShadowBias(projPos);
return projPos.z/projPos.w;
}
#endif
#endif
float3 getRayToCamera(float3 worldPos){
//return -UNITY_MATRIX_V[2].xyz;;
if (unity_OrthoParams.w > 0){
return -UNITY_MATRIX_V[2].xyz;;
}
else
return worldPos - _WorldSpaceCameraPos;
}
v2f vert (appdata v){
v2f o;
o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
float3 worldPos = mul(_Object2World, v.vertex);
o.rayPos = worldPos;
#if defined(RAYCAST_RAY_FROM_LIGHT_POS)
if (_WorldSpaceLightPos0.w > 0){
o.rayDir = worldPos.xyz - _WorldSpaceLightPos0.xyz;
//o.rayDir = getRayToCamera(worldPos);
}
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)){
o.rayDir = -UNITY_MATRIX_V[2].xyz;
}
else{
o.rayDir = getRayToCamera(worldPos);
}
#else
o.rayDir = getRayToCamera(worldPos);
#endif
}
#else
o.rayDir = getRayToCamera(worldPos);
#endif
return o;
}
struct FragOut{
float4 col: SV_Target;
float depth: SV_Depth;
};
FragOut frag (v2f i){
// 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
#if defined(RAYCAST_SHADOW_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;
}
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
}
Now, some explanations.
I got stuck for a while making direcitonal lights work. The only reason why I even managed to make it there, is Unity's frame debugger. If you aren't aware of it, you should give it a try. It shed some light on my shadowcasting issues.
The root of the problem is orthographic projections and special treatment of directional lights. First thing is, I eventually noticed that previous versions of the shader simply won't work if camera is orthographic. Why? Because calculation was not correct. In orthographic space all lines "shot" out of the camera must be parallel. The shader, however, calculated vector from camera to portal using vector substraction, which deformed raytraced primitive. This led to this bugfix:
float3 getRayToCamera(float3 worldPos){
if (unity_OrthoParams.w > 0){
return -UNITY_MATRIX_V[2].xyz;;
}
else
return worldPos - _WorldSpaceCameraPos;
}
This approach fixed orthographic issue, and the earth stopped trying to be cubical. If you wonder what the heck unity_OrthoParams
is, it is one of the built-in shader variables.
This approach did not, however, solve issues with directional lights. While the other lights - spot and point ones - worked as expected (well, more or less, because I found out another bug in point light depth calculation just before posting this), directional light resulted in jumbled shadows, or no shadows at all (which you can see in earlier posting).
For example, this:
^^^ this is one of the many variations of the strange visual artifacts I encountered. while trying to make directional lights work. Obviously at some point shadow wasn't being correctly computed, and I wasn't quite sure where did I mess up this time.
I was planning to abandon directional lights, until I remembered that "Frame Debugger" is a thing, and is worth checking out. Using the frame debugger, I managed to figure out what the unity does while rendering shadows, and what's wrong with my shaders.
First thing I noticed was wrong pragma. I noticed it becase parameters and macro definitions passed to standard shader (which are visible on the "Shader Properties" tab) did not match the ones used by unity standard shader in the same pass.
it turned out that I used #pragma multi_compile_fwdadd_fullshadows
instead of #pragma multi_compile_shadowcaster
in shadowcaster pass.
Next thing I noticed (in frame debugger) was that the sphere didn't have the right silhouette while being rendered into directional light's shadow buffer and unity renders directional light shadow in a very interesting way.
As it can be visible on earlier framedebugger screenshot, when directional light is being rendered, Unity:
- Creates depth texture.
- Renders objects being affected by directional lights - several times.
- Combines shadows into one screen-space shadow texture.
On a rendering preview you would ssee that with each rendering pass used in #2 part of the process, unity renders progressively bigger portion of the map.
I believe this is most likely an implementation of cascaded shadow maps I remember being introduced in one of GDC papers during early 2000s.
The interesting thing about those shadow passes is that they're rendered as spot lights (SPOT
macro is enabled) that use orthographic projection matrix, and the only correct way to get direction to the "light" is to pretty much use unity view matrix.
However, here's an important caveat that took me some time to figure out. Remember unity_OrthoParams
from earlier? This built-in shader variable indicates whether camera is in orthographic mode or not.
Except that while the directional lights are being rendered, it is not correctly set, so even if the scene is being rendered with ortho camera, the .w
value will not correctly reflect that. This issue turned out to be a problem, because
initial pass where unity rendered scene depth used very similar set of flags compared to a pass where unity was being used by lights. So fixing one broke another.
In the end I settled to this very ugly bugfix:
#if defined(RAYCAST_RAY_FROM_LIGHT_POS)
if (_WorldSpaceLightPos0.w > 0){
o.rayDir = worldPos.xyz - _WorldSpaceLightPos0.xyz;
//o.rayDir = getRayToCamera(worldPos);
}
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)){
o.rayDir = -UNITY_MATRIX_V[2].xyz;
}
else{
o.rayDir = getRayToCamera(worldPos);
}
#else
o.rayDir = getRayToCamera(worldPos);
#endif
See that looong UNITY_MATRIX_P[3]....
line? It tests whether the current projection matrix is orthographic or not. Directly. Not the best thing to do in a shader, but that was pretty much the only way to go about it. (I reported this as a bug, by the way).
At the very least, UNITY_MATRIX_P
is a uniform, so the branching shouldn't be going nuts because of it alone.
By the way, this is a line that returns forward vector for an orthographic camera:
return -UNITY_MATRIX_V[2].xyz;
This is the end of today's post.
Hopefully now that the issue of integrating raytraced primitives into unity lighting system is out of the way, I can get to testing more interesting stuff. Like complex objects, reflections, and the like.