reze-engine 0.17.1 → 0.18.0

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 (65) 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/hair.d.ts +3 -0
  23. package/dist/graph/presets/hair.d.ts.map +1 -0
  24. package/dist/graph/presets/hair.js +79 -0
  25. package/dist/graph/presets/metal.d.ts +3 -0
  26. package/dist/graph/presets/metal.d.ts.map +1 -0
  27. package/dist/graph/presets/metal.js +58 -0
  28. package/dist/graph/presets/stockings.d.ts +3 -0
  29. package/dist/graph/presets/stockings.d.ts.map +1 -0
  30. package/dist/graph/presets/stockings.js +54 -0
  31. package/dist/graph/registry.d.ts +28 -0
  32. package/dist/graph/registry.d.ts.map +1 -0
  33. package/dist/graph/registry.js +322 -0
  34. package/dist/graph/schema.d.ts +66 -0
  35. package/dist/graph/schema.d.ts.map +1 -0
  36. package/dist/graph/schema.js +6 -0
  37. package/dist/graph/slots.d.ts +14 -0
  38. package/dist/graph/slots.d.ts.map +1 -0
  39. package/dist/graph/slots.js +123 -0
  40. package/dist/index.d.ts +11 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +9 -0
  43. package/dist/physics/profile.d.ts +18 -0
  44. package/dist/physics/profile.d.ts.map +1 -0
  45. package/dist/physics/profile.js +44 -0
  46. package/package.json +2 -1
  47. package/src/engine.ts +290 -15
  48. package/src/graph/compile.ts +342 -0
  49. package/src/graph/presets/body.ts +103 -0
  50. package/src/graph/presets/cloth_rough.ts +64 -0
  51. package/src/graph/presets/cloth_smooth.ts +56 -0
  52. package/src/graph/presets/default.ts +23 -0
  53. package/src/graph/presets/hair.ts +82 -0
  54. package/src/graph/presets/metal.ts +61 -0
  55. package/src/graph/presets/stockings.ts +57 -0
  56. package/src/graph/registry.ts +351 -0
  57. package/src/graph/schema.ts +60 -0
  58. package/src/graph/slots.ts +145 -0
  59. package/src/index.ts +25 -0
  60. package/dist/physics-debug.d.ts +0 -30
  61. package/dist/physics-debug.d.ts.map +0 -1
  62. package/dist/physics-debug.js +0 -526
  63. package/dist/shaders/passes/physics-debug.d.ts +0 -2
  64. package/dist/shaders/passes/physics-debug.d.ts.map +0 -1
  65. package/dist/shaders/passes/physics-debug.js +0 -69
@@ -0,0 +1,342 @@
1
+ // Graph → WGSL compiler: validate → prune → toposort → peephole → emit.
2
+ // Deterministic by construction (topo ties broken by node id, literals formatted by
3
+ // shortest round-trip) so the same graph JSON always yields byte-identical WGSL.
4
+ // See docs/graph-compiler-spec.md.
5
+
6
+ import { MAX_NODES, MAX_PARAMS } from "./schema"
7
+ import type { Diagnostic, ExposedParam, GraphNode, SocketValue, StyleGraph } from "./schema"
8
+ import { NODE_REGISTRY, canConvert, convert, fmtValue, literalFits } from "./registry"
9
+ import type { NodeSpec, SockT } from "./registry"
10
+ import { assembleModule } from "./slots"
11
+
12
+ export type CompileOptions = {
13
+ /** Fold exposed params to their defaults as consts (no StyleUniforms binding).
14
+ * Used by golden tests and by hosts that don't bind the style buffer. */
15
+ inlineParams?: boolean
16
+ /** Override the graph output with any node's socket — Blender's node-preview
17
+ * (Ctrl+Shift+Click viewer) workflow for the editor. */
18
+ previewNode?: { node: string; socket: string }
19
+ }
20
+
21
+ /** UBO slot for one exposed param: write `value` at style.p[vec4Index] (+ component). */
22
+ export type StyleSlot = {
23
+ id: string
24
+ kind: "float" | "color"
25
+ vec4Index: number
26
+ component?: "x" | "y" | "z" | "w"
27
+ expr: string
28
+ }
29
+
30
+ export type CompileResult = {
31
+ ok: boolean
32
+ /** Full WGSL module (empty string when !ok). */
33
+ wgsl: string
34
+ /** Just the generated node section — snapshot tests and editor debugging. */
35
+ fsBody: string
36
+ slotMap: StyleSlot[]
37
+ diagnostics: Diagnostic[]
38
+ prunedNodes: string[]
39
+ }
40
+
41
+ const ID_RE = /^[a-z0-9_]+$/
42
+
43
+ const err = (message: string, nodeId?: string): Diagnostic => ({ severity: "error", nodeId, message })
44
+ const warn = (message: string, nodeId?: string): Diagnostic => ({ severity: "warning", nodeId, message })
45
+
46
+ // ─── Param slot assignment ──────────────────────────────────────────
47
+ // Floats pack 4-per-vec4 in declaration order; colors take .rgb of a fresh vec4.
48
+ // Depends only on the params array, never on graph topology — a topology edit
49
+ // doesn't move slider slots.
50
+
51
+ const COMPONENTS = ["x", "y", "z", "w"] as const
52
+
53
+ export function assignStyleSlots(params: ExposedParam[]): StyleSlot[] {
54
+ const slots: StyleSlot[] = []
55
+ let vec4Index = 0
56
+ let comp = 0
57
+ for (const p of params) {
58
+ if (p.kind === "color") {
59
+ if (comp > 0) {
60
+ vec4Index++
61
+ comp = 0
62
+ }
63
+ slots.push({ id: p.id, kind: "color", vec4Index, expr: `style.p[${vec4Index}].rgb` })
64
+ vec4Index++
65
+ } else {
66
+ const c = COMPONENTS[comp]
67
+ slots.push({ id: p.id, kind: "float", vec4Index, component: c, expr: `style.p[${vec4Index}].${c}` })
68
+ comp++
69
+ if (comp === 4) {
70
+ vec4Index++
71
+ comp = 0
72
+ }
73
+ }
74
+ }
75
+ return slots
76
+ }
77
+
78
+ // ─── Validation ─────────────────────────────────────────────────────
79
+
80
+ export function validateGraph(graph: StyleGraph, opts: CompileOptions = {}): Diagnostic[] {
81
+ const d: Diagnostic[] = []
82
+ const nodes = new Map<string, GraphNode>()
83
+
84
+ if (graph.version !== 1) d.push(err(`unsupported graph version: ${graph.version}`))
85
+ if (graph.nodes.length > MAX_NODES) d.push(err(`graph has ${graph.nodes.length} nodes (max ${MAX_NODES})`))
86
+
87
+ for (const node of graph.nodes) {
88
+ if (!ID_RE.test(node.id)) d.push(err(`invalid node id "${node.id}" (want /^[a-z0-9_]+$/)`, node.id))
89
+ if (nodes.has(node.id)) d.push(err(`duplicate node id "${node.id}"`, node.id))
90
+ nodes.set(node.id, node)
91
+ const spec = NODE_REGISTRY[node.type]
92
+ if (!spec) {
93
+ d.push(err(`unknown node type "${node.type}"`, node.id))
94
+ continue
95
+ }
96
+ for (const [socket, value] of Object.entries(node.inputs ?? {})) {
97
+ const input = spec.inputs[socket]
98
+ if (!input) d.push(err(`node "${node.id}" (${node.type}) has no input socket "${socket}"`, node.id))
99
+ else if (!literalFits(value, input.type))
100
+ d.push(err(`literal for "${node.id}.${socket}" doesn't fit ${input.type} socket`, node.id))
101
+ }
102
+ }
103
+
104
+ const linkedInputs = new Set<string>()
105
+ for (const link of graph.links) {
106
+ const fromNode = nodes.get(link.from.node)
107
+ const toNode = nodes.get(link.to.node)
108
+ if (!fromNode) {
109
+ d.push(err(`link source node "${link.from.node}" doesn't exist`))
110
+ continue
111
+ }
112
+ if (!toNode) {
113
+ d.push(err(`link target node "${link.to.node}" doesn't exist`))
114
+ continue
115
+ }
116
+ const fromSpec = NODE_REGISTRY[fromNode.type]
117
+ const toSpec = NODE_REGISTRY[toNode.type]
118
+ if (!fromSpec || !toSpec) continue // unknown type already reported
119
+ const fromT = fromSpec.outputs[link.from.socket]
120
+ if (!fromT) {
121
+ d.push(err(`node "${link.from.node}" (${fromNode.type}) has no output socket "${link.from.socket}"`, link.from.node))
122
+ continue
123
+ }
124
+ const toInput = toSpec.inputs[link.to.socket]
125
+ if (!toInput) {
126
+ d.push(err(`node "${link.to.node}" (${toNode.type}) has no input socket "${link.to.socket}"`, link.to.node))
127
+ continue
128
+ }
129
+ if (toInput.type === "vec4") {
130
+ d.push(err(`ramp stop colors must be literals — cannot link into "${link.to.node}.${link.to.socket}"`, link.to.node))
131
+ continue
132
+ }
133
+ if (!canConvert(fromT, toInput.type))
134
+ d.push(
135
+ err(
136
+ `type mismatch: "${link.from.node}.${link.from.socket}" (${fromT}) → "${link.to.node}.${link.to.socket}" (${toInput.type})`,
137
+ link.to.node,
138
+ ),
139
+ )
140
+ const key = `${link.to.node}.${link.to.socket}`
141
+ if (linkedInputs.has(key)) d.push(err(`input "${key}" has more than one incoming link`, link.to.node))
142
+ linkedInputs.add(key)
143
+ }
144
+
145
+ const params = graph.params ?? []
146
+ if (params.length > MAX_PARAMS) d.push(err(`graph exposes ${params.length} params (max ${MAX_PARAMS})`))
147
+ const paramIds = new Set<string>()
148
+ const paramTargets = new Set<string>()
149
+ for (const p of params) {
150
+ if (paramIds.has(p.id)) d.push(err(`duplicate param id "${p.id}"`))
151
+ paramIds.add(p.id)
152
+ const node = nodes.get(p.target.node)
153
+ if (!node) {
154
+ d.push(err(`param "${p.id}" targets missing node "${p.target.node}"`))
155
+ continue
156
+ }
157
+ const spec = NODE_REGISTRY[node.type]
158
+ const input = spec?.inputs[p.target.socket]
159
+ if (!input) {
160
+ d.push(err(`param "${p.id}" targets missing socket "${p.target.node}.${p.target.socket}"`, p.target.node))
161
+ continue
162
+ }
163
+ const key = `${p.target.node}.${p.target.socket}`
164
+ if (linkedInputs.has(key)) d.push(err(`param "${p.id}" targets linked socket "${key}" — sliders only override literals`, p.target.node))
165
+ if (paramTargets.has(key)) d.push(err(`params target socket "${key}" more than once`, p.target.node))
166
+ paramTargets.add(key)
167
+ const wantKind: SockT = p.kind === "color" ? "color" : "float"
168
+ if (input.type !== wantKind)
169
+ d.push(err(`param "${p.id}" is ${p.kind} but socket "${key}" is ${input.type}`, p.target.node))
170
+ if (!literalFits(p.default, input.type)) d.push(err(`param "${p.id}" default doesn't fit ${input.type}`, p.target.node))
171
+ }
172
+
173
+ const out = opts.previewNode ?? graph.output
174
+ const outNode = nodes.get(out.node)
175
+ const outSpec = outNode ? NODE_REGISTRY[outNode.type] : undefined
176
+ const outT = outSpec?.outputs[out.socket]
177
+ if (!outT) d.push(err(`output "${out.node}.${out.socket}" doesn't resolve`, out.node))
178
+ else if (outT === "vec4") d.push(err(`output "${out.node}.${out.socket}" must be color or float`, out.node))
179
+
180
+ return d
181
+ }
182
+
183
+ // ─── Compile ────────────────────────────────────────────────────────
184
+
185
+ export function compileGraph(graph: StyleGraph, opts: CompileOptions = {}): CompileResult {
186
+ const fail = (diagnostics: Diagnostic[]): CompileResult => ({
187
+ ok: false,
188
+ wgsl: "",
189
+ fsBody: "",
190
+ slotMap: assignStyleSlots(graph.params ?? []),
191
+ diagnostics,
192
+ prunedNodes: [],
193
+ })
194
+
195
+ const diagnostics = validateGraph(graph, opts)
196
+ if (diagnostics.some((x) => x.severity === "error")) return fail(diagnostics)
197
+
198
+ const nodes = new Map(graph.nodes.map((node) => [node.id, node]))
199
+ const spec = (node: GraphNode): NodeSpec => NODE_REGISTRY[node.type]
200
+
201
+ // incoming[nodeId.socket] = producer; outgoing edges for toposort
202
+ const incoming = new Map<string, { node: string; socket: string }>()
203
+ const dependsOn = new Map<string, Set<string>>()
204
+ for (const id of nodes.keys()) dependsOn.set(id, new Set())
205
+ for (const link of graph.links) {
206
+ incoming.set(`${link.to.node}.${link.to.socket}`, link.from)
207
+ dependsOn.get(link.to.node)!.add(link.from.node)
208
+ }
209
+
210
+ // Prune: reverse-DFS from the (possibly preview-overridden) output.
211
+ const out = opts.previewNode ?? graph.output
212
+ const reachable = new Set<string>()
213
+ const stack = [out.node]
214
+ while (stack.length) {
215
+ const id = stack.pop()!
216
+ if (reachable.has(id)) continue
217
+ reachable.add(id)
218
+ for (const dep of dependsOn.get(id)!) stack.push(dep)
219
+ }
220
+ const prunedNodes = [...nodes.keys()].filter((id) => !reachable.has(id)).sort()
221
+
222
+ // requiresLink applies to live nodes only.
223
+ for (const id of reachable) {
224
+ const node = nodes.get(id)!
225
+ for (const [socket, input] of Object.entries(spec(node).inputs)) {
226
+ if (input.requiresLink && !incoming.has(`${id}.${socket}`) && node.inputs?.[socket] === undefined)
227
+ diagnostics.push(err(`input "${id}.${socket}" must be linked`, id))
228
+ }
229
+ }
230
+
231
+ // Exposed params → style slots. Slot assignment is topology-independent; a param
232
+ // whose target got pruned keeps its slot but has no effect (warning).
233
+ const params = graph.params ?? []
234
+ const slotMap = assignStyleSlots(params)
235
+ const paramBySocket = new Map<string, { param: ExposedParam; slot: StyleSlot }>()
236
+ params.forEach((param, i) => {
237
+ if (!reachable.has(param.target.node))
238
+ diagnostics.push(warn(`param "${param.id}" targets pruned node "${param.target.node}" — slider has no effect`))
239
+ paramBySocket.set(`${param.target.node}.${param.target.socket}`, { param, slot: slotMap[i] })
240
+ })
241
+ if (diagnostics.some((x) => x.severity === "error")) return fail(diagnostics)
242
+
243
+ // Toposort (Kahn) over reachable nodes; ready set kept sorted for determinism.
244
+ const remaining = new Map<string, Set<string>>()
245
+ for (const id of reachable) {
246
+ const deps = new Set([...dependsOn.get(id)!].filter((dep) => reachable.has(dep)))
247
+ remaining.set(id, deps)
248
+ }
249
+ const order: string[] = []
250
+ const ready = [...remaining.entries()]
251
+ .filter(([, deps]) => deps.size === 0)
252
+ .map(([id]) => id)
253
+ .sort()
254
+ while (ready.length) {
255
+ const id = ready.shift()!
256
+ order.push(id)
257
+ for (const [other, deps] of remaining) {
258
+ if (deps.delete(id) && deps.size === 0) {
259
+ ready.push(other)
260
+ ready.sort()
261
+ }
262
+ }
263
+ remaining.delete(id)
264
+ }
265
+ if (remaining.size) {
266
+ diagnostics.push(err(`cycle through nodes: ${[...remaining.keys()].sort().join(" → ")}`))
267
+ return fail(diagnostics)
268
+ }
269
+
270
+ // ── Emit ──
271
+ const varName = (id: string) => `n_${id}`
272
+ const usesStyle = { current: false }
273
+
274
+ type Resolved = { expr: string; literal?: SocketValue }
275
+
276
+ // Expression for a producer's output socket, converted to the consumer's type.
277
+ const outputExpr = (from: { node: string; socket: string }, wantT: SockT): string => {
278
+ const fromNode = nodes.get(from.node)!
279
+ const fromSpec = spec(fromNode)
280
+ const fromT = fromSpec.outputs[from.socket]
281
+ const base = fromSpec.contextOutputs
282
+ ? fromSpec.contextOutputs[from.socket]
283
+ : varName(from.node) + (fromSpec.outputSelect?.[from.socket] ?? "")
284
+ return convert(fromT, wantT, base)
285
+ }
286
+
287
+ const resolveInput = (node: GraphNode, socket: string, wantT: SockT): Resolved => {
288
+ const key = `${node.id}.${socket}`
289
+ const link = incoming.get(key)
290
+ if (link) return { expr: outputExpr(link, wantT) }
291
+ const exposed = paramBySocket.get(key)
292
+ if (exposed && !opts.inlineParams) {
293
+ usesStyle.current = true
294
+ const raw = exposed.slot.expr
295
+ return { expr: exposed.param.kind === "float" ? convert("float", wantT, raw) : convert("color", wantT, raw) }
296
+ }
297
+ const inputSpec = spec(node).inputs[socket]
298
+ const value = exposed?.param.default ?? node.inputs?.[socket] ?? inputSpec.default
299
+ if (value === undefined) {
300
+ if (inputSpec.contextDefault) return { expr: inputSpec.contextDefault }
301
+ throw new Error(`unresolved input ${key}`) // guarded by requiresLink validation
302
+ }
303
+ return { expr: fmtValue(value, wantT), literal: value }
304
+ }
305
+
306
+ // Peephole canonicalizations — fire only on plain literals (unexposed, unlinked),
307
+ // reproducing the hand-written Safari specializations.
308
+ const emitExpr = (node: GraphNode, args: Record<string, Resolved>): string => {
309
+ const raw: Record<string, string> = {}
310
+ for (const [k, v] of Object.entries(args)) raw[k] = v.expr
311
+ if (node.type === "hue_sat" && args.hue.literal === 0.5)
312
+ return `hue_sat_id(${raw.saturation}, ${raw.value}, ${raw.fac}, ${raw.color})`
313
+ if (node.type.startsWith("mix/") && node.type !== "mix/add_emit") {
314
+ if (args.fac.literal === 0) return raw.a
315
+ if (node.type === "mix/blend" && args.fac.literal === 1) return raw.b
316
+ }
317
+ if (
318
+ node.type === "tex_noise" &&
319
+ args.detail.literal === 2 &&
320
+ args.roughness.literal === 0.5 &&
321
+ args.distortion.literal === 0
322
+ )
323
+ return `tex_noise_d2(${raw.vector}, ${raw.scale})`
324
+ return spec(node).emit!(raw)
325
+ }
326
+
327
+ const lines: string[] = []
328
+ for (const id of order) {
329
+ const node = nodes.get(id)!
330
+ const s = spec(node)
331
+ if (s.contextOutputs) continue
332
+ const args: Record<string, Resolved> = {}
333
+ for (const [socket, input] of Object.entries(s.inputs)) args[socket] = resolveInput(node, socket, input.type)
334
+ lines.push(` let ${varName(id)} = ${emitExpr(node, args)}; // @node:${id}`)
335
+ }
336
+
337
+ lines.push(` let final_color = ${outputExpr(out, "color")}; // @node:${out.node}`)
338
+
339
+ const fsBody = lines.join("\n")
340
+ const wgsl = assembleModule(graph.slot, fsBody, usesStyle.current)
341
+ return { ok: true, wgsl, fsBody, slotMap, diagnostics, prunedNodes }
342
+ }
@@ -0,0 +1,103 @@
1
+ // M_Body as a StyleGraph — port of shaders/materials/body.ts (仿深空之眼 "M_Body").
2
+ // Toon + warm rim + rim1/rim2 stack mixed 50/50 against a Principled BSDF with
3
+ // noise-bumped normal. The hand port's local ramp_ease is ramp_cardinal (identical
4
+ // smoothstep form); the Mapping node's zero loc/rot folds to a scale multiply.
5
+
6
+ import type { StyleGraph } from "../schema"
7
+
8
+ export const BODY_GRAPH: StyleGraph = {
9
+ version: 1,
10
+ name: "Body",
11
+ slot: "body",
12
+ nodes: [
13
+ { id: "tex", type: "texture" },
14
+ { id: "geo", type: "geometry" },
15
+ { id: "str", type: "shader_to_rgb_diffuse" },
16
+ { id: "toon", type: "ramp_constant", inputs: { pos0: 0.0, pos1: 0.2966 } },
17
+ { id: "shadow_tint", type: "hue_sat", inputs: { hue: 0.5, saturation: 2.0, value: 0.3499999940395355, fac: 1.0 } },
18
+ { id: "lit_tint", type: "hue_sat", inputs: { hue: 0.5, saturation: 1.5, value: 1.0, fac: 1.0 } },
19
+ { id: "toon_color", type: "mix/blend" },
20
+ { id: "bc", type: "bright_contrast", inputs: { bright: 0.1, contrast: 0.2 } },
21
+ { id: "emission3", type: "emission", inputs: { strength: 4.0 } },
22
+ { id: "warm_add", type: "math/add", inputs: { b: 0.5 } },
23
+ { id: "warm_clamp", type: "math/clamp01" },
24
+ {
25
+ id: "warm_ramp",
26
+ type: "ramp_cardinal",
27
+ inputs: {
28
+ pos0: 0.2409,
29
+ color0: [0.2426, 0.068, 0.0588, 1.0],
30
+ pos1: 0.4663,
31
+ color1: [0.6677, 0.5024, 0.5126, 1.0],
32
+ },
33
+ },
34
+ { id: "warm_emit", type: "emission", inputs: { strength: 0.30000001192092896 } },
35
+ { id: "rim1_fres", type: "fresnel", inputs: { ior: 2.0 } },
36
+ { id: "rim1_lw", type: "layer_weight/facing", inputs: { blend: 0.24000005424022675 } },
37
+ { id: "rim1_str", type: "math/multiply" },
38
+ { id: "rim1", type: "emission", inputs: { color: [0.984157919883728, 0.6110184788703918, 0.5736401677131653] } },
39
+ { id: "rim2_lw", type: "layer_weight/facing", inputs: { blend: 0.20000000298023224 } },
40
+ { id: "rim2_pow", type: "math/power", inputs: { b: 1.4300000667572021 } },
41
+ { id: "rim2_ramp", type: "ramp_cardinal", inputs: { pos0: 0.0, pos1: 0.5052 } },
42
+ { id: "rim2_mix", type: "mix_shader", inputs: { b: [1.0, 0.4303792119026184, 0.3315804898738861] } },
43
+ { id: "npr_add1", type: "add_shader" },
44
+ { id: "npr_stack", type: "add_shader" },
45
+ { id: "map", type: "mapping", inputs: { scl: [1.0, 1.0, 1.5] } },
46
+ { id: "noise", type: "tex_noise", inputs: { scale: 1.0 } },
47
+ { id: "noise_ramp", type: "ramp_linear", inputs: { pos0: 0.0, pos1: 1.0 } },
48
+ { id: "bump", type: "bump", inputs: { strength: 0.324644535779953 } },
49
+ {
50
+ id: "principled_base",
51
+ type: "mix/blend",
52
+ inputs: { b: [0.6831911206245422, 0.19474034011363983, 0.13732507824897766] },
53
+ },
54
+ { id: "p_emit", type: "emission", inputs: { strength: 0.2 } },
55
+ {
56
+ id: "principled",
57
+ type: "principled",
58
+ inputs: { metallic: 0.0, specular: 0.5, roughness: 0.3, spec_clamp: 10.0, sheen: 0.0, sheen_tint: 0.0 },
59
+ },
60
+ { id: "p_sum", type: "add_shader" },
61
+ { id: "mix_shader_001", type: "mix_shader", inputs: { fac: 0.5 } },
62
+ ],
63
+ links: [
64
+ { from: { node: "str", socket: "value" }, to: { node: "toon", socket: "fac" } },
65
+ { from: { node: "tex", socket: "color" }, to: { node: "shadow_tint", socket: "color" } },
66
+ { from: { node: "tex", socket: "color" }, to: { node: "lit_tint", socket: "color" } },
67
+ { from: { node: "toon", socket: "fac_out" }, to: { node: "toon_color", socket: "fac" } },
68
+ { from: { node: "shadow_tint", socket: "color" }, to: { node: "toon_color", socket: "a" } },
69
+ { from: { node: "lit_tint", socket: "color" }, to: { node: "toon_color", socket: "b" } },
70
+ { from: { node: "toon_color", socket: "color" }, to: { node: "bc", socket: "color" } },
71
+ { from: { node: "bc", socket: "color" }, to: { node: "emission3", socket: "color" } },
72
+ { from: { node: "toon", socket: "fac_out" }, to: { node: "warm_add", socket: "a" } },
73
+ { from: { node: "warm_add", socket: "value" }, to: { node: "warm_clamp", socket: "a" } },
74
+ { from: { node: "warm_clamp", socket: "value" }, to: { node: "warm_ramp", socket: "fac" } },
75
+ { from: { node: "warm_ramp", socket: "color" }, to: { node: "warm_emit", socket: "color" } },
76
+ { from: { node: "rim1_fres", socket: "value" }, to: { node: "rim1_str", socket: "a" } },
77
+ { from: { node: "rim1_lw", socket: "value" }, to: { node: "rim1_str", socket: "b" } },
78
+ { from: { node: "rim1_str", socket: "value" }, to: { node: "rim1", socket: "strength" } },
79
+ { from: { node: "rim2_lw", socket: "value" }, to: { node: "rim2_pow", socket: "a" } },
80
+ { from: { node: "rim2_pow", socket: "value" }, to: { node: "rim2_ramp", socket: "fac" } },
81
+ { from: { node: "emission3", socket: "color" }, to: { node: "rim2_mix", socket: "a" } },
82
+ { from: { node: "rim2_ramp", socket: "fac_out" }, to: { node: "rim2_mix", socket: "fac" } },
83
+ { from: { node: "rim1", socket: "color" }, to: { node: "npr_add1", socket: "a" } },
84
+ { from: { node: "rim2_mix", socket: "color" }, to: { node: "npr_add1", socket: "b" } },
85
+ { from: { node: "npr_add1", socket: "color" }, to: { node: "npr_stack", socket: "a" } },
86
+ { from: { node: "warm_emit", socket: "color" }, to: { node: "npr_stack", socket: "b" } },
87
+ { from: { node: "geo", socket: "rest_pos" }, to: { node: "map", socket: "vector" } },
88
+ { from: { node: "map", socket: "vector" }, to: { node: "noise", socket: "vector" } },
89
+ { from: { node: "noise", socket: "value" }, to: { node: "noise_ramp", socket: "fac" } },
90
+ { from: { node: "noise_ramp", socket: "fac_out" }, to: { node: "bump", socket: "height" } },
91
+ { from: { node: "geo", socket: "normal" }, to: { node: "bump", socket: "normal" } },
92
+ { from: { node: "noise_ramp", socket: "fac_out" }, to: { node: "principled_base", socket: "fac" } },
93
+ { from: { node: "bc", socket: "color" }, to: { node: "principled_base", socket: "a" } },
94
+ { from: { node: "bc", socket: "color" }, to: { node: "p_emit", socket: "color" } },
95
+ { from: { node: "principled_base", socket: "color" }, to: { node: "principled", socket: "base" } },
96
+ { from: { node: "bump", socket: "vector" }, to: { node: "principled", socket: "normal" } },
97
+ { from: { node: "principled", socket: "color" }, to: { node: "p_sum", socket: "a" } },
98
+ { from: { node: "p_emit", socket: "color" }, to: { node: "p_sum", socket: "b" } },
99
+ { from: { node: "npr_stack", socket: "color" }, to: { node: "mix_shader_001", socket: "a" } },
100
+ { from: { node: "p_sum", socket: "color" }, to: { node: "mix_shader_001", socket: "b" } },
101
+ ],
102
+ output: { node: "mix_shader_001", socket: "color" },
103
+ }
@@ -0,0 +1,64 @@
1
+ // M_Rough_Cloth as a StyleGraph — port of shaders/materials/cloth_rough.ts.
2
+ // NPR graph identical to M_Smooth_Cloth, but the noise bump subtree IS live on
3
+ // Principled.Normal (weave bump in rest space) and Roughness is raised to 0.8187.
4
+ // The tex_noise node hits the detail=2 peephole → tex_noise_d2.
5
+
6
+ import type { StyleGraph } from "../schema"
7
+
8
+ export const CLOTH_ROUGH_GRAPH: StyleGraph = {
9
+ version: 1,
10
+ name: "Rough Cloth",
11
+ slot: "cloth_rough",
12
+ nodes: [
13
+ { id: "tex", type: "texture" },
14
+ { id: "geo", type: "geometry" },
15
+ { id: "str", type: "shader_to_rgb_diffuse" },
16
+ { id: "ramp_008", type: "ramp_constant_aa", inputs: { edge: 0.2966 } },
17
+ { id: "mix04_fac", type: "math/multiply", inputs: { b: 0.5 } },
18
+ { id: "dark_tex", type: "hue_sat", inputs: { hue: 0.5, saturation: 1.0, value: 0.19999998807907104, fac: 1.0 } },
19
+ { id: "mix_004", type: "mix/blend" },
20
+ { id: "sep_n", type: "separate_xyz" },
21
+ { id: "bevel_clamp", type: "math/clamp01" },
22
+ { id: "mix_003", type: "mix/blend" },
23
+ { id: "hue_004", type: "hue_sat", inputs: { hue: 0.5, saturation: 0.800000011920929, value: 2.0, fac: 1.0 } },
24
+ { id: "npr_overlay", type: "mix/overlay", inputs: { fac: 1.0 } },
25
+ { id: "npr_emit", type: "emission", inputs: { strength: 18.200000762939453 } },
26
+ { id: "noise", type: "tex_noise", inputs: { scale: 17.7 } },
27
+ { id: "noise_ramp", type: "ramp_linear", inputs: { pos0: 0.0, pos1: 1.0 } },
28
+ { id: "bump", type: "bump", inputs: { strength: 1.0 } },
29
+ { id: "principled_base", type: "hue_sat", inputs: { hue: 0.5, saturation: 1.0, value: 0.800000011920929, fac: 1.0 } },
30
+ {
31
+ id: "principled",
32
+ type: "principled",
33
+ inputs: { metallic: 0.0, specular: 0.8, roughness: 0.8187, spec_clamp: 10.0, sheen: 0.0, sheen_tint: 0.0 },
34
+ },
35
+ { id: "mix_shader_001", type: "mix_shader", inputs: { fac: 0.8999999761581421 } },
36
+ ],
37
+ links: [
38
+ { from: { node: "str", socket: "value" }, to: { node: "ramp_008", socket: "fac" } },
39
+ { from: { node: "ramp_008", socket: "fac_out" }, to: { node: "mix04_fac", socket: "a" } },
40
+ { from: { node: "tex", socket: "color" }, to: { node: "dark_tex", socket: "color" } },
41
+ { from: { node: "mix04_fac", socket: "value" }, to: { node: "mix_004", socket: "fac" } },
42
+ { from: { node: "dark_tex", socket: "color" }, to: { node: "mix_004", socket: "a" } },
43
+ { from: { node: "tex", socket: "color" }, to: { node: "mix_004", socket: "b" } },
44
+ { from: { node: "geo", socket: "normal" }, to: { node: "sep_n", socket: "vector" } },
45
+ { from: { node: "sep_n", socket: "y" }, to: { node: "bevel_clamp", socket: "a" } },
46
+ { from: { node: "bevel_clamp", socket: "value" }, to: { node: "mix_003", socket: "fac" } },
47
+ { from: { node: "mix_004", socket: "color" }, to: { node: "mix_003", socket: "a" } },
48
+ { from: { node: "dark_tex", socket: "color" }, to: { node: "mix_003", socket: "b" } },
49
+ { from: { node: "mix_003", socket: "color" }, to: { node: "hue_004", socket: "color" } },
50
+ { from: { node: "mix_003", socket: "color" }, to: { node: "npr_overlay", socket: "a" } },
51
+ { from: { node: "hue_004", socket: "color" }, to: { node: "npr_overlay", socket: "b" } },
52
+ { from: { node: "npr_overlay", socket: "color" }, to: { node: "npr_emit", socket: "color" } },
53
+ { from: { node: "geo", socket: "rest_pos" }, to: { node: "noise", socket: "vector" } },
54
+ { from: { node: "noise", socket: "value" }, to: { node: "noise_ramp", socket: "fac" } },
55
+ { from: { node: "noise_ramp", socket: "fac_out" }, to: { node: "bump", socket: "height" } },
56
+ { from: { node: "geo", socket: "normal" }, to: { node: "bump", socket: "normal" } },
57
+ { from: { node: "tex", socket: "color" }, to: { node: "principled_base", socket: "color" } },
58
+ { from: { node: "principled_base", socket: "color" }, to: { node: "principled", socket: "base" } },
59
+ { from: { node: "bump", socket: "vector" }, to: { node: "principled", socket: "normal" } },
60
+ { from: { node: "npr_emit", socket: "color" }, to: { node: "mix_shader_001", socket: "a" } },
61
+ { from: { node: "principled", socket: "color" }, to: { node: "mix_shader_001", socket: "b" } },
62
+ ],
63
+ output: { node: "mix_shader_001", socket: "color" },
64
+ }
@@ -0,0 +1,56 @@
1
+ // M_Smooth_Cloth as a StyleGraph — port of shaders/materials/cloth_smooth.ts.
2
+ // NPR toon + bevel + overlay-boosted emission (18.2×) mixed 10/90 against a plain
3
+ // Principled BSDF. The Blender graph's dead bump subtree is omitted (as in the hand
4
+ // port). hue_sat nodes with hue=0.5 compile to the hue_sat_id specialization.
5
+
6
+ import type { StyleGraph } from "../schema"
7
+
8
+ export const CLOTH_SMOOTH_GRAPH: StyleGraph = {
9
+ version: 1,
10
+ name: "Smooth Cloth",
11
+ slot: "cloth_smooth",
12
+ nodes: [
13
+ { id: "tex", type: "texture" },
14
+ { id: "geo", type: "geometry" },
15
+ { id: "str", type: "shader_to_rgb_diffuse" },
16
+ { id: "ramp_008", type: "ramp_constant_aa", inputs: { edge: 0.2966 } },
17
+ { id: "mix04_fac", type: "math/multiply", inputs: { b: 0.5 } },
18
+ { id: "dark_tex", type: "hue_sat", inputs: { hue: 0.5, saturation: 1.0, value: 0.19999998807907104, fac: 1.0 } },
19
+ { id: "mix_004", type: "mix/blend" },
20
+ { id: "sep_n", type: "separate_xyz" },
21
+ { id: "bevel_clamp", type: "math/clamp01" },
22
+ { id: "mix_003", type: "mix/blend" },
23
+ { id: "hue_004", type: "hue_sat", inputs: { hue: 0.5, saturation: 0.800000011920929, value: 2.0, fac: 1.0 } },
24
+ { id: "npr_overlay", type: "mix/overlay", inputs: { fac: 1.0 } },
25
+ { id: "npr_emit", type: "emission", inputs: { strength: 18.200000762939453 } },
26
+ { id: "principled_base", type: "hue_sat", inputs: { hue: 0.5, saturation: 1.0, value: 0.800000011920929, fac: 1.0 } },
27
+ {
28
+ id: "principled",
29
+ type: "principled",
30
+ inputs: { metallic: 0.0, specular: 0.8, roughness: 0.5, spec_clamp: 10.0, sheen: 0.0, sheen_tint: 0.0 },
31
+ },
32
+ { id: "mix_shader_001", type: "mix_shader", inputs: { fac: 0.8999999761581421 } },
33
+ ],
34
+ links: [
35
+ { from: { node: "str", socket: "value" }, to: { node: "ramp_008", socket: "fac" } },
36
+ { from: { node: "ramp_008", socket: "fac_out" }, to: { node: "mix04_fac", socket: "a" } },
37
+ { from: { node: "tex", socket: "color" }, to: { node: "dark_tex", socket: "color" } },
38
+ { from: { node: "mix04_fac", socket: "value" }, to: { node: "mix_004", socket: "fac" } },
39
+ { from: { node: "dark_tex", socket: "color" }, to: { node: "mix_004", socket: "a" } },
40
+ { from: { node: "tex", socket: "color" }, to: { node: "mix_004", socket: "b" } },
41
+ { from: { node: "geo", socket: "normal" }, to: { node: "sep_n", socket: "vector" } },
42
+ { from: { node: "sep_n", socket: "y" }, to: { node: "bevel_clamp", socket: "a" } },
43
+ { from: { node: "bevel_clamp", socket: "value" }, to: { node: "mix_003", socket: "fac" } },
44
+ { from: { node: "mix_004", socket: "color" }, to: { node: "mix_003", socket: "a" } },
45
+ { from: { node: "dark_tex", socket: "color" }, to: { node: "mix_003", socket: "b" } },
46
+ { from: { node: "mix_003", socket: "color" }, to: { node: "hue_004", socket: "color" } },
47
+ { from: { node: "mix_003", socket: "color" }, to: { node: "npr_overlay", socket: "a" } },
48
+ { from: { node: "hue_004", socket: "color" }, to: { node: "npr_overlay", socket: "b" } },
49
+ { from: { node: "npr_overlay", socket: "color" }, to: { node: "npr_emit", socket: "color" } },
50
+ { from: { node: "tex", socket: "color" }, to: { node: "principled_base", socket: "color" } },
51
+ { from: { node: "principled_base", socket: "color" }, to: { node: "principled", socket: "base" } },
52
+ { from: { node: "npr_emit", socket: "color" }, to: { node: "mix_shader_001", socket: "a" } },
53
+ { from: { node: "principled", socket: "color" }, to: { node: "mix_shader_001", socket: "b" } },
54
+ ],
55
+ output: { node: "mix_shader_001", socket: "color" },
56
+ }
@@ -0,0 +1,23 @@
1
+ // Default material as a StyleGraph — Blender's new-material template verbatim:
2
+ // Image Texture → Principled BSDF (Metallic 0, Specular 0.5, Roughness 0.5) → Output.
3
+ // This is both the port of shaders/materials/default.ts (must match it pixel-for-pixel;
4
+ // see tests/graph.test.mjs) and the blank-canvas starter graph an editor offers when
5
+ // the user creates a new style.
6
+
7
+ import type { StyleGraph } from "../schema"
8
+
9
+ export const DEFAULT_GRAPH: StyleGraph = {
10
+ version: 1,
11
+ name: "Principled BSDF",
12
+ slot: "default",
13
+ nodes: [
14
+ { id: "tex", type: "texture" },
15
+ {
16
+ id: "principled",
17
+ type: "principled",
18
+ inputs: { metallic: 0.0, specular: 0.5, roughness: 0.5, spec_clamp: 10.0, sheen: 0.0, sheen_tint: 0.0 },
19
+ },
20
+ ],
21
+ links: [{ from: { node: "tex", socket: "color" }, to: { node: "principled", socket: "base" } }],
22
+ output: { node: "principled", socket: "color" },
23
+ }