API Reference · Sprites, lines and points

PointsNode

Namespace: Ava3D

public sealed class PointsNode : Node

Unlit points, transformed by the node's world matrix. A starfield, a particle spray, a point cloud.

The case this exists for is the one a texture cannot cover: a couple of thousand stars two pixels across. Painted into an equirectangular sky map they land sub-texel and average into grey haze, and no amount of resolution fixes it — the look is that each star is a hard point rather than a filtered smudge.

The points move. Write into PointsNode.Positions and call PointsNode.InvalidateGeometry for a spray whose particles each go their own way; assign a whole new array when the count changes, which invalidates on its own. 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 points combine with the frame. BlendMode.Alpha by default.

Vector3 Color { get; set; }

Linear RGB of every point in the cloud.

bool DepthTest { get; set; }

Whether points are hidden by geometry in front of them. On by default.

bool DepthWrite { get; set; }

Whether points write depth. Off by default, so overlapping points still add up.

BoundingBox LocalBounds { get; }

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

float Opacity { get; set; }

Opacity, 0..1.

required Vector3[] Positions { get; set; }

Where the points are, in the node's own space.

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 Size { get; set; }

Point size — in pixels when PointsNode.SizeAttenuation is false, in world units when it is true.

bool SizeAttenuation { get; set; }

Whether points shrink with distance. Off by default, which gives every point the same size on screen wherever it is.

Off is the setting a starfield wants, and the reason is not performance: stars on a sphere two million units out have to look the same as stars on one at twenty million, because the sphere's radius is an implementation detail of how the sky is drawn and not something the player should be able to see.

Methods

MemberDescription
void InvalidateGeometry()

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

This is the difference between a particle system and a cloud on a growing node. Scaling a node moves every point at a speed proportional to how far out it already was, which is one specific answer; stepping each point yourself is all of the others — drag, gravity, wind, particles that stop.

for (var i = 0; i < spray.Positions.Length; i++)
    spray.Positions[i] += _velocity[i] * dt;

spray.InvalidateGeometry();

Unlike Mesh.InvalidateGeometry this needs no second call: the node is in the graph, so it can tell the scene itself.

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