Trails
A ribbon that follows a path: a strip of point pairs whose points, colours and visible length a system changes every frame, rewritten in place in one GPU buffer.
module: awake:trail
module: awake:scene:trail
ECS: TrailRenderer
Vulkan
WebGPU
Desktop · Android · iOS · Web
A trail is a strip of up to capacity point pairs. Pair i is two points, a and b, each with its
own straight-alpha colour, and the ribbon runs from pair to pair as one triangle strip. It covers a
melee swing's ribbon, a projectile's streak, a tyre mark, a beam or a lightning arc.
- Its GPU buffer is created once, at
capacity, the first frame the trail is drawn. A frame that changed pairs uploads only the range that changed; a frame that only changesvisibleuploads nothing; once warm, nothing is allocated per frame, with dozens of trails on screen. - It is drawn unlit, in the transparent pass, depth tested and not depth written, from both faces, through its entity's world matrix, so it moves with its parent. It casts no shadow.
- It is culled by the box around its visible pairs.
- An optional texture is sampled with
ualong the strip andvacross it:vis 0 ataand 1 atb. Without one, the colour is the vertices' alone.tintmultiplies every vertex's colour.
Place a trail from a scene document
{
"component": "trail_renderer",
"capacity": 300,
"blend": "Additive",
"tint": {"r": 1, "g": 0.6, "b": 0.2, "a": 1}
}
| Field | Default | What it does |
|---|---|---|
capacity |
64 |
The most pairs it holds, 2 to 65536. |
blend |
Additive |
Additive adds to what is behind it; Alpha covers it by its alpha. |
tint |
white | Multiplied into every vertex's colour. |
The document says how the trail is drawn. Its pairs, how many are visible and their colours are
written by a game's systems; texture is set from code.
Drive it from a system
val trail = world.get<TrailRenderer>(entity) ?: return
// When the swing starts: smooth a few sampled key pairs (base and tip) into a ribbon.
val pairs = trail.strip.setSmoothed(baseKeys, tipKeys, steps = 12, Color.White, Color.White, scratch)
trail.visible = 0
// Every frame: reveal a few more pairs and fade from the newest back towards black.
trail.visible = minOf(pairs, trail.visible + 6)
val newest = trail.visible - 1
for (pair in 0..newest) {
val fade = 1f - (newest - pair).toFloat() / newest.coerceAtLeast(1)
trail.strip.setColor(pair, fade, fade, fade, 1f)
}
setSmoothed runs a uniform Catmull-Rom curve through the keys, steps pairs from each key to the
next, and continues the path in a straight line past its first and last keys. Keep one
SmoothingScratch to smooth without allocating; catmullRom evaluates one point.
Without a scene
awake:trail has no scene dependency. An app with its own world draws trails through a
TrailBatch, given each trail's world matrix and the view's frustum planes:
val batch = TrailBatch(renderer)
val frustum = Frustum.planes(lens, aspect)
batch.collect(trail, worldMatrix, frustum, draws)
batch.releaseUnseen() // at the end of the frame: frees trails this frame did not draw
The draws need trailScenePipeline() (awake:asset:shader-pack) in the renderer's plan, which
ProjectRenderPlan includes. On a device without GpuDynamicMeshes, such as a headless host with
no renderer, a batch draws nothing and allocates no GPU memory.
Under the hood
The buffer is a DynamicMesh from the render contract's optional GpuDynamicMeshes capability:
vertices rewritten in place, a fixed index buffer, and a count of indices to draw. Vulkan keeps one
host-visible vertex buffer per frame in flight and brings each up to date, over the range written
since, when a frame that draws it is prepared. WebGPU writes the range on the queue at once, which
is ordered after the frames already submitted.
A trail is drawn by one batch: the batch takes the strip's record of changed pairs as it uploads.