OK, so drawing lines is hard.
Well maybe not hard per se, but certainly tedious.
Looking into it (starting with Matt DesLauriers’ infamous ‘drawing lines is hard’ article linked above), I had the general gist.
To draw the lines I want, the way I want, I need to segment them and render triangles.

Like this, kind of…
No real surprise there, honestly.
Your GPU is a super-powered triangle crunching machine. While line primatives (line-list, line-strip) do exist, they’re not very useful–as far as I can tell. They’re typically aliased, hardware dependent and 1px wide.
Line Segment Renderer
I have now added a stand-alone LineSegmentRenderer to StaticRectangle. Stand-alone in that all GPU resources (buffers, layouts, pipelines etc.) are managed by the renderer itself and it’s separate from the main renderer.
It’s fairly basic right now and likely not as efficient as it can be (I can worry about this later), but for its current purpose–rendering debug locators etc–it’ll do the job just fine.
Speaking of locators, I also created a DebugLocatorRenderer extension. The reason for this separation (and this probably goes without saying…) is that there are other things I can use a LineSegmentRenderer for that isn’t rendering debug locator shapes.
The DebugLocatorRenderer creates the locators in the scene at render time, then renders them into the main render pass.
// Debug rendering
if (this.drawDebug){
this.debugRenderer.createLocators(scene, this.canvas);
this.debugRenderer.render(pass);
}
Line Segment Geometry
The LineSegmentRenderer requires line segments to render, so a LineSegment interface captures everything we need.
export interface LineSegment {
start: vec3;
end: vec3;
color: Uint8Array;
thickness: number;
}
We also need a way to keep track of them and provide some convenience functions for creating a LineSegment.
export class SegmentedLineGeometry {
private segments: LineSegment[] = [];
constructor() {
}
getSegments(): LineSegment[] {
return this.segments;
}
clear(): void {
this.segments = [];
}
addLine(start: vec3, end: vec3, color: Uint8Array, thickness: number = 1): void {
this.segments.push({ start, end, color, thickness });
}
addCircle(
center: vec3,
radius: number,
normal: vec3,
tangent: vec3,
color: Uint8Array,
thickness: number = 1,
segments: number = 32
): void {
this.addArc(
center, radius,
normal, tangent,
0.0, Math.PI * 2.0,
color, thickness, segments
);
}
addArc(
center: vec3,
radius: number,
normal: vec3,
tangent: vec3,
startAngle: number,
endAngle: number,
color: Uint8Array,
thickness: number = 1,
segments: number = 32
): void {
const binormal = vec3.create();
vec3.cross(binormal, normal, tangent);
vec3.normalize(binormal, binormal);
for (let i = 0; i < segments; i++) {
const t1 = startAngle + (i / segments) * (endAngle - startAngle);
const t2 = startAngle + ((i + 1) / segments) * (endAngle - startAngle);
const p1 = vec3.create();
const p2 = vec3.create();
vec3.scaleAndAdd(p1, center, tangent, Math.cos(t1) * radius);
vec3.scaleAndAdd(p1, p1, binormal, Math.sin(t1) * radius);
vec3.scaleAndAdd(p2, center, tangent, Math.cos(t2) * radius);
vec3.scaleAndAdd(p2, p2, binormal, Math.sin(t2) * radius);
this.addLine(p1, p2, color, thickness);
}
}
}
I now have locator shapes for the Directional, Spot and Point lights and I’m currently working on the Camera shape to visualise the camera frustum etc.
Those familar with Maya will recognise them as they’re heavily inspired by Mayas light shapes; it’s how I’m used to seeing them.

Don’t sue me Autodesk
Adding light shapes was immediately helpful. In my last post, I had two lights orbiting a skull mesh, I was fairly sure something wasn’t right with the directional light.
I had added an axis shape to the transform to make sure it was poining the correct way, of which it technically was. However, I had the direction of the light itself set incorrectly and had no way to tell. As soon as I had shape that represented the lights properties, I could see the problem immediately.
Excellent.
Instancing
It may seem unusual and inefficient to render a line by segmenting it. If you’re drawing straight lines, then sure, easy to understand, that’s just 4 points à la two triangles.
But what about a curve?
Yep, you potentially need a lot of segments to draw a smooth curve. If you were looping through each of them and drawing them one by one, then that probably would tank the performance.
But you don’t need to do that.
You can provide a single vertex buffer for the two triangles that make up a line segment and inform the renderer that you are going to render n instances of those two triangles in a single draw call, n being the number of segments.
All the properties of the LineSegment are uploaded to a separate specialised instance buffer and referenced by the shader to draw each segment in the correct place, with the correct colour, length and thickness.
The potential performance bottleneck isn’t the GPU rendering itself, but the CPU work of allocating new buffers and packing the segments every frame. This is something I’ll likely need to tune in the future.
Line Joins
There is an issue rendering line segments, what to you do between them? When the lines are thin and you’re far enough away, it’s not really a problem.
Get a little bit closer though…

A bit gappy
There are a few different ways to approach this, a typical one is to create geometry between them for a seamless join, you can also instance geometry for this but it would be a separate draw call.
Rye Terrell explains this in great detail which you can read about in his blog post Instanced Line Rendering Part I, another excellent, highly recommended resource.
I will be adding this as an option to my LineSegmentRenderer –for certain things it’s necessary.
However, I went a different route with my locators.
Screenspace Scaling
I could have just left them as they were–in all their gappy glory, they’re just debug locators after all.
But if you’ve used any 3d software, you’ll probably have noticed that the lines of these types of locators don’t get thicker the closer you get to them.
I don’t know for sure why this is. Is it intentional so your screen isn’t filled with locator geometry all the time? or it just an implementation detail, that they’re drawn with line primitives for performance reasons?
Regardless I wanted to replicate this for my locators. So my line_segment fragment shader will optionally scale line thickness depending on where the camera is–we refer to this as a ‘screenspace’ scale.
Does this solve the gap issue? See for yourself.
+/-increase/decrease locator scalesttoggles debug locatorsmtoggles MSAA~opens the terminal- Mouse look with left/right mouse buttons
w,a,s,dto movepto pause
An eagle-eye may have spotted that the point light locator always faces the camera, this is a fairly standard 2d technique typically referred to as ‘billboarding.’
I did have a 3d shape for the point light, but it looked weird from certain angles. The billboard version is a lot cleaner and being directionless, it also makes sense.