[XNA] [HLSL] Shadow mapping with directional light creates unexpected results

Started by
6 comments, last by DJTN 12 years, 10 months ago
I have a HLSL shader that draws a deferred directional light and calculates which pixels should be in shadow. The shadow calculations are taken from the Intel/Gamedev "Comparing Shadow Mapping Techniques with Shadow Explorer" article found here.

The problem is that not only do I get a shadow where it should be, I also get shading in areas where I do not want it.

The picture below shows where the shadow should be (yellow circle). The shadow map is the second from the left at the top of the picture. The light position is marked in white along with its bounding frustum.

DirectionalLightShadowProblem.png


My HLSL shader is as follows:



//=============================================
//---[Includes]--------------------------------
//=============================================

#include "..\include\Constants.fxh"
#include "..\include\Common.fxh"

//=============================================
//---[XNA to HLSL Variables]-------------------
//=============================================

bool CastShadow;
texture DepthMap;
float2 HalfPixel;
float4x4 InvViewProjection;
float3 LightColour;
float3 LightDirection;
float LightIntensity;
float3 LightPosition;
float4x4 LightViewProjection;
texture NormalMap; // Normals (with specular power in the alpha channel)
texture ShadowMap;

//=============================================
//---[Texture Samplers]------------------------
//=============================================

sampler NormalSampler = sampler_state
{
Texture = (NormalMap);
AddressU = CLAMP;
AddressV = CLAMP;
MagFilter = POINT;
MinFilter = POINT;
Mipfilter = POINT;
};

sampler DepthSampler = sampler_state
{
Texture = (DepthMap);
AddressU = CLAMP;
AddressV = CLAMP;
MagFilter = POINT;
MinFilter = POINT;
Mipfilter = POINT;
};

sampler ShadowSampler = sampler_state
{
Texture = (ShadowMap);
AddressU = CLAMP;
AddressV = CLAMP;
MagFilter = POINT;
MinFilter = POINT;
Mipfilter = POINT;
};

//=============================================
//---[Structs]---------------------------------
//=============================================

struct VertexShaderInput
{
float4 Position : POSITION0;
float2 TextureCoordinates : TEXCOORD0;
};

struct VertexShaderOutputToPS
{
float4 Position : POSITION0;
float2 TextureCoordinates : TEXCOORD0;
};

struct PixelShaderOutput
{
float4 Color : COLOR0; // Color
float4 SGR : COLOR1; // [R] Specular, [G] Glow and Reflection values
float4 Tangent : COLOR2; // Normal / Tangent map
float4 Depth : COLOR3; // Depth map
};

//=============================================
//---[Functions]-------------------------------
//=============================================

// [Output]
// return 1 for fully lit
// return 0 for fully occluded pixels
// @param fragDepth = light space depth
// @param tex = texture coordinates
// @param texShadowMap = shadow map texture
// @param dTex_dX, dTex_dY = partial derivative with respect to the screen-space x and y coordinates.
float IsNotInShadow(float fragDepth, float3 tex, float3 dTex_dX, float3 dTex_dY)
{
float2 receiverPlaneDepthBias = ComputeReceiverPlaneDepthBias(dTex_dX, dTex_dY);

//float2 fTexelSize = float2(1.0f / ShadowMapWidth, 1.0f / ShadowMapHeight);
//float2 g_shadowTexelSize = float2(fTexelSize.x, fTexelSize.y / CASCADE_LAYERS);

float texelSize = 1.0f / 1024;
float fractionalSamplingError = dot(float2(texelSize, texelSize / CASCADE_LAYERS), abs(receiverPlaneDepthBias));

// [DirectX 10] - VS 4.0 and PS 4.0 Miminum
// SampleGrad Arguments
// - SamplerState
// - Texture Coordinates
// - DDX
// - DDY
//return g_txShadow.SampleGrad(g_samShadowLinearClamp, tex, dTex_dX.xy, dTex_dY.xy).x > (fragDepth - EPSILON - fractionalSamplingError);

// [DirectX 9]
// ShadowSampler type ('SHADOW_MAP_TYPE') is:
// 'Texture2D' If TEXTURE_ARRAY is NOT defined
// 'Texture2DArray' If TEXTURE_ARRAY is defined
return tex2D(ShadowSampler, tex, dTex_dX.xy, dTex_dY.xy).r > (fragDepth - EPSILON - fractionalSamplingError);
}

//=============================================
//---[Vertex Shaders]--------------------------
//=============================================

VertexShaderOutputToPS VertexShaderFunction(VertexShaderInput input)
{
VertexShaderOutputToPS output = (VertexShaderOutputToPS)0;

// 'input.Position.w' should always equal 1
input.Position.w = 1;

output.Position = input.Position;

// [Texel To Pixel Align]
// • Half pixel offset for correct texel centering)
// • Should be done AFTER transformation
output.Position.xy -= HalfPixel;

output.TextureCoordinates = input.TextureCoordinates;

return output;
}

//=============================================
//---[Pixel Shaders]---------------------------
//=============================================

float4 DirectionalLightPS(VertexShaderOutputToPS input) : COLOR0
{
// [Normal]
float3 normalData = tex2D(NormalSampler, input.TextureCoordinates);

// Transform normal from [0, 1] texture coordinate range back into [-1, 1] range
float3 N = normalData.xyz * 2.0f - 1.0f;
N = normalize(N);

// [World Position]
float4 screenPosition;
screenPosition.x = input.TextureCoordinates.x * 2.0f - 1.0f;
screenPosition.y = -(input.TextureCoordinates.y * 2.0f - 1.0f);
screenPosition.z = tex2D(DepthSampler, input.TextureCoordinates).r;
screenPosition.w = 1.0f;

float4 worldPos = mul(screenPosition, InvViewProjection);
worldPos /= worldPos.w;

// ===============================================
// --[Shadow Calculations]------------------------
// ===============================================

// Shadow term (1 = no shadow)
float shadow = 1;

if (CastShadow)
{
// Find screen position as seen by the light
float4 lightScreenPos = mul(worldPos, LightViewProjection);
lightScreenPos /= lightScreenPos.w;

float depth; // Set by GetCascadeIndex
float3 dTex_dX, dTex_dY; // Set by GetCascadeIndex

float3 texShadow = GetCascadeIndex(lightScreenPos.xyz, worldPos.z, depth, dTex_dX, dTex_dY);

// Find shadow term (Range: [0, 1])
shadow = IsNotInShadow(depth, texShadow, dTex_dX, dTex_dY);
}

// ===============================================
// --[Lighting Calculations]----------------------
// ===============================================

/* ===============================================
* --[Diffuse Light]------------------------------
* ===============================================
* I = Di * Dc * N.L
*
* Where:
*
* I = Intensity Of Light
* Di = Diffuse Intensity [float]
* Dc = Diffuse Colour [float3]
* N = Surface Normal [float3]
* L = Direction From Pixel To Light [float3]
*
*/

float Di = LightIntensity;
float3 Dc = LightColour;
float NdotL = max(0, dot(N, -LightDirection)); // -LightDirection = toLight = Vertex to light
float3 DiffuseLight = Di * Dc * NdotL;

/* ===============================================
* --[Final Colour]-------------------------------
* ===============================================
*/

float4 colour = float4(DiffuseLight.rgb, 1);
colour.xyz *= shadow;

return colour;
}

//=============================================
//---[Techniques]------------------------------
//=============================================

technique DirectionalLight
{
pass Pass0
{
VertexShader = compile vs_3_0 VertexShaderFunction();
PixelShader = compile ps_3_0 DirectionalLightPS();
}
}



Currently I know that if I change the near clip values for directional light's projection matrix then the unwanted shadowing is made even worse.

What could be the cause of the extraneous shadowing?
Advertisement
I think this part may be wrong:



float4 worldPos = mul(screenPosition, InvViewProjection);
worldPos /= worldPos.w;



I'm not sure about this homogenous coordinate stuff, but I do recall you divide by w when transforming from world to projection. Since you're doing the reverse, from projection to world, you may have to do:



screenPosition *= screenPosition.w;
float4 worldPos = mul(screenPosition, InvViewProjection);


I also seem to recall only the z component gets divided by w, but I'm really not sure about that. If that's correct though, you'd need to do:


screenPosition.z *= screenPosition.w;
float4 worldPos = mul(screenPosition, InvViewProjection);



I hope this makes sense and isn't completely pointing you down the wrong path :D
Rim van Wersch [ MDXInfo ] [ XNAInfo ] [ YouTube ] - Do yourself a favor and bookmark this excellent free online D3D/shader book!
Thanks regimus but unfortunately your suggestions makes the light (as well as the shadows incorrect).

The light is no longer bound to the area of the cone and the shadows don't appear. :(
I've solved the problem. I needed to subtract a HalfTexel offset to prevent sampling the incorrect part of the shadow map.

What causes the constant "jiggling" of the shadow as the light moves up as shown in this video?

Should I be using LINEAR instead of POINT for the shadow map texture, or does it make little difference?
For shame my suggestion wasn't useful, it seemed logical enough. I guess that proves I need to read up on homogenous coordinates :rolleyes:

In good old uniform shadow mapping you need to set up point sampling, linear interpolation on the shadow map depth value doesn't make sense and can give incorrect results. [s]I'm afraid I don't have any good suggestions as to the cause of the jiggling.[/s] It looks like the jiggling happens only when you 'widen' the light cone, right? If so, it may be down to simple inaccuracy in point sampling the depthmap. I think most unfiltered shadow mapping would suffer from this.
Rim van Wersch [ MDXInfo ] [ XNAInfo ] [ YouTube ] - Do yourself a favor and bookmark this excellent free online D3D/shader book!
What causes the constant "jiggling" of the shadow as the light moves up as shown in this video?

Should I be using LINEAR instead of POINT for the shadow map texture, or does it make little difference?


Since the sampling of the shadow map is a binary test, and hardware linear sampling samples in screenspace (but you really want it to sample in lightspace), if you are sampling on the edge of a shadow map texel, small changes in light position can cause transformation into light space to return the sample of the pixel next to the previous one, which many be completely unshadowed. Another small change of the light position can cause you to sample the previous shadow map texel, suddenly placing that sample back in the shadow.This causes the shadow to "jiggle" or shimmer.

Larger sampling kernels help. VSM or a large PCF sampling kernel will reduce (but not remove) the shimmering effect.

This is worth a read:
http://msdn.microsof...v=vs.85%29.aspx

It includes a fix for shimmering directional light shadows, which is essentially to clamp sampling increments to one pixel in size (so tiny changes in sampling position do not cause the shadow to shimmer, because a minimum sampling coordinate movement is enforced).
Thanks for the ideas guys. I've got some reading to do :)
What is the image format/resolution you are using to store the shadow data? The shadow map data image you've displayed doesn't look correct to me, looks zoomed in. Shadow mapping gets confusing to me because you're drawing the objects to create shadow data, drawing the objects normally and applying the shadow, it's a lot of code for one feature. When I have issues with my shadow maps I usually move the camera to the lights position/direction so I can see what it's seeing to build the shadow map. Because you mentioned it gets worse when you change the near clip planes, I'd check your math that builds the view projections when building the shadow data.

As for the jiggling light, it looks like a percision error..

.

[edit] - sorry just seen where you fixed your issue by adding bias, please disregard my post [/edit]

This topic is closed to new replies.

Advertisement