1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14.

Pages

Thursday, March 31, 2011

Drawing Lines in XNA With SpriteBatch

Lines are a vital primitive that can greatly aid one's work during development. They can be used to draw the actual geometry of an object, show the distance between them, and let testers spot bugs earlier in development.

If you have a drawing interface that only relies on SpriteBatch and don't feel like messing with Vertex Buffers, you can cheat drawing lines using a single texture. By using a 1x1, white pixel, we can create a simple interface to draw lines of any size and color.

First you'll need to create the texture. Having one static copy may be preferred over creating the texture inside every instance. A current project of mine keeps a copy of this texture in a "PrimitiveManager", which oversees a number of different needs. We'll just keep a copy of this pixel in the Line class here for simplicity's sake.

public class Line {
    public static Texture2D Pixel;

    public static Initialize(GraphicsDevice graphics) {
        Pixel = new Texture2D(graphicsDevice, 1, 1, false, SurfaceFormat.Color);
    }
}

This will create our simple 1x1 pixel. By using a Color Overlay, we can have it render in any color we like. It will support transparency too.

What kind of parameters would we like to use when drawing lines? Most lines are defined by a start point, an endpoint, a color, and a line thickness.

Since we are using a 1x1 pixel, the distance between the start and endpoint will be the scale in one direction, and the line thickness in the other. The rotation passed into SpriteBatch will be the Atan2() of the x and y values from this difference.

To draw lines we are going to set our Origin to be {0.0f, 0.5f}. This is the center left of the pixel, and will thereby apply scaling in one direction.

public class Line {
    public static Texture2D Pixel;
    private static Vector2 Origin = new Vector2(0.0f, 0.5f);
    
    public Color color;
    public Vector2 start;
    public Vector2 end;
    public float lineThickness;
    // following is not needed unless you are doing front-to-back
    // or back-to-front drawing
    public float layer;
    
    public static InitializeLineDrawing(GraphicsDevice graphics) {
        Pixel = new Texture2D(graphicsDevice, 1, 1, false, SurfaceFormat.Color);
    }
    
    public void Draw(SpriteBatch spriteBatch) {
        Vector2 d = end - start;
        float angle = Math.Atan2(d.Y, d.X);
        // add 1 for single points and due to the way 
        // the origin is set up
        float distance = d.Length() + 1.0f;
        spriteBatch.Draw(Pixel, start, null, color, angle, Origin, 
            new Vector2(distance, lineThickness), SpriteEffects.None, layer);
    }
}

Why do we add 1 to our distance calculation? There are two reasons:
1) A single point, with a start and end vector in the same space, should draw one pixel on screen. If we don't add 1 to our existing result, single point lines will not be drawn at all because the distance between them will be zero.
2) The origin is offset from the center, and our single pixel texture already occupies a known distance of one. If you look carefully, without this addition, lines will always be drawn one unit short.

In general, there are a number of different ways you can go about implementing this. My code is really just a bare skeleton. The origin can just be passed in every time in the draw method with a hard coded new Vector2(0.0f, 0.5f) constructor call. You might want to write an unload method that disposes of the texture. You could precompute the distance and angle between two points and save them, perhaps as private variables. Properties could work wonderfully to this effect. When either Start or End is changed, simply compute these values and avoid running an expensive square root operation on every frame. Atan2() isn't that expensive, but if you are going to save the distance as a field you might as well do the same with the angle.