#region File Description
//-----------------------------------------------------------------------------
// Game.cs
//
// Microsoft XNA Community Game Platform
// Copyright (C) Microsoft Corporation. All rights reserved.
//-----------------------------------------------------------------------------
#endregion
#region Using Statements
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
#endregion
namespace TrianglePicking
{
///
/// Sample showing how to implement per-triangle picking. This uses a custom
/// content pipeline processor to attach a list of vertex position data to each
/// model as part of the build process, and then implements a ray-to-triangle
/// intersection method to collide against this vertex data.
///
public class TrianglePickingGame : 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 =
{
"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 = .1f;
// how fast does the camera zoom in and out?
const float CameraZoomSpeed = .01f;
// the camera can't be further away than this distance
const float CameraMaxDistance = 10.0f;
// and it can't be closer than this
const float CameraMinDistance = 1.2f;
// the following constants control how the camera's default position
const float CameraDefaultArc = -30.0f;
const float CameraDefaultRotation = 225;
const float CameraDefaultDistance = 3.5f;
#endregion
#region Fields
GraphicsDeviceManager graphics;
// the current input states. These are updated in the HandleInput function,
// and used primarily in the UpdateCamera function.
KeyboardState currentKeyboardState;
GamePadState currentGamePadState;
// 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];
// The next set of variables are used to control the camera used in the sample.
// It is an arc ball camera, so it can rotate in a sphere around the target, and
// zoom in and out.
float cameraArc = CameraDefaultArc;
float cameraRotation = CameraDefaultRotation;
float cameraDistance = CameraDefaultDistance;
Matrix viewMatrix;
Matrix projectionMatrix;
// To keep things efficient, the picking works by first applying a bounding
// sphere test, and then only bothering to test each individual triangle
// if the ray intersects the bounding sphere. This allows us to trivially
// reject many models without even needing to bother looking at their triangle
// data. This field keeps track of which models passed the bounding sphere
// test, so you can see the difference between this approximation and the more
// accurate triangle picking.
List insideBoundingSpheres = new List();
// Store the name of the model underneath the cursor (or null if there is none).
string pickedModelName;
// Vertex array that stores exactly which triangle was picked.
VertexPositionColor[] pickedTriangle =
{
new VertexPositionColor(Vector3.Zero, Color.Magenta),
new VertexPositionColor(Vector3.Zero, Color.Magenta),
new VertexPositionColor(Vector3.Zero, Color.Magenta),
};
// Effect and vertex declaration for drawing the picked triangle.
BasicEffect lineEffect;
// Custom rasterizer state for drawing in wireframe.
static RasterizerState WireFrame = new RasterizerState
{
FillMode = FillMode.WireFrame,
CullMode = CullMode.None
};
#endregion
#region Initialization
public TrianglePickingGame()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
// Set up the world transforms that each model will use. They'll be
// positioned in a line along the x axis.
for (int i = 0; i < modelWorldTransforms.Length; i++)
{
float x = i - modelWorldTransforms.Length / 2;
modelWorldTransforms[i] =
Matrix.CreateTranslation(new Vector3(x, 0, 0));
}
cursor = new Cursor(this, Content);
Components.Add(cursor);
}
protected override void Initialize()
{
// now that the GraphicsDevice has been created, we can create the projection matrix.
projectionMatrix = Matrix.CreatePerspectiveFieldOfView(
MathHelper.ToRadians(45.0f), GraphicsDevice.Viewport.AspectRatio, .01f, 1000);
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");
// create the effect and vertex declaration for drawing the
// picked triangle.
lineEffect = new BasicEffect(graphics.GraphicsDevice);
lineEffect.VertexColorEnabled = true;
}
#endregion
#region Update and Draw
///
/// Allows the game to run logic.
///
protected override void Update(GameTime gameTime)
{
HandleInput();
UpdateCamera(gameTime);
UpdatePicking();
base.Update(gameTime);
}
///
/// Runs a per-triangle picking algorithm over all the models in the scene,
/// storing which triangle is currently under the cursor.
///
void UpdatePicking()
{
// Look up a collision ray based on the current cursor position. See the
// Picking Sample documentation for a detailed explanation of this.
Ray cursorRay = cursor.CalculateCursorRay(projectionMatrix, viewMatrix);
// Clear the previous picking results.
insideBoundingSpheres.Clear();
pickedModelName = null;
// Keep track of the closest object we have seen so far, so we can
// choose the closest one if there are several models under the cursor.
float closestIntersection = float.MaxValue;
// Loop over all our models.
for (int i = 0; i < models.Length; i++)
{
bool insideBoundingSphere;
Vector3 vertex1, vertex2, vertex3;
// Perform the ray to model intersection test.
float? intersection = RayIntersectsModel(cursorRay, models[i],
modelWorldTransforms[i],
out insideBoundingSphere,
out vertex1, out vertex2,
out vertex3);
// If this model passed the initial bounding sphere test, remember
// that so we can display it at the top of the screen.
if (insideBoundingSphere)
insideBoundingSpheres.Add(ModelFilenames[i]);
// Do we have a per-triangle intersection with this model?
if (intersection != null)
{
// If so, is it closer than any other model we might have
// previously intersected?
if (intersection < closestIntersection)
{
// Store information about this model.
closestIntersection = intersection.Value;
pickedModelName = ModelFilenames[i];
// Store vertex positions so we can display the picked triangle.
pickedTriangle[0].Position = vertex1;
pickedTriangle[1].Position = vertex2;
pickedTriangle[2].Position = vertex3;
}
}
}
}
///
/// Checks whether a ray intersects a model. This method needs to access
/// the model vertex data, so the model must have been built using the
/// custom TrianglePickingProcessor provided as part of this sample.
/// Returns the distance along the ray to the point of intersection, or null
/// if there is no intersection.
///
static float? RayIntersectsModel(Ray ray, Model model, Matrix modelTransform,
out bool insideBoundingSphere,
out Vector3 vertex1, out Vector3 vertex2,
out Vector3 vertex3)
{
vertex1 = vertex2 = vertex3 = Vector3.Zero;
// The input ray is in world space, but our model data is stored in object
// space. We would normally have to transform all the model data by the
// modelTransform matrix, moving it into world space before we test it
// against the ray. That transform can be slow if there are a lot of
// triangles in the model, however, so instead we do the opposite.
// Transforming our ray by the inverse modelTransform moves it into object
// space, where we can test it directly against our model data. Since there
// is only one ray but typically many triangles, doing things this way
// around can be much faster.
Matrix inverseTransform = Matrix.Invert(modelTransform);
ray.Position = Vector3.Transform(ray.Position, inverseTransform);
ray.Direction = Vector3.TransformNormal(ray.Direction, inverseTransform);
// Look up our custom collision data from the Tag property of the model.
Dictionary tagData = (Dictionary)model.Tag;
if (tagData == null)
{
throw new InvalidOperationException(
"Model.Tag is not set correctly. Make sure your model " +
"was built using the custom TrianglePickingProcessor.");
}
// Start off with a fast bounding sphere test.
BoundingSphere boundingSphere = (BoundingSphere)tagData["BoundingSphere"];
if (boundingSphere.Intersects(ray) == null)
{
// If the ray does not intersect the bounding sphere, we cannot
// possibly have picked this model, so there is no need to even
// bother looking at the individual triangle data.
insideBoundingSphere = false;
return null;
}
else
{
// The bounding sphere test passed, so we need to do a full
// triangle picking test.
insideBoundingSphere = true;
// Keep track of the closest triangle we found so far,
// so we can always return the closest one.
float? closestIntersection = null;
// Loop over the vertex data, 3 at a time (3 vertices = 1 triangle).
Vector3[] vertices = (Vector3[])tagData["Vertices"];
for (int i = 0; i < vertices.Length; i += 3)
{
// Perform a ray to triangle intersection test.
float? intersection;
RayIntersectsTriangle(ref ray,
ref vertices[i],
ref vertices[i + 1],
ref vertices[i + 2],
out intersection);
// Does the ray intersect this triangle?
if (intersection != null)
{
// If so, is it closer than any other previous triangle?
if ((closestIntersection == null) ||
(intersection < closestIntersection))
{
// Store the distance to this triangle.
closestIntersection = intersection;
// Transform the three vertex positions into world space,
// and store them into the output vertex parameters.
Vector3.Transform(ref vertices[i],
ref modelTransform, out vertex1);
Vector3.Transform(ref vertices[i + 1],
ref modelTransform, out vertex2);
Vector3.Transform(ref vertices[i + 2],
ref modelTransform, out vertex3);
}
}
}
return closestIntersection;
}
}
///
/// Checks whether a ray intersects a triangle. This uses the algorithm
/// developed by Tomas Moller and Ben Trumbore, which was published in the
/// Journal of Graphics Tools, volume 2, "Fast, Minimum Storage Ray-Triangle
/// Intersection".
///
/// This method is implemented using the pass-by-reference versions of the
/// XNA math functions. Using these overloads is generally not recommended,
/// because they make the code less readable than the normal pass-by-value
/// versions. This method can be called very frequently in a tight inner loop,
/// however, so in this particular case the performance benefits from passing
/// everything by reference outweigh the loss of readability.
///
static void RayIntersectsTriangle(ref Ray ray,
ref Vector3 vertex1,
ref Vector3 vertex2,
ref Vector3 vertex3, out float? result)
{
// Compute vectors along two edges of the triangle.
Vector3 edge1, edge2;
Vector3.Subtract(ref vertex2, ref vertex1, out edge1);
Vector3.Subtract(ref vertex3, ref vertex1, out edge2);
// Compute the determinant.
Vector3 directionCrossEdge2;
Vector3.Cross(ref ray.Direction, ref edge2, out directionCrossEdge2);
float determinant;
Vector3.Dot(ref edge1, ref directionCrossEdge2, out determinant);
// If the ray is parallel to the triangle plane, there is no collision.
if (determinant > -float.Epsilon && determinant < float.Epsilon)
{
result = null;
return;
}
float inverseDeterminant = 1.0f / determinant;
// Calculate the U parameter of the intersection point.
Vector3 distanceVector;
Vector3.Subtract(ref ray.Position, ref vertex1, out distanceVector);
float triangleU;
Vector3.Dot(ref distanceVector, ref directionCrossEdge2, out triangleU);
triangleU *= inverseDeterminant;
// Make sure it is inside the triangle.
if (triangleU < 0 || triangleU > 1)
{
result = null;
return;
}
// Calculate the V parameter of the intersection point.
Vector3 distanceCrossEdge1;
Vector3.Cross(ref distanceVector, ref edge1, out distanceCrossEdge1);
float triangleV;
Vector3.Dot(ref ray.Direction, ref distanceCrossEdge1, out triangleV);
triangleV *= inverseDeterminant;
// Make sure it is inside the triangle.
if (triangleV < 0 || triangleU + triangleV > 1)
{
result = null;
return;
}
// Compute the distance along the ray to the triangle.
float rayDistance;
Vector3.Dot(ref edge2, ref distanceCrossEdge1, out rayDistance);
rayDistance *= inverseDeterminant;
// Is the triangle behind the ray origin?
if (rayDistance < 0)
{
result = null;
return;
}
result = rayDistance;
}
///
/// This is called when the game should draw itself.
///
protected override void Draw(GameTime gameTime)
{
GraphicsDevice device = graphics.GraphicsDevice;
device.Clear(Color.CornflowerBlue);
device.BlendState = BlendState.Opaque;
device.DepthStencilState = DepthStencilState.Default;
// Draw the table.
DrawModel(table, Matrix.Identity, tableAbsoluteBoneTransforms);
// 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]);
}
// Draw the outline of the triangle under the cursor.
DrawPickedTriangle();
// Draw text describing the picking results.
DrawText();
base.Draw(gameTime);
}
///
/// Helper for drawing the outline of the triangle currently under the cursor.
///
void DrawPickedTriangle()
{
if (pickedModelName != null)
{
GraphicsDevice device = graphics.GraphicsDevice;
// Set line drawing renderstates. We disable backface culling
// and turn off the depth buffer because we want to be able to
// see the picked triangle outline regardless of which way it is
// facing, and even if there is other geometry in front of it.
device.RasterizerState = WireFrame;
device.DepthStencilState = DepthStencilState.None;
// Activate the line drawing BasicEffect.
lineEffect.Projection = projectionMatrix;
lineEffect.View = viewMatrix;
lineEffect.CurrentTechnique.Passes[0].Apply();
// Draw the triangle.
device.DrawUserPrimitives(PrimitiveType.TriangleList,
pickedTriangle, 0, 1);
// Reset renderstates to their default values.
device.RasterizerState = RasterizerState.CullCounterClockwise;
device.DepthStencilState = DepthStencilState.Default;
}
}
///
/// Helper for drawing text showing the current picking results.
///
void DrawText()
{
// Draw the text twice to create a drop-shadow effect, first in black one
// pixel down and to the right, then again in white at the real position.
Vector2 shadowOffset = new Vector2(1, 1);
spriteBatch.Begin();
// Draw a list of which models passed the initial bounding sphere test.
if (insideBoundingSpheres.Count > 0)
{
string text = "Inside bounding sphere: " +
string.Join(", ", insideBoundingSpheres.ToArray());
Vector2 position = new Vector2(50, 50);
spriteBatch.DrawString(spriteFont, text,
position + shadowOffset, Color.Black);
spriteBatch.DrawString(spriteFont, text,
position, Color.White);
}
// Draw the name of the model that passed the per-triangle picking test.
if (pickedModelName != null)
{
Vector2 position = cursor.Position;
// Draw the text below the cursor position.
position.Y += 32;
// Center the string.
position -= spriteFont.MeasureString(pickedModelName) / 2;
spriteBatch.DrawString(spriteFont, pickedModelName,
position + shadowOffset, Color.Black);
spriteBatch.DrawString(spriteFont, pickedModelName,
position, Color.White);
}
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.
///
private void DrawModel(Model model, Matrix worldTransform,
Matrix[] absoluteBoneTransforms)
{
foreach (ModelMesh mesh in model.Meshes)
{
foreach (BasicEffect effect in mesh.Effects)
{
effect.EnableDefaultLighting();
effect.PreferPerPixelLighting = true;
effect.View = viewMatrix;
effect.Projection = projectionMatrix;
effect.World = absoluteBoneTransforms[mesh.ParentBone.Index] *
worldTransform;
}
mesh.Draw();
}
}
#endregion
#region Handle Input
///
/// Handles input for quitting the game.
///
void HandleInput()
{
currentKeyboardState = Keyboard.GetState();
currentGamePadState = GamePad.GetState(PlayerIndex.One);
// Check for exit.
if (currentKeyboardState.IsKeyDown(Keys.Escape) ||
currentGamePadState.Buttons.Back == ButtonState.Pressed)
{
Exit();
}
}
///
/// Handles input for moving the camera.
///
void UpdateCamera(GameTime gameTime)
{
float time = (float)gameTime.ElapsedGameTime.TotalMilliseconds;
// should we reset the camera?
if (currentKeyboardState.IsKeyDown(Keys.R) ||
currentGamePadState.Buttons.RightStick == ButtonState.Pressed)
{
cameraArc = CameraDefaultArc;
cameraDistance = CameraDefaultDistance;
cameraRotation = CameraDefaultRotation;
}
// Check for input to rotate the camera up and down around the model.
if (currentKeyboardState.IsKeyDown(Keys.Up) ||
currentKeyboardState.IsKeyDown(Keys.W))
{
cameraArc += time * CameraRotateSpeed;
}
if (currentKeyboardState.IsKeyDown(Keys.Down) ||
currentKeyboardState.IsKeyDown(Keys.S))
{
cameraArc -= time * CameraRotateSpeed;
}
cameraArc += currentGamePadState.ThumbSticks.Right.Y * time *
CameraRotateSpeed;
// Limit the arc movement.
cameraArc = MathHelper.Clamp(cameraArc, -90.0f, 90.0f);
// Check for input to rotate the camera around the model.
if (currentKeyboardState.IsKeyDown(Keys.Right) ||
currentKeyboardState.IsKeyDown(Keys.D))
{
cameraRotation += time * CameraRotateSpeed;
}
if (currentKeyboardState.IsKeyDown(Keys.Left) ||
currentKeyboardState.IsKeyDown(Keys.A))
{
cameraRotation -= time * CameraRotateSpeed;
}
cameraRotation += currentGamePadState.ThumbSticks.Right.X * time *
CameraRotateSpeed;
// Check for input to zoom camera in and out.
if (currentKeyboardState.IsKeyDown(Keys.Z))
cameraDistance += time * CameraZoomSpeed;
if (currentKeyboardState.IsKeyDown(Keys.X))
cameraDistance -= time * CameraZoomSpeed;
cameraDistance += currentGamePadState.Triggers.Left * time
* CameraZoomSpeed;
cameraDistance -= currentGamePadState.Triggers.Right * time
* CameraZoomSpeed;
// clamp the camera distance so it doesn't get too close or too far away.
cameraDistance = MathHelper.Clamp(cameraDistance,
CameraMinDistance, CameraMaxDistance);
Matrix unrotatedView = Matrix.CreateLookAt(
new Vector3(0, 0, -cameraDistance), Vector3.Zero, Vector3.Up);
viewMatrix = Matrix.CreateRotationY(MathHelper.ToRadians(cameraRotation)) *
Matrix.CreateRotationX(MathHelper.ToRadians(cameraArc)) *
unrotatedView;
}
#endregion
}
#region Entry Point
///
/// The main entry point for the application.
///
static class Program
{
static void Main()
{
using (TrianglePickingGame game = new TrianglePickingGame())
{
game.Run();
}
}
}
#endregion
}