Thursday, December 1, 2016

xna - HLSL Shadowmap shader for large scale environments


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 = ;
};

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();
}
}

Answer



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


No comments:

Post a Comment

Simple past, Present perfect Past perfect

Can you tell me which form of the following sentences is the correct one please? Imagine two friends discussing the gym... I was in a good s...