Paints a material's base color from a WGSL fragment you write yourself — the
most general color-source, the escape hatch for any spatial → color rule the
built-in sources cannot express.
JavaScript API:
CustomWGSLColorSource
This is the author-WGSL member of the ColorSource family: like every
color-source it gives a material a base color that varies across the surface,
but here you supply the evaluator itself as WGSL rather than composing built-in
pieces. It is the color-source analogue of the wider custom-WGSL family (see
AbstractComputeNode).
The contract. The body must define an evaluateColorSourceRaw entry
function returning straight-alpha RGBA in LINEAR space (you may declare
additional helper functions alongside it):
``wgsl
fn evaluateColorSourceRaw(worldPos: vec3<f32>, uv: vec2<f32>, strokeCoord: vec2<f32>, strokeLength: f32, fragCoord: vec2<f32>) -> vec4<f32>
`
Each input is explicitly named for what it carries: worldPos and
fragCoord are always meaningful; uv is the mesh texture coordinate
(surface and edge paints); strokeCoord/strokeLength are the stroke
parametrization — arc-length progress in .x, cross position in .y,
total arc length in strokeLength (stroke paints). The framework declares
a module-scope constant SK_EVAL_SURFACE: bool — true when the shader is
generated for a surface/edge pipeline, false for a stroke pipeline — so
one body serving both topologies can branch on its context (never on
coordinate values, which cannot discriminate).
Masking is not your concern — the applicability field and the
mix(baseColor, source, weight) blend are applied outside the raw evaluator by
the shared wrapper. You declare no bind groups; instead you read your own
parameters through generated skUniform_<name>()` accessors, one per named
sk-wgsl-uniform child, so a uniform can be animated to drive the shader
without hand-packing a buffer.
This element's own world matrix. The kernel can read this element's own
transform: skWorldMatrix() -> mat4x4<f32> (parent chain included) and
skWorldMatrixInverse(). Author a spatial uniform (a position, a direction) in
the element's LOCAL frame and lift it with
(skWorldMatrix() * vec4<f32>(localPos, 1.0)).xyz (a direction uses w = 0),
or map worldPos back to source-local with the inverse — so the effect moves
WITH the geometry when the source (or an ancestor) is transformed. It is this
ELEMENT's transform, not the painted mesh's, so one source painting several
meshes exposes one consistent frame; place the source under the node whose
motion it should follow. Resolves in every pass — surface, edge, stroke,
stacked paint, and the shadow pipelines.
Key attributes. wgsl supplies the body inline; src loads it from an
external URL (src wins over inline, with no fallback on failure), and the
source is inert — the flat base-color shows through — until it resolves.
alpha-mode declares how the returned alpha is classified for compositing.
Related. A material selects it with base-color-source="#id" (see
sk-plain-material); its parameters are sk-wgsl-uniform,
sk-wgsl-vec2-uniform, sk-wgsl-vec3-uniform,
sk-wgsl-vec4-uniform, sk-wgsl-color-uniform (a colour authored
as a string), and sk-wgsl-f32-array-uniform children. For
a precomputed color array from a compute shader instead of a per-fragment
formula, use sk-compute-buffer-color-source; to layer it, use
sk-composite-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.
<!-- A hand-written WGSL color-source that colours a point by its DISTANCE to
three colour "dots": an inverse-distance (Shepard) blend of the three dot
colours, so each region reads close to its nearest dot and the colour flows
continuously between them. The dots are authored in the SOURCE's LOCAL frame
and lifted to world space in the kernel with `skWorldMatrix()` — so when the
whole cluster spins under a rotating parent, the colour field spins WITH the
geometry instead of sliding off it. The SAME source paints two very different
things: a big SPHERE sitting among the dots, and a thick comet-like STROKE
that swirls around each dot in turn. -->
<sk-scene>
<sk-directional-light p="42deg" h="-185deg" intensity="1.0"></sk-directional-light>
<sk-ambient-light intensity="0.55"></sk-ambient-light>
<sk-orthographic-camera height="13"></sk-orthographic-camera>
<!-- Two materials off the SAME source: the sphere shades normally; the trail
uses `base-color-darken-factor="0"` so it shows the field's pure colour.
They reference `#field-src` by id, so the source element itself can live
wherever its transform should come from — here, inside the turntable. -->
<sk-plain-material id="sphere-mat" base-color="white" base-color-source="#field-src"></sk-plain-material>
<sk-plain-material id="stroke-mat" base-color="white" base-color-darken-factor="0" base-color-source="#field-src"></sk-plain-material>
<!-- The TURNTABLE: a spinning parent node. Because the color-source, the dot
markers, the planet, and the comet all live under it, they rotate together
AND so does the source's world matrix — the colour stays locked to the
dots throughout the spin. (Move `#field-src` out to the scene root and the
field would freeze while the geometry turned — the slide-off this accessor
exists to prevent.) -->
<sk-scene-node id="turntable">
<sk-animation duration="12000ms" iterations="Infinity" easing="linear">
<sk-keyframe offset="0" h="0deg"></sk-keyframe>
<sk-keyframe offset="1" h="360deg"></sk-keyframe>
</sk-animation>
<!-- The shared distance-to-dots colour source, INSIDE the turntable so its
own world matrix carries the turntable's rotation. Three dots, each a
LOCAL-frame position + a LINEAR-rgba colour; `sharpness` concentrates
weight on the nearest dot. -->
<sk-custom-wgsl-color-source id="field-src"
wgsl="
// The author supplies ONLY this entry function (no bind groups); it
// returns straight-alpha RGBA in LINEAR space. `worldPos` is the
// fragment's WORLD position. `skWorldMatrix()` is THIS element's own
// world matrix (parent chain included) — it lifts a LOCAL-frame dot
// to world space so the field tracks the geometry under rotation.
// `skUniform_<name>()` reads a uniform child (dot positions/colours,
// `sharpness`) declared below.
fn evaluateColorSourceRaw(worldPos: vec3<f32>, uv: vec2<f32>, strokeCoord: vec2<f32>, strokeLength: f32, fragCoord: vec2<f32>) -> vec4<f32> {
// Gather the three colour dots (LOCAL position + colour) and the
// blend sharpness from the uniforms declared as children below.
var pos = array<vec3<f32>, 3>(skUniform_dot0Pos(), skUniform_dot1Pos(), skUniform_dot2Pos());
var col = array<vec4<f32>, 3>(skUniform_dot0Color(), skUniform_dot1Color(), skUniform_dot2Color());
let sharpness = skUniform_sharpness();
let toWorld = skWorldMatrix();
// Shepard (inverse-distance-power) blend: weight each dot by
// 1 / distance^sharpness, so the nearest dot dominates and the
// colour flows continuously between them (no hard Voronoi seams).
// A higher `sharpness` shrinks the muddy 3-way centre to a point.
var accColor = vec4<f32>(0.0);
var accWeight = 0.0;
for (var i = 0u; i < 3u; i = i + 1u) {
// Lift the LOCAL dot into world space so the comparison against
// the world-space `worldPos` holds as the cluster rotates.
let dotWorld = (toWorld * vec4<f32>(pos[i], 1.0)).xyz;
// Squared distance, floored so a point AT a dot stays finite.
let d2 = max(dot(worldPos - dotWorld, worldPos - dotWorld), 1e-5);
let w = 1.0 / pow(d2, 0.5 * sharpness);
accColor = accColor + w * col[i];
accWeight = accWeight + w;
}
return accColor / max(accWeight, 1e-6);
}">
<sk-wgsl-vec3-uniform name="dot0Pos" x="-3.0" y="1.6" z="0.0"></sk-wgsl-vec3-uniform>
<sk-wgsl-vec3-uniform name="dot1Pos" x="3.0" y="1.6" z="1.4"></sk-wgsl-vec3-uniform>
<sk-wgsl-vec3-uniform name="dot2Pos" x="0.0" y="-2.2" z="-1.2"></sk-wgsl-vec3-uniform>
<sk-wgsl-color-uniform name="dot0Color" color="#f4a01f"></sk-wgsl-color-uniform>
<sk-wgsl-color-uniform name="dot1Color" color="#f0533a"></sk-wgsl-color-uniform>
<sk-wgsl-color-uniform name="dot2Color" color="#1fb6a6"></sk-wgsl-color-uniform>
<sk-wgsl-uniform name="sharpness" value="5"></sk-wgsl-uniform>
</sk-custom-wgsl-color-source>
<!-- Little markers AT each dot plus the big planet, all painted by the SAME
field: a marker sits where the field is saturated with its dot's colour,
so it reads as that dot. -->
<sk-sphere x="-3.0" y="1.6" z="0.0" radius="0.35" segments="4">
<sk-surface-paint material="#sphere-mat"></sk-surface-paint>
</sk-sphere>
<sk-sphere x="3.0" y="1.6" z="1.4" radius="0.35" segments="4">
<sk-surface-paint material="#sphere-mat"></sk-surface-paint>
</sk-sphere>
<sk-sphere x="0.0" y="-2.2" z="-1.2" radius="0.35" segments="4">
<sk-surface-paint material="#sphere-mat"></sk-surface-paint>
</sk-sphere>
<!-- Consumer 1: a sphere among the dots, painted by the distance field. -->
<sk-sphere id="planet" radius="1.5" segments="5">
<sk-surface-paint material="#sphere-mat"></sk-surface-paint>
</sk-sphere>
<!-- Consumer 2: a comet stroke on a simple closed loop through the three
dots pushed radially outward, so it swings out around each dot in turn.
The SAME field paints it; `pen-down`/`pen-up` animate a fixed-length
window sliding along the loop, and `width-profile` fattens the leading
HEAD (t=1) and tapers the trailing tail (t=0). -->
<sk-spline>
<sk-points-spline-source basis="catmullRom" wrap="closed"
points="[[-4.75,2.34,-0.04],[4.62,2.28,2.12],[0,-3.9,-2.05]]"></sk-points-spline-source>
<sk-stroke-paint material="#stroke-mat" width="30" line-cap="round"
width-profile='{"basis":"linear","points":[{"t":0,"width":0.05},{"t":0.7,"width":0.35},{"t":1,"width":1}]}'>
<!-- Three equal segments, one per dot. A custom cubic-bezier whose
end tangents are NOT flat (unlike ease-in-out) slows the comet
as it rounds each dot WITHOUT stopping, then speeds up between. -->
<sk-animation duration="4000ms" iterations="Infinity">
<sk-keyframe offset="0" pen-down="0" pen-up="0.28" easing="cubic-bezier(0.45, 0.25, 0.55, 0.75)"></sk-keyframe>
<sk-keyframe offset="0.3333" pen-down="0.3333" pen-up="0.6133" easing="cubic-bezier(0.45, 0.25, 0.55, 0.75)"></sk-keyframe>
<sk-keyframe offset="0.6667" pen-down="0.6667" pen-up="0.9467" easing="cubic-bezier(0.45, 0.25, 0.55, 0.75)"></sk-keyframe>
<sk-keyframe offset="1" pen-down="1" pen-up="1.28"></sk-keyframe>
</sk-animation>
</sk-stroke-paint>
</sk-spline>
</sk-scene-node>
</sk-scene>
| Attribute | Type | Default | Description |
|---|---|---|---|
id |
<id> |
— | The element's unique identifier — the standard HTML global id attribute. |
wgsl |
<string> |
"" |
Inline WGSL shader body. Ignored while src is set. |
src |
<string> |
— | External WGSL source URL (src), or null. Assigning starts a load. |
alpha-mode |
<custom-wgsl-alpha-mode> ("auto" | "opaque" | "translucent" | "cutout") |
"auto" |
Author-declared fragment classification (alpha-mode). Drives |
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). |
Standard DOM event-handler content attributes — the value is JavaScript run when the event fires. They behave exactly as on any HTML element.
onload, onerror