Generating Terrain Textures
by (31 May 2001)



Return to The Archives
Introduction


Several months ago I was working on a terrain algorithm. When it was implemented, all of a sudden I realized that I needed some textures. At that moment I rushed into this texture generation stuff.

Here I’ve provided the stuff I’ve found on the net, fertilized with my own ideas.


The Theory


There are different types of terrain. You can call them different types of land if you like. There are for example snow, rocks, grass, mud, etc. Those can be found at various places in nature.

The texture synthesis in this paper is based on combining all types of terrain but weighted with factors according to the traits of the specific place. For each terrain type a probability factor is calculated and then used as a weight while calculating the color of the current pixel.

Calculating the weight

Factors defining the influence of a certain type of terrain at the current point:
Elevation

We have snow on the top of mountains and mud/grass in the lower places (take heed that rocks can be found in all elevations). Thus we need a minimum and a maximum elevation value for every type of land.

Thus for a snow land we will have min/max values 200/255 while for mud land these values may be 5/64.

Slope

Imagine the top of a mountain. It has snow at the top but if you take a close look at really steep piece of land there is hardly any snow but rocks although it happens to be in the snow elevation.

Let us introduce the second factor: slope. It will be specified in degrees.

For a snow land we will have minimum slope 0 and maximum 80 degrees. Thus leaving the range from 80-90 degrees for rocks…

This can be explained with grass/rocks, mud/rocks etc.

Note that we will need the normal vector at that very point to enable angle comparison.
There are some factors that modify already specified factors.
Elevation skew

Imagine that mountaintop again. The sun is shining. The parts that are usually not exposed to the sun are more likely to be snowy. However, as we go up the mountain there is snow no matter how much sun there is. This should result in “skewing” the minimum and maximum elevation depending on the skew factor calculated.

In fact to calculate the real skew factor we will need an angle. The angle of the skewing i.e. where the sun is usually coming from. (Bear in mind that this direction is not necessary the direction of the current light or where the sun currently is)

To summarize:
The skew value is computed from the skew factor and the skew angle (skew azimuth).

Calculate a 2D azimuth vector from the angle: A(cos(skewazimuth), sin(skewazimuth)). Get the 2D vector from the current point normal: B(normal.x,normal.z) Calculate the skew value: (A·B)/sqrt(B·B)

Release of a factor

This “release” value specifies how fast the factor influence fades once it is outside the limits. If the D is the difference between the closer limit border and the height the formula is:

Influence= (Release - D ) / Release;

If D is larger than release, this terrain type is disregarded

For example:
We have min/max elevation 200/255 and Release= 24.
Case 1: height=210. It is in the elevation limits of this type of land so it’s influence factor will be 1.0
Case 2: height h=190. It is obviously outside the min/max elevation parameters and namely it is under the min limit with 10 units. So we get Infuence=(24-10)/24=0.58.

This release thing can be applied to all of the factors and can spawn interesting (and unpredictable) results. The sharper borders you want between the different terrain types the small release should be specified.
Lighting

This is a huge topic, so I’ll stick to a very simple method. Direct lighting. The pixel receives light depending on its normal vector. A simple dot product.

More complicated methods can be exploited here but I think this is slightly off topic with this paper.


Data Structures


In order to enable as many terrain types as we use a dynamic structure. STL offers such structures that can be used with a minor effort.

The terrain type.

Contains the parameters of the terrain type.


struct wj_terrain_type {
	
//color information
	BYTE texID;			//texture ID
	float r,g,b;			//rgb color

	//factors
	float elevMax,elevMin;		//minimum and maximum elevation
	float slopeMax,slopeMin;	//minimum and maximum slope 
	
	//factor modifiers 
	float release;			//elevation release
	float sloperelease;		//slope release

	float skew;			//elevation skew
	float skewAzimuth;		//elevation skew azimuth
};
 


TexID is an identifier of texture in the textures collection. If this value is invalid i.e. no texture is assigned to this terrain type then the r,g,b values are used as color for this terrain type.

Factors were discussed in the Theory section.

A STL list is used to store the terrain types.

The texture type.

This is quite straightforward. The memory pointed by pb is the raw bitmap data loaded from the .bmp file.


struct wj_texture_type {
	char texture[32];		//texture name
	int  width, height;		//dimensions
	BYTE *pb;			//data of the BMP
};
 


A STL vector is used to store textures because the index of the texture in the collection is used as an id (texID) in the terrain type.

The color factor type

The color factor type is used to store all color/weight values that are found to affect the current pixel.


struct color_factor {
	float factor; 			//weight
	float r,g,b;			//color information
};
 


A STL list is used to store all color factor values for a pixel.

Loading data in structures

The terrain type and texture collections are initialized loading a .txt file. Thus one can generate as many textures as one want only specifying different data files.


Algorithm


For each pixel in the generated texture:

Rendering a pixel

  • calculate the height and the normal for that point
  • iterate through all types of terrain in the collection
  • calculate the skew factor and adjust the elevation limits
  • calculate elevation, slope factors and then the final factor for that terrain type
  • store the final factor and the color for that pixel into the color/factor collection
  • add the final factor to a total factor value (for normalization)
  • when all types of terrain are processed for that pixel normalize the factors in the list with the total factor
  • calculate the pixel RGB value for the pixel with the formula RGB=normfactor1*rgb1+ normfactor2*rgb2 … normfactorN*rgbN (where normfactor# = factor#/total factor)
  • adjust the RGB value according to the lighting information calculated.
  • output the calculated RGB to the texture
  • Rendering a pixel - The code

    The TRender function finds a color value with height “h” and normal vector “normal”. If there is a texture use tu, tv as texture coordinates. (It is sometimes more appropriate to use different texture coordinates for different terrain types)

    The g_resource is a global singleton that encapsulates the “terrain type” and texture collections.

    
    DWORD wj_texture_gen::TRender(float h,D3DXVECTOR3 &normal, float tu,float tv)
    {
    	//iterator in the terrain type collection
    	iter_terrain it;
    	
    	//color factor collection and iterator
    	std::list<color_factor> cols;
    	std::list<color_factor>::iterator ci;

    //total factor used for normalization float totalfactor=0.0f;

    //slope of current point (the y value of the normal) float slope=normal.y;

    //the skew denominator float skewDenom=normal.x*normal.x+normal.z*normal.z;

    //are we to do skewing... //if it is a flat terrain there is no need bool doSkew;

    //if skew denominator is "almost" 0 then terrain is flat if (skewDenom >0.00001) { //turn the skewing on and calculate the denominator doSkew=true; skewDenom=1.0f/sqrt(skewDenom); } else doSkew=false;;

    //iterate through terrain type collection for(it=g_resource.terrs.begin(); it!=g_resource.terrs.end();++it) {

    //the new color factor color_factor cf;

    //factor for this material (for clarity) float factor=1.0;

    //first check - elevation float elv_max=it->elevMax; float elv_min=it->elevMin;

    //are we to do skewing ? if (doSkew) { //calculate 2D skew vector float skx=cos(it->skewAzimuth*PI/180); float sky=sin(it->skewAzimuth*PI/180);

    //skew scale value float scale=(normal.x*skx+normal.y*sky)*skewDenom; //adjust elevation limits elv_max+=it->skew*scale; elv_min+=it->skew*scale; } //current elevation release float rel=it->release;

    //if outside limit elevation AND release skip this one if (elv_max+rel<h) continue; if (elv_min-rel>h) continue; if (elv_max<h) { // we are in release compute the factor factor=1-(h-elv_max)/rel; } if (elv_min>h) { // we are in release compute the factor factor=1-(elv_min-h)/rel; }

    //now check the slopes... //slope release float srel=cos(PI/2-(it->sloperelease*PI/180));

    //calculate min and max slope float minslope=cos(it->slopeMin*PI/180); float maxslope=cos(it->slopeMax*PI/180);

    //reverse? if (minslope>maxslope) { float t=maxslope;maxslope=minslope;minslope=t; }

    //this slope is not supported for this type if (slope<minslope-srel) continue; if (slope>maxslope+srel) continue;

    //release? if (slope>maxslope) factor*=1-(slope-maxslope)/srel; if (slope<minslope) factor*=1-(minslope-slope)/srel;

    //this is our terrain type - get the color info if (it->texID==0xff) { cf.r=it->r; cf.g=it->g; cf.b=it->b; } else g_resource.GetTexturePixel(it->texID, tu,tv,cf.r,cf.g,cf.b);

    //and the factor cf.factor=factor;

    //add to the collection cols.push_front(cf);

    //add the factor to the total factor for normalization totalfactor+=factor; }

    //compute direct light float light=-D3DXVec3Dot(&dlight,&normal); if (light<0) light=0;

    //add static (ambient) light light+=slight;

    //rgb values float r,g,b; r=g=b=0;

    //normalizing and acumulating factors for(ci=cols.begin();ci!=cols.end();ci++) { float factor=ci->factor/totalfactor; r+=ci->r*factor; g+=ci->g*factor; b+=ci->b*factor; }

    //light applied r*=light; g*=light; b*=light;

    //return the pixel color return RGB(b*255,g*255,r*255); }


    Problems


    Although the TRender function is working perfectly, using a height map sometimes makes it difficult to compute an accurate normal vector. Or the problem is that an accurate normal vector has been computed from discreet heights on a discreet grid. Troubling with some interpolation will probably lead to better results.

    I’m calculating the normal as a cross- product of two vectors made from the four neighbor points of the current point. A simple linear interpolation is applied to enable non-grid aligned heights.

    Another problem can arise when there are some places that are not covered by any terrain type. This would mean that after all terrain types had been traversed the color/factor collection is empty and we are likely to end with a black pixel in the texture. There is no good solution to this problem as it is a result of lack of information though some tricks can be used. Such as second pass on failure or a second “backup” collection of color/factors.


    Improvements


    Improving the speed.

    I haven’t optimized the code. In fact it is quite chaotically written. After all it is not supposed to work run-time.

    Improving the rendering

    You can create special effects defining a method of blending for each terrain type. Color modulation for example can result in numerous interesting effects.

    Improving realism

    More than one set of terrain types may be used for one landscape to increase the realism. For example different types of grass, grain fields, flowers, mud, loam and many others can be found in the same elevation and slope.

    Other natural phenomenon can be considered. More factors can be introduced such as shadows, humidity, climate, season, human beings, etc. Shadows are major part of a good light engine. However if we already calculated where the shadow is this information can be used in our texture generation. Places that are known to be in the shadow of a hill most of the day usually grow different type of plants than other areas. Concave places get rid of the water more slowly thus are more wet hence terrain type (mud for example) with larger humidity factor will be placed there. Climate and season can be used to switch between terrain type sets.


    The Source Code


    The demo source code, which is available to download here: article_generatingterrtex.zip (384k), is actually used as a texture generator for another demo. It was written in a hurry without any plans or design.

    Classes:

    wj_texture_gen - This is the texture generator. MakeTexture method generates the texture.
    MapInter - interpolates the map grid. Given real coordinates returns a “real” interpolated height.

    Singleton - template class that allows definition of classes that have only one copy and are global though not quite.

    wj_resource - Singleton. Encapsulates “terrain type” and texture collections.
    PureScript - Singleton. Parses text files and loads the data.

    The main function:

    The height map is loaded from a .bmp file only the blue component is used as a height.

    A patch size is specified in the .txt file. The dimensions of the height map are divided to the patch side and appropriate number of textures is generated with the specified resolution.

    For each patch a wj_texture_gen object is instantiated, MakeTexture is called. Then it is written in a file with name that is generated runtime.

    There is texture mirroring which I use in other projects. If you want to remove it remove the line:

    tg.Mirror(px&1,py&1);


    Special Thanks


    I would like to thank Klaus Hartman about his ideas that really made me try texture synthesis myself. (A large set of ideas exposed in the Theory and Improvement sections.)

     

    Copyright 1999-2008 (C) FLIPCODE.COM and/or the original content author(s). All rights reserved.
    Please read our Terms, Conditions, and Privacy information.