Shading Technique - Terrain Shading

From Horde3D Wiki
Jump to: navigation, search

Explanation of Technique

If you look at any piece of land in the real world you'll notice that it's made of a number of different materials. Rocks, dirt, foliage, etc. You'll also notice that the materials at some levels are different than those at higher or lower levels and that some materials don't show up on slopes over a certain degree (like foliage on steep slopes.) Our purpose here is to mimic this aspect of nature to some degree.

The Material

Terrain shader material
<Material>
	<Shader source="terrain.shader.xml"/>
	
	<TexUnit unit="0" map="heightmap.tga" allowCompression="false" />
	<TexUnit unit="1" map="dirt.jpg" />
	<TexUnit unit="2" map="grass.jpg" />
	<TexUnit unit="3" map="rock.jpg" />
	<TexUnit unit="4" map="snow.jpg" />
	
	<Uniform name="sunDir" a="1.0" b="-1.0" c="0.0" />
	
	<!-- a=min height, b=max height, c=min slope, d=max slope -->
	<Uniform name="rockData" a="0.0" b="520.0" c="0.16" d="1.0" />
	<Uniform name="snowData" a="240.0" b="520.0" c="0.0" d="0.7" />
	<Uniform name="dirtData" a="0.0" b="270.0" c="0.0" d="0.83" />
	<Uniform name="grassData" a="0.0" b="256.0" c="0.0" d="0.66" />
	
	<!-- a=grass, b=dirt, c=rock, d=snow -->
	<Uniform name="mapStrength" a="1.0" b="0.4" c="0.28" d="0.86" />
</Material>

The Material Explained

There's nothing fancy going on here of course. I got the values for the data variables and map strength just by fiddling around with them in a small test program.

The Vertex Shader

Terrain vertex shader
uniform vec4 terBlockParams;
attribute float terHeight;
varying float height;
varying vec3 triTexCoords;
				
void main( void )
{
	vec4 newPos = vec4(gl_Vertex.x * terBlockParams.z + terBlockParams.x, terHeight,
						gl_Vertex.z * terBlockParams.z + terBlockParams.y, gl_Vertex.w);

	vec4 pos = calcWorldPos(newPos);
	triTexCoords = newPos.xyz;
	height = pos.y;

	gl_Position = gl_ModelViewProjectionMatrix * pos;
}

Explanation of the Vertex Shader

Some of you may have noticed that this is awfully similar to the example shader that comes with the terrain extension. The only differences here are a.) We're now using a vec3 to hold the texture coordinates which will be required for the triplanar texturing, and b.) We're saving the height of the vertex for the height/slope based portion of the fragment shader.

The Fragment Shader

Terrain fragment shader
uniform vec4 sunDir;
				
// tex0=heightmap, tex1=dirt, tex2=grass, tex3=rock, tex4=snow
uniform sampler2D tex0, tex1, tex2, tex3, tex4;
				
// .x = min height, .y = max height, .z = min slope, .w = max slope
uniform vec4 snowData;
uniform vec4 rockData;
uniform vec4 dirtData;
uniform vec4 grassData;
				
// .x=grass, .y=dirt, .z=rock, .w=snow
uniform vec4 mapStrength;
				
varying float height;
varying vec3 triTexCoords;
								
vec3 light = -normalize( sunDir.xyz );

// this comes from the book Real-Time 3D Terrain Engines Using C++ And DirectX 
float computeWeight(float value, float minExtent, float maxExtent)
{
	float weight = 0.0;
				
	if(value >= minExtent && value <= maxExtent)
	{
		float range = maxExtent - minExtent;
		
		weight = value - minExtent;
						
		// convert to [0, 1] based on its distance to midpoint of the extents
		weight *= 1.0 / range;
		weight -= 0.5;
		weight *= 2.0;
						
		// square result for non-linear falloff
		weight *= weight;
						
		// invert and bound check
		weight = 1.0 - abs(weight);
		weight = clamp(weight, 0.001, 1.0);
	}
					
	return weight;
}
				
void main( void )
{
	vec4 texel = texture2D(tex0, triTexCoords.xz) * 2.0 - 1.0;
	// Use max because of numerical issues
	float ny = sqrt(max(1.0 - texel.b*texel.b - texel.a*texel.a, 0.0));		
	vec3 normal = vec3(texel.b, ny, texel.a);

	// Wrap lighting for sun
	float l = max( dot( normal, light ), 0.0 ) * 0.5 + 0.5;
					
	// slope: 1.0 = steep, 0.0 = flat
	float slope = 1.0 - ny;
					
	vec4 weights = vec4(0.0, 0.0, 0.0, 0.0);
	
	// once all 3 lookups have been done for a texture these weights
	// say how much of the sum of those lookups to use
	weights.x = computeWeight(height, rockData.x, rockData.y) * 
				computeWeight(slope, rockData.z, rockData.w) * mapStrength.z;
	weights.y = computeWeight(height, dirtData.x, dirtData.y) * 
				computeWeight(slope, dirtData.z, dirtData.w) * mapStrength.y;
	weights.z = computeWeight(height, snowData.x, snowData.y) * 
				computeWeight(slope, snowData.z, snowData.w) * mapStrength.w;
	weights.w = computeWeight(height, grassData.x, grassData.y) * 
				computeWeight(slope, grassData.z, grassData.w) * mapStrength.x;
	weights *= 1.0 / (weights.x + weights.y + weights.z + weights.w);
					
	// this comes from the gpu gems 3 article: 
	// generating complex procedural terrains using the gpu
	// used to determine how much of each planar lookup to use
	// for each texture
	vec3 tpweights = abs(normal);
	tpweights = (tpweights - 0.2) * 7.0;
	tpweights = max(tpweights, vec3(0.0, 0.0, 0.0));
	tpweights /= (tpweights.x + tpweights.y + tpweights.z).xxx;
				
	vec4 finalColor = vec4(0.0, 0.0, 0.0, 1.0);
	vec4 tempColor = vec4(0.0, 0.0, 0.0, 1.0);
						
	// dirt
	tempColor = tpweights.z * texture2D(tex1, triTexCoords.xy*5.0);
	tempColor += tpweights.x * texture2D(tex1, triTexCoords.yz*5.0);
	tempColor += tpweights.y * texture2D(tex1, triTexCoords.xz*5.0);
	finalColor += weights.y * tempColor;

	// grass
	tempColor = tpweights.z * texture2D(tex2, triTexCoords.xy*5.0);
	tempColor += tpweights.x * texture2D(tex2, triTexCoords.yz*5.0);
	tempColor += tpweights.y * texture2D(tex2, triTexCoords.xz*5.0);
	finalColor += weights.w * tempColor;
	
	// rock
	tempColor = tpweights.z * texture2D(tex3, triTexCoords.xy*5.0);
	tempColor += tpweights.x * texture2D(tex3, triTexCoords.yz*5.0);
	tempColor += tpweights.y * texture2D(tex3, triTexCoords.xz*5.0);
	finalColor += weights.x * tempColor;
	
	// snow
	tempColor = tpweights.z * texture2D(tex4, triTexCoords.xy*5.0);
	tempColor += tpweights.x * texture2D(tex4, triTexCoords.yz*5.0);
	tempColor += tpweights.y * texture2D(tex4, triTexCoords.xz*5.0);
	finalColor += weights.z * tempColor;
					
	gl_FragColor = finalColor * l;
}

Explanation of the Fragment Shader

The most important part of this shader is in calculating the weights to use not only for each texture but also for each plane(xy, yz and xz.)

The weights variable holds the weights for each individual texture. Height and slope weights are calculated for each texture then multiplied with the strength of the texture which basically tells how important that texture is when it's being blended with others. The last line of the weights calculation makes sure the sum of all the weights is 1.

The tpweights variable holds the weights for each plane. It's based on the normal that was calculated at the start of the function and must also sum to 1.

Once we have all our weights we do a texture lookup using each of the planes in triTexCoords as texture coordinates, multiply each by it's weight, add them all up and then add that sum to the final color.

Final Words

This is quite an expensive shader. You could get away with adding another texture or two to the mix but you'd quickly start limiting your target audience. You may notice this is also a modified version of the example shader from the terrain extension.

Example Results

Terrainus6.jpg