Jump to content

  • Log In with Google      Sign In   
  • Create Account


Basic shadow mapping shader?


Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

  • You cannot reply to this topic
15 replies to this topic

#1 george7378   Members   -  Reputation: 1132

Like
0Likes
Like

Posted 11 April 2013 - 10:01 AM

Hi everyone!

 

I'm beginning to get fed up with my simple shadow that I'm getting by flattening my geometry onto a plane, so at the moment I'd love more than anything to get a good shadow map working. I've read enough about HLSL to understand what a pixel/vertex shader does, the syntax used to create them, and the .fx file format used to hold the shaders and compile them as a technique. I think I'd be able to have a good stab at implementing shadow mapping if I could find a nice, clean example of a bare-bones shader. The one in the SDK example (I'm using D3D9, by the way) seems to contain a lot of unnecessary stuff which isn't related to the shadow map itself, and a lot of other examples online also contain a lot of stuff which is related specifically to the scene in the demo program in question.

 

So, is anyone willing to post a shadow map shader/.fx file in its simplest form for me to have a mess around with? Or failing that, is there a good example or tutorial out there which would allow me to build my own?

 

Thanks, I'd really like to get good shadows working in my game, and I think I'm getting close if I can make this first step!



Sponsor:

#2 VladR   Members   -  Reputation: 722

Like
1Likes
Like

Posted 11 April 2013 - 10:19 AM

I can relate to the frustration of using flattenned shadows, since that's what I've been using few yrs back and then switched to Shadow Mapping and couldn't be happier !

 

I would definitely recommend the Shadow Mapping example from the XNA Samples, since it's drop-dead simple to use and tweak.

 

It took me a mere single evening to load up that sample, debug through it and reimplement the technique into my engine and tweak it to a much higher quality.

 

Due to the similarities between XNA / DX9 I don't suppose you could run into any issues converting that sample.

 

 

Note, that understanding just the shaders is one thing. You also need an understanding of the Render Targets and that you practically have 2 sets of shaders - one for just rendering them into the ShadowMap and the other set that actually applies the ShadowMap to your geometry.

So, I'd make sure you understand, conceptually, how Shadow Mapping works first - since there's quite a lot of stuff that can (and will) go wrong.

 

 

The XNA Sample for SM : http://xbox.create.msdn.com/en-US/education/catalog/sample/shadow_mapping_1

The SM Tutorial : http://www.riemers.net/eng/Tutorials/XNA/Csharp/Series3/Shadow_map.php


VladR    My 3rd person action RPG on GreenLight:    http://steamcommunity.com/sharedfiles/filedetails/?id=92951596

 


#3 george7378   Members   -  Reputation: 1132

Like
0Likes
Like

Posted 11 April 2013 - 10:57 AM

Yeah, the flattened shadows work OK for simple stuff - I managed to make them work better by using the stencil buffer, disabling z-writes in certain places and by making them follow the height of my terrain, but when I've seen what shadow mapping can do, it makes me wish I could use it!

 

It's encouraging that you managed to get it going so quickly - it was sort of the same story with DirectInput - it only took an evening to take a sample and change/optimise some stuff to get fully working controller support in my game.

 

I'll take a look at the sample and see what I can do!



#4 VladR   Members   -  Reputation: 722

Like
1Likes
Like

Posted 11 April 2013 - 11:54 AM

Frankly, the Stencil stuff is probably even more complex and you get all kinds of terrible artifacts when your mesh is not "closed" and has holes. And you can't do self-shadowing of trees with Shadow Volumes.

 

Look here for the self-shadowed tree I got using SM : http://cloud-2.steampowered.com/ugc/596979776527790985/066CF9742ECC3539A073337F27E2F3285B3BB2E4/

 

The best thing - I did not have to do anything ! The damn algorithm just works on everything you throw at it, since it works in Image Space. It does not care if it is a rock, animated character or a tree canopy.

 

The performance hit I got from using SM is about 7% on an XBOX360 and is practically ~free on PC (assuming a full engine/gameplay load)

 

 

It makes me want to cry every single time I think about how much time I spent trying to fix and work around (especially closing the meshes and reexporting them is a nightmare) the principally fundamentally flawed algo like the Shadow Volumes via the Stencil, when we have something as drop-dead simple, beautiful and practically almost free (in terms of a performance hit).

 

The time when Stencil Shadows were an option is a decade behind us. Just let it die like fixed-function pipeline...

 

 

 

 

Feel free to ask here, but I found all replies to questions I had during implementation of the SM in my engine directly on the XNA forums. If I remember correctly there were about 20 threads about SM at that time, so it was easy to read through them and find lots of other SM-related info there.


VladR    My 3rd person action RPG on GreenLight:    http://steamcommunity.com/sharedfiles/filedetails/?id=92951596

 


#5 george7378   Members   -  Reputation: 1132

Like
0Likes
Like

Posted 11 April 2013 - 02:37 PM

That's a lovely image - exactly the sort of thing that makes me want to strive for shadow mapping myself! By the way, I went through the tutorial on the website and extracted the info to create this shader:

 

////////////////////

//Global variables//

////////////////////

float4x4 xWorldViewProjection;

float4x4 xLightsWorldViewProjection;

float4x4 xWorld;

float3 xLightPos;

float xLightPower;

float xAmbient;



Texture xTexture;

Texture xShadowMap;





//////////////////

//Sampler states//

//////////////////

sampler TextureSampler = sampler_state

{

texture = <xTexture>;

magfilter = LINEAR;

minfilter = LINEAR;

mipfilter= LINEAR;

AddressU = mirror;

AddressV = mirror;

};



sampler ShadowMapSampler = sampler_state

{

texture = <xShadowMap>;

magfilter = LINEAR;

minfilter = LINEAR;

mipfilter= LINEAR;

AddressU = clamp;

AddressV = clamp;

};





//////////////////

//I/O structures//

//////////////////

struct VertexToPixel

{

    float4 Position     : POSITION;    

    float2 TexCoords    : TEXCOORD0;

    float3 Normal       : TEXCOORD1;

    float3 Position3D   : TEXCOORD2;

};



struct PixelToFrame

{

    float4 Color        : COLOR0;

};



struct SMapVertexToPixel

{

    float4 Position     : POSITION;

    float4 Position2D   : TEXCOORD0;

};



struct SMapPixelToFrame

{

    float4 Color : COLOR0;

};



struct SSceneVertexToPixel

{

    float4 Position             : POSITION;

    float4 Pos2DAsSeenByLight   : TEXCOORD0;

    float2 TexCoords            : TEXCOORD1;

    float3 Normal               : TEXCOORD2;

    float4 Position3D           : TEXCOORD3;



};



struct SScenePixelToFrame

{

    float4 Color : COLOR0;

};





////////////////////

//Helper functions//

////////////////////

float DotProduct(float3 lightPos, float3 pos3D, float3 normal)

{

    float3 lightDir = normalize(pos3D - lightPos);

    return dot(-lightDir, normal);    

}





///////////////////////////////////////////////////////////

//TECHNIQUE 1: Shaders for building the shadow map itself//

///////////////////////////////////////////////////////////

SMapVertexToPixel ShadowMapVertexShader( float4 inPos : POSITION)

{

    SMapVertexToPixel Output = (SMapVertexToPixel)0;



    Output.Position = mul(inPos, xLightsWorldViewProjection);

    Output.Position2D = Output.Position;



    return Output;

}



SMapPixelToFrame ShadowMapPixelShader(SMapVertexToPixel PSIn)

{

    SMapPixelToFrame Output = (SMapPixelToFrame)0;            



    Output.Color = PSIn.Position2D.z/PSIn.Position2D.w;



    return Output;

}





technique ShadowMap

{

    pass Pass0

    {

        VertexShader = compile vs_2_0 ShadowMapVertexShader();

        PixelShader = compile ps_2_0 ShadowMapPixelShader();

    }

}





//////////////////////////////////////////////////////////////

//TECHNIQUE 2: Shaders for applying the shadows to the scene//

//////////////////////////////////////////////////////////////

SSceneVertexToPixel ShadowedSceneVertexShader(float4 inPos : POSITION, float2 inTexCoords : TEXCOORD0, float3 inNormal : NORMAL)

 {

     SSceneVertexToPixel Output = (SSceneVertexToPixel)0;

 

     Output.Position = mul(inPos, xWorldViewProjection);    

     Output.Pos2DAsSeenByLight = mul(inPos, xLightsWorldViewProjection);    

     Output.Normal = normalize(mul(inNormal, (float3x3)xWorld));    

     Output.Position3D = mul(inPos, xWorld);

     Output.TexCoords = inTexCoords;    

 

     return Output;

 }

 

SScenePixelToFrame ShadowedScenePixelShader(SSceneVertexToPixel PSIn)

 {

     SScenePixelToFrame Output = (SScenePixelToFrame)0;    

 

     float2 ProjectedTexCoords;

     ProjectedTexCoords[0] = PSIn.Pos2DAsSeenByLight.x/PSIn.Pos2DAsSeenByLight.w/2.0f + 0.5f;

     ProjectedTexCoords[1] = -PSIn.Pos2DAsSeenByLight.y/PSIn.Pos2DAsSeenByLight.w/2.0f + 0.5f;

     

     float diffuseLightingFactor = 0;

     if ((saturate(ProjectedTexCoords).x == ProjectedTexCoords.x) && (saturate(ProjectedTexCoords).y == ProjectedTexCoords.y))

     {

         float depthStoredInShadowMap = tex2D(ShadowMapSampler, ProjectedTexCoords).r;

         float realDistance = PSIn.Pos2DAsSeenByLight.z/PSIn.Pos2DAsSeenByLight.w;

         if ((realDistance - 1.0f/100.0f) <= depthStoredInShadowMap)

         {

             diffuseLightingFactor = DotProduct(xLightPos, PSIn.Position3D, PSIn.Normal);

             diffuseLightingFactor = saturate(diffuseLightingFactor);

             diffuseLightingFactor *= xLightPower;            

         }

     }

         

     float4 baseColor = tex2D(TextureSampler, PSIn.TexCoords);                

     Output.Color = baseColor*(diffuseLightingFactor + xAmbient);

 

     return Output;

 }





technique ShadowedScene

{

    pass Pass0

    {

        VertexShader = compile vs_2_0 ShadowedSceneVertexShader();

        PixelShader = compile ps_2_0 ShadowedScenePixelShader();

    }

}

 

I think this may be the thing I'm after - I see what you mean now about the two sets of shaders - the first one renders a depth image and the second one calculates the scene pixel colours based on the depth comparison test.

 

Now all that's needed is to put it into my game, I guess! I'll see how that goes...

 

Up until now, I've been able to use stencil shadows without a problem. I knew the limitations, but I didn't need anything more. Now though, I am getting to the stage where I can make scenes which are complex enough to need something like shadow mapping.

 

I did look at shadow volumes, but it always seemed like an ugly way of doing it - extruding the edges of the mesh? It didn't sound very pretty. Also, I saw that it has problems like, for example, when you want to use it on a low-res sphere, since it works per-edge, you get some horrible artefacts on the seams. Shadow mapping seems to be a per-pixel algorithm by default, which is exactly what we need, because that's how shadows work in real life (sort of!). No extruding edges or flattening geometry - just a case of whether or not the light can see the 'pixel' or not.

 

I'll see how it goes!



#6 phil_t   Crossbones+   -  Reputation: 3109

Like
1Likes
Like

Posted 11 April 2013 - 11:32 PM

I'd agree that shadow mapping is a much more general purpose algorithm than shadow volumes, but I wouldn't call shadow volumes "principally fundamentally flawed". I released my Tank Negotiator game on the Xbox with a shadow volume implementation. It both performed faster and looked better than the shadow map implementation I had. You don't have to deal with aliasing issues, so you get nice sharp edges (which is good for a space game, which mine was). And, on the Xbox at least, texture sampling bandwidth is a frequent bottleneck, which can make good shadow map performance difficult.

 

Shadow maps are plagued with "jaggies", and the techniques used to fix that problem generally make shadow maps a significant performance hit. On the other hand, they are flexible enough that it is fairly easy to trade performance for quality. That flexibility, and the fact that shadow volumes are difficult to work with for many reasons (mainly to do with restrictions on geometry), usually makes SM the better choice.



#7 george7378   Members   -  Reputation: 1132

Like
0Likes
Like

Posted 12 April 2013 - 03:49 AM

I guess with shadow volumes you always get the same resolution because your geometry doesn't change no matter how far away from your light it gets. But yeah, messing around with that geometry seems like a lot of work!
I guess the larger an area your shadow map covers, the more pixellated the edges become. Same goes for the size of the map texture.

I've been wondering too - since my light is a directional light (out at infinity), how do I modify the algorithm to handle a directional light instead of a positional one? Will it still work?

#8 phil_t   Crossbones+   -  Reputation: 3109

Like
0Likes
Like

Posted 12 April 2013 - 10:21 AM

This difference is that point lights use a perspective projection matrix and directional lights use an orthographic projection matrix.



#9 george7378   Members   -  Reputation: 1132

Like
0Likes
Like

Posted 12 April 2013 - 10:59 AM

Yeah - I thought you'd have to do a projection with the light at infinity. What do you do about the light position though for rendering the shadow map?



#10 george7378   Members   -  Reputation: 1132

Like
0Likes
Like

Posted 12 April 2013 - 01:48 PM

OK, I just got it to the point where I can use the first technique in the shader to render the depth as seen from the light:

I'm using one of my old scenes which I made when I first started - a rotating pyramid over a plane. I tried drawing the same geometry immediately afterwards using the second technique along with render targets, but it didn't work - the whole scene was just showing ambient light. I'm not all too sure on exactly what I should be doing with my render targets though (how to select the right one, draw to it, etc...), but I'll do that tomorrow. Here's the depth buffer of the pyramid scene:

 

depthshader.jpg



#11 george7378   Members   -  Reputation: 1132

Like
1Likes
Like

Posted 12 April 2013 - 05:04 PM

...aaandddd... here it is! A semi-working effect-file shadow mapping implementation!

 

shadowmap!.jpg

 

As you can see it's not perfect yet (for some reason, the pyramid doesn't have any diffuse lighting even though it's facing the light) but wow - I'm so pleased! I am really looking foward to seeing how this performs in my main game project at the moment!

 

EDIT: I figured out the diffuse problem - I was passing the wrong light location into the shader.


Edited by george7378, 12 April 2013 - 05:34 PM.


#12 antmck2013   Members   -  Reputation: 113

Like
0Likes
Like

Posted 18 April 2013 - 09:47 PM

I have had a lot of trouble getting shadow mapping working with terrain.  I've used code from tutes too.  In fact pretty much the same code as listed further up this page.  I'm using SharpDX which is Directx10/11 based.  Either the entire terrain is in shadow or it's lit.  Looks really odd when the sun moves through a complete cycle.  Still don't have it working.  People tell me to put other objects in the scene and turn off shadows on the terrain, but I only have terrain.  At this point I just want the terrain to be correctly shadowed.  I'd like to do real-time editing and watch the shadows change.



#13 phil_t   Crossbones+   -  Reputation: 3109

Like
0Likes
Like

Posted 19 April 2013 - 12:51 AM

What are you using for your depth comparison bias?



#14 antmck2013   Members   -  Reputation: 113

Like
0Likes
Like

Posted 19 April 2013 - 02:04 AM

Actually I've tried several different values - done look right.  Is there a way calculate the ideal value?



#15 george7378   Members   -  Reputation: 1132

Like
0Likes
Like

Posted 19 April 2013 - 04:12 AM

For the offset I just started with a small value and increased it until the nasty effects stopped. For long-range shading on terrain (not counting self shadowing) I can get away with a zero offset. As for the other problems, I guess people can't do anything unless they see some code - I suppose your shader would be a good place to start! I probably can't help much myself because I've only just got it working, but maybe I can spot a mistake that I also made and tell you about it!



#16 antmck2013   Members   -  Reputation: 113

Like
0Likes
Like

Posted 19 April 2013 - 08:28 AM

Here's the complete code for shader.

At the moment I've calculated the normals for each vertex but not using them yet.

Also, no real lighting just adding shadows, so I'm darkening pixels where a shadow is.

As I said before I've tried several bias values.  The shadows just don't look right.

This is self shadows on terrain only - at the moment.

 

float4x4 WorldMatrix;
float4x4 ViewMatrix;
float4x4 ProjectionMatrix;

float4x4 LightViewMatrix;
float4x4 LightProjectionMatrix;

float maxHeight;           //maximum height of the terrain

Texture2D heightMap;
Texture2D shadowMap;

Texture2D sandTexture;
Texture2D grassTexture;
Texture2D snowTexture;
Texture2D rockTexture;

SamplerState pointSampler;
SamplerState linearSampler;
SamplerState shadowMapSampler;

struct VS_INPUT
{
    float4 Position : SV_POSITION;
 float2 UV  : TEXCOORD0;
};

struct VS_OUTPUT
{
    float4 Position   : SV_POSITION;
 float4 Normal   : NORMAL0;
    float2 UV    : TEXCOORD0;
 float4 TextureWeights : TEXCOORD1; // Weights used for multitexturing 
 float  Depth            : TEXCOORD2; // Used for texture LOD
 float4 LightPosition : TEXCOORD3; // Position in light space

};

struct SHADOW_VS_INPUT
{
 float4 Position   : SV_POSITION;
    float2 UV    : TEXCOORD0;
};

struct SHADOW_VS_OUTPUT
{
 float4 Position : SV_POSITION;
 float4 Depth    : TEXTURE0;
};

//--------------------------------------------------------------------------------------
// Vertex Shader
//--------------------------------------------------------------------------------------
VS_OUTPUT VS(VS_INPUT input)
{
 float height = heightMap.SampleLevel(pointSampler, input.UV, 0).r;
 input.Position.y = height * maxHeight;

 float4x4 worldViewProj = mul(mul(WorldMatrix, ViewMatrix), ProjectionMatrix);
 float4x4 lightWorldViewProj = mul(mul(WorldMatrix, LightViewMatrix), LightProjectionMatrix);


 // Height values of adjacent vertices
 float y = input.Position.y;
 float N = heightMap.SampleLevel(pointSampler, input.UV, 0, float2(0,-1)).r * maxHeight;
 float S = heightMap.SampleLevel(pointSampler, input.UV, 0, float2(0,1)).r * maxHeight;
 float W = heightMap.SampleLevel(pointSampler, input.UV, 0, float2(-1,0)).r * maxHeight;
 float E = heightMap.SampleLevel(pointSampler, input.UV, 0, float2(1,0)).r * maxHeight;
 float NE = heightMap.SampleLevel(pointSampler, input.UV, 0, float2(1,-1)).r * maxHeight;
 float SW = heightMap.SampleLevel(pointSampler, input.UV, 0, float2(-1,1)).r * maxHeight;

 // Vectors joins adjacent vertices
 float3 vectorN = float3(0, N - y, 1);
 float3 vectorS = float3(0, S - y, -1);
 float3 vectorE = float3(1, E - y, 0);
 float3 vectorW = float3(-1, W - y, 0);
 float3 vectorNE = float3(1, NE - y, 1);
 float3 vectorSW = float3(-1, SW - y, -1);

 // Average normal for current vertex
 float3 normal = normalize(cross(vectorN, vectorNE) + cross(vectorNE, vectorE) + cross(vectorE, vectorS)
     + cross(vectorS, vectorSW) + cross(vectorSW, vectorW) + cross(vectorW, vectorN));

 VS_OUTPUT output = (VS_OUTPUT)0;
 output.Position = mul(input.Position, worldViewProj);
 output.Normal = mul(float4(normal, 1), WorldMatrix);

 float4 texWeights = 0;
  
 texWeights.x = saturate(1.0f - height * 3.0f);
 texWeights.y = saturate(1.0f - abs(height - 0.4f) * 3.0f);
 texWeights.z = saturate(1.0f - abs(height - 0.9f) * 3.0f);
 texWeights.w = 1;

 float totalWeight = texWeights.x + texWeights.y + texWeights.z;
 texWeights.xyz /= totalWeight;

 texWeights.w = 1;

 if (normal.y > 0.81f)
 {
  texWeights.w = 0;
 }
 else if (normal.y > 0.75)
 {
  texWeights.w = normal.y;
 }

 output.TextureWeights = texWeights;
 output.UV = input.UV;

 output.Depth = output.Position.z/output.Position.w;

 output.LightPosition = mul(input.Position, lightWorldViewProj);
 //output.LightPosition /= output.LightPosition.w; 

 return output;
}

//--------------------------------------------------------------------------------------
// Pixel Shader
//--------------------------------------------------------------------------------------
float4 PS(VS_OUTPUT input) : SV_TARGET
{
 float blendDistance = 0.99f;
 float blendWidth = 0.005f;
 float blendFactor = clamp((input.Depth - blendDistance) / blendWidth, 0, 1);

 float4 weights = input.TextureWeights;

 float scale = 4;
 float4 rock = rockTexture.Sample(linearSampler, input.UV * scale);
 float4 sand  = sandTexture.Sample(linearSampler,  input.UV * scale);
 float4 grass = grassTexture.Sample(linearSampler, input.UV * scale);
 float4 snow  = snowTexture.Sample(linearSampler,  input.UV * scale);
 float4 farColour = rock * weights.w + (1 - weights.w) * (sand * weights.x + grass * weights.y + snow * weights.z);

 scale = 16;
 rock = rockTexture.Sample(linearSampler, input.UV * scale);
 sand  = sandTexture.Sample(linearSampler,  input.UV * scale);
 grass = grassTexture.Sample(linearSampler, input.UV * scale);
 snow  = snowTexture.Sample(linearSampler,  input.UV * scale);
 float4 nearColour = rock * weights.w + (1 - weights.w) * (sand * weights.x + grass * weights.y + snow * weights.z);

 float4 colour = lerp(nearColour, farColour, blendFactor);

 //--------------------------------------------------------------------------------------
 // Shadows
 //--------------------------------------------------------------------------------------

 input.LightPosition.xyz /= input.LightPosition.w;

 float2 projectedTexCoords;
    projectedTexCoords[0] = input.LightPosition.x / 2.0f + 0.5f;
    projectedTexCoords[1] = input.LightPosition.y / -2.0f + 0.5f;

 float shadowMapDepth = shadowMap.Sample(shadowMapSampler, projectedTexCoords).r;

  if (shadowMapDepth < input.LightPosition.z - 0.0007f)
 {
  colour *= 0.25;
 }

 return colour;
}

//--------------------------------------------------------------------------------------
// Shadow Vertex Shader
//--------------------------------------------------------------------------------------
SHADOW_VS_OUTPUT ShadowMapVS(SHADOW_VS_INPUT input)
{
 input.Position.w = 1.0f;

 // Generate terrain height from heightmap
    float height = heightMap.SampleLevel(pointSampler, input.UV, 0).r;
 input.Position.y = height * maxHeight;

 //float4x4 lightWorldViewProj = mul(mul(WorldMatrix, LightViewMatrix), LightProjectionMatrix);

 SHADOW_VS_OUTPUT output = (SHADOW_VS_OUTPUT)0;
 //output.Position = mul(input.Position, lightWorldViewProj);

 output.Position = mul(input.Position,  WorldMatrix);
    output.Position = mul(output.Position, LightViewMatrix);
    output.Position = mul(output.Position, LightProjectionMatrix);

 output.Depth = output.Position;

    return output;
}
 
//--------------------------------------------------------------------------------------
// Shadow Pixel Shader
//--------------------------------------------------------------------------------------
float4 ShadowMapPS(SHADOW_VS_OUTPUT input) : SV_TARGET
{
 float zValue = input.Depth.z / input.Depth.w;

 return float4(zValue, zValue, zValue, 1.0f);
}

//--------------------------------------------------------------------------------------
// Techniques
//--------------------------------------------------------------------------------------
technique ShadowMap
{
    pass Pass0
    {
  SetVertexShader(CompileShader(vs_4_0, ShadowMapVS()));
  SetGeometryShader(NULL);
  SetPixelShader(CompileShader(ps_4_0,  ShadowMapPS()));
    }
}

technique Terrain
{
    pass Pass0
    {
        SetVertexShader(CompileShader(vs_4_0, VS()));
  SetGeometryShader(NULL);
  SetPixelShader(CompileShader(ps_4_0,  PS()));
    }
}


Edited by antmck2013, 19 April 2013 - 08:31 AM.





Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.



PARTNERS