Hit testing of non rectangular windows with CEGUI 0.7.x
To use this tutorial with CEGUI's OgreRenderer, you will require a recent bug fix which is available from the stable v0-7 branch of subversion r2564 or later, or a snapshot build of CEGUI dated later than 14th June 2010 (or in other words, a snapshot with a version higher than 0.7.100614). |
Contents
Why this tutorial?
Generally speaking, CEGUI's idea of a window is something that is rectangular. For most normal uses this is fine and perfectly adequate, however, there are times when perhaps it would be nice to have a button (or something else) that's a little bit... special. So, if you're interested in implementing round buttons, pie segment buttons, some type of weird non-rectangular window or window with a hole in it, this tutorial is for you!
What it is
This tutorial is solely about hit testing. Meaning when a CEGUI window reports that a given point hits and when a given point does not hit the window. The code in the tutorial will test the alpha component of the window's rendered imagery, thus allowing pixel accurate hit testing of windows built from irregular shaped imagery.
What it is not
This tutorial does not present a window that is truly non-rectangular. For example, any child content added to the window type shown here will still be clipped – as normal – to the rectangular region defined for the parent window. What is shown is purely about hit testing, although this will be useful in a very large number of cases.
Overview of the what and the how
As you should know, the rendering for a CEGUI window is made up of various images which may be drawn at a one to one pixel size or alternatively may be stretched over some arbitrary area of the window's surface. This presents a challenging situation when we have a desire to examine the pixels of those images, because we can not easily and quickly calculate which bit of a given image is drawn at a particular location for a given window instance. If you factor in the way that various properties can also affect the rendered output, we quickly find ourselves in an almost impossible situation. What we really need is to be able to access the final rendered output of the window in isolation.
In order to achieve our aims, we will have to create a subclass of one of the existing classes in the CEGUI::Window hierarchy and register this type with the system for use. This could be a totally new window type with all new functionality, or we could subclass one of the existing types in order to take advantage of existing functionality, but add in our new hit testing code. For this tutorial we will be sub-classing the CEGUI::PushButton type to provide a push button which has the advanced hit testing which is the topic of this tutorial. At the most basic level, all we are really going to do is override the Window::isHit member function and do something different in there. However, due to the nature of what we are trying to do, some other parts are required in order that we can have access to the required data.
Starting with the 0.7.0 release, CEGUI provides reasonably simple means by which we can render a window to it's own texture. Aside from all the other benefits, this affords us a means to examine the rendering performed for a particular window by examining it's associated texture – and it is this approach that we will use in this tutorial.
The basic approach will be to first do the existing rectangular hit test, because we don't want the expense of testing at the pixel level if the test point is outside of the window's bounding rectangle. Once we know the point is within the window's area, we can then proceed to look at the alpha channel in the rendered output to see if we should report a hit or not – a value of 0 in the alpha channel is 100% transparent, and so will not register a hit, whereas any other value will register a hit. One enhancement that could be made to the code presented here is to allow this to be a configurable threshold value.
Of course, when doing anything of this nature, performance needs to be considered, and you probably already know that frequent reads from texture / video memory by the CPU is likely to be expensive, so our solution attempts to mitigate that somewhat by using a secondary buffer in system RAM that contains a copy of the rendered output, which is only populated when the rendered output changes. This means that for most of the tests we will be operating out of an existing buffer in system memory, which should have minimal impact on overall performance.
Implementation
Class Declaration
First I'll introduce the class declaration, and then we can discuss the various functions and such that we see there. Our class is called AlphaHitWindow, which was somewhat arbitrarily chosen just for the sake of this tutorial.
class AlphaHitWindow : public CEGUI::PushButton
{
public:
//! Window factory name. static const CEGUI::String WidgetTypeName;
//! Constructor AlphaHitWindow(const CEGUI::String& type, const CEGUI::String& name); //! Destructor ~AlphaHitWindow();
// overridden from Window base class bool isHit(const CEGUI::Vector2& position, const bool allow_disabled = false) const;
protected:
//! handler to copy rendered data to a memory buffer bool renderingEndedHandler(const CEGUI::EventArgs& args); // overridden from Window base class bool testClassName_impl(const CEGUI::String& class_name) const;
//! Pointer to buffer holding the render data CEGUI::uint32* d_hitTestBuffer; //! Size of the hit test buffer (i.e. it's capacity) size_t d_hitBufferCapacity; //! Dimensions in pixels of the data in the hit test buffer CEGUI::Size d_hitBufferSize; //! whether data in hit test buffer is inverted. bool d_hitBufferInverted;
}; As we can see, with the exception of the Window::isHit override, the new function renderingEndedHandler and some member variables, the class is mainly standard boiler plate stuff needed when creating a new window type for CEGUI – as such, we'll just be covering the important bits and ignoring those other, standard, parts.
Constructor
Since we have some special requirements, we need to do a couple of things in the constructor. First, because we need texture backing in order to read our rendered output from a texture, we enable the auto rendering surface for our window (if this is not supported, our window will revert back to regular rectangular hit testing).
setUsingAutoRenderingSurface(true);
Next, in order that we only update our buffer when the texture content gets changed, we will subscribe a handler to be called when rendering for each rendering queue is ended.
CEGUI::RenderingSurface* rs = getRenderingSurface();
if (rs) rs->subscribeEvent(CEGUI::RenderingSurface::EventRenderQueueEnded, CEGUI::Event::Subscriber(&AlphaHitWindow::renderingEndedHandler, this));
All simple stuff so far!
The Window::isHit override
This mainly uses the member variables and such which are set up in the event handler discussed below.
The first thing we do is call the base class implementation, and if that returns false, so will we.
if (!CEGUI::PushButton::isHit(position, allow_disabled))
return false;
Next we see if out pointer to a system memory copy of the texture data is valid. If not, it means that either something has gone wrong, or the hardware we're running on does not support what we're trying to do, so in these cases we return true to default back to the standard rectangular hit testing.
if (!d_hitTestBuffer)
return true;
Next we convert the screen pixel point we were given to a local window pixel point, and then use this to calculate the index into the buffer for the pixel described by the given point (this is just a basic calculation of ((y * width) + x).
const CEGUI::Vector2 wpos(CEGUI::CoordConverter::screenToWindow(*this, position));
const size_t idx = (d_hitBufferInverted ? d_hitBufferSize.d_height - wpos.d_y : wpos.d_y) * d_hitBufferSize.d_width + wpos.d_x;
Finally we read the pixel from the buffer, shift right so we are left with just the alpha part and test this against our threshold value – which in this example is simply 0.
return (d_hitTestBuffer[idx] >> 24) > 0;
The renderingEndedHandler event handler function
If there is any magic to doing this, then it happens in this function. It's here that we grab a copy of the rendered texture data and update the other member variables to be ready for subsequent use in the override of the Window::isHit function.
Since our handler will get called for each render queue that is drawn, the first thing we should do is quietly exit for any queue except the one the main rendering will be done on – which is CEGUI::RQ_BASE. In actual fact, it's highly likely that this will be the only queue drawn, but we need the check just to be safe.
if (static_cast<const CEGUI::RenderQueueEventArgs&>(args).queueID != CEGUI::RQ_BASE)
return false;
Next we'll grab the CEGUI::RenderingSurface and make sure it's the right type. If there is no surface or it is not the right type, we'll bail out. Generally this should not happen, but it might if the hardware does not support rendering to texture, or if the user has changed settings in such a way to affect the rendering surface.
CEGUI::RenderingSurface* const rs = getRenderingSurface();
if (!rs || !rs->isRenderingWindow()) return false;
Now we simply get the CEGUI::TextureTarget for our CEGUI::RenderingWindow rendering surface.
CEGUI::TextureTarget& tt =
static_cast<CEGUI::RenderingWindow* const>(rs)->getTextureTarget();
And now we access the actual texture to obtain it's dimensions and calculate the size of the buffer needed to hold the data (in 32bit pixels, since all CEGUI rendering is ARGB format).
CEGUI::Texture& texture = tt.getTexture();
const CEGUI::Size tex_sz(texture.getSize()); const size_t reqd_capacity = static_cast<int>(tex_sz.d_width) * static_cast<int>(tex_sz.d_height);
At this point, we may need to allocate or reallocate our buffer depending on the previous state. So we simply compare the required buffer size we just calculated against the stored buffer size, and if we need a bigger buffer we free the existing one so that we know to reallocate in the next step.
if (reqd_capacity > d_hitBufferCapacity)
{ delete[] d_hitTestBuffer; d_hitTestBuffer = 0; d_hitBufferCapacity = 0; }
Next we see if we already have a buffer allocated, and if not, allocate a buffer of the appropriate size.
if (!d_hitTestBuffer)
{ d_hitTestBuffer = new CEGUI::uint32[reqd_capacity]; d_hitBufferCapacity = reqd_capacity; }
Next we store some details about the buffer so we know how to interpret the data within the isHit function.
d_hitBufferInverted = tt.isRenderingInverted();
d_hitBufferSize = tex_sz;
And the most important step, save the data from the texture into our buffer
texture.saveToMemory(d_hitTestBuffer);
And finally, return that we handled this event
return true;
Registering the Window and making Schemes!
Now we have our new Window type and we know how it all works, we need to register this new type with CEGUI so that the system knows of its existence and can make instances of it. This is another incredibly simple step, requiring just a single line of code.
CEGUI::WindowFactoryManager::addFactory<CEGUI::TplWindowFactory<AlphaHitWindow> >();
Now we can add an entry in our scheme file which uses our new type in a Falagard window mapping, such that it gets used. For this tutorial, we'll use TaharezLook as the example, though the changes or additions are generally the same for any scheme.
We have the option of creating an all new window type mapping, which will allow us to continue to use the existing TaharezLook/Button type, like this:
<FalagardMapping WindowType="TaharezLook/AlphaHitButton" TargetType="AlphaHitWindow" Renderer="Falagard/Button" LookNFeel="TaharezLook/Button" />
Or, we can modify the existing mapping so that TaharezLook/Button will use our new class instead, like this:
<FalagardMapping WindowType="TaharezLook/Button" TargetType="AlphaHitWindow" Renderer="Falagard/Button" LookNFeel="TaharezLook/Button" />
Conclusion
Here we have created a new window type that will take advantage of certain abilities in CEGUI 0.7.x to allow us to have hit testing at the pixel level. The example above uses TaharezLook imagery for the button, which is probably not ideal as an example to show the effects achieved here, and perhaps this will be modified in a future update.
Below you can find the full class declaration and source code for the AlphaHitWindow class. You need to include the main CEGUI.h header file in order to compile the code. I hope you find this useful, and please direct any questions about this to the forum or the #cegui IRC channel on irc.freenode.net.
Class Declaration
class AlphaHitWindow : public CEGUI::PushButton
{
public:
//! Window factory name. static const CEGUI::String WidgetTypeName;
//! Constructor AlphaHitWindow(const CEGUI::String& type, const CEGUI::String& name); //! Destructor ~AlphaHitWindow();
// overridden from Window base class bool isHit(const CEGUI::Vector2& position, const bool allow_disabled = false) const;
protected:
//! handler to copy rendered data to a memory buffer bool renderingEndedHandler(const CEGUI::EventArgs& args); // overridden from Window base class bool testClassName_impl(const CEGUI::String& class_name) const;
//! Pointer to buffer holding the render data CEGUI::uint32* d_hitTestBuffer; //! Size of the hit test buffer (i.e. it's capacity) size_t d_hitBufferCapacity; //! Dimensions in pixels of the data in the hit test buffer CEGUI::Size d_hitBufferSize; //! whether data in hit test buffer is inverted. bool d_hitBufferInverted;
};
Class Implementation
//----------------------------------------------------------------------------//
const CEGUI::String AlphaHitWindow::WidgetTypeName("AlphaHitWindow");
//----------------------------------------------------------------------------// AlphaHitWindow::AlphaHitWindow(const CEGUI::String& type, const CEGUI::String& name) :
CEGUI::PushButton(type, name), d_hitTestBuffer(0), d_hitBufferCapacity(0)
{
// always use this since we want to sample the pre-composed imagery when we // do hit testing and this requires we have texture backing. setUsingAutoRenderingSurface(true);
// here we subscribe an event which will grab a copy of the rendered content // each time it is rendered, so we can easily sample it without needing to // read texture content for every mouse move / hit test. CEGUI::RenderingSurface* rs = getRenderingSurface(); if (rs) rs->subscribeEvent(CEGUI::RenderingSurface::EventRenderQueueEnded, CEGUI::Event::Subscriber(&AlphaHitWindow::renderingEndedHandler, this));
}
//----------------------------------------------------------------------------// AlphaHitWindow::~AlphaHitWindow() {
delete[] d_hitTestBuffer;
}
//----------------------------------------------------------------------------// bool AlphaHitWindow::isHit(const CEGUI::Vector2& position, const bool allow_disabled) const {
// still do the rect test, since we only want to do the detailed test // if absolutely neccessary. if (!CEGUI::PushButton::isHit(position, allow_disabled)) return false;
// if buffer is not allocated, just hit against area rect, so return true if (!d_hitTestBuffer) return true;
const CEGUI::Vector2 wpos(CEGUI::CoordConverter::screenToWindow(*this, position)); const size_t idx = (d_hitBufferInverted ? d_hitBufferSize.d_height - wpos.d_y : wpos.d_y) * d_hitBufferSize.d_width + wpos.d_x; return (d_hitTestBuffer[idx] >> 24) > 0;
}
//----------------------------------------------------------------------------// bool AlphaHitWindow::renderingEndedHandler(const CEGUI::EventArgs& args) {
if (static_cast<const CEGUI::RenderQueueEventArgs&>(args).queueID != CEGUI::RQ_BASE) return false;
// rendering surface needs to exist and needs to be texture backed CEGUI::RenderingSurface* const rs = getRenderingSurface(); if (!rs || !rs->isRenderingWindow()) return false;
CEGUI::TextureTarget& tt = static_cast<CEGUI::RenderingWindow* const>(rs)->getTextureTarget();
CEGUI::Texture& texture = tt.getTexture(); const CEGUI::Size tex_sz(texture.getSize()); const size_t reqd_capacity = static_cast<int>(tex_sz.d_width) * static_cast<int>(tex_sz.d_height);
// see if we need to reallocate buffer: if (reqd_capacity > d_hitBufferCapacity) { delete[] d_hitTestBuffer; d_hitTestBuffer = 0; d_hitBufferCapacity = 0; }
// allocate buffer to hold data if it's not already allocated if (!d_hitTestBuffer) { d_hitTestBuffer = new CEGUI::uint32[reqd_capacity]; d_hitBufferCapacity = reqd_capacity; }
// save details about what will be in the buffer d_hitBufferInverted = tt.isRenderingInverted(); d_hitBufferSize = tex_sz;
// grab a copy of the data. texture.saveToMemory(d_hitTestBuffer);
return true;
}
//----------------------------------------------------------------------------// bool AlphaHitWindow::testClassName_impl(const CEGUI::String& class_name) const {
return (class_name == "AlphaHitWindow")|| CEGUI::PushButton::testClassName_impl(class_name);
}
//----------------------------------------------------------------------------//
Crazyeddie 19:31, 14 June 2010 (UTC)