MOGRE Intermediate Tutorial 7: Projective Decals

Note: This tutorial converted from OGRE and work with MOGRE SDK 1.7.1 r72
Any problems you encounter while working with this tutorial should be posted to the MOGRE Forum.

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:

Decal_shot.png

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:

 Plugin disabled
Plugin attach cannot be executed.
 Plugin disabled
Plugin attach cannot be executed.


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.cs

using System;

namespace Mogre.Demo.IntermediateTutorial6
{
    class Program
    {
        static void Main(string[] args)
        {
            try { new IntermediateTutorial6().Go(); }
            catch
            {
                if (OgreException.IsThrown) ExampleApplication.Example.ShowOgreException();
                else throw;
            }
        }
    }

    class IntermediateTutorial6 : ExampleApplication.Example
    {
        public override void CreateScene()
        {
            sceneMgr.AmbientLight = new ColourValue(0.2f, 0.2f, 0.2f);

            Light light = sceneMgr.CreateLight("MainLight");
            light.Position = new Vector3(20, 80, 50);

            camera.Position = new Vector3(60, 200, 70);
            camera.LookAt(0, 0, 0);

            //set up the ogre heads
            Entity ent = null;
            for (int i = 0; i < 6; i++)
            {
                SceneNode headNode = sceneMgr.RootSceneNode.CreateChildSceneNode();
                ent = sceneMgr.CreateEntity("head" + i.ToString(), "ogrehead.mesh");
                headNode.AttachObject(ent);

                Radian angle = new Radian(i + Math.TWO_PI / 6);
                headNode.Position = new Vector3(75 * Math.Cos(angle), 0, 75 * Math.Sin(angle));
            }

            root.FrameRenderingQueued += FrameRenderingQueued;
        }

        bool FrameRenderingQueued(FrameEvent evt)
        {
            return true;
        }

        void CreateProjector()
        {
        }
        void MakeMaterialRecieveDecal(String matName)
        {
        }
    }
}


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 Frustum();
mProjectorNode = sceneMgr.RootSceneNode.CreateChildSceneNode("DecalProjectorNode");
mProjectorNode.AttachObject(mDecalFrustum);
mProjectorNode.Position = new Vector3(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.ProjectionType = ProjectionType.PT_ORTHOGRAPHIC;
mDecalFrustum.OrthoWindowHeight = 100;

OrthoWindowHeight 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:

MaterialPtr mat = (MaterialPtr)MaterialManager.Singleton.GetByName(matName);
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(SceneBlendType.SBT_TRANSPARENT_ALPHA);
pass.SetDepthBias(1);
pass.LightingEnabled = 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:

TextureUnitState texState = pass.CreateTextureUnitState("decal.png");
texState.SetProjectiveTexturing(true, mDecalFrustum);
texState.SetTextureAddressingMode(TextureUnitState.TextureAddressingMode.TAM_CLAMP);
texState.SetTextureFiltering(FilterOptions.FO_POINT, FilterOptions.FO_LINEAR, FilterOptions.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 the end of the createScene method:

CreateProjector();
for (uint i = 0; i < ent.NumSubEntities; i++)
{
    MakeMaterialRecieveDecal(ent.GetSubEntity(i).MaterialName);
}

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 heads behind 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 Frustum { ProjectionType = ProjectionType.PT_ORTHOGRAPHIC };
SceneNode filterNode = mProjectorNode.CreateChildSceneNode("DecalFilterNode");
filterNode.AttachObject(mFilterFrustum);
filterNode.Orientation = new Quaternion(new Degree(90), 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(TextureUnitState.TextureAddressingMode.TAM_CLAMP);
texState.SetTextureFiltering(TextureFilterOptions.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(Vector3.UNIT_Y, new 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 C++/OGRE implementation in Projective Decals.