reze-engine 0.17.1 → 0.18.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.
Files changed (73) hide show
  1. package/dist/engine.d.ts +34 -0
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +258 -15
  4. package/dist/gpu-profile.d.ts +19 -0
  5. package/dist/gpu-profile.d.ts.map +1 -0
  6. package/dist/gpu-profile.js +120 -0
  7. package/dist/graph/compile.d.ts +34 -0
  8. package/dist/graph/compile.d.ts.map +1 -0
  9. package/dist/graph/compile.js +299 -0
  10. package/dist/graph/presets/body.d.ts +3 -0
  11. package/dist/graph/presets/body.d.ts.map +1 -0
  12. package/dist/graph/presets/body.js +100 -0
  13. package/dist/graph/presets/cloth_rough.d.ts +3 -0
  14. package/dist/graph/presets/cloth_rough.d.ts.map +1 -0
  15. package/dist/graph/presets/cloth_rough.js +61 -0
  16. package/dist/graph/presets/cloth_smooth.d.ts +3 -0
  17. package/dist/graph/presets/cloth_smooth.d.ts.map +1 -0
  18. package/dist/graph/presets/cloth_smooth.js +53 -0
  19. package/dist/graph/presets/default.d.ts +3 -0
  20. package/dist/graph/presets/default.d.ts.map +1 -0
  21. package/dist/graph/presets/default.js +20 -0
  22. package/dist/graph/presets/eye.d.ts +3 -0
  23. package/dist/graph/presets/eye.d.ts.map +1 -0
  24. package/dist/graph/presets/eye.js +30 -0
  25. package/dist/graph/presets/face.d.ts +3 -0
  26. package/dist/graph/presets/face.d.ts.map +1 -0
  27. package/dist/graph/presets/face.js +125 -0
  28. package/dist/graph/presets/hair.d.ts +3 -0
  29. package/dist/graph/presets/hair.d.ts.map +1 -0
  30. package/dist/graph/presets/hair.js +79 -0
  31. package/dist/graph/presets/metal.d.ts +3 -0
  32. package/dist/graph/presets/metal.d.ts.map +1 -0
  33. package/dist/graph/presets/metal.js +58 -0
  34. package/dist/graph/presets/stockings.d.ts +3 -0
  35. package/dist/graph/presets/stockings.d.ts.map +1 -0
  36. package/dist/graph/presets/stockings.js +54 -0
  37. package/dist/graph/registry.d.ts +28 -0
  38. package/dist/graph/registry.d.ts.map +1 -0
  39. package/dist/graph/registry.js +322 -0
  40. package/dist/graph/schema.d.ts +66 -0
  41. package/dist/graph/schema.d.ts.map +1 -0
  42. package/dist/graph/schema.js +6 -0
  43. package/dist/graph/slots.d.ts +14 -0
  44. package/dist/graph/slots.d.ts.map +1 -0
  45. package/dist/graph/slots.js +155 -0
  46. package/dist/index.d.ts +13 -1
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +11 -0
  49. package/dist/physics/profile.d.ts +18 -0
  50. package/dist/physics/profile.d.ts.map +1 -0
  51. package/dist/physics/profile.js +44 -0
  52. package/package.json +2 -1
  53. package/src/engine.ts +290 -15
  54. package/src/graph/compile.ts +342 -0
  55. package/src/graph/presets/body.ts +103 -0
  56. package/src/graph/presets/cloth_rough.ts +64 -0
  57. package/src/graph/presets/cloth_smooth.ts +56 -0
  58. package/src/graph/presets/default.ts +23 -0
  59. package/src/graph/presets/eye.ts +33 -0
  60. package/src/graph/presets/face.ts +136 -0
  61. package/src/graph/presets/hair.ts +82 -0
  62. package/src/graph/presets/metal.ts +61 -0
  63. package/src/graph/presets/stockings.ts +57 -0
  64. package/src/graph/registry.ts +351 -0
  65. package/src/graph/schema.ts +60 -0
  66. package/src/graph/slots.ts +178 -0
  67. package/src/index.ts +27 -0
  68. package/dist/physics-debug.d.ts +0 -30
  69. package/dist/physics-debug.d.ts.map +0 -1
  70. package/dist/physics-debug.js +0 -526
  71. package/dist/shaders/passes/physics-debug.d.ts +0 -2
  72. package/dist/shaders/passes/physics-debug.d.ts.map +0 -1
  73. package/dist/shaders/passes/physics-debug.js +0 -69
@@ -0,0 +1,351 @@
1
+ // Node registry: one entry per Blender-equivalent node backed by NODES_WGSL (nodes.ts).
2
+ // The registry adds no WGSL of its own — a node type exists here only if its function
3
+ // already exists (validated against EEVEE) in the shader library, or is a WGSL builtin.
4
+ // Semantics are frozen Blender 3.6 legacy-EEVEE; enum modes (math op, mix blend type,
5
+ // ramp interpolation) are part of the type string so they are unambiguously topology.
6
+
7
+ import type { SocketValue } from "./schema"
8
+
9
+ export type SockT = "float" | "color" | "vector" | "vec4"
10
+
11
+ export type InputSpec = {
12
+ type: SockT
13
+ default?: SocketValue
14
+ /** Socket is meaningless as a literal (e.g. the color being processed) — must be linked. */
15
+ requiresLink?: boolean
16
+ /** Unlinked fallback is a template local, not a literal (e.g. principled.normal → n). */
17
+ contextDefault?: string
18
+ }
19
+
20
+ export type NodeSpec = {
21
+ inputs: Record<string, InputSpec>
22
+ outputs: Record<string, SockT>
23
+ /** RHS expression for this node's `let`, from resolved arg expressions keyed by socket. */
24
+ emit?: (args: Record<string, string>) => string
25
+ /** Context nodes tap template locals directly — no `let` emitted. */
26
+ contextOutputs?: Record<string, string>
27
+ /** Swizzle applied to the node's variable per output socket (default: none). */
28
+ outputSelect?: Record<string, string>
29
+ }
30
+
31
+ // ─── Literal formatting ───────────────────────────────────────────────
32
+ // Deterministic: same graph JSON → byte-identical WGSL. String(x) is JS shortest
33
+ // round-trip, so full-precision Blender constants (0.15000000596046448) survive.
34
+
35
+ export function fmtFloat(x: number): string {
36
+ if (!Number.isFinite(x)) throw new Error(`non-finite literal: ${x}`)
37
+ const s = String(x)
38
+ return /[.e]/.test(s) ? s : s + ".0"
39
+ }
40
+
41
+ export function fmtValue(value: SocketValue, type: SockT): string {
42
+ if (typeof value === "number") {
43
+ if (type === "float") return fmtFloat(value)
44
+ if (type === "color" || type === "vector") return `vec3f(${fmtFloat(value)})`
45
+ return `vec4f(vec3f(${fmtFloat(value)}), 1.0)`
46
+ }
47
+ if (value.length === 3) {
48
+ const [x, y, z] = value
49
+ if (type === "vec4") return `vec4f(${fmtFloat(x)}, ${fmtFloat(y)}, ${fmtFloat(z)}, 1.0)`
50
+ if (type === "float") throw new Error(`vector literal on float socket`)
51
+ // All-equal shorthand matches the hand-written shaders (vec3f(0.167…)).
52
+ if (x === y && y === z) return `vec3f(${fmtFloat(x)})`
53
+ return `vec3f(${fmtFloat(x)}, ${fmtFloat(y)}, ${fmtFloat(z)})`
54
+ }
55
+ if (type !== "vec4") throw new Error(`vec4 literal on ${type} socket`)
56
+ return `vec4f(${value.map(fmtFloat).join(", ")})`
57
+ }
58
+
59
+ /** Does a literal's shape fit a socket type? (Scalar splats onto color/vector/vec4.) */
60
+ export function literalFits(value: SocketValue, type: SockT): boolean {
61
+ if (typeof value === "number") return true
62
+ if (value.length === 3) return type !== "float"
63
+ return type === "vec4"
64
+ }
65
+
66
+ // ─── Implicit socket conversions (Blender-faithful) ──────────────────
67
+ // vec4 appears only on ramp stop-color literals — never linkable, so conversions
68
+ // cover float/color/vector. vector→float is NOT implicit in Blender; rejected.
69
+
70
+ export function canConvert(from: SockT, to: SockT): boolean {
71
+ if (from === to) return true
72
+ if (from === "color" && to === "float") return true // BT.601 via color_to_value
73
+ if (from === "float" && (to === "color" || to === "vector")) return true
74
+ if ((from === "color" && to === "vector") || (from === "vector" && to === "color")) return true
75
+ return false
76
+ }
77
+
78
+ export function convert(from: SockT, to: SockT, expr: string): string {
79
+ if (from === to) return expr
80
+ if (from === "color" && to === "float") return `color_to_value(${expr})`
81
+ if (from === "float") return `vec3f(${expr})`
82
+ return expr // color ↔ vector: bit-identical vec3f pass-through
83
+ }
84
+
85
+ // ─── Registry ─────────────────────────────────────────────────────────
86
+
87
+ const F = (d: number, requiresLink = false): InputSpec => ({ type: "float", default: d, requiresLink })
88
+ const C = (d: [number, number, number] = [1, 1, 1], requiresLink = false): InputSpec => ({
89
+ type: "color",
90
+ default: d,
91
+ requiresLink,
92
+ })
93
+ const V = (d: [number, number, number] = [0, 0, 0], requiresLink = false): InputSpec => ({
94
+ type: "vector",
95
+ default: d,
96
+ requiresLink,
97
+ })
98
+ const V4 = (d: [number, number, number, number]): InputSpec => ({ type: "vec4", default: d })
99
+
100
+ const RAMP_INPUTS: Record<string, InputSpec> = {
101
+ fac: F(0.5, true),
102
+ pos0: F(0),
103
+ color0: V4([0, 0, 0, 1]),
104
+ pos1: F(1),
105
+ color1: V4([1, 1, 1, 1]),
106
+ }
107
+ const RAMP_OUTPUTS: Record<string, SockT> = { color: "color", alpha: "float", fac_out: "float" }
108
+ // fac_out (.r) matches how a grayscale ramp feeds a scalar consumer in the hand ports —
109
+ // routing through the BT.601 color→float conversion instead would change the value.
110
+ const RAMP_SELECT = { color: ".rgb", alpha: ".a", fac_out: ".r" }
111
+
112
+ export const NODE_REGISTRY: Record<string, NodeSpec> = {
113
+ // ── Context inputs (template locals; no emission) ──
114
+ texture: {
115
+ inputs: {},
116
+ outputs: { color: "color", alpha: "float" },
117
+ contextOutputs: { color: "tex_color", alpha: "tex_s.a" },
118
+ },
119
+ geometry: {
120
+ inputs: {},
121
+ outputs: {
122
+ normal: "vector",
123
+ view: "vector",
124
+ world_pos: "vector",
125
+ rest_pos: "vector",
126
+ uv: "vector",
127
+ // Blender Texture Coordinate → Reflection (view ray mirrored on the normal);
128
+ // drives env-tracking patterns like metal's voronoi sparkle.
129
+ reflection: "vector",
130
+ },
131
+ contextOutputs: {
132
+ normal: "n",
133
+ view: "v",
134
+ world_pos: "input.worldPos",
135
+ rest_pos: "input.restPos",
136
+ uv: "vec3f(input.uv, 0.0)",
137
+ reflection: "reflect(-v, n)",
138
+ },
139
+ },
140
+
141
+ // ── Literals as nodes (for editor ergonomics; inlined literals work too) ──
142
+ value: { inputs: { value: F(0) }, outputs: { value: "float" }, emit: (a) => a.value },
143
+ rgb: { inputs: { color: C() }, outputs: { color: "color" }, emit: (a) => a.color },
144
+
145
+ // ── Color ──
146
+ hue_sat: {
147
+ inputs: { hue: F(0.5), saturation: F(1), value: F(1), fac: F(1), color: C([1, 1, 1], true) },
148
+ outputs: { color: "color" },
149
+ emit: (a) => `hue_sat(${a.hue}, ${a.saturation}, ${a.value}, ${a.fac}, ${a.color})`,
150
+ },
151
+ bright_contrast: {
152
+ inputs: { color: C([1, 1, 1], true), bright: F(0), contrast: F(0) },
153
+ outputs: { color: "color" },
154
+ emit: (a) => `bright_contrast(${a.color}, ${a.bright}, ${a.contrast})`,
155
+ },
156
+ invert: {
157
+ inputs: { fac: F(1), color: C([1, 1, 1], true) },
158
+ outputs: { color: "color" },
159
+ emit: (a) => `invert(${a.fac}, ${a.color})`,
160
+ },
161
+ ramp_constant: {
162
+ inputs: RAMP_INPUTS,
163
+ outputs: RAMP_OUTPUTS,
164
+ outputSelect: RAMP_SELECT,
165
+ emit: (a) => `ramp_constant(${a.fac}, ${a.pos0}, ${a.color0}, ${a.pos1}, ${a.color1})`,
166
+ },
167
+ ramp_linear: {
168
+ inputs: RAMP_INPUTS,
169
+ outputs: RAMP_OUTPUTS,
170
+ outputSelect: RAMP_SELECT,
171
+ emit: (a) => `ramp_linear(${a.fac}, ${a.pos0}, ${a.color0}, ${a.pos1}, ${a.color1})`,
172
+ },
173
+ ramp_cardinal: {
174
+ inputs: RAMP_INPUTS,
175
+ outputs: RAMP_OUTPUTS,
176
+ outputSelect: RAMP_SELECT,
177
+ emit: (a) => `ramp_cardinal(${a.fac}, ${a.pos0}, ${a.color0}, ${a.pos1}, ${a.color1})`,
178
+ },
179
+ ramp_constant_aa: {
180
+ inputs: { fac: F(0.5, true), edge: F(0.5), color0: V4([0, 0, 0, 1]), color1: V4([1, 1, 1, 1]) },
181
+ outputs: RAMP_OUTPUTS,
182
+ outputSelect: RAMP_SELECT,
183
+ emit: (a) => `ramp_constant_edge_aa(${a.fac}, ${a.edge}, ${a.color0}, ${a.color1})`,
184
+ },
185
+ // Blender ColorRamp LINEAR with 3 stops black→white→black (triangular peak at 0.5).
186
+ // The only >2-stop ramp the presets use; folded to closed form like the hand port.
187
+ ramp_tri: {
188
+ inputs: { fac: F(0.5, true) },
189
+ outputs: { value: "float" },
190
+ emit: (a) => `1.0 - abs(2.0 * ${a.fac} - 1.0)`,
191
+ },
192
+
193
+ // ── Math (enum op in type string) ──
194
+ "math/add": { inputs: { a: F(0), b: F(0) }, outputs: { value: "float" }, emit: (a) => `math_add(${a.a}, ${a.b})` },
195
+ "math/multiply": {
196
+ inputs: { a: F(0), b: F(0) },
197
+ outputs: { value: "float" },
198
+ emit: (a) => `math_multiply(${a.a}, ${a.b})`,
199
+ },
200
+ "math/power": {
201
+ inputs: { a: F(0), b: F(1) },
202
+ outputs: { value: "float" },
203
+ emit: (a) => `math_power(${a.a}, ${a.b})`,
204
+ },
205
+ "math/greater_than": {
206
+ inputs: { a: F(0), b: F(0.5) },
207
+ outputs: { value: "float" },
208
+ emit: (a) => `math_greater_than(${a.a}, ${a.b})`,
209
+ },
210
+ "math/clamp01": { inputs: { a: F(0) }, outputs: { value: "float" }, emit: (a) => `saturate(${a.a})` },
211
+
212
+ // ── Mix (blend type in type string) ──
213
+ "mix/blend": {
214
+ inputs: { fac: F(0.5), a: C([1, 1, 1], true), b: C() },
215
+ outputs: { color: "color" },
216
+ emit: (a) => `mix_blend(${a.fac}, ${a.a}, ${a.b})`,
217
+ },
218
+ "mix/overlay": {
219
+ inputs: { fac: F(0.5), a: C([1, 1, 1], true), b: C() },
220
+ outputs: { color: "color" },
221
+ emit: (a) => `mix_overlay(${a.fac}, ${a.a}, ${a.b})`,
222
+ },
223
+ "mix/multiply": {
224
+ inputs: { fac: F(0.5), a: C([1, 1, 1], true), b: C() },
225
+ outputs: { color: "color" },
226
+ emit: (a) => `mix_multiply(${a.fac}, ${a.a}, ${a.b})`,
227
+ },
228
+ "mix/lighten": {
229
+ inputs: { fac: F(0.5), a: C([1, 1, 1], true), b: C() },
230
+ outputs: { color: "color" },
231
+ emit: (a) => `mix_lighten(${a.fac}, ${a.a}, ${a.b})`,
232
+ },
233
+ "mix/linear_light": {
234
+ inputs: { fac: F(0.5), a: C([1, 1, 1], true), b: C() },
235
+ outputs: { color: "color" },
236
+ emit: (a) => `mix_linear_light(${a.fac}, ${a.a}, ${a.b})`,
237
+ },
238
+ // Emission-add: color + scalar-gated emission. Stand-in for Blender's
239
+ // Emission → Add Shader pair in the ShaderToRGB era (see hair's bright-tex gate).
240
+ "mix/add_emit": {
241
+ inputs: { a: C([0, 0, 0], true), b: F(0) },
242
+ outputs: { color: "color" },
243
+ emit: (a) => `${a.a} + vec3f(${a.b})`,
244
+ },
245
+ // Blender Emission shader: color × strength. In the ShaderToRGB-era ports the
246
+ // emission result feeds a Mix Shader directly as radiance. Color may be a literal
247
+ // (body's rim tints) or linked.
248
+ emission: {
249
+ inputs: { color: C([1, 1, 1]), strength: F(1) },
250
+ outputs: { color: "color" },
251
+ emit: (a) => `${a.color} * ${a.strength}`,
252
+ },
253
+ // Blender Add Shader — radiance sum of two evaluated shading results.
254
+ add_shader: {
255
+ inputs: { a: C([0, 0, 0], true), b: C([0, 0, 0], true) },
256
+ outputs: { color: "color" },
257
+ emit: (a) => `${a.a} + ${a.b}`,
258
+ },
259
+ // Mix Shader — plain lerp between two evaluated shading results.
260
+ mix_shader: {
261
+ inputs: { fac: F(0.5), a: C([0, 0, 0], true), b: C([0, 0, 0]) },
262
+ outputs: { color: "color" },
263
+ emit: (a) => `mix(${a.a}, ${a.b}, ${a.fac})`,
264
+ },
265
+
266
+ // ── View-dependent scalars ──
267
+ fresnel: { inputs: { ior: F(1.45) }, outputs: { value: "float" }, emit: (a) => `fresnel(${a.ior}, n, v)` },
268
+ "layer_weight/fresnel": {
269
+ inputs: { blend: F(0.5) },
270
+ outputs: { value: "float" },
271
+ emit: (a) => `layer_weight_fresnel(${a.blend}, n, v)`,
272
+ },
273
+ "layer_weight/facing": {
274
+ inputs: { blend: F(0.5) },
275
+ outputs: { value: "float" },
276
+ emit: (a) => `layer_weight_facing(${a.blend}, n, v)`,
277
+ },
278
+
279
+ // ── Lighting capture ──
280
+ shader_to_rgb_diffuse: {
281
+ inputs: {},
282
+ outputs: { value: "float" },
283
+ emit: () => `shader_to_rgb_diffuse(n, l, sun, amb, shadow)`,
284
+ },
285
+
286
+ // ── Vector ──
287
+ separate_xyz: {
288
+ inputs: { vector: V([0, 0, 0], true) },
289
+ outputs: { x: "float", y: "float", z: "float" },
290
+ outputSelect: { x: ".x", y: ".y", z: ".z" },
291
+ emit: (a) => a.vector,
292
+ },
293
+ vect_cross: {
294
+ // b may be a literal constant (metal crosses the reflection dir with (0,1,0)).
295
+ inputs: { a: V([0, 0, 0], true), b: V([0, 1, 0]) },
296
+ outputs: { vector: "vector" },
297
+ emit: (a) => `vect_math_cross(${a.a}, ${a.b})`,
298
+ },
299
+ mapping: {
300
+ inputs: { vector: V([0, 0, 0], true), loc: V([0, 0, 0]), rot: V([0, 0, 0]), scl: V([1, 1, 1]) },
301
+ outputs: { vector: "vector" },
302
+ emit: (a) => `mapping_point(${a.vector}, ${a.loc}, ${a.rot}, ${a.scl})`,
303
+ },
304
+ bump: {
305
+ // Screen-space bump; world position comes from context (matches bump_lh's port).
306
+ inputs: { strength: F(0.1), height: F(0, true), normal: V([0, 0, 0], true) },
307
+ outputs: { vector: "vector" },
308
+ emit: (a) => `bump_lh(${a.strength}, ${a.height}, ${a.normal}, input.worldPos)`,
309
+ },
310
+
311
+ // ── Procedural textures ──
312
+ tex_noise: {
313
+ inputs: { vector: V([0, 0, 0], true), scale: F(5), detail: F(2), roughness: F(0.5), distortion: F(0) },
314
+ outputs: { value: "float" },
315
+ emit: (a) => `tex_noise(${a.vector}, ${a.scale}, ${a.detail}, ${a.roughness}, ${a.distortion})`,
316
+ },
317
+ tex_gradient: {
318
+ inputs: { vector: V([0, 0, 0], true) },
319
+ outputs: { value: "float" },
320
+ emit: (a) => `tex_gradient_linear(${a.vector})`,
321
+ },
322
+ "tex_voronoi/f1": {
323
+ inputs: { vector: V([0, 0, 0], true), scale: F(5) },
324
+ outputs: { value: "float" },
325
+ emit: (a) => `tex_voronoi_f1(${a.vector}, ${a.scale})`,
326
+ },
327
+ "tex_voronoi/color": {
328
+ inputs: { vector: V([0, 0, 0], true), scale: F(5) },
329
+ outputs: { color: "color" },
330
+ emit: (a) => `tex_voronoi_color(${a.vector}, ${a.scale})`,
331
+ },
332
+
333
+ // ── Principled BSDF (frozen 3.6 legacy-EEVEE semantics: eval_principled port) ──
334
+ // normal defaults to the template's shading normal; link a bump/normal_map chain
335
+ // to perturb it (body/cloth_rough noise bump).
336
+ principled: {
337
+ inputs: {
338
+ base: C([0.8, 0.8, 0.8], true),
339
+ metallic: F(0),
340
+ specular: F(0.5),
341
+ roughness: F(0.5),
342
+ spec_clamp: F(1e30),
343
+ sheen: F(0),
344
+ sheen_tint: F(0),
345
+ normal: { type: "vector", contextDefault: "n" },
346
+ },
347
+ outputs: { color: "color" },
348
+ emit: (a) =>
349
+ `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)`,
350
+ },
351
+ }
@@ -0,0 +1,60 @@
1
+ // Material style graph — JSON-serializable node graph compiled to WGSL by compile.ts.
2
+ // Engine-space (LH, Y-up): porting a Blender graph converts coordinates at authoring
3
+ // time (Blender Normal Z → engine Y), the compiler never sees Blender conventions.
4
+ // See docs/graph-compiler-spec.md.
5
+
6
+ import type { MaterialPreset } from "../engine"
7
+
8
+ export type SocketValue = number | [number, number, number] | [number, number, number, number]
9
+
10
+ export type GraphNode = {
11
+ /** Unique within the graph, /^[a-z0-9_]+$/ — becomes the WGSL variable suffix. */
12
+ id: string
13
+ /** Node registry key, e.g. "hue_sat", "math/power", "mix/blend". */
14
+ type: string
15
+ /** Literal defaults for unlinked input sockets. */
16
+ inputs?: Record<string, SocketValue>
17
+ /** Editor-only metadata (React Flow position). Ignored by the compiler, round-tripped
18
+ * by serialization so engine-side tooling never strips an editor's layout. */
19
+ ui?: { position?: { x: number; y: number } }
20
+ }
21
+
22
+ export type GraphLink = {
23
+ from: { node: string; socket: string }
24
+ to: { node: string; socket: string }
25
+ }
26
+
27
+ /** Adjust-tier slider: overrides one unlinked literal input with a StyleUniforms read. */
28
+ export type ExposedParam = {
29
+ /** Stable key — slot assignment and serialization identity. */
30
+ id: string
31
+ label: string
32
+ target: { node: string; socket: string }
33
+ kind: "float" | "color"
34
+ min?: number
35
+ max?: number
36
+ default: SocketValue
37
+ }
38
+
39
+ export type StyleGraph = {
40
+ version: 1
41
+ name: string
42
+ /** Which preset slot this style targets. The slot owns pass integration (stencil
43
+ * variants like hair-over-eyes, alpha semantics, draw order) — always on,
44
+ * graph-invisible. The graph only restyles the slot's shading. */
45
+ slot: MaterialPreset
46
+ nodes: GraphNode[]
47
+ links: GraphLink[]
48
+ /** Must resolve to a color (vec3f) or float (auto-splatted) socket. */
49
+ output: { node: string; socket: string }
50
+ params?: ExposedParam[]
51
+ }
52
+
53
+ export type Diagnostic = {
54
+ severity: "error" | "warning"
55
+ nodeId?: string
56
+ message: string
57
+ }
58
+
59
+ export const MAX_NODES = 64
60
+ export const MAX_PARAMS = 16
@@ -0,0 +1,178 @@
1
+ // Per-slot WGSL templates. The graph computes only `final_color`; everything the slot
2
+ // owns — texture sample, MMD alpha semantics, discard, lighting context, FSOut/mask
3
+ // writes, and built-in pass effects like hair's over-eyes stencil variant — lives here,
4
+ // always on, graph-invisible. Mirrors the hand-written material shaders line-for-line.
5
+
6
+ import { NODES_WGSL } from "../shaders/materials/nodes"
7
+ import { COMMON_MATERIAL_PRELUDE_WGSL } from "../shaders/materials/common"
8
+ import type { MaterialPreset } from "../engine"
9
+
10
+ export type SlotTemplate = {
11
+ /** Module-scope declarations (pipeline-override constants, helper fns). */
12
+ decls: string
13
+ /** Optional replacement for the standard fs() prelude (custom alpha semantics). */
14
+ prelude?: string
15
+ /** Tail of fs(): consumes `final_color` + template locals, writes FSOut. */
16
+ epilogue: string
17
+ }
18
+
19
+ const DEFAULT_EPILOGUE = ` var out: FSOut;
20
+ out.color = vec4f(final_color, alpha);
21
+ out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
22
+ return out;
23
+ `
24
+
25
+ const DEFAULT_TEMPLATE: SlotTemplate = { decls: "", epilogue: DEFAULT_EPILOGUE }
26
+
27
+ // Hair's built-in over-eyes effect (see hair.ts): the engine compiles two pipeline
28
+ // variants from the same module — normal opaque hair, and a stencil-matched re-draw
29
+ // at 25% alpha so eyes read through the silhouette. Always on for the hair slot.
30
+ const HAIR_TEMPLATE: SlotTemplate = {
31
+ decls: `override IS_OVER_EYES: bool = false;
32
+
33
+ `,
34
+ epilogue: ` var outAlpha = alpha;
35
+ if (IS_OVER_EYES) { outAlpha = alpha * 0.25; }
36
+
37
+ var out: FSOut;
38
+ out.color = vec4f(final_color, outAlpha);
39
+ out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
40
+ return out;
41
+ `,
42
+ }
43
+
44
+ // Stockings' built-in alpha behavior (see stockings.ts): Wyman & McGuire hashed
45
+ // alpha testing on bind-pose restPos replaces the standard threshold discard —
46
+ // sort-independent through self-overlap, dither pinned to the fabric. The graph
47
+ // only computes final_color; the hash gate and the alpha=1 output are slot-owned.
48
+ const STOCKINGS_TEMPLATE: SlotTemplate = {
49
+ decls: `fn _hash3d_wm(a: vec3f) -> f32 {
50
+ return _hash33(a).x * 0.5 + 0.5;
51
+ }
52
+ fn hashed_alpha_threshold(co: vec3f) -> f32 {
53
+ let alphaHashScale: f32 = 1.0;
54
+ let max_deriv = max(length(dpdx(co)), length(dpdy(co)));
55
+ let pix_scale = 1.0 / max(alphaHashScale * max_deriv, 1e-6);
56
+ let pix_scale_log = log2(pix_scale);
57
+ let px_lo = exp2(floor(pix_scale_log));
58
+ let px_hi = exp2(ceil(pix_scale_log));
59
+ let a_lo = _hash3d_wm(floor(px_lo * co));
60
+ let a_hi = _hash3d_wm(floor(px_hi * co));
61
+ let fac = fract(pix_scale_log);
62
+ let x = mix(a_lo, a_hi, fac);
63
+ let a = min(fac, 1.0 - fac);
64
+ let one_a = 1.0 - a;
65
+ let denom = 1.0 / max(2.0 * a * one_a, 1e-6);
66
+ let one_x = 1.0 - x;
67
+ let case_lo = (x * x) * denom;
68
+ let case_mid = (x - 0.5 * a) / max(one_a, 1e-6);
69
+ let case_hi = 1.0 - (one_x * one_x) * denom;
70
+ var threshold = select(case_hi, select(case_lo, case_mid, x >= a), x < one_a);
71
+ return clamp(threshold, 1e-6, 1.0);
72
+ }
73
+
74
+ `,
75
+ prelude: `@fragment fn fs(input: VertexOutput) -> FSOut {
76
+ let tex_s = textureSample(diffuseTexture, diffuseSampler, input.uv);
77
+ // Hashed alpha test (EEVEE "Hashed" blend) instead of the standard threshold.
78
+ let alpha = material.alpha * tex_s.a;
79
+ if (alpha < hashed_alpha_threshold(input.restPos)) { discard; }
80
+
81
+ let n = safe_normal(input.normal);
82
+ let v = normalize(camera.viewPos - input.worldPos);
83
+ let l = -light.lights[0].direction.xyz;
84
+ let sun = light.lights[0].color.xyz * light.lights[0].color.w;
85
+ let amb = light.ambientColor.xyz;
86
+ let shadow = sampleShadow(input.worldPos, n);
87
+ let tex_color = tex_s.rgb;
88
+
89
+ `,
90
+ epilogue: ` var out: FSOut;
91
+ out.color = vec4f(final_color, 1.0);
92
+ out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
93
+ return out;
94
+ `,
95
+ }
96
+
97
+ // Eye's built-in rear-view gate (see eye.ts): open-shell PMX heads don't occlude
98
+ // the eye from behind, so it would draw (and stamp the see-through stencil) through
99
+ // the back of the head. Gate by camera-vs-face hemisphere via the 頭 bone's skinning
100
+ // matrix. Discarding here drops color, depth, and the stencil stamp together. The
101
+ // stencil-stamp pipeline state + front-face cull are slot-owned in createSlotPipeline;
102
+ // the graph only computes the eye's shading (Principled + emission).
103
+ const EYE_TEMPLATE: SlotTemplate = {
104
+ decls: "",
105
+ prelude: `@fragment fn fs(input: VertexOutput) -> FSOut {
106
+ let tex_s = textureSample(diffuseTexture, diffuseSampler, input.uv);
107
+ let alpha = material.alpha * tex_s.a;
108
+ if (alpha < 0.001) { discard; }
109
+
110
+ let n = safe_normal(input.normal);
111
+ let v = normalize(camera.viewPos - input.worldPos);
112
+
113
+ if (material.headBoneIndex >= 0.0) {
114
+ let hm = skinMats[u32(material.headBoneIndex)];
115
+ let faceDir = -normalize(hm[2].xyz);
116
+ if (dot(faceDir, v) < -0.15) { discard; }
117
+ }
118
+
119
+ let l = -light.lights[0].direction.xyz;
120
+ let sun = light.lights[0].color.xyz * light.lights[0].color.w;
121
+ let amb = light.ambientColor.xyz;
122
+ let shadow = sampleShadow(input.worldPos, n);
123
+ let tex_color = tex_s.rgb;
124
+
125
+ `,
126
+ epilogue: DEFAULT_EPILOGUE,
127
+ }
128
+
129
+ export const SLOT_TEMPLATES: Partial<Record<MaterialPreset, SlotTemplate>> = {
130
+ hair: HAIR_TEMPLATE,
131
+ stockings: STOCKINGS_TEMPLATE,
132
+ eye: EYE_TEMPLATE,
133
+ }
134
+
135
+ export function slotTemplate(slot: MaterialPreset): SlotTemplate {
136
+ return SLOT_TEMPLATES[slot] ?? DEFAULT_TEMPLATE
137
+ }
138
+
139
+ // Adjust-tier uniforms — fixed 16-vec4f block (256 B) so the single shared material
140
+ // bind group layout serves every graph; non-graph presets bind a zero buffer.
141
+ export const STYLE_UNIFORMS_WGSL = `struct StyleUniforms { p: array<vec4f, 16> };
142
+ @group(2) @binding(4) var<uniform> style: StyleUniforms;
143
+
144
+ `
145
+
146
+ // fs() prelude — identical to the hand-written materials so template locals keep the
147
+ // exact names the registry's context nodes and emit functions reference.
148
+ const FS_PRELUDE = `@fragment fn fs(input: VertexOutput) -> FSOut {
149
+ let tex_s = textureSample(diffuseTexture, diffuseSampler, input.uv);
150
+ // MMD alpha semantics: material alpha × texture alpha (hair/lace textures cut
151
+ // their shapes in the alpha channel).
152
+ let alpha = material.alpha * tex_s.a;
153
+ if (alpha < 0.001) { discard; }
154
+
155
+ let n = safe_normal(input.normal);
156
+ let v = normalize(camera.viewPos - input.worldPos);
157
+ let l = -light.lights[0].direction.xyz;
158
+ let sun = light.lights[0].color.xyz * light.lights[0].color.w;
159
+ let amb = light.ambientColor.xyz;
160
+ let shadow = sampleShadow(input.worldPos, n);
161
+ let tex_color = tex_s.rgb;
162
+
163
+ `
164
+
165
+ export function assembleModule(slot: MaterialPreset, fsBody: string, includeStyleUniforms: boolean): string {
166
+ const tmpl = slotTemplate(slot)
167
+ return (
168
+ NODES_WGSL +
169
+ COMMON_MATERIAL_PRELUDE_WGSL +
170
+ (includeStyleUniforms ? STYLE_UNIFORMS_WGSL : "") +
171
+ tmpl.decls +
172
+ (tmpl.prelude ?? FS_PRELUDE) +
173
+ fsBody +
174
+ "\n" +
175
+ tmpl.epilogue +
176
+ "}\n"
177
+ )
178
+ }
package/src/index.ts CHANGED
@@ -10,11 +10,38 @@ export {
10
10
  type MaterialPreset,
11
11
  type ResolvedMaterialPreset,
12
12
  type MaterialPresetMap,
13
+ type ApplyStyleResult,
13
14
  type GizmoDragEvent,
14
15
  type GizmoDragCallback,
15
16
  type GizmoDragKind,
16
17
  } from "./engine"
17
18
  export { parsePmxFolderInput, pmxFileAtRelativePath, type PmxFolderInputResult } from "./folder-upload"
19
+ export {
20
+ compileGraph,
21
+ validateGraph,
22
+ assignStyleSlots,
23
+ type CompileOptions,
24
+ type CompileResult,
25
+ type StyleSlot,
26
+ } from "./graph/compile"
27
+ export type {
28
+ StyleGraph,
29
+ GraphNode,
30
+ GraphLink,
31
+ ExposedParam,
32
+ SocketValue,
33
+ Diagnostic,
34
+ } from "./graph/schema"
35
+ export { NODE_REGISTRY, type NodeSpec, type SockT } from "./graph/registry"
36
+ export { HAIR_GRAPH } from "./graph/presets/hair"
37
+ export { DEFAULT_GRAPH } from "./graph/presets/default"
38
+ export { CLOTH_SMOOTH_GRAPH } from "./graph/presets/cloth_smooth"
39
+ export { CLOTH_ROUGH_GRAPH } from "./graph/presets/cloth_rough"
40
+ export { METAL_GRAPH } from "./graph/presets/metal"
41
+ export { BODY_GRAPH } from "./graph/presets/body"
42
+ export { STOCKINGS_GRAPH } from "./graph/presets/stockings"
43
+ export { EYE_GRAPH } from "./graph/presets/eye"
44
+ export { FACE_GRAPH } from "./graph/presets/face"
18
45
  export { Model } from "./model"
19
46
  export { Vec3, Quat, Mat4 } from "./math"
20
47
  export type {
@@ -1,30 +0,0 @@
1
- import type { RezePhysics } from "./physics";
2
- export declare class PhysicsDebugRenderer {
3
- private device;
4
- private bindGroup;
5
- private wirePipelineSphere;
6
- private wirePipelineBox;
7
- private wirePipelineCapsule;
8
- private wireSphereBuffer;
9
- private wireBoxBuffer;
10
- private wireCapsuleBuffer;
11
- private wireSphereCount;
12
- private wireBoxCount;
13
- private wireCapsuleCount;
14
- private solidPipelineSphere;
15
- private solidPipelineBox;
16
- private solidPipelineCapsule;
17
- private solidSphereBuffer;
18
- private solidBoxBuffer;
19
- private solidCapsuleBuffer;
20
- private solidSphereCount;
21
- private solidBoxCount;
22
- private solidCapsuleCount;
23
- private instanceBuffer;
24
- private instanceData;
25
- private instanceCapacity;
26
- constructor(device: GPUDevice, cameraUniformBuffer: GPUBuffer, presentationFormat: GPUTextureFormat);
27
- render(pass: GPURenderPassEncoder, physics: RezePhysics): void;
28
- destroy(): void;
29
- }
30
- //# sourceMappingURL=physics-debug.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"physics-debug.d.ts","sourceRoot":"","sources":["../src/physics-debug.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAmB5C,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,MAAM,CAAW;IACzB,OAAO,CAAC,SAAS,CAAc;IAG/B,OAAO,CAAC,kBAAkB,CAAmB;IAC7C,OAAO,CAAC,eAAe,CAAmB;IAC1C,OAAO,CAAC,mBAAmB,CAAmB;IAC9C,OAAO,CAAC,gBAAgB,CAAW;IACnC,OAAO,CAAC,aAAa,CAAW;IAChC,OAAO,CAAC,iBAAiB,CAAW;IACpC,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,YAAY,CAAQ;IAC5B,OAAO,CAAC,gBAAgB,CAAQ;IAIhC,OAAO,CAAC,mBAAmB,CAAmB;IAC9C,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,oBAAoB,CAAmB;IAC/C,OAAO,CAAC,iBAAiB,CAAW;IACpC,OAAO,CAAC,cAAc,CAAW;IACjC,OAAO,CAAC,kBAAkB,CAAW;IACrC,OAAO,CAAC,gBAAgB,CAAQ;IAChC,OAAO,CAAC,aAAa,CAAQ;IAC7B,OAAO,CAAC,iBAAiB,CAAQ;IAEjC,OAAO,CAAC,cAAc,CAAW;IACjC,OAAO,CAAC,YAAY,CAAc;IAClC,OAAO,CAAC,gBAAgB,CAAQ;gBAG9B,MAAM,EAAE,SAAS,EACjB,mBAAmB,EAAE,SAAS,EAC9B,kBAAkB,EAAE,gBAAgB;IA2HtC,MAAM,CAAC,IAAI,EAAE,oBAAoB,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI;IAmI9D,OAAO,IAAI,IAAI;CAShB"}