Projective Decals        

Introduction

This is a quick implementation of Intermediate Tutorial 6. The class TerrainDecal represents a texture that is projected orthogonaly on top of the terrain from a given point and with a given size. This class will detect the terrain pages that are affected (limited to four, because only the four corners of the projection area are tested) and adds a pass to the materials of those pages.
If the Decal has to move, just call updatePosition(). I think you can guess the meaning of updateSize(). ;)

Using the class

on startup:

  1. init (doesn`t show the decal, just prepares)
  2. updatePosition
  3. show


after that, do whenever necessary:

  • hide
  • show
  • updatePosition
  • updateSize

Performance

One problem with this implementation is the performance: since every decal adds another pass to the underlaying terrain pages, the fps hit is quite hard as soon as multiple decals act at the same time. So I will be happy to accept optimizations! There are some promising new approaches in this forum thread, which I will investigate further in the near future.

Code

class TerrainDecal
{
protected:
    Ogre::Vector3 mPos;            // center
    Ogre::Vector2 mSize;        // size of decal

    std::string mTexture;        // texture to apply

    Ogre::SceneNode* mNode;        // the projection node
    Ogre::Frustum* mFrustum;    // the projection frustum

    Ogre::SceneManager* mSceneManager;    // pointer to PLSM2

    bool mVisible;                // is the decal visible/active or not?

    // info about materials that are receiving the decal
    std::map<std::string,Ogre::Pass*> mTargets;            // passes mapped by material names

    bool isPosOnTerrain(Ogre::Vector3 pos)
    {
        // get the terrain boundaries
        Ogre::AxisAlignedBox box;
        mSceneManager->getOption("MapBoundaries",&box);

        // check if pos is in box, ignore y
        pos.y = 0;
        return box.intersects(pos);
    }

    std::string getMaterialAtPosition(Ogre::Vector3 pos)
    {
        void* myOptionPtr = &pos;

        // check if position is on battlefield
        if( isPosOnTerrain(pos) )
        {
            mSceneManager->getOption ("getMaterialPageName", myOptionPtr);
            std::string name = **static_cast<std::string**>(myOptionPtr);
            return name;
        }
        else
            return "";
    }

    void addMaterial(std::string matName)
    {
        // check if material is already decalled
        if( mTargets.find(matName) != mTargets.end() )
        {
            Ogre::LogManager::getSingleton().getDefaultLog()->logMessage("material should be added to decal but was already!");
            return;
        }

        using namespace Ogre;

        // get the material ptr
        MaterialPtr mat = (MaterialPtr)MaterialManager::getSingleton().getByName(matName);

        // create a new pass in the material to render the decal
        Pass* pass = mat->getTechnique(0)->createPass();

        // set up the decal's texture unit
        TextureUnitState *texState = pass->createTextureUnitState(mTexture);
        texState->setProjectiveTexturing(true, mFrustum);
        texState->setTextureAddressingMode(TextureUnitState::TAM_CLAMP);
        texState->setTextureFiltering(FO_POINT, FO_LINEAR, FO_NONE);

        // set our pass to blend the decal over the model's regular texture
        pass->setSceneBlending(SBT_TRANSPARENT_ALPHA);
        pass->setDepthBias(1);

        // set the decal to be self illuminated instead of lit by scene lighting
        pass->setLightingEnabled(false);

        // save pass in map
        mTargets[matName] = pass;

        Ogre::LogManager::getSingleton().getDefaultLog()->logMessage(std::string("added material to decal: ") + matName + 
            "(" + Ogre::StringConverter::toString(mTargets.size()) + " materials loaded)");
    }

    std::map<std::string,Ogre::Pass*>::iterator removeMaterial(std::string matName)
    {
        // remove our pass from the given material
        mTargets[matName]->getParent()->removePass(mTargets[matName]->getIndex());

        Ogre::LogManager::getSingleton().getDefaultLog()->logMessage(std::string("removed material from decal: ") + matName + 
            "(" + Ogre::StringConverter::toString(mTargets.size()-1) + " materials loaded)");

        // remove from map
        return mTargets.erase(mTargets.find(matName));
    }

public:
    TerrainDecal()
    {
        mVisible = false;
        mNode = 0;
        mFrustum = 0;
    };

    ~TerrainDecal()
    {
        hide();

        // delete frustum
        mNode->detachAllObjects();
        delete mFrustum;

        // destroy node
        mNode->getParentSceneNode()->removeAndDestroyChild(mNode->getName());
    };

    void init( Ogre::SceneManager* man, Ogre::Vector2 size, std::string tex )
    {
        using namespace Ogre;

        // set SM
        mSceneManager = man;

        // init projective decal
        // set up the main decal projection frustum
        mFrustum = new Ogre::Frustum();
        mNode = mSceneManager->getRootSceneNode()->createChildSceneNode();
        mNode->attachObject(mFrustum);
        mFrustum->setProjectionType(Ogre::PT_ORTHOGRAPHIC);
        mNode->setOrientation(Ogre::Quaternion(Ogre::Degree(90),Ogre::Vector3::UNIT_X));

        // set given values
        updateSize(size);
        mTexture = tex;        // texture to apply

        // load the images for the decal and the filter
        TextureManager::getSingleton().load
            (mTexture, ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, TEX_TYPE_2D, 1);

        mVisible = false;
    }

    void show()
    {
        if( !mVisible )
        {
            mVisible = true;
            updatePosition(mPos);
        }
    }

    void hide()
    {
        if( mVisible )
        {
            // remove all added passes
            while( !mTargets.empty() )
                removeMaterial(mTargets.begin()->first);

            mVisible = false;
        }
    }

    void updatePosition( Ogre::Vector3 pos )
    {
        // don`t do anything if pos didn`t change
        if( pos == mPos )
            return;

        // save the new position
        mPos = pos;
        mNode->setPosition(pos.x,pos.y+1000,pos.z);

        // don`t show if invisible
        if( !isVisible() )
            return;

        // check near pages (up to 4)
        std::list<std::string> neededMaterials;
        Ogre::Vector3 t;

        // x high        z high
        t = Ogre::Vector3(mPos.x+mSize.x/2.0f,1000,mPos.z+mSize.y/2.0f);
        neededMaterials.push_back(getMaterialAtPosition(t));

        // x high        z low
        t = Ogre::Vector3(mPos.x+mSize.x/2.0f,1000,mPos.z-mSize.y/2.0f);
        neededMaterials.push_back(getMaterialAtPosition(t));

        // x low        z low
        t = Ogre::Vector3(mPos.x-mSize.x/2.0f,1000,mPos.z-mSize.y/2.0f);
        neededMaterials.push_back(getMaterialAtPosition(t));

        // x low        z high
        t = Ogre::Vector3(mPos.x-mSize.x/2.0f,1000,mPos.z+mSize.y/2.0f);
        neededMaterials.push_back(getMaterialAtPosition(t));

        // remove empties
        neededMaterials.remove("");

        // remove doubles
        neededMaterials.unique();

        // compare needed materials with used
        
        // for every used material
        std::map<std::string,Ogre::Pass*>::iterator used = mTargets.begin();
        while(true)
        {
            // stop if we are through
            if( used == mTargets.end() )
                break;

            // find in needed
            std::list<std::string>::iterator needed = 
                std::find(neededMaterials.begin(),neededMaterials.end(),used->first);
            
            if( needed == neededMaterials.end() )
            {
                // material is not needed any longer, so remove it
                used = removeMaterial(used->first);
            }
            else
            {                
                // remove it from needed list, bedause it is already loaded
                neededMaterials.remove(used->first);

                // go further
                used++;
            }
        }

        // add all remaining needed to used list
        while( !neededMaterials.empty() )
        {
            addMaterial(neededMaterials.front());
            neededMaterials.erase(neededMaterials.begin());
        }
    }

    void updateSize(Ogre::Vector2 size)
    {
        if( mSize != size )
        {
            // save param
            mSize = size;

            // update aspect ratio
            mFrustum->setAspectRatio(mSize.x/mSize.y);

            // update height
            mFrustum->setOrthoWindowHeight(mSize.y);
        }
    }

    bool isVisible()
    {
        return mVisible;
    }
};


Alias: Projective_Decals