Rendering Terrain Part 14 – Improving Shadows and The Day/Night Cycle

I’ve made some small improvements to the shadows and lighting. I fixed the issue I talked about last post with the terrain appearing to wobble in the shadow map. I implemented a simple method of changing the colour and brightness of the Sun as the directional light spins. I also softened the shadows significantly. They were incredibly dark before, but now feel much better to me. Let’s talk about each of these.

Fixing The Wobble

As I mentioned last post, I had accidentally set my Sun’s up vector to be sideways so that the terrain rotated the wrong way in the shadow map. I also noted that it looked like it was warping at certain points in the rotation. I had corrected the up vector and then noted that the terrain appeared to wobble, or rotate slightly around the wrong axis. Take a look at this video to see what I’m talking about:

You can see right from the start of the video that the terrain isn’t square with the light, as it should be with our current orientation. It is also rotating, first in one direction and then the other, as it spins. The original code to create the view and projection matrices for the light came from that DirectX 12 book I’m always referencing.

XMFLOAT4X4 DayNightCycle::GetShadowViewProjectionMatrixTransposed(XMFLOAT3 centerBoundingSphere, float radiusBoundingSphere) {
	LightSource light = mdlSun.GetLight();
	XMVECTOR lightdir = XMLoadFloat3(&light.direction);
	XMVECTOR targetpos = XMLoadFloat3(&centerBoundingSphere);
	XMVECTOR lightpos = -2.0f*radiusBoundingSphere * lightdir; // <----- Seems like an odd way to set the light's position.

	XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
	up = XMVector3Cross(lightdir, up);

	XMMATRIX V = XMMatrixLookAtLH(lightpos, targetpos, up); // light space view matrix
	
	// transform bounding sphere to light space
	XMFLOAT3 spherecenterls;
	XMStoreFloat3(&spherecenterls, XMVector3TransformCoord(targetpos, V));

	// orthographic frustum
	float l = spherecenterls.x - radiusBoundingSphere;
	float b = spherecenterls.y - radiusBoundingSphere;
	float n = spherecenterls.z - radiusBoundingSphere;
	float r = spherecenterls.x + radiusBoundingSphere;
	float t = spherecenterls.y + radiusBoundingSphere;
	float f = spherecenterls.z + radiusBoundingSphere;
	XMMATRIX P = XMMatrixOrthographicOffCenterLH(l, r, b, t, n, f);

	XMMATRIX S = XMMatrixTranspose(V * P);
	XMFLOAT4X4 final;
	XMStoreFloat4x4(&final, S);

	return final;
}

This code is obviously for a completely different light and scene from my own, so its no surprise that it didn’t work exactly as is. When I reviewed the code, I thought the lightpos vector seemed a little odd. The targetpos is the center of the bounding sphere of the scene. The lightpos is defined as an offset based on the bounding sphere, but not based on the center of the bounding sphere. This will only work if the bounding sphere is always at (0, 0, 0)! Well, there’s my problem. My scene’s center isn’t at (0, 0, 0). It is at (w, d, 0), where w and d are the width and depth of our height map. So I made a simple change to this line and subtracted the existing value from the target position. This fixed the problem entirely. I won’t bother with another video since it would just show the height map rotating in exactly one direction. Pretty boring.
An interesting note about the lightpos vector. I was actually able to reduce the equation down to targetpos – lightdir. The extra multipliers made absolutely no difference to the final view matrix.
I also realized as I was writing this post that my light’s up vector was still upside down. That was an easy fix, though. I was getting the up vector by taking the cross product of the light direction with an arbitrary orthogonal vector (0, 1, 0). Swapping the order of the vectors in the cross product corrected the direction of the up vector.

XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
up = XMVector3Cross(up, lightdir);

Changing the Sun’s Colour and Brightness

I had wanted to create a method of changing the Sun’s colour and brightness as the light ‘revolved about the world’ since I decided to add a day/night cycle. I wanted to capture a bit of the colour that shows up at dawn and dusk, even though I don’t have a sky box to really capture what it would look like. I don’t intend to create the sky box during this project, but I’ll likely revisit it in a future project. For now, though, I wanted the colour in the light of the Sun so that it would affect the colour of the terrain. I held off implementing it because I didn’t feel it was necessary to implementing shadows. But then I ran into a problem where the steeper slopes of the terrain were still being partially lit by the Sun when it was well underneath them. Now, obviously this actually points out a problem with the shadows, not the lighting, since the Sun is behind the terrain and thus the terrain should be in shadow. The problem being, I think, that at the current resolution of the shadow map, the terrain was registering as not 100% within shadow, so some diffuse and specular light was still showing up. This could likely be fixed by fiddling with the depth bias or, another idea I’ll discuss in further down, adding a skirt to the terrain.
But I still used this as an excuse to work on something other than the shadows that were frustrating me.

Not the actual colours used. Just for descriptive purposes.

Not the actual colours used. Just for descriptive purposes.


The system I went with is really simple. The Sun’s directional light rotates 360 degrees about it’s origin, creating what could be visualized as a clock face. So at any time of day, you can get an angle out of 360 degrees. I decided to divide the day up into equal pieces, picking 12 angles, each 30 degrees around the circle, starting with 0 at midnight. For each angle, I defined a diffuse and specular colour. As time passes and the light’s direction rotates, we can calculate the new angle the light is pointing along, find which of the angles it is between and how far it is between them, and then interpolate between the selected colour values. I chose my colour values pretty arbitrarily, but tried to have them similar to what I think Sunlight looks like at an equivalent time of day. Obviously, all of the values at night are 0. I left ambient light at a flat value for now.

static const double DEG_PER_MILLI = 360.0 / 86400000.0; // the number of degrees per millisecond, assuming you rotate 360 degrees in 24 hours.
static const XMFLOAT4 SUN_DIFFUSE_COLORS[] = {
	{ 0.0f, 0.0f, 0.0f, 1.0f },
	{ 0.0f, 0.0f, 0.0f, 1.0f },
	{ 0.0f, 0.0f, 0.0f, 1.0f },
	{ 0.9f, 0.2f, 0.2f, 1.0f },
	{ 0.98f, 0.86f, 0.2f, 1.0f },
	{ 0.8f, 0.8f, 0.6f, 1.0f },
	{ 0.8f, 0.8f, 0.8f, 1.0f },
	{ 0.8f, 0.8f, 0.6f, 1.0f },
	{ 0.98f, 0.86f, 0.2f, 1.0f},
	{ 0.9f, 0.2f, 0.2f, 1.0f },
	{ 0.0f, 0.0f, 0.0f, 1.0f },
	{ 0.0f, 0.0f, 0.0f, 1.0f }
};

static const XMFLOAT4 SUN_SPECULAR_COLORS[] = {
	{ 0.0f, 0.0f, 0.0f, 1.0f },
	{ 0.0f, 0.0f, 0.0f, 1.0f },
	{ 0.0f, 0.0f, 0.0f, 1.0f },
	{ 0.5f, 0.5f, 0.5f, 1.0f },
	{ 0.8f, 0.8f, 0.8f, 1.0f },
	{ 1.0f, 1.0f, 1.0f, 1.0f },
	{ 1.0f, 1.0f, 1.0f, 1.0f },
	{ 1.0f, 1.0f, 1.0f, 1.0f },
	{ 0.8f, 0.8f, 0.8f, 1.0f },
	{ 0.5f, 0.5f, 0.5f, 1.0f },
	{ 0.0f, 0.0f, 0.0f, 1.0f },
	{ 0.0f, 0.0f, 0.0f, 1.0f }
};

XMFLOAT4 ColorLerp(XMFLOAT4 color1, XMFLOAT4 color2, float interpolator) {
	//x + s(y - x)
	XMVECTOR c1 = XMLoadFloat4(&color1);
	XMVECTOR c2 = XMLoadFloat4(&color2);
	XMFLOAT4 newcolor;
	XMStoreFloat4(&newcolor, c1 + interpolator * (c2 - c1));

	return newcolor;
}

void DayNightCycle::Update() {
	time_point<system_clock> now = system_clock::now();

	if (!isPaused) {
		// get the amount of time in ms since the last time we updated.
		milliseconds elapsed = duration_cast<milliseconds>(now - mtLast);

		// calculate how far to rotate.
		double angletorotate = elapsed.count() * mPeriod * DEG_PER_MILLI;
		float angleinrads = XMConvertToRadians((float)angletorotate);

		// rotate the sun's direction vector.
		XMFLOAT3 tmp = mdlSun.GetLight().direction;
		XMVECTOR dir = XMLoadFloat3(&tmp);
		XMVECTOR rot = XMQuaternionRotationRollPitchYaw(0.0f, -(float)angleinrads, 0.0f);
		dir = XMVector3Normalize(XMVector3Rotate(dir, rot));
		XMStoreFloat3(&tmp, dir);
		mdlSun.SetLightDirection(tmp);

		// use the angletorotate to calculate what the current colour of the Sun should be.
		float newangle = fmod(mCurrentSunAngle + (float)angletorotate, 360.0f);
		int iColor1, iColor2; // color indices to get colors to interpolate between
		float iInterpolator; // amount to interpolate by
		if (newangle >= 330.0f) {
			iColor1 = 11;
			iColor2 = 0;
			iInterpolator = (newangle - 330.0f) / 30.0f;
		} else if (newangle >= 300.0f) {
			iColor1 = 10;
			iColor2 = 11;
			iInterpolator = (newangle - 300.0f) / 30.0f;
		} else if (newangle >= 270.0f) {
			iColor1 = 9;
			iColor2 = 10;
			iInterpolator = (newangle - 270.0f) / 30.0f;
		} else if (newangle >= 240.0f) {
			iColor1 = 8;
			iColor2 = 9;
			iInterpolator = (newangle - 240.0f) / 30.0f;
		} else if (newangle >= 210.0f) {
			iColor1 = 7;
			iColor2 = 8;
			iInterpolator = (newangle - 210.0f) / 30.0f;
		} else if (newangle >= 180.0f) {
			iColor1 = 6;
			iColor2 = 7;
			iInterpolator = (newangle - 180.0f) / 30.0f;
		} else if (newangle >= 150.0f) {
			iColor1 = 5;
			iColor2 = 6;
			iInterpolator = (newangle - 150.0f) / 30.0f;
		} else if (newangle >= 120.0f) {
			iColor1 = 4;
			iColor2 = 5;
			iInterpolator = (newangle - 120.0f) / 30.0f;
		} else if (newangle >= 90.0f) {
			iColor1 = 3;
			iColor2 = 4;
			iInterpolator = (newangle - 90.0f) / 30.0f;
		} else if (newangle >= 60.0f) {
			iColor1 = 2;
			iColor2 = 3;
			iInterpolator = (newangle - 60.0f) / 30.0f;
		} else if (newangle >= 30.0f) {
			iColor1 = 1;
			iColor2 = 2;
			iInterpolator = (newangle - 30.0f) / 30.0f;
		} else if (newangle >= 0.0f) {
			iColor1 = 0;
			iColor2 = 1;
			iInterpolator = newangle / 30.0f;
		}
		
		mdlSun.SetDiffuseColor(ColorLerp(SUN_DIFFUSE_COLORS[iColor1], SUN_DIFFUSE_COLORS[iColor2], iInterpolator));
		mdlSun.SetSpecularColor(ColorLerp(SUN_SPECULAR_COLORS[iColor1], SUN_SPECULAR_COLORS[iColor2], iInterpolator));

		mCurrentSunAngle = newangle;
	}

	// update the time for the next pass.
	mtLast = now;
}

Perhaps there’s a fancier way of doing this without a giant if/else if block, but this doesn’t slow the program down measurably and is pretty easy to follow. The results are also quite acceptable to me.

I may try adjusting the brightness at noon a bit. It feels a little too bright. You can also see, particularly in the lower left portion of the terrain1, an odd shifting of the shadows. I’m not entirely certain what causes it, but I seemed to be able to eliminate it when I was experimenting with Slope Scaled Depth Bias earlier. Hopefully I can get rid of it entirely once I’m done with shadows. Another issue that is slightly visible in the video, is a slight shimmering in the shadows as they move. The video was taken with a 2048×2048 shadow map and a 2048×2048 height map. If I raise the shadow map to 4096×4096, we use more memory and performance goes down slightly, but the shimmering is no longer visible. I’m hoping that with Cascaded Shadow Maps, we’ll be able to keep the size of the shadow maps a bit smaller and minimize the shimmering. This article by Microsoft has a brief section indicating that this is caused by moving the light or the camera such that the light’s view frustum changes by an amount that doesn’t line up neatly with a texel in the shadow map. They offer up a solution, but not a complete one. I’ll have to look into this fix and see if I can sort it out.

Softening the Shadows

The PCF kernel I implemented, taken from Frank Luna’s DirectX 12 book mentioned previously, is supposed to help soften shadows, but I have found that I’m getting very hard shadows.
hardshadows
I think the problem is in my definition. When people talk about hard and soft shadows, they talk about the edges of the shadows blending into the surroundings. What I’m talking about is how dark the insides of the shadows are. In the sample code provided with the book, the shadows cast are very light, whereas mine are pitch black. He appears to have a more complex model for the lights and materials than I do, so that is likely the reason.
For the time being, I simply take the maximum of the shadow factor and the light’s ambient intensity and multiply that with the terrain’s diffuse colour value, instead of just the shadow factor directly. The specular is still directly multiplied against the shadow factor as we don’t want specular reflection in the shadows. I actually wound up eliminating the ambient component of the lighting calculation this way as that is now part of the diffuse component. I can now adjust my light’s ambient intensity to soften the shadows, without impacting the lit areas.
softshadows

Problems that Persist

So the shadows look pretty decent in a still image, but there are definitely problems that still need to be addressed. There is a little bit of shimmering when the camera moves around but the shadows are still. I’m pretty certain this is due to the resolution. It seems less of an issue when I use larger shadow maps and is not really visible at a distance. I’m fairly confident Cascaded Shadow Maps will fix this.
When the Sun is moving, however, there is quite a bit of shimmering going on. I definitely need to address this and see if I can stabilize them. This is likely a combination of the resolution and the light not moving in texel-sized increments, as I alluded to above.
I also have an issue with the shadows on steep slopes. This seems to be related to what face of the polygons I cull in the shadow map. If I cull the front faces, as I have been doing, there tends to be light bleeding through where steeper slopes meet level areas.
shadowsfrontculling
If I switch to back-face fulling, as we use in normal rendering and as Microsoft recommends, or no culling at all, it seems to eliminate this problem but at the expense of some very serious shadow acne.
shadowsnoculling
This problem does NOT appear to be rectified by simply turning up the resolution. The major fix to this, as with many of the problems I’ve mentioned, seems to be in setting my Depth Biases correctly. The values I’ve tried haven’t helped with the issue presented by front-face culling but perhaps I’ll have better luck with back-face culling. However, back-face culling also introduces a new, possibly related problem. With the height map featured in most of the images and all of the videos I’ve posted so far, there are regions along the edge of the map that are elevated above the terrain’s interior. When the Sun rises or sets behind these regions, with back-face culling enabled, it actually shines through the terrain and messes up the shadows.
backfacecullingissue
This definitely can’t be fixed by simply fiddling with the Depth Biases, and turning culling off would be another performance hit, but there is a simple solution that may help with some of the other issues. As I mentioned previously, I could add a skirt to the terrain. Essentially, I’d be turning it into a proper, closed 3D object. It would be a little tricky to figure out the correct indices so that the skirt correctly attached to the terrain along the edges, but would otherwise be straight forward. And it would add very few polygons. Given the current tessellation level of the base mesh, we’d be looking at about 8200 additional triangles on a 4096×4096 height map. I’d have to come up with a solution to avoid tessellating the edges of the skirt that aren’t also the edge of the terrain, but that shouldn’t be a big deal. This is probably something I should have done from the start and I will likely add this before moving on to Cascaded Shadow Maps, just to see if it does fix any of my existing problems.

That’s it for today. You can grab the latest code off of GitHub.

Traagen