Skip to main content

History: Selection Buffer

Source of version: 3

Copy to clipboard
            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 ~np~2^24~/np~ 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.

{img fileId="1681"}
{img fileId="1682"}

[[Image:ogre_16_logo.gif|frame|right|link=Selection Buffer#Compatibility|'''Compatibility''']]
[[Image:ogre_17_logo.gif|frame|right|link=Selection Buffer#Compatibility|'''Compatibility''']]

These classes are a C++ port of the Python version found [http://wiki.python-ogre.org/index.php/CodeSnippets_Selection_Buffer|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.

{maketoc}

!!!SelectionBuffer.h

{CODE(wrap="1", colors="c++")}#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{CODE}

!!!MaterialSwitcher.h

{CODE(wrap="1", colors="c++")}#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{CODE}

!!!SelectionRenderListener.h

{CODE(wrap="1", colors="c++")}#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{CODE}

!!!MaterialSwitcher.cpp

{CODE(wrap="1", colors="c++")}#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();
}{CODE}

!!!SelectionRenderListener.cpp

{CODE(wrap="1", colors="c++")}#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);
}{CODE}

!!!SelectionBuffer.cpp

{CODE(wrap="1", colors="c++")}#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->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();
}{CODE}

!!!PlainColor.material

{CODE(wrap="1", colors="c++")}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
			}
		}
	}
}{CODE}

!!!Plaincolor.cg

{CODE(wrap="1", colors="c++")}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;
}{CODE}

!!Compatibility

This code is known to work in Ogre 1.6 and Ogre 1.7.

!!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:
{CODE(wrap="1", colors="c++")}#include "SelectionBuffer.h"{CODE}
Then initialization:
{CODE(wrap="1", colors="c++")}// 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);{CODE}
Then process every frame:
{CODE(wrap="1", colors="c++")}// 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");
    }
  }
}{CODE}
        

History

Information Version
Wed 21 of Mar, 2012 21:00 GMT-0000 brunobg Added line mRenderTexture->getViewport(0)->setShadowsEnabled(false); so code works with shadows 25
Tue 05 of Apr, 2011 18:52 GMT-0000 DragonM Add background color specification to floated table of contents div, which makes it opaque. Removed clear dynamic variable since the opaque div covers section header horizontal rules. 24
Tue 05 of Apr, 2011 18:11 GMT-0000 DragonM Add description section title. 23
Tue 05 of Apr, 2011 01:38 GMT-0000 DragonM Fix section indents, add icons, add download link to source archive, reorder sections, float table of contents and collapse source sections since they're less important with a download. 22
Fri 30 of Apr, 2010 18:50 GMT-0000 jacmoe 21
Fri 30 of Apr, 2010 18:44 GMT-0000 jacmoe 20
Fri 30 of Apr, 2010 18:43 GMT-0000 jacmoe 19
Fri 30 of Apr, 2010 18:42 GMT-0000 jacmoe 18
Fri 30 of Apr, 2010 18:42 GMT-0000 jacmoe 17
Fri 30 of Apr, 2010 18:41 GMT-0000 jacmoe 16
Fri 30 of Apr, 2010 18:41 GMT-0000 jacmoe 15
Fri 30 of Apr, 2010 18:40 GMT-0000 jacmoe 14
Fri 30 of Apr, 2010 18:35 GMT-0000 jacmoe 13
Fri 30 of Apr, 2010 18:33 GMT-0000 jacmoe 12
Fri 30 of Apr, 2010 18:32 GMT-0000 jacmoe 11
Fri 30 of Apr, 2010 18:27 GMT-0000 jacmoe 10
Fri 30 of Apr, 2010 18:26 GMT-0000 jacmoe 9
Fri 30 of Apr, 2010 18:26 GMT-0000 jacmoe 8
Fri 30 of Apr, 2010 18:25 GMT-0000 jacmoe 7
Fri 30 of Apr, 2010 18:24 GMT-0000 jacmoe 6
Fri 30 of Apr, 2010 18:23 GMT-0000 jacmoe 5
Fri 30 of Apr, 2010 18:21 GMT-0000 jacmoe 4
Fri 30 of Apr, 2010 18:19 GMT-0000 jacmoe 3
Fri 30 of Apr, 2010 18:18 GMT-0000 jacmoe 2
Fri 30 of Apr, 2010 18:15 GMT-0000 jacmoe 1