Tobias Alexander Franke

Terrain Texture Generation Redux

Introduction

A terrain isn’t textured by one pattern alone. Depending on the heightmap, there are a variety of features from sandy beaches to snowtop mountains that make up the range of possible texture combinations. With a set of textures for different landscape attributes at different heights and a heightmap, the only question that remains is: how to mix the textures? This short article will explain how the final texture for the terrain is generated.

Figure 1: Heightmap, texture and rendering Figure 1: Heightmap, texture and rendering

Terrain Texture Generation Redux is a new version of my old article on flipCode, but without the horrible english!

Mixing textures

Let’s assume that the landscape has n different attributes. For every attribute, there is a corresponding texture, for instance snow or grass. The mechanism works like this: Iterate through all pixels (x/y) in the heightmap and read the height-value at that location. Then, depending on the height, fetch the texture representing the attribute for a height-region and add that color to an output-texture at the same location (x/y). For example, a landscape that has height-values ranging from 0 to 255 could be divided like this:

The problem with this approach would be the very sharp edges on the output texture when two pixels border two regions. What would be way better is to fade the pixels from one region to another. To do that, there’s need for a fading-function.

float texfactor(float h1, float h2, float w)
{
    float percent;
    percent = (w - std::abs(h1 - h2)) / w;
    
    if(percent < 0.0f) 
        percent = 0.0f;
    else if(percent > 1.0f) 
        percent = 1.0f;

    return percent;
}

The function shown in the sourcecode is a weighting-function and has two parameteres h1 and h2, which represent two height-values, and a third parameter w that represents the absolute height of one region. What this function does is to return a percentage value that indicates wheter h2 is in a region defined by a representative value in that region h1, for instance the upper border. The last parameter is to adjust the height of one region, so in the case of four height-regions that equally divide a range of 256 values, w would be set to 64.

Moving further with the above example: The percentage of visibility for grass for a height value of 200 would yield 0%, since at that height there’s only rocks and snow left.

Code

In order to make it all work, some code to read and write image files is also neccessary! Assuming that it’s already been taken care off, the following code effectively does the magic:

struct rgb
{
    unsigned char r, g, b;
};

void generate_terrain_texture(std::vector<bitmap>& attributes, 
                              bitmap& heightmap,
                              bitmap& output,
                              unsigned int max_height)

{
    // the height of a region
    float region_height = max_height/attributes.size();
    
    // percentage of visibility for each attribute
    std::vector<float> texture_factor; 
    
    // the height at pos (x/y)
    float hmap_height; 

    // the new rgb values to be written
    rgb color;

    // all rgb values at pos (x/y) for all attributes
    std::vector<rgb> old_color;

    // iterate through all pixels
    for(size_t y = 0; y < heightmap.width; ++y) 
    for(size_t x = 0; x < heightmap.height; ++x)
    {
        // get height at pos (x/y) out of bitmap
        hmap_height = heightmap.getheight(x, y); 
        
        for(size_t i = 0; i < attributes.size(); ++i)
        {
            // get percentage for all bitmaps(Regions)
            texture_factor.push_back(
                texfactor(max_height - region_height, hmap_height)
            );
            
            // read all texture rgb values
            attributes[i].getcolor(x, y, color);
            old_color.push_back(color);
        }
        
        assert (old_color.size() == texture_factor.size() &&
        "Error: More colors than factors available or vice versa!");
        
        color.r = color.g = color.b = 0;
        
        // compose new rgb values for final texture
        for(size_t i = 0; i < old_color.size(); ++i)
        {        
            color.r += old_color[i].r * texture_factor[i];
            color.g += old_color[i].g * texture_factor[i];
            color.b += old_color[i].b * texture_factor[i];
        }
        
        // write new color to texture
        output.setcolor(x, y, color); 
    }
}

Conclusion And Acknowledgements

Besides mixing various texturesets together into one large image, there are also other methods like texture-splatting. The method described here has the disadvantage that in order to make it look good, the texture itself has to be incredibly large, or else it will just blur out. One can counter that with a detail texture.

This article has been used by Keith Ditchburn, one of the programmers of Emperor: Battle for Dune, for his T2 Texture Generator (which by now is way more advanced than just generating textures the way it is described in this article).

References

  1. Tobias Alexander Franke, Terrain Texture Generation
  2. Keith Ditchburn, T2 Texture Generator
 2001-04-30
 General TextureSynthesis
 Comments