Sign in to follow this  

Help deducing cause for cubic shadow mapping edge artifact?

Recommended Posts

Posted (edited)

Yep, it's time for even more shadow mapping issues on my end...

 

I'm encountering a rather weird artifact with my omnidirectional light's shadow mapping, as can be seen in the following image: 

scene.png

It appears to align with the corners where the shadow cube's faces meet. I'm sampling the skybox (also a cubemap) in the same way though, so I don't think my sampling direction is faulty:

#if SHADER_MODEL >= 41
	float4 moments = ShadowCubeEV.SampleGrad(
		ShadowMapEVSampler, 
		float4(shadowVector, arraySlice), 
		shadowVectorDDX, 
		shadowVectorDDY
	);
#else
	float4 moments = ShadowCubeEV.SampleGrad(ShadowMapEVSampler, shadowVector, shadowVectorDDX, shadowVectorDDY);
#endif

The shadow "direction" vector (it is actually a non-unit vector, which works fine with TextureCube sampling) is simply computed as the world-space distance between the light source and the pixel being shaded:

float3 shadowVector	= -(PointLight[lightId].wPos - P); // World-space distance between light source and fragment being shaded
float3 shadowVectorDDX	= ddx(shadowPos); // These may possibly be wrong, but shouldn't cause the afforementioned issues?
float3 shadowVectorDDY  = ddy(shadowPos); // Need to be supplied as different code branches provide manual partial derivatives

Furthermore I'm using exponential variance shadow maps. The shadow maps are generated from normal depth maps, rendered using a 90° FOV perspective camera with its range set to the radius of the light source and generated for each cardinal direction in the scene (east, west, north, south, up and down). I'm translating the shaded pixel's world-space coordinate into the light space of these maps like so:

float lightSpaceDepth = (light.matProj._43 / length(shadowVector)) + light.matProj._33;

light.matProj is the projection matrix used when rendering the depth maps as described above.

There is nothing in the shadow maps that to me would hint at the experienced artifact either; feel free to have a look for yourself (click on the images to view higher resolution versions):

 

evsm_single_positive_thumb.png

Positive moment (x component)
Range: ~1.85x1017 .. 2.35x1017

 

evsm_single_negative_thumb.png

Negative moment (y component)
Range: ~6.94x10-3 .. 6.74x10-3

 

evsm_quad_positive_thumb.png

Positive quadratic moment (z component)
Range: ~3.42x1034 .. 5.54x1034

 

evsm_quad_negative_thumb.png

Negative quadratic moment (w component)
Range: 4.50x105 .. 5.00x105

 

So my question then is whether anybody happen to recognize the artifact and / or have any ideas what may be causing it? More code / images can be provided on request.

 

 

Update 1: It appears that this is a self-shadowing issue near the edges of the cubemap faces. If the ground object is excluded from the shadow pass, the floor artifacts go away, however the digit meshes aligning with these corner directions (1/4/6/9) then show severe self-shadowing as well. It is not (realistically) rectifiable by using various depth / variance biases. 

Edited by Husbjörn

Share this post


Link to post
Share on other sites

Shameless bump here, but may it be that my way of calculating the light space depth (see the first post) does not hold up as the cube face edges are approached? The self-shadowing certainly increases the closer to such an edge it is.

Share this post


Link to post
Share on other sites
Posted (edited)

No worries. The artifact would be the large "4-pointed star" false shadow on the floor mesh. I didn't make it clear, but the light source is in the middle of the ring of digit meshes, a bit above their top heights, so it isn't at floor level.

Here are two new screenshots, including a decal to indicate the light source's position in the scene:

ground.png
Floor self-shadowing with harder edges to better show the outline.

 

meshes.png

As you can see here there is significant self-shadowing on the '4' and '1' meshes, which align with the corners of the shadow map faces (the shadowed areas in the first image), while the other meshes (which end up more in the middle of their respective shadow map faces) do not show this. The floor mesh has been removed from the shadow mapping calculations to better emphasize that a similar self-shadowing effect is present on other meshes as well.

 

 

Edit: the camera is facing north (+Z) in all screenshots, while the shadow map is rendered along the +X, -X, +Y, -Y, +Z and -Z directions from the middle of the decal's position above.

Edited by Husbjörn

Share this post


Link to post
Share on other sites

Ahh I see now, sorry for the confusion. Perhaps there's a mismatch between the depth you're storing in your EVSM maps, and the depth you're comparing against when computing lighting? Would you mind posting the shader code that you use for rendering the shadow map, and converting to EVSM moments?

Share this post


Link to post
Share on other sites

Certainly. As a matter of fact my EVSM conversion is based on your Shadows example implementation  :)

I have quite a few different functions coming together in my lighting system, and I'm also using various wrapper classes for D3D functionality, but I hope it will make sense.

 

 

So first out, the shadow cube faces are rendered in a single pass using a geometry shader which copies the scene meshes into 6 different spaces and writes them to different render target array indices. The main program code for this looks like so:

// This is a temporary Texture2DArray with 8 slices. Initial depth maps are rendered here until
// it fits no more (ie. 2x4 for 2 directional lights, can pad out with single slices for spot lights).
// For point lights I'm currently just rendering to slices 0..5.
gGlob.pShadowCamera->SetDepthStencilBuffer(evsmDepthBuffer);

// Provide cube mapping view-projection transforms to the shader technique
const float		radius	= light->GetRange();
const XMMATRIX	matView = XMMatrixInverse(nullptr, light->GetTransform().GetFinalTransform());
const XMMATRIX	matProj = XMMatrixPerspectiveFovLH((float)D3DXToRadian(90.0f), 1.0f, 0.1f, radius);
{
	XMFLOAT3 lightPos;
	light->GetTransform().GetFinalPosition(lightPos);
	const XMVECTOR origin	= XMLoadFloat3(&lightPos);
	static const XMVECTOR lookat[6] {
		origin + XMVectorSet(1.0f, 0.0f, 0.0f, 0.0f),
		origin + XMVectorSet(-1.0f, 0.0f, 0.0f, 0.0f),
		origin + XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f),
		origin + XMVectorSet(0.0f, -1.0f, 0.0f, 0.0f),
		origin + XMVectorSet(0.0f, 0.0f, 1.0f, 0.0f),
		origin + XMVectorSet(0.0f, 0.0f, -1.0f, 0.0f),
	};
	static const XMVECTOR up[6] {
		XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f),
		XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f),
		XMVectorSet(0.0f, 0.0f, -1.0f, 0.0f),
		XMVectorSet(0.0f, 0.0f, 1.0f, 0.0f),
		XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f),
		XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f)
	};

	for (size_t n = 0; n < 6; n++)
		cbCubeMatrix->Set<XMMATRIX>(sizeof(XMMATRIX) * n, XMMatrixLookAtLH(origin, lookat[n], up[n]) * matProj);
		
	auto pBuffer = cbCubeMatrix->ResolveD3DBuffer();
	gGlob.pDevCon->GSSetConstantBuffers(ResourceSlotBindings::CB_CUBEMAP_TRANSFORM, 1, &pBuffer);	// Only needed by the geometry shader
}

/* MESH CULLING CODE OMITTED */

// Render the depth (shadow) map (note that this automatically clears the associated depth-stencil view)
gGlob.pShadowCamera->RenderShadowMap(SHADOWTECH_POINT, gGlob.pCullingCamera, true);	// Only a single render pass here so we can reset the per-pass data
// And store the projection matrix so that it is available for transforming sampled depths on the GPU
light->SetProjectionMatrix(matProj); // This one is transposed and written into a StructuredBuffer if different from the previous value

And here are the shaders used in the above render:

struct gs_depth_in {
	float4 pos	: SV_POSITION;
	float4 posW	: WORLD_POSITION;
	float2 texcoord : TEXCOORD;
};

struct gs_depth_out {
	float4 pos	: SV_POSITION;
	float2 texcoord : TEXCOORD;
	uint   rtIndex	: SV_RENDERTARGETARRAYINDEX;
};

cbuffer CubeMatrix : register(CBID_CUBEMAP_TRANSFORM) {
	float4x4 CubeViewProj[6];
};

gs_depth_in VS_CubicalShadowMapping(vs_in IN) { 
	gs_depth_in OUT;
	OUT.posW	= mul(IN.pos, Transform[IN.iID].World);
	OUT.pos		= mul(OUT.posW, ViewProj);
	OUT.texcoord	= IN.texcoord;

	return OUT;
}

[maxvertexcount(18)]
void GS_CubicalShadowMapping(triangle gs_depth_in input[3], inout TriangleStream<gs_depth_out> TriStream) {
	gs_depth_out output;

	for (uint rt = 0; rt < 6; rt++) {	// Should be automatically unrolled by the 
		for (uint v = 0; v < 3; v++) {	// shader compiler as the range is constant
			output.pos	= mul(input[v].posW, CubeViewProj[rt]);
			output.texcoord = input[v].texcoord;
			output.rtIndex	= rt;

			TriStream.Append(output);
		}
		TriStream.RestartStrip();
	}
}

void PS_CubicalShadowMapping(gs_depth_out IN) {	
	clip(DiffuseMap.Sample(DiffuseSampler, IN.texcoord).a - 0.1);
	return;
}

The pixel shader is only used for objects with transparent textures, ie. in my example scene I'm using a null pixel shader.

 

 

Following this the depth map arrays are converted to the exponential variance map. This is the actual TextureCube, or TextureCubeArray if using feature level 4.1 or above (the temporary depth map is just a texture array without the cubemap flag set). The CPU-side code for this is as follows:

// This version handles multiple simultaneous render targets
void LightManager::GenerateExponentialShadowMaps(PImage depthArray, PImage output, size_t firstDepthSlice, size_t firstOutputSlice, size_t numSlices) { 
#ifndef NDEBUG
	assert(numSlices <= 8);
#endif

	// Assume that the input (depth buffer) is currently bound to the output depth-stencil buffer slot; as such we'll need to unbind it.
	gGlob.pShadowCamera->SetDepthStencilBuffer(nullptr);	// We don't need any depth stencil buffer for this; indeed the shader technique disables depth writes / reads
	
	// Set output render targets
	for(size_t n = 0; n < numSlices; n++) {
		gGlob.pShadowCamera->SetRenderTarget(output, firstOutputSlice + n, n, true);
		cbAddressEVSM->Set<UINT>(n * sizeof(XMUINT4), firstDepthSlice + n);	// NOTE: HLSL interprets each array element as a 4-component (128-bit) vector in order to speed up indexing
	}
	cbAddressEVSM->ResolveD3DBuffer();	// Update technique cbuffer to reflect the target input depth slices to sample

	// Begin scene drawing
	gGlob.BeginRenderPass(gGlob.pShadowCamera, false, true, true);	// We don't have to clear the render target or set the frame buffer as the shaders do not rely on these
	// Set input layout
	Mesh::SetCurrentTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
	// Apply necessary shader technique changes (TODO: consider changing this to only update the data that actually *needs* to be changed)
	techEVSMGenerator[numSlices - 1]->Set();
	// Set the input texture
	gGlob.pCurrentTexture[0]		= depthArray.operator->();
	gGlob.pCurrentTexResourceView[0]	= depthArray->GetResourceView();
	gGlob.pDevCon->PSSetShaderResources(0, 1, gGlob.pCurrentTexResourceView);
	// Draw screen quad
	gGlob.pDevCon->DrawInstanced(4, 1, 0, 0);
	// Unbind the texture again in anticipation of this probably being rendered to again in the (likely immediately) following depth pass
	gGlob.pCurrentTexture[0]		= nullptr;
	gGlob.pCurrentTexResourceView[0]	= nullptr;
	gGlob.pDevCon->PSSetShaderResources(0, 1, gGlob.pCurrentTexResourceView);
	// Conclude draw pass (the render target should be overwritten before there is any reason to assume it would be bound as input)
	gGlob.EndRenderPass(gGlob.pShadowCamera, true);
}

The "shader technique" mentioned in the comments above is just a collection of shaders, constant buffers and various states. The states used for the techEVSMGenerators disable blending (BLEND_ONE for source, BLEND_ZERO for target), sets AlphaLineAntiAliasingEnabled = true and disables depth reads / writes like so:

dss.disabled = PDepthStencilState(new DepthStencilState());
dss.disabled->SetDepthEnabled(false);
dss.disabled->SetStencilEnabled(false);
dss.disabled->SetDepthFunction(D3D11_COMPARISON_LESS);
dss.disabled->SetStencilWriteMask(0xff);
dss.disabled->SetStencilReadMask(0xff);
dss.disabled->SetFrontFaceStencilFunction(D3D11_COMPARISON_ALWAYS);
dss.disabled->SetFrontFaceStencilPassOperation(D3D11_STENCIL_OP_KEEP);
dss.disabled->SetFrontFaceStencilFailOperation(D3D11_STENCIL_OP_KEEP);
dss.disabled->SetFrontFaceStencilDepthFailOperation(D3D11_STENCIL_OP_KEEP);
dss.disabled->SetBackFaceStencilFunction(D3D11_COMPARISON_ALWAYS);
dss.disabled->SetBackFaceStencilPassOperation(D3D11_STENCIL_OP_KEEP);
dss.disabled->SetBackFaceStencilFailOperation(D3D11_STENCIL_OP_KEEP);
dss.disabled->SetBackFaceStencilDepthFailOperation(D3D11_STENCIL_OP_KEEP);

The vertex shader creates a "screen quad" like so (the viewport size is set to the dimensions of the EVSM (the render target at this point)):

// Vertex locations in clip space for a screen quad
static const float4x4 Vertex = {
	-1, -1, 0, 1, 
	-1, 1, 0, 1, 
	1, -1, 0, 1, 
	1, 1, 0, 1
};

// Likewise we can set up a constant array of the texture coordinates for the corner vertices of the screen quad
static const float4x2 Texcoord = {
	0, 1, 
	0, 0, 
	1, 1, 
	1, 0
};

// Simple vertex shader output struct
struct vs_out {
	float4 pos	: SV_POSITION;
	float2 texcoord : TEXCOORD;
};


// The vertex shader program
vs_out VS_ScreenQuad(uint id : SV_VertexId) {
	vs_out OUT;
	
	OUT.pos		= Vertex[id];
	OUT.texcoord	= Texcoord[id];
	
	return OUT;
}

The pixel shader is set based on MSAA settings and the number of simultaneous inputs / outputs. Corresponding multisampling is enabled on the temporary depth buffer array described above. Here is the source for the version that converts 6 maps with 8x MSAA, which is the one used in my previously posted images. Take note that changing the MSAA level / disabling it in no way affects the shadow artifacts.

struct sOutput6 {
	float4 channel1 : SV_Target0;
	float4 channel2 : SV_Target1;
	float4 channel3 : SV_Target2;
	float4 channel4 : SV_Target3;
	float4 channel5 : SV_Target4;
	float4 channel6 : SV_Target5;
};

// ....

sOutput6 PS_EVSM_6_MSAA8(vs_out IN) : SV_Target0 {
	sOutput6 OUT;

	const float weight = 1.0 / 8.0;
	const int3 coord[6] = {
		int3(IN.pos.xy, SourceSlice[0]),
		int3(IN.pos.xy, SourceSlice[1]),
		int3(IN.pos.xy, SourceSlice[2]),
		int3(IN.pos.xy, SourceSlice[3]),
		int3(IN.pos.xy, SourceSlice[4]),
		int3(IN.pos.xy, SourceSlice[5])
	};
	float4 average[6] = {
		float4(0, 0, 0, 0),
		float4(0, 0, 0, 0),
		float4(0, 0, 0, 0),
		float4(0, 0, 0, 0),
		float4(0, 0, 0, 0),
		float4(0, 0, 0, 0)
	};

	[unroll]
	for (uint s = 0; s < 6; s++) {
		[unroll]
		for (uint n = 0; n < 8; n++) {
			average[s] += evsm::ComputeMoments(DepthMSAA8.Load(coord[s], n)) * weight;
		}
	}

	OUT.channel1 = average[0];
	OUT.channel2 = average[1];
	OUT.channel3 = average[2];
	OUT.channel4 = average[3];
	OUT.channel5 = average[4];
	OUT.channel6 = average[5];
	return OUT;
}

And  the EVSM helper functions:

namespace evsm {
	/**
	 * Helper function; applies exponential warp to the specified depth value (given in the 0..1 range) 
	 **/
	float2 WarpDepth(float depth, float2 exponents) {
		depth = 2.0 * depth - 1.0;	// Rescale to (-1..+1) range
		return float2(exp(exponents.x * depth), -exp(-exponents.y * depth));
	}

	/**
	 * Helper function; retrieves the negative and positive exponents used for warping depth values.
	 * The returned exponents will be clamped to fit in a 32-bit floating point value.
	 **/
	float2 GetSafeExponents(float positiveExponent, float negativeExponent) {
		float2 res = float2(positiveExponent, negativeExponent);
		return min(res, 42.0);
	}

	/**
	 * Reduces light bleeding
	 **/
	float ReduceLightBleeding(float pMax, float amount) {
	   // Remove the [0, amount] tail and linearly rescale [amount, 1].
	   return Linstep(amount, 1.0f, pMax);
	}

	/**
	 * Helper function; computes probabilistic upper bound
	 **/
	float ChebyshevUpperBound(float2 moments, float mean, float minVariance, float lightBleedingReduction) {
		// Compute variance
		float variance = moments.y - (moments.x * moments.x);
		variance = max(variance, minVariance);

		// Compute probabilistic upper bound
		float d = mean - moments.x;
		float pMax = variance / (variance + (d * d));

		pMax = ReduceLightBleeding(pMax, lightBleedingReduction);

		// One-tailed Chebyshev
		return (mean <= moments.x ? 1.0f : pMax);
        }

            /**
             * Helper function; computes the EVSM moments based on the provided (clip) depth value (range 0..1).
             **/
            float4 ComputeMoments(float depth) {
                float2 vsmDepth = evsm::WarpDepth(depth, VarianceExponents); // UPDATE: exponents provided through cbuffer, pre-clamped
                return float4(vsmDepth.xy, vsmDepth.xy * vsmDepth.xy);
            }

};

 
    

And the Linstep utility function:

/**
 * Linear "stepping" function, akin to the smoothstep intrinsic.
 * Returns 0 if value < minValue, 1 if value > maxValue and otherwise linearly 
 * rescales the value such that minValue yields 0 and maxValue yields 1.
 **/
float Linstep(float minValue, float maxValue, float value) {
	return saturate((value - minValue) / (maxValue - minValue));
}

The EVSM cube can then be blurred in two steps via an intermediary texture. Disabling this blurring does not affect the artifacts, but here are the pixel shaders all the same. The same screen quad vertex shader from above is used:

/* Six render targets */
ps_out6 PS_MultiBlur6_Horizontal(vs_out IN) {
	uint2	coords = uint2(IN.pos.xy);
	ps_out6 res;	// Zero-initialized by default

	[loop]
	for(int i = -SampleRadius; i <= SampleRadius; i++) {
		uint2 tc	= uint2((uint)clamp((int)coords.x, 0, MapSize), coords.y);
		float weight	= Kernel[KernelOffset + i].weight;
		res.result1 += TextureArray[uint3(tc, ArraySlice)] * weight;
		res.result2 += TextureArray[uint3(tc, ArraySlice + 1)] * weight;
		res.result3 += TextureArray[uint3(tc, ArraySlice + 2)] * weight;
		res.result4 += TextureArray[uint3(tc, ArraySlice + 3)] * weight;
		res.result5 += TextureArray[uint3(tc, ArraySlice + 4)] * weight;
		res.result6 += TextureArray[uint3(tc, ArraySlice + 5)] * weight;
	}

	return res;
}

ps_out6 PS_MultiBlur6_Vertical(vs_out IN) {
	uint2   coords = uint2(IN.pos.xy);
	ps_out6 res;	// Zero-initialized by default

	[loop]
	for(int i = -SampleRadius; i <= SampleRadius; i++) {
		uint2 tc	= uint2(coords.x, (uint)clamp((int)coords.y + i, 0, MapSize));
		float weight	= Kernel[KernelOffset + i].weight;
		res.result1 += TextureArray[uint3(tc.xy, ArraySlice)] * weight;
		res.result2 += TextureArray[uint3(tc.xy, ArraySlice + 1)] * weight;
		res.result3 += TextureArray[uint3(tc.xy, ArraySlice + 2)] * weight;
		res.result4 += TextureArray[uint3(tc.xy, ArraySlice + 3)] * weight;
		res.result5 += TextureArray[uint3(tc.xy, ArraySlice + 4)] * weight;
		res.result6 += TextureArray[uint3(tc.xy, ArraySlice + 5)] * weight;
	}

	return res;
}

That concludes the shadow map generation.
Finally the scene is rendered "normally". This is the relevant part of the pixel shader that samples the shadow map. The pointLight[x].matProj matrix is the same one that was set near the beginning of this post and is used to translate the into the appropriate light space depth range used by the shadow maps.
P is the world-space position of the fragment currently being shaded and pointLight[lightId].wPos is the world-space position of the light source. The PointLightTable is used to only process visible point lights in the scene.

	/* Process point lights */
	tableIndex	= 0;
	lightId		= pointLightData.y > 0 ? PointLightTable.Load(pointLightData.x) : 0xffffffff;		// Used to offset by + tableIndex * 4, but that is always 0 here so
	[loop]
	for(n = 0; n < NumPointLights; n++) {
		// Figure out sample location vector (normal) and partial derivatives
		float3 shadowPos    = -(PointLight[lightId].wPos - P);
		float3 shadowPosDDX = ddx(shadowPos);
		float3 shadowPosDDY = ddy(shadowPos);

		[branch]
		if(n == lightId) {
			sLightContrib contrib;
			contrib = ComputePointLightContribNew(PointLight[lightId], V, P.xyz, N, shadowPos, shadowPosDDX, shadowPosDDY);
			total.diffuse  += contrib.diffuse;
			total.specular += contrib.specular;

			// Look for next light table index if applicable
			if(++tableIndex < pointLightData.y)
				lightId = PointLightTable.Load(pointLightData.x + (tableIndex * 4));
			else
				lightId = 0xffffffff;
		}
	}
/**
 * New, simplified implementation for use with cubical exponential variance shadow maps.
 **/
sLightContrib ComputePointLightContribNew(sPointLight light, float3 V, float3 P, float3 N, float3 shadowPos, float3 shadowPosDDX, float3 shadowPosDDY) {
	sLightContrib res = (sLightContrib)0;

	// Compute distance and a direction vector between the currently rendered pixel's world position (P) and the light's position
	float3 L	= light.wPos - P;
	float  dist 	= length(L);
	L /= dist;

	// Sample shadow map if applicable; no need to do lighting calculations if in (complete) shadow
	float depth		= (light.matProj._43 / length(shadowPos)) + light.matProj._33;
	float shadowFactor 	= SampleExponentialVarianceShadowCube(depth, shadowPos, shadowPosDDX, shadowPosDDY, light.lightBleedingReduction, light.minVarianceBias, light.shadowMapId);

	[branch]
	if(shadowFactor != 0.0) {
		// Compute attenuation
		float att = saturate(CalculateAttenuation(dist, light.attCoef.x, light.attCoef.y, light.attCoef.z)) * light.intensity * shadowFactor;

		// Compute diffuse and specular contributions of this light source
		res.diffuse	= CalculateDiffuseContrib(L, N, light.colour) * att;
		res.specular	= CalculateSpecularContrib(V, L, N, light.colour, 20.0) * att;		// TODO: Allow changing the specular intensity later on, probably through another argument to this function
	}

	return res;
}

And finally, the actual shadow map sampling function. The EVSM-functions are the same that were presented above:

/**
 * Samples a cubical exponential variance shadow map.
 * As the sampled texture is a cubemap, it is sampled using the provided normal vector (which doesn't need to
 * be normalized). The light space depth should correspond to what is stored in the shadow map, ie. be a 
 * non-linear light projection clip space coordinate.
 **/
float SampleExponentialVarianceShadowCube(
	float  lightSpaceDepth, 
	float3 normal, 
	float3 normalDDX, 
	float3 normalDDY, 
	float  lightBleedingReduction, 
	float  minVarianceBias, 
	uint   arraySlice
) {
	// Sample shadow map
	float2 warpedDepth = evsm::WarpDepth(lightSpaceDepth, VarianceExponents);
#if SHADER_MODEL >= 41
	float4 moments		= ShadowCubeEV.SampleGrad(
		ShadowMapEVSampler, 
		float4(normal, arraySlice), 
		normalDDX, 
		normalDDY
	);
#else
	float4 moments		= ShadowCubeEV.SampleGrad(ShadowMapEVSampler, normal, normalDDX, normalDDY);
#endif

	float2 depthScale	= minVarianceBias * 0.01 * VarianceExponents * warpedDepth;
	float2 minVariance	= depthScale * depthScale;

	float posContrib	= evsm::ChebyshevUpperBound(moments.xz, warpedDepth.x, minVariance.x, lightBleedingReduction);
	float negContrib	= evsm::ChebyshevUpperBound(moments.yw, warpedDepth.y, minVariance.y, lightBleedingReduction);
	return min(posContrib, negContrib);
}

That should be about it, let me know if I've missed anything or if something is unclear (I bet lots of things are  ^_^).

 

Edit: I apologize for the bad spacing; apparently the editor insists on removing empty lines between code snippets.

Edit 2: Added the evsm::ComputeMoments HLSL function that I accidentally left out before.

Edited by Husbjörn

Share this post


Link to post
Share on other sites
Sorry for the delay: I've been meaning to get back to this but it's been a busy week.
 
Anyhow, it does indeed look like you have a mismatch between the depth value that you're using to compute the warped shadow map, and the depth value that you're using to compare against it during the lighting phase. It looks like you have 6 perspective projections for rendering your cubemap, with each projection oriented along one of the major world-space axes (+/- X, Y, and Z). After rendering a depth buffer for each cubemap face, you run your EVSM conversion shader that loads directly from the depth buffer and computes the EVSM moments. This means that the depth value you're using for computing those moments is z/w after applying your combined view * projection matrix for a particular cubemap face. Then in your lighting shader, you're computing using this as your light-space depth comparison value:

float3 shadowPos  = P - PointLight[lightId].wPos;
float depth = (light.matProj._43 / length(shadowPos)) + light.matProj._33;
float2 warpedDepth = evsm::WarpDepth(lightSpaceDepth, VarianceExponents);
This doesn't match what you're storing in the shadow map. Doing proj._43 / depth + proj.33 will give you post-projection z/w, but only if "depth" is the view-space Z value of the coordinate. In other words, it's the projection of the coordinate onto the shadow camera's local Z axis (the "forward" or "lookAt" direction). Take a look a this diagram:
 
post-118414-0-32769000-1480490706.png
 
You're currently using the length of the red vector to compute your z/w value. However what you really want is the length of the green vector, which is the projection of the red vector onto the blue vector (which is the direction that the shadow camera is facing). Hopefully this shows why you were getting larger errors at the corners of your cubemaps, and correct results closer to the center.
 
Now the tricky part is that you actually used 6 different shadow cameras to render your cubemap, with a different lookAt direction for each face. So to compute that green vector, you have to determine which cubemap face you're going to same from, and then project the C -> P vector using the lookAt direction for that particular cubemap face. The way that the hardware chooses a cubemap face is to look at the 3 components of the sampling vector and see which one is largest to see if it should use an X, Y, or Z face. Then it looks at the sign of the vector to choose between the positive and negative face. Then once you have have the right face worked out, the projection is a simple dot product between the facing direction and the sampling direction.
 
Fortunately this is all easier than it sounds, since the cubemap facing directions align with the major axes. It's even easier in your case, since you chose to line up your cubemap face cameras with the major axes in world-space. Since the major axes are (1, 0, 0,), (0, 1, 0), and (0, 0, 1), the projection onto those vectors is really just "selecting" the component that corresponds to that axes (e.g. the projection onto the X axis is just the X component of the sampling direction). So we can combine the face selection and projection into the same operation, like this:
 
 
float projectedDepth = max(max(abs(shadowPos.x), abs(shadowPos.y)), abs(shadowPos.z));
 
Basically you just figure out which component is biggest, and use that. We can just use abs() and ignore the sign, because we don't actually care if we're going to sample from the positive or negative face. We only care about the projection onto that axis. Try computing this value, and then applying the projection matrix  43 and 33 values to compute z/w. If everything else is correct, then hopefully it should fix your issue.
 
Alternatively, you should consider computing your moments off of a linear depth value instead of a non-linear depth value. With a perspective projection, z/w is highly non-linear, which means you're have more precision dedicated to the range closer to the near clipping plane. If you were following my shadow map sample, I was just using post-projection z because I was only dealing with sun shadows, which used orthographic projections. An orthographic projection will produce a linear Z value between [0, 1], where the depth is basically the fraction of the distance between the near and far clip planes. W is also 1 after an orthographic projection, so you don't even need to divide by it. However with a perspective projection doesn't have this nice property, so if you want a linear depth value then you have to compute one yourself. There's two ways that you might do that:

  • During your EVSM conversion, compute the original view-space Z value from the depth buffer. You can do this using the 33 and 43 components of your projection matrix, by inverting the equation you're using for converting the post-projection z/w value. From there, you can compute a [0, 1] value by very similar to what you get from an orthographic projection by doing (z - nearClip) / (farClip - nearClip). Then you can compute EVSM moments from that value, and repeat the same steps when doing the shadow calculations.
  • Compute your EVSM moments directly from length(shadowPos). During your EVSM conversion, compute the original view-space position from the depth buffer value by applying the inverse of your projection matrix. After that you can just compute the length of the resulting position, and then you'll probably want to scale and bias the value so that it fits into [-1, 1] or [0, 1]. Then during your shadow calculations you can compute length(shadowPos), apply the same scale and bias to get to [0,1], and compute moments from that.
Edited by MJP

Share this post


Link to post
Share on other sites

No worries, thank you for taking the time; I truly appreciate it  :)

 

So yeah, that's spot on. Thanks for the in-depth explanations which helped a lot (I've got to admit I really should have signed up for more math courses at uni).

Using your "quick fix" solution I get this much more pleasing result:
scene_new.png

Now if I could just get the light bleeding at the base (where the meshes and ground connect in this case) reduced so that the beginning of the shadows don't disappear like in the above screenshot I think this should almost be ready to deploy  :)

I will look into your suggestions about using a linear shadow map further when I get some time off. I have to ask though, will it really accomplish much? As far as I understand it, the precision is pretty much lost as soon as the depth maps are rendered as they are non-linear in the first place. So converting it back into view-space depth in the EVSM generation (post processing) step should not really improve the resolution, or am I missing something here?
For the record, my depth map is D32_FLOAT and the EVSM map A32R32G32B32_FLOAT. I can see how it would have a higher impact if the latter used 16-bit channels instead.

 

 

Again, thanks a lot for your time and effort MJP. Cheers!

Edited by Husbjörn

Share this post


Link to post
Share on other sites

Glad you got it working! Starting with a linear depth value before warping can still help with filtering and other operations. Otherwise you will get different behavior in areas closer to the shadow caster than in areas further from the shadow caster.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this