Can't fix shadow acne with bias

Started by
22 comments, last by Yann_A 9 years, 2 months ago
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.
Advertisement
I've recently had some success with normal offset shadows although it is no panacea. Very simple to implement though.

Hummm normal offset shadow ? Never heard of that (still very new to rendering).

Any article, tutorial to suggest ?

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!

Beginner here <- please take any opinions with grain of salt

Normal offset shadows is a very simple idea. Basically you just add a small amount of the normal of the surface you are shadowing to the position you use for lookup. My implementation looks like this:


vector pos = mul(input.vpos + vector(input.normal * 0.4f, 0), worldviewproj);

float2 c = 0.5f * pos.xy / pos.w + float2(0.5f, 0.5f);
c.y = 1.0f - c.y;

float s = 1 - newsample(c, (1 - (pos.z / pos.w)) + 0.005f);

If you look at the first line, I add the (world space) normal of the pixel (from the vertex shader) times a fudge factor to the position I use to lookup in the shadow map (worldviewproj is the shadow matrix here and input.vpos is the original world position from the vertex shader).

There are some diagrams floating around on the internet showing why this works, but it isn't much covered yet and no comprehensive write ups that I've found, but it is a very simple idea to try out. Some people have found it isn't suitable for their particular use cases but seems to work great for mine.

If I remove this offset, I have horrendous shadow acne all over the view. This removes it all perfectly.


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

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 :-)

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

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 !

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

This topic is closed to new replies.

Advertisement