Skip to main content
GameDev.net gamedev.net
🔒 Locked 🎮 Unity

Fitting directional light in view frustum

Started by Merx Oct 21, 2015 at 12:58 PM 12 replies 11.5k views
Original Post
Merx
Merx

Im trying to optimize my shadow maps by making my directional shadow maps fit in the view frustum, however this is causing me some problems. I tried several things, but all didn't work, or didn't work properly.

Assuming i only have the following information:

vec3 describing direction of the directional light

perspective camera with values (fov, width, height, near plane, far plane) and transform matrix

How can i use this information to create the correct light space matrix to generate and sample my shadow maps?

Any help would be greatly appreciated!

cozzie
cozzie
Hi.
Not sure how to fully do this, but just some thoughts:

- the directional light has no range (basically to infinity)
- I would assume you want to cast shadows for objects that are inside the frustum, or better, within a specific range of the viewer
-- there's a margin for objects which bounding volume is outside the frustum but the shadow might be (partially) inside, for this you have to decide what you want
- a potential performance thing could be processing to many objects, watch out for this

Hope this helps a bit
Crealysm game & engine development: http://www.crealysm.com

Looking for a passionate, disciplined and structured producer? PM me
Alundra
Alundra

If you want perfect fit for your shadowmap you have to use the depth buffer as information and find the min/max, this method is called SDSM.

Directional light shadow map needs cascade to works on all distance correctly, you can find all informations on internet about it.

If you go for the depth buffer method, don't use a sphere for your shadow map, you will only loose quality.

Digitalfragment
Digitalfragment

calculate your view-projection matrix for the main camera, and invert it
transform the 8 corners of the NDC cube through the inverted projection-matrix (with -1 for near-z if opengl, instead of 0 IIRC?)
perspective divide the 8 resulting coordinates

that gives you the 8 corners of your view frustum in worldspace

take the average of all 8, that gives you the midpoint of the frustum

generate an up & right vector perpendicular to your light direction

create a view matrix centred on the origin, using your light direction and these up/right vectors

the projection matrix is an ortho matrix where the left is the minimum of the dot product of all 8 corners and the right vector, the right is the maximum of the dot product of all 8 corners and the right vector, the top is the maximum of the dot product of all 8 corners and the up vector, and you should be able to guess how to derive the other 3 values smile.png

That gives the tightest standard projection to fit the entire frustum.

You generally want to pull the near plane of the ortho matrix back to fit all shadow casters in the scene or "pancake" them at the near plane in the vertex shader.

The shadowmap sampling matrix is the viewprojection for the shadow rendering, but with a scale and bias afterwards as transforming by the matrix will give values between -1 and 1, whereas the texture sampling will need 0 to 1

For cascaded shadows, the process is the same as above, but you split the camera frustum up into multiple z slices, which gives tighter projections closer to the camera.

Merx
Merx

I'm probably asking for too much here, but would anyone be able to write pseudo code for me? I would've liked to post my current progress here, but my project files are on my desktop, which i won't have access to until after the weekend. Doesn't have to be anything fancy (obviously) like "pancaking" or csm, or anything of the sort. I'm fairly sure i can get that to work myself once i properly understand this.

If anyone could do that for me that would be amazing!

Alundra
Alundra

Here the code to compute the 8 corners of the camera frustum in world-space :


void CCameraComponent::ComputeFrustumCornerPoints( CVector3* Points ) const
{
  // Get property values.
  const float NearClipPlane = GetNearClipPlane();
  const float FarClipPlane = GetFarClipPlane();

  // Compute the projection inverse scale factor.
  const float ScaleXInv = 1.0f / m_ProjectionMatrix( 0, 0 );
  const float ScaleYInv = 1.0f / m_ProjectionMatrix( 1, 1 );

  // Compute corners in view space.
  CVector3 CornersVS[ 8 ];
  const float NearX = ScaleXInv * NearClipPlane;
  const float NearY = ScaleYInv * NearClipPlane;
  CornersVS[ 0 ] = CVector3( -NearX, +NearY, NearClipPlane );
  CornersVS[ 1 ] = CVector3( +NearX, +NearY, NearClipPlane );
  CornersVS[ 2 ] = CVector3( +NearX, -NearY, NearClipPlane );
  CornersVS[ 3 ] = CVector3( -NearX, -NearY, NearClipPlane );
  const float FarX = ScaleXInv * FarClipPlane;
  const float FarY = ScaleYInv * FarClipPlane;
  CornersVS[ 4 ] = CVector3( -FarX, +FarY, FarClipPlane );
  CornersVS[ 5 ] = CVector3( +FarX, +FarY, FarClipPlane );
  CornersVS[ 6 ] = CVector3( +FarX, -FarY, FarClipPlane );
  CornersVS[ 7 ] = CVector3( -FarX, -FarY, FarClipPlane );

  // Compute the inverse of the view matrix.
  const CMatrix4 InverseViewMatrix = m_ViewMatrix.Inversed();

  // Compute the world space corners.
  for( UInt32 i = 0; i < 8; ++i )
    Points[ i ] = InverseViewMatrix.TransformPoint( CornersVS[ i ] );
}
Merx
Merx

I almost got it working, the shadows are rendered fine, the only problem is that under certain angles shadows get cut off or completely disapear. Obviously something is going wrong, but i don't know what. Here is my code:

Getting world positions of frustum corners:


void Game::getFrustumWorldSpace(std::vector<glm::vec4>& points)
{
	glm::mat4 inverseProjectionMatrix = glm::inverse(projection * view);

	for (unsigned int x = 0; x < 2; x++)
		for (unsigned int y = 0; y < 2; y++)
			for (unsigned int z = 0; z < 2; z++)
			{
				glm::vec4 projClipSpacePosition(x*2.0f - 1.0f, y*2.0f - 1.0f, z*2.0f - 1.0f, 1.0f);
				glm::vec4 projWorldSpacePosition = inverseProjectionMatrix * projClipSpacePosition;
				points.push_back(projWorldSpacePosition / projWorldSpacePosition.w);
			}
}

Generating the lightVPMatrix:


glm::mat4 Game::getDirLightVPMatrix(std::vector<glm::vec4> points)
{
	glm::mat4 lvMatrix = glm::lookAt(glm::normalize(lightDir), glm::vec3(0.0f), glm::vec3(1.0f));

	glm::vec4 transf = lvMatrix * points[0];
	float minZ = transf.z;
	float maxZ = transf.z;
	float minX = transf.z;
	float maxX = transf.x;
	float minY = transf.y;
	float maxY = transf.y;

	for (unsigned int i = 1; i < 8; i++)
	{
		transf = lvMatrix * points[i];

		if (transf.z > maxZ) maxZ = transf.z;
		if (transf.z < minZ) minZ = transf.z;
		if (transf.x > maxX) maxX = transf.x;
		if (transf.x < minX) minX = transf.x;
		if (transf.y > maxY) maxY = transf.y;
		if (transf.y < minY) minY = transf.y;
	}

	glm::mat4 lpMatrix = glm::ortho(-1.0f, 1.0f, -1.0f, 1.0f, minZ, maxZ);

	const float scaleX = 2.0f / (maxX - minX);
	const float scaleY = 2.0f / (maxY - minY);
	const float offsetX = -0.5f * (minX + maxX) * scaleX;
	const float offsetY = -0.5f * (minY + maxY) * scaleY;

	glm::mat4 cropMatrix(1.0f);
	cropMatrix[0][0] = scaleX;
	cropMatrix[1][1] = scaleY;
	cropMatrix[3][0] = offsetX;
	cropMatrix[3][1] = offsetY;

	return cropMatrix * lpMatrix * lvMatrix;
}

If someone could help my find the error i made that would be greatly appreciated!

Digitalfragment
Digitalfragment

Are the shadows disappearing because they are offscreen? You're fitting the shadow projection's near plane tightly to the frustum, which will cause objects that are offscreen but should still cast shadows into the screen to go missing. Apart from that, the math all looks good :D

Merx
Merx

its not that the shadow casters are offscreen, ill show some screenshots with the problem, as well with the shadow map generated. These seem to me kinda... off.

st9G7jX.jpg

Everything going allright...

Vp25uSH.jpg

Shadows getting cut off

lD00Z11.jpg

Shadows completely gone besides the character

The vertex shader i use to generate the shadow (no fragment because shadow maps wink.png )

The lightSpaceMatrix is the matrix generated in the prev shown getDirLightVPMatrix function


#version 330 core
layout (location = 0) in vec3 position;

uniform mat4 lightSpaceMatrix;
uniform mat4 model;

void main()
{
    gl_Position = lightSpaceMatrix * model * vec4(position, 1.0f);
} 
Alundra
Alundra

I don't see clearly the shadow map but the problem is maybe because your object is not completely rendered in the shadow map which cause to have shadow cutted.

If it's the case, that means you don't have the good size of ortho projection when generating the shadow map.

Here the full code to generate the shadow matrix using cascaded shadow map :


// Set the shadow view matrix.
CMatrix4 ShadowViewMatrix;
ShadowViewMatrix.LookAt( -DirLight, CVector3( 0.0f, 0.0f, 0.0f ) );

// Get the camera projection matrix.
const CMatrix4 ProjectionMatrix = CameraComponent->GetProjectionMatrix();

// Compute the camera view to light view matrix.
const CMatrix4 CameraViewToLight = CameraComponent->GetViewMatrix().Inversed() * ShadowViewMatrix;

// For each cascade find the transformation from shadow to cascade space.
for( UInt32 i = 0; i < 4; ++i )
{
  // Compute the projection inverse scale factor.
  const float ScaleXInv = 1.0f / ProjectionMatrix( 0, 0 );
  const float ScaleYInv = 1.0f / ProjectionMatrix( 1, 1 );

  // Compute corners in view space.
  CVector3 CornersVS[ 8 ];
  const float NearX = ScaleXInv * m_CascadeSplitArray[ i ];
  const float NearY = ScaleYInv * m_CascadeSplitArray[ i ];
  CornersVS[ 0 ] = CVector3( -NearX, +NearY, m_CascadeSplitArray[ i ] );
  CornersVS[ 1 ] = CVector3( +NearX, +NearY, m_CascadeSplitArray[ i ] );
  CornersVS[ 2 ] = CVector3( +NearX, -NearY, m_CascadeSplitArray[ i ] );
  CornersVS[ 3 ] = CVector3( -NearX, -NearY, m_CascadeSplitArray[ i ] );
  const float FarX = ScaleXInv * m_CascadeSplitArray[ i + 1 ];
  const float FarY = ScaleYInv * m_CascadeSplitArray[ i + 1 ];
  CornersVS[ 4 ] = CVector3( -FarX, +FarY, m_CascadeSplitArray[ i + 1 ] );
  CornersVS[ 5 ] = CVector3( +FarX, +FarY, m_CascadeSplitArray[ i + 1 ] );
  CornersVS[ 6 ] = CVector3( +FarX, -FarY, m_CascadeSplitArray[ i + 1 ] );
  CornersVS[ 7 ] = CVector3( -FarX, -FarY, m_CascadeSplitArray[ i + 1 ] );

  // Compute the AABB.
  CAxisAlignedBoundingBox AABB;
  for( UInt32 k = 0; k < 8; ++k )
    AABB.Encapsulate( CameraViewToLight.Transform( CornersVS[ k ] ) );

  // Set the cascade projection matrix.
  CMatrix4 CascadeProjectionMatrix;
  CascadeProjectionMatrix.OrthoOffCenter( AABB.m_Min.x, AABB.m_Max.x, AABB.m_Min.y, AABB.m_Max.y, AABB.m_Min.z, AABB.m_Max.z );

  // Compute the shadow view-proj matrix.
  m_CascadeViewProjArray[ i ] = ShadowViewMatrix * CascadeProjectionMatrix;
}

You can remove the for loop to only have one cascade which is what you actually have.

Merx
Merx

This is what i made of that code


glm::mat4 Game::getDirLightVPMatrix()
{
	glm::mat4 lvMatrix = glm::lookAt(-glm::normalize(lightDir), glm::vec3(0.0f), glm::vec3(1.0f));
	glm::mat4 CameraViewToLight = glm::inverse(view) * lvMatrix;

	const float ScaleXInv = 1.0f / projection[0][0];
	const float ScaleYInv = 1.0f / projection[1][1];

	std::vector<glm::vec3> CornersVS;
	CornersVS.reserve(8);

	const float NearX = ScaleXInv * near;
	const float NearY = ScaleYInv * near;
	CornersVS.push_back(glm::vec3(-NearX, +NearY, near));
	CornersVS.push_back(glm::vec3(+NearX, +NearY, near));
	CornersVS.push_back(glm::vec3(+NearX, -NearY, near));
	CornersVS.push_back(glm::vec3(-NearX, -NearY, near));
	const float FarX = ScaleXInv * far;
	const float FarY = ScaleYInv * far;
	CornersVS.push_back(glm::vec3(-FarX, +FarY, far));
	CornersVS.push_back(glm::vec3(+FarX, +FarY, far));
	CornersVS.push_back(glm::vec3(+FarX, -FarY, far));
	CornersVS.push_back(glm::vec3(-FarX, -FarY, far));

	glm::vec4 transf = CameraViewToLight * glm::vec4(CornersVS[0], 1.0);
	float minZ = transf.z;
	float maxZ = transf.z;
	float minX = transf.x;
	float maxX = transf.x;
	float minY = transf.y;
	float maxY = transf.y;

	for (unsigned int i = 1; i < 8; i++)
	{
		transf = CameraViewToLight * glm::vec4(CornersVS[i], 1.0f);

		if (transf.z > maxZ) maxZ = transf.z;
		if (transf.z < minZ) minZ = transf.z;
		if (transf.x > maxX) maxX = transf.x;
		if (transf.x < minX) minX = transf.x;
		if (transf.y > maxY) maxY = transf.y;
		if (transf.y < minY) minY = transf.y;
	}

	glm::mat4 lpMatrix = glm::ortho(minX, maxX, minY, maxY, minZ, maxZ);

	return lpMatrix * lvMatrix;
}

it does work, and it does generate a different shadow map, but it suffers from very similar problem

Alundra
Alundra
This does not actually work properly, the shadows move slighly when the camera moves

This is a normal problem, you have to use a bounding sphere to avoid this problem, since the projection change when the camera rotate, you have the problem.

Bounding Sphere is a common method to avoid this problem, it's called "Stable Cascaded Shadow Map".

In other words, you have to make the projection rotation invariant and use a way to stabilize the translation too.

Using SDSM, it's not needed because the projection is very efficient, it only takes the distance needed based on the depth buffer, the problem is not visible.

Here a link from MSDN which speaks about common problems and solutions : https://msdn.microsoft.com/en-us/library/windows/desktop/ee416324(v=vs.85).aspx

It has a part about "Shimmering shadow edges" which is the camera translation issue.

Merx
Merx

thnx, but it was another issue, the shadows moving where i was talking about was just a typo on my part :P

Merx
Merx

ok, found the problems with my code. The reason why my shadows disapeared under certain angles was a really embarrasing typo:


float minX = transf.z;

The second problem, that shadows got cut off was just a simple logic problem while creating the lightViewMatrix, where


glm::mat4 lvMatrix = glm::lookAt(glm::vec3(0.0f), -glm::normalize(lightPos), glm::vec3(0.0f, 1.0f, 0.0f));

had to be


glm::vec4 frustumCenter = points[0];
for (unsigned int i = 1; i < 8; i++)
{
	frustumCenter += points[i];
}
frustumCenter /= 8;

glm::mat4 lvMatrix = glm::lookAt(glm::vec3(frustumCenter) + glm::normalize(lightPos), glm::vec3(frustumCenter), glm::vec3(0.0f, 1.0f, 0.0f));

If anyone who has a similar problem happens to run across this, this is my complete code:


//function where you get the lightViewProjectionMatrix;
std::vector<glm::vec4> corners;
corners.reserve(8);

getFrustumWorldSpace(corners);
lightVPMatrix = getDirLightVPMatrix(corners);

void Game::getFrustumWorldSpace(std::vector<glm::vec4>& points)
{
	glm::mat4 inverseProjectionMatrix = glm::inverse(projection * view);

	for (unsigned int x = 0; x < 2; x++)
		for (unsigned int y = 0; y < 2; y++)
			for (unsigned int z = 0; z < 2; z++)
			{
				glm::vec4 projClipSpacePosition(x*2.0f - 1.0f, y*2.0f - 1.0f, z*2.0f - 1.0f, 1.0f);
				glm::vec4 projWorldSpacePosition = inverseProjectionMatrix * projClipSpacePosition;
				points.push_back(projWorldSpacePosition / projWorldSpacePosition.w);
			}
}

glm::mat4 Game::getDirLightVPMatrix(std::vector<glm::vec4> points)
{
	glm::vec4 frustumCenter = points[0];
	for (unsigned int i = 1; i < 8; i++)
	{
		frustumCenter += points[i];
	}
	frustumCenter /= 8;

	glm::mat4 lvMatrix = glm::lookAt(glm::vec3(frustumCenter) + glm::normalize(lightPos), glm::vec3(frustumCenter), glm::vec3(0.0f, 1.0f, 0.0f));

	glm::vec4 transf = lvMatrix * points[0];
	float minZ = transf.z;
	float maxZ = transf.z;
	float minX = transf.x;
	float maxX = transf.x;
	float minY = transf.y;
	float maxY = transf.y;

	for (unsigned int i = 1; i < 8; i++)
	{
		transf = lvMatrix * points[i];

		if (transf.z > maxZ) maxZ = transf.z;
		if (transf.z < minZ) minZ = transf.z;
		if (transf.x > maxX) maxX = transf.x;
		if (transf.x < minX) minX = transf.x;
		if (transf.y > maxY) maxY = transf.y;
		if (transf.y < minY) minY = transf.y;
	}

	glm::mat4 lpMatrix = glm::ortho(-1.0f, 1.0f, -1.0f, 1.0f, minZ, maxZ);

	const float scaleX = 2.0f / (maxX - minX);
	const float scaleY = 2.0f / (maxY - minY);
	const float offsetX = -0.5f * (minX + maxX) * scaleX;
	const float offsetY = -0.5f * (minY + maxY) * scaleY;

	glm::mat4 cropMatrix(1.0f);
	cropMatrix[0][0] = scaleX;
	cropMatrix[1][1] = scaleY;
	cropMatrix[3][0] = offsetX;
	cropMatrix[3][1] = offsetY;

	return cropMatrix * lpMatrix * lvMatrix;
}

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.