Perform hit testing in a 3D program that uses WPF, XAML, and C#

hit testing

Many three-dimensional programs need to perform hit testing to determine when the user clicks on something. This example draws two interlocked tetrahedrons inside a cage. When you click on one of the objects, the program displays information about the object you hit.

The program takes action when it receives a MouseDown event. Unfortunately the Viewport3D control does not fire that event unless the user clicks on an object in the three-dimensional data. If the user clicks on the background, the event isn’t raised.

You can work around this problem by placing the Viewport3D inside some other control such as a Border and then catching the MouseDown event provided by that control.

Even that solution has a catch. The Border doesn’t doesn’t raise its MouseDown event if it has a transparent background. (Presumably that’s the same problem with the Viewport3D in the first place. It has a transparent background, so mouse clicks on it don’t register.) You can solve the new problem by giving the Border a non-transparent background such as Black or White.

The following XAML code shows how this program defines its Border and Viewport3D controls.

<Grid>
    <Border Grid.Row="0" Grid.Column="0" Background="White"
        MouseDown="MainViewport_MouseDown">
        <Viewport3D Grid.Row="0" Grid.Column="0"
            Name="MainViewport" />
    </Border>
</Grid>

When the user clicks on the Viewport3D, the program needs to figure out which object you clicked. To do that, it stores the objects it creates in the Models dictionary defined by the following code.

// A record of the 3D models we build.
private Dictionary<Model3D, string> Models =
    new Dictionary<Model3D, string>();

When it creates its models, the program adds each of them to the dictionary, as in the following code.

Models.Add(model1, "Green model");

Later, when the user clicks on the Border, the following code performs the hit test.

// See what was clicked.
private void MainViewport_MouseDown(object sender,
    MouseButtonEventArgs e)
{
    // Get the mouse's position relative to the viewport.
    Point mouse_pos = e.GetPosition(MainViewport);

    // Perform the hit test.
    HitTestResult result =
        VisualTreeHelper.HitTest(MainViewport, mouse_pos);

    // Display information about the hit.
    RayMeshGeometry3DHitTestResult mesh_result =
        result as RayMeshGeometry3DHitTestResult;
    if (mesh_result == null) this.Title = "";
    else
    {
        // Display the name of the model.
        this.Title = Models[mesh_result.ModelHit];

        // Display more detail about the hit.
        Console.WriteLine("Distance: " +
            mesh_result.DistanceToRayOrigin);
        Console.WriteLine("Point hit: (" +
            mesh_result.PointHit.ToString() + ")");

        Console.WriteLine("Triangle:");
        MeshGeometry3D mesh = mesh_result.MeshHit;
        Console.WriteLine("    (" +
            mesh.Positions[mesh_result.VertexIndex1].ToString()
                + ")");
        Console.WriteLine("    (" +
            mesh.Positions[mesh_result.VertexIndex2].ToString()
                + ")");
        Console.WriteLine("    (" +
            mesh.Positions[mesh_result.VertexIndex3].ToString()
                + ")");
    }
}

This code gets the mouse’s position relative to the viewport. It then calls the VisualTreeHelper class’s static HitTest method to see what (if anything) was hit inside the MainViewport control. (VisualTreeHelper is in the System.Windows.Media namespace.)

The program then converts the result into a RayMeshGeometry3DHitTestResult object. If that object is null, the user clicked on the background instead of something in the model. In that case, the program clears the window’s Title.

If the click did hit something, the program displays information about the hit. It uses the mesh result’s ModelHit property as an index into the Models dictionary. The dictionary returns the hit model’s name, and the program displays that name in the form’s title bar.

Next the program displays more information about the hit in the Console window. It displays:

  • The distance from the viewing origin to the point of intersection with the mesh that was hit.
  • The coordinates of the point of intersection.
  • The vertices of the mesh triangle that contains the point of intersection.

You could use the extra triangle information to figure out what part of the mesh was hit. For example, if the mesh represents a three-dimensional car, you might be able to use the extra information to determine what part of the car was clicked.

There’s still a limit to what you could do about it, however. All of the triangles in a MeshGeometry3D object share the same material. That means you can’t change the material used by part of the mesh without changing the entire mesh’s material. You could change the texture coordinates for the hit triangle’s vertices to make it display some other part of the material, but doing anything fancy would take some work. (I may try that as a later example.)

(The VisualTreeHelper class has two other overloaded versions of the HitTest method that don’t return immediately. Instead they invoke a callback method when they find hits. These versions add a couple of capabilities that the version used in this example doesn’t. First, they take a filter method that lets you filter the kinds of hits you want to see. Second, they continue invoking the callback to report more hits until you tell them to stop. For example, if the clicked point lies above a stack of objects, the callback is invoked for each of those objects.)


Download Example   Follow me on Twitter   RSS feed   Donate




This entry was posted in algorithms, drawing, geometry, graphics, mathematics, wpf, XAML and tagged , , , , , , , , , , , , , , , , , , , , , . Bookmark the permalink.

9 Responses to Perform hit testing in a 3D program that uses WPF, XAML, and C#

  1. Let the user select and deselect objects in a 3D program that uses WPF, XAML, and C#

    The example Perform hit testing in a 3D program that uses WPF, XAML, and C# shows how to tell when the user clicks an object in a 3D WPF program. This example uses similar techniques to let the user select and deselect objects. This program contains several refinements over previous 3D WPF examples. In particular, its static MeshExtensions class includes more methods for adding simple shapes to a mesh. This example uses the AddBox extension method to add a bunch of cubes to the 3D model. That method simply creates the triangles needed to build a box. …

  2. Pingback: Let the user select and deselect 3D objects

  3. Ardahan says:

    I apply these codes in my application but in the Models[mesh_result.ModelHit] section , 3D model in dictionary and mesh_result.ModelHit doesn’t matching and it gives error.
    Can you help me please?

    • RodStephens says:

      I can’t tell you what’s happening in your program. I would download the example and look at how it works to try to see if you left something out of your program.

      Also notice how the example checks whether nothing is hit. And note that the program fills the Models list with the names of the models. If you don’t do that, then it wouldn’t be able to display the model’s name.

      • Ardahan says:

        I apply all the code above. In my case the error is just happening when getting name of model from dictionary using mesh_result.ModelHits key.
        //in this code it gives error
        this.Title = Models[mesh_result.ModelHit];
        Error: An unhandled exception of type ‘System.Collections.Generic.KeyNotFoundException’ occurred in mscorlib.dll

        Additional information: The given key was not present in the dictionary.

        So, comparing two same models doesn’t work.

        If you give any contact information I can send the project folder. If you want I can add all the .cs code here.

  4. RodStephens says:

    Yeah, it’s saying you’re clicking on a model that isn’t in the dictionary. You should add it.

    You can email me the project (delete the obj and bin directories first) at RodStephens@CSharpHelper.com and I’ll take a look.

    • Ardahan says:

      Thank you for helping; I am send an email to you.

      • RodStephens says:

        For anyone who reads this thread in the future, the problem was that the program was using a Helix Toolkit importer to load a model. The result was a Graphics3DGroup and the program was treating it like a Graphics3D. That works (Graphics3DGroup is a subclass) but sometimes the hit testing was returning one of the group’s children not the group itself, so that’s what the program needed to look for.

        The revised program loads the model and then loops through the resulting group’s Children collection, adding them to the dictionary.

  5. Yun says:

    Great article and blogs. I’m not a professional programmer, but need to make some 3D model importer and slicer for my project. This blog helps a lot!

    Thanks!

Leave a Reply

Your email address will not be published. Required fields are marked *