PVG
17. 2d Debug Drawing — Program Video Games

17. 2d Debug Drawing

In this lesson, we'll extend the debug drawing system with some 2D shapes. This allows us to visualise collision shapes both in 3D and 2D.

Overview

Our debug drawing system currently supports:

  • 3D shapes (rectangles, circles, spheres, boxes) rendered in world space

We'll add:

  • 2D shapes (rectangles, circles) - rendered as screen overlays in this case
  • Text rendering with custom fonts

Adding Font Support

First, we need to add font support for clean text rendering. Add a new font file to assets (https://github.com/i-tu/Hasklig):

assets/fonts/Hasklig-Regular.ttf

You can use any font you like. I think this font is nice as a debug font.

Then update the game state to include font loading:

// In main.odin - Game_State struct
debug: struct {
    font: rl.Font,
    draw_commands: [dynamic]Debug_Draw_Command,
    draw_commands_2d: [dynamic]Debug_Draw_Command_2D,
},

Load the font during initialisation:

// In main() - after rl.InitWindow()
gs.debug.font = rl.LoadFontEx("assets/fonts/Hasklig-Regular.ttf", 16, nil, 128)

Debug Command Structure Refactoring

Let's reorganise our debug command structures for better consistency. Move the color field to the end of each struct:

Debug_Draw_Command_Rect_3D :: struct {
    rect: Rect,
    center: Vec3,
    z_offset: f32,
    rotation_matrix: Mat4,
    color: rl.Color,
}

Debug_Draw_Command_Circle_3D :: struct {
    center: Vec3,
    radius: f32,
    axis: Vec3,
    angle: f32,
    color: rl.Color,
}

Debug_Draw_Command_Sphere :: struct {
    center: Vec3,
    radius: f32,
    color: rl.Color,
}

Debug_Draw_Command_Box :: struct {
    obb: OBB3,
    color: rl.Color,
}

Adding 2D Debug Commands

Create new structures for 2D debug drawing:

Debug_Draw_Command_Rect :: struct {
    rect: Rect,
    color: rl.Color,
}

Debug_Draw_Command_Circle :: struct {
    center: Vec2,
    radius: f32,
    color: rl.Color,
}

Debug_Draw_Command_2D :: union {
    Debug_Draw_Command_Rect,
    Debug_Draw_Command_Circle,
}

Debug Drawing Functions

Add functions to push 2D debug commands and flush them:

debug_draw_push_2d :: proc(cmd: Debug_Draw_Command_2D) {
    append(&gs.debug.draw_commands_2d, cmd)
}

debug_draw_flush_2d :: proc() {
    for cmd in gs.debug.draw_commands_2d {
        switch v in cmd {
        case Debug_Draw_Command_Rect:
            rl.DrawRectangleRec(v.rect, v.color)
        case Debug_Draw_Command_Circle:
            rl.DrawCircleV(v.center, v.radius, v.color)
        }
    }
    clear(&gs.debug.draw_commands_2d)
}

Text Rendering Helpers

Create convenient text rendering functions:

debug_draw_text_ex :: proc(pos: Vec2, color: rl.Color, format: string, args: ..any) {
    rl.DrawTextEx(gs.debug.font, fmt.ctprintf(format, ..args), pos, 16, 0, color)
}

debug_draw_text :: proc(pos: Vec2, format: string, args: ..any) {
    rl.DrawTextEx(gs.debug.font, fmt.ctprintf(format, ..args), pos, 16, 0, rl.WHITE)
}

Cross-Section Visualisation

The most interesting addition is the cross-section visualisation in the physics system. This shows 2D representations of how a sphere intersects with oriented bounding boxes.

In the collision detection code, add visualisation for each cross-section:

// Calculate visualisation parameters
largest_obb_axis_size := max(obb.size.x, max(obb.size.y, obb.size.z))
viz_scale := f32(50)
viz_offset := Vec2{f32(gs.window_width) - largest_obb_axis_size * viz_scale - 20, 20}

// XY Cross-section (green)
viz_color := rl.Color{64, 255, 64, 80}

debug_draw_push_2d(Debug_Draw_Command_Rect{
    rect = {
        viz_offset.x,
        viz_offset.y,
        cross_section_xy.width * viz_scale,
        cross_section_xy.height * viz_scale
    },
    color = viz_color,
})

debug_draw_push_2d(Debug_Draw_Command_Circle{
    center = viz_offset + (local_sphere.xy + (obb.size.xy / 2)) * viz_scale,
    radius = sphere_radius * viz_scale,
    color = viz_color,
})

Repeat similar code for XZ (red) and ZY (blue) cross-sections, adjusting the viz_offset.y and colors accordingly.

Updating the Render Loop

Update the render function to use the new debug text system:

render :: proc() {
    // ... existing 3D rendering code ...
    
    rl.EndMode3D()
    
    debug_draw_flush_2d()
    
    debug_draw_text(Vec2{8, 8}, "Frame: %d, Frame Time: %f, FPS: %d, Session: %f", 
        gs.time.frame, gs.time.delta, gs.time.fps, gs.time.session)
    debug_draw_text(Vec2{8, 26}, "Up: %v, Down: %v, Left: %v, Right: %v", 
        gs.input.up, gs.input.down, gs.input.left, gs.input.right)
    
    rl.EndDrawing()
}

Understanding Cross-Section Visualisation

The cross-section visualisation shows three 2D views of how a sphere interacts with an oriented box:

  1. XY Cross-section (Green): Looking down the Z-axis
  2. XZ Cross-section (Red): Looking down the Y-axis
  3. ZY Cross-section (Blue): Looking down the X-axis

Each visualisation shows:

  • A rectangle representing the box's cross-section in that plane
  • A circle representing the sphere's cross-section in that plane

This is incredibly useful for debugging collision detection, as you can see exactly how the shapes intersect in each dimension.

Key Benefits

This debug drawing system provides:

  1. Visual Debugging: See collision shapes in real-time
  2. Cross-section Analysis: Understand 3D collisions through 2D projections

The system is designed to be non-intrusive - debug commands are queued during physics calculations and flushed during rendering, keeping the separation of concerns clean.

Next Steps

With this debug drawing system in place, we can:

  • Add more debug shape types as needed
  • Implement toggleable debug modes
  • Add colour coding for different collision states

The cross-section visualisation is particularly powerful for understanding complex 3D collision scenarios and will be useful as we implement the collision response code in the next lessons.