Paints a material's base color from a GPU-computed table of positioned color stops, looked up by a field's spatial weight — the escape hatch for a spatial color no built-in source computes, driven straight from a compute buffer with zero copy, with each color placed at its OWN parametric position.
JavaScript API:
ComputeBufferColorSource
This is the GPU-computed member of the ColorSource family: like every
color-source it gives a material a base color that varies across the surface,
but the colors come from a table a compute node produces on the GPU rather
than from a color-map, an image, or a procedural formula. It is the
GPU-computed analogue of sk-ramp-color-map: each record is a
(offset, color) stop, and an intensity-field DRIVER supplies a [0,1]
weight from where a point is that is resolved against those stops the same
way a ramp resolves its authored <sk-color-stop> offsets — find the
bracketing pair by position and interpolate. Because each stop carries its
own offset, the table can be non-uniformly spaced: dense stops in one
region, sparse in another, or a sharp feature at an arbitrary position — an
addressing a fixed i/(count-1) grid cannot express. A spherical, linear, or
composite field addresses the table radially, directionally, or carved,
exactly as it would drive a color-map.
Key attributes. compute-node (required) references the producing
sk-compute-node by id; the node must declare output-kind="color-stops".
compute-property-key selects which of its outputs to read (default
output.0); alpha-mode declares how the sampled records' alpha is
classified. intensity-field is the DRIVER whose [0,1] weight is looked up
against the stops; field (from the ColorSource base) is the
APPLICABILITY mask — where the source shows versus the flat base-color. Both
fields follow the same absence convention: an omitted intensity-field drives
the weight to 1 (the LAST stop's color), and an omitted field applies the
source at full weight everywhere.
Stop contract. Each record is 32 bytes — an offset vec4<f32> (the
[0,1] position in .x) and a color vec4<f32> (straight-alpha RGBA in
linear space), bound zero-copy from the producer's buffer. Offsets must be a
finite, monotonically non-decreasing sequence in [0,1]; the producing kernel
emits them sorted (the lookup does NOT sort on the GPU, so out-of-order data
yields undefined placement). A weight below the first offset or above the last
clamps to that endpoint stop's color; coincident offsets produce a hard step.
The lookup honors up to 100 stops. When the producer re-dispatches
continuously, the source keeps the scene re-rendering.
Related. Its producer is a sk-compute-node (see
AbstractComputeNode); its driver is any Field
(sk-linear-gradient-field, sk-spherical-gradient-field,
sk-composite-field, …). A material selects the source with
base-color-source="#id" (see sk-plain-material). Its CPU-authored
counterpart with arbitrary offsets is sk-ramp-color-map +
sk-color-stop; its field-driven color-map sibling is
sk-color-map-source; for hand-written per-fragment color instead of a
precomputed table, use sk-custom-wgsl-color-source.
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 colors are COMPUTED ON THE GPU and evolve over time — the one thing a
static color ramp cannot do. A `continuous` compute node runs a small
ripple simulation: each of 100 stops holds a wave height driven by the
sim clock (`params.time`), mapped through a THREE-colour palette
(trough -> mid -> crest). A vertical `intensity-field` looks that live
table up by height, and ONE material reads it — shared across three
surfaces / paint types (a thick helix STROKE, a capsule WIREFRAME, an
elongated cube SURFACE), proving the source is independent of what it
paints. The wave frequency, speed, and the three colours are
`sk-wgsl-*-uniform` inputs the kernel reads by name via
`skUniform_<name>()`; the engine binds `output`/`params`, the author
writes only `fn main`. -->
<sk-scene>
<sk-compute-node id="ripple" output-kind="color-stops" output-size="100" continuous
wgsl="
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid: vec3u) {
let i = gid.x;
if (i >= params.output_size) { return; }
// Each stop sits at a uniform offset along the table, 0..1
// (monotonically increasing — the color-stops contract).
let s = f32(i) / f32(params.output_size - 1u);
// Ripple field: two wave trains travelling in opposite
// directions, advanced by the sim clock. Where they meet they
// interfere, so the crests drift and beat rather than just
// scroll. h is a wave height in -1..1.
let k = skUniform_freq() * 6.2831853;
let phase = params.time * skUniform_speed();
let h = 0.5 * sin(k * s - phase)
+ 0.5 * sin(k * 0.7 * s + phase * 1.3);
// Map the height to a three-colour palette:
// trough (h=-1) -> mid (h=0) -> crest (h=+1).
let m = 0.5 * (h + 1.0);
let lo = mix(skUniform_trough(), skUniform_mid(), smoothstep(0.0, 0.5, m));
let color = mix(lo, skUniform_crest(), smoothstep(0.5, 1.0, m));
// Write this stop: its offset and its computed colour.
output[i].offset = vec4<f32>(s, 0.0, 0.0, 0.0);
output[i].color = color;
}">
<!-- Wave controls (skUniform_freq / skUniform_speed). `speed` is
animatable; kept to a gentle crawl here so the ripple drifts. -->
<sk-wgsl-uniform name="freq" value="1"></sk-wgsl-uniform>
<sk-wgsl-uniform name="speed" value="0.001"></sk-wgsl-uniform>
<!-- The three palette colours as LINEAR RGBA (sRGB #833AB4 / #FD1D89 /
#FC668B — purple -> pink -> coral), read as skUniform_<name>() -->
<sk-wgsl-vec4-uniform name="trough" x="0.2270" y="0.0423" z="0.4564" w="1"></sk-wgsl-vec4-uniform>
<sk-wgsl-vec4-uniform name="mid" x="0.9823" y="0.0123" z="0.2502" w="1"></sk-wgsl-vec4-uniform>
<sk-wgsl-vec4-uniform name="crest" x="0.9734" y="0.1342" z="0.2595" w="1"></sk-wgsl-vec4-uniform>
</sk-compute-node>
<!-- Driver: a vertical field looks the live table up by HEIGHT, so the
ripple reads as travelling horizontal bands on every surface (its +Z
ramp rotated to +Y with p="-90deg"). -->
<sk-linear-gradient-field id="up" y="-2" extent="4" p="-90deg" visualize></sk-linear-gradient-field>
<sk-compute-buffer-color-source id="ripple-source"
compute-node="#ripple" intensity-field="#up"></sk-compute-buffer-color-source>
<!-- ONE material, driven by the live ripple buffer, shared across three
surfaces and three paint types. -->
<sk-plain-material id="mat" base-color="white" base-color-darken-factor="0"
base-color-source="#ripple-source"></sk-plain-material>
<sk-directional-light p="42deg" h="130deg" intensity="1.0"></sk-directional-light>
<sk-ambient-light intensity="0.35"></sk-ambient-light>
<!-- LEFT — the color-stop reference helix (a cubic-Bézier curve), painted
as a thick round-capped STROKE so the ripples read clearly. -->
<sk-points-spline x="-5.5" basis="bezier" wrap="open"
points="[[1.2,-1.8,0],[1.2,-1.6,0.663],[0.663,-1.4,1.2],[0,-1.2,1.2],[-0.663,-1,1.2],[-1.2,-0.8,0.663],[-1.2,-0.6,0],[-1.2,-0.4,-0.663],[-0.663,-0.2,-1.2],[0,0,-1.2],[0.663,0.2,-1.2],[1.2,0.4,-0.663],[1.2,0.6,0],[1.2,0.8,0.663],[0.663,1,1.2],[0,1.2,1.2],[-0.663,1.4,1.2],[-1.2,1.6,0.663],[-1.2,1.8,0]]">
<sk-stroke-paint material="#mat" width-space="world" width="0.7" line-cap="round"></sk-stroke-paint>
</sk-points-spline>
<!-- CENTER — a capsule along Y, drawn as a WIREFRAME (edge paint), the
same material on its edges. -->
<sk-capsule radius="0.9" height="3" cap-segments="8" height-segments="8" rotation-segments="24">
<sk-edge-paint material="#mat"></sk-edge-paint>
</sk-capsule>
<!-- RIGHT — a cube elongated along Y, the same material as a SURFACE. -->
<sk-cube x="5.5" size-x="1.4" size-y="4.5" size-z="1.4">
<sk-surface-paint material="#mat"></sk-surface-paint>
</sk-cube>
</sk-scene>
| Attribute | Type | Default | Description |
|---|---|---|---|
id |
<id> |
— | The element's unique identifier — the standard HTML global id attribute. |
compute-nodeanimatable |
<id-ref> |
— | The producing compute node (instance, wired at connect via its id). reference by id |
compute-property-key |
<property-name> |
"output.0" |
The output key to read (output.N). |
alpha-mode |
<custom-wgsl-alpha-mode> ("auto" | "opaque" | "translucent" | "cutout") |
"auto" |
Author-declared alpha classification of the sampled records. |
intensity-fieldanimatable |
<id-ref> |
— | Gets the driver field feeding the [0,1] scalar the concrete source turns reference by id |
fieldanimatable |
<id-ref> |
— | Gets the optional applicability field that masks where this source reference by id |
xanimatable |
<number> |
0 |
Gets the X position component. |
yanimatable |
<number> |
0 |
Gets the Y position component. |
zanimatable |
<number> |
0 |
Gets the Z position component. |
hanimatable |
<angle> |
0 |
Gets the heading (Y-axis) rotation in radians. |
panimatable |
<angle> |
0 |
Gets the pitch (X-axis) rotation in radians. |
banimatable |
<angle> |
0 |
Gets the bank (Z-axis) rotation in radians. |
visualize |
<boolean> |
false |
Whether the visualization is visible (wireframe splines + center marker). |
interactive-controls |
<boolean> |
false |
Whether interactive controls are shown (draggable markers for adjusting parameters). |