CodeSnippets Using ogreterrain
From PyWiki
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