I’m working on a racing game in Monogame. The game runs fine so far using Monogame’s BasicEffect (link to video).

I wanted to add shadows so I implemented a shadowmap using this as a base.

and a shadowmap of 4096×4096 I get nice shadows:

My game actually has a draw distance of 500 units instead of the 20 units used in my test code. This greatly reduces the accuracy of the shadowmap, so much it becomes unusable (some blobs appear under some of the cars). For example the same code with a projection distance of 200 units:

**Now to my question:**

How can I improve the quality of the shadows with bigger draw distances?

I tried using different drawdistances for the shadowmap and for the actual drawing (hoping shadows would not matter in the far distance) but it produced more artefacts than decent shadows.

I’m considering scaling my entire game down- but it would mean scaling the ingame logic as well so I’m hoping to find an answer here first.

**Edit: this is the (currently not working) shader I’m working on based on János Turánszki suggestions**

```
float4x4 World;
float4x4 View;
float4x4 Projection;
float4x4 LightViewProjClose;
float4x4 LightViewProjMedium;
float4x4 LightViewProjFar;
float3 LightDirection1 = float3(-1.5, 0.45, 0);
float4 LightColor1 = float4(1, 1, 1, 1);
float3 LightDirection2 =float3(1,1,0.2);
float4 LightColor2 = float4(0.2, 0.2, 0.25, 1);
float4 AmbientColor = float4(0.15, 0.15, 0.15, 0.15);
float DepthBias = 0.001f;
texture ShadowMap;
sampler ShadowMapSampler = sampler_state
{
Texture = <ShadowMap>;
};
struct VertexShaderInput
{
float4 Position : SV_POSITION;
float4 Normal : NORMAL0;
float4 Color : COLOR0;
};
struct ShadowVertexShaderInput
{
float4 Position : SV_POSITION;
float4 Normal : NORMAL0;
float4 Color : COLOR0;
};
struct VertexShaderOutput
{
float4 Position : SV_POSITION;
float4 Normal : TEXCOORD0;
float4 Color : COLOR0;
float4 WorldPos : TEXCOORD2;
};
struct CreateShadowMap_VSOut
{
float4 Position : POSITION;
float4 Color : COLOR0;
};
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
VertexShaderOutput output;
float4x4 WorldViewProj = mul(mul(World, View), Projection);
// Transform the models verticies and normal
output.Position = mul(input.Position, WorldViewProj);
output.Normal = input.Normal;
output.Color = input.Color;
// Save the vertices postion in world space
output.WorldPos = mul(input.Position, World);
return output;
}
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
// Color of the model
float4 diffuseColor = input.Color;
// Intensity based on the direction of the light
float diffuseIntensity1 = clamp(dot(input.Normal.xyz, LightDirection1.xyz),0,1);
float diffuseIntensity2 = clamp(dot(input.Normal.xyz, LightDirection2.xyz), 0, 1);
// Final diffuse color with ambient color added
float4 diffuse = saturate((diffuseIntensity1 * LightColor1 + diffuseIntensity2 * LightColor2) * diffuseColor);
// Find the position of this pixel in light space in the close projection
float4 lightingPosition = mul(input.WorldPos, LightViewProjClose);
if (((lightingPosition.x > -0.5) && (lightingPosition.x<0.5)) && ((lightingPosition.y>-0.5) && (lightingPosition.y < 0.5)))
{
// Find the position in the shadow map for this pixel
float2 ShadowTexCoord = 0.5 * lightingPosition.xy /
lightingPosition.w + float2(0.5, 0.5);
ShadowTexCoord.y = 1.0f - ShadowTexCoord.y;
// Get the current depth stored in the shadow map (red component for close)
float shadowdepth = tex2D(ShadowMapSampler, ShadowTexCoord).r;
// Calculate the current pixel depth
// The bias is used to prevent folating point errors that occur when
// the pixel of the occluder is being drawn
float ourdepth = (lightingPosition.z / lightingPosition.w) - DepthBias;
// Check to see if this pixel is in front or behind the value in the shadow map
if (shadowdepth < ourdepth)
{
// Shadow the pixel by lowering the intensity
diffuse *= float4(0.5, 0.5, 0.5, 0);
};
}
lightingPosition = mul(input.WorldPos, LightViewProjMedium);
if (((lightingPosition.x > -0.5) && (lightingPosition.x<0.5)) && ((lightingPosition.y>-0.5) && (lightingPosition.y < 0.5)))
{
// Find the position in the shadow map for this pixel
float2 ShadowTexCoord = 0.5 * lightingPosition.xy /
lightingPosition.w + float2(0.5, 0.5);
ShadowTexCoord.y = 1.0f - ShadowTexCoord.y;
// Get the current depth stored in the shadow map (green component for medium)
float shadowdepth = tex2D(ShadowMapSampler, ShadowTexCoord).g;
// Calculate the current pixel depth
// The bias is used to prevent folating point errors that occur when
// the pixel of the occluder is being drawn
float ourdepth = (lightingPosition.z / lightingPosition.w) - DepthBias;
// Check to see if this pixel is in front or behind the value in the shadow map
if (shadowdepth < ourdepth)
{
// Shadow the pixel by lowering the intensity
diffuse *= float4(0.5, 0.5, 0.5, 0);
};
}
//do the same trick using the 'far' matrix.
lightingPosition = mul(input.WorldPos, LightViewProjFar);
if (((lightingPosition.x > -0.5) && (lightingPosition.x<0.5)) && ((lightingPosition.y>-0.5) && (lightingPosition.y < 0.5)))
{
// Find the position in the shadow map for this pixel
float2 ShadowTexCoord = 0.5 * lightingPosition.xy /
lightingPosition.w + float2(0.5, 0.5);
ShadowTexCoord.y = 1.0f - ShadowTexCoord.y;
// Get the current depth stored in the shadow map (blue component for far)
float shadowdepth = tex2D(ShadowMapSampler, ShadowTexCoord).b;
// Calculate the current pixel depth
// The bias is used to prevent folating point errors that occur when
// the pixel of the occluder is being drawn
float ourdepth = (lightingPosition.z / lightingPosition.w) - DepthBias;
// Check to see if this pixel is in front or behind the value in the shadow map
if (shadowdepth < ourdepth)
{
// Shadow the pixel by lowering the intensity
diffuse *= float4(0.5, 0.5, 0.5, 0);
};
}
return diffuse;
}
CreateShadowMap_VSOut ShadowVertexShaderFunction(VertexShaderInput input)
{
CreateShadowMap_VSOut output;
float4 position;
float4 color;
//check against close projection
float4 lightingPosition = mul(mul(input.Position, World), LightViewProjClose);
//by calculating the pixel position- find if it is part of the close projection matrix.
if (lightingPosition.x > -0.5 && lightingPosition.x<0.5 && lightingPosition.y>-0.5 && lightingPosition.y < 0.5)
{
position = mul(input.Position, mul(World, LightViewProjClose));
color = float4(position.z / position.w, 0,0,0); //place in R component
}
//check against medium projection
lightingPosition = mul(mul(input.Position, World), LightViewProjMedium);
if (lightingPosition.x > -0.5 && lightingPosition.x<0.5 && lightingPosition.y>-0.5 && lightingPosition.y < 0.5)
{
position = mul(input.Position, mul(World, LightViewProjMedium));
color = float4(0,position.z / position.w, 0, 0); //place in G component
}
//check against far projection
lightingPosition = mul(mul(input.Position, World), LightViewProjFar);
if (lightingPosition.x > -0.5 && lightingPosition.x<0.5 && lightingPosition.y>-0.5 && lightingPosition.y < 0.5)
{
position = mul(input.Position, mul(World, LightViewProjFar));
color = float4(0, 0, position.z / position.w, 0); //place in B component
}
output.Position = position;
output.Color = color;
return output;
}
float4 ShadowPixelShaderFunction(CreateShadowMap_VSOut input) : COLOR0
{
return input.Color;
}
technique MyTechnique
{
pass Pass1
{
VertexShader = compile vs_4_0_level_9_3 VertexShaderFunction();
PixelShader = compile ps_4_0_level_9_3 PixelShaderFunction();
}
}
technique ShadowTechnique
{
pass Pass1
{
VertexShader = compile vs_4_0_level_9_3 ShadowVertexShaderFunction();
PixelShader = compile ps_4_0_level_9_3 ShadowPixelShaderFunction();
}
}
```