My original proposal of just calculating the angle between the normal and light direction and setting it to shadowed if it’s nearly perpendicular has a couple issues. First, the shadow will just suddenly pop onto the face when the critical angle is reached, which is pretty jarring. If I smooth out the transition, then any shadows from other objects will not be visible during the transition. An object’s shadow may suddenly disappear while moving over this fake shadow.
Fortunately, I found another solution. I was able to get pretty good results with the mobile shader (I still haven’t found where the cascaded shadow map is for shader model 5). These are my edits to MobileBasePassPixelShader.usf (highlighted in red)
// Cascaded Shadow Map
Settings.ShadowDepthTexture = MobileDirectionalLight.DirectionalLightShadowTexture;
Settings.ShadowDepthTextureSampler = MobileDirectionalLight.DirectionalLightShadowSampler;
Settings.TransitionScale = MobileDirectionalLight.DirectionalLightShadowTransition;
Settings.ShadowBufferSize = MobileDirectionalLight.DirectionalLightShadowSize;
Settings.bSubsurface = false;
Settings.bTreatMaxDepthUnshadowed = false;
Settings.DensityMulConstant = 0;
Settings.ProjectionDepthBiasParameters = 0;
float4 ShadowPosition = float4(0,0,0,0);
for (int i = 0; i < MAX_MOBILE_SHADOWCASCADES; i++)
if (MaterialParameters.ScreenPosition.w < MobileDirectionalLight.DirectionalLightShadowDistances*)
ShadowPosition = mul(float4(MaterialParameters.ScreenPosition.xyw, 1), MobileDirectionalLight.DirectionalLightScreenToShadow*);
break; // position found.
// Process CSM only when ShadowPosition is valid.
if (ShadowPosition.z > 0)
// Clamp pixel depth in light space for shadowing opaque, because areas of the shadow depth buffer that weren't rendered to will have been cleared to 1
// We want to force the shadow comparison to result in 'unshadowed' in that case, regardless of whether the pixel being shaded is in front or behind that plane
float LightSpacePixelDepthForOpaque = min(ShadowPosition.z, 0.99999f);
Settings.SceneDepth = LightSpacePixelDepthForOpaque;
#if MOBILE_CSM_QUALITY == 0
half ShadowMap = ManualNoFiltering(ShadowPosition.xy, Settings);
#elif MOBILE_CSM_QUALITY == 1
half ShadowMap = Manual1x1PCF(ShadowPosition.xy, Settings);
#elif MOBILE_CSM_QUALITY == 2
half ShadowMap = Manual2x2PCF(ShadowPosition.xy, Settings);
#error Unsupported MOBILE_CSM_QUALITY value.
float Fade = saturate(MaterialParameters.ScreenPosition.w * MobileDirectionalLight.DirectionalLightDistanceFadeMAD.x + MobileDirectionalLight.DirectionalLightDistanceFadeMAD.y);
// lerp out shadow based on fade params.
ShadowMap = lerp(ShadowMap, 1.0, Fade * Fade);
Shadow = ShadowMap;
Shadow = min(ShadowMap, Shadow);
// Shadow values above 0.25 can potentially be shadow striping artifacts, so assume it shouldn’t be shadowed and set Shadow to 1
if(Shadow > 0.25f)
Shadow = 1.f;
#endif /* DIRECTIONAL_LIGHT_CSM */
float NoL = max(0, dot(float3(MaterialParameters.WorldNormal), float3(MobileDirectionalLight.DirectionalLightDirection)));
// Remove potential shadow striping artifacts based on weak shadow values and if the normal is within 16 degrees of perpendicular with the light direction
/if(Shadow > 0.25f && NoL < 0.275f)
Shadow = 1.f;
The first edit will remove any shadows with values greater than 0.25 (the higher the shadow value, the weaker the shadow). Since most striping artifacts only produce weak shadows, this will remove striping artifacts on all models with flat normals and most models with smooth normals. This comes at a cost, though. The edges of the shadows will not have a smooth blur (though this may be preferred if you want crisp shadows and don’t mind slightly jagged shadow boundaries).
The second edit (which is commented out) will only remove shadow values greater than 0.25 if the normal is near perpendicular to the light direction (from my tests, it seems like 16 degrees from perpendicular is about where the striping artifacts begin appearing). This allows you to keep blurred shadow edges on most faces. This works perfectly for models with flat normals, but models with smooth normals will have more striping artifacts than the first approach.
Regardless, I’m pretty happy with the result so far
These edits will only work on mobile. Just copy them over to MobileBasePassPixelShader.usf (replacing the corresponding sections of the original shader). The shaders will recompile if you restart the engine or type “recompileshaders changed” without the quotes into the console.