CodeSnippets Using ogreterrain

From PyWiki

Jump to: navigation, search

Contents

Installation

You can install the ogreterrain module as you would any module via the instructions however if you encounter any errors from the installation this patch [1] may resolve them.


Simple Terrain

Dealing with the camera

# setting up the camera.
camera = self.camera
camera.setPosition(1683, 100, 2116)
camera.lookAt(1963, 50, 1660)
camera.setNearClipDistance(0.1)
camera.setFarClipDistance(50000)
 
if (self.root.getRenderSystem().getCapabilities().hasCapability(ogre.RSC_INFINITE_FAR_PLANE)):
	camera.setFarClipDistance(0)
 
self.materialManager = ogre.MaterialManager.getSingleton()
self.materialManager.setDefaultTextureFiltering(ogre.TFO_ANISOTROPIC)
self.materialManager.setDefaultAnisotropy(7)

What it does, besides setting the position and orientation of the camera, is adjust the near and far clip distances. A terrain is often fairly big, and we want our camera to be able to see far into the distance. If the RenderSystem supports it, make it infinite. We also set some defaults on the material manager.

Setting up directional and ambient light

The Terrain component uses a directional light to compute the terrain lightmap, so let's put a directional light into our scene:

lightdir = ogre.Vector3(0.55, -0.3, 0.75)
lightdir.normalise()
 
light = self.sceneManager.createLight("tstLight")
light.setType(ogre.Light.LT_DIRECTIONAL)
light.setDirection(lightdir)
light.setDiffuseColour(ogre.ColourValue(1.0, 1.0, 1.0))
light.setSpecularColour(ogre.ColourValue(0.4, 0.4, 0.4))
 
sceneManager.AmbientLight = 0.2, 0.2, 0.2

We also set some ambient light to smooth out the lighting.

Setting up the terrain

First we need to define an instance of TerrainGlobalOptions(), we will need this later on:

self.terrainGlobals = ogreterrain.TerrainGlobalOptions()

Then we construct our TerrainGroup object, which is managing our Terrain instances:

self.terrainGroup = ogreterrain.TerrainGroup(self.sceneManager, ogreterrain.Terrain.ALIGN_X_Z, 513, 12000)
self.terrainGroup.setFilenameConvention("BasicTutorial3Terrain", "dat")
self.terrainGroup.setOrigin(ogre.Vector3(0, 0, 0))

The TerrainGroup class constructor takes our SceneManager instance, terrain alignment option, terrain size and terrain world size as parameters. Then we tell the TerrainGroup what name we would like it to use when saving our terrain, using the setFilenameConvention function. And lastly we set the origin of the terrain group.

Now it's time to configure our terrain:

self.configureTerrainDefaults(light)

We will define and explain this function later in the tutorial. Notice that we're passing our directional light to the function.

Next we define our terrain and instruct the TerrainGroup to load it:

self.defineTerrain(0, 0)
 
self.terrainGroup.loadAllTerrains(True)

Since we only have one terrain, we'll only be calling the defineTerrain function once. If we had multiple, this is where we would load each of them.

Now we loop through each of our imported terrains and generate the blend maps for them:

if (self.terrainsImported):
	it = self.terrainGroup.getTerrainIterator()
	for t in self.terrainGroup.getTerrainIterator():
		self.initBlendMaps(t.instance)

And finally we cleanup the creation of the terrains by freeing resources used in the process:

self.terrainGroup.freeTemporaryResources()

configureTerrainDefaults

Now define a configureTerrainDefaults function with a light parameter and place the following inside it:

self.terrainGlobals.setMaxPixelError(8)
self.terrainGlobals.setCompositeMapDistance(3000)

MaxPixelError decides how precise our terrain is going to be. A lower number will mean a more accurate terrain, at the cost of performance (because of more vertices). CompositeMapDistance decides how far the Ogre terrain will render the lightmapped terrain.

Next, let's deal with the lightmapping, using our directional light:

self.terrainGlobals.setLightMapDirection(light.getDerivedDirection())
self.terrainGlobals.setCompositeMapAmbient(self.sceneManager.getAmbientLight())
self.terrainGlobals.setCompositeMapDiffuse(light.getDiffuseColour())

It uses our light to set direction and diffuse colour and sets the diffuse colour to match our scene manager's ambient light.

Now we configure the import settings:

# Configure default import settings for if we use imported image
defaultimp = self.terrainGroup.getDefaultImportSettings()
defaultimp.terrainSize = 513
defaultimp.worldSize = 12000
defaultimp.inputScale = 600
defaultimp.minBatchSize = 33
defaultimp.maxBatchSize = 65

We won't cover the what and how of those values in this tutorial, but terrainSize and worldSize are set to match our global sizes (what we told our TerrainGroup), and inputScale decides how the heightmap image is scaled up. We are using a scale here because images have limited precision. A raw heightmap, for instance, doesn't normally need scaling because the values are stored as an array of unscaled floats.

The last bit is our textures:

# textures
layer0 = ogreterrain.Terrain.LayerInstance()
layer0.worldSize = 100
layer0.textureNames.append("dirt_grayrocky_diffusespecular.dds")
layer0.textureNames.append("dirt_grayrocky_normalheight.dds")
defaultimp.layerList.append(layer0)
 
layer1 = ogreterrain.Terrain.LayerInstance()
layer1.worldSize = 30
layer1.textureNames.append("grass_green-01_diffusespecular.dds")
layer1.textureNames.append("grass_green-01_normalheight.dds")
defaultimp.layerList.append(layer1)
 
layer2 = ogreterrain.Terrain.LayerInstance()
layer2.worldSize = 200
layer2.textureNames.append("growth_weirdfungus-03_diffusespecular.dds")
layer2.textureNames.append("growth_weirdfungus-03_normalheight.dds")
defaultimp.layerList.append(layer2)

We initialise each layer by setting the 'worldSize' and by specifying the texture names. 'worldSize' decides how big each splat of textures is going to be. A smaller value will increase the resolution of the rendered texture layer. The default material generator takes two textures per layer:

  • diffuse_specular - diffuse texture with a specular map in the alpha channel
  • normal_height - normal map with a height map in the alpha channel

defineTerrain

This is our defineTerrain function:

def defineTerrain(self, x, y):
	filename = self.terrainGroup.generateFilename(x, y)
		RGM = ogre.ResourceGroupManager.getSingleton()
		if ( RGM.resourceExists(self.terrainGroup.getResourceGroup(), filename) ):
			self.terrainGroup.defineTerrain(x, y)
		else :
			img = self.getTerrainImage((x % 2) != 0, (y%2) != 0)
			self.terrainGroup.defineTerrain(x, y, img)
			self.terrainsImported = True

This function is simple, but clever: First, it asks our TerrainGroup what file name it would use to generate the terrain. Then if checks if there is a file by that name in our resource group. If there is, it means that we generated a binary terrain data file already, and thus there is no need to import it from an image. If there isn't a data file present, it means we have to generate our terrain, and we load the image and use that to define it.

The function uses a small utility function called getTerrainImage defined below:

getTerrainImage

This function loads 'terrain.png' from our resource locations, and flips it if necessary.

def getTerrainImage(self, flipX, flipY):
	img = ogre.Image()
	img.load("terrain.png", ogre.ResourceGroupManager.DEFAULT_RESOURCE_GROUP_NAME)
	if flipX:
		img.flipAroundY()
	if flipY:
		img.flipAroundX() 
	return img

initBlendMaps

This is the initBlendMaps function in its entirety:

def initBlendMaps(self, terrain):
	blendMap0 = terrain.getLayerBlendMap(1)
	blendMap1 = terrain.getLayerBlendMap(2)
	minHeight0 = 70
	fadeDist0 = 40.0
	minHeight1 = 70
	fadeDist1 = 15.0
 
	pBlend1 = blendMap1.getBlendPointer()    # returns the address of the buffer
	size = terrain.getLayerBlendMapSize() * terrain.getLayerBlendMapSize()
	blend_data=(ctypes.c_float * size).from_address(pBlend1)
	index = 0
	for y in range(terrain.getLayerBlendMapSize()):
		for x in range( terrain.getLayerBlendMapSize() ):
			# using ctypes
			tx = ctypes.c_float(0.0)
			ty = ctypes.c_float(0.0)
 
			blendMap0.convertImageToTerrainSpace(x, y, ctypes.addressof(tx), ctypes.addressof(ty))
			height = terrain.getHeightAtTerrainPosition(tx.value, ty.value)
			val = (height - minHeight0) / fadeDist0
			val = Clamp(val, 0, 1)
 
			val = (height - minHeight1) / fadeDist1
			val = Clamp(val, 0, 1)
			blend_data [index] = val
			index += 1
 
	blendMap0.dirty()
	blendMap1.dirty()
	blendMap0.update()
	blendMap1.update()

We won't go into the gritty details of how it works in this tutorial. Let's just say that it uses the terrain height to splat the three layers on the terrain. Notice the use of getLayerBlendMap and getBlendPointer. 'Nuff said.

Compile and Run

You can save this in the python-ogre folder under demos/ogre so it can run using the same plugins.cfg/resources.cfg as the other python-ogre demos, however you do need to add the following two resources to resources.cfg:

FileSystem=../media/materials/textures/nvidia
FileSystem=../media/PCZAppMedia

Here is the source code in its entirety:

import sys
sys.path.insert(0,'..')
import PythonOgreConfig
 
import ogre.renderer.OGRE as ogre
import ogre.renderer.ogreterrain as ogreterrain
import SampleFramework as sf
import ctypes as ctypes
 
def Clamp ( val, low, high ):
    	if val < low: return low
    	if val > high: return high
    	return val
 
class TerrainApplication(sf.Application):
 
	def _chooseSceneManager(self):
		# self.sceneManager = self.root.createSceneManager("TerrainSceneManager")
		self.sceneManager = self.root.createSceneManager(ogre.ST_GENERIC)
 
	def _createScene(self):
		sceneManager = self.sceneManager
 
		# setting up the camera.
		camera = self.camera
		camera.setPosition(1683, 100, 2116)
		camera.lookAt(1963, 50, 1660)
		camera.setNearClipDistance(0.1)
		camera.setFarClipDistance(50000)
 
		if (self.root.getRenderSystem().getCapabilities().hasCapability(ogre.RSC_INFINITE_FAR_PLANE)):
			camera.setFarClipDistance(0)
 
		self.materialManager = ogre.MaterialManager.getSingleton()
		self.materialManager.setDefaultTextureFiltering(ogre.TFO_ANISOTROPIC)
		self.materialManager.setDefaultAnisotropy(7)
 
		lightdir = ogre.Vector3(0.55, -0.3, 0.75)
		lightdir.normalise()
 
		light = self.sceneManager.createLight("tstLight")
		light.setType(ogre.Light.LT_DIRECTIONAL)
		light.setDirection(lightdir)
		light.setDiffuseColour(ogre.ColourValue(1.0, 1.0, 1.0))
		light.setSpecularColour(ogre.ColourValue(0.4, 0.4, 0.4))
 
		sceneManager.AmbientLight = 0.2, 0.2, 0.2
 
		self.terrainGlobals = ogreterrain.TerrainGlobalOptions()
		self.terrainGroup = ogreterrain.TerrainGroup(self.sceneManager, ogreterrain.Terrain.ALIGN_X_Z, 513, 12000)
		self.terrainGroup.setFilenameConvention("BasicTutorial3Terrain", "dat")
		self.terrainGroup.setOrigin(ogre.Vector3(0, 0, 0))
 
		self.configureTerrainDefaults(light)
 
		self.defineTerrain(0, 0)
 
		self.terrainGroup.loadAllTerrains(True)
 
		if (self.terrainsImported):
			it = self.terrainGroup.getTerrainIterator()
			for t in self.terrainGroup.getTerrainIterator():
				self.initBlendMaps(t.instance)
 
		self.terrainGroup.freeTemporaryResources()
 
	def configureTerrainDefaults(self, light):
		self.terrainGlobals.setMaxPixelError(8)
		self.terrainGlobals.setCompositeMapDistance(3000)
		self.terrainGlobals.setLightMapDirection(light.getDerivedDirection())
		self.terrainGlobals.setCompositeMapAmbient(self.sceneManager.getAmbientLight())
		self.terrainGlobals.setCompositeMapDiffuse(light.getDiffuseColour())
 
		# Configure default import settings for if we use imported image
		defaultimp = self.terrainGroup.getDefaultImportSettings()
		defaultimp.terrainSize = 513
		defaultimp.worldSize = 12000
		defaultimp.inputScale = 600
		defaultimp.minBatchSize = 33
		defaultimp.maxBatchSize = 65
 
		# textures
		layer0 = ogreterrain.Terrain.LayerInstance()
		layer0.worldSize = 100
		layer0.textureNames.append("dirt_grayrocky_diffusespecular.dds")
		layer0.textureNames.append("dirt_grayrocky_normalheight.dds")
		defaultimp.layerList.append(layer0)
 
		layer1 = ogreterrain.Terrain.LayerInstance()
		layer1.worldSize = 30
		layer1.textureNames.append("grass_green-01_diffusespecular.dds")
		layer1.textureNames.append("grass_green-01_normalheight.dds")
		defaultimp.layerList.append(layer1)
 
		layer2 = ogreterrain.Terrain.LayerInstance()
		layer2.worldSize = 200
		layer2.textureNames.append("growth_weirdfungus-03_diffusespecular.dds")
		layer2.textureNames.append("growth_weirdfungus-03_normalheight.dds")
		defaultimp.layerList.append(layer2)
 
	def defineTerrain(self, x, y):
		filename = self.terrainGroup.generateFilename(x, y)
		RGM = ogre.ResourceGroupManager.getSingleton()
		if ( RGM.resourceExists(self.terrainGroup.getResourceGroup(), filename) ):
			self.terrainGroup.defineTerrain(x, y)
		else :
			img = self.getTerrainImage((x % 2) != 0, (y%2) != 0)
			self.terrainGroup.defineTerrain(x, y, img)
			self.terrainsImported = True
 
	def getTerrainImage(self, flipX, flipY):
		img = ogre.Image()
		img.load("terrain.png", ogre.ResourceGroupManager.DEFAULT_RESOURCE_GROUP_NAME)
		if flipX:
			img.flipAroundY()
		if flipY:
			img.flipAroundX() 
		return img
 
	def initBlendMaps(self, terrain):
		blendMap0 = terrain.getLayerBlendMap(1)
		blendMap1 = terrain.getLayerBlendMap(2)
		minHeight0 = 70
		fadeDist0 = 40.0
		minHeight1 = 70
		fadeDist1 = 15.0
 
		pBlend1 = blendMap1.getBlendPointer()    # returns the address of the buffer
		size = terrain.getLayerBlendMapSize() * terrain.getLayerBlendMapSize()
		blend_data=(ctypes.c_float * size).from_address(pBlend1)
		index = 0
		for y in range(terrain.getLayerBlendMapSize()):
			for x in range( terrain.getLayerBlendMapSize() ):
				# using ctypes
				tx = ctypes.c_float(0.0)
				ty = ctypes.c_float(0.0)
 
				blendMap0.convertImageToTerrainSpace(x, y, ctypes.addressof(tx), ctypes.addressof(ty))
				height = terrain.getHeightAtTerrainPosition(tx.value, ty.value)
				val = (height - minHeight0) / fadeDist0
				val = Clamp(val, 0, 1)
 
				val = (height - minHeight1) / fadeDist1
				val = Clamp(val, 0, 1)
				blend_data [index] = val
				index += 1
 
		blendMap0.dirty()
		blendMap1.dirty()
		blendMap0.update()
		blendMap1.update()        
 
if __name__ == '__main__':
	try:
		application = TerrainApplication()
		application.go()
	except ogre.OgreException, e:
		print e
Personal tools