@nodetool-ai/gpu 0.8.3 → 0.8.4-a24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- 2026-10-12T12:29:47.616Z
1
+ 2026-10-16T07:29:05.468Z
@@ -1,36 +1,39 @@
1
1
  /**
2
- * `filters.grain@1` — film grain as a multiplicative gain wobble.
2
+ * `filters.grain@1` — film grain modelled on exposed stock.
3
3
  *
4
- * **Multiplied, not added.** The input is premultiplied, so `rgb <= a` holds on
5
- * every pixel and adding noise would break it: a translucent layer would come
6
- * back with channels brighter than its own alpha, which reads as a bright
7
- * fringe once it composites. A scalar gain preserves the invariant (the same
8
- * reason `filters.vignette@1` may scale), and it is also closer to how exposed
9
- * stock behaves — grain bites in the midtones and leaves black black, because
10
- * there is nothing there to modulate.
4
+ * **Bell-shaped, clumped, two octaves.** Each lattice point draws roughly
5
+ * Gaussian noise (three hashes summed), and smooth value noise interpolates
6
+ * between points, so `size` grows soft clumps rather than square blocks. A
7
+ * second octave at twice the frequency adds the fine layer real grain has.
11
8
  *
12
- * **Reproducible.** The pattern is a hash of the grain cell and `seed`, with no
9
+ * **Follows exposure.** The noise is added to the straight colour and weighted
10
+ * by luma: zero in pure black, strongest through shadows and midtones, thinner
11
+ * in the highlights. The result is clamped to 0..1 and premultiplied again, so
12
+ * `rgb <= a` holds by construction on a translucent layer.
13
+ *
14
+ * **Reproducible.** The pattern is a hash of the lattice and `seed`, with no
13
15
  * clock and no RNG state, so two renders of the same frame are byte-identical
14
16
  * and a cached render can be handed back. A host that wants the pattern to roll
15
- * passes the frame's own time as the seed.
17
+ * passes the frame's own time as the seed; the shader folds it into a small
18
+ * exact integer so large frame times keep their precision.
16
19
  */
17
20
  import * as d from "typegpu/data";
18
21
  export declare const GrainParams: d.WgslStruct<{
19
- /** Gain wobble, 0..1. 0 leaves the picture untouched. */
22
+ /** Grain strength, 0..1. 1 is a 0.15 deviation in midtones. 0 is a no-op. */
20
23
  amount: d.F32;
21
- /** Grain cell size in source pixels. 1 is per-pixel. */
24
+ /** Grain clump size in source pixels. 1 is per-pixel; larger is softer, coarser stock. */
22
25
  size: d.F32;
23
- /** 0 = one gain for the pixel (monochrome), 1 = per-channel colour noise. */
26
+ /** 0 = one noise value for the pixel (monochrome), 1 = per-channel colour noise. */
24
27
  colorAmount: d.F32;
25
28
  /** Rolls the pattern. Same seed, same grain. */
26
29
  seed: d.F32;
27
30
  }>;
28
31
  export declare const filtersGrainV1: import("../../../../module.js").ShaderModule<d.WgslStruct<{
29
- /** Gain wobble, 0..1. 0 leaves the picture untouched. */
32
+ /** Grain strength, 0..1. 1 is a 0.15 deviation in midtones. 0 is a no-op. */
30
33
  amount: d.F32;
31
- /** Grain cell size in source pixels. 1 is per-pixel. */
34
+ /** Grain clump size in source pixels. 1 is per-pixel; larger is softer, coarser stock. */
32
35
  size: d.F32;
33
- /** 0 = one gain for the pixel (monochrome), 1 = per-channel colour noise. */
36
+ /** 0 = one noise value for the pixel (monochrome), 1 = per-channel colour noise. */
34
37
  colorAmount: d.F32;
35
38
  /** Rolls the pattern. Same seed, same grain. */
36
39
  seed: d.F32;
@@ -1 +1 @@
1
- {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../../../../../src/shaders/filters/grain/v1/module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,CAAC,MAAM,cAAc,CAAC;AAGlC,eAAO,MAAM,WAAW;IACtB,yDAAyD;;IAEzD,wDAAwD;;IAExD,6EAA6E;;IAE7E,gDAAgD;;EAEhD,CAAC;AAQH,eAAO,MAAM,cAAc;IAhBzB,yDAAyD;;IAEzD,wDAAwD;;IAExD,6EAA6E;;IAE7E,gDAAgD;;GAoFhD,CAAC"}
1
+ {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../../../../../src/shaders/filters/grain/v1/module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,CAAC,MAAM,cAAc,CAAC;AAGlC,eAAO,MAAM,WAAW;IACtB,6EAA6E;;IAE7E,0FAA0F;;IAE1F,oFAAoF;;IAEpF,gDAAgD;;EAEhD,CAAC;AAQH,eAAO,MAAM,cAAc;IAhBzB,6EAA6E;;IAE7E,0FAA0F;;IAE1F,oFAAoF;;IAEpF,gDAAgD;;GA0IhD,CAAC"}
@@ -1,28 +1,31 @@
1
1
  /**
2
- * `filters.grain@1` — film grain as a multiplicative gain wobble.
2
+ * `filters.grain@1` — film grain modelled on exposed stock.
3
3
  *
4
- * **Multiplied, not added.** The input is premultiplied, so `rgb <= a` holds on
5
- * every pixel and adding noise would break it: a translucent layer would come
6
- * back with channels brighter than its own alpha, which reads as a bright
7
- * fringe once it composites. A scalar gain preserves the invariant (the same
8
- * reason `filters.vignette@1` may scale), and it is also closer to how exposed
9
- * stock behaves — grain bites in the midtones and leaves black black, because
10
- * there is nothing there to modulate.
4
+ * **Bell-shaped, clumped, two octaves.** Each lattice point draws roughly
5
+ * Gaussian noise (three hashes summed), and smooth value noise interpolates
6
+ * between points, so `size` grows soft clumps rather than square blocks. A
7
+ * second octave at twice the frequency adds the fine layer real grain has.
11
8
  *
12
- * **Reproducible.** The pattern is a hash of the grain cell and `seed`, with no
9
+ * **Follows exposure.** The noise is added to the straight colour and weighted
10
+ * by luma: zero in pure black, strongest through shadows and midtones, thinner
11
+ * in the highlights. The result is clamped to 0..1 and premultiplied again, so
12
+ * `rgb <= a` holds by construction on a translucent layer.
13
+ *
14
+ * **Reproducible.** The pattern is a hash of the lattice and `seed`, with no
13
15
  * clock and no RNG state, so two renders of the same frame are byte-identical
14
16
  * and a cached render can be handed back. A host that wants the pattern to roll
15
- * passes the frame's own time as the seed.
17
+ * passes the frame's own time as the seed; the shader folds it into a small
18
+ * exact integer so large frame times keep their precision.
16
19
  */
17
20
  import tgpu from "typegpu";
18
21
  import * as d from "typegpu/data";
19
22
  import { defineModule } from "../../../../module.js";
20
23
  export const GrainParams = d.struct({
21
- /** Gain wobble, 0..1. 0 leaves the picture untouched. */
24
+ /** Grain strength, 0..1. 1 is a 0.15 deviation in midtones. 0 is a no-op. */
22
25
  amount: d.f32,
23
- /** Grain cell size in source pixels. 1 is per-pixel. */
26
+ /** Grain clump size in source pixels. 1 is per-pixel; larger is softer, coarser stock. */
24
27
  size: d.f32,
25
- /** 0 = one gain for the pixel (monochrome), 1 = per-channel colour noise. */
28
+ /** 0 = one noise value for the pixel (monochrome), 1 = per-channel colour noise. */
26
29
  colorAmount: d.f32,
27
30
  /** Rolls the pattern. Same seed, same grain. */
28
31
  seed: d.f32
@@ -58,9 +61,45 @@ fn hash31(p: vec3f) -> f32 {
58
61
  return fract((p3.x + p3.y) * p3.z);
59
62
  }
60
63
 
61
- /** Signed noise in [-1, 1] for one grain cell and channel. */
62
- fn grainAt(cell: vec2f, seed: f32, channel: f32) -> f32 {
63
- return hash31(vec3f(cell.x, cell.y, seed + channel * 19.19)) * 2.0 - 1.0;
64
+ /**
65
+ * Approximately Gaussian noise with unit deviation at one lattice point: the
66
+ * sum of three uniform hashes (Irwin-Hall), centred and rescaled. Real grain is
67
+ * the sum of many silver crystals, so its density is bell-shaped, not flat.
68
+ */
69
+ fn gaussAt(cell: vec2f, seed: f32) -> f32 {
70
+ let a = hash31(vec3f(cell.x, cell.y, seed));
71
+ let b = hash31(vec3f(cell.x + 57.0, cell.y + 57.0, seed));
72
+ let c = hash31(vec3f(cell.x + 113.0, cell.y + 113.0, seed));
73
+ return (a + b + c - 1.5) * 2.0;
74
+ }
75
+
76
+ /**
77
+ * Smooth value noise over the grain lattice. Interpolating between lattice
78
+ * points turns a large size into soft clumps instead of hard square blocks. At
79
+ * integer coordinates the weights are exactly zero, so size 1 returns the
80
+ * lattice value itself: fine, per-pixel grain.
81
+ */
82
+ fn valueNoise(p: vec2f, seed: f32) -> f32 {
83
+ let i = floor(p);
84
+ let f = p - i;
85
+ let u = f * f * (vec2f(3.0) - 2.0 * f);
86
+ let n00 = gaussAt(i, seed);
87
+ let n10 = gaussAt(i + vec2f(1.0, 0.0), seed);
88
+ let n01 = gaussAt(i + vec2f(0.0, 1.0), seed);
89
+ let n11 = gaussAt(i + vec2f(1.0, 1.0), seed);
90
+ return mix(mix(n00, n10, u.x), mix(n01, n11, u.x), u.y);
91
+ }
92
+
93
+ /**
94
+ * Two octaves: the clump at the chosen size and a finer layer at twice the
95
+ * frequency. The doubling and the fixed offset keep every floored coordinate an
96
+ * exact float, so the CPU mirror can reproduce the lattice bit for bit.
97
+ */
98
+ fn grainAt(p: vec2f, seed: f32, channel: f32) -> f32 {
99
+ let s = seed + channel * 19.19;
100
+ let coarse = valueNoise(p, s);
101
+ let fine = valueNoise(p * 2.0 + vec2f(17.0, 29.0), s + 7.0);
102
+ return (coarse + 0.5 * fine) * 0.8944;
64
103
  }
65
104
 
66
105
  @compute @workgroup_size(16, 16, 1)
@@ -70,24 +109,42 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
70
109
  let coords = vec2<i32>(i32(gid.x), i32(gid.y));
71
110
  let v = layout.$.params;
72
111
  let color = textureLoad(layout.$.source, coords, 0);
112
+ if (color.a <= 0.0) {
113
+ textureStore(layout.$.outputTexture, coords, color);
114
+ return;
115
+ }
73
116
 
74
- // Quantize to grain cells so size coarsens the stock instead of just
75
- // reseeding it. Sub-pixel sizes collapse back to per-pixel.
76
- let cell = floor(vec2f(f32(gid.x), f32(gid.y)) / max(v.size, 1.0));
117
+ // Fold the seed into a small exact integer, then turn it into a whole-pixel
118
+ // shift of the lattice. Frame-time seeds run into the hundreds of thousands,
119
+ // where f32 hashing loses the bits that make consecutive frames differ.
120
+ let folded = v.seed - 8192.0 * floor(v.seed / 8192.0);
121
+ let shift = floor(vec2f(
122
+ hash31(vec3f(folded, 1.7, 9.2)),
123
+ hash31(vec3f(folded, 4.1, 3.3))
124
+ ) * 512.0);
125
+ let p = vec2f(f32(gid.x), f32(gid.y)) / max(v.size, 1.0) + shift;
77
126
 
78
- let mono = grainAt(cell, v.seed, 0.0);
79
- let perChannel = vec3f(
80
- grainAt(cell, v.seed, 1.0),
81
- grainAt(cell, v.seed, 2.0),
82
- grainAt(cell, v.seed, 3.0)
83
- );
84
- let n = mix(vec3f(mono), perChannel, clamp(v.colorAmount, 0.0, 1.0));
127
+ let mono = grainAt(p, folded, 0.0);
128
+ var n = vec3f(mono);
129
+ if (v.colorAmount > 0.0) {
130
+ let perChannel = vec3f(
131
+ grainAt(p, folded, 1.0),
132
+ grainAt(p, folded, 2.0),
133
+ grainAt(p, folded, 3.0)
134
+ );
135
+ n = mix(n, perChannel, clamp(v.colorAmount, 0.0, 1.0));
136
+ }
85
137
 
86
- let gain = vec3f(1.0) + n * clamp(v.amount, 0.0, 1.0);
87
- // Saturating against alpha rather than 1.0 is what keeps rgb <= a when the
88
- // gain runs above 1 on an already-bright premultiplied pixel.
89
- let rgb = clamp(color.rgb * gain, vec3f(0.0), vec3f(color.a)); // premul: ok
90
- textureStore(layout.$.outputTexture, coords, vec4<f32>(rgb, color.a));
138
+ // Work on the straight colour: grain follows exposure, not coverage.
139
+ let straight = color.rgb / color.a;
140
+ let luma = dot(straight, vec3f(0.2126, 0.7152, 0.0722));
141
+ // Film response: nothing to expose in pure black, strongest through the
142
+ // shadows and midtones, thinning out toward the highlights.
143
+ let response = clamp(luma * 6.0, 0.0, 1.0) * (1.0 - 0.65 * luma * luma);
144
+ let sigma = clamp(v.amount, 0.0, 1.0) * 0.15 * response;
145
+ let grained = clamp(straight + n * sigma, vec3f(0.0), vec3f(1.0));
146
+ // Re-premultiplying a value in 0..1 keeps rgb <= a by construction.
147
+ textureStore(layout.$.outputTexture, coords, vec4<f32>(grained * color.a, color.a));
91
148
  }
92
149
  `,
93
150
  io: {
@@ -1 +1 @@
1
- {"version":3,"file":"module.js","sourceRoot":"","sources":["../../../../../src/shaders/filters/grain/v1/module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,IAAI,MAAM,SAAS,CAAC;AAC3B,OAAO,KAAK,CAAC,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAErD,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,yDAAyD;IACzD,MAAM,EAAE,CAAC,CAAC,GAAG;IACb,wDAAwD;IACxD,IAAI,EAAE,CAAC,CAAC,GAAG;IACX,6EAA6E;IAC7E,WAAW,EAAE,CAAC,CAAC,GAAG;IAClB,gDAAgD;IAChD,IAAI,EAAE,CAAC,CAAC,GAAG;CACZ,CAAC,CAAC;AAEH,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC;IAClC,MAAM,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE;IAC5B,aAAa,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE;IAC/C,MAAM,EAAE,EAAE,OAAO,EAAE,WAAW,EAAE;CACjC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,cAAc,GAAG,YAAY,CAAC;IACzC,EAAE,EAAE,eAAe;IACnB,OAAO,EAAE,CAAC;IACV,OAAO,EAAE,UAAU;IACnB,QAAQ,EAAE,SAAS;IACnB,0EAA0E;IAC1E,iDAAiD;IACjD,SAAS,EAAE,eAAe;IAC1B,IAAI,EAAE,SAAS;IACf,MAAM,EAAE,WAAW;IACnB,aAAa,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE;IAC9D,OAAO,EAAE;QACP,MAAM,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE;QACvD,IAAI,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE;QACnD,WAAW,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE;QAC5D,IAAI,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE;KACxB;IACD,MAAM;IACN,aAAa,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;IAC1B,IAAI,EAAE,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsClB;IACC,EAAE,EAAE;QACF,MAAM,EAAE;YACN,MAAM,EAAE;gBACN,UAAU,EAAE,MAAM;gBAClB,KAAK,EAAE,eAAe;gBACtB,YAAY,EAAE,CAAC,YAAY,CAAC;aAC7B;SACF;QACD,MAAM,EAAE;YACN,UAAU,EAAE,MAAM;YAClB,KAAK,EAAE,eAAe;YACtB,MAAM,EAAE,YAAY;YACpB,UAAU,EAAE,gBAAgB;SAC7B;QACD,GAAG,EAAE,gBAAgB;KACtB;CACF,CAAC,CAAC"}
1
+ {"version":3,"file":"module.js","sourceRoot":"","sources":["../../../../../src/shaders/filters/grain/v1/module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,IAAI,MAAM,SAAS,CAAC;AAC3B,OAAO,KAAK,CAAC,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAErD,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,6EAA6E;IAC7E,MAAM,EAAE,CAAC,CAAC,GAAG;IACb,0FAA0F;IAC1F,IAAI,EAAE,CAAC,CAAC,GAAG;IACX,oFAAoF;IACpF,WAAW,EAAE,CAAC,CAAC,GAAG;IAClB,gDAAgD;IAChD,IAAI,EAAE,CAAC,CAAC,GAAG;CACZ,CAAC,CAAC;AAEH,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC;IAClC,MAAM,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE;IAC5B,aAAa,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE;IAC/C,MAAM,EAAE,EAAE,OAAO,EAAE,WAAW,EAAE;CACjC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,cAAc,GAAG,YAAY,CAAC;IACzC,EAAE,EAAE,eAAe;IACnB,OAAO,EAAE,CAAC;IACV,OAAO,EAAE,UAAU;IACnB,QAAQ,EAAE,SAAS;IACnB,0EAA0E;IAC1E,iDAAiD;IACjD,SAAS,EAAE,eAAe;IAC1B,IAAI,EAAE,SAAS;IACf,MAAM,EAAE,WAAW;IACnB,aAAa,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE;IAC9D,OAAO,EAAE;QACP,MAAM,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE;QACvD,IAAI,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE;QACnD,WAAW,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE;QAC5D,IAAI,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE;KACxB;IACD,MAAM;IACN,aAAa,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;IAC1B,IAAI,EAAE,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4FlB;IACC,EAAE,EAAE;QACF,MAAM,EAAE;YACN,MAAM,EAAE;gBACN,UAAU,EAAE,MAAM;gBAClB,KAAK,EAAE,eAAe;gBACtB,YAAY,EAAE,CAAC,YAAY,CAAC;aAC7B;SACF;QACD,MAAM,EAAE;YACN,UAAU,EAAE,MAAM;YAClB,KAAK,EAAE,eAAe;YACtB,MAAM,EAAE,YAAY;YACpB,UAAU,EAAE,gBAAgB;SAC7B;QACD,GAAG,EAAE,gBAAgB;KACtB;CACF,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodetool-ai/gpu",
3
- "version": "0.8.3",
3
+ "version": "0.8.4-a24.1",
4
4
  "description": "Canonical blend-mode catalog and shared WGSL blend functions for NodeTool compositors (sketch, timeline, Compositor node)",
5
5
  "type": "module",
6
6
  "engines": {
@@ -15,10 +15,8 @@
15
15
  "lint": "node ../../scripts/run-tsc.mjs --noEmit"
16
16
  },
17
17
  "dependencies": {
18
- "@nodetool-ai/protocol": "0.8.3",
19
- "typegpu": "^0.11.6"
20
- },
21
- "optionalDependencies": {
18
+ "@nodetool-ai/protocol": "0.8.4-a24.1",
19
+ "typegpu": "^0.11.6",
22
20
  "webgpu": "^0.4.0"
23
21
  },
24
22
  "devDependencies": {