API Reference · Sprites, lines and points

LineNode

Namespace: Ava3D

public sealed class LineNode : Node

Unlit line segments, transformed by the node's world matrix.

What this is for is detail that shading cannot hold: panel seams, blueprint edges, the outline of a hatch. A texture blurs those as soon as the surface is at an angle or far away, and geometry that thin disappears into the depth buffer. Lines stay crisp because they are not surfaces.

The endpoints move. Write into LineNode.Positions and call LineNode.InvalidateGeometry for a wake or a waveform that keeps its length; assign a whole new array when the length changes, which invalidates on its own. What is not free is doing neither — the backends cache GPU buffers against the array's identity, so an array written in place and not declared changes nothing on screen.

Properties

MemberDescription
BlendMode Blend { get; set; }

How the line combines with the frame. BlendMode.Alpha by default.

Vector3 Color { get; set; }

Linear RGB of the line.

bool DepthTest { get; set; }

Whether the line is hidden by geometry in front of it. On by default.

bool DepthWrite { get; set; }

Whether the line writes depth. Off by default: lines are usually drawn over a surface they sit on, and writing depth from a primitive one pixel wide makes it fight with that surface.

BoundingBox LocalBounds { get; }

The extent of the endpoints, computed once on first use.

float Opacity { get; set; }

Opacity, 0..1. Lines drawn over a hull usually want a low value — they are a hint, not a border.

required Vector3[] Positions { get; set; }

Endpoint pairs: [0]–[1] is one segment, [2]–[3] the next. The length is even; a trailing odd vertex is ignored rather than joined to anything.

Assigning always counts as a change, even when the array handed in is the one already there — there is no cheap way to know whether its contents moved, and the assignment is the caller saying they did. Null is taken as empty rather than throwing, which draws nothing.

float Width { get; set; }

Width in pixels, measured on the screen and therefore the same at any distance. 1 by default.

Honoured on all three backends, but not by the same means. Skia strokes the line. OpenGL, WebGL 2 and Metal cannot: every mainstream GL driver clamps glLineWidth to 1, and Metal has no line-width control at all — so anything wider than one device pixel is drawn as two triangles per segment, expanded to the requested width after the perspective divide. That costs six vertices per segment instead of two and a second copy of the endpoints on the GPU, which is why a line that already lands on one device pixel stays on the cheap path. Note the device: this width is in pixels as the viewer sees them, so a hairline on a Retina display is two of them and takes the wide path rather than coming out half as heavy as it does everywhere else.

Ends are square-cut, so a polyline built from separate segments shows a notch where two of them meet at a sharp angle. That is the same shape on every backend, and the fix is to overlap the segments rather than to butt them together.

Methods

MemberDescription
void InvalidateGeometry()

Says that the contents of LineNode.Positions have changed, so the renderers upload them again, Node.LocalBounds is measured again, and the scene redraws.

This is how a line animates without allocating: keep one array the length the effect needs, write into it, and say so.

// A wake: every point steps back one place, and the newest is where the ship is now.
Array.Copy(wake.Positions, 0, wake.Positions, 1, wake.Positions.Length - 1);
wake.Positions[0] = ship.WorldPosition;
wake.InvalidateGeometry();

Unlike Mesh.InvalidateGeometry this needs no second call: the node is in the graph, so it can tell the scene itself. A mesh cannot, because it belongs to no one scene in particular.

The cost is one buffer upload per backend, and no managed allocation: the flattening scratch is reused. On OpenGL the GPU buffer is re-specified and keeps its name and its vertex array object. On Metal it is released and a new one taken, because a command buffer retains what it references until the GPU is finished and writing into a buffer a frame in flight is still reading is a race — so that one allocation is the cost of correctness rather than an oversight. Either way this is far cheaper than assigning a new array every frame, which creates and destroys buffers on every backend and is the trap both exist to close.

Worth knowing before you leave it out: the CPU fallback reads the array afresh every frame and has no buffer that can go stale, so forgetting this call looks like it works there and only breaks on OpenGL and Metal.

See also