Jump to content
  • Advertisement
Sign in to follow this  
Yann ALET

DX11 Can't fix shadow acne with bias

This topic is 1496 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

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 :-)
 
BTW, the full source is available here if you're interested : https://www.dropbox.com/s/0rmiln0bua3pshk/source.zip?dl=0
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.

Share this post


Link to post
Share on other sites
Advertisement
I've recently had some success with normal offset shadows although it is no panacea. Very simple to implement though.

Share this post


Link to post
Share on other sites

Bearing in mind that I'm new to shadow mapping myself:

 

Thin geometry will increase the peter-panning effect, so, if your geometry is thinner than your bias, you end up with that offset, if I understand things correctly. You can try adding depth to your ground geometry. It's perhaps not an ideal solution (as you increase your vertex count), but it should help.

 

For the shadow acne, try culling the front faces when rendering your shadow map (if you're not already. I didn't see it in your code, but I'm not terribly familiar with directx. But, I imagine directx lets you front/back cull faces). Then go back to back-face culling when rendering the scene.

 

I'm sure someone will have some better advice, but these are things that I would try. Good luck!

Share this post


Link to post
Share on other sites


For the shadow acne, try culling the front faces when rendering your shadow map (if you're not already. I didn't see it in your code, but I'm not terribly familiar with directx. But, I imagine directx lets you front/back cull faces). Then go back to back-face culling when rendering the scene.

yes do that, render backfaces to your shadowmap

 

think about how big a texel from the shadowmap is if it's projected onto your terrain, clearly the area each texel has to cover is bigger if the light is low above the ground and producing very long shadows. biasing can correct this to a certain extent but if your shadowmap resolution is too small that won't help either. a simple solution would be to just increase the shadowmap resolution, and/or limit the possible light angle.

i think a proper solution would be to use a cascaded shadowmapping technique

Share this post


Link to post
Share on other sites

Thanks for all your suggestions !

 

I'll try Aardvajk's approach 1st.

I tried to implement your method, but it doesn't give me the expected result.

The shader i build the shadow map with, is actually a very simple vertex shader where i just draw the scene on the deph buffer :

 

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

My first attempt was to slightly "push" vertices away from their position in the direction of their normal vector :

 

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 + vin.NormalL * 0.2f, 1.0f), worldViewProj);
vout.Tex  = mul(float4(vin.Tex, 0.0f, 1.0f), gTexTransform).xy;

return vout;
}

This made things worse :

 

5YctwWK.png

 

I think i probably did this wrong, but i am not exactly sure what i missed here. Just to be sure, i increased the factor by 2 :

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 + vin.NormalL * 2.0f, 1.0f), worldViewProj);
vout.Tex  = mul(float4(vin.Tex, 0.0f, 1.0f), gTexTransform).xy;

return vout;
}

XAdk55L.png

 

We can see on the depth buffer faces of cubes going away from each others in the direction of normals, i i think that's the point of this technique.

 

Then i tried a similar approach by trying to scale the scene up by 5% just for the shadow map, and this was also a failure : 

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.05f, 1.0f), worldViewProj);
vout.Tex  = mul(float4(vin.Tex, 0.0f, 1.0f), gTexTransform).xy;

return vout;
}

9ifS6FR.png

 

I'll keep experimenting though :-)

Share this post


Link to post
Share on other sites

You don't use the  normal-offset approach at shadow map creation but when sampling the shadowmap.

 

Ohhhhhh !!!

how did i miss that ?! Ok i'll try right now :-)

 

Thanks !

Share this post


Link to post
Share on other sites

Omg it works, thank you all so much !!!

 

// Generate projective tex-coords to project shadow map onto scene.
float4x4 worldShadowTransform = mul(world, gShadowTransform); 
vout.ShadowPosH = mul(float4(vin.PosL + (vin.NormalL*0.1f), 1.0f), worldShadowTransform);

yu2zeXg.png

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!