Debug Drawing
Some things are hard to debug by reading numbers: whether a bounding box is where you think it is, which way a spline is heading, what a second camera can actually see. Debug drawing lets you put that geometry on screen for a frame so you can look at it.
Everything on this page is immediate mode: geometry lasts one frame and is discarded once that frame has been rendered. To keep something visible, issue it again every frame from an update handler.
WireRenderer
WireRenderer draws wireframe shapes. Create one and keep it around:
import { Color, Vec3, WireRenderer } from 'playcanvas';
const wire = new WireRenderer(app);
app.on('update', (dt) => {
wire.color = Color.RED;
wire.sphere(new Vec3(0, 1, 0), 2);
});
State lives on the renderer
Rather than passing options to every call, the renderer holds the state its shapes use:
| Property | Meaning |
|---|---|
color | The color shapes are drawn in. Alpha is respected. |
layer | The layer to draw into, or null for the immediate layer. |
depthTest | Whether shapes are hidden by geometry in front of them. |
segments | How many line segments approximate a full circle. |
transform | A matrix applied to every point, or null. |
Set it once and draw as much as you like — drawing many shapes with the same state allocates nothing:
wire.color = Color.GREEN;
for (const item of items) {
wire.sphere(item.getPosition(), item.radius);
}
A second set of state is simply a second renderer. Instances hold no GPU resources, and instances sharing a layer and depth test mode are drawn together, so using several costs nothing extra:
const solid = new WireRenderer(app);
const xray = new WireRenderer(app);
xray.depthTest = false; // drawn on top of everything
transform is useful for drawing a group of shapes in some other space. Set it, draw, and every point is transformed on the way out:
wire.transform = entity.getWorldTransform();
wire.boxMinMax(localMin, localMax); // drawn in the entity's space
wire.transform = null;
Shapes
wire.line(start, end);
wire.lines(positions, colors); // Vec3[] pairs, optional Color[] per point
wire.linesPacked(positions, colors); // packed xyz / rgba numbers, the fastest form
wire.polyline(positions, colors); // an open strip
wire.loop(positions, colors); // a closed strip
wire.box(box); // BoundingBox or OrientedBox
wire.boxMinMax(min, max);
wire.sphere(center, radius);
wire.circle(center, normal, radius);
wire.cylinder(start, end, radius);
wire.capsule(start, end, radius);
wire.cone(apex, direction, angle, length);
wire.plane(center, normal, size);
wire.point(position, size);
wire.arrow(from, to);
wire.axes(matrix, size); // red, green and blue for x, y and z
wire.frustum(source); // a camera, or a view-projection matrix
wire.light(lightComponent); // the light's shape and extent, in its own color
For the line functions, colors is optional. Omit it and the renderer's color is used for everything; supply one color per point and each segment fades between its ends.
linesPacked takes plain arrays or Float32Arrays of numbers rather than Vec3 and Color instances, which makes it the cheapest way to submit a lot of geometry. Note that leaving colors off is a larger saving than the choice of array type — a uniform color avoids writing a color per vertex entirely.
Visualizing a camera
wire.frustum() draws the volume a camera can see. It works on a camera that is not currently rendering, which is the interesting case — you can fly one camera around while looking through another:
wire.color = Color.YELLOW;
wire.frustum(observerEntity.camera);
Combined with Frustum#containsAabb this makes culling visible:
const viewProjection = new Mat4().mul2(observer.camera.projectionMatrix, viewMatrix);
frustum.setFromMat4(viewProjection);
for (const meshInstance of meshInstances) {
const inside = frustum.containsAabb(meshInstance.aabb);
(inside ? greenWire : redWire).box(meshInstance.aabb);
}
A note on plane
The rotation of the square within its plane is derived from the normal you pass, and no such derivation is continuous over every direction. An animated normal will therefore make the square appear to jump as it passes the direction where that derivation switches. To rotate a square smoothly, pass a fixed normal and drive transform instead.
This does not affect circle, cylinder, capsule or cone, whose rings are symmetric about their axis.
Thick lines
Everything above draws single pixel lines, which is what you want for a debugging overlay. For lines that are part of the rendered scene — a highlighted path, a road network, an annotation — use WideLineRenderer instead. It supports width in screen pixels or world units, caps, joins, dashes and gradients, and it is retained rather than immediate: you add a line once and it stays until you remove it.
Performance counters
Debug drawing shows you where things are. To see how long they take, use MiniStats.
Examples
- Wire Shapes — every shape, animated
- Frustum Culling — a camera's view volume, and bounds colored by whether it contains them
- Lines — all of the line functions at once