OGRE Projective Decals

Decal_shot.png

 


Introduction
In this tutorial we will be covering how to add projective decals to an object in the scene. Projective texturing is useful when you want to do something like a selection indicator on the ground, an aiming sight that's projected on what you're aiming at, or some other type of decal that's projected onto something (but doesn't become a permanent part of the target like splatting). Here's a screenshot of an aiming site being projected onto everyone's favorite ogre head:

 

You can find the code for this tutorial here. As you go through the tutorial you should be slowly adding code to your own project and watching the results as we build it.

Getting Started
New Textures
Before we get started on this project, we need to add two new images we will be using:
Decal.png
Decal_filter.png

The best place to put this is in the media/materials/textures folder (for most people this should be located in the OgreSDK folder). Also note the character case by making sure the file names are all lower case as they are referenced as such in the tutorial.

The Initial Code
Set up your Intermediate Tutorial 6 project to look something like this: 

IntermediateTutorial6.h 

#ifndef __IntermediateTutorial6_h_
#define __IntermediateTutorial6_h_
 
#include "BaseApplication.h"
 
class IntermediateTutorial6 : public BaseApplication
{
public:
    IntermediateTutorial6(void);
    virtual ~IntermediateTutorial6(void);
 
protected:
    virtual void createScene(void);
	virtual bool frameRenderingQueued(const Ogre::FrameEvent& evt);
	virtual void createProjector();
	virtual void makeMaterialRecieveDecal(const Ogre::String& matName);
 
	Ogre::SceneNode* mProjectorNode;
	Ogre::Frustum* mDecalFrustum;
	Ogre::Frustum* mFilterFrustum;
	float mAnim;
};
 
#endif // #ifndef __IntermediateTutorial6_h_
  
IntermediateTutorial6.cpp 

#include "IntermediateTutorial6.h"
 
//-------------------------------------------------------------------------------------
IntermediateTutorial6::IntermediateTutorial6(void)
{
}
//-------------------------------------------------------------------------------------
IntermediateTutorial6::~IntermediateTutorial6(void)
{
}
 
//-------------------------------------------------------------------------------------
void IntermediateTutorial6::createScene(void)
{
	mSceneMgr->setAmbientLight(Ogre::ColourValue(0.2f, 0.2f, 0.2f));
 
	Ogre::Light* light = mSceneMgr->createLight("MainLight");
	light->setPosition(20, 80, 50);
 
	mCamera->setPosition(60, 200, 70);
	mCamera->lookAt(0,0,0);
 
	Ogre::Entity* ent;
	for (int i = 0; i < 6; i++)
	{
		Ogre::SceneNode* headNode = mSceneMgr->getRootSceneNode()->createChildSceneNode();
		ent = mSceneMgr->createEntity("head" + Ogre::StringConverter::toString(i), "ogrehead.mesh");
		headNode->attachObject(ent);
 
		Ogre::Radian angle(i + Ogre::Math::TWO_PI / 6);
		headNode->setPosition(75 * Ogre::Math::Cos(angle), 0, 75 * Ogre::Math::Sin(angle));
	}
}
 
bool IntermediateTutorial6::frameRenderingQueued(const Ogre::FrameEvent& evt)
{
	return BaseApplication::frameRenderingQueued(evt);
}
 
void IntermediateTutorial6::createProjector()
{
}
 
void IntermediateTutorial6::makeMaterialRecieveDecal(const Ogre::String& matName)
{
}
 
 
 
#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
#define WIN32_LEAN_AND_MEAN
#include "windows.h"
#endif
 
#ifdef __cplusplus
extern "C" {
#endif
 
#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
    INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT )
#else
    int main(int argc, char *argv[])
#endif
    {
        // Create application object
        IntermediateTutorial6 app;
 
        try {
            app.go();
        } catch( Ogre::Exception& e ) {
#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
            MessageBox( NULL, e.getFullDescription().c_str(), "An exception has occured!", MB_OK | MB_ICONERROR | MB_TASKMODAL);
#else
            std::cerr << "An exception has occured: " <<
                e.getFullDescription().c_str() << std::endl;
#endif
        }
 
        return 0;
    }
 
#ifdef __cplusplus
}
#endif
  
Compile and run this program before continuing. You should see six Ogre heads. 

Projecting Decals
Frustums
A frustum represents a pyramid capped at a the near and far end, which represents a visible area or a projection. Ogre uses this to represent cameras with (the Camera class derives directly from the Frustum class). In this tutorial, we will be using a frustum to project the decal onto the meshes in the scene. 

The first thing we will do to create the projector is to create the frustum which represents it and attach it to a SceneNode. Find the createProjector method and add the following code:

mDecalFrustum = new Ogre::Frustum();
mProjectorNode = mSceneMgr->getRootSceneNode()->createChildSceneNode("DecalProjectorNode");
mProjectorNode->attachObject(mDecalFrustum);
mProjectorNode->setPosition(0,5,0);
This creates a projector which will grow the decal as you get farther and farther away from it, a lot like how a film projector works. If you want to create a projector which maintains a constant size and shape of decal at whatever distance we set, you should add the following code (but we do not for this tutorial): 

// Do not add this to the project
mDecalFrustum->setProjectionType(Ogre::PT_ORTHOGRAPHIC);
mDecalFrustum->setOrthoWindowHeight(100);
setOrthoWindowHeight() is used together with aspect ratio to set the size of an orthographic frustum.

Before continuing, please take note of where our frustum is projecting the decal. In this application there is a ring of Ogre heads and the frustum sits in the dead center of them (though shifted up slightly, by 5 units), pointed in the -Z direction (which is the default since we did not change the orientation). This means that, eventually, when we run the application decals will be projected onto the back Ogre heads.

Modifying the Material
In order for the decal to actually show up on an object, the material that it uses has to receive the decal. We do this by creating a new pass which renders the decal on top of the regular texture. The frustum determines the location, size, and shape of the projected decal. In this demo we will be modifying the material itself to receive the decal, but for most real applications, you should probably create a clone of the material to modify so you can switch it off by setting the material back to the original one. 

The first thing we will do is get the material and and create a new pass for the material. Find the makeMaterialReceiveDecal and add the following code:

Ogre::MaterialPtr mat = (Ogre::MaterialPtr)Ogre::MaterialManager::getSingleton().getByName(matName);
Ogre::Pass *pass = mat->getTechnique(0)->createPass();
Now that we have created our pass, we need to setup blending and lighting. We will be adding a new texture which must be blended properly with the current texture already on the object. To do this we will set the scene blending to be transparent alpha, and the depth bias to be 1 (so that there is no transparency in the decal). Lastly we need to disable lighting for the material so that it always shows up no matter what the lighting of the scene is. If you want the decal in your application to be affected by the scene lighting you should not add that last function call: 

pass->setSceneBlending(Ogre::SBT_TRANSPARENT_ALPHA);
pass->setDepthBias(1);
pass->setLightingEnabled(false);
Now that we have our new pass we need to create a new texture unit state using our decal.png image. The second function call turns on projective texturing and takes in the frustum we have created. The final two calls setup the filtering and addressing modes:

Ogre::TextureUnitState *texState = pass->createTextureUnitState("decal.png");
texState->setProjectiveTexturing(true, mDecalFrustum);
texState->setTextureAddressingMode(Ogre::TextureUnitState::TAM_CLAMP);
texState->setTextureFiltering(Ogre::FO_POINT, Ogre::FO_LINEAR, Ogre::FO_NONE);
We have set the texture addressing mode to clamp so that the decal doesn't "loop" itself on the object. For the filtering options, we have set the magnification of the object to use standard linear, but we have basically turned off filtering for minification (FO_POINT), and turned off mipmapping entirely. This prevents the border of the decal (which is transparent) from getting blurred into the rest of the texture when it is minimized. If we do not do that, there will be ugly smearing all over the outside of the place the decal is projected. 

This is all you need to do to setup the material. 

Calling the Functions
Now that we have built the functions, we actually need to call them to setup the projector and the material. Add the following code to theend of the createScene method: 

createProjector();
for (unsigned int i = 0; i < ent->getNumSubEntities(); i++)
{
        makeMaterialReceiveDecal(ent->getSubEntity(i)->getMaterialName());
}
Note that the ent variable already has one of the ogre head entities stored in it from the previous loop. Since all the ogre heads use the same material, we only need to select a random one of them to grab the material names from.

Compile and run the application, you should see a few Ogre heads with a decal projected onto them.

Getting Rid of the Back Projection
Introduction
As you have probably noticed when running the application, there are actually two decals being projected. The first is projected in the -Z direction, which is where our frustum is facing, the other is projected in the +Z direction, onto the ogre headsbehind the frustum we have created. The reason for this is when a decal is projected out of the front of the frustum, a corresponding (inverted) decal is projected out of the back of it.

This is obviously not what we want. To fix it we will introduce a filter that will remove the back projection.

Modifying the Projector
To filter the back projection, we need a new frustum for the filter which points in the direction we wish to filter. Add the following code to the createProjector method:

mFilterFrustum = new Ogre::Frustum();
mFilterFrustum->setProjectionType(Ogre::PT_ORTHOGRAPHIC);
Ogre::SceneNode *filterNode = mProjectorNode->createChildSceneNode("DecalFilterNode");
filterNode->attachObject(mFilterFrustum);
filterNode->setOrientation(Ogre::Quaternion(Ogre::Degree(90),Ogre::Vector3::UNIT_Y));
This should all be familiar. The only difference is that we have rotated the node by 90 degrees to face backwards.

Modifying the Material
Now we need to add another texture state to the pass we added on the material. Add the following to makeMaterialReceiveDecal:

texState = pass->createTextureUnitState("decal_filter.png");
texState->setProjectiveTexturing(true, mFilterFrustum);
texState->setTextureAddressingMode(Ogre::TextureUnitState::TAM_CLAMP);
texState->setTextureFiltering(Ogre::TFO_NONE);
This all should look familiar. Note that we are using the filter texture, the filter frustum, and the we have turned off filtering. Compile and run the application. You should now see only the forward projection of the decals.

Showing Off the Projection
Simple Rotation
To show off the projection, we will rotate the projector. To rotate the projector, simply add the following line of code to the beginning of the frameRenderingQueued method:

mProjectorNode->rotate(Ogre::Vector3::UNIT_Y, Ogre::Degree(evt.timeSinceLastFrame * 10));
Compile and run the application. You will now see the decal projected along the circle of ogre heads.

One Final Note
One last thing to note about decals, if you use decals in your application, be sure that the outer border pixels of the decals are completely transparent (zero alpha). If not, the decal will smear due to the way texture clamping works.

Work around 

The above notice is true, but there's a way around it if your texture doesn't already have transparent borders:
As long as your texture is in a format that supports alpha channels (as e.g. PNG), you can set the Texture Address Mode to TAM_BORDER (or border, if you're doing this in a script) and set the Texture Border Colour to (0, 0, 0, 0). This causes any texture coordinates outside the range 0, 1 to have the border color you specify, which is black with 0 alpha. So essentially, you just added a transparent border to your texture.

You can find a quick implementation in Projective Decals. 

 

 

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;
    }
};
  

 



 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值