Interaction in Voreen

Introduction


This tutorial is going to focus on how interactive elements in the canvas (where the three-dimensional data is displayed) are handled. We will discuss our subject using Voreen’s trackball navigation as an abstract approach and its clipping planes as a detailed example.

Table Of Contents


Canvas in Voreen


All canvas types supported by Voreen provide an interface to call back mouse and keyboard signals. This feature allows creating functions, which use both devices in combination. For example, by pressing the left mouse button and moving the mouse, Voreen’s datasets rotate; when pressing additional shift, the dataset moves in mouse direction.
Usually, Voreen renders its content on a tgt::QtCanvas, which converts the qt-events into tgt::Events and broadcasts them to a tgt::EventListener registered in the canvas. It is possible to connect more than one listener to a canvas and process each event by multiple listeners, because an event is forwarded along the queue until a listener calls the event’s accept() method.
Also, Voreen supports other canvases than QtCanvas: The tiny graphics toolbox provides event mapping for GlutCanvas, SDLCanvas, wxCanvas.

tgt::EventHandler


Each implementation of GLCanvas comprehends a tgt::EventHandler managing a list of EventListeners connected to the canvas. This can be done by adding them to the tail of the queue (using addListenerToBack() ) or to the front (using addListenerToFront() ). Currently, this is the only way to influence the order of listeners.
Here is an example how to connect a trackball navigation trackNavi_ with canvas_:

TrackballNavigation*  trackNavi_ = new TrackballNavigation(trackball_, true, 0.05f, 15.f);
canvas_->getEventHandler()->addListenerToBack(trackNavi_);

tgt::EventListener


Now, first take a look at the abstract tgt::EventListener class, which must be overridden by your own listener class. Therefore, you have got to implement the following methods:

  • mousePressEvent (MouseEvent *e)
  • mouseReleaseEvent (MouseEvent *e)
  • mouseMoveEvent (MouseEvent *e)
  • mouseDoubleClickEvent (MouseEvent *e)
  • wheelEvent (MouseEvent *e)
  • timerEvent (TimeEvent *e)
  • keyEvent (KeyEvent *e)

The trackball navigation example – An abstract explanation of the interaction concept


The most obvious listener in Voreen is voreen::TrackballNavigation, which is predominantly responsible for mouse interaction. It receives tgt-events and construes them as data set modification like rotation, zoom and movement. Most of these functionalities have been transferred to tgt::Trackball. Hence, the trackball navigation forwards events to a trackball marked out by a pointer and holds a member variable of a tgt::Camera. Then, the camera is manipulated by the trackball class, so that the view changes due to our mouse moves as we expect from previous experience. Finally, the renderer retrieves the modelview matrix from the camera for its purpose.
To receive events, an instance of your new class must be added to the canvas. How this is done can be seen in the tgt::EventHandler example.

The clipping widget


In this section, we will take a detailed look at the clipping planes used in Voreen’s fancyraycaster to see how to implement an event listener. This is an example for a listener, which fetches events before they get to the trackball navigation. As we will see, ClippingWidget decides whether a control arrow is clicked by the mouse and accepts the event in the positive case. Otherwise, ClippingWidget ignores it and the event handler passes it to the next listener.
The clipping planes are initialized in the rendererfactory. The code below shows how the ClippingWidget ‘clippWidget’ is added as a geometry renderer to the geometry processor ‘geom’, which already owns a pointer to a bounding box. Further ‘clippWidget’ is connected as a mouse listener to the painter via Voreen’s message system (see message tutorial). Finally, the geometry processor is added to the rendering pipeline.


renderfactory.cpp
   ...
   GeometryProcessor* geom = new GeometryProcessor(camera_, tc_, pg);
   ...   
   geom->addGeomRenderer(bbox);
   ClippingWidget* clippWidget = new ClippingWidget(camera_, tc_, pg);
   geom->addGeomRenderer(clippWidget);
   MsgDistr.postMessage(
     new TemplateMessage<tgt::EventListener*>(VoreenPainter::addMouseListener_,
										            (tgt::EventListener*)clippWidget));        
   ...
   rootPipline->addRenderer(geom);

Given that the ClippingWidget renders some geometry to the screen, it is obviously a geometry renderer. For this reason it is derived by tgt::EventListener and GeometryRenderer. So the methods listed in geomclippingwidget.h have to be implemented.


geomclippingwidget.h
#include "tgt/event/eventlistener.h"

namespace voreen {

class ClippingWidget : public GeometryRenderer, public tgt::EventListener {
public:
    ClippingWidget(tgt::Camera* camera, TextureContainer* tc=0, ProxyGeometry* pg=0);
    ~ClippingWidget();

    void processMessage(Message* msg, const Identifier& dest = Identifier::all);
    void render();

    virtual void mousePressEvent(tgt::MouseEvent *e);
    virtual void mouseMoveEvent(tgt::MouseEvent *e);
    virtual void mouseReleaseEvent(tgt::MouseEvent *e);
    virtual void mouseDoubleClickEvent(tgt::MouseEvent* e);

protected:

private:
   int isClicked(int x, int y);
   void paintBoundingGeometry(float trans_x,float trans_y,float trans_z, int direction);

   ....

   };
} // namespace voreen

We will first discuss the public methods derived from GeometryRenderer:

  • processMessage() reacts to identifiers for setting the slice color or resetting the clipping planes; it also calls the respective methods of the widget or directly implements the functionality.

  • render() draws the whole geometry like wire frame, planes (when they are active) and the control elements. There is some code, which is fundamental for subsequent considerations regarding the identification of the controls to be activated.

First an ID Manager is initialized in the widget’s constructor, which will calculate whether a control element is clicked.

...

IDManager id1;
id1.addNewPickObj("clipp.x-contr1");
id1.addNewPickObj("clipp.x-contr2");
id1.addNewPickObj("clipp.y-contr1");
id1.addNewPickObj("clipp.y-contr2");
id1.addNewPickObj("clipp.z-contr1");
id1.addNewPickObj("clipp.z-contr2");

...

  • Line 1: define an IDManager id1

  • Line 2- 7: add identifiers for the control elements

The IDManager renders the object tagged by an identifier into a texture using particular RGB values, so that he is able to decide if the object is placed under the mouse’s two dimensional coordinates by analyzing the texture.
Hence, in the render()-method, we have to draw each slider twice by calling paintBoundingGeometry(). The first pass paints the arrow on the screen and the second pass commits the geometry to the IDManager.
The clipping widget’s geometry is a cube, so that there are exactly six clipping planes represented by consecutive numeration, starting with “1”. From now on, this paper will only cover the first case and leave out the analogue ones to avoid long code snippets and repetition.

...
//X-Control1 rendering
glColor3f(1.0f,0.0f,0.0f);
paintBoundingGeometry(-volSize.x + (x_control1 * 2 * volSize.x),volSize.y,volSize.z , 4);
id1.startBufferRendering("clipp.x-contr1");
paintBoundingGeometry(-volSize.x + (x_control1 * 2 * volSize.x),volSize.y,volSize.z , 4);
id1.stopBufferRendering();
glLoadIdentity();
tgt::multMatrix(camera_->getModelViewMatrix());
paintBoundingGeometry(-volSize.x + (x_control1 * 2 * volSize.x),-volSize.y,-volSize.z,3);
id1.startBufferRendering("clipp.x-contr1");
paintBoundingGeometry(-volSize.x + (x_control1 * 2 * volSize.x),-volSize.y,-volSize.z,3);
id1.stopBufferRendering();
glLoadIdentity();
tgt::multMatrix(camera_->getModelViewMatrix());
if(bx_control1){
   renderQuad(1);
}
glEnable(GL_LIGHTING);

//X-Control2 rendering
....

Before we start to concentrate on event handling, let’s first take a look at the helper classes:

  • isClicked(int, int)uses IDManager::isClicked() to calculate which of the six control elements is hit by the mouse. It delivers the number of the control clicked, or “0” if none. Therefore, its input is a two dimensional vector containing the mouse coordinates routed in the manner described below.

        if(id1.isClicked("clipp.x-contr1", x, tc_->getSize().y - y)) return 1;

  • paintBoundingGeometry() as mentioned above: paints the arrows of the bounding geometry.

  • renderQuad(int number) renders the plane represented by number

  • setLightParams() sets the light parameters

  • saveMatrixStacks() / restoreMatrixStacks() takes care of the state of the OpenGL engine

mousePressEvent()

Now we have picked up all the trimmings to focus on pressing a mouse button as our first event. It is handled in mousePressEvent() by fetching a tgt::MouseEvent. So we receive two dimensional coordinates of the canvas position pointed out by the mouse click and analyse them to set the bool variables constituting the state of the control elements.

void ClippingWidget::mousePressEvent(tgt::MouseEvent *e)
{
   isClicked_ = isClicked(e->coord().x, e->coord().y);
   if (isClicked_) {

        e->accept();

        x_prev = e->coord().x;
     y_prev = e->coord().y;

        switch(isClicked_)
        {
        case 1 :
            bx_control1 = true;
            if( x_lock_->get() ) {
                bx_control2=true;
            }
            break;
	......
        case 6 :
            bz_control2=true;
            if( z_lock_->get() ) {
                bz_control1=true;
            }
            break;
        }
        MsgDistr.postMessage(new BoolMsg(VoreenPainter::switchCoarseness_, true), VoreenPainter::visibleViews_);
        MsgDistr.postMessage(new Message(VoreenPainter::repaint_), VoreenPainter::visibleViews_);
    }
    else e->ignore();
    if (isClicked_ && e->modifiers() & (tgt::Event::LSHIFT | tgt::Event::RSHIFT)){
        shift_lock_ = true;
        if(isClicked_ == 1 || isClicked_ == 2){
            x_lock_->set(!x_lock_->get());
            e->accept();
            return;
        }
        if(isClicked_ == 3 || isClicked_ == 4){
            y_lock_->set(!y_lock_->get());
            e->accept();
            return;
        }
        if(isClicked_ == 5 || isClicked_ == 6){
            z_lock_->set(!z_lock_->get());
            e->accept();
            return;
        }
    }

}
  • Lines 04-06: check if the mouse is above a control element. In positive case, we notice that the user wants to interact with our widget and accept the mousePressEvent.
  • Line 11-13: case differentiation on the six eventualities
  • Line 14-16: change corresponding bool values
  • Line 27: switch to coarseness mode to accelerate interactive rendering
  • Line 28: repaint the canvas
  • Line 30: when no arrow is focussed by the cursor, ignore mousePressEvent
  • Line 31: check global tgt event tgt::Event::xSHIFT using e->modifiers() for special functionalities. In case of using a shift button and the mouse, the opposite clipping planes are moved simultaneously and a line is shown illustrating the range between both planes

mouseDoubleClickEvent()

This method does not offer new concepts concerning event handling in Voreen. It treats the tgt::MouseEvent nearly in the same manner described in mousePressEvent(). The only difference is in its effect: A double click locks the opposite planes; from now on they will be moved as if a shift key is pressed.

mouseReleaseEvent()

Here the reaction to releasing a mouse button is implemented. In our case the bool variables representing the activation state of a control element are set to false.

void ClippingWidget::mouseReleaseEvent(tgt::MouseEvent *e)
{
   if(isClicked_){
     isClicked_ = 0;
        bx_control1=false;
        bx_control2=false;
        by_control1=false;
        by_control2=false;
        bz_control1=false;
        bz_control2=false;
        if(shift_lock_){
            x_lock_->set(false);
            y_lock_->set(false);
            z_lock_->set(false);
            shift_lock_ = false;
        }
   }
   e->ignore();
}
  • Line 03: if isClicked_ != 0 we have got at least one active slider and need to reset
  • Line 04-15: setting isClicked to “0” and all bool variables to false
  • Line 18: maybe there is another content, which needs to react to the release event. Hence we forward the event.

mouseMoveEvent()

Because this method is called, when the mouse is moved and its left button clicked (Hence mousePressEvent() is always processed before), here most parts of interaction programming take place. In principle the mouse coordinates are fetched as seen in mousePressEvent() and some calculations corresponding to the proxy geometry are made. Also the communication between the clipping widget and the proxy geometry occurs in mouseMoveEvent(). This is the last step to integrate our interactive widget into Voreen. It is done by Voreen’s messaging system, which is explained in another tutorial. So here are only the major code lines outpointed:

MsgDistr.postMessage(new IntMsg(ProxyGeometry::setRightClipPlane_,
					 static_cast<int>(100 - (x_control2*100))));

This message notifies the proxy geometry how to update the right clipping plane.

MsgDistr.postMessage(new Message(VoreenPainter::repaint_), VoreenPainter::visibleViews_);

This message provokes repainting of the canvas.

© VisCG WWU Münster