How to: Detect Whether a User Clicked a 3D Object

This example demonstrates how to check whether the mouse is positioned over a 3D object by creating a ray starting at the camera's near clipping plane and ending at its far clipping plane.

Note

This example applies only to Windows development. The Mouse and MouseState objects are not supported on Xbox 360.

To check whether the mouse is positioned over a 3D object

  1. Get the current state of the mouse by using GetState.

    MouseState mouseState = Mouse.GetState();
    
  2. Get the current screen coordinates of the mouse from X and Y.

    int mouseX = mouseState.X;
    int mouseY = mouseState.Y;
    
  3. Using Viewport.Unproject, determine points in world space on the near and far clip plane. For the point on the near plane, pass a source vector with x and y set to the mouse position and z set to 0. For the point on the far plane, pass a source vector with x and y set to the mouse position and z set to 1. For both points, pass Unproject the current projection matrix and view matrix and a translation matrix for the point (0,0,0).

    Vector3 nearsource = new Vector3( (float)mouseX, (float)mouseY, 0f );
    Vector3 farsource = new Vector3( (float)mouseX, (float)mouseY, 1f );
    
    Matrix world = Matrix.CreateTranslation( 0, 0, 0 );
    
    Vector3 nearPoint = graphics.GraphicsDevice.Viewport.Unproject( nearsource, proj, view, world );
    
    Vector3 farPoint = graphics.GraphicsDevice.Viewport.Unproject( farsource, proj, view, world );
    
  4. Create a Ray starting at the near point and pointing toward the far point.

    // Create a ray from the near clip plane to the far clip plane.
    Vector3 direction = farPoint - nearPoint;
    direction.Normalize();
    Ray pickRay = new Ray( nearPoint, direction );
    
  5. Loop through the objects in the world using Intersects to check whether the Ray intersects each object. If the Ray does intersect an object, check whether it is the closest object intersected so far. If it is, store the object and the distance at which it was intersected, replacing any previously stored object. When the objects have been completely looped through, the last object stored will be the closest object underneath the area the user clicked.

//Game1.cs
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Storage;
using Microsoft.Xna.Framework.Content;

class Game1 : Microsoft.Xna.Framework.Game
{
    public struct WorldObject
    {
        public Vector3 position;
        public Model model;
        public Texture2D texture2D;
    }

    WorldObject[] worldObjects;

    Matrix view;
    Matrix proj;

    ContentManager contentManager;
    GraphicsDeviceManager graphics;
    Model sphere;
    Texture2D sphereTexture;
    Texture2D selectedTexture;

    // Set avatar position and rotation variables.
    Vector3 avatarPosition = new Vector3( 0, 0, -8 );
    Vector3 avatarHeadOffset = new Vector3( 0, 1, 1 );
    float avatarYaw;


    // Set the direction the camera points without rotation.
    Vector3 cameraReference = new Vector3( 0, 0, 10 );

    Vector3 thirdPersonReference = new Vector3( 0, 25, -25 );

    // Set rates in world units per 1/60th second (the default fixed-step interval).
    float rotationSpeed = 1f / 60f;
    float forwardSpeed = 25f / 60f;

    // Set information about the game window.
    static int screenWidth = 800;
    static int screenHeight = 600;
    static float aspectRatio = (float)screenWidth / (float)screenHeight;

    // Set field of view of the camera in radians (pi/4 is 45 degrees).
    static float FOV = MathHelper.PiOver4;

    // Set z-values of the near and far clipping planes.
    static float nearClip = 5.0f;
    static float farClip = 1000.0f;

    // Set the camera state, avatar's center, first-person, third-person.
    int cameraState;
    bool cameraStateKeyDown;

    void CheckMouseClick()
    {
        MouseState mouseState = Mouse.GetState();
        if (mouseState.LeftButton == ButtonState.Pressed)
        {

            Ray pickRay = GetPickRay();


            //Nullable<float> result = pickRay.Intersects(triangleBB);
            int selectedIndex = -1;
            float selectedDistance = float.MaxValue;
            for (int i = 0; i < worldObjects.Length; i++)
            {
                worldObjects[i].texture2D = sphereTexture;
                BoundingSphere sphere = worldObjects[i].model.Meshes[0].BoundingSphere;
                sphere.Center = worldObjects[i].position;
                Nullable<float> result = pickRay.Intersects( sphere );
                if (result.HasValue == true)
                {
                    if (result.Value < selectedDistance)
                    {
                        selectedIndex = i;
                        selectedDistance = result.Value;
                    }
                }
            }
            if (selectedIndex > -1)
            {
                worldObjects[selectedIndex].texture2D = selectedTexture;
            }

        }
    }


    Ray GetPickRay()
    {
        MouseState mouseState = Mouse.GetState();

        int mouseX = mouseState.X;
        int mouseY = mouseState.Y;

        Vector3 nearsource = new Vector3( (float)mouseX, (float)mouseY, 0f );
        Vector3 farsource = new Vector3( (float)mouseX, (float)mouseY, 1f );

        Matrix world = Matrix.CreateTranslation( 0, 0, 0 );

        Vector3 nearPoint = graphics.GraphicsDevice.Viewport.Unproject( nearsource, proj, view, world );

        Vector3 farPoint = graphics.GraphicsDevice.Viewport.Unproject( farsource, proj, view, world );

        // Create a ray from the near clip plane to the far clip plane.
        Vector3 direction = farPoint - nearPoint;
        direction.Normalize();
        Ray pickRay = new Ray( nearPoint, direction );

        return pickRay;
    }

    // Update the position and direction of the avatar.
    void UpdateAvatarPosition()
    {
        KeyboardState keyboardState = Keyboard.GetState();

        if (keyboardState.IsKeyDown( Keys.Left ))
        {
            // Rotate left.
            avatarYaw += rotationSpeed;
        }
        if (keyboardState.IsKeyDown( Keys.Right ))
        {
            // Rotate right.
            avatarYaw -= rotationSpeed;
        }
        if (keyboardState.IsKeyDown( Keys.Up ))
        {
            Matrix forwardMovement = Matrix.CreateRotationY( avatarYaw );
            Vector3 v = new Vector3( 0, 0, forwardSpeed );
            v = Vector3.Transform( v, forwardMovement );
            avatarPosition.Z += v.Z;
            avatarPosition.X += v.X;
        }
        if (keyboardState.IsKeyDown( Keys.Down ))
        {
            Matrix forwardMovement = Matrix.CreateRotationY( avatarYaw );
            Vector3 v = new Vector3( 0, 0, -forwardSpeed );
            v = Vector3.Transform( v, forwardMovement );
            avatarPosition.Z += v.Z;
            avatarPosition.X += v.X;
        }
    }
    void GetCurrentCamera()
    {
        KeyboardState keyboardState = Keyboard.GetState();

        // Toggle the state of the camera.
        if (keyboardState.IsKeyDown( Keys.Tab ))
        {
            cameraStateKeyDown = true;
        }
        else if (cameraStateKeyDown == true)
        {
            cameraStateKeyDown = false;
            cameraState += 1;
            cameraState %= 3;
        }
    }
    protected override void Update( GameTime gameTime )
    {
        base.Update( gameTime );
        GetCurrentCamera();
        UpdateAvatarPosition();
        CheckMouseClick();
    }

    protected override void Draw( GameTime gameTime )
    {
        base.Draw( gameTime );

        GraphicsDevice device = graphics.GraphicsDevice;

        device.Clear( ClearOptions.Target | ClearOptions.DepthBuffer, new Vector4( 0, 0, 0, 255 ), 1.0f, 0 );


        // Turn off culling so the back side of the triangles can be seen.
        //graphics.GraphicsDevice.RenderState.CullMode = CullMode.None;

        switch (cameraState)
        {
            default:
            case 0:
                UpdateCamera();
                break;
            case 1:
                UpdateCameraFirstPerson();
                break;
            case 2:
                UpdateCameraThirdPerson();
                break;
        }

        for (int i = 0; i < worldObjects.Length; i++)
        {
            Matrix world = Matrix.CreateTranslation( worldObjects[i].position );
            DrawModel( worldObjects[i].model, world, worldObjects[i].texture2D );
        }


    }

    void UpdateCamera()
    {
        // Calculate the camera's current position.
        Vector3 cameraPosition = avatarPosition;

        // Copy the camera's reference vector.
        Vector3 cameraLookat = cameraReference;

        // Create a vector pointing the direction the camera is facing.
        cameraLookat = Vector3.Transform( cameraLookat, Matrix.CreateRotationY( avatarYaw ) );

        // Calculate the position the camera is looking at.
        cameraLookat += cameraPosition;

        // Set up the view matrix and projection matrix.
        view = Matrix.CreateLookAt( cameraPosition, cameraLookat, new Vector3( 0.0f, 1.0f, 0.0f ) );
        proj = Matrix.CreatePerspectiveFieldOfView( FOV, aspectRatio, nearClip, farClip );
    }
    void UpdateCameraFirstPerson()
    {
        // Transform the head offset so the camera is positioned properly relative to the avatar.
        Vector3 headOffset = Vector3.Transform( avatarHeadOffset, Matrix.CreateRotationY( avatarYaw ) );

        // Calculate the camera's current position.
        Vector3 cameraPosition = avatarPosition + headOffset;

        // Copy the camera's reference vector.
        Vector3 cameraLookat = cameraReference;

        // Create a vector pointing the direction the camera is facing.
        cameraLookat = Vector3.Transform( cameraLookat, Matrix.CreateRotationY( avatarYaw ) );

        // Calculate the position the camera is looking at.
        cameraLookat += cameraPosition;
        cameraLookat += headOffset;

        // Set up the view matrix and projection matrix.
        view = Matrix.CreateLookAt( cameraPosition, cameraLookat, new Vector3( 0.0f, 1.0f, 0.0f ) );
        proj = Matrix.CreatePerspectiveFieldOfView( FOV, aspectRatio, nearClip, farClip );
    }
    void UpdateCameraThirdPerson()
    {
        // Copy the third person camera's unrotated offset.
        Vector3 cameraPosition = thirdPersonReference;

        // Create a vector pointing the direction the camera is facing.
        cameraPosition = Vector3.Transform( cameraPosition, Matrix.CreateRotationY( avatarYaw ) );

        // Calculate the position the camera is looking at.
        cameraPosition += avatarPosition;

        // Set up the view matrix and projection matrix.
        view = Matrix.CreateLookAt( cameraPosition, avatarPosition, new Vector3( 0.0f, 1.0f, 0.0f ) );
        proj = Matrix.CreatePerspectiveFieldOfView( FOV, aspectRatio, nearClip, farClip );
    }

    public Game1()
    {
        graphics = new GraphicsDeviceManager( this );
        contentManager = new ContentManager( Services );
        worldObjects = new WorldObject[3];
        worldObjects[0] = new WorldObject();
        worldObjects[0].model = sphere;
        worldObjects[0].position = new Vector3( 0, 0, 0 );
        worldObjects[0].texture2D = sphereTexture;

        worldObjects[1] = new WorldObject();
        worldObjects[1].model = sphere;
        worldObjects[1].position = new Vector3( -2, 0, 0 );
        worldObjects[1].texture2D = sphereTexture;

        worldObjects[2] = new WorldObject();
        worldObjects[2].model = sphere;
        worldObjects[2].position = new Vector3( 2, 0, 0 );
        worldObjects[2].texture2D = sphereTexture;

        // The mouse is not visible by default.
        IsMouseVisible = true;
    }

    protected override void LoadGraphicsContent( bool loadAllContent )
    {
        base.LoadGraphicsContent( loadAllContent );
        if (loadAllContent)
        {
            sphere = contentManager.Load<Model>( "sphere" );
            sphereTexture = contentManager.Load<Texture2D>( "unselectedtexture" );
            selectedTexture = contentManager.Load<Texture2D>( "selectedtexture" );
            for (int i = 0; i < worldObjects.Length; i++)
            {
                worldObjects[i].model = sphere;
                worldObjects[i].texture2D = sphereTexture;
            }

        }
    }

    protected override void UnloadGraphicsContent( bool unloadAllContent )
    {
        base.UnloadGraphicsContent( unloadAllContent );
        if (unloadAllContent)
        {
            contentManager.Unload();
        }
    }



    void DrawModel( Model model, Matrix world, Texture2D texture )
    {
        foreach (ModelMesh mesh in model.Meshes)
        {
            foreach (BasicEffect be in mesh.Effects)
            {
                be.Projection = proj;
                be.View = view;
                be.World = world;
                be.Texture = texture;
                be.TextureEnabled = true;


            }
            mesh.Draw();
        }
    }
}

See Also

How to: Rotate and Move a Camera
How to: Render a Model