History: Intermediate Tutorial 6
Preview of version: 50
- «
- »
Any problems you encounter during working with this tutorial should be posted in the Help Forum
.
Table of contents
Introduction
This tutorial will cover projecting decals on to a mesh. This is a common technique used to highlight units in certain strategy games by painting a circle on the ground around their feet. It could also be used to paint a gun's laser sight on to its target.
Here is what we're going to create to demonstrate the effect:
The full source for this tutorial is here.
There is also source available that uses the BaseApplication framework and Ogre 1.7 here.
Prerequisites
This tutorial assumes that you already know how to set up an Ogre project and compile it successfully. Knowledge of the topics from previous tutorials is also assumed.
These two images are required for this tutorial:
No such attachment on this page
No such attachment on this page
Put these files into your media/materials/textures folder. Note: the tutorial uses lower case names for these files (i.e. "decal.png" and "decal_filter.png"). Either save the images with lower case or change it when you add the code to the tutorial.
This tutorial also uses a material that is not from the Samples folder. You can use whatever you want, but to get the look from the tutorial you can place this material into one of the material scripts you're using. They are stored in /media/materials/scripts.
material Tutorial/WaterFloor { technique { pass { texture_unit { texture spacesky.jpg } } } }
Setting Up the Scene
First, comment out the setAmbientLight call at the beginning of createScene. Now, create a light and position the camera:
Ogre::Light* light = mSceneMgr->createLight("MainLight"); light->setPosition(0, 40, 0); mCamera->setPosition(60, 100, 70); mCamera->lookAt(0, 0, 0);
Now we'll add a floor plane.
// Add floor plane Ogre::Plane plane; plane.normal = Ogre::Vector3::UNIT_Y; plane.d = 0; Ogre::MeshManager::getSingleton().createPlane( "floor", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, plane, 450.0, 450.0, 10, 10, true, 1, 50.0, 50.0, Ogre::Vector3::UNIT_Z); Ogre::Entity* planeEntity = mSceneMgr->createEntity(); planeEntity->setMaterialName("Tutorial/SpaceFloor"); planeEntity->setCastShadows(false); mSceneMgr->getRootSceneNode()->createChildSceneNode()->attachObject(planeEntity);
This should all be familiar from the Basic Tutorial series. Next we are going to build a ring of Ogre heads to project our decals on to.
// Add Ogre heads int numOfHeads = 14; double radius = 180; Ogre::Radian curAngle(0); Ogre::Entity* ogreEntity; for (int i = 0; i < numOfHeads; ++i) { Ogre::SceneNode* headNode = mSceneMgr->getRootSceneNode()->createChildSceneNode(); ogreEntity = mSceneMgr->createEntity("ogrehead.mesh"); headNode->setScale(2.2, 2.8, 2.2); headNode->attachObject(ogreEntity); curAngle = curAngle + Ogre::Radian(Ogre::Math::TWO_PI / numOfHeads); headNode->setPosition( -radius * Ogre::Math::Sin(curAngle), 20, -radius * Ogre::Math::Cos(curAngle)); headNode->rotate(Ogre::Vector3::UNIT_Y, curAngle); }
You can play around with the number of heads and the radius. Our code will automatically place them evenly around the circle. We won't go into the details here, but it's probably becomming clear that a good understanding of mathematics will be extremely useful for a graphics programmer. This is why you learned trigonometry in school. It was so you could build righteous Ogre head structures someday.
Projecting Decals
You create a frustum by chopping off the top of something like a pyramid or a cone. Here's a nice royalty free image of one:
Ogre uses a frustum to represent the camera. In fact, the Camera class inherits directly from the Frustum class. You can now see why Ogre's representation of the camera doesn't behave as if it were a point-like object. When we called getCameraToViewportRay in previous tutorials it projected the rays from the "cap" of the Camera frustum. In this tutorial, we will be using a Frustum to project our decal onto the Ogre meshes.
The first step will be creating the this projector. Add the following to our BasicApp class:
void createProjector(); void addDecalToMaterial(const Ogre::String& matName);
void BasicApp::createProjector() { } void BasicApp::addDecalToMaterial(const Ogre::String& matName); { }
We will get to the addDecalToMaterial method shortly. To start building our projector, we will first create the frustum. Add the following to createProjector:
mDecalFrustum = new Ogre::Frustum(); mProjectorNode = mSceneMgr->getRootSceneNode()->createChildSceneNode(); mProjectorNode->attachObject(mDecalFrustum); mProjectorNode->setPosition(0, 30, 0);
The projector is place in the center of the ring of Ogre heads. The image produced by this projector will grow larger as it moves farther away from the surface it is projecting onto. This is like how a real film projector would behave. If you wanted a projector which maintains a constant size image, then you would use code like this:
// Do not add to your project. mDecalFrustum->setProjectionType(Ogre::PT_ORTHOGRAPHIC); mDecalFrustum->setOrthoWindowHeight(100);
Applying the Decal
Even though we've already setup our projector, we still need to apply the projection to our entity's materials. We do this by creating a new Pass which renders the decal on to our materials. In this tutorial, we will be modifying the material directly, but you should generally modify a copy of your materials so that you can return to the original material. Add the following to addDecalToMaterial:
Ogre::MaterialPtr mat = (Ogre::MaterialPtr)Ogre::MaterialManager::getSingleton().getByName(matName); Ogre::Pass *pass = mat->getTechnique(0)->createPass();
Now that we have a Pass object, we need to set up blending and lighting. We will be adding a new texture which must be blended with the object's current texture. To do this, we will set the scene blending to be transparent alpha and the depth bias to 1. This will make sure there is no transparency in the decal. Finally, we need to disable lighting for the material so that it will always show like other GUI elements.
pass->setSceneBlending(Ogre::SBT_TRANSPARENT_ALPHA); pass->setDepthBias(1); pass->setLightingEnabled(false);
Now we'll create a new TextureUnitState using our decal.png. We then turn on projective texturing, and set some options.
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 repeat on the object. We have set the magnification of the object to linear, and we have basically turned off minification (FO_POINT). We've completely disabled mipmapping. This prevents the transparent edges of the decal from being blurred into our material.
That's the entire setup for the material.
Calling the Functions
We need to call the functions we've just built. Add the following to createScene right after the code to build the ogre heads:
createProjector(); for (unsigned int i = 0; i < planeEntity->getNumSubEntities(); ++i) makeMaterialReceiveDecal(planeEntity->getSubEntity(i)->getMaterialName()); for (unsigned int i = 0; i < ogreEntity->getNumSubEntities(); ++i) makeMaterialReceiveDecal(ogreEntity->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
As you've probably noticed, the decal is being projected forwards and backwards. Ogre naturally projects the decal in both directions. For our current purposes, this is not what we want. To fix this, we will introduce a filter that will remove the back projection.
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
We are going to add our filter as another texture state for our Pass we added to our material. Add the following to addDecalToMaterial:
texState = pass->createTextureUnitState("decal_filter.png"); texState->setProjectiveTexturing(true, mFilterFrustum); texState->setTextureAddressingMode(Ogre::TextureUnitState::TAM_CLAMP); texState->setTextureFiltering(Ogre::TFO_NONE);
This should look familiar from before. We are using the decal_filter texture and the filter frustum. We have also turned off filtering. Compile and run the application. You should now only see the forward projection.
Showing Off the Projection
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.
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 (such as 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.
Exercises
Easy
Intermediate
Difficult
Advanced
Conclusion
TODO
Full Source
The full source for this tutorial is here.
Next
Alias: Intermediate_Tutorial_6