Draws a spline whose polyline is computed on the GPU by a compute node — the way to render a curve generated by a simulation or parallel kernel (a particle trail, an evolving wave, any per-vertex position array a compute pass writes).
JavaScript API:
ComputeBufferSplineSource
This is the GPU-fed member of the spline-source family: a spline-source supplies
the curve geometry for a AbstractSpline, so it lives as the single
geometry child of a sk-spline. It owns no geometry of its own and
allocates no buffers — it names a sk-compute-node producer and reads that
node's output polyline, which the renderer binds zero-copy; CPU-side queries
(pointAt, bounds, cloner matrices) come from the inherited readback path and
are null until the first readback completes.
Key attributes. compute-node is a #id reference to the
sk-compute-node that produces the polyline — the direct, common wire-in.
compute-property-key selects which of that node's outputs to read
(default output.0). is-closed declares whether the generated polyline forms
a closed loop (last vertex joins the first). Like every spline-source it also
carries the family's distribution / distribution-count controls: a
spline-source is itself a points-source, so a sk-cloner given one places
clones along the curve, and these controls choose how the curve is sampled into
those positions.
Related. It is one input modality of the spline-source family alongside
sk-svg-spline-source, sk-points-spline-source,
sk-trail-spline-source, sk-morph-spline-source, and its GPU
sibling sk-custom-wgsl-spline-source (which generates the polyline from
inline WGSL instead of a separate producer). Its producer is a
sk-compute-node; its host is a sk-spline, whose curve then draws
with a stroke paint or carries an sk-align-to-spline-constraint.
Serve these docs to run the live example.
WebGPU needs a secure context — open this page via grunt serve
rather than double-clicking the file. The Markup tab works from disk.
<!-- The spline's polyline is produced entirely on the GPU: a compute node runs
a WGSL kernel that writes one SplineVertex per output index, tracing a
3D trefoil knot whose ribbon swells and thins along its length (a
per-vertex width), the swells travelling as the wave's phase advances
each frame (`continuous`). A three-colour gradient runs along the ribbon
and TRAVELS by animating the progress field's `remap-phase` — the whole
lookup slides while every colour stays put in the ramp, so the palette
flows without the muddy inter-keyframe averaging that animating stop
colours would cause. The source reads that node's `output.0` buffer
zero-copy — no CPU geometry is authored. Swap the kernel math and the
curve changes; this is the escape hatch for simulation-driven splines. -->
<sk-scene>
<!-- `continuous` re-dispatches the kernel every frame, so the width wave's
moving phase actually animates: the swells travel along the ribbon. -->
<sk-compute-node id="sim" output-size="512" continuous
wgsl="
// ── The compute KERNEL ──────────────────────────────────────────
// The author supplies ONLY this @compute entry point named `main`.
// The engine has already declared everything it reads/writes (do
// NOT redeclare these): the `output` array, the `params` uniform,
// and the `SplineVertex` record struct. One invocation runs per
// output index.
//
// @builtin(global_invocation_id) gid : vec3u
// The invocation's global id. gid.x is THIS vertex's index i.
//
// params : ComputeNodeParams (engine uniform), fields:
// params.output_size : u32 — vertex count (here 512)
// params.time : f32 — scene clock in MILLISECONDS
// params.progress : f32 — normalized play progress
//
// output[i] : SplineVertex — the vertex this invocation writes:
// SplineVertex(px, py, pz, width) — position + a per-vertex
// width MULTIPLIER that scales the stroke paint's width
// (1.0 = unchanged). Here it swells the ribbon along the curve.
//
// skUniform_<name>() — reads a <sk-wgsl-uniform> child by name.
// skUniform_radius() -> f32 (declared below as `radius`)
// skUniform_cycles() -> f32 (declared below as `cycles`)
// skUniform_speed() -> f32 (declared below as `speed`)
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid: vec3u) {
let i = gid.x;
// Guard: dispatch is rounded up to the workgroup size, so the
// last group has extra invocations — skip out-of-range ones.
if (i >= params.output_size) { return; }
// Parameter along the closed curve, 0 .. 2*PI.
let t = f32(i) / f32(params.output_size - 1u) * 6.283185;
// A trefoil knot, scaled by the `radius` uniform.
let s = skUniform_radius();
let px = (sin(t) + 2.0 * sin(2.0 * t)) * s;
let py = (cos(t) - 2.0 * cos(2.0 * t)) * s;
let pz = (-sin(3.0 * t)) * s;
// Per-vertex width: a sinusoid swelling between 2 and 4, with
// `cycles` full oscillations around the loop. Subtracting a
// time term (params.time is MS) advances the wave's PHASE each
// frame, so the swells travel along the ribbon. Using a whole
// number of cycles keeps the width continuous at the seam
// where the closed curve rejoins itself.
let phase = params.time * skUniform_speed();
let width = 3 + sin(skUniform_cycles() * t - phase);
output[i] = SplineVertex(px, py, pz, width);
}">
<!-- Custom uniforms, read in the kernel by name. `cycles` should be a
positive whole number so the width joins smoothly at the seam. -->
<sk-wgsl-uniform name="radius" value="1.2"></sk-wgsl-uniform>
<sk-wgsl-uniform name="cycles" value="4"></sk-wgsl-uniform>
<sk-wgsl-uniform name="speed" value="0.003"></sk-wgsl-uniform>
</sk-compute-node>
<!-- BASE layer: colour the stroke by progress along the curve. The
progress field's `remap-curve` folds 0..1 progress into a 0->1->0
triangle, so the 3-stop ramp reads symmetrically (purple at both ends,
cornflowerblue at the middle) and meets itself at the closed-loop
seam — no need for mirrored stops. Animating `remap-phase` 0->1 slides
that triangle's lookup all the way around, so the bright band TRAVELS
along the ribbon — a travelling gradient with pure, stable colours
(only the lookup moves; the ramp is untouched). -->
<sk-progress-stroke-field id="along" clamp="true"
remap-curve="points:[[0,0],[0.5,1],[1,0]]">
<sk-animation duration="6s" iterations="Infinity">
<sk-keyframe offset="0" remap-phase="0"></sk-keyframe>
<sk-keyframe offset="1" remap-phase="1"></sk-keyframe>
</sk-animation>
</sk-progress-stroke-field>
<sk-ramp-color-map id="ramp">
<sk-color-stop stop-offset="0" stop-color="#833AB4"></sk-color-stop>
<sk-color-stop stop-offset="0.5" stop-color="#FD1D89"></sk-color-stop>
<sk-color-stop stop-offset="1" stop-color="cornflowerblue"></sk-color-stop>
</sk-ramp-color-map>
<sk-color-map-source id="gradient-src" intensity-field="#along" color-map="#ramp"></sk-color-map-source>
<!-- TOP layer: a single white rim on the ribbon's outer EDGES. A
cross-stroke-field is full at the centreline and 0 at the edges;
`remap-curve="inverse:linear"` flips it so the weight is high at the
edges instead. A tiny `softness` (0.02) keeps the rim edge sharp — a
thin anti-aliased transition, not a blurry bleed — and
`cap-field-mode="match"` wraps the rim around the round caps. Its
color-map ramps from transparent white to opaque white, so the field
weight fades the rim in — transparent in the core (a no-op), rising to
a translucent grey only at the edges. -->
<sk-cross-stroke-field id="edges" threshold="0.85" softness="0.2"
cap-field-mode="match" remap-curve="inverse:linear"></sk-cross-stroke-field>
<sk-ramp-color-map id="rim">
<sk-color-stop stop-offset="0" stop-color="rgba(100,100,100, 0)"></sk-color-stop>
<sk-color-stop stop-offset="1" stop-color="rgba(100,100,100, 0.5)"></sk-color-stop>
</sk-ramp-color-map>
<sk-color-map-source id="edge-src" intensity-field="#edges" color-map="#rim"></sk-color-map-source>
<!-- Composite: the edge rim MULTIPLIED over the gradient, so the edges are
shaded down toward grey (a soft ambient-occlusion-like darkening). -->
<sk-composite-color-source id="comp" color-sources="#gradient-src #edge-src"
blend-modes="normal multiply"></sk-composite-color-source>
<sk-plain-material id="mat" base-color="white" base-color-darken-factor="0"
base-color-source="#comp"></sk-plain-material>
<sk-spline>
<sk-compute-buffer-spline-source compute-node="#sim"
compute-property-key="output.0" is-closed="true"></sk-compute-buffer-spline-source>
<sk-stroke-paint material="#mat" width="0.25" width-space="world"
line-join="round" line-cap="round"></sk-stroke-paint>
<sk-animation duration="60s" iterations="Infinity">
<sk-keyframe offset="0" h="0deg"></sk-keyframe>
<sk-keyframe offset="1" h="360deg"></sk-keyframe>
</sk-animation>
</sk-spline>
</sk-scene>
| Attribute | Type | Default | Description |
|---|---|---|---|
is-closedanimatable |
<boolean> |
false |
Whether the spline forms a closed loop (last vertex joins the first). |
compute-nodeanimatable |
<id-ref> |
— | The directly referenced compute node, or null when the source is reference by id |
compute-property-key |
<property-name> |
"output.0" |
The computed-property key read from the referenced compute node |
distributionanimatable |
<spline-distribution> ("control-points" | "tessellated" | "arc-length-spaced") |
— | How clone points are placed along the spline — at the control points, at |
distribution-count |
<number> |
— | The number of evenly spaced points to place when the distribution is |