Title: Perform hit testing in a 3D program that uses WPF, XAML, and C#
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 the example to experiment with it and to see additional details.
|