Now that we have the ability to put objects in space and move around them, it would also be nice to be able to choose which object we are focussed on. One method of doing this would be to click on the object on the screen and have the camera refocus itself around that object. This method of choosing an object from the screen with the mouse is called picking. We will implement it in a 3 step process.
Step 1: Setting up the Framework for Picking
The first thing we need to do is have some input from the mouse to play with and see if an object in our scene was clicked on so we will make some general functions for each of the 3 steps we are going to take. The first part of picking is simply getting the mouse clicks and sending them on to our scene. This code will go in the demo code and should look something like this:
// In the Main method demo1.MouseDown += new MouseEventHandler(demo1_MouseDown); // New function below Main static void demo1_MouseDown(object sender, MouseEventArgs e) { demo1.MyScene.Pick(e, demo1.MyCamera, demo1.MyDevice); }
Step 2: Getting the Scene to Pick all of our Objects
Logically this leads us to writing the next part of the picking function, converting the 2D point into a 3D ray by unprojecting it using an inverse matrix we will create by taking a few settings from our camera and video card. Here is the logic and code to accomplish what turns out to be a slightly lengthy group of actions:
public bool Pick(MouseEventArgs mouseEvent, HMCamera myCamera, Device myDevice) { // Get most of our placeholders for information ready Point mouseLoc = mouseEvent.Location; bool pickFound = false; IntersectInformation intInf; Vector3 rayStart, rayDirection; Matrix proj = myCamera.Projection;
Now we will convert the mouse click location into a 3D position in our world using the screen width and some simple algebra to move the center of reference to the middle of the screen instead of the top left corner where the original mouse point is taken from.
// Convert the mouse point into a 3D point Vector3 v; v.X = (((2.0f * mouseLoc.X) / myDevice.PresentationParameters.BackBufferWidth) – 1) / proj.M11; v.Y = -(((2.0f * mouseLoc.Y) / myDevice.PresentationParameters.BackBufferHeight) – 1) / proj.M22; v.Z = 1.0f;
Now, using the inverse of the matrix we normally use to project our 3D points, we will unproject our point to get a ray that shoots into the screen from the point we clicked and store it in two vectors, one for the ray’s initial point, and one for its direction
// Get the inverse of the composite view and world matrix Matrix m = myCamera.View * myCamera.World; m.Invert(); // Transform the screen space pick ray into 3D space rayDirection.X = v.X * m.M11 + v.Y * m.M21 + v.Z * m.M31; rayDirection.Y = v.X * m.M12 + v.Y * m.M22 + v.Z * m.M32; rayDirection.Z = v.X * m.M13 + v.Y * m.M23 + v.Z * m.M33; rayStart.X = m.M41; rayStart.Y = m.M42; rayStart.Z = m.M43;
Now comes the fun part. We must loop through all of the objects in our scene to find out which ones were clicked on (some may be behind others) and save the distances from the camera viewport so we know that the closer object was the one clicked on the screen, to store which object was clicked on, I created an instance variable at the top of our HMScene class: public HMObject activeObject;
This will allow us to access the object outside of the scene and manipulate some of our camera variables to change whenever a new object is clicked on. Here is what the code looks like for looping through our objects and saving the closest one:
float minDistance = float.MaxValue; for(int i = 0; i < WorldObjects.Count; i++) { HMObject o = (HMObject)WorldObjects[i]; if(o.Pick(rayStart, rayDirection, out intInf) && intInf.Dist < minDistance) { pickFound = true; activeObject = o; } } return pickFound; }
Now that we have our scene class looping through and calling our objects’ Pick method, we should probably write that method. For now we will just write the code for a mesh since a lot of the intersection math is handled by DirectX, but we will add other checks later so we can reuse our pick functionality to check things like GUI windows or other types of objects that might be added later on.
Step 3: Checking if an Object was Picked
The method for finding out whether an object was hit by the ray is much simpler to implement because DirectX does a lot of this for us. All we have to do is convert the ray into the local coordinates of the model we are checking (that is, unproject the ray using the model’s scaling, rotation, and translation matrices) and have the built in Mesh.Intersect function tell us whether we have hit home or not. Here is what this looks like:
public override bool Pick(Vector3 rayStart, Vector3 rayDirection, out IntersectInformation intInf) { // Convert ray to model space Matrix World = Matrix.Scaling(myScaling) * Matrix.RotationYawPitchRoll(myRotation.Y, myRotation.X, myRotation.Z) * Matrix.Translation(myPosition); Matrix inverseWorld = Matrix.Invert(World); Vector3 localStart = Vector3.TransformCoordinate(rayStart, inverseWorld); Vector3 localDirection = Vector3.TransformNormal(rayDirection, inverseWorld); localDirection.Normalize(); // Check for mesh intersection return myMesh.Intersect(localStart, localDirection, out intInf); }
That’s it! Now our engine will successfully test any mesh based objects that we load and report back with a true or false whether that object was clicked on or not. It will also set the clicked on object to be active in the scene so we can access it and play with other things (like camera focus) once we know what was clicked, which is how I will finish up this tutorial
Step 4: Putting our Picking to Good Use
Just so we can get an idea of how to put picking together with our current camera and see what we can do with just the few simple things we have learned and implemented so far, I decided to add a few meshes to the scene and allow the user to click a mesh and have the camera instantly focus on that mesh. To do this, we will only need to add a bit of code to our Demo class that will handle changing the camera’s radius so it is adjusted to the new object’s distance as well. We will also need to add a function in our camera to let us set a new focus point. Here is what the camera methods will look like:
public void SetTarget(Vector3 newTarget) { target = newTarget; } public void SetRadius(float newRadius){ radius = newRadius; } // Also remember to either make the location of HMObjects and the HMCamera public // or to make a property in each of these that will give you public access to them public Vector3 Position { get { return position; } }
The new code in the demo will look like this:
static void demo1_MouseDown(object sender, MouseEventArgs e) { if(demo1.MyScene.Pick(e, demo1.MyCamera, demo1.MyDevice)) { Vector3 objPosition = demo1.MyScene.activeObject.Position; Vector3 camPosition = demo1.MyCamera.Position; demo1.MyCamera.SetTarget(objPosition); demo1.MyCamera.SetRadius(Vector3.Length(objPosition – camPosition)); } } … HMMesh mesh1 = new HMMesh(“tiger.x”, new Vector3(0,0,0), new Vector3(), new Vector3(1, 1, 1), demo1.MyDevice); HMMesh mesh2 = new HMMesh(“tiger.x”, new Vector3(-5,0,5), new Vector3(), new Vector3(1, 1, 1), demo1.MyDevice); HMMesh mesh3 = new HMMesh(“tiger.x”, new Vector3(5,0,5), new Vector3(), new Vector3(1, 1, 1), demo1.MyDevice); demo1.MyScene.AddObject(mesh1); demo1.MyScene.AddObject(mesh2); demo1.MyScene.AddObject(mesh3);
Now if we run the demo we are able to click on any of our 3 tigers and have the camera fly around any of them. The motion of the camera is a bit choppy when changing targets, which is because of the way we update the camera’s position each frame. We will make this much smoother in the future when we change the camera’s motion with a scripting system that will be implemented in a later tutorial.