Runs your own WGSL compute shader on the GPU to generate data — vertex positions, particle state, a simulation step — that the rest of the scene reads.
JavaScript API:
ComputeNode
The compute node is the concrete member of the AbstractComputeNodecompute-and-custom-WGSL family: it runs its own compute dispatch on the GPU
timeline — its own command buffer, possibly multi-step (steps-per-frame) and
stateful / self-re-arming (continuous) — decoupled from any consumer.
Consumers read its TYPED outputs by key (output.0 …) via a
compute-node="#id" reference on a compute-buffer-* source.
The element owns the binding ABI (group 0): the record-typed output
storage array, the 32-byte params uniform
(time/progress/output_size/output_count/output_stride), and the
packed named-uniform block (<sk-wgsl-uniform> children, read via the
generated skUniform_<name>() accessors). The author supplies ONLY the
@compute @workgroup_size(…) fn main kernel — declaring bindings in
author WGSL is a validation error.
Outputs are slices of one shared buffer, padded so each slice starts on a
256-byte boundary (minStorageBufferOffsetAlignment); records for output
i live at [i * output_stride, i * output_stride + output_size).
A kernel writes its OWN slice through output (invocation-owned, so
race-free), but reads any SIBLING slice through previous — a per-step
snapshot of output the scaffold also binds at group 0. Cross-slice reads
through output race the sibling invocation's own write and are undefined
per the WGSL spec; previous holds the committed previous-step state, so a
neighbour-reading continuous sim (e.g. n-body) evolves deterministically
regardless of GPU invocation order.
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.
<!-- ONE curve, THREE consumers. The SAME ring(t) math — a tilted circle
lifted by a z-wave, so it never crosses itself — is evaluated by three
compute nodes, one per output-kind (a node's output-kind fixes its record
type, so the three consumers need three nodes). The polyline node feeds a
SPLINE (the stroke tracing the ring); the point-matrices node feeds a
CLONER (beads placed along the ring); the color-stops node feeds the
beads' MATERIAL, looked up by each bead's progress around the ring. -->
<sk-scene>
<sk-directional-light p="42deg" h="130deg" intensity="1.0"></sk-directional-light>
<sk-ambient-light intensity="0.4"></sk-ambient-light>
<!-- Node 1 — the ring as a POLYLINE (SplineVertex per index). -->
<sk-compute-node id="ring-curve" output-kind="spline-polyline" output-size="256"
wgsl="
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid: vec3u) {
let i = gid.x;
if (i >= params.output_size) { return; }
let t = f32(i) / f32(params.output_size - 1u) * 6.2831853;
// Tilted circle + single z-wave: a clean, non-self-crossing ring.
let px = 3.2 * cos(t);
let py = 3.2 * sin(t);
let pz = 1.6 * sin(t);
output[i] = SplineVertex(px, py, pz, 1.0); // px, py, pz, width
}"></sk-compute-node>
<!-- Node 2 — the SAME ring as PLACEMENT MATRICES (PointData per bead). 8
beads spaced around the loop; each record is a translation matrix. -->
<sk-compute-node id="ring-points" output-kind="point-matrices" output-size="8"
wgsl="
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid: vec3u) {
let i = gid.x;
if (i >= params.output_size) { return; }
let t = f32(i) / f32(params.output_size) * 6.2831853; // 0..2PI, no duplicate
let p = vec3<f32>(3.2 * cos(t), 3.2 * sin(t), 1.6 * sin(t));
var pt: PointData; // column 3 = position, cols 0-2 = identity basis
pt.localMatrix = mat4x4<f32>(
vec4<f32>(1.0, 0.0, 0.0, 0.0),
vec4<f32>(0.0, 1.0, 0.0, 0.0),
vec4<f32>(0.0, 0.0, 1.0, 0.0),
vec4<f32>(p, 1.0));
output[i] = pt;
}"></sk-compute-node>
<!-- Node 3 — a purple / pink / gold PALETTE as three HARD BANDS
(color-stops: offset + colour, LINEAR RGBA). Coincident offsets
(…,1/3),(1/3,…) make a step, so each third of the range is one flat
colour — easy to read against the stroke and the beads. -->
<sk-compute-node id="ring-palette" output-kind="color-stops" output-size="6"
wgsl="
@compute @workgroup_size(6)
fn main(@builtin(global_invocation_id) gid: vec3u) {
let i = gid.x;
if (i >= params.output_size) { return; }
// 3 bands via coincident offsets: purple [0,1/3), pink
// [1/3,2/3), gold [2/3,1].
var offs = array<f32, 6>(0.0, 0.3333, 0.3333, 0.6667, 0.6667, 1.0);
let purple = vec4<f32>(0.227, 0.042, 0.456, 1.0); // #833AB4
let pink = vec4<f32>(0.982, 0.012, 0.250, 1.0); // #FD1D89
let gold = vec4<f32>(0.973, 0.434, 0.060, 1.0); // #FCB045
var cols = array<vec4<f32>, 6>(purple, purple, pink, pink, gold, gold);
output[i].offset = vec4<f32>(offs[i], 0.0, 0.0, 0.0);
output[i].color = cols[i];
}"></sk-compute-node>
<!-- CONSUMER 1: a spline stroke tracing the ring, painted with the SAME
compute palette by stroke PROGRESS — the reference band pattern that
shows what colour each position around the ring maps to. -->
<sk-progress-stroke-field id="along" clamp="true"></sk-progress-stroke-field>
<sk-compute-buffer-color-source id="ring-color" compute-node="#ring-palette"
intensity-field="#along"></sk-compute-buffer-color-source>
<sk-plain-material id="line-mat" base-color="white" base-color-darken-factor="0"
base-color-source="#ring-color"></sk-plain-material>
<sk-spline id="ring">
<sk-compute-buffer-spline-source compute-node="#ring-curve"></sk-compute-buffer-spline-source>
<sk-stroke-paint material="#line-mat" width="8" width-space="screen" line-cap="round"
width-profile='{"basis":"linear","points":[{"t":0.2,"width":0},{"t":1,"width":1}]}'></sk-stroke-paint>
</sk-spline>
<!-- CONSUMER 3 wiring: colour each bead by its PROGRESS around the ring
(spline-progress-gradient-field measures position along #ring), looked
up against the compute palette. The compute node sets the colours. -->
<sk-spline-progress-gradient-field id="around" spline-id="#ring"></sk-spline-progress-gradient-field>
<sk-compute-buffer-color-source id="bead-color" compute-node="#ring-palette"
intensity-field="#around"></sk-compute-buffer-color-source>
<sk-plain-material id="bead-mat" base-color="white" base-color-darken-factor="0"
base-color-source="#bead-color"></sk-plain-material>
<!-- CONSUMER 2: a cloner places one sphere template at each computed point. -->
<sk-cloner mode="vertex">
<sk-compute-buffer-points-source compute-node="#ring-points"></sk-compute-buffer-points-source>
<sk-sphere radius="0.35" segments="3">
<sk-surface-paint material="#bead-mat"></sk-surface-paint>
</sk-sphere>
</sk-cloner>
</sk-scene>
| Attribute | Type | Default | Description |
|---|---|---|---|
id |
<id> |
— | The element's unique identifier — the standard HTML global id attribute. |
draggable |
<boolean> |
false |
Gets whether this node can be dragged. |
wgsl |
<string> |
"" |
The author compute kernel: a `@compute |
src |
<string> |
— | URL of an external .wgsl file providing the kernel, mirroring |
output-kind |
<compute-output-kind> ("spline-polyline" | "point-matrices" | "color-array" | "color-stops") |
"spline-polyline" |
The typed output contract. |
output-count |
<number> |
1 |
Number of typed outputs the node produces (computed-property keys |
output-size |
<number> |
1 |
Records per output (e.g. vertices per polyline). |
steps-per-frame |
<number> |
1 |
Compute dispatches per frame (sub-steps). |
continuous |
<boolean> |
false |
Whether the node re-arms itself every frame (a continuous, stateful |
progress |
<number> |
0 |
Animatable progress value (0 to 1). |
Standard DOM event-handler content attributes — the value is JavaScript run when the event fires. They behave exactly as on any HTML element.
onclick, ondblclick, onauxclick, oncontextmenu, onpointerdown, onpointerup, onpointermove, onpointercancel, onpointerover, onpointerout, onpointerenter, onpointerleave, ongotpointercapture, onlostpointercapture, onwheel, ondragstart, ondrag, ondragenter, ondragover, ondragleave, ondrop, ondragend, onload, onerror