Game Development Community

dev|Pro Game Development Curriculum

Torque Terrain Generator.

by Derk Adams · 03/02/2005 (12:09 am) · 15 comments

Download Code File

Background:
The current Torque method for terrain generation is the use of the terrain editor to use algorithms and manual editing to create a terrain file. Given the method of creating static missions, this method of terrain generation is sufficient. However, for those who want a more automated method of terrain generation, there were no tools available. The only remnant is in /terrain/terrData.cc with the console function "makeTestTerrain." The only problem it that it is from the Tribes 2 days and no longer works for the current terrain file structure.

Discussion:
I have reverse engineered the terrain editor to create a console function that will create a new terrain file, generate the height map, enter terrain texture materials, and create the material masks. What is missing is the height and texture scripts which are text strings that the terrain editor uses set up the function list. Since the equations I use to generate the heights and masks are different from the editor, I felt that it wasn't important to reproduce the editor scripts.

Comments:
I am using the fsm algorithm for the height map, but I have provided the code to do the sinus function and the bitmap heightmap since I did them before settling on the fsm. I am using the slope method for the material masks, but have provided the code for the height method since I developed that one first. There is very little error checking in the code, I assume that the correct information will be provided to the function, use at your own risk.

This will work on single player ONLY since terrain files are not sent over the network. If someone can develop a method to transmit the terrain files (the texture materials can be on the client machine), I would be grateful.

My example save the terrain to the file "mission.ter" To use the terrain, you will need to modify your mission (.mis) file to use that file. I have included four textures to use so you can easily see where each texture is being applied. They are simply primary color solids.

Key:
I use the same indication of modifications as a patch file. A line beginning with a "-" is to be removed and a line beginning with a "+" is to be added. Be sure to remove the "+" when you actually put the line into your code. A few lines around the changes are shown to give context to the changes. The triple dot "..." is showing that information has been removed for brevity purposes.

Development Environment:
February 2005
Head 1.2.2
Win32
Single Player

Implementation:

EngineFile: /terrain/terrData.h
Add the folowing at the top of the file.
#ifndef _MRANDOM_H_
#include "math/mRandom.h"
#endif
#ifndef _TERRAFORMER_NOISE_H_
#include "editor/terraformerNoise.h"
#endif
EngineFile: /terrain/terrData.cc
Anywhere in the file add the following.
ConsoleFunction(makeNewTerrain, void, 2, 15, "(string fileName, ...) - makes a new terrain file.")
{
   F32 thisHeightMap[TerrainBlock::BlockSize * TerrainBlock::BlockSize];
   F32 totalHeightMap[TerrainBlock::BlockSize * TerrainBlock::BlockSize];
   F32 noiseMap[TerrainBlock::BlockSize * TerrainBlock::BlockSize];

   S32 baseMaterial = 0;
   S32 lowMaterial = 1;
   S32 highMaterial = 2;
   S32 steepMaterial = 3;

   F32 fbmHeight[2];
   F32 fbmFreq[2];
   F32 fbmCut[2];

   char targetFileName[256];
   dStrcpy(targetFileName,argv[1]);

   char baseMaterialName[256];
   dStrcpy(baseMaterialName,argv[2]);
   char lowMaterialName[256];
   dStrcpy(lowMaterialName,argv[3]);
   char highMaterialName[256];
   dStrcpy(highMaterialName,argv[4]);
   char steepMaterialName[256];
   dStrcpy(steepMaterialName,argv[5]);

   fbmHeight[0] = dAtof(argv[6]);
   fbmFreq[0] = dAtof(argv[7]);
   fbmCut[0] = dAtof(argv[8]);

   fbmHeight[1] = dAtof(argv[9]);
   fbmFreq[1] = dAtof(argv[10]);
   fbmCut[1] = dAtof(argv[11]);

   F32 turbFactor = dAtof(argv[12]);
   F32 turbRadius = dAtof(argv[13]);

   TerrainFile *terr = new TerrainFile;

   // add materials
   terr->mMaterialFileName[baseMaterial] = StringTable->insert(baseMaterialName);
   terr->mMaterialFileName[lowMaterial] = StringTable->insert(lowMaterialName);
   terr->mMaterialFileName[highMaterial] = StringTable->insert(highMaterialName);
   terr->mMaterialFileName[steepMaterial] = StringTable->insert(steepMaterialName);

   // Build final alpha maps
   for (S32 i = 0; i < TerrainBlock::MaterialGroups; i++) {	   
     terr->mMaterialAlphaMap[i] = new U8[TerrainBlock::BlockSize * TerrainBlock::BlockSize];
     dMemset(terr->mMaterialAlphaMap[i], 0, TerrainBlock::BlockSize * TerrainBlock::BlockSize);
     TerrainBlock *buildMaterialMap();
   }

   // Clear material and height maps
   for(U32 k = 0; k < TerrainBlock::BlockSize * TerrainBlock::BlockSize; k++) {
     terr->mHeightMap[k] = 0; // Clear final height map
     totalHeightMap[k] = 0; // Clear working height map
     terr->mBaseMaterialMap[k] = 0; // Clear final material map
     terr->mMaterialAlphaMap[baseMaterial][k] = 0;
     terr->mMaterialAlphaMap[lowMaterial][k] = 0;
     terr->mMaterialAlphaMap[highMaterial][k] = 0;
     terr->mMaterialAlphaMap[steepMaterial][k] = 0;
   }

   // Prep randomness
   MRandom random;
   Noise2D noise;
 
   U32 seed = Platform::getVirtualMilliseconds() * 57;
   seed = (seed<<13) ^ seed;
   seed = (seed * (seed * seed * 15731 + 789221) + 1376312589) & 0x7fffffff;
   random.setSeed(seed);
   noise.setSeed(seed);

   // Fbm Fractal
   for (U32 f=0;f<2;f++) {
     if (fbmHeight[f] > 0) {

       U32 size = (U32)TerrainBlock::BlockSize;
       U32 interval = (U32)fbmFreq[f];
       F32 h = 1.0 - 1.0;
       F32 octaves = 1.0;

       interval = getMin(U32(128), getMax(U32(1), interval));
       F32 H = getMin(1.0f, getMax(0.0f, h));
       octaves = getMin(5.0f, getMax(1.0f, octaves));
       F32 lacunarity = 2.0f;
       F32 exponent_array[32];

       // precompute and store spectral weights      
       // seize required memory for exponent_array
       F32 frequency = 1.0;
       for (U32 i=0; i<=octaves; i++)
       {
         // compute weight for each frequency 
         exponent_array[i] = mPow( frequency, -H );
         frequency *= lacunarity;
       }

       // initialize temporary height matrix 
       for (S32 k=0; k < (size*size); k++)
         thisHeightMap[k] = 0.0f;

       F32 scale = 1.0f / (F32)size * interval;
       for (S32 o=0; o<octaves; o++)
       {
         F32 exp = exponent_array[o];
         for (S32 y=0; y<size; y++)
	 {
           F32 fy = (F32)y * scale; 
           for (S32 x=0; x<size; x++)
	   {
             F32 fx = (F32)x * scale; 
             F32 vnoise = noise.getValue(fx, fy, (S32)interval);
             U32 offset = x + (y << TerrainBlock::BlockShift);
             thisHeightMap[offset] += vnoise * exp; 
	   }
	 }
         scale    *= lacunarity;
         interval  = (U32)(interval * lacunarity);
       }

       F32 fmin = 0;
       F32 fmax = 0;
       for (S32 i=0; i < (TerrainBlock::BlockSize*TerrainBlock::BlockSize); i++)
       {
         // Invert
         thisHeightMap[i] = -thisHeightMap[i];
         // Chop bottom off
         if (thisHeightMap[i] > fbmCut[f]) thisHeightMap[i] = fbmCut[f];
         // Get min and max values
         if (thisHeightMap[i] > fmin) fmin = thisHeightMap[i];
         if (thisHeightMap[i] < fmax) fmax = thisHeightMap[i];
       }
       // Set scale and put in height matrix
       scale = fbmHeight[f]/(fmax-fmin);
       for (S32 i=0; i < (TerrainBlock::BlockSize*TerrainBlock::BlockSize); i++) {
         totalHeightMap[i] += (thisHeightMap[i]-fmin)* scale;
       }
     }
   }

   // Add turbulence to final values
   // early out if there is nothing to do
   if (turbFactor != 0.0f && turbRadius != 0.0f)
   {
     turbFactor *= 20; // just a magic number
     F32 scale = 1.0f/(F32)TerrainBlock::BlockSize;
     for (S32 y=0; y<TerrainBlock::BlockSize; y++)
     {
       F32 fy = (F32)y * scale;
       for (S32 x=0; x<TerrainBlock::BlockSize; x++)
       {
         F32 fx = (F32)x * scale;
         F32 t  = noise.turbulence(fx, fy, 32) * turbFactor;
         F32 dx  = turbRadius * mSin( t );
         F32 dy  = turbRadius * mCos( t );
         U32 offset = x + (y << TerrainBlock::BlockShift);
         U32 orig = ((S32)(x + dx) & (TerrainBlock::BlockSize-1)) +
                    (((S32)(y + dy) & (TerrainBlock::BlockSize-1)) << TerrainBlock::BlockShift);
         terr->mHeightMap[offset] = floatToFixed(totalHeightMap[orig]);
       }
     }
   }

   // New seed to not overlap height noise
   seed = Platform::getVirtualMilliseconds() * 31;
   seed = (seed<<13) ^ seed;
   seed = (seed * (seed * seed * 15731 + 789221) + 1376312589) & 0x7fffffff;
   random.setSeed(seed);
   noise.setSeed(seed);

   // Create noise map
   F32 nmin = 0;
   F32 nmax = 0;
   F32 interval = 64.0f;
   F32 scale = 1.0f / (F32)TerrainBlock::BlockSize * interval;
   for (S32 y=0; y<TerrainBlock::BlockSize; y++)
   {
     F32 fy = (F32)y * scale;
     for (S32 x=0; x<TerrainBlock::BlockSize; x++)
     {
       F32 fx = (F32)x * scale;
       U32 offset = x + (y << TerrainBlock::BlockShift);
       noiseMap[offset] = noise.getValue(fx, fy, (S32)interval);
       if (noiseMap[offset] < nmin) nmin = noiseMap[offset];
       if (noiseMap[offset] > nmax) nmax = noiseMap[offset];
     }
   }

   // Normalise noise map (0.0-1.0f)
   scale = 1.0f / (nmax-nmin);
   for (S32 i=0; i < (TerrainBlock::BlockSize*TerrainBlock::BlockSize); i++)
     noiseMap[i] = (noiseMap[i]-nmin) * scale;

   for (S32 y=0; y<TerrainBlock::BlockSize; y++)
   {
     for (S32 x=0; x<TerrainBlock::BlockSize; x++)
     {
       // for each height look at the immediate surrounding heights and find max slope
       F32 array[9];
       F32 maxDelta = 0;
       // 0 1 2
       // 3 4 5
       // 5 6 8   
       U32 offset = x + (y << TerrainBlock::BlockShift);
       S32 a = 0;
       for (S32 r=-1; r<=1; r++) {
         for (S32 c=-1; c<=1; c++) {
           S32 click = offset + (r * TerrainBlock::BlockSize) + c;
           if (click < 0) click = TerrainBlock::BlockSize*TerrainBlock::BlockSize - click;
           array[a] = terr->mHeightMap[click];
           a++;
         }
       }
       F32 height = array[4];
       for (S32 i=0; i<9; i++)
       {
         F32 delta = mFabs(array[i] - height); 
         if ( (i&1) == 0)
           delta *= 0.70711f;    // compensate for diagonals
         if (delta > maxDelta)
           maxDelta = delta;
       }
       F32 slopeVal = mAtan( maxDelta, 8 ) * (2.0f/M_PI);
        if (slopeVal > 0.94) { // ~22 degree slope
          terr->mMaterialAlphaMap[steepMaterial][offset] = 255;
	} else {
	  if (slopeVal > 0.9) { // ~11 degree slope
	    terr->mMaterialAlphaMap[highMaterial][offset] = 255;
	  } else {
	    terr->mMaterialAlphaMap[lowMaterial][offset] = 255 * noiseMap[offset];;
	  }
        }
        terr->mMaterialAlphaMap[baseMaterial][offset] = 255 - 
                                                        terr->mMaterialAlphaMap[lowMaterial][offset] -
                                                        terr->mMaterialAlphaMap[highMaterial][offset] 

-
                                                        terr->mMaterialAlphaMap[steepMaterial]

[offset];
     }
   }

   // Save
   terr->save(targetFileName);
}
To use the sinus function use the following.
// Generate sinus function
   if (sinHeight > 0) {
     F32 invBlockSize = 1 / F32(TerrainBlock::BlockSize);

     U32 iterations = 31;
     // Clear temp file
     for(U32 k = 0; k < TerrainBlock::BlockSize * TerrainBlock::BlockSize; k++)
       thisHeightMap[k] = 0;

     for(S32 i = 0; i < iterations; i += 2)
     {
       F32 period = M_2PI * (i + 1) * invBlockSize;
       F32 xOffset = random.randF() * M_2PI;
       F32 yOffset = random.randF() * M_2PI;

       F32 interval = i + 2;
       F32 sqInterval = invBlockSize * interval;

       for(S32 y = 0; y < TerrainBlock::BlockSize; y++)
       {
         F32 cosy = mCos(y * period + yOffset);
         for(S32 x = 0; x < TerrainBlock::BlockSize; x++)
         {
           F32 sinx = mSin(x * period + xOffset);
           U32 offset = x + (y << TerrainBlock::BlockShift);
           thisHeightMap[offset] += (sinx + cosy) * noise.getValue(x * sqInterval, y * sqInterval, 

(S32)interval);
         }
       }
     }

     F32 smin = 0;
     F32 smax = 0;
     for (S32 i=0; i < (TerrainBlock::BlockSize*TerrainBlock::BlockSize); i++)
     {
       if (thisHeightMap[i] > smin) smin = thisHeightMap[i];
       if (thisHeightMap[i] < smax) smax = thisHeightMap[i];
     }

     F32 scale = sinHeight/(smax-smin);
     for (S32 i=0; i < (TerrainBlock::BlockSize*TerrainBlock::BlockSize); i++) {
       terr->mHeightMap[i] += floatToFixed((thisHeightMap[i]-smin)* scale);
     }
   }
To use a bitmap for the heightmap use the following.
// Heightmap bitmap
   char *filename = "common/editor/heightscripts/terrain.png";
   GBitmap *bmp = (GBitmap *) ResourceManager->loadInstance(filename);
   if (bmp) {
     for (S32 y=0; y<TerrainBlock::BlockSize; y++)
     {
       for (S32 x=0; x<TerrainBlock::BlockSize; x++)
       {
         U8 *rgb = bmp->getAddress(x,(TerrainBlock::BlockSize-1)-y);
         // compute the luminance of each RGB
         U32 offset = x + (y << TerrainBlock::BlockShift);
         terr->mHeightMap[offset] = (((F32)rgb[0]) * (0.299f/256.0f) +
                                    ((F32)rgb[1]) * (0.587f/256.0f) +
                                    ((F32)rgb[2]) * (0.114f/256.0f)) * 100 * maxHeight;
       }
     }
     delete bmp;
   }
To use height to create material masks use the following.
// Create height material maps
   F32 lowCutoff = 60;
   F32 lowSpread = 10;
   bool distort = true;
   for (S32 k=0; k < (TerrainBlock::BlockSize*TerrainBlock::BlockSize); k++) {
     if (terr->mHeightMap[k] < floatToFixed(lowCutoff)) {
       terr->mMaterialAlphaMap[lowMaterial][k] = 255;
     } else {
       if (terr->mHeightMap[k] < floatToFixed(lowCutoff + lowSpread)) {
	 F32 factor = lowSpread / (fixedToFloat(terr->mHeightMap[k]) - lowCutoff);
	 terr->mMaterialAlphaMap[lowMaterial][k] = 255 * factor;
       } else {
         terr->mMaterialAlphaMap[highMaterial][k] = 255;
       }
       if (distort) {
	 terr->mMaterialAlphaMap[lowMaterial][k] = terr->mMaterialAlphaMap[lowMaterial][k] * noiseMap

[k];
	 terr->mMaterialAlphaMap[highMaterial][k] = terr->mMaterialAlphaMap[highMaterial][k] * 

noiseMap[k];
       }
     }
     terr->mMaterialAlphaMap[baseMaterial][k] = 255 -
                                                terr->mMaterialAlphaMap[lowMaterial][k] -
                                                terr->mMaterialAlphaMap[highMaterial][k] -
                                                terr->mMaterialAlphaMap[steepMaterial][k];
   }
If you want to know the Min and Max of the terrain height map, use the following.
F32 fmin = 0;
   F32 fmax = 0;
   for (S32 i=0; i < (TerrainBlock::BlockSize*TerrainBlock::BlockSize); i++)
   {
     // Get min and max values
     if (terr->mHeightMap[i] < fmin) fmin = terr->mHeightMap[i];
     if (terr->mHeightMap[i] > fmax) fmax = terr->mHeightMap[i];
   }
   Con::printf("Terrain %f %f",fixedToFloat(fmin),fixedToFloat(fmax));
ScriptFile:
I have a button in /client/ui/mainMenuGui.gui that runs the following function.
function makeTerrain()
{
  %targetTerrain = "fps/data/missions/mission.ter";
  %fbm1Height = 250; // 0 for none
  %fbm1Freq = 6; // 1-24
  %fbm1Cut = -0.1; // -1 to 1 negative is into ground
  %fbm2Height = 20; // 0 for none
  %fbm2Freq = 10; // 1-24
  %fbm2Cut = 1; // -1 to 1
  %turbFactor = 0.50; // 0-1
  %turbRadius = 20; // 1-40
  
  %baseMaterial = "fps/data/terrains/primary_base";
  %lowMaterial = "fps/data/terrains/primary_low";
  %highMaterial = "fps/data/terrains/primary_high";
  %steepMaterial = "fps/data/terrains/primary_steep";

  makeNewTerrain(%targetTerrain,%baseMaterial,%lowMaterial,%highMaterial,%steepMaterial,%fbm1Height,%

fbm1Freq,%fbm1Cut,%fbm2Height,%fbm2Freq,%fbm2Cut,%turbFactor,%turbRadius);

}
TextureFiles:
Place the jpg files in "fps/data/terrains"

Credits:
Derk Adams - adamsderk@hotmail.com

#1
02/28/2005 (12:56 pm)
All I can say is WOW!
Now if we can just get it to work in Multiplayer we'll be golden!
#2
03/01/2005 (9:27 am)
Well, last night I came up with a method to use this in multiplayer. I will test the concept when I shift my program from client to server.

Follow me... random numbers aren't random, they are based on a seed. This code uses two random number seeds. Those can be called in the script and added to the variables I have currently listed above. All of these variables can be added to the terrain datablock in the mission file (yes, you will have to generate mission files in script). These variables will be passed to the client which can generate an exact copy of the server terrain file. That way, the heightmaps will line up during the network conversation. So in summary, the server and client will independently generate terrain files using the shared variables, but the resulting files will be identical.

I'll let you know when I implement it.

Thanks.
#3
03/02/2005 (5:20 am)
Wouldn't it be simpler just to expand on the mission loading scenario? When you join a server if you don't have the .mis file it and some other things can be "pushed" by the server to ya.

I don't really see a reason why current terrain couldn't be saved, then pushed to the client, during mission loading.

Anyways just a thought.

Thanx for the useful resource.
#4
03/02/2005 (10:01 am)
CEC,

As networks are rather slow, bandwidth is always a concern. It would not make sense to push a 500,000 byte file when you could send only 500 bytes of instructions and have the client machine recreate the terrain.

Thanks
#5
03/02/2005 (2:04 pm)
@Derek: Using random number seeds to "synchronize" between server and the clients is a good idea and it works. I used the same method in DerangedRaid, although even the initial building placement, weather and spawn points were also based on the single seed value. ;)
#6
03/03/2005 (1:38 am)
"To use a bitmap for the heightmap use the following."

Where should that code be used?
#7
03/03/2005 (8:03 am)
You've got a great point there, Derk. Never really thought of it that way.
#8
03/03/2005 (8:27 am)
Leo,

The extra code is there to help you to expand the resource, it is not plug and play. You will need to understand the given code before you start modifying it.

To answer, it can go before, after, or in place of the fbm fractal.

To explain a little, "thisHeightMap" is used for each height process, then it is added to the "totalHeightMap." Both of these use F32 values. Then total is converted to "terr->mHeightMap" which uses U16 values.

Unless you are doing additional processing, use the heightmap script and put it directly into "terr->mHeightMap" but if you use additional code pieces, be sure to add to "terr->mHeightMap" (+=) instead of directly applying the values (=).

HIH
#9
03/04/2005 (1:17 pm)
@Derk

Great resource need to try this out with I get a chance this weekend.

Dont forget Melv FXresources. He uses a the seed method to send information to the clients over the network to insure that all the foliage appears the same for each client.

John H.
#10
03/27/2006 (2:22 pm)
Awesome, this is exciting! I'm going to try to use this to generate random RTS maps.
#11
08/18/2006 (1:53 pm)
I am having an issue with the terrain texturing. It looks like it almost shows the texture but it is distorted and flickers constantly. I don't have problems with any other textures in the game. I was looking through and I think the offending line is this:

TerrainBlock *buildMaterialMap();

What does that even do? My guess is that the mBaseMaterial[] in the TerrainFile is not getting set at all and I really don't know where it is supposed to get set. Can anyone help with any of these problems. (By the way, this is happening on all the other team members computers as well.)
#12
08/18/2006 (7:55 pm)
Jacob,

are you using multiple textures per gridsquare? You might try using just one texture per square and see if that makes a difference.

also check that the values of mMaterialAlphaMap add up to exactly 255 for each gridSquare.

I didn't see any flickering when I used this resource.
#13
08/18/2006 (8:18 pm)
Jacob,

Geom's response triggered my memory of when that happened to me. It turned out to be the number of textures added to the terrain. There is a preexisting limit of 4. So be creative in your use.

And Geom, there is not a requirement to have the alphas add up to 255. You can have less, or even more, but the results aren't spectacular.

Thanks.
#14
01/06/2010 (9:34 pm)
Derek, are you making any plans to update this for T3D?
#15
01/08/2010 (1:02 am)
Cosmas,

Unfortunately, I am not in a financial position to upgrade to T3D.

Thanks.