Cascaded Shadow Map Issue

Started by
46 comments, last by riuthamus 11 years, 1 month ago

We have been trying to get cascaded shadow maps working from MJP's sample but I can't seem to get it working right. All I get is a diagonal area of shadow across the ground regardless of which direction I look or where the light is. From my debugging of the shader, it seems like the problem is that the shader that builds the occlusion map is sampling the cascade map from the wrong point. I've gone over the code several times and can't see what would be causing the issue.

gallery_1_8_26705.png

gallery_1_8_189040.png


This is the part of the shader that calculates the texture coordinate which seems to be wrong:


// Reconstruct
view-space position from the depth buffer
float pixelDepth =
DepthMap.Sample(DepthMapSampler, input.TexCoord).r;
float4 position =
float4(pixelDepth * input.FrustumCornerVS, 1.0f);

// Determine the depth
of the pixel with respect to the light
float4x4 inverseLVP = mul(InverseView,
lightViewProjection);
float4 positionLight = mul(position,
inverseLVP);

float lightDepth = (positionLight.z /
positionLight.w);

// Transform from light space to shadow map texture
space.
float2 shadowTexCoord = 0.5 * positionLight.xy / positionLight.w +
float2(0.5f, 0.5f);
shadowTexCoord.x = shadowTexCoord.x / SPLITS +
offset;
shadowTexCoord.y = 1.0f - shadowTexCoord.y;

// Offset the
coordinate by half a texel so we sample it correctly
shadowTexCoord += (0.5f
/ ShadowMapSize);

Any help with this would be greatly appreciated, thanks in advance.

Advertisement

My guess would be that your not creating your orthogonal projection matrix for the shadow map wide enough to contain the whole scene. Try widening that projection matrix and see if the problem is resolved.

Hm, okay ill have him take a look at that.

Why would that cause the sampling position to be wrong? And how would I just make it bigger? The code for the frustum building is also from MJP's sample, so there shouldn't be any fundamental problems with it unless I ported it wrong.

Anybody else have any suggestions? we really would like to get CSM working.

Without seeing your code It's hard to say exactly where the issue is. The shader code you posted looks alright. From the images you have posted I've seen that issue before when the shadow frustum hasn't been built correctly and/or the frustum is missing a transform into light space. Maybe also look over the matrices you're passing into the shader to make sure they are correct.

Well, here's the code for the frustum creation. It's possible the issue is from convention differences between XNA (what the sample uses) and SharpDX (which is what we're using), but I wouldn't really know

public static void Draw()
{
	LightDirection = Environment.SkyDome.SunLight.LightDirection;
	Engine.Context.OutputMerger.SetTargets(depthStencil.DepthStencilView, depthMap);
	Engine.Context.ClearRenderTargetView(depthMap, Color.White);

	// Get corners of the main camera's bounding frustum
	var viewMatrix = World.Camera.View;
	frustumCornersWS = World.Camera.BoundingFrustum.GetCorners();

	//frustumCornersVS = Vector3.Transform(frustumCornersWS, ref viewMatrix);
	var vector = new Vector4[8];
	Vector3.Transform(frustumCornersWS, ref viewMatrix, vector);
	for(var i = 0; i < 8; i++)
	{
		frustumCornersVS[i] = new Vector3(vector[i].X, vector[i].Y, vector[i].Z);
	}
	for(var i = 0; i < 4; i++)
	{
		farFrustumCornersVS[i] = frustumCornersVS[i + 4];
	}
	// Calculate the cascade splits.  We calculate these so that each successive
	// split is larger than the previous, giving the closest split the most amount
	// of shadow detail.  
	const float n = Splits;
	const float near = 1;
	var far = World.Camera.FarPlaneDistance;
	splitDepths[0] = near;
	splitDepths[Splits] = far;
	const float splitConstant = 0.95f; //0.95f
	for(var i = 1; i < splitDepths.Length - 1; i++)
	{
		splitDepths[i] = splitConstant * near * (float)Math.Pow(far / near, i / n) + (1.0f - splitConstant) * ((near + (i / n)) * (far - near));
	}
	// Render our scene geometry to each split of the cascade
	for(var i = 0; i < Splits; i++)
	{
		var minZ = splitDepths[i];
		var maxZ = splitDepths[i + 1];

		lightCameras[i] = CalculateFrustum(minZ, maxZ);

		RenderDepthMap(i);
	}

	Engine.Context.Rasterizer.SetViewports(Engine.Viewport);
	RenderShadowMap();
}

private static OrthographicCamera CalculateFrustum(float minZ, float maxZ)
{
	// Shorten the view frustum according to the shadow view distance
	var cameraMatrix = World.Camera.World;

	for(var i = 0; i < 4; i++)
		splitFrustumCornersVS[i] = frustumCornersVS[i + 4] * (minZ / World.Camera.FarPlaneDistance);

	for(var i = 4; i < 8; i++)
		splitFrustumCornersVS[i] = frustumCornersVS[i] * (maxZ / World.Camera.FarPlaneDistance);

	var vector = new Vector4[8];
	Vector3.Transform(splitFrustumCornersVS, ref cameraMatrix, vector);

	for(var i = 0; i < 8; i++)
	{
		frustumCornersWS[i] = new Vector3(vector[i].X, vector[i].Y, vector[i].Z);
	}

	// Position the shadow-caster camera so that it's looking at the centroid,
	// and backed up in the direction of the sunlight
	var viewMatrix = Matrix.LookAtRH(Vector3.Zero - (LightDirection*100), Vector3.Zero, new Vector3(0, 1, 0));

	// Determine the position of the frustum corners in light space
	Vector3.Transform(frustumCornersWS, ref viewMatrix, vector);

	for(var i = 0; i < 8; i++)
	{
		frustumCornersLS[i] = new Vector3(vector[i].X, vector[i].Y, vector[i].Z);
	}

	// Calculate an orthographic projection by sizing a bounding box 
	// to the frustum coordinates in light space
	var mins = frustumCornersLS[0];
	var maxes = frustumCornersLS[0];
	for(var i = 1; i < 8; i++)
	{
		mins = Vector3.Min(mins, frustumCornersLS[i]);
		maxes = Vector3.Max(maxes, frustumCornersLS[i]);
	}
	if(toggleJitter)
	{
		// We snap the camera to 1 pixel increments so that moving the camera does not cause the shadows to jitter.
		// This is a matter of integer dividing by the world space size of a texel
		var diagonalLength = (frustumCornersWS[0] - frustumCornersWS[6]).Length();
		diagonalLength += 2;    //Without this, the shadow map isn't big enough in the world.
		var worldsUnitsPerTexel = diagonalLength / (ShadowResolution * Splits);

		var vBorderOffset = (new Vector3(diagonalLength, diagonalLength, diagonalLength) - (maxes - mins)) * 0.5f;
		maxes += vBorderOffset;
		mins -= vBorderOffset;

		mins /= worldsUnitsPerTexel;
		mins.X = (float)Math.Floor(mins.X);
		mins.Y = (float)Math.Floor(mins.Y);
		mins.Z = (float)Math.Floor(mins.Z);
		mins *= worldsUnitsPerTexel;

		maxes /= worldsUnitsPerTexel;
		maxes.X = (float)Math.Floor(maxes.X);
		maxes.Y = (float)Math.Floor(maxes.Y);
		maxes.Z = (float)Math.Floor(maxes.Z);
		maxes *= worldsUnitsPerTexel;
	}

	var lightCamera = new OrthographicCamera(mins.X, maxes.X, mins.Y, maxes.Y, -maxes.Z - nearClipOffset, -mins.Z);

	lightCamera.SetViewMatrix(ref viewMatrix);

	return lightCamera;
}

I haven't used SlimDX before so I can't comment on its usage. I did see a problem with how you are positioning the frustum. Specifically in the following line:


var viewMatrix = Matrix.LookAtRH(Vector3.Zero - (LightDirection*100), Vector3.Zero, new Vector3(0, 1, 0));

This code means the shadow frustum never moves with the camera. MJP's examples calculates the frustum center (instead of using Vector3.Zero) with the following code:


// Find the centroid
Vector3 frustumCentroid = new Vector3(0,0,0);
for (int i = 0; i < 8; i++)
	frustumCentroid += frustumCornersWS[i];
frustumCentroid /= 8;

There is one limitation with the example MJP gives as well that may be problematic for you. Only the viewable objects in the scene are being considered as shadow casters. So if you have an object behind the camera that's casting a shadow in front of the camera then the shadow will not be visible. Depending on your application this may not be an issue, but it's something to keep in mind.

That part of the code is actually changed from the sample. There's a comment on MJP's blog where a user uploaded a changed ComputeFrustum function that is supposed to reduce jitter that occurs when the camera is moving. The code is here: http://pastebin.com/Yn5SVPUP. Is that code actually wrong? Should I just use MJP's original version instead?

It's not wrong, just specific for that scene. Give the original code a go and see if the issues go away.

This topic is closed to new replies.

Advertisement