reze-engine 0.41.0 → 0.41.2

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.
Files changed (48) hide show
  1. package/README.md +4 -2
  2. package/dist/engine.d.ts +31 -3
  3. package/dist/engine.d.ts.map +1 -1
  4. package/dist/engine.js +190 -18
  5. package/dist/graph/presets/body.js +2 -2
  6. package/dist/graph/presets/cloth_rough.js +2 -2
  7. package/dist/graph/presets/cloth_smooth.js +2 -2
  8. package/dist/graph/presets/default.js +2 -2
  9. package/dist/graph/presets/eye.js +2 -2
  10. package/dist/graph/presets/face.js +2 -2
  11. package/dist/graph/presets/hair.js +2 -2
  12. package/dist/graph/presets/metal.js +2 -2
  13. package/dist/graph/presets/stockings.js +3 -3
  14. package/dist/graph/registry.d.ts.map +1 -1
  15. package/dist/graph/registry.js +426 -10
  16. package/dist/graph/style-group.d.ts +46 -0
  17. package/dist/graph/style-group.d.ts.map +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/shaders/agx-lut.d.ts +11 -0
  21. package/dist/shaders/agx-lut.d.ts.map +1 -0
  22. package/dist/shaders/agx-lut.js +29 -0
  23. package/dist/shaders/materials/common.d.ts +1 -1
  24. package/dist/shaders/materials/common.d.ts.map +1 -1
  25. package/dist/shaders/materials/common.js +10 -0
  26. package/dist/shaders/materials/nodes.d.ts +1 -1
  27. package/dist/shaders/materials/nodes.d.ts.map +1 -1
  28. package/dist/shaders/materials/nodes.js +957 -576
  29. package/dist/shaders/passes/composite.d.ts.map +1 -1
  30. package/dist/shaders/passes/composite.js +76 -4
  31. package/package.json +1 -1
  32. package/src/engine.ts +211 -17
  33. package/src/graph/presets/body.ts +2 -2
  34. package/src/graph/presets/cloth_rough.ts +2 -2
  35. package/src/graph/presets/cloth_smooth.ts +2 -2
  36. package/src/graph/presets/default.ts +2 -2
  37. package/src/graph/presets/eye.ts +2 -2
  38. package/src/graph/presets/face.ts +2 -2
  39. package/src/graph/presets/hair.ts +2 -2
  40. package/src/graph/presets/metal.ts +2 -2
  41. package/src/graph/presets/stockings.ts +3 -3
  42. package/src/graph/registry.ts +439 -10
  43. package/src/graph/style-group.ts +44 -0
  44. package/src/index.ts +2 -0
  45. package/src/shaders/agx-lut.ts +30 -0
  46. package/src/shaders/materials/common.ts +10 -0
  47. package/src/shaders/materials/nodes.ts +962 -581
  48. package/src/shaders/passes/composite.ts +76 -4
@@ -31,17 +31,21 @@ export function fmtValue(value, type) {
31
31
  return `vec3f(${fmtFloat(x)})`;
32
32
  return `vec3f(${fmtFloat(x)}, ${fmtFloat(y)}, ${fmtFloat(z)})`;
33
33
  }
34
+ if (type === "float")
35
+ throw new Error(`color literal on float socket`);
36
+ // Blender's colour sockets are RGBA, so a ported literal arrives with four
37
+ // components whatever it feeds. Only a stop colour keeps the alpha; every other
38
+ // socket here is a vec3 and drops it, which is what Blender does downstream.
34
39
  if (type !== "vec4")
35
- throw new Error(`vec4 literal on ${type} socket`);
40
+ return fmtValue([value[0], value[1], value[2]], type);
36
41
  return `vec4f(${value.map(fmtFloat).join(", ")})`;
37
42
  }
38
43
  /** Does a literal's shape fit a socket type? (Scalar splats onto color/vector/vec4.) */
39
44
  export function literalFits(value, type) {
40
45
  if (typeof value === "number")
41
46
  return true;
42
- if (value.length === 3)
43
- return type !== "float";
44
- return type === "vec4";
47
+ // 3 and 4 components both fit anything but a float — see fmtValue on RGBA.
48
+ return type !== "float";
45
49
  }
46
50
  // ─── Implicit socket conversions (Blender-faithful) ──────────────────
47
51
  // vec4 appears only on ramp stop-color literals — never linkable, so conversions
@@ -90,6 +94,18 @@ const RAMP_OUTPUTS = { color: "color", alpha: "float", fac_out: "float" };
90
94
  // fac_out (.r) matches how a grayscale ramp feeds a scalar consumer in the hand ports —
91
95
  // routing through the BT.601 color→float conversion instead would change the value.
92
96
  const RAMP_SELECT = { color: ".rgb", alpha: ".a", fac_out: ".r" };
97
+ /** Principled v2 reflectance, evaluated at compile time when it can be. */
98
+ function foldSpecular(ior, level) {
99
+ const i = Number(ior);
100
+ const l = Number(level);
101
+ if (!Number.isFinite(i) || !Number.isFinite(l))
102
+ return `principled_specular(${ior}, ${level})`;
103
+ const r = (i - 1) / Math.max(i + 1, 1e-6);
104
+ // Snapped to 9 significant digits: the arithmetic leaves double noise
105
+ // (0.2² · 2 · 0.5 / 0.08 = 0.5000000000000001) that is meaningless in an f32
106
+ // and only makes the emitted shader harder to read.
107
+ return fmtFloat(Number(((r * r * 2 * Math.max(l, 0)) / 0.08).toPrecision(9)));
108
+ }
93
109
  export const NODE_REGISTRY = {
94
110
  // ── Context inputs (template locals; no emission) ──
95
111
  texture: {
@@ -118,6 +134,42 @@ export const NODE_REGISTRY = {
118
134
  reflection: "reflect(-v, n)",
119
135
  },
120
136
  },
137
+ // The scene's key light. Blender NPR presets rarely use a diffuse closure —
138
+ // they build their own term, typically dot(normal, direction) pushed through a
139
+ // ramp or a soft threshold band, because that is what gives an anime shader its
140
+ // hard terminator. Reaching that term needs the direction as a value, which no
141
+ // other node exposes: `shader_to_rgb*` and `bsdf_diffuse` bake the whole closure
142
+ // and hand back a result. `shadow` is the same cascade sample those closures
143
+ // take, so a graph can tint its own shadow instead of accepting theirs.
144
+ //
145
+ // A ported graph reads `direction` where the Blender original read an Attribute
146
+ // fed by a light empty; the empty and our sun mean the same thing.
147
+ light: {
148
+ inputs: {},
149
+ outputs: { direction: "vector", color: "color", ambient: "color", shadow: "float" },
150
+ contextOutputs: { direction: "l", color: "sun", ambient: "amb", shadow: "shadow" },
151
+ },
152
+ // The head bone's world basis — what an SDF face shadow is built on.
153
+ //
154
+ // That technique compares a face-shaped distance field against the light's
155
+ // angle IN THE HEAD'S OWN FRAME, so the shadow sweeps across the face as the
156
+ // light moves and stays put as the head turns. Without the frame there is
157
+ // nothing to measure the angle against, and the effect cannot be expressed at
158
+ // all. `right` also carries the sign that mirrors the field's U coordinate,
159
+ // which is how one half-face texture serves both sides.
160
+ //
161
+ // Read from the 頭 bone's skinning matrix, already bound to the fragment stage
162
+ // for the eye's rear-view gate. A model without that bone falls back to bone 0
163
+ // rather than sampling out of bounds; the face shadow is then wrong, not unsafe.
164
+ head_basis: {
165
+ inputs: {},
166
+ outputs: { forward: "vector", right: "vector", up: "vector" },
167
+ contextOutputs: {
168
+ forward: "(-normalize(skinMats[u32(max(material.headBoneIndex, 0.0))][2].xyz))",
169
+ right: "normalize(skinMats[u32(max(material.headBoneIndex, 0.0))][0].xyz)",
170
+ up: "normalize(skinMats[u32(max(material.headBoneIndex, 0.0))][1].xyz)",
171
+ },
172
+ },
121
173
  // PMX material's diffuse color (the authored base tint). Multiply the diffuse texture
122
174
  // by this for the MMD-correct base — untextured materials carry their color here, so a
123
175
  // texture-only base would render them white.
@@ -136,6 +188,258 @@ export const NODE_REGISTRY = {
136
188
  outputs: { color: "color" },
137
189
  emit: (a) => `pmx_sphere_map(${a.base}, ${a.strength}, n)`,
138
190
  },
191
+ // ── Blender 5.2 Math node, full operation set ──
192
+ "math/absolute": { inputs: { a: F(0) }, outputs: { value: "float" }, emit: (a) => `math_absolute(${a.a})` },
193
+ "math/sqrt": { inputs: { a: F(0) }, outputs: { value: "float" }, emit: (a) => `math_sqrt(${a.a})` },
194
+ "math/inversesqrt": { inputs: { a: F(0) }, outputs: { value: "float" }, emit: (a) => `math_inversesqrt(${a.a})` },
195
+ "math/exponent": { inputs: { a: F(0) }, outputs: { value: "float" }, emit: (a) => `math_exponent(${a.a})` },
196
+ "math/sign": { inputs: { a: F(0) }, outputs: { value: "float" }, emit: (a) => `math_sign(${a.a})` },
197
+ "math/round": { inputs: { a: F(0) }, outputs: { value: "float" }, emit: (a) => `math_round(${a.a})` },
198
+ "math/floor": { inputs: { a: F(0) }, outputs: { value: "float" }, emit: (a) => `math_floor(${a.a})` },
199
+ "math/ceil": { inputs: { a: F(0) }, outputs: { value: "float" }, emit: (a) => `math_ceil(${a.a})` },
200
+ "math/truncate": { inputs: { a: F(0) }, outputs: { value: "float" }, emit: (a) => `math_truncate(${a.a})` },
201
+ "math/fraction": { inputs: { a: F(0) }, outputs: { value: "float" }, emit: (a) => `math_fraction(${a.a})` },
202
+ "math/sine": { inputs: { a: F(0) }, outputs: { value: "float" }, emit: (a) => `math_sine(${a.a})` },
203
+ "math/cosine": { inputs: { a: F(0) }, outputs: { value: "float" }, emit: (a) => `math_cosine(${a.a})` },
204
+ "math/tangent": { inputs: { a: F(0) }, outputs: { value: "float" }, emit: (a) => `math_tangent(${a.a})` },
205
+ "math/arcsine": { inputs: { a: F(0) }, outputs: { value: "float" }, emit: (a) => `math_arcsine(${a.a})` },
206
+ "math/arccosine": { inputs: { a: F(0) }, outputs: { value: "float" }, emit: (a) => `math_arccosine(${a.a})` },
207
+ "math/arctangent": { inputs: { a: F(0) }, outputs: { value: "float" }, emit: (a) => `math_arctangent(${a.a})` },
208
+ "math/radians": { inputs: { a: F(0) }, outputs: { value: "float" }, emit: (a) => `math_radians(${a.a})` },
209
+ "math/degrees": { inputs: { a: F(0) }, outputs: { value: "float" }, emit: (a) => `math_degrees(${a.a})` },
210
+ "math/subtract": { inputs: { a: F(0), b: F(0) }, outputs: { value: "float" }, emit: (a) => `math_subtract(${a.a}, ${a.b})` },
211
+ "math/divide": { inputs: { a: F(0), b: F(1) }, outputs: { value: "float" }, emit: (a) => `math_divide(${a.a}, ${a.b})` },
212
+ "math/logarithm": { inputs: { a: F(0), b: F(1) }, outputs: { value: "float" }, emit: (a) => `math_logarithm(${a.a}, ${a.b})` },
213
+ "math/minimum": { inputs: { a: F(0), b: F(0) }, outputs: { value: "float" }, emit: (a) => `math_minimum(${a.a}, ${a.b})` },
214
+ "math/maximum": { inputs: { a: F(0), b: F(0) }, outputs: { value: "float" }, emit: (a) => `math_maximum(${a.a}, ${a.b})` },
215
+ "math/less_than": { inputs: { a: F(0), b: F(0) }, outputs: { value: "float" }, emit: (a) => `math_less_than(${a.a}, ${a.b})` },
216
+ "math/modulo": { inputs: { a: F(0), b: F(1) }, outputs: { value: "float" }, emit: (a) => `math_modulo(${a.a}, ${a.b})` },
217
+ "math/floored_modulo": { inputs: { a: F(0), b: F(1) }, outputs: { value: "float" }, emit: (a) => `math_floored_modulo(${a.a}, ${a.b})` },
218
+ "math/snap": { inputs: { a: F(0), b: F(1) }, outputs: { value: "float" }, emit: (a) => `math_snap(${a.a}, ${a.b})` },
219
+ "math/pingpong": { inputs: { a: F(0), b: F(1) }, outputs: { value: "float" }, emit: (a) => `math_pingpong(${a.a}, ${a.b})` },
220
+ "math/arctan2": { inputs: { a: F(0), b: F(0) }, outputs: { value: "float" }, emit: (a) => `math_arctan2(${a.a}, ${a.b})` },
221
+ "math/multiply_add": { inputs: { a: F(0), b: F(0), c: F(0) }, outputs: { value: "float" }, emit: (a) => `math_multiply_add(${a.a}, ${a.b}, ${a.c})` },
222
+ "math/compare": { inputs: { a: F(0), b: F(0), c: F(0) }, outputs: { value: "float" }, emit: (a) => `math_compare(${a.a}, ${a.b}, ${a.c})` },
223
+ "math/smooth_min": { inputs: { a: F(0), b: F(0), c: F(0) }, outputs: { value: "float" }, emit: (a) => `math_smooth_min(${a.a}, ${a.b}, ${a.c})` },
224
+ "math/smooth_max": { inputs: { a: F(0), b: F(0), c: F(0) }, outputs: { value: "float" }, emit: (a) => `math_smooth_max(${a.a}, ${a.b}, ${a.c})` },
225
+ "math/wrap": { inputs: { a: F(0), b: F(0), c: F(0) }, outputs: { value: "float" }, emit: (a) => `math_wrap(${a.a}, ${a.b}, ${a.c})` },
226
+ // ── Blender 5.2 Vector Math node ──
227
+ "vector_math/normalize": { inputs: { a: V([0, 0, 0], true) }, outputs: { vector: "vector" }, emit: (a) => `vector_normalize(${a.a})` },
228
+ "vector_math/absolute": { inputs: { a: V([0, 0, 0], true) }, outputs: { vector: "vector" }, emit: (a) => `vector_absolute(${a.a})` },
229
+ "vector_math/floor": { inputs: { a: V([0, 0, 0], true) }, outputs: { vector: "vector" }, emit: (a) => `vector_floor(${a.a})` },
230
+ "vector_math/ceil": { inputs: { a: V([0, 0, 0], true) }, outputs: { vector: "vector" }, emit: (a) => `vector_ceil(${a.a})` },
231
+ "vector_math/fraction": { inputs: { a: V([0, 0, 0], true) }, outputs: { vector: "vector" }, emit: (a) => `vector_fraction(${a.a})` },
232
+ "vector_math/add": { inputs: { a: V([0, 0, 0], true), b: V() }, outputs: { vector: "vector" }, emit: (a) => `vector_add(${a.a}, ${a.b})` },
233
+ "vector_math/subtract": { inputs: { a: V([0, 0, 0], true), b: V() }, outputs: { vector: "vector" }, emit: (a) => `vector_subtract(${a.a}, ${a.b})` },
234
+ "vector_math/multiply": { inputs: { a: V([0, 0, 0], true), b: V() }, outputs: { vector: "vector" }, emit: (a) => `vector_multiply(${a.a}, ${a.b})` },
235
+ "vector_math/divide": { inputs: { a: V([0, 0, 0], true), b: V() }, outputs: { vector: "vector" }, emit: (a) => `vector_divide(${a.a}, ${a.b})` },
236
+ "vector_math/cross": { inputs: { a: V([0, 0, 0], true), b: V() }, outputs: { vector: "vector" }, emit: (a) => `vector_cross(${a.a}, ${a.b})` },
237
+ "vector_math/project": { inputs: { a: V([0, 0, 0], true), b: V() }, outputs: { vector: "vector" }, emit: (a) => `vector_project(${a.a}, ${a.b})` },
238
+ "vector_math/reflect": { inputs: { a: V([0, 0, 0], true), b: V() }, outputs: { vector: "vector" }, emit: (a) => `vector_reflect(${a.a}, ${a.b})` },
239
+ "vector_math/minimum": { inputs: { a: V([0, 0, 0], true), b: V() }, outputs: { vector: "vector" }, emit: (a) => `vector_minimum(${a.a}, ${a.b})` },
240
+ "vector_math/maximum": { inputs: { a: V([0, 0, 0], true), b: V() }, outputs: { vector: "vector" }, emit: (a) => `vector_maximum(${a.a}, ${a.b})` },
241
+ "vector_math/modulo": { inputs: { a: V([0, 0, 0], true), b: V() }, outputs: { vector: "vector" }, emit: (a) => `vector_modulo(${a.a}, ${a.b})` },
242
+ "vector_math/snap": { inputs: { a: V([0, 0, 0], true), b: V() }, outputs: { vector: "vector" }, emit: (a) => `vector_snap(${a.a}, ${a.b})` },
243
+ "vector_math/dot": { inputs: { a: V([0, 0, 0], true), b: V() }, outputs: { value: "float" }, emit: (a) => `vector_dot(${a.a}, ${a.b})` },
244
+ "vector_math/distance": { inputs: { a: V([0, 0, 0], true), b: V() }, outputs: { value: "float" }, emit: (a) => `vector_distance(${a.a}, ${a.b})` },
245
+ "vector_math/length": { inputs: { a: V([0, 0, 0], true) }, outputs: { value: "float" }, emit: (a) => `vector_length(${a.a})` },
246
+ "vector_math/scale": { inputs: { a: V([0, 0, 0], true), scale: F(1) }, outputs: { vector: "vector" }, emit: (a) => `vector_scale(${a.a}, ${a.scale})` },
247
+ "vector_math/multiply_add": { inputs: { a: V([0, 0, 0], true), b: V(), c: V() }, outputs: { vector: "vector" }, emit: (a) => `vector_multiply_add(${a.a}, ${a.b}, ${a.c})` },
248
+ "vector_math/faceforward": { inputs: { a: V([0, 0, 0], true), b: V(), c: V() }, outputs: { vector: "vector" }, emit: (a) => `vector_faceforward(${a.a}, ${a.b}, ${a.c})` },
249
+ "vector_math/refract": { inputs: { a: V([0, 0, 0], true), b: V(), ior: F(1.45) }, outputs: { vector: "vector" }, emit: (a) => `vector_refract(${a.a}, ${a.b}, ${a.ior})` },
250
+ "vector_math/wrap": { inputs: { a: V([0, 0, 0], true), b: V(), c: V() }, outputs: { vector: "vector" }, emit: (a) => `vector_wrap(${a.a}, ${a.b}, ${a.c})` },
251
+ // ── Blender 5.2 Mix (Color) blend set ──
252
+ "mix/add": { inputs: { fac: F(0.5), a: C([1, 1, 1], true), b: C() }, outputs: { color: "color" }, emit: (a) => `mix_add(${a.fac}, ${a.a}, ${a.b})` },
253
+ "mix/subtract": { inputs: { fac: F(0.5), a: C([1, 1, 1], true), b: C() }, outputs: { color: "color" }, emit: (a) => `mix_subtract(${a.fac}, ${a.a}, ${a.b})` },
254
+ "mix/darken": { inputs: { fac: F(0.5), a: C([1, 1, 1], true), b: C() }, outputs: { color: "color" }, emit: (a) => `mix_darken(${a.fac}, ${a.a}, ${a.b})` },
255
+ "mix/difference": { inputs: { fac: F(0.5), a: C([1, 1, 1], true), b: C() }, outputs: { color: "color" }, emit: (a) => `mix_difference(${a.fac}, ${a.a}, ${a.b})` },
256
+ "mix/exclusion": { inputs: { fac: F(0.5), a: C([1, 1, 1], true), b: C() }, outputs: { color: "color" }, emit: (a) => `mix_exclusion(${a.fac}, ${a.a}, ${a.b})` },
257
+ "mix/screen": { inputs: { fac: F(0.5), a: C([1, 1, 1], true), b: C() }, outputs: { color: "color" }, emit: (a) => `mix_screen(${a.fac}, ${a.a}, ${a.b})` },
258
+ "mix/soft_light": { inputs: { fac: F(0.5), a: C([1, 1, 1], true), b: C() }, outputs: { color: "color" }, emit: (a) => `mix_soft_light(${a.fac}, ${a.a}, ${a.b})` },
259
+ "mix/dodge": { inputs: { fac: F(0.5), a: C([1, 1, 1], true), b: C() }, outputs: { color: "color" }, emit: (a) => `mix_dodge(${a.fac}, ${a.a}, ${a.b})` },
260
+ "mix/burn": { inputs: { fac: F(0.5), a: C([1, 1, 1], true), b: C() }, outputs: { color: "color" }, emit: (a) => `mix_burn(${a.fac}, ${a.a}, ${a.b})` },
261
+ "mix/divide": { inputs: { fac: F(0.5), a: C([1, 1, 1], true), b: C() }, outputs: { color: "color" }, emit: (a) => `mix_divide(${a.fac}, ${a.a}, ${a.b})` },
262
+ "mix/hue": { inputs: { fac: F(0.5), a: C([1, 1, 1], true), b: C() }, outputs: { color: "color" }, emit: (a) => `mix_hue(${a.fac}, ${a.a}, ${a.b})` },
263
+ "mix/saturation": { inputs: { fac: F(0.5), a: C([1, 1, 1], true), b: C() }, outputs: { color: "color" }, emit: (a) => `mix_saturation(${a.fac}, ${a.a}, ${a.b})` },
264
+ "mix/value": { inputs: { fac: F(0.5), a: C([1, 1, 1], true), b: C() }, outputs: { color: "color" }, emit: (a) => `mix_value(${a.fac}, ${a.a}, ${a.b})` },
265
+ "mix/color": { inputs: { fac: F(0.5), a: C([1, 1, 1], true), b: C() }, outputs: { color: "color" }, emit: (a) => `mix_color_blend(${a.fac}, ${a.a}, ${a.b})` },
266
+ // ── RGB Curves, sampled. See rgb_curve in nodes.ts for why five. ──
267
+ rgb_curve: {
268
+ inputs: {
269
+ color: C([1, 1, 1], true),
270
+ fac: F(1),
271
+ y0: F(0),
272
+ y1: F(0.25),
273
+ y2: F(0.5),
274
+ y3: F(0.75),
275
+ y4: F(1),
276
+ },
277
+ outputs: { color: "color" },
278
+ emit: (a) => `mix(${a.color}, rgb_curve(${a.color}, ${a.y0}, ${a.y1}, ${a.y2}, ${a.y3}, ${a.y4}), ${a.fac})`,
279
+ },
280
+ // UV Map — one layer on a PMX, which is the mesh UV the texture node uses.
281
+ uv_map: {
282
+ inputs: {},
283
+ outputs: { uv: "vector" },
284
+ contextOutputs: { uv: "vec3f(input.uv, 0.0)" },
285
+ },
286
+ // ── Normal Map, Vector Transform ──
287
+ normal_map: {
288
+ inputs: { color: C([0.5, 0.5, 1], true), strength: F(1) },
289
+ outputs: { normal: "vector" },
290
+ emit: (a) => `node_normal_map(${a.color}, ${a.strength}, n, input.worldPos, input.uv)`,
291
+ },
292
+ "vector_transform/world_to_camera": {
293
+ inputs: { vector: V([0, 0, 0], true) },
294
+ outputs: { vector: "vector" },
295
+ emit: (a) => `vector_world_to_camera(${a.vector})`,
296
+ },
297
+ "vector_transform/camera_to_world": {
298
+ inputs: { vector: V([0, 0, 0], true) },
299
+ outputs: { vector: "vector" },
300
+ emit: (a) => `vector_camera_to_world(${a.vector})`,
301
+ },
302
+ "vector_transform/point_world_to_camera": {
303
+ inputs: { vector: V([0, 0, 0], true) },
304
+ outputs: { vector: "vector" },
305
+ emit: (a) => `point_world_to_camera(${a.vector})`,
306
+ },
307
+ // ── Shader nodes. Shaders travel as RGB here (see shader_to_rgb_diffuse), so
308
+ // a "transparent" shader is the colour that contributes nothing; the actual
309
+ // cutout is the group's alphaMode, which is where transparency belongs in a
310
+ // rasteriser without order-independent blending.
311
+ bsdf_transparent: { inputs: {}, outputs: { color: "color" }, emit: () => `vec3f(0.0)` },
312
+ bsdf_diffuse: {
313
+ inputs: { color: C([0.8, 0.8, 0.8], true) },
314
+ outputs: { color: "color" },
315
+ emit: (a) => `${a.color} * shader_to_rgb_lit(n, l, sun, amb, shadow)`,
316
+ },
317
+ // ── Nodes with no meaning on a PMX, answered honestly with a constant ──
318
+ // Each returns what Blender returns for the absent case, so a graph that reads
319
+ // one degrades to a sensible look instead of failing to compile. Documented
320
+ // rather than silently wrong.
321
+ //
322
+ // attribute: MMD meshes carry no vertex colour layer, so Color reads white and
323
+ // Fac reads 1 — the identity for the multiply these usually feed.
324
+ attribute: {
325
+ inputs: {},
326
+ outputs: { color: "color", fac: "float" },
327
+ contextOutputs: { color: "vec3f(1.0)", fac: "1.0" },
328
+ },
329
+ // object_info: one model, one instance — Random is the only field with a real
330
+ // use (per-instance variation) and there are no instances to vary.
331
+ object_info: {
332
+ inputs: {},
333
+ outputs: { location: "vector", color: "color", random: "float" },
334
+ contextOutputs: { location: "vec3f(0.0)", color: "vec3f(1.0)", random: "0.0" },
335
+ },
336
+ // light_path: this is a raster pass, so every shaded fragment IS a camera ray.
337
+ light_path: {
338
+ inputs: {},
339
+ outputs: { is_camera_ray: "float", is_shadow_ray: "float", ray_depth: "float" },
340
+ contextOutputs: { is_camera_ray: "1.0", is_shadow_ray: "0.0", ray_depth: "0.0" },
341
+ },
342
+ // ── Image Texture on a style-group slot ──
343
+ // The PMX gives a material ONE image; this style needs several, so the extra
344
+ // maps ride on the group. Slot is part of the type because it selects a
345
+ // binding, which is topology rather than a value. The uv input defaults to the
346
+ // mesh UV, matching an unlinked Vector socket in Blender.
347
+ "tex_image/0": {
348
+ inputs: { uv: { type: "vector", contextDefault: "vec3f(input.uv, 0.0)" } },
349
+ outputs: { color: "color", alpha: "float" },
350
+ outputSelect: { color: ".rgb", alpha: ".a" },
351
+ emit: (a) => `group_tex0(${a.uv}.xy)`,
352
+ },
353
+ "tex_image/1": {
354
+ inputs: { uv: { type: "vector", contextDefault: "vec3f(input.uv, 0.0)" } },
355
+ outputs: { color: "color", alpha: "float" },
356
+ outputSelect: { color: ".rgb", alpha: ".a" },
357
+ emit: (a) => `group_tex1(${a.uv}.xy)`,
358
+ },
359
+ "tex_image/2": {
360
+ inputs: { uv: { type: "vector", contextDefault: "vec3f(input.uv, 0.0)" } },
361
+ outputs: { color: "color", alpha: "float" },
362
+ outputSelect: { color: ".rgb", alpha: ".a" },
363
+ emit: (a) => `group_tex2(${a.uv}.xy)`,
364
+ },
365
+ "tex_image/3": {
366
+ inputs: { uv: { type: "vector", contextDefault: "vec3f(input.uv, 0.0)" } },
367
+ outputs: { color: "color", alpha: "float" },
368
+ outputSelect: { color: ".rgb", alpha: ".a" },
369
+ emit: (a) => `group_tex3(${a.uv}.xy)`,
370
+ },
371
+ // ── Blender 5.2 colour utilities ──
372
+ separate_color: {
373
+ inputs: { color: C([1, 1, 1], true) },
374
+ outputs: { r: "float", g: "float", b: "float" },
375
+ outputSelect: { r: ".r", g: ".g", b: ".b" },
376
+ emit: (a) => a.color,
377
+ },
378
+ "separate_color/hsv": {
379
+ inputs: { color: C([1, 1, 1], true) },
380
+ outputs: { h: "float", s: "float", v: "float" },
381
+ outputSelect: { h: ".x", s: ".y", v: ".z" },
382
+ emit: (a) => `rgb_to_hsv(${a.color})`,
383
+ },
384
+ "separate_color/hsl": {
385
+ inputs: { color: C([1, 1, 1], true) },
386
+ outputs: { h: "float", s: "float", l: "float" },
387
+ outputSelect: { h: ".x", s: ".y", l: ".z" },
388
+ emit: (a) => `rgb_to_hsl(${a.color})`,
389
+ },
390
+ combine_color: {
391
+ inputs: { r: F(0), g: F(0), b: F(0) },
392
+ outputs: { color: "color" },
393
+ emit: (a) => `vec3f(${a.r}, ${a.g}, ${a.b})`,
394
+ },
395
+ "combine_color/hsv": {
396
+ inputs: { h: F(0), s: F(0), v: F(0) },
397
+ outputs: { color: "color" },
398
+ emit: (a) => `hsv_to_rgb(vec3f(${a.h}, ${a.s}, ${a.v}))`,
399
+ },
400
+ "combine_color/hsl": {
401
+ inputs: { h: F(0), s: F(0), l: F(0) },
402
+ outputs: { color: "color" },
403
+ emit: (a) => `hsl_to_rgb(vec3f(${a.h}, ${a.s}, ${a.l}))`,
404
+ },
405
+ combine_xyz: {
406
+ inputs: { x: F(0), y: F(0), z: F(0) },
407
+ outputs: { vector: "vector" },
408
+ emit: (a) => `vec3f(${a.x}, ${a.y}, ${a.z})`,
409
+ },
410
+ gamma: {
411
+ inputs: { color: C([1, 1, 1], true), gamma: F(1) },
412
+ outputs: { color: "color" },
413
+ emit: (a) => `node_gamma(${a.color}, ${a.gamma})`,
414
+ },
415
+ // Map Range: the interpolation is topology, as everywhere else here. The
416
+ // clamped form is Blender's default, which is why it carries the bare name.
417
+ map_range: {
418
+ inputs: { value: F(0, true), from_min: F(0), from_max: F(1), to_min: F(0), to_max: F(1) },
419
+ outputs: { value: "float" },
420
+ emit: (a) => `map_range_clamped(${a.value}, ${a.from_min}, ${a.from_max}, ${a.to_min}, ${a.to_max})`,
421
+ },
422
+ "map_range/linear": {
423
+ inputs: { value: F(0, true), from_min: F(0), from_max: F(1), to_min: F(0), to_max: F(1) },
424
+ outputs: { value: "float" },
425
+ emit: (a) => `map_range_linear(${a.value}, ${a.from_min}, ${a.from_max}, ${a.to_min}, ${a.to_max})`,
426
+ },
427
+ "map_range/smoothstep": {
428
+ inputs: { value: F(0, true), from_min: F(0), from_max: F(1), to_min: F(0), to_max: F(1) },
429
+ outputs: { value: "float" },
430
+ emit: (a) => `map_range_smooth(${a.value}, ${a.from_min}, ${a.from_max}, ${a.to_min}, ${a.to_max})`,
431
+ },
432
+ // Vector Rotate — the rotation TYPE is topology.
433
+ "vector_rotate/axis_angle": {
434
+ inputs: { vector: V([0, 0, 0], true), center: V(), axis: V([0, 0, 1]), angle: F(0) },
435
+ outputs: { vector: "vector" },
436
+ emit: (a) => `vector_rotate_axis(${a.vector}, ${a.center}, ${a.axis}, ${a.angle})`,
437
+ },
438
+ "vector_rotate/euler_xyz": {
439
+ inputs: { vector: V([0, 0, 0], true), center: V(), rotation: V() },
440
+ outputs: { vector: "vector" },
441
+ emit: (a) => `vector_rotate_euler(${a.vector}, ${a.center}, ${a.rotation})`,
442
+ },
139
443
  // ── Literals as nodes (for editor ergonomics; inlined literals work too) ──
140
444
  value: { inputs: { value: F(0) }, outputs: { value: "float" }, emit: (a) => a.value },
141
445
  rgb: { inputs: { color: C() }, outputs: { color: "color" }, emit: (a) => a.color },
@@ -179,8 +483,27 @@ export const NODE_REGISTRY = {
179
483
  outputSelect: RAMP_SELECT,
180
484
  emit: (a) => `ramp_constant_edge_aa(${a.fac}, ${a.edge}, ${a.color0}, ${a.color1})`,
181
485
  },
486
+ // Blender ColorRamp LINEAR with three arbitrary stops. A two-stop ramp cannot
487
+ // express a shadow that passes through a colour on its way — a warm terminator
488
+ // between cool shadow and lit skin is three stops, and that middle band is
489
+ // where a lot of NPR character lives. Decomposing it into two ramps and a
490
+ // select costs three extra nodes and stops reading as a ramp.
491
+ ramp_linear_3: {
492
+ inputs: {
493
+ fac: F(0.5, true),
494
+ pos0: F(0),
495
+ color0: V4([0, 0, 0, 1]),
496
+ pos1: F(0.5),
497
+ color1: V4([0.5, 0.5, 0.5, 1]),
498
+ pos2: F(1),
499
+ color2: V4([1, 1, 1, 1]),
500
+ },
501
+ outputs: RAMP_OUTPUTS,
502
+ outputSelect: RAMP_SELECT,
503
+ emit: (a) => `ramp_linear3(${a.fac}, ${a.pos0}, ${a.color0}, ${a.pos1}, ${a.color1}, ${a.pos2}, ${a.color2})`,
504
+ },
182
505
  // Blender ColorRamp LINEAR with 3 stops black→white→black (triangular peak at 0.5).
183
- // The only >2-stop ramp the presets use; folded to closed form like the hand port.
506
+ // Folded to closed form like the hand port.
184
507
  ramp_tri: {
185
508
  inputs: { fac: F(0.5, true) },
186
509
  outputs: { value: "float" },
@@ -270,11 +593,35 @@ export const NODE_REGISTRY = {
270
593
  emit: (a) => `layer_weight_facing(${a.blend}, n, v)`,
271
594
  },
272
595
  // ── Lighting capture ──
596
+ /**
597
+ * Shader → RGB on a white diffuse closure, reduced to a scalar.
598
+ *
599
+ * Rec.709 luminance, kept exactly as it was: every shipped preset ramps this,
600
+ * and changing the weights would move every terminator in the library.
601
+ */
273
602
  shader_to_rgb_diffuse: {
274
603
  inputs: {},
275
604
  outputs: { value: "float" },
276
605
  emit: () => `shader_to_rgb_diffuse(n, l, sun, amb, shadow)`,
277
606
  },
607
+ /**
608
+ * The same closure as a COLOUR, which is what Blender's Shader to RGB
609
+ * actually returns.
610
+ *
611
+ * A warm sun against a cool ambient radiates warm light and cool shadow, and
612
+ * that colour IS the look in most of this style. Reducing it to luminance
613
+ * first — the only thing reachable before — leaves every ramp working on grey.
614
+ *
615
+ * Linking this into a scalar socket still yields a float, because Blender's
616
+ * own implicit Color → Value conversion (BT.601) happens at the link. So the
617
+ * two nodes differ only in whether the graph wants the colour or the scalar,
618
+ * and each gets Blender's answer for its own question.
619
+ */
620
+ shader_to_rgb: {
621
+ inputs: {},
622
+ outputs: { color: "color" },
623
+ emit: () => `shader_to_rgb_lit(n, l, sun, amb, shadow)`,
624
+ },
278
625
  // ── Vector ──
279
626
  separate_xyz: {
280
627
  inputs: { vector: V([0, 0, 0], true) },
@@ -323,18 +670,87 @@ export const NODE_REGISTRY = {
323
670
  // ── Principled BSDF (frozen 3.6 legacy-EEVEE semantics: eval_principled port) ──
324
671
  // normal defaults to the template's shading normal; link a bump/normal_map chain
325
672
  // to perturb it (body/cloth_rough noise bump).
673
+ /**
674
+ * Principled BSDF, Blender 5.2 socket names.
675
+ *
676
+ * v2 (4.0+) renamed most of what a graph touches and re-derived one of them,
677
+ * which is exactly where a transcribed port goes quietly wrong: Specular
678
+ * became "Specular IOR Level" and no longer sets reflectance directly — IOR
679
+ * does, and the level scales it. So f0 is computed the v2 way here, and the
680
+ * defaults land on 0.04 for ior 1.5 at level 0.5, matching both versions at
681
+ * their defaults while tracking v2 when a preset moves either.
682
+ *
683
+ * Emission Strength defaults to 0 in v2 where 3.6's Emission was visible, so a
684
+ * naive port turns every emissive material black. Following v2 means honouring
685
+ * that default and letting the preset say otherwise.
686
+ *
687
+ * Not implemented, and not silently wrong — a graph setting these gets the
688
+ * base BSDF rather than a wrong approximation of them: Coat, Transmission,
689
+ * Subsurface, Anisotropic, Thin Film. They need path-traced or
690
+ * multi-scatter machinery this renderer does not have.
691
+ */
326
692
  principled: {
327
693
  inputs: {
328
- base: C([0.8, 0.8, 0.8], true),
694
+ base_color: C([0.8, 0.8, 0.8], true),
329
695
  metallic: F(0),
330
- specular: F(0.5),
331
696
  roughness: F(0.5),
332
- spec_clamp: F(1e30),
333
- sheen: F(0),
697
+ ior: F(1.5),
698
+ specular_ior_level: F(0.5),
699
+ sheen_weight: F(0),
334
700
  sheen_tint: F(0),
701
+ emission_color: C([1, 1, 1]),
702
+ emission_strength: F(0),
335
703
  normal: { type: "vector", contextDefault: "n" },
704
+ /** Ours, not Blender's: caps firefly speculars from noise-bumped NDF
705
+ * aliasing, which EEVEE hides behind TAA and we have none. */
706
+ spec_clamp: F(1e30),
336
707
  },
337
708
  outputs: { color: "color" },
338
- emit: (a) => `eval_principled(PrincipledIn(${a.base}, ${a.metallic}, ${a.specular}, ${a.roughness}, ${a.spec_clamp}, ${a.sheen}, ${a.sheen_tint}), ${a.normal}, l, v, sun, amb, shadow)`,
709
+ emit: (a) => {
710
+ // Fold the reflectance when both sockets are literals, which is nearly
711
+ // always. It keeps the emitted WGSL as tight as the 3.6 form was — and at
712
+ // the defaults it folds to exactly 0.5, which is what 3.6's Specular held,
713
+ // so the two versions agree byte-for-byte where a preset changed nothing.
714
+ const spec = foldSpecular(a.ior, a.specular_ior_level);
715
+ const bsdf = `eval_principled(PrincipledIn(${a.base_color}, ${a.metallic}, ` +
716
+ `${spec}, ${a.roughness}, ` +
717
+ `${a.spec_clamp}, ${a.sheen_weight}, ${a.sheen_tint}), ${a.normal}, l, v, sun, amb, shadow)`;
718
+ // v2 defaults Emission Strength to 0, which is the overwhelming case. Emit
719
+ // the term only when it can do something, so the common shader carries no
720
+ // dead add and the output stays readable.
721
+ return a.emission_strength === "0.0" ? bsdf : `${bsdf} + ${a.emission_color} * ${a.emission_strength}`;
722
+ },
339
723
  },
340
724
  };
725
+ // ─── Blender socket parity ────────────────────────────────────────────
726
+ // Blender's Math node carries three Value inputs whatever operation is selected,
727
+ // its Vector Math node three Vectors plus a Scale, and Vector Rotate the union of
728
+ // every rotation type's inputs. Sockets are part of the node, not of the mode.
729
+ //
730
+ // Declaring the same set here is what makes a port mechanical: a transcriber maps
731
+ // socket-for-socket and never has to know which inputs a given operation happens
732
+ // to read. The emit functions read only what their operation uses, so the added
733
+ // sockets change no generated WGSL — an unread one is inert.
734
+ //
735
+ // Applied as a pass rather than written into each entry so that every operation
736
+ // KEEPS the default it already declared (divide's b is 1, not 0). Only genuinely
737
+ // missing sockets are added.
738
+ function widen(prefix, sockets) {
739
+ for (const [key, spec] of Object.entries(NODE_REGISTRY)) {
740
+ if (!key.startsWith(prefix))
741
+ continue;
742
+ for (const [name, def] of Object.entries(sockets)) {
743
+ if (!(name in spec.inputs))
744
+ spec.inputs[name] = def;
745
+ }
746
+ }
747
+ }
748
+ widen("math/", { a: F(0), b: F(0), c: F(0) });
749
+ widen("vector_math/", { a: V(), b: V(), c: V(), scale: F(1) });
750
+ widen("vector_rotate/", {
751
+ vector: V([0, 0, 0], true),
752
+ center: V(),
753
+ axis: V([0, 0, 1]),
754
+ angle: F(0),
755
+ rotation: V(),
756
+ });
@@ -15,6 +15,52 @@ export type StyleGroup = {
15
15
  renderClass?: RenderClass;
16
16
  /** Alpha-handling axis, orthogonal to renderClass. Default "opaque". */
17
17
  alphaMode?: AlphaMode;
18
+ /**
19
+ * Extra image maps for this group's shading, up to four.
20
+ *
21
+ * A PMX material carries exactly ONE image, and a Blender-authored look is
22
+ * usually built on more than that — a lightmap encoding shadow thresholds and
23
+ * specular masks in its channels, a ramp, a detail map. Those belong to the
24
+ * LOOK rather than to the model, so they ride on the group: the same graph
25
+ * applied to another character brings its own maps with it.
26
+ *
27
+ * Read by the `tex_image/0`…`tex_image/3` nodes, in order. A slot left empty
28
+ * (or a graph reading past the end) samples 1×1 white, so a missing map is a
29
+ * no-op rather than a failure.
30
+ */
31
+ images?: (GroupImage | GroupImageSource | null)[];
32
+ };
33
+ export type GroupImageSource = ImageBitmap | HTMLImageElement | HTMLCanvasElement;
34
+ /**
35
+ * A group map, with the one thing the engine cannot infer from the pixels:
36
+ * whether they are colour.
37
+ *
38
+ * A PMX texture is always colour, so material textures are uploaded sRGB
39
+ * unconditionally. A group's maps are not — half of them are data whose channels
40
+ * encode thresholds, masks and distances, and running those through an sRGB
41
+ * decode bends every value. Blender asks the same question per image, and a
42
+ * ported look carries whatever answer it was authored with.
43
+ *
44
+ * A bare source reads as data (`srgb: false`), because a map that is silently
45
+ * mis-decoded as colour is the harder failure to see: it renders, just wrong.
46
+ */
47
+ /**
48
+ * `premultiplied` — whether the upload should multiply RGB by alpha.
49
+ *
50
+ * Blender premultiplies a straight-alpha image for rendering, so a transparent
51
+ * pixel contributes nothing. A highlight texture is white everywhere with its
52
+ * shape only in alpha, and without this it screens to a white card instead of
53
+ * resolving to its shape. Left off for a channel-packed map, whose alpha is a
54
+ * separate signal and would scale colour by something unrelated.
55
+ *
56
+ * It has to happen HERE rather than in the host's decode: copyExternalImageToTexture
57
+ * converts to the destination's tagged alpha mode, so a premultiplied ImageBitmap
58
+ * copied into an untagged texture is un-premultiplied again on the way in.
59
+ */
60
+ export type GroupImage = {
61
+ source: GroupImageSource;
62
+ srgb?: boolean;
63
+ premultiplied?: boolean;
18
64
  };
19
65
  export type GroupDiagnostic = {
20
66
  groupId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"style-group.d.ts","sourceRoot":"","sources":["../../src/graph/style-group.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AACvD,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAE1C,MAAM,MAAM,UAAU,GAAG;IACvB,wFAAwF;IACxF,EAAE,EAAE,MAAM,CAAA;IACV;2CACuC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,2EAA2E;IAC3E,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,sFAAsF;IACtF,KAAK,EAAE,WAAW,CAAA;IAClB,uEAAuE;IACvE,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,wEAAwE;IACxE,SAAS,CAAC,EAAE,SAAS,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,UAAU,EAAE,CAAC;IAAC,EAAE,EAAE,OAAO,CAAA;CAAE,CAAA;AAEzF,MAAM,MAAM,sBAAsB,GAAG;IACnC,sCAAsC;IACtC,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,EAAE,eAAe,EAAE,CAAA;IACzB,iEAAiE;IACjE,gBAAgB,EAAE,MAAM,EAAE,CAAA;IAC1B,kFAAkF;IAClF,SAAS,EAAE,MAAM,EAAE,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,UAAU,EAAE,CAAC;IAAC,OAAO,EAAE,SAAS,EAAE,CAAA;CAAE,CAAA"}
1
+ {"version":3,"file":"style-group.d.ts","sourceRoot":"","sources":["../../src/graph/style-group.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AACvD,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAE1C,MAAM,MAAM,UAAU,GAAG;IACvB,wFAAwF;IACxF,EAAE,EAAE,MAAM,CAAA;IACV;2CACuC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,2EAA2E;IAC3E,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,sFAAsF;IACtF,KAAK,EAAE,WAAW,CAAA;IAClB,uEAAuE;IACvE,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,wEAAwE;IACxE,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,EAAE,CAAC,UAAU,GAAG,gBAAgB,GAAG,IAAI,CAAC,EAAE,CAAA;CAClD,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG,WAAW,GAAG,gBAAgB,GAAG,iBAAiB,CAAA;AAEjF;;;;;;;;;;;;GAYG;AACH;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,UAAU,GAAG;IAAE,MAAM,EAAE,gBAAgB,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,aAAa,CAAC,EAAE,OAAO,CAAA;CAAE,CAAA;AAE9F,MAAM,MAAM,eAAe,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,UAAU,EAAE,CAAC;IAAC,EAAE,EAAE,OAAO,CAAA;CAAE,CAAA;AAEzF,MAAM,MAAM,sBAAsB,GAAG;IACnC,sCAAsC;IACtC,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,EAAE,eAAe,EAAE,CAAA;IACzB,iEAAiE;IACjE,gBAAgB,EAAE,MAAM,EAAE,CAAA;IAC1B,kFAAkF;IAClF,SAAS,EAAE,MAAM,EAAE,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,UAAU,EAAE,CAAC;IAAC,OAAO,EAAE,SAAS,EAAE,CAAA;CAAE,CAAA"}
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export { compileGraph, validateGraph, assignStyleSlots, type CompileOptions, typ
4
4
  export type { ShaderGraph, GraphNode, GraphLink, ExposedParam, SocketValue, Diagnostic, } from "./graph/schema";
5
5
  export { NODE_REGISTRY, type NodeSpec, type SockT } from "./graph/registry";
6
6
  export { RENDER_CLASSES, type RenderClass, type AlphaMode, type RenderClassInfo } from "./graph/render-class";
7
- export type { StyleGroup, GroupDiagnostic, ApplyStyleGroupsResult, ApplyStyleGroupResult, } from "./graph/style-group";
7
+ export type { StyleGroup, GroupImage, GroupImageSource, GroupDiagnostic, ApplyStyleGroupsResult, ApplyStyleGroupResult, } from "./graph/style-group";
8
8
  export { HAIR_GRAPH } from "./graph/presets/hair";
9
9
  export { DEFAULT_GRAPH } from "./graph/presets/default";
10
10
  export { CLOTH_SMOOTH_GRAPH } from "./graph/presets/cloth_smooth";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}