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
  1. <Material>
    
  2.         <Shader source="terrain.shader.xml"/>
    
  3.  
    
  4.         <TexUnit unit="0" map="heightmap.tga" allowCompression="false" />
    
  5.         <TexUnit unit="1" map="dirt.jpg" />
    
  6.         <TexUnit unit="2" map="grass.jpg" />
    
  7.         <TexUnit unit="3" map="rock.jpg" />
    
  8.         <TexUnit unit="4" map="snow.jpg" />
    
  9.  
    
  10.         <Uniform name="sunDir" a="1.0" b="-1.0" c="0.0" />
    
  11.  
    
  12.         <!-- a=min height, b=max height, c=min slope, d=max slope -->
    
  13.         <Uniform name="rockData" a="0.0" b="520.0" c="0.16" d="1.0" />
    
  14.         <Uniform name="snowData" a="240.0" b="520.0" c="0.0" d="0.7" />
    
  15.         <Uniform name="dirtData" a="0.0" b="270.0" c="0.0" d="0.83" />
    
  16.         <Uniform name="grassData" a="0.0" b="256.0" c="0.0" d="0.66" />
    
  17.  
    
  18.         <!-- a=grass, b=dirt, c=rock, d=snow -->
    
  19.         <Uniform name="mapStrength" a="1.0" b="0.4" c="0.28" d="0.86" />
    
  20. </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
  1. uniform vec4 terBlockParams;
    
  2. attribute float terHeight;
    
  3. varying float height;
    
  4. varying vec3 triTexCoords;
    
  5.  
    
  6. void main( void )
    
  7. {
    
  8.         vec4 newPos = vec4(gl_Vertex.x * terBlockParams.z + terBlockParams.x, terHeight,
    
  9.                                                 gl_Vertex.z * terBlockParams.z + terBlockParams.y, gl_Vertex.w);
    
  10.  
    
  11.         vec4 pos = calcWorldPos(newPos);
    
  12.         triTexCoords = newPos.xyz;
    
  13.         height = pos.y;
    
  14.  
    
  15.         gl_Position = gl_ModelViewProjectionMatrix * pos;
    
  16. }
    

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
  1. uniform vec4 sunDir;
    
  2.  
    
  3. // tex0=heightmap, tex1=dirt, tex2=grass, tex3=rock, tex4=snow
    
  4. uniform sampler2D tex0, tex1, tex2, tex3, tex4;
    
  5.  
    
  6. // .x = min height, .y = max height, .z = min slope, .w = max slope
    
  7. uniform vec4 snowData;
    
  8. uniform vec4 rockData;
    
  9. uniform vec4 dirtData;
    
  10. uniform vec4 grassData;
    
  11.  
    
  12. // .x=grass, .y=dirt, .z=rock, .w=snow
    
  13. uniform vec4 mapStrength;
    
  14.  
    
  15. varying float height;
    
  16. varying vec3 triTexCoords;
    
  17.  
    
  18. vec3 light = -normalize( sunDir.xyz );
    
  19.  
    
  20. // this comes from the book Real-Time 3D Terrain Engines Using C++ And DirectX 
    
  21. float computeWeight(float value, float minExtent, float maxExtent)
    
  22. {
    
  23.         float weight = 0.0;
    
  24.  
    
  25.         if(value >= minExtent && value <= maxExtent)
    
  26.         {
    
  27.                 float range = maxExtent - minExtent;
    
  28.  
    
  29.                 weight = value - minExtent;
    
  30.  
    
  31.                 // convert to [0, 1] based on its distance to midpoint of the extents
    
  32.                 weight *= 1.0 / range;
    
  33.                 weight -= 0.5;
    
  34.                 weight *= 2.0;
    
  35.  
    
  36.                 // square result for non-linear falloff
    
  37.                 weight *= weight;
    
  38.  
    
  39.                 // invert and bound check
    
  40.                 weight = 1.0 - abs(weight);
    
  41.                 weight = clamp(weight, 0.001, 1.0);
    
  42.         }
    
  43.  
    
  44.         return weight;
    
  45. }
    
  46.  
    
  47. void main( void )
    
  48. {
    
  49.         vec4 texel = texture2D(tex0, triTexCoords.xz) * 2.0 - 1.0;
    
  50.         // Use max because of numerical issues
    
  51.         float ny = sqrt(max(1.0 - texel.b*texel.b - texel.a*texel.a, 0.0));             
    
  52.         vec3 normal = vec3(texel.b, ny, texel.a);
    
  53.  
    
  54.         // Wrap lighting for sun
    
  55.         float l = max( dot( normal, light ), 0.0 ) * 0.5 + 0.5;
    
  56.  
    
  57.         // slope: 1.0 = steep, 0.0 = flat
    
  58.         float slope = 1.0 - ny;
    
  59.  
    
  60.         vec4 weights = vec4(0.0, 0.0, 0.0, 0.0);
    
  61.  
    
  62.         // once all 3 lookups have been done for a texture these weights
    
  63.         // say how much of the sum of those lookups to use
    
  64.         weights.x = computeWeight(height, rockData.x, rockData.y) * 
    
  65.                                 computeWeight(slope, rockData.z, rockData.w) * mapStrength.z;
    
  66.         weights.y = computeWeight(height, dirtData.x, dirtData.y) * 
    
  67.                                 computeWeight(slope, dirtData.z, dirtData.w) * mapStrength.y;
    
  68.         weights.z = computeWeight(height, snowData.x, snowData.y) * 
    
  69.                                 computeWeight(slope, snowData.z, snowData.w) * mapStrength.w;
    
  70.         weights.w = computeWeight(height, grassData.x, grassData.y) * 
    
  71.                                 computeWeight(slope, grassData.z, grassData.w) * mapStrength.x;
    
  72.         weights *= 1.0 / (weights.x + weights.y + weights.z + weights.w);
    
  73.  
    
  74.         // this comes from the gpu gems 3 article: 
    
  75.         // generating complex procedural terrains using the gpu
    
  76.         // used to determine how much of each planar lookup to use
    
  77.         // for each texture
    
  78.         vec3 tpweights = abs(normal);
    
  79.         tpweights = (tpweights - 0.2) * 7.0;
    
  80.         tpweights = max(tpweights, vec3(0.0, 0.0, 0.0));
    
  81.         tpweights /= (tpweights.x + tpweights.y + tpweights.z).xxx;
    
  82.  
    
  83.         vec4 finalColor = vec4(0.0, 0.0, 0.0, 1.0);
    
  84.         vec4 tempColor = vec4(0.0, 0.0, 0.0, 1.0);
    
  85.  
    
  86.         // dirt
    
  87.         tempColor = tpweights.z * texture2D(tex1, triTexCoords.xy*5.0);
    
  88.         tempColor += tpweights.x * texture2D(tex1, triTexCoords.yz*5.0);
    
  89.         tempColor += tpweights.y * texture2D(tex1, triTexCoords.xz*5.0);
    
  90.         finalColor += weights.y * tempColor;
    
  91.  
    
  92.         // grass
    
  93.         tempColor = tpweights.z * texture2D(tex2, triTexCoords.xy*5.0);
    
  94.         tempColor += tpweights.x * texture2D(tex2, triTexCoords.yz*5.0);
    
  95.         tempColor += tpweights.y * texture2D(tex2, triTexCoords.xz*5.0);
    
  96.         finalColor += weights.w * tempColor;
    
  97.  
    
  98.         // rock
    
  99.         tempColor = tpweights.z * texture2D(tex3, triTexCoords.xy*5.0);
    
  100.         tempColor += tpweights.x * texture2D(tex3, triTexCoords.yz*5.0);
    
  101.         tempColor += tpweights.y * texture2D(tex3, triTexCoords.xz*5.0);
    
  102.         finalColor += weights.x * tempColor;
    
  103.  
    
  104.         // snow
    
  105.         tempColor = tpweights.z * texture2D(tex4, triTexCoords.xy*5.0);
    
  106.         tempColor += tpweights.x * texture2D(tex4, triTexCoords.yz*5.0);
    
  107.         tempColor += tpweights.y * texture2D(tex4, triTexCoords.xz*5.0);
    
  108.         finalColor += weights.z * tempColor;
    
  109.  
    
  110.         gl_FragColor = finalColor * l;
    
  111. }
    

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

Personal tools