Wednesday, 27 June 2012

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.


Posted Mon, Oct 15 2007 1:30 PM by Charles Humphrey | Add post to favorites | Add blog to favorites
Filed under: Randomchaos 3D Engine, Ray Picking, XNA 1.0 [Edit Tags]



Comments

Tim wrote re: Engine Design - Ray Picking
on Thu, Jun 25 2009 12:46 AM
What is the camera viewport?
Charles Humphrey wrote re: Engine Design - Ray Picking
on Thu, Jun 25 2009 11:11 AM
OK, if you look in the camera class in the sample I have a Viewport object, this is a reference to the GraphicsDevice.Viewport.
Here we use the Viewport to get the near and far range (the end points) of the Ray. Subtracting the near from the far gives us a direction for the Ray and the near point is where we are firing the Ray from.

No comments:

Post a Comment