Performing custom rendering at the right time

From CEGUI Wiki - Crazy Eddie's GUI System (Open Source)
Jump to: navigation, search

Written for CEGUI 0.7


Works with versions 0.7.x (obsolete)

What is this tutorial?

This is an advanced tutorial showing one of the ways in which it is possible to hook into the CEGUI rendering process to draw custom imagery using the underlying rendering API directly.

The actual code that is shown here demonstrates a window that has a link to another window, and that link is indicated graphically by way of a line with two arrows. This was inspired by this forum post, but I think that it is important to state that the technique can also be used for totally different purposes too.

The example code is using OpenGL, although similar things are possible with the other APIs too.

Overview

It is probably worthwhile discussing why some 'advanced technique' is required in order to perform certain types of custom rendering in the first place.

All of the things rendered internally by CEGUI are cached and queued for later drawing. This means that when a window draws itself, it has not at that stage actually drawn anything, but rather it has queued some instructions for what to draw when the actual rendering is performed later on.

The biggest issue with this arises when you need custom rendering that relates to a given window, as opposed to custom rendering done before or after the CEGUI::System::renderGUI call. How does one know precisely when to do this custom rendering such that it is layered correctly in regards to the other CEGUI based rendering? You can't render directly in response to the window events that signal a window's rendering has started or ended, because these events relate to CEGUI based rendering which can be – and is – cached.

What you actually need to do is hook into the part of the process that queues things for rendering, and queue your own custom object to perform whatever rendering you need. The objects that get queued are based on the CEGUI::GeometryBuffer interface, the key thing to know is that the only function of that interface used during the final rendering process is the GeometryBuffer::draw function. This means that it's possible to implement a custom GeometryBuffer whose draw function renders directly to the active CEGUI::RenderingSurface (such as the screen / back buffer), rather than drawing some buffered geometry like the regular implementation for the CEGUI::Renderer that you use.

The basic scenario is that we need a subclass of some CEGUI::Window type so we can override the Window::drawSelf function in order to queue our custom GeometryBuffer implementation. And, of course, a custom GeometryBuffer implementation that will perform whatever rendering we desire. These will now be discussed.

Implementation

LinkedWindow class

For this example, we will develop a LinkedWindow class that is based on the existing FrameWindow type. The class declaration looks like this:

class LinkedWindow : public CEGUI::FrameWindow
{
public:
    //! 'Namespace' string used for global events on this class.
    static const CEGUI::String EventNamespace;
    //! String holding the type name of this widget.
    static const CEGUI::String WidgetTypeName;
 
    //! set window that will be the target of our link line.
    void setTarget(const CEGUI::Window* target);
    //! return pointer that is our current link target.
    const CEGUI::Window* getTarget() const;
 
    LinkedWindow(const CEGUI::String& type, const CEGUI::String& name);
    ~LinkedWindow();
 
protected:
    // overridden from base class.
    void drawSelf(const CEGUI::RenderingContext& ctx);
    bool testClassName_impl(const CEGUI::String& class_name) const;
 
    //! pointer to a GeometryBuffer that will render our link line.
    CEGUI::GeometryBuffer* d_linksGeometry;
    //! pointer to the target window of our link line.  May be zero.
    const CEGUI::Window* d_target;
};

The key thing included here is the drawSelf override, the rest is either boilerplate or related to the specific implementation of this demo. Perhaps it's worthwhile to point out the pointer to a GeometryBuffer though, since this will actually point to an instance of our custom implementation of GeometryBuffer and is initialised in the LinkedWindow constructor.

The Window::drawSelf override

Since - at least for this demo - we want our custom rendering to appear between windows, we need to ensure that the RenderingSurface we use is not a surface local to the window. Here we see the code tests to see if the surface we are given in the context is a rendering window, and if it is, we choose to use the owner of that RenderingWindow instead. This is intended to avoid the AutoRenderingSurface of a FrameWindow, though in the demo we switch this off anyway. Depending on your own specific circumstances the code below may not be entirely robust (but it will suffice for most normal uses).

    RenderingSurface* surface;
 
    if (ctx.surface->isRenderingWindow())
        surface = &static_cast<RenderingWindow*>(ctx.surface)->getOwner();
    else
        surface = ctx.surface;

The next part is key, and is in fact, single most important line in the tutorial. This is where we queue our custom GeometryBuffer instance to the surface we chose above.

    surface->addGeometryBuffer(ctx.queue, *d_linksGeometry);\

Finally we call the base class implementation so that the regular FrameWindow rendering is now performed (actually, it's queued - just like our custom GeometryBuffer above!)

    FrameWindow::drawSelf(ctx);

LinkGeometryBuffer class

The class declaration for LinkGeometryBuffer is as follows:

class LinkGeometryBuffer : public CEGUI::GeometryBuffer
{
public:
    LinkGeometryBuffer(LinkedWindow* owner);
 
    // required interface functions for base class.
    void draw() const;
    void setTranslation(const CEGUI::Vector3& v) {}
    void setRotation(const CEGUI::Vector3& r) {}
    void setPivot(const CEGUI::Vector3& p) {}
    void setClippingRegion(const CEGUI::Rect& region) {}
    void appendVertex(const CEGUI::Vertex& vertex) {}
    void appendGeometry(const CEGUI::Vertex* const vbuff, CEGUI::uint vertex_count) {}
    void setActiveTexture(CEGUI::Texture* texture) {}
    void reset() {}
    CEGUI::Texture* getActiveTexture() const {return 0;}
    CEGUI::uint getVertexCount() const {return 0;}
    CEGUI::uint getBatchCount() const {return 0;}
    void setRenderEffect(CEGUI::RenderEffect* effect) {}
    CEGUI::RenderEffect* getRenderEffect() {return 0;}
 
protected:
    //! helper that uses GL calls to render the link line
    static void drawLink(const CEGUI::Vector2& source, const CEGUI::Vector2& dest,
                         float source_size, float dest_size);
    //! helper that uses GL calls to render an arrow.
    static void drawArrowRight(float x, float y, float sz);
 
    //! LinkedWindow that created and owns the GeometryBuffer.
    LinkedWindow* d_owner;
};

As you can see it's pretty much just the required functions as specified by the CEGUI::GeometryBuffer interface, along with a couple of helper functions which are specific to this demo.

The GeometryBuffer::draw override

Below is the code for the custom drawing function. There is little to discuss, since this is just some very basic code and OpenGL calls. We basically determine the source and destination locations which will be the end points of the link line, and call a helper function that makes the GL calls to draw. It's probably worth highlighting the calls that save and restore the GL attributes, we do this because CEGUI will not expect its states to get messed up mid-way through rendering! We don't save the model-view matrix, since this is reset by each GeometryBuffer anyway.

    if (!d_owner->getTarget())
        return;
 
    const Rect src_area(d_owner->getUnclippedOuterRect());
    const Rect dst_area(d_owner->getTarget()->getUnclippedOuterRect());
 
    const Vector2 source(
        src_area.d_right,
        src_area.d_top + ((src_area.d_bottom - src_area.d_top) / 2));
 
    const Vector2 dest(
        dst_area.d_left,
        dst_area.d_top + ((dst_area.d_bottom - dst_area.d_top) / 2));
 
    glPushClientAttrib(GL_CLIENT_ALL_ATTRIB_BITS);
    glPushAttrib(GL_ALL_ATTRIB_BITS);
 
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
 
    glBindTexture(GL_TEXTURE_2D, 0);
    glDisable(GL_SCISSOR_TEST);
 
    glColor3f(0.1, 1, 0.2);
    drawLink(source, dest, 15, 9);
 
    glPopAttrib();
    glPopClientAttrib();

And that really is all there is to it!

Registering the new window type

The new window type is registered using the WindowFactoryManager and the templatised WindowFactory, like so:

WindowFactoryManager::getSingleton().addFactory<TplWindowFactory<LinkedWindow> >();

For this example, the falagard mapping that makes this usable is specified in code, but it's just as easily added to your XML scheme file:

WindowFactoryManager::getSingleton().
    addFalagardWindowMapping("TaharezLook/LinkedWindow",
                             "LinkedWindow",
                             "TaharezLook/FrameWindow",
                             "Falagard/FrameWindow");

Conclusion

Here we have seen how to hook into the CEGUI rendering process by queueing a custom GeometryBuffer implementation along with (or instead of) the regular GeometryBuffer queued by the Window's drawSelf function. This allows any custom rendering to be performed at the correct time so it appears at the same layered position as a window in the output.

Example Code

LinkedWindow Class Declaration (LinkedWindow.h)

#ifndef _LINKED_WINDOW_H
#define _LINKED_WINDOW_H
 
#include <elements/CEGUIFrameWindow.h>
 
/*!
\brief
    Custom subclass of FrameWindow.  The main purpose of this is so we can
    override the Window::drawSelf function in order to add an instance of
    our custom GeometryBuffer at an appropriate place - which will then
    render the link lines when it's draw function is called.
*/
class LinkedWindow : public CEGUI::FrameWindow
{
public:
    //! 'Namespace' string used for global events on this class.
	static const CEGUI::String EventNamespace;
    //! String holding the type name of this widget.
    static const CEGUI::String WidgetTypeName;
 
    //! set window that will be the target of our link line.
    void setTarget(const CEGUI::Window* target);
    //! return pointer that is our current link target.
    const CEGUI::Window* getTarget() const;
 
    LinkedWindow(const CEGUI::String& type, const CEGUI::String& name);
    ~LinkedWindow();
 
protected:
    // overridden from base class.
    void drawSelf(const CEGUI::RenderingContext& ctx);
    bool testClassName_impl(const CEGUI::String& class_name) const;
 
    //! pointer to a GeometryBuffer that will render our link line.
    CEGUI::GeometryBuffer* d_linksGeometry;
    //! pointer to the target window of our link line.  May be zero.
    const CEGUI::Window* d_target;
};
 
#endif

LinkedWindow Class Implementation (LinkedWindow.cpp)

#include <CEGUIRenderingWindow.h>
#include <CEGUIRenderingContext.h>
#include "LinkedWindow.h"
#include "LinkGeometryBuffer.h"
 
using namespace CEGUI;
 
//----------------------------------------------------------------------------//
const String LinkedWindow::EventNamespace("LinkedWindow");
const String LinkedWindow::WidgetTypeName("LinkedWindow");
 
//----------------------------------------------------------------------------//
LinkedWindow::LinkedWindow(const CEGUI::String& type, const CEGUI::String& name) :
    FrameWindow(type, name),
    d_linksGeometry(new LinkGeometryBuffer(this)),
    d_target(0)
{
}
 
//----------------------------------------------------------------------------//
void LinkedWindow::setTarget(const CEGUI::Window* target)
{
    d_target = target;
}
 
//----------------------------------------------------------------------------//
const Window* LinkedWindow::getTarget() const
{
    return d_target;
}
 
//----------------------------------------------------------------------------//
LinkedWindow::~LinkedWindow()
{
    delete d_linksGeometry;
}
 
//----------------------------------------------------------------------------//
void LinkedWindow::drawSelf(const RenderingContext& ctx)
{
    RenderingSurface* surface;
 
    if (ctx.surface->isRenderingWindow())
        surface = &static_cast<RenderingWindow*>(ctx.surface)->getOwner();
    else
        surface = ctx.surface;
 
    surface->addGeometryBuffer(ctx.queue, *d_linksGeometry); 
 
    FrameWindow::drawSelf(ctx);
}
 
//----------------------------------------------------------------------------//
bool LinkedWindow::testClassName_impl(const CEGUI::String& class_name) const
{
    if (class_name == LinkedWindow::WidgetTypeName)
        return true;
 
    return FrameWindow::testClassName_impl(class_name);
}
 
//----------------------------------------------------------------------------//

LinkGeometryBuffer Class Declaration (LinkGeometryBuffer.h)

#ifndef _LINK_GEOMETRY_BUFFER_H
#define _LINK_GEOMETRY_BUFFER_H
 
#include <CEGUIGeometryBuffer.h>
 
class LinkedWindow;
 
 
/*!
/brief
    Custom GeometryBuffer used for drawing links from a LinkedWindow to its
    target window.
 
\note
    The only part of the regular GeometryBuffer interface we will implement is
    the GeometryBuffer::draw function, the rest is stubbed out.  Note also that
    we don't actually 'buffer' anything here, but do direct drawing within the
    draw function.
*/
class LinkGeometryBuffer : public CEGUI::GeometryBuffer
{
public:
    LinkGeometryBuffer(LinkedWindow* owner);
 
    // required interface functions for base class.
    void draw() const;
    void setTranslation(const CEGUI::Vector3& v) {}
    void setRotation(const CEGUI::Vector3& r) {}
    void setPivot(const CEGUI::Vector3& p) {}
    void setClippingRegion(const CEGUI::Rect& region) {}
    void appendVertex(const CEGUI::Vertex& vertex) {}
    void appendGeometry(const CEGUI::Vertex* const vbuff, CEGUI::uint vertex_count) {}
    void setActiveTexture(CEGUI::Texture* texture) {}
    void reset() {}
    CEGUI::Texture* getActiveTexture() const {return 0;}
    CEGUI::uint getVertexCount() const {return 0;}
    CEGUI::uint getBatchCount() const {return 0;}
    void setRenderEffect(CEGUI::RenderEffect* effect) {}
    CEGUI::RenderEffect* getRenderEffect() {return 0;}
 
protected:
    //! helper that uses GL calls to render the link line
    static void drawLink(const CEGUI::Vector2& source, const CEGUI::Vector2& dest,
                         float source_size, float dest_size);
    //! helper that uses GL calls to render an arrow.
    static void drawArrowRight(float x, float y, float sz);
 
    //! LinkedWindow that created and owns the GeometryBuffer.
    LinkedWindow* d_owner;
};
 
#endif

LinkGeometryBuffer Class Implementation (LinkGeometryBuffer.cpp)

#include "LinkGeometryBuffer.h"
#include "LinkedWindow.h"
#include <GL/gl.h>
 
using namespace CEGUI;
 
//----------------------------------------------------------------------------//
LinkGeometryBuffer::LinkGeometryBuffer(LinkedWindow* owner) :
    d_owner(owner)
{
}
 
//----------------------------------------------------------------------------//
void LinkGeometryBuffer::draw() const
{
    if (!d_owner->getTarget())
        return;
 
    const Rect src_area(d_owner->getUnclippedOuterRect());
    const Rect dst_area(d_owner->getTarget()->getUnclippedOuterRect());
 
    const Vector2 source(
        src_area.d_right,
        src_area.d_top + ((src_area.d_bottom - src_area.d_top) / 2));
 
    const Vector2 dest(
        dst_area.d_left,
        dst_area.d_top + ((dst_area.d_bottom - dst_area.d_top) / 2));
 
    glPushClientAttrib(GL_CLIENT_ALL_ATTRIB_BITS);
    glPushAttrib(GL_ALL_ATTRIB_BITS);
 
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
 
    glBindTexture(GL_TEXTURE_2D, 0);
    glDisable(GL_SCISSOR_TEST);
 
    glColor3f(0.1, 1, 0.2);
    drawLink(source, dest, 15, 9);
 
    glPopAttrib();
    glPopClientAttrib();
}
 
//----------------------------------------------------------------------------//
void LinkGeometryBuffer::drawLink(const Vector2& source, const Vector2& dest,
                                  float source_size, float dest_size)
{
    drawArrowRight(source.d_x, source.d_y, source_size);
 
    glBegin(GL_LINES);
        glVertex2f(source.d_x + (source_size - 1), source.d_y);
        glVertex2f(dest.d_x - (dest_size - 1), dest.d_y);
    glEnd();
 
    drawArrowRight(dest.d_x - dest_size, dest.d_y, dest_size);
}
 
//----------------------------------------------------------------------------//
void LinkGeometryBuffer::drawArrowRight(float x, float y, float sz)
{
    glBegin(GL_TRIANGLES);
        glVertex2f(x, y - sz / 2);
        glVertex2f(x, y + sz / 2);
        glVertex2f(x + sz, y);
    glEnd();
}
 
//----------------------------------------------------------------------------//

Demo App code using GLUT (main.cpp)

#include <GL/glut.h>
#include <CEGUI.h>
#include <RendererModules/OpenGL/CEGUIOpenGLRenderer.h>
#include "LinkedWindow.h"
 
using namespace CEGUI;
 
//----------------------------------------------------------------------------//
void drawFunc()
{
    glClearColor(0, 0, 0, 1);
    glClear(GL_COLOR_BUFFER_BIT);
 
    System::getSingleton().renderGUI();
 
    glutSwapBuffers();
    glutPostRedisplay();
}
 
//----------------------------------------------------------------------------//
void moveFunc(int x, int y)
{
    System::getSingleton().injectMousePosition(x, y);
}
 
//----------------------------------------------------------------------------//
void mouseButtonFunc(int button, int state, int, int)
{
    MouseButton cegui_btn;
 
    switch (button)
    {
    case GLUT_MIDDLE_BUTTON:
        cegui_btn = MiddleButton;
        break;
 
    case GLUT_RIGHT_BUTTON:
        cegui_btn = RightButton;
        break;
 
    default:
        cegui_btn = LeftButton;
    }
 
    if (state == GLUT_UP)
        System::getSingleton().injectMouseButtonUp(cegui_btn);
    else
        System::getSingleton().injectMouseButtonDown(cegui_btn);
}
 
//----------------------------------------------------------------------------//
void initialiseGLUT(int argc, char* argv[])
{
    glutInit(&argc, argv);
    glutInitWindowSize(800, 600);
    glutInitDisplayMode(GLUT_DOUBLE );
    glutCreateWindow("CEGUI Demo App");
    glutDisplayFunc(drawFunc);
    glutMotionFunc(moveFunc);
    glutPassiveMotionFunc(moveFunc);
    glutMouseFunc(mouseButtonFunc);
}
 
//----------------------------------------------------------------------------//
void initialiseCEGUIResources()
{
    DefaultResourceProvider* rp = static_cast<DefaultResourceProvider*>(
        System::getSingleton().getResourceProvider());
    rp->setResourceGroupDirectory("schemes", "/usr/local/share/CEGUI/schemes");
    rp->setResourceGroupDirectory("imagesets", "/usr/local/share/CEGUI/imagesets");
    rp->setResourceGroupDirectory("fonts", "/usr/local/share/CEGUI/fonts");
    rp->setResourceGroupDirectory("looknfeel", "/usr/local/share/CEGUI/looknfeel");
 
    Scheme::setDefaultResourceGroup("schemes");
    Imageset::setDefaultResourceGroup("imagesets");
    Font::setDefaultResourceGroup("fonts");
    WidgetLookManager::setDefaultResourceGroup("looknfeel");
}
 
//----------------------------------------------------------------------------//
void registerCustomWindow()
{
    // register the new window type
    WindowFactoryManager::getSingleton().
        addFactory<TplWindowFactory<LinkedWindow> >();
 
    // make a mapping that uses the existing TL/FrameWindow renderer.
    WindowFactoryManager::getSingleton().
        addFalagardWindowMapping("TaharezLook/LinkedWindow",
                                 "LinkedWindow",
                                 "TaharezLook/FrameWindow",
                                 "Falagard/FrameWindow");
}
 
//----------------------------------------------------------------------------//
bool checkBoxHandler(const EventArgs& a)
{
    const WindowEventArgs& wa = static_cast<const WindowEventArgs&>(a);
    Checkbox* cb = static_cast<Checkbox*>(wa.window);
 
    const String lnkWindowName(cb->getUserString("LinkedWindow"));
 
    if (!lnkWindowName.empty())
    {
        Window* lw = WindowManager::getSingleton().getWindow(lnkWindowName);
        lw->setVisible(cb->isSelected());
    }
}
 
//----------------------------------------------------------------------------//
LinkedWindow* createLinkedWindow(const String& name,
                                 float x, float y, float w, float h,
                                 const String& title,
                                 bool initially_hidden)
{
 
    LinkedWindow* wnd = static_cast<LinkedWindow*>(
        WindowManager::getSingleton().
            createWindow("TaharezLook/LinkedWindow", name));
 
    wnd->setPosition(UVector2(UDim(x, 0), UDim(y, 0)));
    wnd->setSize(UVector2(UDim(w, 0), UDim(h, 0)));
    wnd->setUsingAutoRenderingSurface(false);
    wnd->setText(title);
    wnd->setVisible(!initially_hidden);
 
    return wnd;
}
 
//----------------------------------------------------------------------------//
Checkbox* createCheckbox(float x, float y, const String& label)
{
    Checkbox* wnd = static_cast<Checkbox*>(WindowManager::getSingleton().
        createWindow("TaharezLook/Checkbox"));
    wnd->setPosition(UVector2(UDim(x, 0), UDim(y, 0)));
    wnd->setSize(UVector2(UDim(1.0, 0), UDim(0.2, 0)));
    wnd->setText(label);
    wnd->subscribeEvent(Checkbox::EventCheckStateChanged, checkBoxHandler);
 
    return wnd;
}
 
//----------------------------------------------------------------------------//
int main(int argc, char* argv[])
{
    initialiseGLUT(argc, argv);
 
    System::setDefaultXMLParserName("ExpatParser");
    OpenGLRenderer::bootstrapSystem();
    initialiseCEGUIResources();
 
    SchemeManager::getSingleton().create("TaharezLook.scheme");
    System::getSingleton().setDefaultMouseCursor("TaharezLook", "MouseArrow");
 
    registerCustomWindow();
 
    WindowManager& winMgr(WindowManager::getSingleton());
    Window* root = winMgr.createWindow("DefaultWindow");
    System::getSingleton().setGUISheet(root);
 
    LinkedWindow* main = createLinkedWindow("main", 0.4, 0.2, 0.25, 0.25, "Main Window", false);
    root->addChildWindow(main);
 
    LinkedWindow* src1 = createLinkedWindow("src1", 0.1, 0.1, 0.2, 0.15, "Src Wnd 1", true);
    root->addChildWindow(src1);
 
    LinkedWindow* src2 = createLinkedWindow("src2", 0.1, 0.3, 0.2, 0.15, "Src Wnd 2", true);
    root->addChildWindow(src2);
 
    LinkedWindow* dst1 = createLinkedWindow("dst1", 0.7, 0.1, 0.25, 0.25, "Dst Wnd 1", false);
    root->addChildWindow(dst1);
 
    Window* tg1 = createCheckbox(0, 0, "Link Target 1");
    main->addChildWindow(tg1);
    src1->setTarget(tg1);
    tg1->setUserString("LinkedWindow", "src1");
 
    Window* tg2 = createCheckbox(0, 0.2, "Link Target 2");
    main->addChildWindow(tg2);
    tg2->setUserString("LinkedWindow", "");
 
    Window* tg3 = createCheckbox(0, 0.4, "Link Target 3");
    main->addChildWindow(tg3);
    src2->setTarget(tg3);
    tg3->setUserString("LinkedWindow", "src2");
 
    Window* button = winMgr.createWindow("TaharezLook/Button");
    button->setPosition(UVector2(UDim(0.0, 0), UDim(0.3, 0)));
    button->setSize(UVector2(UDim(1.0, 0), UDim(0.2, 0)));
    button->setText("Engage!");
    button->setUserString("LinkedWindow", "main");
    dst1->addChildWindow(button);
    main->setTarget(button);
 
    glutMainLoop();
 
    OpenGLRenderer::destroySystem();
 
    return 0;
}
 
//----------------------------------------------------------------------------//