#region File Description //----------------------------------------------------------------------------- // DebugShapeRenderer.cs // // Microsoft XNA Community Game Platform // Copyright (C) Microsoft Corporation. All rights reserved. //----------------------------------------------------------------------------- #endregion using System; using System.Collections.Generic; using System.Diagnostics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace ShapeRenderingSample { /// /// A system for handling rendering of various debug shapes. /// /// /// The DebugShapeRenderer allows for rendering line-base shapes in a batched fashion. Games /// will call one of the many Add* methods to add a shape to the renderer and then a call to /// Draw will cause all shapes to be rendered. This mechanism was chosen because it allows /// game code to call the Add* methods wherever is most convenient, rather than having to /// add draw methods to all of the necessary objects. /// /// Additionally the renderer supports a lifetime for all shapes added. This allows for things /// like visualization of raycast bullets. The game would call the AddLine overload with the /// lifetime parameter and pass in a positive value. The renderer will then draw that shape /// for the given amount of time without any more calls to AddLine being required. /// /// The renderer's batching mechanism uses a cache system to avoid garbage and also draws as /// many lines in one call to DrawUserPrimitives as possible. If the renderer is trying to draw /// more lines than are allowed in the Reach profile, it will break them up into multiple draw /// calls to make sure the game continues to work for any game. public static class DebugShapeRenderer { // A single shape in our debug renderer class DebugShape { /// /// The array of vertices the shape can use. /// public VertexPositionColor[] Vertices; /// /// The number of lines to draw for this shape. /// public int LineCount; /// /// The length of time to keep this shape visible. /// public float Lifetime; } // We use a cache system to reuse our DebugShape instances to avoid creating garbage private static readonly List cachedShapes = new List(); private static readonly List activeShapes = new List(); // Allocate an array to hold our vertices; this will grow as needed by our renderer private static VertexPositionColor[] verts = new VertexPositionColor[64]; // Our graphics device and the effect we use to render the shapes private static GraphicsDevice graphics; private static BasicEffect effect; // An array we use to get corners from frustums and bounding boxes private static Vector3[] corners = new Vector3[8]; // This holds the vertices for our unit sphere that we will use when drawing bounding spheres private const int sphereResolution = 30; private const int sphereLineCount = (sphereResolution + 1) * 3; private static Vector3[] unitSphere; /// /// Initializes the renderer. /// /// The GraphicsDevice to use for rendering. [Conditional("DEBUG")] public static void Initialize(GraphicsDevice graphicsDevice) { // If we already have a graphics device, we've already initialized once. We don't allow that. if (graphics != null) throw new InvalidOperationException("Initialize can only be called once."); // Save the graphics device graphics = graphicsDevice; // Create and initialize our effect effect = new BasicEffect(graphicsDevice); effect.VertexColorEnabled = true; effect.TextureEnabled = false; effect.DiffuseColor = Vector3.One; effect.World = Matrix.Identity; // Create our unit sphere vertices InitializeSphere(); } /// /// Adds a line to be rendered for just one frame. /// /// The first point of the line. /// The second point of the line. /// The color in which to draw the line. [Conditional("DEBUG")] public static void AddLine(Vector3 a, Vector3 b, Color color) { AddLine(a, b, color, 0f); } /// /// Adds a line to be rendered for a set amount of time. /// /// The first point of the line. /// The second point of the line. /// The color in which to draw the line. /// The amount of time, in seconds, to keep rendering the line. [Conditional("DEBUG")] public static void AddLine(Vector3 a, Vector3 b, Color color, float life) { // Get a DebugShape we can use to draw the line DebugShape shape = GetShapeForLines(1, life); // Add the two vertices to the shape shape.Vertices[0] = new VertexPositionColor(a, color); shape.Vertices[1] = new VertexPositionColor(b, color); } /// /// Adds a triangle to be rendered for just one frame. /// /// The first vertex of the triangle. /// The second vertex of the triangle. /// The third vertex of the triangle. /// The color in which to draw the triangle. [Conditional("DEBUG")] public static void AddTriangle(Vector3 a, Vector3 b, Vector3 c, Color color) { AddTriangle(a, b, c, color, 0f); } /// /// Adds a triangle to be rendered for a set amount of time. /// /// The first vertex of the triangle. /// The second vertex of the triangle. /// The third vertex of the triangle. /// The color in which to draw the triangle. /// The amount of time, in seconds, to keep rendering the triangle. [Conditional("DEBUG")] public static void AddTriangle(Vector3 a, Vector3 b, Vector3 c, Color color, float life) { // Get a DebugShape we can use to draw the triangle DebugShape shape = GetShapeForLines(3, life); // Add the vertices to the shape shape.Vertices[0] = new VertexPositionColor(a, color); shape.Vertices[1] = new VertexPositionColor(b, color); shape.Vertices[2] = new VertexPositionColor(b, color); shape.Vertices[3] = new VertexPositionColor(c, color); shape.Vertices[4] = new VertexPositionColor(c, color); shape.Vertices[5] = new VertexPositionColor(a, color); } /// /// Adds a frustum to be rendered for just one frame. /// /// The frustum to render. /// The color in which to draw the frustum. [Conditional("DEBUG")] public static void AddBoundingFrustum(BoundingFrustum frustum, Color color) { AddBoundingFrustum(frustum, color, 0f); } /// /// Adds a frustum to be rendered for a set amount of time. /// /// The frustum to render. /// The color in which to draw the frustum. /// The amount of time, in seconds, to keep rendering the frustum. [Conditional("DEBUG")] public static void AddBoundingFrustum(BoundingFrustum frustum, Color color, float life) { // Get a DebugShape we can use to draw the frustum DebugShape shape = GetShapeForLines(12, life); // Get the corners of the frustum frustum.GetCorners(corners); // Fill in the vertices for the bottom of the frustum shape.Vertices[0] = new VertexPositionColor(corners[0], color); shape.Vertices[1] = new VertexPositionColor(corners[1], color); shape.Vertices[2] = new VertexPositionColor(corners[1], color); shape.Vertices[3] = new VertexPositionColor(corners[2], color); shape.Vertices[4] = new VertexPositionColor(corners[2], color); shape.Vertices[5] = new VertexPositionColor(corners[3], color); shape.Vertices[6] = new VertexPositionColor(corners[3], color); shape.Vertices[7] = new VertexPositionColor(corners[0], color); // Fill in the vertices for the top of the frustum shape.Vertices[8] = new VertexPositionColor(corners[4], color); shape.Vertices[9] = new VertexPositionColor(corners[5], color); shape.Vertices[10] = new VertexPositionColor(corners[5], color); shape.Vertices[11] = new VertexPositionColor(corners[6], color); shape.Vertices[12] = new VertexPositionColor(corners[6], color); shape.Vertices[13] = new VertexPositionColor(corners[7], color); shape.Vertices[14] = new VertexPositionColor(corners[7], color); shape.Vertices[15] = new VertexPositionColor(corners[4], color); // Fill in the vertices for the vertical sides of the frustum shape.Vertices[16] = new VertexPositionColor(corners[0], color); shape.Vertices[17] = new VertexPositionColor(corners[4], color); shape.Vertices[18] = new VertexPositionColor(corners[1], color); shape.Vertices[19] = new VertexPositionColor(corners[5], color); shape.Vertices[20] = new VertexPositionColor(corners[2], color); shape.Vertices[21] = new VertexPositionColor(corners[6], color); shape.Vertices[22] = new VertexPositionColor(corners[3], color); shape.Vertices[23] = new VertexPositionColor(corners[7], color); } /// /// Adds a bounding box to be rendered for just one frame. /// /// The bounding box to render. /// The color in which to draw the bounding box. [Conditional("DEBUG")] public static void AddBoundingBox(BoundingBox box, Color color) { AddBoundingBox(box, color, 0f); } /// /// Adds a bounding box to be rendered for a set amount of time. /// /// The bounding box to render. /// The color in which to draw the bounding box. /// The amount of time, in seconds, to keep rendering the bounding box. [Conditional("DEBUG")] public static void AddBoundingBox(BoundingBox box, Color color, float life) { // Get a DebugShape we can use to draw the box DebugShape shape = GetShapeForLines(12, life); // Get the corners of the box box.GetCorners(corners); // Fill in the vertices for the bottom of the box shape.Vertices[0] = new VertexPositionColor(corners[0], color); shape.Vertices[1] = new VertexPositionColor(corners[1], color); shape.Vertices[2] = new VertexPositionColor(corners[1], color); shape.Vertices[3] = new VertexPositionColor(corners[2], color); shape.Vertices[4] = new VertexPositionColor(corners[2], color); shape.Vertices[5] = new VertexPositionColor(corners[3], color); shape.Vertices[6] = new VertexPositionColor(corners[3], color); shape.Vertices[7] = new VertexPositionColor(corners[0], color); // Fill in the vertices for the top of the box shape.Vertices[8] = new VertexPositionColor(corners[4], color); shape.Vertices[9] = new VertexPositionColor(corners[5], color); shape.Vertices[10] = new VertexPositionColor(corners[5], color); shape.Vertices[11] = new VertexPositionColor(corners[6], color); shape.Vertices[12] = new VertexPositionColor(corners[6], color); shape.Vertices[13] = new VertexPositionColor(corners[7], color); shape.Vertices[14] = new VertexPositionColor(corners[7], color); shape.Vertices[15] = new VertexPositionColor(corners[4], color); // Fill in the vertices for the vertical sides of the box shape.Vertices[16] = new VertexPositionColor(corners[0], color); shape.Vertices[17] = new VertexPositionColor(corners[4], color); shape.Vertices[18] = new VertexPositionColor(corners[1], color); shape.Vertices[19] = new VertexPositionColor(corners[5], color); shape.Vertices[20] = new VertexPositionColor(corners[2], color); shape.Vertices[21] = new VertexPositionColor(corners[6], color); shape.Vertices[22] = new VertexPositionColor(corners[3], color); shape.Vertices[23] = new VertexPositionColor(corners[7], color); } /// /// Adds a bounding sphere to be rendered for just one frame. /// /// The bounding sphere to render. /// The color in which to draw the bounding sphere. [Conditional("DEBUG")] public static void AddBoundingSphere(BoundingSphere sphere, Color color) { AddBoundingSphere(sphere, color, 0f); } /// /// Adds a bounding sphere to be rendered for a set amount of time. /// /// The bounding sphere to render. /// The color in which to draw the bounding sphere. /// The amount of time, in seconds, to keep rendering the bounding sphere. [Conditional("DEBUG")] public static void AddBoundingSphere(BoundingSphere sphere, Color color, float life) { // Get a DebugShape we can use to draw the sphere DebugShape shape = GetShapeForLines(sphereLineCount, life); // Iterate our unit sphere vertices for (int i = 0; i < unitSphere.Length; i++) { // Compute the vertex position by transforming the point by the radius and center of the sphere Vector3 vertPos = unitSphere[i] * sphere.Radius + sphere.Center; // Add the vertex to the shape shape.Vertices[i] = new VertexPositionColor(vertPos, color); } } /// /// Draws the shapes that were added to the renderer and are still alive. /// /// The current game timestamp. /// The view matrix to use when rendering the shapes. /// The projection matrix to use when rendering the shapes. [Conditional("DEBUG")] public static void Draw(GameTime gameTime, Matrix view, Matrix projection) { // Update our effect with the matrices. effect.View = view; effect.Projection = projection; // Calculate the total number of vertices we're going to be rendering. int vertexCount = 0; foreach (var shape in activeShapes) vertexCount += shape.LineCount * 2; // If we have some vertices to draw if (vertexCount > 0) { // Make sure our array is large enough if (verts.Length < vertexCount) { // If we have to resize, we make our array twice as large as necessary so // we hopefully won't have to resize it for a while. verts = new VertexPositionColor[vertexCount * 2]; } // Now go through the shapes again to move the vertices to our array and // add up the number of lines to draw. int lineCount = 0; int vertIndex = 0; foreach (DebugShape shape in activeShapes) { lineCount += shape.LineCount; int shapeVerts = shape.LineCount * 2; for (int i = 0; i < shapeVerts; i++) verts[vertIndex++] = shape.Vertices[i]; } // Start our effect to begin rendering. effect.CurrentTechnique.Passes[0].Apply(); // We draw in a loop because the Reach profile only supports 65,535 primitives. While it's // not incredibly likely, if a game tries to render more than 65,535 lines we don't want to // crash. We handle this by doing a loop and drawing as many lines as we can at a time, capped // at our limit. We then move ahead in our vertex array and draw the next set of lines. int vertexOffset = 0; while (lineCount > 0) { // Figure out how many lines we're going to draw int linesToDraw = Math.Min(lineCount, 65535); // Draw the lines graphics.DrawUserPrimitives(PrimitiveType.LineList, verts, vertexOffset, linesToDraw); // Move our vertex offset ahead based on the lines we drew vertexOffset += linesToDraw * 2; // Remove these lines from our total line count lineCount -= linesToDraw; } } // Go through our active shapes and retire any shapes that have expired to the // cache list. bool resort = false; for (int i = activeShapes.Count - 1; i >= 0; i--) { DebugShape s = activeShapes[i]; s.Lifetime -= (float)gameTime.ElapsedGameTime.TotalSeconds; if (s.Lifetime <= 0) { cachedShapes.Add(s); activeShapes.RemoveAt(i); resort = true; } } // If we move any shapes around, we need to resort the cached list // to ensure that the smallest shapes are first in the list. if (resort) cachedShapes.Sort(CachedShapesSort); } /// /// Creates the unitSphere array of vertices. /// private static void InitializeSphere() { // We need two vertices per line, so we can allocate our vertices unitSphere = new Vector3[sphereLineCount * 2]; // Compute our step around each circle float step = MathHelper.TwoPi / sphereResolution; // Used to track the index into our vertex array int index = 0; // Create the loop on the XY plane first for (float a = 0f; a < MathHelper.TwoPi; a += step) { unitSphere[index++] = new Vector3((float)Math.Cos(a), (float)Math.Sin(a), 0f); unitSphere[index++] = new Vector3((float)Math.Cos(a + step), (float)Math.Sin(a + step), 0f); } // Next on the XZ plane for (float a = 0f; a < MathHelper.TwoPi; a += step) { unitSphere[index++] = new Vector3((float)Math.Cos(a), 0f, (float)Math.Sin(a)); unitSphere[index++] = new Vector3((float)Math.Cos(a + step), 0f, (float)Math.Sin(a + step)); } // Finally on the YZ plane for (float a = 0f; a < MathHelper.TwoPi; a += step) { unitSphere[index++] = new Vector3(0f, (float)Math.Cos(a), (float)Math.Sin(a)); unitSphere[index++] = new Vector3(0f, (float)Math.Cos(a + step), (float)Math.Sin(a + step)); } } /// /// A method used for sorting our cached shapes based on the size of their vertex arrays. /// private static int CachedShapesSort(DebugShape s1, DebugShape s2) { return s1.Vertices.Length.CompareTo(s2.Vertices.Length); } /// /// Gets a DebugShape instance for a given line counta and lifespan. /// private static DebugShape GetShapeForLines(int lineCount, float life) { DebugShape shape = null; // We go through our cached list trying to find a shape that contains // a large enough array to hold our desired line count. If we find such // a shape, we move it from our cached list to our active list and break // out of the loop. int vertCount = lineCount * 2; for (int i = 0; i < cachedShapes.Count; i++) { if (cachedShapes[i].Vertices.Length >= vertCount) { shape = cachedShapes[i]; cachedShapes.RemoveAt(i); activeShapes.Add(shape); break; } } // If we didn't find a shape in our cache, we create a new shape and add it // to the active list. if (shape == null) { shape = new DebugShape { Vertices = new VertexPositionColor[vertCount] }; activeShapes.Add(shape); } // Set the line count and lifetime of the shape based on our parameters. shape.LineCount = lineCount; shape.Lifetime = life; return shape; } } }