Hi there,
Lately i decided to dig up an old project of mine i stopped working on because shadows was giving me a hard time.
Well, my shadow issue is still there, and i need help to figure this thing out.
My problem :
I have shadow acne :
I am learning D3D programming with Franck Luna's book which comes with code sample i used in my engine.
The book says that acne is a common issue when working with shadow mapping, and is usually fixed by increasing the bias.
I did, and predictably the acne was gone, but then i had the "peter pan" issue where the shadow would look detached from it's source :
According to the book, you have to make lots of experiment with your scene to find the sweet spot (perfect bias i presume).
Well i tried a lot of different values, but instead of finding the perfect bias, i ended up having both peter pan AND acne.
When i start digging on the internet how to fix shadows, i often find articles about PCF filtering which and other advanced techniques which, if i am not mistaken, will not help me fix my acne issue but improve shadow edges quality.
Implementation :
I tried to implement Frank Luna's shadow mapping sample the best i could in my engine:
Main draw call :
void LightGame::DrawScene()
{
mGameComponents[GameComponent::Id::Land]->BuildShadowMap();
ResetRenderTarget();
mGameComponents[GameComponent::Id::Land]->Draw();
// ...
HR(mSwapChain->Present(0, 0));
}
1- I first build the Land's shadow map by rendering the land on the depth buffer only.
2- Then i reset the render target back to the backbuffer.
3- Then render the land normaly now that it has the shadow map.
This is how i build the shadow map. Note that the "Chunk" class represents the "Land" component.
void Chunk::BuildShadowMap()
{
// Prepare drawing on depth buffer only.
GetShadowMap()->SetRenderTargetToDepthBuffer();
// I build the transform matrix i'll use to transform the shadow map from light, to player perspective.
// I also initialize mLightView, and mLightProj in there.
BuildShadowTransform();
// Draw the land from the light perspective.
XMMATRIX view = XMLoadFloat4x4(&mLightView);
XMMATRIX proj = XMLoadFloat4x4(&mLightProj);
XMMATRIX viewProj = XMMatrixMultiply(view, proj);
mBuildShadowMapFX->SetViewProj(viewProj);
ID3DX11EffectTechnique* tessSmapTech = mBuildShadowMapFX->BuildShadowMapTech;
ID3D11DeviceContext* deviceContext = Graphics::GetInstance().GetImmediateContext();
deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
deviceContext->IASetInputLayout(mBuildShadowMapFX->GetInputLayout());
UINT stride[] = {sizeof(Vertex::Basic32), sizeof(InstanceData)};
UINT offset[] = {0, 0};
ID3D11Buffer* bufferPointers[2] = {mVB, mInstanceBuffer};
D3DX11_TECHNIQUE_DESC techDesc;
tessSmapTech->GetDesc( &techDesc );
deviceContext->IASetVertexBuffers(0, 2, bufferPointers, stride, offset);
deviceContext->IASetIndexBuffer(mIB, DXGI_FORMAT_R32_UINT, 0);
for(UINT p = 0; p < techDesc.Passes; ++p)
{
mBuildShadowMapFX->SetViewProj(viewProj);
mBuildShadowMapFX->SetTexTransform(XMMatrixScaling(2.0f, 1.0f, 1.0f));
tessSmapTech->GetPassByIndex(p)->Apply(0, deviceContext);
deviceContext->DrawIndexedInstanced(36, mNumberOfActiveCubes, 0, 0, 0);
}
}
This is how i build the transform matrix for the shadow map.
void Chunk::BuildShadowTransform(void)
{
DirectionalLight* sunLight = World::GetInstance().GetSunlight();
XMFLOAT3 lightPosFloat3;
XMVECTOR lightDir = XMLoadFloat3(&sunLight->Direction);
XMVECTOR lightPos = mBoundingSphere->Radius*(lightDir * -1.0f) * 2;
XMStoreFloat3(&lightPosFloat3, lightPos);
lightPosFloat3.x += mBoundingSphere->Center.x;
lightPosFloat3.z += mBoundingSphere->Center.z;
lightPos = XMLoadFloat3(&lightPosFloat3);
XMVECTOR targetPos = XMLoadFloat3(&mBoundingSphere->Center);
XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
XMMATRIX V = XMMatrixLookAtLH(lightPos, targetPos, up);
// Transform bounding sphere to light space.
XMFLOAT3 sphereCenterLS;
XMStoreFloat3(&sphereCenterLS, XMVector3TransformCoord(targetPos, V));
// Ortho frustum in light space encloses scene.
float l = sphereCenterLS.x - mBoundingSphere->Radius;
float b = sphereCenterLS.y - mBoundingSphere->Radius;
float n = sphereCenterLS.z - mBoundingSphere->Radius;
float r = sphereCenterLS.x + mBoundingSphere->Radius;
float t = sphereCenterLS.y + mBoundingSphere->Radius;
float f = sphereCenterLS.z + mBoundingSphere->Radius;
XMMATRIX P = XMMatrixOrthographicOffCenterLH(l, r, b, t, n, f);
// Transform NDC space [-1,+1]^2 to texture space [0,1]^2
XMMATRIX T(
0.5f, 0.0f, 0.0f, 0.0f,
0.0f, -0.5f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.0f, 1.0f);
XMMATRIX S = V*P*T;
XMStoreFloat4x4(&mLightView, V);
XMStoreFloat4x4(&mLightProj, P);
XMStoreFloat4x4(&mShadowTransform, S);
}
mBuildShadowMapFX shader :
#include "utility.fx"
cbuffer cbPerObject
{
float4x4 gViewProj;
float4x4 gTexTransform;
};
struct VertexIn
{
float3 PosL : POSITION0;
float3 NormalL : NORMAL;
float2 Tex : TEXCOORD;
float2 IsSelected : TEXCOORD1;
float3 World : POSITION1;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float2 Tex : TEXCOORD;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout;
float4x4 world = GetIdentity();
world[3][0] = vin.World.x;
world[3][1] = vin.World.y;
world[3][2] = vin.World.z;
float4x4 worldViewProj = mul(world, gViewProj);
vout.PosH = mul(float4(vin.PosL, 1.0f), worldViewProj);
vout.Tex = mul(float4(vin.Tex, 0.0f, 1.0f), gTexTransform).xy;
return vout;
}
RasterizerState Depth
{
DepthBias = 1000; // <- This is where i play with the bias !!!
DepthBiasClamp = 0.0f;
SlopeScaledDepthBias = 1.0f;
};
technique11 BuildShadowMapTech
{
pass P0
{
SetVertexShader( CompileShader( vs_5_0, VS() ) );
SetGeometryShader( NULL );
SetPixelShader( NULL );
SetRasterizerState(Depth);
}
}
And now the main shader rendering the land using the shadow map :
#include "light.fx"
#include "utility.fx"
float oneThird = 0.3334f;
float twoThird = 0.6667f;
float borderYThickness = 0.007f;
float borderXThickness = 0.007f / 3.0f;
float4 borderColor = float4(0.1f, 0.1f, 0.1f, 1.0f);
struct VertexIn
{
float3 PosL : POSITION0;
float3 NormalL : NORMAL;
float2 Tex : TEXCOORD0;
float2 IsSelected : TEXCOORD1;
float3 World : POSITION1;
float2 CubeType : TEXCOORD2;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float3 PosW : POSITION;
float3 PosVS : POSITION1;
float3 ViewRay : POSITION2;
float3 NormalW : NORMAL;
float2 Tex : TEXCOORD;
float IsSelected : DEPTH;
float4 ShadowPosH : TEXCOORD1;
float CubeType : DEPTH1;
};
cbuffer cbPerObject
{
float4x4 gWorld;
float4x4 gView;
float4x4 gWorldInvTranspose;
float4x4 gViewProj;
float4x4 gProjInvTranspose;
float4x4 gTexTransform;
float4x4 gShadowTransform;
Material gMaterial;
};
cbuffer cbPerFrame
{
DirectionalLight gDirLight;
float3 gEyePosW;
};
// Nonnumeric values cannot be added to a cbuffer.
Texture2DArray gBlockMapArray;
SamplerState Aniso
{
Filter = ANISOTROPIC;
MaxAnisotropy = 4;
AddressU = WRAP;
AddressV = WRAP;
};
SamplerComparisonState samShadow
{
Filter = COMPARISON_MIN_MAG_LINEAR_MIP_POINT;
AddressU = BORDER;
AddressV = BORDER;
AddressW = BORDER;
BorderColor = float4(0.0f, 0.0f, 0.0f, 0.0f);
ComparisonFunc = LESS_EQUAL;
};
RasterizerState DisableCulling
{
CullMode = BACK;
};
SamplerState samLinear
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = Wrap;
AddressV = Wrap;
};
Texture2D gShadowMap;
VertexOut VS(VertexIn vin)
{
VertexOut vout;
float4x4 world = GetIdentity();
world[3][0] = vin.World.x;
world[3][1] = vin.World.y;
world[3][2] = vin.World.z;
float4x4 WorldViewMatrix = mul(world, gView);
// Transform to world space.
vout.PosW = mul(float4(vin.PosL, 1.0f), world).xyz;
// Find transformed normals.
vout.NormalW = mul(vin.NormalL, (float3x3)world);
// Transform to homogeneous clip space.
vout.PosH = mul(float4(vout.PosW, 1.0f), gViewProj);
vout.Tex = mul(float4(vin.Tex, 0.0f, 1.0f), gTexTransform).xy;
vout.IsSelected = vin.IsSelected.x;
// Generate projective tex-coords to project shadow map onto scene.
float4x4 worldShadowTransform = mul(world, gShadowTransform);
vout.ShadowPosH = mul(float4(vin.PosL, 1.0f), worldShadowTransform);
float3 positionVS = mul(vin.PosL, gProjInvTranspose);
vout.ViewRay = float3(positionVS.xy / positionVS.z, 1.0f);
vout.PosVS = mul(float4(vin.PosL, 1.0f), WorldViewMatrix);
vout.CubeType = vin.CubeType.x;
return vout;
}
float GetPercentLit(SamplerComparisonState samShadow, Texture2D shadowMap, float4 shadowPosH)
{
// Complete projection by doing division by w.
shadowPosH.xyz /= shadowPosH.w;
// Depth in NDC space (from light source).
float depth = shadowPosH.z;
return shadowMap.SampleCmpLevelZero(samShadow, shadowPosH.xy, depth).r;
}
float4 PS(VertexOut pin) : SV_Target
{
// Highlighting edges of the block if it's selected.
if(pin.IsSelected == 1.0f)
{
if(pin.Tex.x > oneThird && pin.Tex.x < oneThird + borderXThickness)
{
return borderColor;
}
if(pin.Tex.x > twoThird - borderXThickness && pin.Tex.x < twoThird)
{
return borderColor;
}
if(pin.Tex.y > 0.0 && pin.Tex.y < borderYThickness)
{
return borderColor;
}
if(pin.Tex.y > 1.0 - borderYThickness)
{
return borderColor;
}
}
// Interpolating normal can unnormalize it, so normalize it.
pin.NormalW = normalize(pin.NormalW);
// Start with a sum of zero.
float4 ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
float3 uvw = float3(pin.Tex, pin.CubeType);
float4 texColor = gBlockMapArray.Sample(Aniso, uvw);
ambient = gMaterial.Ambient * gDirLight.Ambient;
// The light vector aims opposite the direction the light rays travel.
float3 lightVec = -gDirLight.Direction;
// Add diffuse and specular term, provided the surface is in
// the line of site of the light.
float diffuseFactor = dot(lightVec, pin.NormalW);
// Flatten to avoid dynamic branching.
[flatten]
if( diffuseFactor > 0.0f )
{
diffuse = diffuseFactor * gMaterial.Diffuse * gDirLight.Diffuse;
}
float percentLit = GetPercentLit(samShadow, gShadowMap, pin.ShadowPosH);
diffuse = diffuse * percentLit;
float4 litColor = texColor * (diffuse + ambient);
// Common to take alpha from diffuse material.
litColor.a = gMaterial.Diffuse.a;
return litColor;
}
technique11 BlockTech
{
pass P0
{
SetVertexShader( CompileShader( vs_5_0, VS() ) );
SetGeometryShader( NULL );
SetPixelShader( CompileShader( ps_5_0, PS() ) );
SetRasterizerState(DisableCulling);
}
}
I think this is it. I tried all kind of things, but i still do not understand where the issue is.
Any help or suggestion would be greatly appreciated :-)
I have a dependency on Effect11.lib i compiled with VS2013, so you might need to rebuild it if you have and older version of VS.