Wide Lines
WebGL and WebGPU can only draw lines one pixel wide. WideLineRenderer works around that by rendering each segment as camera-facing geometry, giving you lines with real thickness, rounded ends, mitred corners, dashes and colour gradients.
Use it for line geometry that is part of the rendered scene — a highlighted route, a road network, a measurement annotation, a trajectory. For single pixel wireframes used to inspect a scene while developing, see Debug Drawing instead.
Lines and renderers
There are two pieces. A WideLine holds the point data for one line. A WideLineRenderer owns a set of lines and draws them:
import { LINECAP_ROUND, LINEJOIN_ROUND, WideLine, WideLineRenderer } from 'playcanvas';
const renderer = new WideLineRenderer(app);
const line = new WideLine();
line.set(
new Float32Array([-2, 0, 0, 0, 1, 0, 2, 0, 0]), // three points, packed xyz
new Float32Array([1, 0, 0, 1, 1, 0, 0, 1, 1]), // a colour per point, packed rgb
new Float32Array([4, 12, 4]) // a width per point
);
line.cap = LINECAP_ROUND;
line.join = LINEJOIN_ROUND;
renderer.add(line);
app.on('destroy', () => renderer.destroy());
Unlike debug drawing, this is retained: add a line once and it stays until you remove() it. There is no need to reissue it every frame.
Colours and widths can each be given per point, or as a single value used by every point.
Styling a line
These are properties of the WideLine, so each line in a renderer can look different:
| Property | Purpose |
|---|---|
cap | How the ends are finished — LINECAP_BUTT, LINECAP_ROUND or LINECAP_SQUARE. |
join | How corners are joined — LINEJOIN_MITER, LINEJOIN_BEVEL or LINEJOIN_ROUND. |
closed | Joins the last point back to the first. |
dashLength, gapLength | Dash pattern. Leave dashLength at zero for a solid line. |
dashOffset | Shifts the dash pattern along the line, for marching-ants effects. |
Updating point data
setPositions, setColors and setWidths each replace their data while keeping the point count, reusing the line's existing storage:
line.setPositions(updatedPositions);
Use set() when the number of points itself needs to change.
Renderer settings
| Property | Purpose |
|---|---|
widthUnits | LINEWIDTH_SCREEN (default) measures width in screen pixels, so lines keep their thickness at any distance. LINEWIDTH_WORLD measures in world units, so they shrink with distance like geometry. |
layer | The layer to render into. Defaults to the Immediate layer. |
depthTest, depthWrite | Interaction with the depth buffer. Both default to true. |
enabled | Turns the whole renderer off. |
capacity | Instance buffer size, measured in segments. |
Performance
Every segment owned by a renderer is drawn as one GPU instance, and all of them are submitted together — so adding more WideLine objects does not add draw calls. A renderer with visible segments costs one draw call per camera that renders its layer.
The trade-off is that changing any line rebuilds the instance data for every line that renderer owns. So mixing rarely-changing and frequently-changing lines in one renderer makes the static data rebuild needlessly. There is no static/dynamic flag; you separate them with two renderers:
const staticLines = new WideLineRenderer(app);
const dynamicLines = new WideLineRenderer(app);
staticLines.add(roadNetwork); // built and uploaded once
dynamicLines.add(projectilePath); // updated every frame
capacity grows automatically, but setting it up front avoids GPU buffer reallocations when you know the maximum segment count. clear() removes the lines while keeping the capacity for reuse.
The batch is not frustum culled, so set enabled to false when none of its lines need drawing.
Limitations
- Rendering is opaque. Colours are rgb; the alpha component of a
Coloris ignored and transparent lines are not supported. - Call
destroy()to release the mesh, material and instance buffer. Lines detached this way remain usable and can be added to another renderer.
Examples
- Wide Line
- Wide Lines Styles — caps, joins, dashes and gradients side by side
- Wide Lines Dynamic — updating point data every frame