Rectangular selections with Qt and OpenSceneGraph

Tags: programming, software

Published on
« Previous post: A brief critique of the singleton … — Next post: Simple object picking with … »

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.