Shadow mapping on Monogame

Hi,

I am pretty new to Monogame as well as game programming in general, so please excuse my ignorance if my question sounds stupid.

I have been trying to get basic shadow mapping working, but for the life of me, I can’t seem to figure it out.

My first step is to try and get the depth shadow map created and then display that on the screen. I have tried following Riemer’s tutorials as well as follow the MS XNA tutorial on Shadow Mapping.

I also tried converting some code online to work with Monogame, but those don’t seem to work… I am either getting compile errors on the samples that seem a pain to debug or the displayed results aren’t correct i.e. screen is black or textures missing etc.

It seems impossible to find a basic Shadow Mapping example for Monogame that actually compiles and works.

Anyone have some functional code or tutorial for Monogame they could point me to?

Thanks!

For sun? Spotlight? Point light?

Anyways, the shadow map creation is the same for all of them, so let’s start with that.

You can basically work with the riemer’s tutorial …

Somewhere in your initialize or load you wanto to create a rendertarget for the shadow map.
Something like this

shadowMapRenderTarget = new RenderTarget2D(graphicsDevice, 2048, 2048, false, SurfaceFormat.Single, DepthFormat.Depth24, 0, RenderTargetUsage.PlatformContents);

We also need to load our shader, which we saved as an fx file (I’ll come to that later)

_ShadowMapGenerate = content.Load<Effect>("Shaders/ShadowMapsGenerate");

Then for each frame (or when the shadow caster moves) you need to set up your shadow matrices like this

Matrix lightView = Matrix.CreateLookAt(lightPosition,
                        lightPosition + _lightDirection,
                        Vector3.Up);

Matrix  lightProjection = Matrix.CreateOrthographic(width, height, nearZ, farZ);

Note: Orthographic means it’s a directional light (for example sunlight). If you have point or spot lights you use a perspective projection.

Then we need to combine these two for our lightViewProjection.

Matrix lightViewProjection = lightView * lightProjection;

Ok. So we setup the basics - now onto drawing all our shadows into the shadowMapRenderTarget.

private void DrawShadows(... our entities)
{
      graphicsDevice.SetRenderTarget(shadowMapRenderTarget);

      ... foreach of our entities we need the model and their position
      ...
        foreach(entity...)
        {
         DrawShadowMap(model, modelworldmatrix)
         }

}


private void DrawShadowMap(Model model, Matrix world)
        {
            for (int index = 0; index < model.Meshes.Count; index++)
            {
                ModelMesh mesh = model.Meshes[index];
                    for (int i = 0; i < mesh.MeshParts.Count; i++)
                    {
                        ModelMeshPart meshpart = mesh.MeshParts[i];
                        _ShadowMapGenerate.Parameters["WorldViewProj"].SetValue(world * LightViewProjection);

                        _ShadowMapGenerate.CurrentTechnique.Passes[0].Apply();

                        _graphicsDevice.SetVertexBuffer(meshpart.VertexBuffer);
                        _graphicsDevice.Indices = (meshpart.IndexBuffer);
                        int primitiveCount = meshpart.PrimitiveCount;
                        int vertexOffset = meshpart.VertexOffset;
                        int vCount = meshpart.NumVertices;
                        int startIndex = meshpart.StartIndex;

                        _graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, vertexOffset, startIndex,
                            primitiveCount);
                    }
            }
        }

Note that I use _ShadowMapGenerate here. I hope it’s somewhat clear so far. I think this should correspond to the Riemer’s tutorial.

Onto the shader.

matrix  LightViewProj;

struct CreateShadowMap_VSOut
{
    float4 Position : POSITION;
    float Depth : TEXCOORD0;
};

//  CREATE SHADOW MAP
CreateShadowMap_VSOut CreateShadowMap_VertexShader(float4 Position : SV_POSITION)
{
    CreateShadowMap_VSOut Out;
    Out.Position    = mul(Position, mul(World, LightViewProj));
    Out.Depth       = Out.Position.z / Out.Position.w;
    
    return Out;
}

float4 CreateShadowMap_PixelShader(CreateShadowMap_VSOut input) : COLOR
{
    return float4(input.Depth, 0, 0, 0);
}

// Technique for creating the shadow map
technique CreateShadowMap
{
    pass Pass1
    {
        VertexShader = compile vs_5_0 CreateShadowMap_VertexShader();
        PixelShader = compile ps_5_0 CreateShadowMap_PixelShader();
    }
}

That’s it!

If you want to draw the renderTarget in the output (for debug purposes, or just to check how it looks) you can put this at the end of your Draw() function

_graphicsDevice.SetRenderTarget(null);
            _spriteBatch.Begin(0, BlendState.Opaque, SamplerState.AnisotropicClamp);
            _spriteBatch.Draw(_shadowMapRenderTarget, new Rectangle(0, 0, width, height), Color.White);
            _spriteBatch.End();

width and height being the output size obviously.

I hope this helps a bit. If you managed to make this work the rest (actual shadowing) is not too far off. Report back!

6 Likes

@kosmonautgames Thanks a ton for the help! This is the first time I actually got the ShadowMap to show anything on the screen! Your code is well explained so was easy enough to get it up and running. There were a couple typos which were easy enough to fix e.g. the Shader has a LightViewProj parameter but the code references a WorldViewProj.

Also, your shader is set for SM5, but it looks like Monogame won’t compile it with anything set above SM3. How are you able to compile it with SM5? Is that for a specific platform only?

If you don’t mind too much… could you talk me through the second half where we now use the ShadowMap and actually generate the final image?

I have managed to accomplish in about 30 minutes with your help what I have been struggling with for days!

Thanks again… your help is invaluable!

Ah well, if you use the Desktop platform aka DirectX you can use SM5, if you use OpenGL you have to use a lower one. But that should not be a problem in that case since we don’t really need any advanced shader functions here.

So yeah about that shadow mapping now

I hope you have a basic shader set up for the lighting of your objects.

In there you want to have a new multiplier called shadowContribution or something.

So your final output for the pixel is for example

finalvalue = texture_color * (diffuse_color * shadowContribution + ambientContribution) + specular_color * shadowContribution;

So if shadowContribution is 1 it means our objects is fully lit. You can change the naming if you think something else makes sense.

Our shadow contribution can be calculated as follows:

NdotL should be calculated already, we need that here.

float4 lightingPosition = mul(input.WorldPos, LightViewProj);
    // Find the position in the shadow map for this pixel
    float2 ShadowTexCoord = mad(0.5f , lightingPosition.xy / lightingPosition.w , float2(0.5f, 0.5f));
    ShadowTexCoord.y = 1.0f - ShadowTexCoord.y;

	// Get the current depth stored in the shadow map
    float ourdepth = (lightingPosition.z / lightingPosition.w);

`shadowContribution = CalcShadowTermPCF(ourdepth, NdotL, ShadowTexCoord, 3);`

The actual function CalcShadowTermPCF could look like this.

Note you should set the DepthBias up front (const float DepthBias = 0.02;) so you can change it from the game itself.

// Calculates the shadow term using PCF
    float CalcShadowTermPCF(float light_space_depth, float ndotl, float2 shadow_coord)
    {
        float shadow_term = 0;

       //float2 v_lerps = frac(ShadowMapSize * shadow_coord);

        float variableBias = clamp(0.001 * tan(acos(ndotl)), 0, DepthBias);

    	//safe to assume it's a square
        float size = 1 / ShadowMapSize.x;
    	
        float samples[4];
        samples[0] = (light_space_depth - variableBias < ShadowMap.Sample(ShadowMapSampler, shadow_coord).r);
        samples[1] = (light_space_depth - variableBias < ShadowMap.Sample(ShadowMapSampler, shadow_coord + float2(size, 0)).r);
        samples[2] = (light_space_depth - variableBias < ShadowMap.Sample(ShadowMapSampler, shadow_coord + float2(0, size)).r);
        samples[3] = (light_space_depth - variableBias < ShadowMap.Sample(ShadowMapSampler, shadow_coord + float2(size, size)).r);

        shadow_term = (samples[0] + samples[1] + samples[2] + samples[3]) / 4.0;
    	//shadow_term = lerp(lerp(samples[0],samples[1],v_lerps.x),lerp(samples[2],samples[3],v_lerps.x),v_lerps.y);

        return shadow_term;
    }

Remember to put it above the actual pixel shader function, otherwise the compiler won’t know it’s there (unless you announce it up front like this (float CalcShadowTermPCF(float fLightDepth, float slope, float2 vTexCoord); somewhere above the pixelshader function)

Sorry I don’t have much time right now, try to make it work. If it doesn’t come back and I’ll explain.

You will need a shadowMap texture, you can use this

Texture2D ShadowMap;
SamplerState ShadowMapSampler
{
    Texture = (ShadowMap);
    MinFilter = point;
    MagFilter = point;
    MipFilter = point;
    AddressU = Wrap;
    AddressV = Wrap;
};

EDIT: I’m back. We need the world coordinates of our pixel. Then we transform it to the perspective of the light and check it against the depth saved in the shadow map. If it is in front its lit, otherwise it is in shadow.
This PCF variant samples more than one pixel

1 Like

@kosmonautgames thanks again! I will try this out this evening and let you know how it goes.

Hi @kosmonautgames

I managed to get the basics of the shadowing up and running, so thanks for the help!

A couple of interesting things I came across:

I have been playing around with an animated model (i.e. SkinnedModel from the XNA demo) and even though I was not rendering it using the shadow effect (I am still using SkinnedEffect), it was creating a shadow in the scene, albeit at the incorrect location due to (I am guessing) using the camera location and not the light location and rendering the model depth to the ShadowMap.

I had a heck of a time figuring out how to align the lights to positions where I expected them to be. Some locations rendered a completely dark scene (even though I expected the light to be visible), so it took a lot of trial and error trying to get something to display. This was especially true of directional lights.

Spot / point lights (using a perspective transform) seemed a better for actually getting a visible shadow, but were finicky in terms of location / near plane / far plane, and the shadows across multiple models were not consistent i.e. which way the shadow was cast… which was sometimes in opposing directions.

I was also getting some cases where multiple extraneous shadows were being cast, but I suspect this is because I was somehow wrapping around the texture / shadowmap.

Right now even though my shadows are (somewhat) functional, they are pretty blocky. I’ll have to figure that out at some point.

I guess I still have a lot to learn about lights, so I am taking a step back and starting to build a real light system i.e. light classes / entities etc so its easier to manipulate the lights in a scene.

I also want to move to a deferred shading / lighting system, so will eventually look at how I can integrate the shading system into that.

The one thing that’s in the back of my mind is how to deal with shadows when you have multiple lights… I’ve heard the term “light sorting” etc being mentioned, so will need to learn about those things.

I will definitely be back with more questions soon :slight_smile:

Thanks!

Blocky shadows are a common issue, when dealing with shadowmapping. There are techniques to achieve better results, for example you could try PCF (percentage closer filtering) or VSM (variance shadow mapping). This would give you soft shadows. Sometimes though you just want hard shadows to be cast. Then you could try to increase the resolution of the depth map.

For sunlight or directional light, there is another technique, where the view frustum gets split. CSM (cascaded shadow maps).

If you want some samples you can download my deferred Engine and check out the DeferredDirectionaLight shader, i have some different techniques in there.

The PCF I gave you is a bit blocky. If you want a better variant (more expensive) use this

A good input for the iSqrtSamples would be between 3 and 7

// Calculates the shadow term using PCF with edge tap smoothing
float CalcShadowTermSoftPCF(float fLightDepth, float ndotl, float2 vTexCoord, int iSqrtSamples)
{
    float fShadowTerm = 0.0f;
    
    float variableBias = clamp(0.0005 * tan(acos(ndotl)), 0.00001, DepthBias);

    //float variableBias = (-cos(ndotl) + 1)*0.02;
    //variableBias = DepthBias;

    float shadowMapSize = ShadowMapSize.x;

    float fRadius = iSqrtSamples - 1; //mad(iSqrtSamples, 0.5, -0.5);//(iSqrtSamples - 1.0f) / 2;

    for (float y = -fRadius; y <= fRadius; y++)
    {
        for (float x = -fRadius; x <= fRadius; x++)
        {
            float2 vOffset = 0;
            vOffset = float2(x, y);
            vOffset /= shadowMapSize;
            //vOffset *= 2;
            //vOffset /= variableBias*200;
            float2 vSamplePoint = vTexCoord + vOffset;
            float fDepth = ShadowMap.Sample(ShadowMapSampler, vSamplePoint).x;
            float fSample = (fLightDepth <= fDepth + variableBias);
            
            // Edge tap smoothing
            float xWeight = 1;
            float yWeight = 1;
            
            if (x == -fRadius)
                xWeight = 1 - frac(vTexCoord.x * shadowMapSize);
            else if (x == fRadius)
                xWeight = frac(vTexCoord.x * shadowMapSize);
                
            if (y == -fRadius)
                yWeight = 1 - frac(vTexCoord.y * shadowMapSize);
            else if (y == fRadius)
                yWeight = frac(vTexCoord.y * shadowMapSize);
                
            fShadowTerm += fSample * xWeight * yWeight;
        }
    }
    
    fShadowTerm /= (fRadius*fRadius*4);
    
    return fShadowTerm;
}

Or you can modify the original PCF with some edge tapping

float CalcShadowTermPCF(float light_space_depth, float ndotl, float2 shadow_coord)
{
    float shadow_term = 0;

    //float2 v_lerps = frac(ShadowMapSize * shadow_coord);

    float variableBias = GetVariableBias(ndotl);

    //safe to assume it's a square
    float size = 1.0f / 2048;
    	
    float samples[5];
    samples[0] = (light_space_depth - variableBias < 1-ShadowMap.SampleLevel(pointSampler, shadow_coord, 0).r);
    samples[1] = (light_space_depth - variableBias < 1 - ShadowMap.SampleLevel(pointSampler, shadow_coord + float2(size, 0), 0).r) * frac(shadow_coord.x * ShadowMapSize);
    samples[2] = (light_space_depth - variableBias < 1 - ShadowMap.SampleLevel(pointSampler, shadow_coord + float2(0, size), 0).r) * frac(shadow_coord.y * ShadowMapSize);
    samples[3] = (light_space_depth - variableBias < 1 - ShadowMap.SampleLevel(pointSampler, shadow_coord - float2(size, 0), 0).r) * (1-frac(shadow_coord.x * ShadowMapSize));
    samples[4] = (light_space_depth - variableBias < 1 - ShadowMap.SampleLevel(pointSampler, shadow_coord - float2(0, size), 0).r) * (1 - frac(shadow_coord.y * ShadowMapSize));


    shadow_term = (samples[0] + samples[1] + samples[2] + samples[3] + samples[4]) / 5.0;
    //shadow_term = lerp(lerp(samples[0],samples[1],v_lerps.x),lerp(samples[2],samples[3],v_lerps.x),v_lerps.y);

    return shadow_term;
}
2 Likes

@kosmonautgames I’ve been following your Deferred Engine thread. It looks awesome! I will definitely check it out.

My first pass is usually to try and build something from spec / docs, and once it all comes crumbling down, see how others have done it :smiley:

@Kwyrky thanks for the info. I did read about cascaded shadow maps and will try that out at some point. There seems like a ton of different ways to do shadows and optimize them.

As usual… Too much to do and not enough time! :smiley:

@kosmonautgames I just tried running my game on Android again, and when I have shadows enabled, it’s showing me a red screen instead of the “normal” view. It works fine on PC (OpenGL).

Any ideas why it’s not running as expected on Android?

Thanks!

It’s possible you need to use a different texture format for the rendertargets for mobile