Google
 

Tuesday, May 29, 2007

Engine Design - Ray Picking


Here I will show how I manage to pick 3D objects from the scene using the mouse. Now as you can imagine this can lead to a few issues, one of which is converting a mouses 2D coordinates to 3D coordinates that can be used in the scene.

Before I give you the code (again) I will first try and explain how this method works. I use a very similar thing in my terrain object to pick vert's from the terrain in my PickTerrain method which you will see in my next post.

My Scene picking class has just one public method "GetClickedModel" it is passed the mouse 2D coords and the Scene object we are trying to pick from. First thing we need to do is convert the mouse coordinates to some kind of 3D representation, I do this with a Ray object, this Ray object is given a starting point and a direction. So basically where your ray starts and where you are pointing it. To get the ray's starting point I use the following code.


Vector3 nearSource = camera.Viewport.Unproject(new Vector3(mousecoords.X,mousecoords.Y, camera.Viewport.MinDepth), camera.Projection, camera.View, Matrix.Identity);


This gives us the 3D position of the 2D mouse in the scene. We now need to get the direction the ray is pointing in, to do this we first need to know the furthest point our ray will point at this is archived with this line of code.


Vector3 farSource = camera.Viewport.Unproject(new Vector3(mousecoords.X, mousecoords.Y, camera.Viewport.MaxDepth), camera.Projection, camera.View, Matrix.Identity);



And now to get the direction of the ray we simply do this


Vector3 direction = farSource - nearSource;



We can now construct our ray using the nearSource and the direction Vector3 variables like this.


myRay = new Ray(nearSource, direction);



Now we have the ray constructed we need to find out what objects it hits on it's way from the nearSource to the farSource points in the scene. To do this I wrote a method called RayIntersects that returns the object that is hit by the ray. This method simply goes through the objects in the scene and checks if the ray intersects them. This is not enough as you may end up picking a object that is behind your intended target, to manage this the intersected objects are placed in an array list along with there distance from the nearSource I then iterate through the array finding the object with the shortest distance and return that as the selected object.

Here is the picking class in full:

   public class RCScenePicker
   {
       private static Ray myRay;
       private static ArrayList RayHitList;

       public static Ray RayPicker
       {
           get { return myRay; }
           set { myRay = value; }
       }
       private RCScenePicker() { }

       public static RCObject GetClickedModel(Point mousecoords,RCSceneGraph Scene)
       {
           RCCamera camera = RCCameraManager.ActiveCamera;

           Vector3 nearSource = camera.Viewport.Unproject(new Vector3(mousecoords.X, mousecoords.Y, camera.Viewport.MinDepth), camera.Projection, camera.View, Matrix.Identity);
           Vector3 farSource = camera.Viewport.Unproject(new Vector3(mousecoords.X, mousecoords.Y, camera.Viewport.MaxDepth), camera.Projection, camera.View, Matrix.Identity);
           Vector3 direction = farSource - nearSource;

           direction.Normalize();

           myRay = new Ray(nearSource, direction);
           return RayIntersects(Scene);
                       
       }

       private static RCObject RayIntersects(RCSceneGraph Scene)
       {
           RCObject retVal = null;
           RayHitList = new ArrayList();
           
           for (int n = 0; n < Scene.SceneRoot.Nodes.Count; n++)
           {
               RCObject obj = (RCObject)(((RCObjectNode)Scene.SceneRoot.Nodes[n]).Object);
               BoundingBox bb = obj.ObjectsBoundingBox;
               Nullable<float> distance;
               myRay.Intersects(ref bb,out distance);                
               if (distance != null)
               {
                   object[] thisObj = new object[2];
                   thisObj[0] = (int)distance;
                   thisObj[1] = obj;

                   RayHitList.Add(thisObj);
               }
           }

           // Now get the object nearest the camera.
           object[] lastDist = new object[] {(int)RCCameraManager.ActiveCamera.Viewport.MaxDepth,new RCObject("tmp")};

           for (int o = 0; o < RayHitList.Count; o++)
           {
               if((int)((object[])RayHitList[o])[0] < (int)lastDist[0])
                   lastDist = ((object[])RayHitList[o]);
           }

           if(RayHitList.Count > 0)
               retVal = (RCObject)lastDist[1];

           
           return retVal;
       }        
   }



Once an object is picked I draw it's bounding box to show it has been picked.

Before Pick



After Pick




As I have a picker in my terrain object that will be the focus of my next post.

4 comments:

  1. Hi Charles,

    Looks like you got some good work here. I too am working on an engine of sorts for my 3D Tower Defence project over at www.artificial-studios.co.uk

    I have some questions about your addions and changes to the Hazy Mind engine. If you dont mind discussing it abit more shoot me an email at: mike.cann at gmail dot com.

    cheers :D

    Mike

    ReplyDelete
  2. Thanks mike,

    Your Tower Defense project looks good, especially the editor, look forward to seeing the finished article, and thanks for the credit :P

    ReplyDelete
  3. Could you please also add your ray class, especially the intersects function with bounding box on the ray class. Thanks;

    ReplyDelete
  4. The Ray class is part of the XNA framework. It's not my class sorry.

    ReplyDelete

Note: Only a member of this blog may post a comment.