Description
The following classes are a C++ implementation of a pixel perfect object selection mechanism.
It works by rendering a single frame to a texture with custom substituted materials that colorize each entity with a unique color (up to 224 colors/objects). The MaterialSwitcher class saves which color belongs to which entity. The SelectionBuffer class then retrieves the correct Entity from the SceneManager. It does not work on StaticGeometry or any other geometry that isn't independent entities, in Ogre terms.
These classes are a C++ port of the Python version found here. The behavior of the C++ version differs from the original Python version in one notable way: render colors are assigned sequentially, rather than randomly. This makes interpreting the debug overlay more visually difficult, but simplifies the code somewhat.
%ogre17%%ogre16%
Download
Downloadable 7-Zip archive of all eight source files:
SelectionBufferSource.7z
Usage
The following fragments won't work as-is. You'll need to put them in appropriate places to integrate them with your code.
First the necessary include:
Copy to clipboard
#include "SelectionBuffer.h"
Then initialization:
Copy to clipboard
// Hang on to this pointer somewhere (e.g. a member of a user interface manager class)
SelectionBuffer *mSelectionBuffer;
// SceneManager *sceneManager; (previously created)
// RenderWindow *renderWindow; (which can come from calling Ogre::Root::initialise(), among other places)
mSelectionBuffer = new SelectionBuffer(sceneManager, renderWindow);
You'll also need to make sure the material scripts are loaded using the Ogre resource system. (Sample code not shown.)
Then process every frame:
Copy to clipboard
// showSelectionDebugOverlay is a boolean whose value is determined by, for instance, a key press
if(mSelectionBuffer)
mSelectionBuffer->ShowOverlay(showSelectionDebugOverlay);
Entity *selectedEntity;
// rightMouseClicked is a boolean whose value came from, for instance,
// an OIS::MouseListener mouseReleased(const OIS::MouseEvent &arg, OIS::MouseButtonID id) method
if(rightMouseClicked)
{
if(mSelectionBuffer)
{
// mouseAbsX and mouseAbsY are the absolute coordinates of the click from, for instance, an OIS::MouseState
selectedEntity = mSelectionBuffer->OnSelectionClick(mouseAbsX, mouseAbsY);
if(selectedEntity != 0)
{
LogManager::getSingleton().logMessage("Selected " + selectedEntity->getName());
// Do something useful to the selected entity here
}
else
{
LogManager::getSingleton().logMessage("Entity not found");
}
}
}
[+]
Source
All of the following eight files are complete in themselves and included in the downloadable archive. They only need to be added to your build system to be used.
[+]
SelectionBuffer.h
SelectionBuffer.h
Copy to clipboard
#ifndef SELECTIONBUFFER_H
#define SELECTIONBUFFER_H
using namespace Ogre;
class MaterialSwitcher;
class SelectionRenderListener;
class SelectionBuffer
{
private:
SceneManager *mSceneMgr;
Camera *mCamera;
RenderTarget *mRenderTarget;
// This is the material listener - Note: it is controlled by a separate
// RenderTargetListener, not applied globally to all targets
MaterialSwitcher *mMaterialSwitchListener;
SelectionRenderListener *mSelectionTargetListener;
TexturePtr mTexture;
RenderTexture *mRenderTexture;
uint8 *mBuffer;
PixelBox *mPixelBox;
Overlay *mSelectionDebugOverlay;
void CreateRTTOverlays();
void Update();
void UpdateBufferSize();
public:
SelectionBuffer(SceneManager *sceneMgr, RenderTarget *renderTarget);
~SelectionBuffer();
Entity *OnSelectionClick(int x, int y);
void ShowOverlay(bool show);
};
#endif
[+]
MaterialSwitcher.h
MaterialSwitcher.h
Copy to clipboard
#ifndef MATERIALSWITCHER_H
#define MATERIALSWITCHER_H
using namespace Ogre;
class SelectionBuffer;
struct cmp_ColourValue
{
bool operator()(const ColourValue &a, const ColourValue &b) const
{
return a.getAsBGRA() < b.getAsBGRA();
}
};
class MaterialSwitcher : public MaterialManager::Listener
{
private:
typedef std::map<ColourValue, String, cmp_ColourValue> ColorMap;
typedef std::map<ColourValue, String, cmp_ColourValue>::const_iterator ColorMapConstIter;
String mEmptyString;
ColourValue mCurrentColor;
String mLastEntity;
Technique *mLastTechnique;
MaterialSwitcher::ColorMap mColorDict;
void getNextColor();
friend SelectionBuffer;
public:
MaterialSwitcher();
~MaterialSwitcher();
virtual Technique *handleSchemeNotFound(unsigned short schemeIndex, const String &schemeName,
Material *originalMaterial, unsigned short lodIndex,
const Renderable *rend);
const String &getEntityName(const ColourValue &color) const;
void reset();
};
#endif
[+]
SelectionRenderListener.h
SelectionRenderListener.h
Copy to clipboard
#ifndef SELECTIONRENDERLISTENER_H
#define SELECTIONRENDERLISTENER_H
using namespace Ogre;
class MaterialSwitcher;
/**
We need this attached to the depth target, otherwise we get problems with the compositor
MaterialSwitcher should NOT be running all the time - rather only when we're
specifically rendering the target that needs it
*/
class SelectionRenderListener : public RenderTargetListener
{
private:
MaterialSwitcher *mMaterialListener;
public:
SelectionRenderListener(MaterialSwitcher *switcher);
~SelectionRenderListener();
virtual void preRenderTargetUpdate(const RenderTargetEvent &evt);
virtual void postRenderTargetUpdate(const RenderTargetEvent &evt);
};
#endif
Source Files
[+]
MaterialSwitcher.cpp
MaterialSwitcher.cpp
Copy to clipboard
#include "MaterialSwitcher.h"
MaterialSwitcher::MaterialSwitcher() : mLastTechnique(0)
{
mCurrentColor = ColourValue(0.0, 0.0, 0.0);
}
MaterialSwitcher::~MaterialSwitcher()
{
}
Technique *MaterialSwitcher::handleSchemeNotFound(unsigned short schemeIndex, const Ogre::String &schemeName,
Ogre::Material *originalMaterial, unsigned short lodIndex,
const Ogre::Renderable *rend)
{
if(rend)
{
if(typeid(*rend) == typeid(SubEntity))
{
const SubEntity *subEntity = static_cast<const SubEntity *>(rend);
if(mLastEntity == subEntity->getParent()->getName())
{
const_cast<SubEntity *>(subEntity)->setCustomParameter(1, Vector4(mCurrentColor.r, mCurrentColor.g, mCurrentColor.b, 1.0));
}
else
{
ResourcePtr res = MaterialManager::getSingleton().load("PlainColor", ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
mLastTechnique = static_cast<MaterialPtr>(res)->getTechnique(0);
getNextColor();
const_cast<SubEntity *>(subEntity)->setCustomParameter(1, Vector4(mCurrentColor.r, mCurrentColor.g, mCurrentColor.b, 1.0));
mLastEntity = subEntity->getParent()->getName();
mColorDict[mCurrentColor] = mLastEntity;
}
return mLastTechnique;
}
}
return 0;
}
const String &MaterialSwitcher::getEntityName(const Ogre::ColourValue &color) const
{
ColorMapConstIter iter = mColorDict.find(color);
if(iter != mColorDict.end())
return (*iter).second;
else
return mEmptyString;
}
void MaterialSwitcher::getNextColor()
{
ARGB color = mCurrentColor.getAsARGB();
color++;
mCurrentColor.setAsARGB(color);
// Alternatively, for an easier to interpret debug overlay with an
// increased risk of dictionary collision:
// mCurrentColor.setAsARGB(Ogre::Math::UnitRandom()*0x00FFFFFF + 0xFF000000);
}
void MaterialSwitcher::reset()
{
mCurrentColor = ColourValue(0.0, 0.0, 0.0);
mLastEntity.clear();
mColorDict.clear();
}
[+]
SelectionRenderListener.cpp
SelectionRenderListener.cpp
Copy to clipboard
#include "SelectionRenderListener.h"
#include "MaterialSwitcher.h"
SelectionRenderListener::SelectionRenderListener(MaterialSwitcher *switcher) : mMaterialListener(switcher)
{
}
SelectionRenderListener::~SelectionRenderListener()
{
delete mMaterialListener;
}
void SelectionRenderListener::preRenderTargetUpdate(const RenderTargetEvent &evt)
{
MaterialManager::getSingleton().addListener(mMaterialListener);
}
void SelectionRenderListener::postRenderTargetUpdate(const RenderTargetEvent &evt)
{
MaterialManager::getSingleton().removeListener(mMaterialListener);
}
[+]
SelectionBuffer.cpp
SelectionBuffer.cpp
Copy to clipboard
#include <Ogre.h>
#include "MaterialSwitcher.h"
#include "SelectionRenderListener.h"
#include "SelectionBuffer.h"
SelectionBuffer::SelectionBuffer(SceneManager *sceneMgr, RenderTarget *renderTarget) : mSceneMgr(sceneMgr),
mRenderTarget(renderTarget),
mBuffer(0),
mPixelBox(0)
{
mCamera = mSceneMgr->getCamera("MainCamera");
mMaterialSwitchListener = new MaterialSwitcher();
mSelectionTargetListener = new SelectionRenderListener(mMaterialSwitchListener);
unsigned int width = mRenderTarget->getWidth();
unsigned int height = mRenderTarget->getHeight();
mTexture = TextureManager::getSingleton().createManual("SelectionPassTex", ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,
TEX_TYPE_2D, width, height, 0, PF_R8G8B8, TU_RENDERTARGET);
mRenderTexture = mTexture->getBuffer()->getRenderTarget();
mRenderTexture->setAutoUpdated(false);
mRenderTexture->setPriority(0);
mRenderTexture->addViewport(mCamera);
mRenderTexture->getViewport(0)->setOverlaysEnabled(false);
mRenderTexture->getViewport(0)->setClearEveryFrame(true);
mRenderTexture->getViewport(0)->setShadowsEnabled(false);
mRenderTexture->addListener(mSelectionTargetListener);
mRenderTexture->getViewport(0)->setMaterialScheme("aa");
HardwarePixelBufferSharedPtr pixelBuffer = mTexture->getBuffer();
size_t bufferSize = pixelBuffer->getSizeInBytes();
mBuffer = new uint8[bufferSize];
mPixelBox = new PixelBox(pixelBuffer->getWidth(), pixelBuffer->getHeight(),
pixelBuffer->getDepth(), pixelBuffer->getFormat(), mBuffer);
CreateRTTOverlays();
}
SelectionBuffer::~SelectionBuffer()
{
TextureManager::getSingleton().unload("SelectionPassTex");
delete mPixelBox;
delete[] mBuffer;
delete mSelectionTargetListener;
delete mMaterialSwitchListener;
}
void SelectionBuffer::Update()
{
UpdateBufferSize();
mMaterialSwitchListener->reset();
mRenderTexture->update();
mRenderTexture->copyContentsToMemory(*mPixelBox, RenderTarget::FB_FRONT);
}
void SelectionBuffer::UpdateBufferSize()
{
unsigned int width = mRenderTarget->getWidth();
unsigned int height = mRenderTarget->getHeight();
if(width != mRenderTexture->getWidth() || height != mRenderTexture->getHeight())
{
TextureManager::getSingleton().unload("SelectionPassTex");
delete[] mBuffer;
delete mPixelBox;
mTexture = TextureManager::getSingleton().createManual("SelectionPassTex", ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,
TEX_TYPE_2D, width, height, 0, PF_R8G8B8, TU_RENDERTARGET);
mRenderTexture = mTexture->getBuffer()->getRenderTarget();
mRenderTexture->setAutoUpdated(false);
mRenderTexture->setPriority(0);
mRenderTexture->addViewport(mCamera);
mRenderTexture->getViewport(0)->setOverlaysEnabled(false);
mRenderTexture->getViewport(0)->setClearEveryFrame(true);
mRenderTexture->addListener(mSelectionTargetListener);
mRenderTexture->getViewport(0)->setMaterialScheme("aa");
HardwarePixelBufferSharedPtr pixelBuffer = mTexture->getBuffer();
size_t bufferSize = pixelBuffer->getSizeInBytes();
mBuffer = new uint8[bufferSize];
mPixelBox = new PixelBox(pixelBuffer->getWidth(), pixelBuffer->getHeight(),
pixelBuffer->getDepth(), pixelBuffer->getFormat(), mBuffer);
}
}
Entity *SelectionBuffer::OnSelectionClick(int x, int y)
{
Update();
size_t posInStream = (mPixelBox->getWidth() * y) * 4;
posInStream += x * 4;
BGRA color(0);
memcpy((void *)&color, mBuffer+posInStream, 4);
ColourValue cv;
cv.setAsARGB(color);
cv.a = 1.0f;
const String &entName = mMaterialSwitchListener->getEntityName(cv);
if(entName.empty())
return 0;
else
return mSceneMgr->getEntity(entName);
}
void SelectionBuffer::CreateRTTOverlays()
{
MaterialPtr baseWhite = MaterialManager::getSingleton().getDefaultSettings();
MaterialPtr selectionBufferTexture = baseWhite->clone("SelectionDebugMaterial");
TextureUnitState *textureUnit = selectionBufferTexture->getTechnique(0)->getPass(0)->createTextureUnitState();
textureUnit->setTextureName("SelectionPassTex");
OverlayManager *mgr = OverlayManager::getSingletonPtr();
mSelectionDebugOverlay = mgr->create("SelectionDebugOverlay");
OverlayContainer *panel = static_cast<OverlayContainer *>(mgr->createOverlayElement("Panel", "SelectionDebugPanel"));
panel->setMetricsMode(GMM_PIXELS);
panel->setPosition(10, 10);
panel->setDimensions(400, 280);
panel->setMaterialName("SelectionDebugMaterial");
mSelectionDebugOverlay->add2D(panel);
mSelectionDebugOverlay->show();
}
void SelectionBuffer::ShowOverlay(bool show)
{
if(show)
mSelectionDebugOverlay->show();
else
mSelectionDebugOverlay->hide();
}
{img fileId=2033"} Material Files
[+]
PlainColor.material
PlainColor.material
Copy to clipboard
vertex_program PlainColor_VS cg
{
source PlainColor.cg
entry_point main_plain_color_vp
profiles vs_1_1 arbvp1
default_params
{
param_named_auto worldViewProj worldviewproj_matrix
}
}
fragment_program PlainColor_PS cg
{
source PlainColor.cg
entry_point main_plain_color_fp
profiles ps_1_1 arbfp1
default_params
{
param_named inColor float4 1 1 1 1
}
}
material PlainColor
{
// Material has one technique
technique
{
// This technique has one pass
pass
{
// Make this pass use the vertex shader defined above
vertex_program_ref PlainColor_VS
{
}
// Make this pass use the pixel shader defined above
fragment_program_ref PlainColor_PS
{
param_named_auto inColor custom 1
}
}
}
}
[+]
Plaincolor.cg
Plaincolor.cg
Copy to clipboard
void main_plain_color_vp(
// Vertex Inputs
float4 position : POSITION, // Vertex position in model space
float2 texCoord0 : TEXCOORD0, // Texture UV set 0
// Outputs
out float4 oPosition : POSITION, // Transformed vertex position
out float2 uv0 : TEXCOORD0, // UV0
// Model Level Inputs
uniform float4x4 worldViewProj)
{
// Calculate output position
oPosition = mul(worldViewProj, position);
// Simply copy the input vertex UV to the output
uv0 = texCoord0;
}
void main_plain_color_fp(
// Pixel Inputs
float2 uv0 : TEXCOORD0, // UV interpolated for current pixel
// Outputs
out float4 color : COLOR, // Output color we want to write
uniform float4 inColor
)
{
color = inColor;
}
Compatibility
This code is known to work in Ogre 1.6 and Ogre 1.7.