HarmonicInk
Color and contours for a signed harmonic field.
Explore this node in the cookbook →
The parameter tables below cover this shader’s own declarations. The graphics engine also adds the shared size controls.
Wire HarmonicChladni.out to input, or HarmonicGeometry/GeometryBlend/HarmonicTransport.geometry directly to geometry. A valid geometry array takes precedence over the texture input. The raw field is unchanged. BioColors.rgb can feed palette directly as an [N,3] array. Alpha preserves domain masks. range is fixed, so morphs do not silently change their contrast.
input
geometry
palette
out
- Type name
graphics:HarmonicInk- Plane
graphics- Tags
imagetransform- Language
wgsl- Tier
shader- Bundle
harmonic-geometry- Source
node-bundles/harmonic-geometry/HarmonicInk.wgsl- Availability
- available
Slots
| Slot | Direction | Type | |
|---|---|---|---|
input | input | TEXTURE | |
geometry | input | ARRAY | |
palette | input | ARRAY | |
out | output | TEXTURE |
Parameters
ink
| Name | Type | Default | Range | Doc |
|---|---|---|---|---|
style | str | nodal | nodal | signed | magnitude | contours | density | Zero lines, signed field, absolute value, contours, or an already computed nodal density. |
range | float | 1 | 0.001 … 100 | Absolute field value that fills the color range. |
width | float | 0.045 | 0.001 … 0.4 | Nodal width relative to field range. |
contours | float | 8 | 1 … 40 |
tone
| Name | Type | Default | Range | Doc |
|---|---|---|---|---|
warmth | float | 0.45 | 0 … 1 | |
exposure | float | 1.6 | 0.1 … 6 |
Source
The current source of this node, as it stands in the goofi repository at node-bundles/harmonic-geometry/HarmonicInk.wgsl.
/* goofi
{ "doc": "Color and contours for a signed harmonic field.\nWire HarmonicChladni.out to input, or HarmonicGeometry/GeometryBlend/HarmonicTransport.geometry directly to geometry. A valid geometry array takes precedence over the texture input. The raw field is unchanged. BioColors.rgb can feed palette directly as an [N,3] array. Alpha preserves domain masks. range is fixed, so morphs do not silently change their contrast.",
"tags": ["image", "transform"],
"inputs": [{"name": "input", "kind": "TEXTURE"}, {"name": "geometry", "kind": "ARRAY"}, {"name": "palette", "kind": "ARRAY"}],
"params": [
{"group": "ink", "name": "style", "kind": "str", "default": "nodal", "options": ["nodal", "signed", "magnitude", "contours", "density"], "doc": "Zero lines, signed field, absolute value, contours, or an already computed nodal density."},
{"group": "ink", "name": "range", "kind": "float", "default": 1.0, "min": 0.001, "max": 100.0, "doc": "Absolute field value that fills the color range."},
{"group": "ink", "name": "width", "kind": "float", "default": 0.045, "min": 0.001, "max": 0.4, "doc": "Nodal width relative to field range."},
{"group": "ink", "name": "contours", "kind": "float", "default": 8.0, "min": 1.0, "max": 40.0},
{"group": "tone", "name": "warmth", "kind": "float", "default": 0.45, "min": 0.0, "max": 1.0},
{"group": "tone", "name": "exposure", "kind": "float", "default": 1.6, "min": 0.1, "max": 6.0}
] }
*/
fn color_at(t: f32) -> vec3f {
let dims = textureDimensions(palette);
if dims.x == 3u && dims.y >= 1u {
let at = clamp(t, 0.0, 1.0) * f32(dims.y - 1u);
let a = i32(floor(at)); let b = min(a + 1, i32(dims.y) - 1);
let ca = vec3f(textureLoad(palette, vec2i(0, a), 0).r, textureLoad(palette, vec2i(1, a), 0).r, textureLoad(palette, vec2i(2, a), 0).r);
let cb = vec3f(textureLoad(palette, vec2i(0, b), 0).r, textureLoad(palette, vec2i(1, b), 0).r, textureLoad(palette, vec2i(2, b), 0).r);
return max(mix(ca, cb, fract(at)), vec3f(0.0));
}
let dark = vec3f(0.006, 0.016, 0.031);
let cool = vec3f(0.035, 0.55, 0.53);
let warm = vec3f(1.0, 0.46, 0.14);
let hue = mix(cool, warm, p.warmth);
return mix(dark, hue, smoothstep(0.0, 0.65, t)) + vec3f(0.78, 0.85, 0.67) * pow(max(t, 0.0), 5.0);
}
fn shade(uv: vec2f) -> vec4f {
var sample = textureSample(input, samp, uv);
if goofi_geo_valid(geometry) {
sample = goofi_geo_field(geometry, uv);
if goofi_geo_header(geometry, 1u) == 12u { sample.r = length(sample.rg); }
}
let v = sample.r / p.range;
var tone = exp(-0.5 * v * v / (p.width * p.width));
switch p.style {
case 1u: { tone = clamp(0.5 + 0.5 * v, 0.0, 1.0); }
case 2u: { tone = clamp(abs(v), 0.0, 1.0); }
case 3u: { tone = pow(0.5 + 0.5 * cos(v * 6.28318530718 * p.contours), 10.0); }
case 4u: { tone = clamp(sample.r, 0.0, 1.0); }
default: {}
}
let color = 1.0 - exp(-color_at(tone) * p.exposure);
return vec4f(pow(max(color, vec3f(0.0)), vec3f(1.0 / 2.2)), sample.a);
}