12 min read

Picking

Introduction

In previous tutorials, we have talked a lot about various methods of input from the user. In this tutorial, we are going to take a look at an advanced component of mouse input called picking. In many 3D games, the user needs to be able to select objects in the scene. In other words, the program needs to be able to determine what the user is choosing, or “picking”, when they click on a particular point on the screen. This is not a trivial task, and it would normally require quite a bit of work. However, the XNA people made this fairly easy for us to do.

In this tutorial, we are going to go through the process of performing picking. Because this is a more advanced tutorial, we will start by laying out some initial code to start with. Of course, you may be reading this tutorial because you have found a need for this in your own game. In this case, feel free to simply add the picking code into your game and skip over the Initial Code section. After we lay out the groundwork for this tutorial, we will discuss the basic idea behind picking and then proceed to write some simple code to perform the picking.

Initial Code

Because this tutorial is more advanced, we are going to start with the code below. Create a new project and replace the code in the main game class (Game1.cs) with the code below:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

namespace Picking
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        Model asteroid;
        Model smallShip;
        Model largeShip;

        Matrix asteroidWorld;
        Matrix smallShipWorld;
        Matrix largeShipWorld;

        string message = "Picking does not work yet.";
        SpriteFont font;

        private Matrix view = Matrix.CreateLookAt(new Vector3(10, 10, 10), new Vector3(0, 0, 0), Vector3.UnitY);
        private Matrix projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45), 800f / 480f, 0.1f, 100f);

        public Game1()
        {
            this.IsMouseVisible = true;
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            font = Content.Load<SpriteFont>("font");

            asteroid = Content.Load<Model>("LargeAsteroid");
            smallShip = Content.Load<Model>("Ship");
            largeShip = Content.Load<Model>("ship2");

            asteroidWorld = Matrix.CreateTranslation(new Vector3(5, 0, 0));
            smallShipWorld = Matrix.CreateTranslation(new Vector3(0, 0, 5));
            largeShipWorld = Matrix.CreateTranslation(new Vector3(-30, -30, -30));
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            DrawModel(asteroid, asteroidWorld, view, projection);
            DrawModel(smallShip, smallShipWorld, view, projection);
            DrawModel(largeShip, largeShipWorld, view, projection);

            spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend);
            spriteBatch.DrawString(font, message, new Vector2(100, 100), Color.Black);
            spriteBatch.End();

            base.Draw(gameTime);
        }

        private void DrawModel(Model model, Matrix world, Matrix view, Matrix projection)
        {
            foreach (ModelMesh mesh in model.Meshes)
            {
                foreach (BasicEffect effect in mesh.Effects)
                {
                    effect.EnableDefaultLighting();
                    effect.World = world;
                    effect.View = view;
                    effect.Projection = projection;
                }

                mesh.Draw();
            }
        }
    }
}

Hopefully, most of the code here looks familiar to you. There is a lot of stuff here about drawing models, which you should go back and do if you haven’t seen it before. It also does a little bit with lighting and also some drawing with sprite fonts. If you haven’t seen these things yet, it might be a good idea to go back and look at them as well, but you can probably get by without it just fine, too.

Additionally, you will need to get the content for this project or change the models to something else. You can find the three models (Asteroid, SimpleShip, and Ship2) in the 3D Model Library . You will also need to create a sprite font called “font.spritefont”, which is discussed in the tutorial on using sprite fonts. Alternatively, you could just remove all of the sprite font stuff, but you will need to do something different to check to see if your picking code is working later.

When you get this code into your game, and acquire the three models and create the sprite font, you should be able to compile the code and run it. You should get something similar to the screenshot below:

Screenshot 1

The Basic Picking Method

The actual method for doing picking is fairly simple. The first thing that we will do is to create a ray in the scene that starts at the near clipping plane and goes into the scene. This ray will indicate the line that the mouse is currently over. Once we have this ray, we will look at the bounding spheres contained within each of the parts of the model and determine whether or not the ray intersects any of them. If there is an intersection, then we know the mouse is currently over the object in question. Otherwise, we know that the mouse is not over the object. It’s as simple as that. Now we’ll go on and write the code to actually do this.

Picking Code

We are now ready to write the code for picking. We will follow the same basic steps that we outlined in the previous section. The first thing we will do is to create a method that will create a ray into the scene from the mouse location. Note that this actually doesn’t need to actually be the location of the mouse on the screen, but rather, it can be any point on the screen. Add the following code to your game as a method in your main game class:

public Ray CalculateRay(Vector2 mouseLocation, Matrix view,
    Matrix projection, Viewport viewport)
{
    Vector3 nearPoint = viewport.Unproject(new Vector3(mouseLocation.X,
            mouseLocation.Y, 0.0f),
            projection,
            view,
            Matrix.Identity);

    Vector3 farPoint = viewport.Unproject(new Vector3(mouseLocation.X,
            mouseLocation.Y, 1.0f),
            projection,
            view,
            Matrix.Identity);

            Vector3 direction = farPoint - nearPoint;
            direction.Normalize();

    return new Ray(nearPoint, direction);
}

In addition to the location on the screen (mouseLocation), this method is also going to require the view and projection matrices as well as the viewport that is currently in use. This is because, in order to determine where the ray is in the scene, you need to know how the scene is set up. This is determined by these three parameters.

The next thing this method does is creates a point at the near clipping plane and a point at the far clipping plane. This is done by using the Viewport.Unproject method. These points are located on the ray that we eventually want to calculate. In other words, they are both directly under the mouse, but one is located really close to the screen, and one is located really far away from the screen.

Once we have created these points, we need to calculate the direction that the ray is going. This is the next thing we do, with the command Vector3 direction = farPoint - nearPoint;. We then normalize the direction vector and return the new ray, which is determined by its starting point (the point at the near clipping plane) and the direction that it goes in.

The next thing we will do is to create a method that will determine whether the ray intersects a BoundingSphere object. This is done with the simple code below. Add this to your main game class as well:

public float? IntersectDistance(BoundingSphere sphere, Vector2 mouseLocation,
            Matrix view, Matrix projection, Viewport viewport)
{
    Ray mouseRay = CalculateRay(mouseLocation, view, projection, viewport);
    return mouseRay.Intersects(sphere);
}

Once again, in this method, we see how much work the XNA people have done for us. In this method, the first line simply calculates the ray using the method we created before. We then just call the ray’s Intersects method, passing in the sphere that we want to check. Notice, however, that the return type of this method is float?. It returns the distance to the intersection point, if there is one, and returns null if there isn’t one. In C#, the types that end in a ? are types that are allowed to be null. So while the type float has to be an actual floating-point value, the type float? can be either a floating-point value or null. Also notice that instead of returning a Boolean value (true or false), this returns a floating-point value. At first, this may seem like kind of an annoying feature, (we will have to convert this to true or false ourselves), but in reality, it is very useful. It can easily be used to find the closest object that the mouse is over, which is very useful.

The final thing we will want to do is to create a final method that will take a model and its world transformation matrix, along with the rest of the stuff needed to build the ray, and determine whether or not the ray intersects the model. We can do this with the code below. Add it to your game as another method to your main game class:

public bool Intersects(Vector2 mouseLocation,
            Model model, Matrix world,
            Matrix view, Matrix projection,
            Viewport viewport)
{
    for (int index = 0; index < model.Meshes.Count; index++)
    {
        BoundingSphere sphere = model.Meshes[index].BoundingSphere;
        sphere = sphere.Transform(world);
        float? distance = IntersectDistance(sphere, mouseLocation, view, projection, viewport);

        if (distance != null)
        {
            return true;
        }
    }

    return false;
}

This method just goes through each of the model’s meshes and gets the bounding sphere for the mesh. It transforms the bounding sphere to the appropriate location in the 3D world using the model’s world matrix and then calculates the distance to the intersection point. If there was an intersection, then the distance to the intersection point will not be null (it will give back an actual value) and then this method will return true indicating that the ray intersected at least one bounding circle in the model. If you are moving the meshes around individually, like we did in the tutorial on mesh-by-mesh animation, then you will need to transform the bounding spheres appropriately. The bounding sphere needs to be moved to the same location that the mesh is drawn in, or it won’t work.

With these three methods completed, we are ready to call this from our game and use it. If you are working with the code at the beginning of the tutorial, add the following code to your Update() method:

Vector2 mouseLocation = new Vector2(Mouse.GetState().X, Mouse.GetState().Y);
Viewport viewport = this.GraphicsDevice.Viewport;

bool mouseOverSomething = false;

if(Intersects(mouseLocation, asteroid, asteroidWorld, view, projection, viewport))
{
    message = "Mouse Over:  Asteroid";
    mouseOverSomething = true;
}
if(Intersects(mouseLocation, smallShip, smallShipWorld, view, projection, viewport))
{
    message = "Mouse Over:  Small Ship";
    mouseOverSomething = true;
}
if(Intersects(mouseLocation, largeShip, largeShipWorld, view, projection, viewport))
{
    message = "Mouse Over:  Large Ship";
    mouseOverSomething = true;
}

if (!mouseOverSomething)
{
    message = "Mouse Over:  None";
}

This does a few things. First, it determines the mouse location and viewport in use, so that they can be used in a minute. Then it checks each of the models in the scene. If it is over them, it sets up the message to say that the mouse is over a particular object. If it gets through all three of them and hasn’t found a match, then it sets the message to say that the mouse isn’t over anything. This should be enough to allow the picking to work. You should now be able to run the game and see that when you move the mouse over particular objects, the message says what object the mouse is over.

Screenshot 2

Of course, this is set up to detect what the mouse is over. It doesn’t check to see if the mouse is being clicked or anything. This is not hard to do and was covered in the mouse input tutorials. Below is the source code for the entire program, assembled together.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

namespace Picking
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        Model asteroid;
        Model smallShip;
        Model largeShip;

        Matrix asteroidWorld;
        Matrix smallShipWorld;
        Matrix largeShipWorld;

        string message = "Picking does not work yet.";
        SpriteFont font;

        private Matrix view = Matrix.CreateLookAt(new Vector3(10, 10, 10), new Vector3(0, 0, 0), Vector3.UnitY);
        private Matrix projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45), 800f / 480, 0.1f, 100f);

        public Game1()
        {
            this.IsMouseVisible = true;
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            font = Content.Load<SpriteFont>("font");

            asteroid = Content.Load<Model>("LargeAsteroid");
            smallShip = Content.Load<Model>("Ship");
            largeShip = Content.Load<Model>("ship2");

            asteroidWorld = Matrix.CreateTranslation(new Vector3(5, 0, 0));
            smallShipWorld = Matrix.CreateTranslation(new Vector3(0, 0, 5));
            largeShipWorld = Matrix.CreateTranslation(new Vector3(-30, -30, -30));
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            Vector2 mouseLocation = new Vector2(Mouse.GetState().X, Mouse.GetState().Y);
            Viewport viewport = this.GraphicsDevice.Viewport;

            bool mouseOverSomething = false;

            if (Intersects(mouseLocation, asteroid, asteroidWorld, view, projection, viewport))
            {
                message = "Mouse Over:  Asteroid";
                mouseOverSomething = true;
            }
            if (Intersects(mouseLocation, smallShip, smallShipWorld, view, projection, viewport))
            {
                message = "Mouse Over:  Small Ship";
                mouseOverSomething = true;
            }
            if (Intersects(mouseLocation, largeShip, largeShipWorld, view, projection, viewport))
            {
                message = "Mouse Over:  Large Ship";
                mouseOverSomething = true;
            }

            if (!mouseOverSomething)
            {
                message = "Mouse Over:  None";
            }

            base.Update(gameTime);
        }

        public Ray CalculateRay(Vector2 mouseLocation, Matrix view,
                Matrix projection, Viewport viewport)
        {
            Vector3 nearPoint = viewport.Unproject(new Vector3(mouseLocation.X,
                    mouseLocation.Y, 0.0f),
                    projection,
                    view,
                    Matrix.Identity);

            Vector3 farPoint = viewport.Unproject(new Vector3(mouseLocation.X,
                    mouseLocation.Y, 1.0f),
                    projection,
                    view,
                    Matrix.Identity);

            Vector3 direction = farPoint - nearPoint;
            direction.Normalize();

            return new Ray(nearPoint, direction);
        }

        public float? IntersectDistance(BoundingSphere sphere, Vector2 mouseLocation,
            Matrix view, Matrix projection, Viewport viewport)
        {
            Ray mouseRay = CalculateRay(mouseLocation, view, projection, viewport);
            return mouseRay.Intersects(sphere);
        }

        public bool Intersects(Vector2 mouseLocation,
            Model model, Matrix world,
            Matrix view, Matrix projection,
            Viewport viewport)
        {
            for (int index = 0; index < model.Meshes.Count; index++)
            {
                BoundingSphere sphere = model.Meshes[index].BoundingSphere;
                sphere = sphere.Transform(world);
                float? distance = IntersectDistance(sphere, mouseLocation, view, projection, viewport);

                if (distance != null)
                {
                    return true;
                }
            }

            return false;
        }


        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            DrawModel(asteroid, asteroidWorld, view, projection);
            DrawModel(smallShip, smallShipWorld, view, projection);
            DrawModel(largeShip, largeShipWorld, view, projection);

            spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend);
            spriteBatch.DrawString(font, message, new Vector2(100, 100), Color.Black);
            spriteBatch.End();

            base.Draw(gameTime);
        }

        private void DrawModel(Model model, Matrix world, Matrix view, Matrix projection)
        {
            foreach (ModelMesh mesh in model.Meshes)
            {
                foreach (BasicEffect effect in mesh.Effects)
                {
                    effect.EnableDefaultLighting();
                    effect.World = world;
                    effect.View = view;
                    effect.Projection = projection;
                }

                mesh.Draw();
            }
        }

    }
}
Download the complete project