Rendering Terrain Part 31 – Conclusion

After some four and a half months working on this project, I believe that I have finally come to the end. I think I’ve pretty much met my goals. But let’s recap and see.

1. I want to render the height map in a black and white 2D texture view. I want to work with height maps of more or less arbitrary size, determined at run time.

Done and done. The program opens up with a 2D texture view of the height map. I have a number of height maps that I’ve tested it with, ranging in size from 205×205 to 4096×4096. They all work without issue.

2. I want a sort of bird’s eye view, looking down at the terrain from far away and at an angle so you can see the entire terrain at once. I’ll be treating the height map as being 1 pixel per 1m2.

3. I want a first person view so I can run around on the terrain. I’ll implement simple mouse look and lock the camera to be a certain distance from the surface.

I originally thought of these as two separate views, but I later decided to treat them as one. So in our 3D view, you can fly around freely or lock the camera to the terrain. The locking the camera to the terrain feature is new. I had actually forgotten about wanting to do that.
It was relatively simple to implement. As far as the camera was concerned, I just needed to get the position of the eye, use it to find the height of the terrain at that point, and then reset the eye to a specific height above the terrain.

if (m_LockToTerrain) {
	XMFLOAT4 eye = m_Cam.GetEyePosition();
	float h = m_pT->GetHeightAtPoint(eye.x, eye.y) + 2;
	m_Cam.LockPosition(XMFLOAT4(eye.x, eye.y, h, 1.0f));
}

Calculating the height of the terrain at the given point was a little bit trickier since the final height of a point is actually calculated by the GPU when rendering. I had to reproduce the same calculation on the CPU. Of course, we’re not sampling a texture. We’re indexing into an array. In fact, two arrays as we need to add the height value from the displacement map to the height map value. But we also need to calculate the normal so we can offset in the correct direction.

float Terrain::GetHeightMapValueAtPoint(float x, float y) {
	// use bilinear interpolation to calculate the height of the base terrain at point (x, y).
	float x1 = floorf(x);
	x1 = x1 < 0.0f ? 0.0f : x1 > m_wHeightMap ? m_wHeightMap : x1;
	float x2 = ceilf(x);
	x2 = x2 < 0.0f ? 0.0f : x2 > m_wHeightMap ? m_wHeightMap : x2;
	float dx = x - x1;
	float y1 = floorf(y);
	y1 = y1 < 0.0f ? 0.0f : y1 > m_hHeightMap ? m_hHeightMap : y1;
	float y2 = ceilf(y);
	y2 = y2 < 0.0f ? 0.0f : y2 > m_hHeightMap ? m_hHeightMap : y2;
	float dy = y - y1;
	
	float a = (float)m_dataHeightMap[(int)(y1 * m_wHeightMap + x1) * 4] / 255.0f;
	float b = (float)m_dataHeightMap[(int)(y1 * m_wHeightMap + x2) * 4] / 255.0f;
	float c = (float)m_dataHeightMap[(int)(y2 * m_wHeightMap + x1) * 4] / 255.0f;
	float d = (float)m_dataHeightMap[(int)(y2 * m_wHeightMap + x2) * 4] / 255.0f;

	return bilerp(a, b, c, d, dx, dy);
}

float Terrain::GetDisplacementMapValueAtPoint(float x, float y) {
	float _x = x / m_wDisplacementMap / 32;
	float _y = y / m_hDisplacementMap / 32;
	// use bilinear interpolation to calculate the height of the base terrain at point (x, y).
	float x1 = floorf(_x);
	float x2 = ceilf(_x);
	float dx = _x - x1;
	float y1 = floorf(_y);
	float y2 = ceilf(_y);
	float dy = _y - y1;

	float a = (float)m_dataDisplacementMap[(int)(y1 * m_wDisplacementMap + x1) * 4 + 3] / 255.0f;
	float b = (float)m_dataDisplacementMap[(int)(y1 * m_wDisplacementMap + x2) * 4 + 3] / 255.0f;
	float c = (float)m_dataDisplacementMap[(int)(y2 * m_wDisplacementMap + x1) * 4 + 3] / 255.0f;
	float d = (float)m_dataDisplacementMap[(int)(y2 * m_wDisplacementMap + x2) * 4 + 3] / 255.0f;

	return bilerp(a, b, c, d, dx, dy);
}

XMFLOAT3 Terrain::CalculateNormalAtPoint(float x, float y) {
	XMFLOAT2 b(x, y - 0.3f / m_hHeightMap);
	XMFLOAT2 c(x + 0.3f / m_wHeightMap, y - 0.3f / m_hHeightMap);
	XMFLOAT2 d(x + 0.3f / m_wHeightMap, y);
	XMFLOAT2 e(x + 0.3f / m_wHeightMap, y + 0.3f / m_hHeightMap);
	XMFLOAT2 f(x, y +  0.3f / m_hHeightMap);
	XMFLOAT2 g(x - 0.3f / m_wHeightMap, y + 0.3f / m_hHeightMap);
	XMFLOAT2 h(x - 0.3f / m_wHeightMap, y);
	XMFLOAT2 i(x - 0.3f / m_wHeightMap, y - 0.3f / m_hHeightMap);

	float zb = GetHeightMapValueAtPoint(b.x, b.y) * m_scaleHeightMap;
	float zc = GetHeightMapValueAtPoint(c.x, c.y) * m_scaleHeightMap;
	float zd = GetHeightMapValueAtPoint(d.x, d.y) * m_scaleHeightMap;
	float ze = GetHeightMapValueAtPoint(e.x, e.y) * m_scaleHeightMap;
	float zf = GetHeightMapValueAtPoint(f.x, f.y) * m_scaleHeightMap;
	float zg = GetHeightMapValueAtPoint(g.x, g.y) * m_scaleHeightMap;
	float zh = GetHeightMapValueAtPoint(h.x, h.y) * m_scaleHeightMap;
	float zi = GetHeightMapValueAtPoint(i.x, i.y) * m_scaleHeightMap;

	float u = zg + 2 * zh + zi - zc - 2 * zd - ze;
	float v = 2 * zb + zc + zi - ze - 2 * zf - zg;
	float w = 8.0f;

	XMFLOAT3 norm(u, v, w);
	XMVECTOR normalized = XMVector3Normalize(XMLoadFloat3(&norm));
	XMStoreFloat3(&norm, normalized);

	return norm;
}

float Terrain::GetHeightAtPoint(float x, float y) {
	float z = GetHeightMapValueAtPoint(x, y) * m_scaleHeightMap;
	float d = 2.0f * GetDisplacementMapValueAtPoint(x, y) - 1.0f;
	XMFLOAT3 norm = CalculateNormalAtPoint(x, y);
	XMFLOAT3 pos(x, y, z);
	XMVECTOR normal = XMLoadFloat3(&norm);
	XMVECTOR position = XMLoadFloat3(&pos);
	XMVECTOR posFinal = position + normal * 0.5f * d;
	XMFLOAT3 fp;
	XMStoreFloat3(&fp, posFinal);

	return fp.z;
}

This isn’t perfect. Because we displace the terrain’s vertices along the normals, we cause the terrain to bump out in ways that this doesn’t take into account. When we climb a steep hill, our camera may wind up inside the terrain because there are vertices that have been projected out above the point we are at. Since their original position is a different (x, y) value, we can’t easily find them.
We’d need some sort of collision detection system to handle this. We’d have to find our expected position and then collide that with the terrain to see if we can actually be there. If not, adjust for it.

4. I’m going to be using Direct3D 12.0 for this.

Yup. I used DirectX 12. It was totally unnecessary and I probably would have had a generally easier time working in DirectX 11, but it was good to get some experience with the basics of DX12. That being said, I still have a lot to learn. I only used Committed Resources, but what about Reserved and Placed Resources? What about multi-threading, which is pretty much the whole point of using DX12? I’ll have to tackle these and other points in future projects.

5. I want to implement tessellation. I’ll only implement it in the first person view mode as the bird’s eye view mode camera is too far away from the terrain to be visible anyway. I haven’t decided at this point whether or not I want to implement any sort of Level of Detail system. I think it depends on the FPS I’m getting. Long term, the tessellation would be part of the LOD system and would automatically be taken into account so there would be no difference between the 2 3D view modes I want. I don’t know if I’ll implement that right away or not.

I wound up implementing an LOD system using dynamic tessellation. It actually turned out to be one of the easier parts of this project.

6. In terms of texturing the terrain, I’d prefer not to use textures, but rather just use a decent colour palette.

I did implement a simple colour palette. Nothing too fancy, but it works. I actually wound up adding texturing and bump mapping as well. I think that looks a lot better.

7. I’d like to implement self shadowing.

This was by far the most frustrating part of the project. Implementing basic Shadow Mapping wasn’t terrible, but Cascaded Shadow Maps was painful. I did eventually get them figured out and working correctly and now I think they look pretty good.

There will be only one source of light: the Sun. It would be nice, however, to be able to support arbitrary directions for the Sun, so that I can have a day-night cycle and treat the height map as though it is at an arbitrary position on the globe.

I didn’t really do all this. I do have a Sun. Hell, I almost implemented a second light source for the moon. It wouldn’t have been that difficult to add, but it would have killed the frame rate as I’d have needed an additional four shadow passes for it.
My Shadow Mapping implementation will also handle an arbitrary position for the light source. But my actual day/night cycle is hard-coded to a specific, easy to implement, axis. I’m not going to deal with changing that now. It would just be a matter of implementing something different in the DayNightCycle class.

8. While I’m not terribly worried about efficiency with this project, I’d still like to maintain a frame rate at or above 60fps. Best case, I’d like it to stay above 120fps.

I actually didn’t do too terrible with this. With diffuse texturing disabled, the frame rate pretty much always stays above 60fps. I didn’t quite manage to keep it above 120fps. On average, it runs closer to 100fps.
With diffuse texturing enabled, the frame rate drops significantly, but we still stay above 30fps. That’s not terrible.
To improve on performance, I’d have to find a way to further reduce texture sampling. That seems to be the real killer right now. I was geometry limited until I implemented triplanar mapping. If I could find a way to generate (u, v) texture coordinates for the terrain that took the shape of the terrain into account and avoided texture stretching, I could drastically reduce the number of samples needed.
I never did revisit trying to reduce the number of passes required for Shadow Mapping. Using CSM currently means I need four passes, and we need to tessellate and render the terrain for each of those passes. I’ll have to come back to this at some point.
If we could get back to being geometry limited, I think there’s a big savings that could be made with the terrain’s mesh. Right now, we’re generating a static, homogeneous mesh centered on the origin. If we instead centered the mesh on the eye and stepped down the resolution of the patches as we got further away, we could reduce the number of triangles by a fair bit, while maintaining or even improving the visible resolution. I definitely want to implement this at some point, but I don’t really think it will have an impact on this project with the current texture sampling scheme.

My desktop will be my target platform. I’m running Windows 10 Pro, 32GB of RAM, a GTX680 with 4GB of video RAM, and a 4K monitor. So any target fps I aim for will be on that computer. As I’ve already made mention to, I’m working in Visual Studio 2015 and using Direct3D 12.0, although my card actually only supports feature level 11.0 so I won’t be using any more advanced features. I really have no reason to in this project, anyway.
EDIT – I have a 4K monitor, but for most of the performance info I mention, I’m actually only rendering a 1920×1080 window.

Nothing changed here. I tried running the program at 4K, but that drops the frame rate a bit further. I didn’t test extensively, but we drop down to at least 25fps, probably lower at worst case.

Overall, I’m pretty happy with the results of this project. I do think I wound up tackling too much at once. The shear number of posts attached to this project can tell you that.
What was new to me in this project? DirectX 12, tessellation, bump mapping, texture splatting, shadow maps, and CSM. So, pretty much everything of note.
In future projects I’d like to try and minimize the number of new things I introduce. That will lead to more, smaller projects. At least in theory. To some extent, projects may still wind up being of a decent size, as I’ll often be building on work done in earlier projects. The goal is just to not overwhelm myself with new topics like I’d say I did in this project.

For the latest version of the code, see GitHub.

Traagen