Skip to main content

Debug-renderer

Rapier is a physics engine, it doesn't render anything. What is displayed by your application comes from your own renderer, and game assets generally don’t match the shapes seen by the physics engine exactly. Therefore a collider given the wrong size, a joint attached at the wrong place, or a rigid-body that is not where its sprite is, can be difficult to debug (and easy to misinterpret as physics-engine-bugs).

To help with debugging physics, Rapier’s debug-renderer exists to convert the content of the physics scene into a set of colored lines that your application to obtain a wireframe view of what Rapier actually sees.

info

The debug-renderer is behind the debug-render cargo feature, which is disabled by default.

Despite its name, the debug-render doesn’t actually contains any windowing/rasterization/shader code. Instead, it provides to a backend implementing DebugRenderBackend the set of lines that needs to be displayed by your own graphics engine. This makes it possible to integrate the debug-rendering to any graphics stack your application might use. From the DebugRenderBackend, only the draw_line method must be implemented and everything else has a default implementation expressed in terms of individual lines.

Note that the colors are given in the HSLA format, therefore a backend expecting RGBA colors is expected to convert them:

// The backend receives the lines to be drawn. A real one would push them to the renderer of the
// application instead of collecting them.
struct LineCollector {
lines: Vec<(Vector, Vector, DebugColor)>,
}

impl DebugRenderBackend for LineCollector {
fn draw_line(&mut self, _object: DebugRenderObject, a: Vector, b: Vector, color: DebugColor) {
self.lines.push((a, b, color));
}
}

The DebugRenderPipeline is then given the sets of the scene once per rendered frame. Its DebugRenderStyle specifies the colors and various length properties, whereas its DebugRenderMode selects what is drawn so you can select only the element types you are interested in debugging.

// The style gives the colors and sizes, the mode selects what is drawn.
let mut debug_render = DebugRenderPipeline::new(
DebugRenderStyle::default(),
DebugRenderMode::COLLIDER_SHAPES | DebugRenderMode::CONTACTS,
);
let mut backend = LineCollector { lines: vec![] };

for _ in 0..10 {
world.step();

// The debug-rendering is done after the step, once per frame to be drawn.
backend.lines.clear();
debug_render.render(
&mut backend,
&world.bodies,
&world.colliders,
&world.impulse_joints,
&world.multibody_joints,
&world.narrow_phase,
&world.soft_bodies,
);
}

println!("{} lines to draw", backend.lines.len());
info

The debug-renderer is behind the debug-render-2d and debug-render-3d cargo features (the one matching the dimension of the crate is enabled by default, and can also be enabled with the debug-render alias). The rapier-debug-render feature enables the debug-render pipeline of Rapier alone, without any Bevy rendering dependency, in case you want to implement your own DebugRenderBackend.

The debug-renderer is enabled by adding the RapierDebugRenderPlugin to your app. It draws the lines with the gizmos of Bevy, after the propagation of the transforms, for every physics context. The plugin is also where the initial DebugRenderStyle (the colors and various length properties) and DebugRenderMode (what is drawn, so you can select only the element types you are interested in debugging) are given. Use RapierDebugRenderPlugin::default().disabled() to add the plugin without rendering anything until it is enabled:

.add_plugins(RapierDebugRenderPlugin {
// Only draw the collider shapes and the joints.
mode: DebugRenderMode::COLLIDER_SHAPES
| DebugRenderMode::IMPULSE_JOINTS
| DebugRenderMode::MULTIBODY_JOINTS,
..default()
})

After initialization, the debug-renderer is controlled by the DebugRenderContext resource:

  • enabled switches the debug-rendering on and off.
  • mode selects what is drawn, with one boolean per flag of DebugRenderMode (so it can be edited by the tools based on Bevy's reflection, like the other fields of this resource).
  • style gives the colors and the lengths of the lines.
  • default_collider_debug selects whether the collider shapes are drawn by default (ColliderDebug::AlwaysRender) or only for the colliders asking for it (ColliderDebug::NeverRender).
  • scale_lengths_by_length_unit (enabled by default) makes the lengths of the style expressed in meters: they are multiplied by the length unit of each physics context, e.g., by the pixels-per-meter in 2D.
fn modify_debug_render(mut debug_render: ResMut<DebugRenderContext>) {
// Toggle the debug-renderer.
debug_render.enabled = !debug_render.enabled;
// Draw the contacts and the AABBs too.
debug_render.mode.contacts = true;
debug_render.mode.collider_aabbs = true;
// Expressed in meters: multiplied by the length unit of each context.
debug_render.style.rigid_body_axes_length = 1.0;
}

Finally, the rendering of individual objects can be customized by adding the following components to their entities:

  • ColliderDebugColor overrides the color of the collider (and of its AABB).
  • ColliderDebug overrides the default_collider_debug of the DebugRenderContext for this collider (its shape, its AABB, and the contact pairs it is involved in).
  • DebugRenderColor overrides the color of everything attached to the entity: its collider (unless it has a ColliderDebugColor), its rigid-body axes, its joint, and its soft-body.
  • DebugRenderVisibility::Hidden hides everything attached to the entity (except a collider with a ColliderDebug component).
// This collider is drawn in red.
commands.spawn((
Transform::from_xyz(0.0, 400.0, 0.0),
RigidBody::Dynamic,
Collider::ball(50.0),
ColliderDebugColor(Hsla::hsl(0.0, 1.0, 0.5)),
));
// Everything attached to this entity (its collider, its rigid-body, its joint, etc.) is
// drawn in blue.
commands.spawn((
Transform::from_xyz(200.0, 400.0, 0.0),
RigidBody::Dynamic,
Collider::cuboid(50.0, 50.0),
DebugRenderColor(Hsla::hsl(220.0, 1.0, 0.3)),
));
// Nothing attached to this entity is drawn.
commands.spawn((
Transform::from_xyz(-200.0, 400.0, 0.0),
RigidBody::Dynamic,
Collider::cuboid(50.0, 50.0),
DebugRenderVisibility::Hidden,
));
// The shape of this collider is never drawn, whatever the `default_collider_debug`.
commands.spawn((
Transform::from_xyz(-400.0, 400.0, 0.0),
Collider::cuboid(50.0, 50.0),
ColliderDebug::NeverRender,
));

The debug shape data is computed by the World.debugRender method, which gives back one vertex buffer and one color buffer describing the lines to be drawn. These buffers are meant to be given to the line renderer of your application as they are:

for (let k = 0; k < 10; ++k) {
world.step();

// The buffers are the lines to be drawn: two floats per vertex, four per color, and two
// vertices per line. They are meant to be given to the line renderer of your application.
let buffers = world.debugRender();
console.log(buffers.vertices.length / 4, "lines to draw");
}

Note that the colliders can be filtered out of the debug-rendering, either by their query filter flags or by an arbitrary closure, which is useful when only a part of a large scene is of interest (not keep in mind that a closure will introduce an overhead).

Despite its name, the debug-renderer doesn’t actually contains any windowing/rasterization/shader code. Instead, r3DebugRender copies the set of lines that needs to be displayed by your own graphics engine into a buffer of R3DebugLine given by the application. Each line is given by its world-space end points (a and b) and its color. This makes it possible to integrate the debug-rendering to any graphics stack your application might use. Note that the colors are given in the HSLA format (the hue being in degrees), therefore a renderer expecting RGBA colors is expected to convert them:

// The lines are given with HSLA colors. A renderer expecting RGBA colors has to convert them.
static void hsla_to_rgba(const float hsla[4], float rgba[4]) {
float h = fmodf(hsla[0], 360.0f) / 60.0f;
float c = (1.0f - fabsf(2.0f * hsla[2] - 1.0f)) * hsla[1];
float x = c * (1.0f - fabsf(fmodf(h, 2.0f) - 1.0f));
float m = hsla[2] - c / 2.0f;
float r = 0.0f, g = 0.0f, b = 0.0f;

if (h < 1.0f) {
r = c;
g = x;
} else if (h < 2.0f) {
r = x;
g = c;
} else if (h < 3.0f) {
g = c;
b = x;
} else if (h < 4.0f) {
g = x;
b = c;
} else if (h < 5.0f) {
r = x;
b = c;
} else {
r = c;
b = x;
}

rgba[0] = r + m;
rgba[1] = g + m;
rgba[2] = b + m;
rgba[3] = hsla[3];
}

r3DebugRender is then called once per rendered frame. Its mode argument is a combination of the R3_DEBUG_* flags (e.g. R3_DEBUG_COLLIDER_SHAPES or R3_DEBUG_CONTACTS) that selects what is drawn, so you can select only the element types you are interested in debugging. Just like the scene queries returning several results, calling it with a NULL buffer and a zero capacity gives the number of lines, then a second call with a buffer large enough copies them (keep in mind that each of these calls computes all the lines):

// The mode selects what is drawn.
uint32_t mode = R2_DEBUG_COLLIDER_SHAPES | R2_DEBUG_CONTACTS;
// The buffer receiving the lines is kept from one frame to the next.
R2DebugLine *lines = NULL;
size_t capacity = 0;
size_t num_lines = 0;

for (int i = 0; i < 10; i++) {
r2Step(world, NULL, NULL);

// The debug-rendering is done after the step, once per frame to be drawn:
// get the number of lines, grow the buffer if needed, then copy the lines.
num_lines = r2DebugRender(world, mode, NULL, 0);
if (num_lines > capacity) {
capacity = num_lines;
lines = realloc(lines, capacity * sizeof(*lines));
}
num_lines = r2DebugRender(world, mode, lines, capacity);

for (size_t j = 0; j < num_lines; j++) {
float rgba[4];
hsla_to_rgba(lines[j].color, rgba);
// Give the segment from `lines[j].a` to `lines[j].b`, with the color `rgba`,
// to the renderer of the application.
}
}

printf("%zu lines to draw\n", num_lines);
free(lines);

The lines are drawn with the default style. The colors and various length properties of the lines can be customized by giving an R3DebugRenderStyle, initialized by r3DefaultDebugRenderStyle, to r3DebugRenderWithStyle:

// The style gives the colors (in HSLA) and sizes of the lines.
R2DebugRenderStyle style = r2DefaultDebugRenderStyle();
// Draw the colliders attached to dynamic rigid-bodies in blue.
style.collider_dynamic_color[0] = 240.0f;
style.collider_dynamic_color[1] = 1.0f;
style.collider_dynamic_color[2] = 0.5f;
style.collider_dynamic_color[3] = 1.0f;
// Draw longer contact normals.
style.contact_normal_length = 0.5;

num_lines = r2DebugRenderWithStyle(world, mode, &style, NULL, 0);
lines = malloc(num_lines * sizeof(*lines));
num_lines = r2DebugRenderWithStyle(world, mode, &style, lines, num_lines);

Despite its name, the debug-renderer doesn’t actually contains any windowing/rasterization/shader code. Instead, the DebugRenderPipeline gives the set of lines that needs to be displayed by your own graphics engine (e.g. matplotlib, pygame, or any 3D viewer) to a backend. This makes it possible to integrate the debug-rendering to any graphics stack your application might use. The backend can be any object with a draw_line method (as described by the DebugRenderBackend protocol), which is called for each line with the kind of object the line belongs to (a DebugRenderObject), the two end points of the line, and its color. Note that the colors are given in the HSLA format (the hue being in degrees), therefore a backend expecting RGBA colors is expected to convert them, e.g., with the DebugColor.rgba property:

# The backend receives the lines to be drawn. A real one would push them to the renderer of the
# application instead of collecting them.
class LineCollector:
def __init__(self):
self.lines = []

def draw_line(self, object, a, b, color):
# The color is given in the HSLA format: `color.rgba` converts it to RGBA.
self.lines.append((a, b, color.rgba))

The DebugRenderPipeline is then given the sets of the world once per rendered frame (the soft-bodies being optional). Its DebugRenderStyle specifies the colors and various length properties, whereas its DebugRenderMode selects what is drawn (its flags being combined with the | operator) so you can select only the element types you are interested in debugging:

# The style gives the colors and sizes, the mode selects what is drawn.
debug_render = rp.DebugRenderPipeline(
style=rp.DebugRenderStyle(),
mode=rp.DebugRenderMode.COLLIDER_SHAPES | rp.DebugRenderMode.CONTACTS,
)
backend = LineCollector()

for _ in range(10):
world.step()

# The debug-rendering is done after the step, once per frame to be drawn.
backend.lines.clear()
debug_render.render(
world.rigid_bodies,
world.colliders,
world.impulse_joints,
world.multibody_joints,
world.narrow_phase,
backend,
soft_bodies=world.soft_bodies,
)

print(f"{len(backend.lines)} lines to draw")

Calling a Python method for each line is slow. If you don't need to process the lines one by one, DebugRenderPipeline.render_to_arrays computes all of them without calling back into Python, and gives them as NumPy arrays. Giving a DebugLineCollector as the backend of DebugRenderPipeline.render also avoids these calls, the lines being collected until DebugLineCollector.clear is called:

# All the lines are computed without calling back into Python.
lines, colors, objects = debug_render.render_to_arrays(
world.rigid_bodies,
world.colliders,
world.impulse_joints,
world.multibody_joints,
world.narrow_phase,
soft_bodies=world.soft_bodies,
)
# `lines` has the shape (N, 2, 3): the two end points of each line.
# `colors` has the shape (N, 4): the RGBA color of each line.
# `objects` has the shape (N,): the kind of object each line belongs to.
collider_lines = lines[objects == rp.DebugRenderObject.COLLIDER.kind]
print(f"{len(collider_lines)} lines for the collider shapes")

Finally, the style and the mode can be changed at any time with the style and mode properties of the pipeline. The style property gives the style used by the pipeline itself, so modifying it changes the next renders (a whole DebugRenderStyle can also be assigned to it):

# The style is modified in place: the next renders take it into account.
debug_render.style.subdivisions = 40 # Smoother curved shapes.
debug_render.style.contact_normal_length = 0.5
debug_render.style.collider_fixed_color = rp.DebugColor.from_rgba(0.5, 0.5, 0.5, 1.0)

# Also draw the AABBs of the colliders.
debug_render.mode = debug_render.mode | rp.DebugRenderMode.COLLIDER_AABBS
warning

The debug-rendering is not free: it walks every collider of the scene and converts its shape into lines at each frame. Therefore it is meant to be enabled only when debugging rather than a player-facing representation of the game objects.