Rendering Terrain Part 27 – Making Things Look Better

Last post, I talked about wanting to make things look a bit better by having the same texture for all steep areas, no matter the height, rather than trying to blend between them. I figure this is more like what you see in nature. Mountains tend to be made up of essentially the same type of rock, from bottom to top.
So I wound up following this tutorial much more closely. I also wound up flipping the height and slope based texturing around. So last post we were getting the colour/texture based on height first, then slope. Now we are getting the slope first, then the height. So now I’m using a flat slope colour/texture, a steep slope colour/texture, and an in between. I use the flat value for a steeper slope than the tutorial. I found I liked keeping the flat value up to a slope of 0.6, then use the intermediate value up to 0.65, then the steep value for the rest. The result is grass or snow on slopes you would expect them to stick to, rock in steep areas, and dirt in the transition areas.
As far as looks were concerned, I didn’t take any pictures at this stage, but I was reasonably happy with it. I didn’t like the transition from grass to snow. It was too sudden, so I brought back the rock only region. Basically, we just transition from grass to rock, then from rock to snow, with no ‘just rock’ region. This, I find, helps spread things out a bit. The transition is still pretty noticeable from a distance, but that’s not necessarily wrong. This real mountain sure looks like it has a pretty hard transition line from a distance.
realmountain

At this point, I was still having a hard time accepting the unnatural looking blending between layers by height. Slope looked reasonable, perhaps because the change was more sudden and the blending just cleans it up. Blending by height looked like crap, though. This isn’t the best screenshot to show it, but you can see the vibrant green turn into a more sickly green near the top before fading to white.
coloursblending
I don’t know how to fix that so it looks good with colours, but I’ve been wanting to implement textures anyway.
Adding diffuse textures at this point is pretty easy. I just load four more textures into my texture array. I ensure the diffuse textures are in the same order as the normal maps, then I can use the same functions to look the colour up as for the normal, just with a different starting index.
Again, I skipped grabbing photos of what it looked like at this point. Pretty good, but it still had the odd blending happening. I decided to implement this ‘Advanced Terrain Texture Splatting’ method to fix it. The only difference is that instead of using an opacity map, I’m just using the same blend value I was already calculating. For the depth map, I used CrazyBump again to create a grey scale height value from the diffuse map. Then I saved that value into the alpha channel of each of my detail and diffuse maps. In a real engine, you’d probably pack this into the files to minimize load times, but I’ve just got the depth maps as separate files and combine them into one texture at load time.
Here’s some of the final shader functions:

float3 GetTexByHeightPlanar(float height, float3 uvw, float low, float med, float high) {
	float bounds = scale * 0.005f;
	float transition = scale * 0.6f;
	float lowBlendStart = transition - 2 * bounds;
	float highBlendEnd = transition + 2 * bounds;
	float3 c;

	if (height < lowBlendStart) {
		c = detailmaps.Sample(displacementsampler, float3(uvw.xy, low));
	} else if (height < transition) {
		float4 c1 = detailmaps.Sample(displacementsampler, float3(uvw.xy, low));
		float4 c2 = detailmaps.Sample(displacementsampler, float3(uvw.xy, med));

		float blend = (height - lowBlendStart) * (1.0f / (transition - lowBlendStart));
		float ma = max(c1.a + 1 - blend, c2.a + blend) - 0.2f;

		float b1 = max(c1.a + 1 - blend - ma, 0);
		float b2 = max(c2.a + blend - ma, 0);

		c = (c1.xyz * b1 + c2.xyz * b2) / (b1 + b2);
	} else if (height < highBlendEnd) {
		float4 c1 = detailmaps.Sample(displacementsampler, float3(uvw.xy, med));
		float4 c2 = detailmaps.Sample(displacementsampler, float3(uvw.xy, high));

		float blend = (height - transition) * (1.0f / (highBlendEnd - transition));
		float ma = max(c1.a + 1 - blend, c2.a + blend) - 0.2f;

		float b1 = max(c1.a + 1 - blend - ma, 0);
		float b2 = max(c2.a + blend - ma, 0);

		c = (c1.xyz * b1 + c2.xyz * b2) / (b1 + b2);
	} else {
		c = detailmaps.Sample(displacementsampler, float3(uvw.xy, high));
	}

	return c;
}

float3 GetTexByHeightTriplanar(float height, float3 uvw, float3 N, float index1, float index2) {
	float bounds = scale * 0.005f;
	float transition = scale * 0.6f;
	float blendStart = transition - bounds;
	float blendEnd = transition + bounds;
	float3 c;

	if (height < blendStart) {
		c = SampleDetailTriplanar(uvw, N, index1);
	} else if (height < blendEnd) {
		float4 c1 = SampleDetailTriplanar(uvw, N, index1);
		float4 c2 = SampleDetailTriplanar(uvw, N, index2);
		float blend = (height - blendStart) * (1.0f / (blendEnd - blendStart));
		float ma = max(c1.a + 1 - blend, c2.a + blend) - 0.2f;

		float b1 = max(c1.a + 1 - blend - ma, 0);
		float b2 = max(c2.a + blend - ma, 0);

		c = (c1.xyz * b1 + c2.xyz * b2) / (b1 + b2);
	} else {
		c = SampleDetailTriplanar(uvw, N, index2);
	}

	return c;
}

float4 GetColorByHeight(float height, float low, float med, float high) {
	float bounds = scale * 0.005f;
	float transition = scale * 0.6f;
	float lowBlendStart = transition - 2 * bounds;
	float highBlendEnd = transition + 2 * bounds;
	float4 c;

	if (height < lowBlendStart) {
		c = colors[low];
	} else if (height < transition) {
		float4 c1 = colors[low];
		float4 c2 = colors[med];

		float blend = (height - lowBlendStart) * (1.0f / (transition - lowBlendStart));

		c = lerp(c1, c2, blend);
	} else if (height < highBlendEnd) {
		float4 c1 = colors[med];
		float4 c2 = colors[high];

		float blend = (height - transition) * (1.0f / (highBlendEnd - transition));

		c = lerp(c1, c2, blend);
	} else {
		c = colors[high];
	}

	return c;
}

float3 GetTexBySlope(float slope, float height, float3 N, float3 uvw, float startingIndex) {
	float3 c;
	float blend;
	if (slope < 0.6f) {
		blend = slope / 0.6f;
		float3 c1 = GetTexByHeightPlanar(height, uvw, 0 + startingIndex, 3 + startingIndex, 1 + startingIndex);
		float3 c2 = GetTexByHeightTriplanar(height, uvw, N, 2 + startingIndex, 3 + startingIndex);
		c = lerp(c1, c2, blend);
	} else if (slope < 0.65f) {
		blend = (slope - 0.6f) * (1.0f / (0.65f - 0.6f));
		float3 c1 = GetTexByHeightTriplanar(height, uvw, N, 2 + startingIndex, 3 + startingIndex);
		float3 c2 = SampleDetailTriplanar(uvw, N, 3 + startingIndex);
		c = lerp(c1, c2, blend);
	} else {
		c = SampleDetailTriplanar(uvw, N, 3 + startingIndex);
	}

	return c;
}

float4 GetColorBySlope(float slope, float height) {
	float4 c;
	float blend;
	if (slope < 0.6f) {
		blend = slope / 0.6f;
		float4 c1 = GetColorByHeight(height, 0, 3, 1);
		float4 c2 = GetColorByHeight(height, 2, 3, 3);
		c = lerp(c1, c2, blend);
	} else if (slope < 0.65f) { 
		blend = (slope - 0.6f) * (1.0f / (0.65f - 0.6f)); 
		float4 c1 = GetColorByHeight(height, 2, 3, 3); 
		float4 c2 = colors[3]; c = lerp(c1, c2, blend); 
	} else { 
		c = colors[3]; 
	}	 

	return c; 
} 

float3 PerturbNormalByHeightSlope(float height, float slope, float3 N, float3 V, float3 uvw) { 
	float3 c = GetTexBySlope(slope, height, N, uvw, 0) - 0.5f; 
	float3x3 TBN = cotangent_frame(N, -V, uvw); 
	return normalize(mul(c, TBN)); 
} 

float4 dist_based_texturing(float height, float slope, float3 N, float3 V, float3 uvw) { 
	float dist = length(V); 
	if (dist > 75) return GetColorBySlope(slope, height);
	else if (dist > 25) {
		float blend = (dist - 25.0f) * (1.0f / (75.0f - 25.0f));
		float4 c1 = float4(GetTexBySlope(slope, height, N, uvw, 4), 1);
		float4 c2 = GetColorBySlope(slope, height);
		return lerp(c1, c2, blend);
	} else return float4(GetTexBySlope(slope, height, N, uvw, 4), 1);
}

float3 dist_based_normal(float height, float slope, float3 N, float3 V, float3 uvw) {
	float dist = length(V);

	float3 N1 = perturb_normal(N, V, uvw / 16, displacementmap, displacementsampler);
	//float3 N1 = perturb_normal_triplanar(N, V, uvw / 8, displacementmap, displacementsampler);

	if (dist > 150) return N;

	if (dist > 100) {
		float blend = (dist - 100.0f) / 50.0f;

		return lerp(N1, N, blend);
	}

	float3 N2 = PerturbNormalByHeightSlope(height, slope, N1, V, uvw);

	if (dist > 50) return N1;

	if (dist > 25) {
		float blend = (dist - 25.0f) / 25.0f;

		return lerp(N2, N1, blend);
	}

	return N2;
}

As you can see, I’ve implemented distance based texturing as well. I don’t have multiple levels of detail for my textures and using these textures at a distance causes shimmering as pixels shift colours. I need to minimize texturing in the distance to help with performance, anyway.
I’ve also implemented a toggle between using colours and using textures. Now, if you press ‘t’, you’ll toggle the textures on and off. While I was at it, I switched the render modes around so pressing ‘1’ shows you the height map, ‘2’ shows you the shadow maps, and ‘3’ puts you in 3D mode.

The final performance with the diffuse textures off was about 20.5ms, 48.7fps. With textures enabled, we’re a fair bit slower at 32.1ms, 31.2fps, worst case. On average, we’re running between 15-20ms, which puts us really close to 60fps. Still, it’s the worst case performance that matters. I’m going to settle with having managed to keep it at or above 30fps. I’m happy with how it looks and that was the main goal anyway.
Here’s some shots with and without texturing.

No textures, just colours.

No textures, just colours.


Same shot with texturing.

Same shot with texturing.


A region showing blending without textures.

A region showing blending without textures.


The same blend region with texturing enabled.

The same blend region with texturing enabled.


I also wanted to get some video of the terrain with and without texturing. I decided to mess with the scale of the height map, reducing it by a quarter for the video.

That’s it for what I want to add to the engine for this project. Obviously, there’s more that I’d like to do, but I’ll save those for future projects. Now, I need to rewrite the code for this project to clean it up. I’ve learned a lot and I think I can organize things better. I’m not sure I’ll have that done for Tuesday, so I may talk about something else games or graphics related, rather than another project post. We’ll see.
I’m only planning on doing the one post on the changes I make to the project, then I’ll write up a conclusion. After that, I’ll post an intro to my next project. I’ll be reusing a fair bit of this project for that one, but I think it’ll be a pretty cool project.

For the latest code, see GitHub.

Traagen