Skip to main content

History: Selection Buffer

Source of version: 25 (current)

Copy to clipboard
            {DIV(type="div",width="50",float="right",bg="#FFFFFF")}
{maketoc}{DIV}
!{img fileId=2038} 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 2{SUP()}24{SUP} 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 [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.

%ogre17%%ogre16%
!{img fileId=2037} Download
Downloadable 7-Zip archive of all eight source files:
{img fileId=2034 link=dl2030 title=Download} [dl2030|SelectionBufferSource.7z]

!{img fileId=2031} 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}
You'll also need to make sure the material scripts are loaded using the Ogre resource system. (%warning%Sample code not shown.)

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}

!-{img fileId=2031} 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.
!!{img fileId=2032} Header Files
!!!-{img fileId=2032} SelectionBuffer.h
{CODE(wrap="1", caption="SelectionBuffer.h", 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}
!!!-{img fileId=2032} MaterialSwitcher.h
{CODE(wrap="1", caption="MaterialSwitcher.h", 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}
!!!-{img fileId=2032} SelectionRenderListener.h
{CODE(wrap="1", caption="SelectionRenderListener.h", 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}
!!{img fileId=2031} Source Files
!!!-{img fileId=2031} MaterialSwitcher.cpp
{CODE(wrap="1", caption="MaterialSwitcher.cpp", 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}
!!!-{img fileId=2031} SelectionRenderListener.cpp
{CODE(wrap="1", caption="SelectionRenderListener.cpp", 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}
!!!-{img fileId=2031} SelectionBuffer.cpp
{CODE(wrap="1", caption="SelectionBuffer.cpp", 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->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();
}{CODE}
!!{img fileId=2033"} Material Files
!!!-{img fileId=2033} PlainColor.material
{CODE(wrap="1", caption="PlainColor.material", 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}
!!!-{img fileId=2033} Plaincolor.cg
{CODE(wrap="1", caption="Plaincolor.cg", 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}

!{img fileId=2036} Compatibility
This code is known to work in Ogre 1.6 and Ogre 1.7.