#region File Description //----------------------------------------------------------------------------- // Game.cs // // Microsoft XNA Community Game Platform // Copyright (C) Microsoft Corporation. All rights reserved. //----------------------------------------------------------------------------- #endregion #region Using Statements using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Input.Touch; using BoundingVolumeRendering; #endregion namespace PickingSample { /// /// This sample shows how to see if a user's cursor is over an object, and how to /// find out where on the screen an object is. The sample puts several objects on a /// table. If the cursor is on the object, the object's name is displayed. See the /// accompanying doc file for more information. /// public class PickingSampleGame : Microsoft.Xna.Framework.Game { #region Constants // ModelFilenames is the list of models that we will be putting on top of the // table. These strings will be used as arguments to content.Load and // will be drawn when the cursor is over an object. static readonly string[] ModelFilenames = new string[]{ "Sphere", "Cats", "P2Wedge", "Cylinder", }; // the following constants control the speed at which the camera moves // how fast does the camera move up, down, left, and right? const float CameraRotateSpeed = .01f; // the following constants control how the camera's default position const float CameraDefaultArc = -15.0f; const float CameraDefaultRotation = 185; const float CameraDefaultDistance = 4.3f; #endregion #region Fields GraphicsDeviceManager graphics; // a SpriteBatch and SpriteFont, which we will use to draw the objects' names // when they are selected. SpriteBatch spriteBatch; SpriteFont spriteFont; // The cursor is used to tell what the user's pointer/mouse is over. The cursor // is moved with the left thumbstick. On windows, the mouse can be used as well. Cursor cursor; // the table that all of the objects are drawn on, and table model's // absoluteBoneTransforms. Since the table is not animated, these can be // calculated once and saved. Model table; Matrix[] tableAbsoluteBoneTransforms; // these are the models that we will draw on top of the table. we'll store them // and their bone transforms in arrays. Again, since these models aren't // animated, we can calculate their bone transforms once and save the result. Model[] models = new Model[ModelFilenames.Length]; Matrix[][] modelAbsoluteBoneTransforms = new Matrix[ModelFilenames.Length][]; // each model will need one more matrix: a world transform. This matrix will be // used to place each model at a different location in the world. Matrix[] modelWorldTransforms = new Matrix[ModelFilenames.Length]; Matrix viewMatrix; Matrix projectionMatrix; // this variable will store the current rotation value as the camera // rotates around the scene float cameraRotation = CameraDefaultRotation; // this variable will tell our game whether or not to draw a mesh's bounding sphere bool drawBoundingSphere = true; #endregion #region Initialization public PickingSampleGame() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; #if WINDOWS_PHONE // Frame rate is 30 fps by default for Windows Phone. TargetElapsedTime = TimeSpan.FromTicks(333333); graphics.IsFullScreen = true; #endif } protected override void Initialize() { // Set up the world transforms that each model will use. They'll be // positioned in a line along the x axis. modelWorldTransforms[0] = Matrix.CreateTranslation(new Vector3(-1.5f, 0, 0)); modelWorldTransforms[1] = Matrix.CreateTranslation(new Vector3(-.5f, 0, 0)); modelWorldTransforms[2] = Matrix.CreateTranslation(new Vector3(.5f, 0, 0)); modelWorldTransforms[3] = Matrix.CreateTranslation(new Vector3(1.5f, 0, 0)); cursor = new Cursor(this); Components.Add(cursor); base.Initialize(); } /// /// Load your graphics content. /// protected override void LoadContent() { // load all of the models that will appear on the table: for (int i = 0; i < ModelFilenames.Length; i++) { // load the actual model, using ModelFilenames to determine what // file to load. models[i] = Content.Load(ModelFilenames[i]); // create an array of matrices to hold the absolute bone transforms, // calculate them, and copy them in. modelAbsoluteBoneTransforms[i] = new Matrix[models[i].Bones.Count]; models[i].CopyAbsoluteBoneTransformsTo( modelAbsoluteBoneTransforms[i]); } // now that we've loaded in the models that will sit on the table, go // through the same procedure for the table itself. table = Content.Load("Table"); tableAbsoluteBoneTransforms = new Matrix[table.Bones.Count]; table.CopyAbsoluteBoneTransformsTo(tableAbsoluteBoneTransforms); // create a spritebatch and load the font, which we'll use to draw the // models' names. spriteBatch = new SpriteBatch(graphics.GraphicsDevice); spriteFont = Content.Load("hudFont"); // calculate the projection matrix now that the graphics device is created. projectionMatrix = Matrix.CreatePerspectiveFieldOfView( MathHelper.PiOver4, GraphicsDevice.Viewport.AspectRatio, .01f, 1000); // Initialize the renderer for our bounding spheres BoundingSphereRenderer.Initialize(GraphicsDevice, 45); } #endregion #region Update and Draw /// /// Allows the game to run logic. /// protected override void Update(GameTime gameTime) { // Check for exit. if (Keyboard.GetState().IsKeyDown(Keys.Escape) || GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) { Exit(); } // we rotate our view around the models over time float time = (float)gameTime.ElapsedGameTime.TotalMilliseconds; cameraRotation += time * CameraRotateSpeed; Matrix unrotatedView = Matrix.CreateLookAt( new Vector3(0, 0, -CameraDefaultDistance), Vector3.Zero, Vector3.Up); viewMatrix = Matrix.CreateRotationY(MathHelper.ToRadians(cameraRotation)) * Matrix.CreateRotationX(MathHelper.ToRadians(CameraDefaultArc)) * unrotatedView; // base.Update will update all of the components in the .Components // collection, including the cursor. base.Update(gameTime); } /// /// This is called when the game should draw itself. /// protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); // drawing sprites changes some render states around, which don't play // nicely with 3d models. In particular, we want to enable the depth buffer and turn off alpha blending. GraphicsDevice.DepthStencilState = DepthStencilState.Default; GraphicsDevice.BlendState = BlendState.Opaque; // draw the table. DrawModel is a function defined below draws a model using // a world matrix and the model's bone transforms. DrawModel(table, Matrix.Identity, tableAbsoluteBoneTransforms, false); // use the same DrawModel function to draw all of the models on the table. for (int i = 0; i < models.Length; i++) { DrawModel(models[i], modelWorldTransforms[i], modelAbsoluteBoneTransforms[i], drawBoundingSphere); } // now we'll check to see if the cursor is over any of the models, and draw // their names if it is. DrawModelNames(); base.Draw(gameTime); } private void DrawModelNames() { // begin on the spritebatch, because we're going to be drawing some text. spriteBatch.Begin(); // If the cursor is over a model, we'll draw its name. To figure out if // the cursor is over a model, we'll use cursor.CalculateCursorRay. That // function gives us a world space ray that starts at the "eye" of the // camera, and shoots out in the direction pointed to by the cursor. Ray cursorRay = cursor.CalculateCursorRay(projectionMatrix, viewMatrix); // go through all of the models... for (int i = 0; i < models.Length; i++) { // check to see if the cursorRay intersects the model.... if (RayIntersectsModel(cursorRay, models[i], modelWorldTransforms[i], modelAbsoluteBoneTransforms[i])) { // now we know that we want to draw the model's name. We want to // draw the name a little bit above the model: but where's that? // SpriteBatch.DrawString takes screen space coordinates, but the // model's position is stored in world space. // we'll use Viewport.Project, which will project a world space // point into screen space. We'll project the vector (0,0,0) using // the model's world matrix, and the view and projection matrices. // that will tell us where the model's origin is on the screen. Vector3 screenSpace = graphics.GraphicsDevice.Viewport.Project( Vector3.Zero, projectionMatrix, viewMatrix, modelWorldTransforms[i]); // we want to draw the text a little bit above that, so we'll use // the screen space position - 60 to move up a little bit. A better // approach would be to calculate where the top of the model is, and // draw there. It's not that much harder to do, but to keep the // sample easy, we'll take the easy way out. Vector2 textPosition = new Vector2(screenSpace.X, screenSpace.Y - 60); // we want to draw the text centered around textPosition, so we'll // calculate the center of the string, and use that as the origin // argument to spriteBatch.DrawString. DrawString automatically // centers text around the vector specified by the origin argument. Vector2 stringCenter = spriteFont.MeasureString(ModelFilenames[i]) / 2; // to make the text readable, we'll draw the same thing twice, once // white and once black, with a little offset to get a drop shadow // effect. // first we'll draw the shadow... Vector2 shadowOffset = new Vector2(1, 1); spriteBatch.DrawString(spriteFont, ModelFilenames[i], textPosition + shadowOffset, Color.Black, 0.0f, stringCenter, 1.0f, SpriteEffects.None, 0.0f); // ...and then the real text on top. spriteBatch.DrawString(spriteFont, ModelFilenames[i], textPosition, Color.White, 0.0f, stringCenter, 1.0f, SpriteEffects.None, 0.0f); } } spriteBatch.End(); } /// /// DrawModel is a helper function that takes a model, world matrix, and /// bone transforms. It does just what its name implies, and draws the model. /// /// the model to draw /// where to draw the model /// the model's bone transforms. this can /// be calculated using the function Model.CopyAbsoluteBoneTransformsTo private void DrawModel(Model model, Matrix worldTransform, Matrix[] absoluteBoneTransforms, bool drawBoundingSphere) { // nothing tricky in here; this is the same model drawing code that we see // everywhere. we'll loop over all of the meshes in the model, set up their // effects, and then draw them. foreach (ModelMesh mesh in model.Meshes) { foreach (BasicEffect effect in mesh.Effects) { effect.EnableDefaultLighting(); effect.View = viewMatrix; effect.Projection = projectionMatrix; effect.World = absoluteBoneTransforms[mesh.ParentBone.Index] * worldTransform; } mesh.Draw(); if (drawBoundingSphere) { // the mesh's BoundingSphere is stored relative to the mesh itself. // (Mesh space). We want to get this BoundingSphere in terms of world // coordinates. To do this, we calculate a matrix that will transform // from coordinates from mesh space into world space.... Matrix world = absoluteBoneTransforms[mesh.ParentBone.Index] * worldTransform; // ... and then transform the BoundingSphere using that matrix. BoundingSphere sphere = TransformBoundingSphere(mesh.BoundingSphere, world); // now draw the sphere with our renderer BoundingSphereRenderer.Draw(sphere, viewMatrix, projectionMatrix); } } } /// /// This helper function checks to see if a ray will intersect with a model. /// The model's bounding spheres are used, and the model is transformed using /// the matrix specified in the worldTransform argument. /// /// the ray to perform the intersection check with /// the model to perform the intersection check with. /// the model's bounding spheres will be used. /// a matrix that positions the model /// in world space /// this array of matrices contains the /// absolute bone transforms for the model. this can be obtained using the /// Model.CopyAbsoluteBoneTransformsTo function. /// true if the ray intersects the model. private static bool RayIntersectsModel(Ray ray, Model model, Matrix worldTransform, Matrix[] absoluteBoneTransforms) { // Each ModelMesh in a Model has a bounding sphere, so to check for an // intersection in the Model, we have to check every mesh. foreach (ModelMesh mesh in model.Meshes) { // the mesh's BoundingSphere is stored relative to the mesh itself. // (Mesh space). We want to get this BoundingSphere in terms of world // coordinates. To do this, we calculate a matrix that will transform // from coordinates from mesh space into world space.... Matrix world = absoluteBoneTransforms[mesh.ParentBone.Index] * worldTransform; // ... and then transform the BoundingSphere using that matrix. BoundingSphere sphere = TransformBoundingSphere(mesh.BoundingSphere, world); // now that the we have a sphere in world coordinates, we can just use // the BoundingSphere class's Intersects function. Intersects returns a // nullable float (float?). This value is the distance at which the ray // intersects the BoundingSphere, or null if there is no intersection. // so, if the value is not null, we have a collision. if (sphere.Intersects(ray) != null) { return true; } } // if we've gotten this far, we've made it through every BoundingSphere, and // none of them intersected the ray. This means that there was no collision, // and we should return false. return false; } /// /// This helper function takes a BoundingSphere and a transform matrix, and /// returns a transformed version of that BoundingSphere. /// /// the BoundingSphere to transform /// how to transform the BoundingSphere. /// the transformed BoundingSphere/ private static BoundingSphere TransformBoundingSphere(BoundingSphere sphere, Matrix transform) { BoundingSphere transformedSphere; // the transform can contain different scales on the x, y, and z components. // this has the effect of stretching and squishing our bounding sphere along // different axes. Obviously, this is no good: a bounding sphere has to be a // SPHERE. so, the transformed sphere's radius must be the maximum of the // scaled x, y, and z radii. // to calculate how the transform matrix will affect the x, y, and z // components of the sphere, we'll create a vector3 with x y and z equal // to the sphere's radius... Vector3 scale3 = new Vector3(sphere.Radius, sphere.Radius, sphere.Radius); // then transform that vector using the transform matrix. we use // TransformNormal because we don't want to take translation into account. scale3 = Vector3.TransformNormal(scale3, transform); // scale3 contains the x, y, and z radii of a squished and stretched sphere. // we'll set the finished sphere's radius to the maximum of the x y and z // radii, creating a sphere that is large enough to contain the original // squished sphere. transformedSphere.Radius = Math.Max(scale3.X, Math.Max(scale3.Y, scale3.Z)); // transforming the center of the sphere is much easier. we can just use // Vector3.Transform to transform the center vector. notice that we're using // Transform instead of TransformNormal because in this case we DO want to // take translation into account. transformedSphere.Center = Vector3.Transform(sphere.Center, transform); return transformedSphere; } #endregion } }