Cascaded Shadow Map shimmering effect

Started by
14 comments, last by Ghost_RacCooN 8 years, 4 months ago

I've been trying to get a single directional shadows orthogonal matrix to fit around the view frustum before I move on to splitting it up and calculating a tighter bound for it. However I can't seem to get the shimmering effect to go away. I've been following microsofts example https://msdn.microsoft.com/en-us/library/windows/desktop/ee416324(v=vs.85).aspx like everyone else but I can't get the shimmering to go away, unless I'm missing something here. And yeah, don't mind the large texels from the shadows, the scene/view frustum is pretty big because I wanted to test it properly. Here's the effect:

And the code:


	GLfloat far = -INFINITY;
	GLfloat near = INFINITY;

	//Multiply all the world space frustum corners with the view matrix of the light
	Frustum cameraFrustum = CameraMan.getActiveCamera()->mFrustum;
	lightViewMatrix = glm::lookAt((cameraFrustum.frustumCenter - glm::vec3(-0.447213620f, -0.89442790f, 0.0f)), cameraFrustum.frustumCenter, glm::vec3(0.0f, 0.0f, 1.0f));

	glm::vec3 arr[8];
	for (unsigned int i = 0; i < 8; ++i)
		arr[i] = glm::vec3(lightViewMatrix * glm::vec4(cameraFrustum.frustumCorners[i], 1.0f));

	glm::vec3 minO = glm::vec3(INFINITY, INFINITY, INFINITY);
	glm::vec3 maxO = glm::vec3(-INFINITY, -INFINITY, -INFINITY);
	
	for (auto& vec : arr)
	{
		minO = glm::min(minO, vec);
		maxO = glm::max(maxO, vec);
	}

	far = maxO.z;
	near = minO.z;

	//Get the longest diagonal of the frustum, this along with texel sized increments is used to keep the shadows from shimmering
	//far top right - near bottom left
	glm::vec3 longestDiagonal = cameraFrustum.frustumCorners[0] - cameraFrustum.frustumCorners[6];
	GLfloat lengthOfDiagonal = glm::length(longestDiagonal);
	longestDiagonal = glm::vec3(lengthOfDiagonal);

	glm::vec3 borderOffset = (longestDiagonal - (maxO - minO)) * glm::vec3(0.5f, 0.5f, 0.5f);

	borderOffset *= glm::vec3(1.0f, 1.0f, 0.0f);

	maxO += borderOffset;
	minO -= borderOffset;

	GLfloat worldUnitsPerTexel = lengthOfDiagonal / 1024.0f;
	glm::vec3 vWorldUnitsPerTexel = glm::vec3(worldUnitsPerTexel, worldUnitsPerTexel, 0.0f);

	minO /= vWorldUnitsPerTexel;
	minO = glm::floor(minO);
	minO *= vWorldUnitsPerTexel;

	maxO /= vWorldUnitsPerTexel;
	maxO = glm::floor(maxO);
	maxO *= vWorldUnitsPerTexel;

	lightOrthoMatrix = glm::ortho(minO.x, maxO.x, minO.y, maxO.y, near, far);

	//For more accurate near and far planes, clip the scenes AABB with the orthographic frustum
	//calculateNearAndFar();

At first I thought I was simply using a too big scene for such a small texture, but even with smaller near/far values I get pixel shimmering. I based the texel increment/longest diagonal solution on the code that goes along with that article but i can't quite get it right

EDIT: this is how the frustum is calculated each frame, and how the shadow textures are setup


void Camera::updateFrustum()
{
	//Just to visualise it http://www.panohelp.com/lensfov.html
	float nearHeight = 2 * tan(mFOV / 2) * mNear;
	float nearWidth = nearHeight * mRatio;

	float farHeight = 2 * tan(mFOV / 2) * mFar;
	float farWidth = farHeight * mRatio;

	glm::vec3 fc = mPos + mFront * mFar;
	glm::vec3 nc = mPos + mFront * mNear;

	mFrustum.frustumCorners[0] = fc + (mUp * farHeight / 2.0f) - (mRight * farWidth / 2.0f);
	mFrustum.frustumCorners[1] = fc + (mUp * farHeight / 2.0f) + (mRight * farWidth / 2.0f);
	mFrustum.frustumCorners[2] = fc - (mUp * farHeight / 2.0f) - (mRight * farWidth / 2.0f);
	mFrustum.frustumCorners[3] = fc - (mUp * farHeight / 2.0f) + (mRight * farWidth / 2.0f);

	mFrustum.frustumCorners[4] = nc + (mUp * nearHeight / 2.0f) - (mRight * nearWidth / 2.0f);
	mFrustum.frustumCorners[5] = nc + (mUp * nearHeight / 2.0f) + (mRight * nearWidth / 2.0f);
	mFrustum.frustumCorners[6] = nc - (mUp * nearHeight / 2.0f) - (mRight * nearWidth / 2.0f);
	mFrustum.frustumCorners[7] = nc - (mUp * nearHeight / 2.0f) + (mRight * nearWidth / 2.0f);

	mFrustum.frustumCenter = mPos + mFront * ((mFar - mNear) / 2.0f);
}

	mShadowMapWidth = 1024;
	mShadowMapHeight = 1024;

	// Directional light shadow map buffer
	glGenFramebuffers(1, &mDirectionalShadowFBO);
	glBindFramebuffer(GL_FRAMEBUFFER, mDirectionalShadowFBO);

	glGenTextures(1, &mDirectionalShadowDepthTexture);
	glBindTexture(GL_TEXTURE_2D, mDirectionalShadowDepthTexture);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, mShadowMapWidth, mShadowMapHeight, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, mDirectionalShadowDepthTexture, 0);
	glDrawBuffer(GL_NONE);
	glReadBuffer(GL_NONE);

        // This code removes the shimmering effect along the edges of shadows due to
        // the light changing to fit the camera.
        if( m_eSelectedCascadesFit == FIT_TO_SCENE ) 
        {
            // Fit the ortho projection to the cascades far plane and a near plane of zero. 
            // Pad the projection to be the size of the diagonal of the Frustum partition. 
            // 
            // To do this, we pad the ortho transform so that it is always big enough to cover 
            // the entire camera view frustum.
            XMVECTOR vDiagonal = vFrustumPoints[0] - vFrustumPoints[6];
            vDiagonal = XMVector3Length( vDiagonal );
            
            // The bound is the length of the diagonal of the frustum interval.
            FLOAT fCascadeBound = XMVectorGetX( vDiagonal );
            
            // The offset calculated will pad the ortho projection so that it is always the same size 
            // and big enough to cover the entire cascade interval.
            XMVECTOR vBoarderOffset = ( vDiagonal - 
                                        ( vLightCameraOrthographicMax - vLightCameraOrthographicMin ) ) 
                                        * g_vHalfVector;
            // Set the Z and W components to zero.
            vBoarderOffset *= g_vMultiplySetzwToZero;
            
            // Add the offsets to the projection.
            vLightCameraOrthographicMax += vBoarderOffset;
            vLightCameraOrthographicMin -= vBoarderOffset;

            // The world units per texel are used to snap the shadow the orthographic projection
            // to texel sized increments.  This keeps the edges of the shadows from shimmering.
            FLOAT fWorldUnitsPerTexel = fCascadeBound / (float)m_CopyOfCascadeConfig.m_iBufferSize;
            vWorldUnitsPerTexel = XMVectorSet( fWorldUnitsPerTexel, fWorldUnitsPerTexel, 0.0f, 0.0f ); 


        } 

And this code above is the one in the microsoft example

Advertisement

Hi, Niruz91, I may be missing something, but I don't really see, how this gives you the amount of world units per shadow map texel:


GLfloat worldUnitsPerTexel = lengthOfDiagonal / 1024.0f;

Shouldn't you be dividing the dimensions of the shadow map bound instead of the camera frustum diagonal? Also, since the light frustum will probably be rectangular, the shadow map will have different resolution in terms of world units along the X and Y axis, yielding different values for the components of your vWorldUnitsPerTexel vector. Assuming minO and maxO are the corners of the shadow bound, it will be something like


glm::vec3((maxO.x - minO.x) / 1024.0f, (maxO.y - minO.y) / 1024.0f, 0.0f);

EDIT: Sorry, just read through the microsoft code and it looks like it does the same thing, which means it's probably not the issue

Yeah, I tried changing the code too and it produced the same results. I found this though, but I'm not sure exactly what he means by it:

Discretize the position of the frustum, based on the size of texels in the shadow map. In other words, if the shadow map is 1024×1024, then you only allow the frustum to move around in discrete steps of 1/1024th of the frustum size. (You also need to increase the size of the frustum by a factor of 1024/1023, to give room for the shadow frustum and view frustum to slip against each other.)

from http://gamedev.stackexchange.com/questions/73851/how-do-i-fit-the-camera-frustum-inside-directional-light-space and yeah, I still can't quite figure out what's wrong.

I'm not sure how much use this is, but here's a video of the shadows view, it seems to be stables as I'm moving/rotating around:

EDIT: Made another video that clearly shows the texel jumps as well as how the shadows are moving with a slower camera

Here's the code I use to get rid of that artifact. I don't see where you are rounding to the nearest pixel in the code you posted, you need a floor operation or integer conversion to correctly round the light's viewport.


// bounds == 2D bounding box of camera frustum corners rotated to light-local space.
// Round the light's bounding box to the nearest texel unit to reduce flickering.
Vector2f unitsPerTexel = 2.0f*Vector2f( boundsSize.x, boundsSize.y ) / Vector2f( (Float)shadowWidth, (Float)shadowHeight );

bounds.min.x = math::floor( bounds.min.x / unitsPerTexel.x )*unitsPerTexel.x;
bounds.max.x = math::floor( bounds.max.x / unitsPerTexel.x )*unitsPerTexel.x;

bounds.min.y = math::floor( bounds.min.y / unitsPerTexel.y )*unitsPerTexel.y;
bounds.max.y = math::floor( bounds.max.y / unitsPerTexel.y )*unitsPerTexel.y;

// then, use rounded bounding box and the light orientation to construct the light's view/projection matrices.

Maybe I'm doing something wrong here.. I tried your code like this:


	glm::vec2 unitsPerTexel = 2.0f*glm::vec2(longestDiagonal.x, longestDiagonal.y) / glm::vec2((GLfloat)1024, (GLfloat)1024);

	minO.x = glm::floor(minO.x / unitsPerTexel.x)*unitsPerTexel.x;
	maxO.x = glm::floor(maxO.x / unitsPerTexel.x)*unitsPerTexel.x;

	minO.y = glm::floor(minO.y / unitsPerTexel.y)*unitsPerTexel.y;
	maxO.y = glm::floor(maxO.y / unitsPerTexel.y)*unitsPerTexel.y;

        lightOrthoMatrix = glm::ortho(minO.x, maxO.x, minO.y, maxO.y, near, far);

Which produced the same results unfortunately, this was my original code... I'm suspecting something else is wrong somewhere but I don't know what that could be


	GLfloat worldUnitsPerTexel = lengthOfDiagonal / 1024.0f;
	//glm::vec3 vWorldUnitsPerTexel = glm::vec3(worldUnitsPerTexel, worldUnitsPerTexel, 0.0f);
	glm::vec3 vWorldUnitsPerTexel = glm::vec3((maxO.x - minO.x) / 1024.0f, (maxO.y - minO.y) / 1024.0f, 0.0f);

	minO /= vWorldUnitsPerTexel;
	minO = glm::floor(minO);
	minO *= vWorldUnitsPerTexel;

	maxO /= vWorldUnitsPerTexel;
	maxO = glm::floor(maxO);
	maxO *= vWorldUnitsPerTexel;

To solve your issue you have to transform your code to have translation invariance.

To achieve that you have to check if the camera moved at least half a pixel, add the offset on the position of the shadow map when it's true.

To have rotation invariance you have to use a bounding sphere and make the projection matrix based on it.

I'm actually thinking about giving up and using spheres. From Jonathan Blows article I found this

So basically, you take the a frustum slice in worldspace, ensure that it is completely enclosed in a sphere, and then ensure that the sphere is completely enclosed in a square cylinder; the square is your shadow map.

Using this method, would you still need to clip the Scene AABB against the view frustum to get a tight fit around any possible shadow casters?

Using a bounding sphere will give you the rotation invariance but you still need the translation invariance to solve all issues.

Finally got them stabilized, now I'm not sure exactly what I did wrong because just using a sphere around the frustum didn't remove any rotation shimmering, and I triple checked that it had the same size constantly. Ended up finding some code over here which I found made more sense than anything I've read in any paper: http://www.gamedev.net/topic/650743-improving-cascade-shadow/ And how I used it in my code:


//Calculate the viewMatrix from the frustum center and light direction
Frustum cameraFrustum = CameraMan.getActiveCamera()->mFrustum;
glm::vec3 lightDirection = glm::normalize(glm::vec3(-0.447213620f, -0.89442790f, 0.0f));
lightViewMatrix = glm::lookAt((cameraFrustum.frustumCenter - lightDirection), cameraFrustum.frustumCenter, glm::vec3(0.0f, 1.0f, 0.0f));

//Get the longest radius in world space
GLfloat radius = glm::length(cameraFrustum.frustumCenter - cameraFrustum.frustumCorners[6]);
for (unsigned int i = 0; i < 8; ++i)
{
GLfloat distance = glm::length(cameraFrustum.frustumCorners[i] - cameraFrustum.frustumCenter);
radius = glm::max(radius, distance);

}
radius = std::ceil(radius);

//Create the AABB from the radius
glm::vec3 maxOrtho = cameraFrustum.frustumCenter + glm::vec3(radius);
glm::vec3 minOrtho = cameraFrustum.frustumCenter - glm::vec3(radius);

//Get the AABB in light view space
maxOrtho = glm::vec3(lightViewMatrix*glm::vec4(maxOrtho, 1.0f));
minOrtho = glm::vec3(lightViewMatrix*glm::vec4(minOrtho, 1.0f));

//Just checking when debugging to make sure the AABB is the same size
GLfloat lengthofTemp = glm::length(maxOrtho - minOrtho);

//Store the far and near planes
far = maxOrtho.z;
near = minOrtho.z;

lightOrthoMatrix = glm::ortho(minOrtho.x, maxOrtho.x, minOrtho.y, maxOrtho.y, near, far);


// Create the rounding matrix, by projecting the world-space origin and determining
// the fractional offset in texel space
glm::mat4 shadowMatrix = lightOrthoMatrix * lightViewMatrix;
glm::vec4 shadowOrigin = glm::vec4(0.0f, 0.0f, 0.0f, 1.0f);
shadowOrigin = shadowMatrix * shadowOrigin;
GLfloat storedW = shadowOrigin.w;
shadowOrigin = shadowOrigin * 4096.0f / 2.0f;

glm::vec4 roundedOrigin = glm::round(shadowOrigin);
glm::vec4 roundOffset = roundedOrigin - shadowOrigin;
roundOffset = roundOffset *  2.0f / 4096.0f;
roundOffset.z = 0.0f;
roundOffset.w = 0.0f;

glm::mat4 shadowProj = lightOrthoMatrix;
shadowProj[3] += roundOffset;
lightOrthoMatrix = shadowProj;

Two things I noted is that the uses a ceil operation on the radius, and then creates a new orthographic matrix with adjusted values. I'm not entirely sure how it works just yet since I'm still happy that it finally is working. Video just for kicks

This topic is closed to new replies.

Advertisement