Adding a terrain in Ogre 1.7

With the Ogre wiki tutorial #3 : Terrain, Sky, and Fog  we will have a nice looking terrain but it is not ours. So let's change the code a little bit.

First we need to understand what is done.

In Ogre3D 1.7 importing a heightmap and creating a terrain is done with the Terrain component.

The new terrain uses many classes (list available here). The first one we need to setup is the TerrainGlobalOptions (Options class which just stores default options for the terrain and provides a few getters and setters).

mTerrainGlobals = OGRE_NEW Ogre::TerrainGlobalOptions();

You should construct a single instance of this class per application and do so before you start working with any other terrain classes.

Next, we create a TerrainGroup which is a helper class to manage a grid of terrains but it does not do any paging (which is done by the paging component we'll see later on).

mTerrainGroup = OGRE_NEW Ogre::TerrainGroup(mSceneMgr, Ogre::Terrain::ALIGN_X_Z, 513, 12000.0f);
    mTerrainGroup->setFilenameConvention(Ogre::String("BasicTutorial3Terrain"), Ogre::String("dat"));
    mTerrainGroup->setOrigin(Ogre::Vector3::ZERO);

"setFileNameConvention" specifies the convention for saving your terrain blocks to files named <prefix>_<index>.<extension> (ie. BasicTutorial3Terrain_00000000.dat). When saving all the terrains (saveAllTerrains())  every terrain block will be saved with this name in the original heightmap directory. The saving process is a bit long the first time but once the .dat is created the loading is quick because we won't have to save it again.

"setOrigin" defines the center position of the grid of terrain.

Need to customize your terrain ? The tutorial uses a function to store all of its default configuration options :

    configureTerrainDefaults(light);

This function defines some mTerrainGlobals options.

Then we will have to define the terrain blocks before loading them (here we have only one terrain block so the for loop is useless) :

for (long x = 0; x <= 0; ++x)
        for (long y = 0; y <= 0; ++y)
            defineTerrain(x, y);
 
    // sync load since we want everything in place when we start
    mTerrainGroup->loadAllTerrains(true);

Warning!!! The defineTerrain(x,y) function here isn't part of a Ogre3D class, it's a BasicTutorial3 method:

void BasicTutorial3::defineTerrain(long x, long y)
{
    Ogre::String filename = mTerrainGroup->generateFilename(x, y);
    if (Ogre::ResourceGroupManager::getSingleton().resourceExists(mTerrainGroup->getResourceGroup(), filename))
    {
        mTerrainGroup->defineTerrain(x, y);
    }
    else
    {
        Ogre::Image img;
        getTerrainImage(x % 2 != 0, y % 2 != 0, img);
        mTerrainGroup->defineTerrain(x, y, &img);
        mTerrainsImported = true;
    }
 
}

This function only checks if the terrain has been generated by asking the ResourceGroupManager.
If it's here, it calls the TerrainGroup method defineTerrain with only x & y for arguments.
If it's not here it calls this method with 3 arguments adding the &img which will be used to generate the terrain.

Once the terrain is defined, we call the loadAllTerrains function and we're almost done.

We need to manage the texture now.
Remember the mTerrainGlobals, this variable stores global information about our terrains but it also stores the blendmaps textures (a blendmap is a texture with 2 colors helping us to know where a texture should be : red where the texture 1shoud be and black where it shouldn't be).

// Configure default import settings for if we use imported image
    Ogre::Terrain::ImportData& defaultimp = mTerrainGroup->getDefaultImportSettings();
    defaultimp.terrainSize = 513;
    defaultimp.worldSize = 12000.0f;
    defaultimp.inputScale = 600;
    defaultimp.minBatchSize = 33;
    defaultimp.maxBatchSize = 65;
// textures
    defaultimp.layerList.resize(3);
    defaultimp.layerList[0].worldSize = 100;
    defaultimp.layerList[0].textureNames.push_back("dirt_grayrocky_diffusespecular.dds");
    defaultimp.layerList[0].textureNames.push_back("dirt_grayrocky_normalheight.dds");
    defaultimp.layerList[1].worldSize = 30;
    defaultimp.layerList[1].textureNames.push_back("grass_green-01_diffusespecular.dds");
    defaultimp.layerList[1].textureNames.push_back("grass_green-01_normalheight.dds");
    defaultimp.layerList[2].worldSize = 200;
    defaultimp.layerList[2].textureNames.push_back("growth_weirdfungus-03_diffusespecular.dds");
    defaultimp.layerList[2].textureNames.push_back("growth_weirdfungus-03_normalheight.dds");

Here you can change your textures.
If you want to know how to generate the texture.dds textures read this article: Ogre Terrain Textures.

You can find many sample textures on the cgtextures website. And I suggest that you use Crazybump to generate the normal, opacity, specular, diffuse and displacement maps.

Once you defined your textures you need to use, you can use the blendmaps we created in Worldmachine dune generation article, which is done in the initBlendMaps tutorial function.

Now the customization begins

What do you need :

  • A heightmap generated with worldmachine
  • Blendmaps generated with worldmachine
  • Seamless textures found on Cgtextures

 

Override the getTerrainImage to upload your heightmap :

void getTerrainImage(bool flipX, bool flipY, Ogre::Image& img)
{
    img.load("desert_heightmap.png", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
    if (flipX)
        img.flipAroundY();
    if (flipY)
        img.flipAroundX();
 
}

Override the definTerrain function to specify the ressource group:

void OutDoorScene::defineTerrain(long x, long y)
{
    Ogre::String filename = mTerrainGroup->generateFilename(x, y);
    if (Ogre::ResourceGroupManager::getSingleton().resourceExists(
        Ogre::ResourceGroupManager::getSingleton().findGroupContainingResource("desert_heightmap.png"), filename))
    {
        mTerrainGroup->defineTerrain(x, y);
    }
    else
    {
        Ogre::Image img;
        //Ogre::String ImageArray[3][3]={{"NE","EE","SE"},{"NN","MM","SS"},{"NW","WW","SW"}};
        //img.load("heightmap"+ImageArray[x][y]+".png", Ogre::ResourceGroupManager::AUTODETECT_RESOURCE_GROUP_NAME);
        //img.load("heightmapSS1.png", Ogre::ResourceGroupManager::AUTODETECT_RESOURCE_GROUP_NAME);
        //img.flipAroundY();
        //img.flipAroundX();
        getTerrainImage(x % 2 != 0, y % 2 != 0, img);
        mTerrainGroup->defineTerrain(x, y, &img);
        mTerrainsImported = true;
    }
}

Override the initBlendMaps function to specify your blendmaps :

void OutDoorScene::initBlendMaps(Ogre::Terrain* terrain)
{
    int bmSize = terrain->getLayerBlendMapSize();
 
    Ogre::String blendImages[2];  //filenames of the blendmaps(one per layer except for the first one)
    blendImages[0] =  "desert_blendmap_slope.png";
    blendImages[1] =  "desert_blendmap_angle.png";
 
    float opacity[2];  //opacity wanted for the layers(one per layer except for the first one)
    opacity[0] = 1;
    opacity[1] = 1;
 
    for(int bi=1;bi < 3;bi++) {
        Ogre::Image img;
        img.load(blendImages[bi-1],"Maps");
 
        Ogre::TerrainLayerBlendMap* blendMap1 = terrain->getLayerBlendMap(bi);
 
        float* pBlend1 = blendMap1->getBlendPointer(); // pointer towards one value in the blendmap
        Ogre::uint16 xBegin;
        Ogre::uint16 xEnd;
        Ogre::uint16 xStep;
        Ogre::uint16 yBegin;
        Ogre::uint16 yEnd;
        Ogre::uint16 yStep;
        yBegin = 0;
        yEnd = terrain->getLayerBlendMapSize();
        yStep = 1;
        xBegin = 0;
        xEnd = terrain->getLayerBlendMapSize();
        xStep = 1;
        for (Ogre::uint16 y = yBegin; y != yEnd; y+=yStep)
        {
            for (Ogre::uint16 x = xBegin; x != xEnd; x+=xStep)
            {
                //matching sizes between load blendmap image and blendmap matrix values
                Ogre::Real tx=(float(x)/float(bmSize));
                Ogre::Real ty=(float(y)/float(bmSize));
                int ix = int(img.getWidth()*tx);
                int iy = int(img.getWidth()*ty);
                //reading blendmap image color and saving it in the blendmap matrix values
                Ogre::ColourValue cv = Ogre::ColourValue::Black;
                cv = img.getColourAt(ix,iy,0);
                Ogre::Real val = cv[0]*opacity[bi-1]; //only the red color canal is used
                *pBlend1++ = val;
            }
        }
 
        blendMap1->dirty();  //tells ogre the blendmaps have been changed
        blendMap1->update();  //tells the GPU to recalculate
    }
}

Override the configureTerrainDefaults function to specify your textures :

void OutDoorScene::configureTerrainDefaults(Ogre::Light* light)
{
    // Configure global
    mTerrainGlobals->setMaxPixelError(8);
    // testing composite map
    mTerrainGlobals->setCompositeMapDistance(3000);
 
    // Important to set these so that the terrain knows what to use for derived (non-realtime) data
    mTerrainGlobals->setLightMapDirection(light->getDerivedDirection());
    mTerrainGlobals->setCompositeMapAmbient(mSceneMgr->getAmbientLight());
    mTerrainGlobals->setCompositeMapDiffuse(light->getDiffuseColour());
 
    // Configure default import settings for if we use imported image
    Ogre::Terrain::ImportData& defaultimp = mTerrainGroup->getDefaultImportSettings();
    defaultimp.terrainSize = 513;
    defaultimp.worldSize = 2000.0f;
    defaultimp.inputScale = 850.0f;
    defaultimp.minBatchSize = 33;
    defaultimp.maxBatchSize = 65;
    // textures
    defaultimp.layerList.resize(3);
    defaultimp.layerList[0].worldSize = 30;  // layer "splat" size
    defaultimp.layerList[0].textureNames.push_back("sand_005_diffusespecular.dds");
    defaultimp.layerList[0].textureNames.push_back("sand_005_normalheight.dds");
    defaultimp.layerList[1].worldSize = 100;
    defaultimp.layerList[1].textureNames.push_back("Cliffs0143_9_S_diffusespecular.dds");
    defaultimp.layerList[1].textureNames.push_back("Cliffs0143_9_S_normalheight.dds");
    defaultimp.layerList[2].worldSize = 100;
    defaultimp.layerList[2].textureNames.push_back("SoilBeach0087_1_S_diffusespecular.dds");
    defaultimp.layerList[2].textureNames.push_back("SoilBeach0087_1_S_normalheight.dds");
 
 
    /*
    // Code to merge png as alpha channel
    Ogre::Image combined;
 
    combined.loadTwoImagesAsRGBA("texture_SS_sable_COLOR.png", "texture_SS_sable_SPEC.png",
      Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, Ogre::PF_BYTE_RGBA);
    combined.save("texture_SS_sable_diffusespecular.png");
 
    combined.loadTwoImagesAsRGBA("texture_SS_sable_NRM.png", "texture_SS_sable_DISP.png",
      Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, Ogre::PF_BYTE_RGBA);
    combined.save("texture_SS_sable_normalheight.png");
    */
}

Don't forget to specify the right ressources path and to delete the .dat each time you modify the terrain. And that's it.

Now you can see some rocks on the high slope parts of the terrain and some micro sand dunes \o/.