Rendering Terrain Part 23 – Height and Slope Based Colours

Getting blending of the normal maps to work is giving me a lot of trouble. I keep getting artifacts at transitions between zones that I can’t explain. Therefore, I’m just going to talk about picking the colour based on the height and slope of the terrain.

This tutorial on Rastertek does a pretty good job of explaining slope based texturing. I pretty much just replaced the texture lookups with static colours.
That tutorial also uses the y-axis as up whereas I use the z-axis. He also has a typo in the first paragraph which makes it sound like he’s using N.y – 1 when the code clearly uses 1 – N.y. I actually found that for my code, using 1 – N.z does not give adequate results.

Slopes coloured with N.z - 1

Slopes coloured with N.z – 1


It may be difficult to tell from the above image, but all of the slopes are well above 45° angles. I feel like I’m getting a lot less variance than they do in the tutorial.

This StackOverflow topic gives another approach to calculating the slope from the normal. It appears to be based in the same math. If your vertical is defined as (0, 0, 1), as mine is, then N.z will be the Cosine of the angle between the normal and vertical. This means that you can get the angle of the slope in Radians by taking the Inverse Cosine of N.z.

float slope = acos(norm.z);

The results appear much better:

Slope calculated using acos(N.z).

Slope calculated using acos(N.z).

So how do we actually choose the colours based on slope? It’s pretty easy, actually. Just define bands of slope values and decide what colours will be represented within those bands. For bands that represent transition from one slope to another, linearly interpolate between them.
I decided to only have two colours for slopes: steep and flat. I have four colours all together for different heights, which we’ll get to. Here are my current colour definitions:

float4 grass = float4(0.22f, 0.52f, 0.11f, 1.0f);
float4 dirt = float4(0.35f, 0.20f, 0.0f, 1.0f);
float4 rock = float4(0.42f, 0.42f, 0.52f, 1.0f);
float4 snow = float4(0.8f, 0.8f, 0.8f, 1.0f);

With those defined and my slope calculated, I can now figure out the final colour by slope:

float4 slope_based_color(float slope, float4 colorSteep, float4 colorFlat) {
	if (slope < 0.25f) {
		return colorFlat;
	}

	if (slope < 0.5f) {
		float blend = (slope - 0.25f) * (1.0f / (0.5f - 0.25f));

		return lerp(colorFlat, colorSteep, blend);
	} else {
		return colorSteep;
	}
}

Calculating colour based on height is exactly the same. I’m using my worldpos.z value, which is already scaled. I could also use the [0, 1] value stored in our height map, but that would require another texture lookup to get. The height function first finds the height band we’re in, then calls slope_based_color() to determine the colours to blend between and then interpolates between them.

float4 height_and_slope_based_color(float height, float slope) {
	float4 grass = float4(0.22f, 0.52f, 0.11f, 1.0f);
	float4 dirt = float4(0.35f, 0.20f, 0.0f, 1.0f);
	float4 rock = float4(0.42f, 0.42f, 0.52f, 1.0f);
	float4 snow = float4(0.8f, 0.8f, 0.8f, 1.0f);

	float bounds = scale * 0.02f;
	float transition = scale * 0.6f;
	float greenBlendEnd = transition + bounds;
	float greenBlendStart = transition - bounds;
	float snowBlendEnd = greenBlendEnd + 2 * bounds;

	if (height < greenBlendStart) {
		// get grass/dirt values
		return slope_based_color(slope, dirt, grass);
	}

	if (height < greenBlendEnd) {
		// get both grass/dirt values and rock values and blend
		float4 c1 = slope_based_color(slope, dirt, grass);
		float4 c2 = rock;
	
		float blend = (height - greenBlendStart) * (1.0f / (greenBlendEnd - greenBlendStart));
		
		return lerp(c1, c2, blend);
	}

	if (height < snowBlendEnd) {
		// get rock values and rock/snow values and blend
		float4 c1 = rock;
		float4 c2 = slope_based_color(slope, rock, snow);
		
		float blend = (height - greenBlendEnd) * (1.0f / (snowBlendEnd - greenBlendEnd));

		return lerp(c1, c2, blend);
	}

	// get rock/snow values
	return slope_based_color(slope, rock, snow);
}

This isn’t necessarily a great way of doing things. Blending between different colours can feel a little unnatural. Especially if the colours are quite distinct. Perhaps a better artist could pick colours that work better together, or we could have more bands to blend between.

coloursblending

You can see the grass and dirt blending into the rock in this image.


The other thing I’m wondering about is whether there is a way to do this without all of the dynamic branching. That used to be a very bad thing to have in a shader. Perhaps it isn’t a big deal anymore, but it feels like I should be trying to eliminate as much of that as possible.
There isn’t a lot else I can think to do, short of adding another texture to sample to determine which colour a pixel should be, and we’d still probably want some level of blending to smooth things out.

If we were using diffuse textures instead of flat colours, I would want something like what’s described here, but I don’t think we can handle that many texture lookups at the moment. The normal maps for the added detail is killing us already.

Speaking of normals, hopefully I’ll get height and slope based normal blending figured out for the next post. Once I find a solution for that, then I want to talk about optimizations and bug fixes, some I’ll implement right away, some will be left for a future project. Then I’ll refactor my code one last time because it is a mess and I think I can structure it a lot better. Then we’re done with this project for now and we’ll move on to the next one.

For the latest code, goto GitHub.

Traagen