goofi v3.0.0

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.

HarmonicInk wgsl
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

SlotDirectionType
inputinputTEXTURE
geometryinputARRAY
paletteinputARRAY
outoutputTEXTURE

Parameters

ink

NameTypeDefaultRangeDoc
stylestrnodalnodal | signed | magnitude | contours | densityZero lines, signed field, absolute value, contours, or an already computed nodal density.
rangefloat10.001 … 100Absolute field value that fills the color range.
widthfloat0.0450.001 … 0.4Nodal width relative to field range.
contoursfloat81 … 40

tone

NameTypeDefaultRangeDoc
warmthfloat0.450 … 1
exposurefloat1.60.1 … 6

Source

The current source of this node, as it stands in the goofi repository at node-bundles/harmonic-geometry/HarmonicInk.wgsl.

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);
}

← All nodes