Rectangular selections with Qt and OpenSceneGraph
Tags: programming, software
In a previous post on this topic, I already talked about
how to integrate Qt and OpenSceneGraph in a thread-safe manner. In the following post, I will
briefly explain how to select objects in a 3D scene using a rectangular selection area. I want to
achieve the following: The user shall be able to draw a rectangle on top of the 3D scene content
that is shown in a viewer widget. All drawables that are intersected by said rectangle shall be
collected for further processing. Sounds rather easy—and it even will turn out to be, thanks
to Qt and osgUtil
.
The following code snippets refer to the QtOSG
project which was
developed in the previously-referenced post. The code is still released under the “MIT Licence”
and you are still most welcome to use it.
Why osgManipulator
is not the solution
If you peruse the documentation of OpenSceneGraph, you might stumble over the osgManipulator
library. Initially, I considered it to potentially solve this problem. After some fiddling with it,
I can say that this is patently not the truth. osgManipulator
seems to be unmaintained at the
moment,
and the developers of OpenSceneGraph seem to struggle with it themselves.
Besides, osgManipulator
is meant to offer a way for manipulating objects within a scene, by
placing a selector in said scene. I merely want to draw a rectangular area and find intersections.
QPainter
to the rescue
It then occurred to me that I do not have to draw anything into the existing scene. It will be perfectly sufficient if the rectangle is drawn by Qt. I only need to extract its coordinates to calculate intersections (by projecting the rectangle into scene coordinates). I envisioned the following process for performing a selection:
- Upon a
mousePressEvent()
, the mouse position (in window coordinates) is stored as the start and end point of the rectangle - During each
mouseMoveEvent()
, as long as the selection is active (i.e. the left mouse button is being pressed), the current mouse position is used as the end point of the rectangle - Upon the final
mouseReleaseEvent()
, the selection rectangle is projected into the scene and intersections with all visible objects are calculated (see below)
The rectangle itself is painted during the paintEvent()
of the OSG widget. Assuming that we
already stored and updated the coordinates during the events described above, we have the following
drawing code:
void OSGWidget::paintEvent( QPaintEvent* /* paintEvent */ )
{
QPainter painter( this );
painter.setRenderHint( QPainter::Antialiasing );
this->paintGL();
if( selectionActive_ && !selectionFinished_ )
{
painter.setPen( Qt::black );
painter.setBrush( Qt::transparent );
painter.drawRect( makeRectangle( selectionStart_, selectionEnd_ ) );
}
painter.end();
}
The makeRectangle()
uses the start and end coordinates of the selection rectangle and calculates
how to draw the corresponding QRect
:
QRect makeRectangle( const QPoint& first, const QPoint& second )
{
if( second.x() >= first.x() && second.y() >= first.y() )
return QRect( first, second );
else if( second.x() < first.x() && second.y() >= first.y() )
return QRect( QPoint( second.x(), first.y() ), QPoint( first.x(), second.y() ) );
else if( second.x() < first.x() && second.y() < first.y() )
return QRect( second, first );
else if( second.x() >= first.x() && second.y() < first.y() )
return QRect( QPoint( first.x(), second.y() ), QPoint( second.x(), first.y() ) );
return QRect();
}
The mousePressEvent()
, the mouseMoveEvent()
, and the mouseReleaseEvent()
only require minor
additions, as well:
void OSGWidget::mousePressEvent( QMouseEvent* event )
{
if( selectionActive_ && event->button() == Qt::LeftButton )
{
selectionStart_ = event->pos();
selectionEnd_ = selectionStart_; // Deletes the old selection
selectionFinished_ = false; // As long as this is set, the rectangle will be drawn
}
else
{
// Normal processing
}
}
void OSGWidget::mouseMoveEvent( QMouseEvent* event )
{
// Note that we have to check the buttons mask in order to see whether the
// left button has been pressed. A call to `button()` will only result in
// `Qt::NoButton` for mouse move events.
if( selectionActive_ && event->buttons() & Qt::LeftButton )
{
selectionEnd_ = event->pos();
// Ensures that new paint events are created while the user moves the
// mouse.
this->update();
}
else
{
// Normal processing
}
}
void OSGWidget::mouseReleaseEvent( QMouseEvent* event )
{
if( selectionActive_ && event->button() == Qt::LeftButton )
{
selectionEnd_ = event->pos();
selectionFinished_ = true; // Will force the painter to stop drawing the
// selection rectangle
this->processSelection();
}
else
{
// Normal processing
}
}
Calculating intersections
So far, we have draw a selection rectangle on top of a viewer widget. We now need to process the
selection by projecting the rectangle, which is in window coordinates, into the scene, which is
not. Fortunately, we can use the marvellous osgUtil
library here (I am not sugar-coating this,
the library is extremely useful and versatile). We first need to transform Qt’s window
coordinates, which assume that the origin of the window is in the upper-left corner, into OSG’s
window coordinates, in which the origin is the lower-left corner. We then use the polytope
intersector, an auxiliary class in the osgUtil
library that permits intersecting scenes with
arbitrary polytopes. Our polytope will be very simple—it consists of the (transformed)
coordinates of the selection rectangle. Here is the nice part: osgUtil
will project the rectangle
into the scene for us, so there is no need for further coordinate transformations!
The rest of the function merely shows how to use the intersection visitor class and extract the names of each intersected object. By setting up the polytope intersector properly, each object is ensured to be intersected at most once. This seemed the most useful behaviour for me.
void OSGWidget::processSelection()
{
QRect selectionRectangle = makeRectangle( selectionStart_, selectionEnd_ );
int widgetHeight = this->height();
double xMin = selectionRectangle.left();
double xMax = selectionRectangle.right();
double yMin = widgetHeight - selectionRectangle.bottom();
double yMax = widgetHeight - selectionRectangle.top();
osgUtil::PolytopeIntersector* polytopeIntersector
= new osgUtil::PolytopeIntersector( osgUtil::PolytopeIntersector::WINDOW,
xMin, yMin,
xMax, yMax );
// This limits the amount of intersections that are reported by the
// polytope intersector. Using this setting, a single drawable will
// appear at most once while calculating intersections. This is the
// preferred and expected behaviour.
polytopeIntersector->setIntersectionLimit( osgUtil::Intersector::LIMIT_ONE_PER_DRAWABLE );
osgUtil::IntersectionVisitor iv( polytopeIntersector );
for( unsigned int viewIndex = 0; viewIndex < viewer_->getNumViews(); viewIndex++ )
{
osgViewer::View* view = viewer_->getView( viewIndex );
if( !view )
throw std::runtime_error( "Unable to obtain valid view for selection processing" );
osg::Camera* camera = view->getCamera();
if( !camera )
throw std::runtime_error( "Unable to obtain valid camera for selection processing" );
camera->accept( iv );
if( !polytopeIntersector->containsIntersections() )
continue;
auto intersections = polytopeIntersector->getIntersections();
for( auto&& intersection : intersections )
qDebug() << "Selected a drawable:" << QString::fromStdString( intersection.drawable->getName() );
}
}
Note that the polytope intersector is applied to all views of the viewer widget. In a composite
widget with multiple views, this results in objects being reported multiple times. The easiest way
to avoid this is to use an std::set
that stores the intersected objects.
Code, code, code
The code is available in a git repository. I welcome any pull requests.