1
\$\begingroup\$

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 4096x4096 I get nice shadows:

Shadows with a shadowmap shader

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:

shadows with a drawdistance 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();
    }
}
\$\endgroup\$
5
  • \$\begingroup\$ The problem is that you are using a return in that if statement, you can't do that in a shader. if(1==1) will get optimized out that's why it works. Just save the value which you want to return and return outside of an if statement. \$\endgroup\$ Dec 23, 2015 at 8:04
  • \$\begingroup\$ I think I have it working; however a critical mistake that I don't know how to solve. In the create shadowvertexshader I output the depth in a R,G or B component. However, in the pixel shader, I simply return that color- eventually removing the color that was there in the first place. So, how do I 'add' that component instead of overwriting the value? \$\endgroup\$
    – Felsir
    Dec 23, 2015 at 13:35
  • \$\begingroup\$ You could use an additive blendstate (set it up before your Draw call) if you don't want your pixel shader overwrite the current fragment color, but insead adding to it. \$\endgroup\$ Dec 23, 2015 at 13:48
  • \$\begingroup\$ It doesn't seem to work. ` GraphicsDevice.SetRenderTarget(_shadowMap); GraphicsDevice.Clear(Color.White); GraphicsDevice.BlendState = BlendState.Additive;` I also tried clearing with Black, but the result is that either no shadows appear or everything is shadowed. \$\endgroup\$
    – Felsir
    Dec 23, 2015 at 13:56
  • \$\begingroup\$ I'm having more trouble implementing this than I thought. I'll try to break down the problems into smaller questions, like this gamedev.stackexchange.com/questions/113592/… \$\endgroup\$
    – Felsir
    Dec 24, 2015 at 12:39

1 Answer 1

2
\$\begingroup\$

The standard way of handling this is by using cascaded shadow maps.

The idea is that you render multiple shadow maps. Objects closer to the camera should be rendered to a smaller shadow map near the camera, distant objects to a bigger shadow map. If the shadow maps are the same resolution, then you get fine details to shadows which are close to you because you get more shadow map pixels per world units, while farther away shadows will get much less detail, but you won't notice that because they are far from you.
The trick is how you select the cascades so it will look good.

A nice article which is more in-depth: MSDN

\$\endgroup\$
7
  • \$\begingroup\$ I'll look into that! I understand the concept- now I need to figure out how to get it to work in my shader. \$\endgroup\$
    – Felsir
    Dec 21, 2015 at 19:32
  • \$\begingroup\$ The easiest way is uploading the view-projection matrices for each shadow map, check the current pixel as you do now for each one. If a pixel is visible after multiplying with one of the matrices (is inside -0.5, 0.5 range), then use that shadow map for it. ;) \$\endgroup\$ Dec 21, 2015 at 23:08
  • \$\begingroup\$ You mean repeating this line for each matrix? float4 lightingPosition = mul(input.WorldPos, LightViewProj); and check if the resulting xy are within -0.5,0.5? \$\endgroup\$
    – Felsir
    Dec 22, 2015 at 10:16
  • \$\begingroup\$ Also, now I only use the red component in the shadowmap, would it be smart to reuse the shadowmap and fill the R,G,A components for other cascades for a total of 4 cascades? \$\endgroup\$
    – Felsir
    Dec 22, 2015 at 10:27
  • \$\begingroup\$ You are right on both of them. :) \$\endgroup\$ Dec 22, 2015 at 12:09

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .