# WGSL helper functions shared between nodes. # # Files here beginning with `_` are not nodes; this one declares the helper # library every node may draw on by name. `build.rs` generates # `ops::helpers::` from each entry, and validates that a node's # `helpers:` list names something defined here — a typo is a build error # naming the key, not a WGSL compile failure in generated source. # # **Single source of truth.** The composer deduplicates helpers by *name*, so # two definitions of one name would silently emit whichever came first and two # nodes would compute, say, luminance differently depending on graph order. # That is a genuinely hard bug to see, which is why a helper is defined # exactly once, here, and referenced everywhere else. doc: | WGSL helper functions shared between operations. Generated from `ops/_helpers.yaml`. A node names the helpers it needs and the composer emits each one once, however many nodes asked for it. helpers: luminance: doc: | Rec. 709 luminance, the weighting that matches sRGB primaries. Applied to camera-space values it is an approximation — the true weights depend on the camera matrix — but using it here keeps the tonal operations working on sensor-native data, where highlight headroom still exists. wgsl: | fn luminance(c: vec3) -> f32 { return dot(c, vec3(0.2126, 0.7152, 0.0722)); } tone_position: doc: | Map linear luminance onto a perceptual 0..1 position. Tonal controls must feel evenly spaced to the eye, and linear light is not: middle grey sits at 0.18, so a linear weight would call almost everything a shadow. The cube root approximates lightness cheaply and behaves well near zero, where a log would diverge. wgsl: | fn tone_position(luma: f32) -> f32 { return clamp(pow(max(luma, 0.0), 1.0 / 3.0), 0.0, 1.0); } apply_tone_gain: doc: | Scale a colour by a gain while preserving its hue. Multiplying the three channels equally keeps chromaticity fixed, so lifting shadows does not desaturate them the way an additive lift would. wgsl: | fn apply_tone_gain(c: vec3, gain: f32) -> vec3 { return c * gain; } colour_saturation: doc: | How far a colour sits from grey, in 0..1. The max-minus-min definition (HSV chroma) rather than a standard deviation: it matches what the eye reads as 'colourfulness' and it is what makes vibrance's roll-off land where users expect. wgsl: | fn colour_saturation(c: vec3) -> f32 { let hi = max(c.r, max(c.g, c.b)); let lo = min(c.r, min(c.g, c.b)); if (hi <= 0.0) { return 0.0; } return (hi - lo) / hi; }