Terrain from a heightmap

In this tutorial we will demonstrate the usage of GB.Scene.TerrainSceneNode for rendering terrain. We will be using the FPS camera from the Cameras tutorial. The following topics are covered:

  1. Creating a project
  2. GB.Scene.TerrainSceneNode
  3. Positioning objects on the terrain

The tutorial's code files and media files are installed along with Ginjo-Builder and can be accessed from the 'Project' window that is shown when the editor starts. This tutorial is called '06_Terrain' in the 'Tutorials' tab.

1. Creating a project

First we create a project and compiler options XML file. (See Render loop for a more detailed description of creating a project.)

  1. Create a new project through the Project menu.
  2. Right click on project's top level tree node named "New GBP(Unsaved)" and select Rename folder. Rename the folder to "Terrain".
  3. Save the project as "TerrainProject.gbp" from the Project menu.
  4. Add a new file to the project tree and save it as "renderTerrain.gbc"
  5. Add a new file to the project tree and save it as "Terrain.xml". (The XML file must have the same name as the top level tree node.)

Open "Terrain.xml" and just like in Render loop add the <exe> and <startup> elements. We will also import the GB namespace so we don't have to type it out every time. (For the full list of XML tags see Compiler options.)

<?xml version="1.0" encoding="utf-8"?>
<root>
   <exe path=".\game.exe" />
   <startup name="renderTerrain.main" />
   
   <ImportSymbols>
      <namespace name="GB"  />
   </ImportSymbols>
</root>

2. GB.Scene.TerrainSceneNode

The TerrainSceneNode uses the Geo Mipmap algorithm to produce anti-aliased textured terrains. Mipmapping builds scaled versions of textures and uses different textures when rendering portions of the scene where low texture detail is needed. Mipmapping can save memory and rendering time, but its primary purpose is to increase the quality of the scene by reducing aliasing. The terrain sections (patches) are assembled based on the camera's viewpoint given the level of detail required.

The TerrainSceneNode uses a CLOD (Continuous Level of Detail) algorithm which updates each terrain patch based on a LOD (Level of Detail). LOD is determined based on a terrain patch's distance from the camera. The Patch Size of the terrain must always be a size of 2n+1 ( i.e. 8+1(=9), 16+1(=17)).

Behind the scenes TerrainSceneNode renders from an Index Buffer. The MaxLOD available is directly dependent on the patch size of the terrain. LOD 0 contains all of the indeces to draw all the triangles at the maximum detail for a patch. As the LOD goes up by 1, the number of steps taken when generating indices increases by 2LOD. The steps can be no larger than the size of the patch: having a LOD of 8, with a patch size of 17, is asking the algoritm to generate indices every 28 (256) vertices, which is not possible with a patch size of 17. The maximum LOD for a patch size of 17 is 4 (i.e. solving for n in 17=2n+1). Thus, there are 5 LOD levels (MaxLOD=5): LOD 0 (full detail), LOD 1 (every 2 vertices), LOD 2 (every 4 vertices), LOD 3 (every 8 vertices) and LOD 4 (every 16 vertices).

In our XML file we specified the startup function as "renderTerrain.main". Double-click on "renderTerrain.gbc" in the project tree to open it, and add the following "main" function which initializes the Irrlicht engine, adds an FPS camera and hides the mouse cursor. To quit the program press ALT+F4.

function main(var:string cmdArgs[]) returns Int32
{
   var:gb.IrrlichtCreationParameters options=new gb.IrrlichtCreationParameters()
   //Without anti-aliasing our model edges would be jagged 
   options.AntiAliasing=255
   //Tell Irrlicht to use OpenGL for graphics
   options.DriverType=gb.video.DriverType.OpenGL
   //Turn off logging, we won't be using it for now
   options.LoggingLevel=gb.LogLevel.None
   
   var:gb.IrrlichtDevice engine = gb.IrrlichtDevice.CreateDevice(options)
   //Also set our window's title
   engine.SetWindowCaption('Render Terrain')
   
   var:video.VideoDriver driver=engine.VideoDriver
   var:Scene.SceneManager smgr=engine.SceneManager

   //An FPS camera is controlled by the mouse and keys just like one finds in normal FPS games.
   //Press Alt+F4 to quit
   var:gb.scene.CameraSceneNode camera= smgr.AddCameraSceneNodeFPS(null, 100, 1.2)
   //the usual Field of View angle for FPS is 90 degrees
   camera.FOV=1.57079633   //FOV is in radians
   camera.Position = new gb.core.Vector3Df(2700 * 2, 255 * 2, 2600 * 2);
   camera.Target = new gb.core.Vector3Df(2397 * 2, 343 * 2, 2700 * 2);
   camera.FarValue = 42000
   engine.CursorControl.Visible = false

}

Note the camera's Position, Target and FarValue. Now we will add the terrain scene node to our "main" function after our camera code. The terrain is rendered based on a heightmap. The heightmap we are using in this tutorial is a 256-by-256 grey scale bitmap scaled 40 times its original size. Note that the heightmap has to be square (width=height). The call to AddTerrainSceneNode sets the scale of the terrain, the LOD, the patch size and a smoothing factor of 4. We also set the Wireframe material flag to true so that we can see the terrain (we haven't specified a texture for the terrain yet.)

var:scene.TerrainSceneNode TerrainNode=smgr.AddTerrainSceneNode('..\media\terrain-heightmap.bmp',null,-1,new core.Vector3Df(),new core.Vector3Df(),new core.Vector3Df(40, 4.4, 40),new video.Color(255, 255, 255),5, scene.TerrainPatchSize._17,4)
TerrainNode.SetMaterialFlag(video.MaterialFlag.Lighting, false)
TerrainNode.SetMaterialFlag(video.MaterialFlag.Wireframe, true)

Note that you can use the IntelliDoc Symbol Info feature to look-up symbol types and definitions. The 'lightbulb' button on the toolbar looks-up the symbol type for the current cursor location and displays it in a tooltip. Pressing F1 also shows the tooltip.

We finish the "main" function with the render loop:

var:video.Color background=new video.Color(160, 160, 160)
while (engine.Run()){
    driver.BeginScene(video.ClearBufferFlag.All, background)
    smgr.DrawAll()
    driver.EndScene()
}

When you run the program you should see a wire frame terrain.

Next we are going to apply a seamless texture to the terrain. Comment out the wire frame material flag and modify your code as shown below to add a video.MaterialType.Solid texture.

var:scene.TerrainSceneNode TerrainNode=smgr.AddTerrainSceneNode('..\media\terrain-heightmap.bmp',null,-1,new core.Vector3Df(),new core.Vector3Df(),new core.Vector3Df(40, 4.4, 40),new video.Color(255, 255, 255),5, scene.TerrainPatchSize._17,4)
TerrainNode.SetMaterialFlag(video.MaterialFlag.Lighting, false)
//TerrainNode.SetMaterialFlag(video.MaterialFlag.Wireframe, true)
TerrainNode.SetMaterialTexture(0, driver.GetTexture('..\media\seamless-rock-face.jpg'))
TerrainNode.SetMaterialType(video.MaterialType.Solid)
TerrainNode.ScaleTexture(10)

When you run the program you should see a rock face texture applied to the terrain.

Next we are going to replace the single texture with a video.MaterialType.DetailMap. A detail mapped material uses two textures: one as a diffuse color map while the second is used for showing close up detail. Modify the texture code as follows (results shown below):

var:scene.TerrainSceneNode TerrainNode=smgr.AddTerrainSceneNode('..\media\terrain-heightmap.bmp',null,-1,new core.Vector3Df(),new core.Vector3Df(),new core.Vector3Df(40, 4.4, 40),new video.Color(255, 255, 255),5, scene.TerrainPatchSize._17,4)
TerrainNode.SetMaterialFlag(video.MaterialFlag.Lighting, false)
//TerrainNode.SetMaterialFlag(video.MaterialFlag.Wireframe, true)
TerrainNode.SetMaterialTexture(0, driver.GetTexture('..\media\terrain-texture.jpg'))
TerrainNode.SetMaterialTexture(1, driver.GetTexture('..\media\seamless-desert-sand.jpg'))
TerrainNode.SetMaterialType(video.MaterialType.DetailMap)
TerrainNode.ScaleTexture(1,50)

3. Positioning objects on the terrain

In order to place object/models on the terrain or relative to the terrain we need to get the terrain's height using GB.Scene.TerrainSceneNode.GetHeight(var:single X, var:single Z) The function returns the height (Y coordinate) for the specified point. If the point is not on the terrain, the returned value will be -3.402823466E+38F, which is the equivalent of C++ "-FLT_MAX" ("minus max float value").