Shadow Casting
So now we have a nicely rendered sphere on a quad that is lit, textured, and can be moved, scaled, and rotated around. So lets make it cast shadows! For that we’ll need to make a shadow caster pass in our shader. Luckily the same vertex shader can be reused for both passes, since all it does is create a quad and pass along the ray origin and direction. And those of course will be exactly the same for the shadows as it is for the camera, right? Then the fragment shader really just needs to output the depth, so you can delete all that pesky UV and lighting code.
Oh.
The ray origin and direction need to be coming from the light, not the camera. And the value we’re using for the ray origin is always the current camera position, not the light. The good news is that’s not hard to fix. We can replace any usage of _WorldSpaceCameraPos with UNITY_MATRIX_I_V._m03_m13_m23 which gets the current view’s world position from the inverse view matrix. Now as long as the shadows are rendered with perspective projections it should all work!
Oh. Oh, no.
Directional shadows use an orthographic projection.
Orthographic Pain
The nice thing with perspective projection and ray tracing is the ray origin is where the camera is. That’s really easy to get, even for arbitrary views, as shown above. For orthographic projections the ray direction is the forward view vector. That’s easy enough to get from the inverse view matrix again.
// forward in view space is -z, so we want the negative vector
float3 worldSpaceViewForward = -UNITY_MATRIX_I_V._m02_m12_m22;
But how do we get the orthographic ray origin? If you try and search online you’ll probably come across a bunch of examples that use a c# script to get the inverse projection matrix. Or abuse the current unity_OrthoParams which has information about the orthographic projection’s width and height. You can then use the clip space position to reconstruct the near view plane position the ray is originating from. The problem with these approaches is they’re all getting the camera’s orthographic settings, not the current light’s. So instead we have to calculate the inverse matrix in the shader!
float4x4 inverse(float4x4 m) {
float n11 = m[0][0], n12 = m[1][0], n13 = m[2][0], n14 = m[3][0];
float n21 = m[0][1], n22 = m[1][1], n23 = m[2][1], n24 = m[3][1];
float n31 = m[0][2], n32 = m[1][2], n33 = m[2][2], n34 = m[3][2];
float n41 = m[0][3], n42 = m[1][3], n43 = m[2][3], n44 = m[3][3]; float t11 = n23 * n34 * n42 - n24 * n33 * n42 + n24 * n32 * n43 - n22 * n34 * n43 - n23 * n32 * n44 + n22 * n33 * n44;// ... hold up, how many more lines are there of this?!
Ok, lets not do that. Those are just the first few lines of a >30 line function of increasing length and complexity. There’s got to be a better way.
The Nearly View Plane
As it turns out, you don’t need any of that. We don’t actually need the ray origin to be at the near plane. The ray origin really just needs to be the mesh’s position pulled back along the forward view vector. Just far enough to make sure it’s not starting inside the volume of the sphere. At least assuming the camera itself isn’t already inside the sphere. And a “near plane” at the camera’s position instead of the actual near plane totally fits that bill.
We already know the world position of the vertex in the vertex shader. So we could transform the world position into view space. Zero out the viewSpacePos.z, and transform back into world space. That results in a usable ray origin for an orthographic projection!
// transform world space vertex position into view space
float4 viewSpacePos = mul(UNITY_MATRIX_V, float4(worldPos, 1.0));// flatten the view space position to be on the camera plane
viewSpacePos.z = 0.0;// transform back into world space
float4 worldRayOrigin = mul(UNITY_MATRIX_I_V, viewSpacePos);// orthographic ray dir
float3 worldRayDir = worldSpaceViewForward;// and to object space
o.rayDir = mul(unity_WorldToObject, float4(worldRayDir, 0.0));
o.rayOrigin = mul(unity_WorldToObject, worldRayOrigin);
And really we don’t even need to do all that. Remember that super power of the dot() I mentioned above? We just need the camera to vertex position vector and the normalized forward view vector. We already have the camera to vertex position vector, that’s the original perspective world space ray direction. And we know the forward view vector by extracting it from the matrix mentioned above. Conveniently this vector comes already normalized! So we can remove two of the matrix multiplies in the above code and do this instead:
float3 worldSpaceViewPos = UNITY_MATRIX_I_V._m03_m13_m23;
float3 worldSpaceViewForward = -UNITY_MATRIX_I_V._m02_m12_m2;// originally the perspective ray dir
float3 worldCameraToPos = worldPos - worldSpaceViewPos;// orthographic ray dir
float3 worldRayDir = worldSpaceViewForward * -dot(worldCameraToPos, worldSpaceViewForward);// orthographic ray origin
float3 worldRayOrigin = worldPos - worldRayDir;o.rayDir = mul(unity_WorldToObject, float4(worldRayDir, 0.0));
o.rayOrigin = mul(unity_WorldToObject, float4(worldRayOrigin, 1.0));
* There is one minor caveat. This does not work for oblique projections (aka a sheared orthographic projection). For that you really do need the inverse projection matrix. Sheared perspective projections are fine though!
Light Facing Billboard
Remember how we’re doing camera facing billboards? And that fancy math to scale the quad to account for the perspective? We don’t need any of that for an orthographic projection. Just need to do view facing billboarding and scale the quad by only the object transform’s max scale. However maybe lets not delete all of that code quite yet. We can use the existing rotation matrix construction as is, just change the forward vector to be the negative worldSpaceViewForward vector instead of the worldSpacePivotToCamera vector.
A Point of Perspective
In fact now might be a good time to talk about how the spot lights and point lights use perspective projection. If we want to support directional lights, spot lights, and point light shadows we’re going to need to support both perspective and orthographic in the same shader. Unity also uses this pass to render the camera depth texture. This means we need to detect if the current projection matrix is orthographic or not and choose between the two paths.
Well, we can find out what kind of projection matrix we’re using by checking a specific component of it. The very last component of a projection matrix will be 0.0 if it’s a perspective projection matrix, and will be 1.0 if it’s an orthographic projection matrix.
bool isOrtho = UNITY_MATRIX_P._m33 == 1.0;// billboard code
float3 forward = isOrtho ? -worldSpaceViewForward : normalize(worldSpacePivotToCamera);
// do the rest of the billboard code// quad scaling code
float quadScale = maxScale;
if (!isOrtho)
{
// do that perfect scaling code
}// ray direction and origin code
float3 worldRayOrigin = worldSpaceViewPos;
float3 worldRayDir = worldPos - worldSpaceRayOrigin;
if (isOrtho)
{
worldRayDir = worldSpaceViewForward * -dot(worldRayDir, worldSpaceViewForward);
worldRayOrigin = worldPos - worldRayDir;
}o.rayDir = mul(unity_WorldToObject, float4(worldRayDir, 0.0));
o.rayOrigin = mul(unity_WorldToObject, float4(worldRayOrigin, 1.0));// don't worry, I'll show the whole vertex shader later
And now we have a vertex function that can correctly handle both orthographic and perspective projection! And nothing needs to change in the fragment shader to account for this. Oh, and we really can use the same function for both the shadow caster and forward lit pass. And now you can use an orthographic camera as well!
Shadow Bias
Now if you’d been following along, you’ll have a shadow caster pass outputting depth. But we’re not calling any of the usual functions a shadow caster usually has for applying offset. At the moment this isn’t obvious since we’re not self shadowing yet, but it’ll be a problem if we don’t fix it.
We’re not going to use the built in TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) macro for the vertex shader for this since we need to do the bias in the fragment shader. Luckily, there’s another benefit to doing the raytracing in object space. The first function that the shadow caster vertex shader macro calls assumes the position being passed to it is in object space! I mean, that makes sense, since it assumes it’s working on the starting object space vertex position. But this means we can use the biasing functions the shadow caster macros call directly using the position we’ve raytraced and they’ll just work!
Tags { "LightMode" = "ShadowCaster" }ZWrite On ZTest LEqualCGPROGRAM
#pragma vertex vert
#pragma fragment frag_shadow#pragma multi_compile_shadowcaster// yes, I know the vertex function is missingfixed4 frag_shadow (v2f i,
out float outDepth : SV_Depth
) : SV_Target
{
// ray origin
float3 rayOrigin = i.rayOrigin; // normalize ray vector
float3 rayDir = normalize(i.rayDir); // ray sphere intersection
float rayHit = sphIntersect(rayOrigin, rayDir, float4(0,0,0,0.5)); // above function returns -1 if there's no intersection
clip(rayHit); // calculate object space position
float3 objectSpacePos = rayDir * rayHit + rayOrigin; // output modified depth
// yes, we pass in objectSpacePos as both arguments
// second one is for the object space normal, which in this case
// is the normalized position, but the function transforms it
// into world space and normalizes it so we don't have to
float4 clipPos = UnityClipSpaceShadowCasterPos(objectSpacePos, objectSpacePos);
clipPos = UnityApplyLinearShadowBias(clipPos);
outDepth = clipPos.z / clipPos.w; return 0;
}
ENDCG
That it. And this works for every shadow caster variant.* Directional light shadows, spot light shadows, point light shadows, and the camera depth texture! You know, should we ever want to support multiple lights…
* I didn’t add support for GLES 2.0 point light shadows. That requires outputting the distance from the light as the shadow caster pass’s color value instead of just a hard coded 0. It’s not too hard to add, but it makes the shader a bit messier with a few #if and special case data we’d need to calculate. So I didn’t include it.
from Hacker News https://ift.tt/3q5TZ5f
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.