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.