|
This blog entry is a sequel to a previous blog entry.
3D hit testing is accomplished in WPF using the VisualTreeHelper.HitTest method. This method has a couple of different overloads, one of which is specifically designed for hit testing 3-D objects. 3D hit testing essentially works the same way that 2D hit testing works, except you essentially project a ray from the camera into the scene, and if a 2D hit test occurs somewhere along that ray, it triggers a hit test result.
When you're hit testing in WPF, you're using an asynchronous callback mechanism. This essentially means that you can respond to multiple hits on the same click without having to do any additional plumbing. For example, if your mouse click actually clicks into two overlapping 3D models, the hit test callback method will fire twice.
Conventional 3D meshes don't provide their own "Click" event handler, which would, of course, make conventional hit testing unnecessary. So we have to trap the Mousedown event from the 3D viewport itself. This will trap and hit test all clicks anywhere in the 3D scene without interfering with any 2D visual elements in the window at the time.
The following code shows the Mousedown event handler for my 3D View Port:
private void ViewPort_MouseDown(object sender,
System.Windows.Input.MouseButtonEventArgs args)
{
Point mouseposition = args.GetPosition(mainViewport);
Point3D testpoint3D = new Point3D(mouseposition.X, mouseposition.Y, 0);
Vector3D testdirection = new Vector3D(mouseposition.X, mouseposition.Y, 10);
PointHitTestParameters pointparams = new PointHitTestParameters(mouseposition);
RayHitTestParameters rayparams = new RayHitTestParameters(testpoint3D, testdirection);
//test for a result in the Viewport3D
VisualTreeHelper.HitTest(mainViewport, null, HTResult, pointparams);
}
This code creates a ray from the mouse click position and passes that information as a RayHitTestParameters object to the HitTest method along with the callback method, HTResult. This code is actually in one of the few Feb CTP MSDN examples that actually works properly.
The next thing you need to do is write the callback function that will be invoked every time the HitTest method finds results:
public HitTestResultBehavior HTResult(System.Windows.Media.HitTestResult rawresult)
{
RayHitTestResult rayResult = rawresult as RayHitTestResult;
if (rayResult != null)
{
RayMeshGeometry3DHitTestResult rayMeshResult = rayResult as RayMeshGeometry3DHitTestResult;
if (rayMeshResult != null)
{
GeometryModel3D hitgeo = rayMeshResult.ModelHit as GeometryModel3D;
// do something with the model hit, like change
// colors or start an animation storyboard
}
}
return HitTestResultBehavior.Continue;
}
If you want to stop after the first hit result, then you can simply not return HitTestResultBehavior.Continue, you can instead return HitTestResultBehavior.Stop.
What I ran into here is that this is great if all I want to do is visually affect the model that was clicked. So, if I want to make the model start spinning after you click it, or get brighter, or fire off a complex animation where the object moves around the scene - that's great. But what I'm after is a direct manipulation paradigm, where I'm clicking cubes (or whatever mesh you like) to get at data in a compelling blend of 2- and 3-D interface.
In the example I'm building, each of these cubes is actually going to represent a WCF service hosted by some other user on some computer. When you click the cube, the 3D client will interrogate that service and provide a 2D panel that contains the information that resulted from interrogating the service.
If I am going to interrogate a service in response to clicking a 3D mesh, I need to be able to differentiate between two meshes. A few samples I've seen have compared the geometry... but in this case - I could potentially have a scene with 20 cubes , all of which have the same geometry, and the only thing separating them is the Translate transform that placed them in different positions.
What I really want is IDs or Keys associated with each mesh. Avalon (WPF) doesn't provide for a means for me to access the name or key of a mesh from within the hit testing code. So, what I'm going to do is create a Generics-based Dictionary that contains as its key the mesh itself. The value corresponding to each key will be the unique identifier for the mesh.
First, declare the Dictionary that will contain the mesh IDs:
private Dictionary<GeometryModel3D, string> meshes = new Dictionary<GeometryModel3D,string>();
The following code is how I was dynamically adding cubes to the scene. It shows how I was creating a dictionary with unique identifiers for each mesh contained within it:
// create a couple extra cubes
for (int i = 1; i < 10; i++)
{
ModelVisual3D extraCube = new ModelVisual3D();
Model3DGroup modelGroup = new Model3DGroup();
GeometryModel3D model3d = new GeometryModel3D();
model3d.Geometry = (MeshGeometry3D)this.Resources["UnitCube"];
DiffuseMaterial graySide = new DiffuseMaterial(new SolidColorBrush(Colors.White));
model3d.Material = graySide;
// Add mesh and identifier..
meshes.Add(model3d, "Cube " + i.ToString());
modelGroup.Children.Add(model3d);
extraCube.Content = modelGroup;
mainViewport.Children.Add(extraCube);
// give the cube a different (cascading) location in the scene
extraCube.Transform = new TranslateTransform3D(-4 * i, -4 * i, -4 * i);
}
In the next blog entry, I'll show you how to dynamically show a 2D panel that contains data associated with the 3D mesh that you clicked, that can be obtained from a WIndows Communication Foundation service hosted remotely. What I'm aiming for is direct manipulation and some real 'out of the box' (pun intended) data visualization. I'll also show off one of the cooler BitmapEffects that you can slap onto a panel, the DropShadowEffect.
- The .NET Addict
I know this is extremely past due, I'm using the RC1 tools, but when I use
GetHashCode and compare it to instantiated objects, it seems to work for
me.