8: Mouse Handling

This entry is part 8 of a 12-part series on WPF 3D.

3D and the Mouse

Many 3D applications want to offer interactive capabilities using the mouse:

  • Click on an object in the 3D scene to "select" so that further actions can be applied that specific item.
  • Click and drag to rotate, zoom or pan.
  • Show coordinates or other information when hovering over specific objects.

The mouse lives in a 2D world.  The objects in your scene are in a 3D world.  You need a few tricks to bridge the gap.

The Need for an Overlay

WPF makes it fairly simply to get mouse notifications for any Visual.  It is therefore tempting to just add mouse event handlers to the Viewport3D object, somewhat like this:

<Viewport3D
   
MouseUp="OnViewportMouseUp"
   
MouseDown="OnViewportMouseDown"
   
MouseMove="OnViewportMouseMove" >

The problem with this approach is that the Viewport3D will only receive mouse events when the mouse is actually hovering over one of the rendered triangles in the scene.  When the mouse is over the background, no events are sent. 

For some situations (such as picking a 3D object, or a 3D scene which fills the entire screen), this is fine.  For others (such as interactive rotation of a model centered on the screen), this may not be not so good.

The usual fix for this problem is to overlay another element directly on top of the Viewport3D.  The overlay must be transparent to allow the Viewport3D to be completely visible.  The mouse event handlers should be placed on the overlay instead, as the Viewport3D will receive no mouse events at all.  Because the overlay and the Viewport3D have the same 2D coordinate system, all the math works out just fine.  In XAML, this approach might look something like this:

<Grid >
  <
Viewport3D Grid.Row="0" Grid.Column="0" >
  (model stuff goes here)
  </
Viewport3D>
  <
Canvas Grid.Row="0" Grid.Column="0"
   
Background="Transparent"
   
MouseUp="OnViewportMouseUp"
   
MouseDown="OnViewportMouseDown"
   
MouseMove="OnViewportMouseMove"  />
</
Grid>

From 2D to 3D

Inside the mouse handlers, you want to take the 2D coordinates of the mouse click and find the 3D object where that click occurred.  WPF 3D makes part of this work simple with VisualTreeHelper.HitTest().  After that, the handling will depend greatly on your application.

For example, in Sawdust, every 3D scene is generated on the fly, constructed from a data structure which was built from the solid modeling code.  When the user clicks on the 3D model, a specific piece of wood is selected so that further operations can be applied directly to it.  My code for OnViewportMouseDown() looks something like this:

/// <summary>
///
On mouse click, select the specific board
/// where the click happened.
/// </summary>
///
<param name="sender"></param>
///
<param name="args"></param>
public void OnViewportMouseDown(
    object sender,
    System.Windows.Input.MouseEventArgs args)
{
    if (vstuff.models == null)
    {
        return;
    }

    if (
        Keyboard.IsKeyDown(Key.LeftCtrl)
        || Keyboard.IsKeyDown(Key.RightCtrl)
        )
    {
        // extending the selection. 
        // don't unselect all first.
    }
    else
    {
        UnselectAll();
    }

    RayMeshGeometry3DHitTestResult rayMeshResult =
        (RayMeshGeometry3DHitTestResult)
          VisualTreeHelper.HitTest(myVP, args.GetPosition(myVP));
   
    if (rayMeshResult != null)
    {
        PartialModel found = null;
        foreach (PartialModel pm in vstuff.models)
        {
            if (pm.mesh == rayMeshResult.MeshHit)
            {
                found = pm;
                break;
            }
        }

        if (found != null)
        {
            if (IsSelected(found.bag.solid))
            {
                Unselect(found.bag.solid);
            }
            else
            {
                Select(found.bag.solid);
            }
        }
    }
}

I included all the code, even though most of this routine is specific to my application.  In particular, vstuff, PartialModel, IsSelect(), Select(), Unselect(), and UnselectAll() are all elements of Sawdust.  Your app will have different things, depending on the manner and data structures used to generate your 3D scenes.

The key is the RayMeshGeometry3DHitTestresult returned by VisualTreeHelper.HitTest().  The members of this object contain plenty of helpful information.  In my particular case, I use the MeshHit member to search my data structures so I can find the original information used to construct that piece of the 3D model.  My life would be a bit easier if MeshGeometry3D had something like a Tag member, so I could just tuck a reference to my PartialModel object right there for easy retrieval.