reze-engine 0.18.0 → 0.19.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 (50) hide show
  1. package/dist/engine.d.ts +45 -28
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +339 -144
  4. package/dist/graph/compile.d.ts +6 -0
  5. package/dist/graph/compile.d.ts.map +1 -1
  6. package/dist/graph/compile.js +1 -1
  7. package/dist/graph/presets/body.js +1 -1
  8. package/dist/graph/presets/cloth_rough.js +1 -1
  9. package/dist/graph/presets/cloth_smooth.js +1 -1
  10. package/dist/graph/presets/default.js +1 -1
  11. package/dist/graph/presets/eye.d.ts +3 -0
  12. package/dist/graph/presets/eye.d.ts.map +1 -0
  13. package/dist/graph/presets/eye.js +30 -0
  14. package/dist/graph/presets/face.d.ts +3 -0
  15. package/dist/graph/presets/face.d.ts.map +1 -0
  16. package/dist/graph/presets/face.js +125 -0
  17. package/dist/graph/presets/hair.js +1 -1
  18. package/dist/graph/presets/metal.js +1 -1
  19. package/dist/graph/presets/stockings.js +1 -1
  20. package/dist/graph/render-class.d.ts +16 -0
  21. package/dist/graph/render-class.d.ts.map +1 -0
  22. package/dist/graph/render-class.js +15 -0
  23. package/dist/graph/schema.d.ts +4 -5
  24. package/dist/graph/schema.d.ts.map +1 -1
  25. package/dist/graph/slots.d.ts +7 -12
  26. package/dist/graph/slots.d.ts.map +1 -1
  27. package/dist/graph/slots.js +73 -73
  28. package/dist/graph/style-group.d.ts +38 -0
  29. package/dist/graph/style-group.d.ts.map +1 -0
  30. package/dist/graph/style-group.js +6 -0
  31. package/dist/index.d.ts +5 -1
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +3 -0
  34. package/package.json +1 -1
  35. package/src/engine.ts +395 -156
  36. package/src/graph/compile.ts +7 -1
  37. package/src/graph/presets/body.ts +1 -1
  38. package/src/graph/presets/cloth_rough.ts +1 -1
  39. package/src/graph/presets/cloth_smooth.ts +1 -1
  40. package/src/graph/presets/default.ts +1 -1
  41. package/src/graph/presets/eye.ts +33 -0
  42. package/src/graph/presets/face.ts +136 -0
  43. package/src/graph/presets/hair.ts +1 -1
  44. package/src/graph/presets/metal.ts +1 -1
  45. package/src/graph/presets/stockings.ts +1 -1
  46. package/src/graph/render-class.ts +33 -0
  47. package/src/graph/schema.ts +4 -6
  48. package/src/graph/slots.ts +81 -85
  49. package/src/graph/style-group.ts +39 -0
  50. package/src/index.ts +9 -1
@@ -8,6 +8,7 @@ import type { Diagnostic, ExposedParam, GraphNode, SocketValue, StyleGraph } fro
8
8
  import { NODE_REGISTRY, canConvert, convert, fmtValue, literalFits } from "./registry"
9
9
  import type { NodeSpec, SockT } from "./registry"
10
10
  import { assembleModule } from "./slots"
11
+ import type { AlphaMode, RenderClass } from "./render-class"
11
12
 
12
13
  export type CompileOptions = {
13
14
  /** Fold exposed params to their defaults as consts (no StyleUniforms binding).
@@ -16,6 +17,11 @@ export type CompileOptions = {
16
17
  /** Override the graph output with any node's socket — Blender's node-preview
17
18
  * (Ctrl+Shift+Click viewer) workflow for the editor. */
18
19
  previewNode?: { node: string; socket: string }
20
+ /** Pass-integration class the fs() shell is assembled for (stencil/cull/gate). The
21
+ * graph is pure shading; the group supplies this. Default "auto". */
22
+ renderClass?: RenderClass
23
+ /** Alpha-handling axis, orthogonal to renderClass. Default "opaque". */
24
+ alphaMode?: AlphaMode
19
25
  }
20
26
 
21
27
  /** UBO slot for one exposed param: write `value` at style.p[vec4Index] (+ component). */
@@ -337,6 +343,6 @@ export function compileGraph(graph: StyleGraph, opts: CompileOptions = {}): Comp
337
343
  lines.push(` let final_color = ${outputExpr(out, "color")}; // @node:${out.node}`)
338
344
 
339
345
  const fsBody = lines.join("\n")
340
- const wgsl = assembleModule(graph.slot, fsBody, usesStyle.current)
346
+ const wgsl = assembleModule(opts.renderClass ?? "auto", opts.alphaMode ?? "opaque", fsBody, usesStyle.current)
341
347
  return { ok: true, wgsl, fsBody, slotMap, diagnostics, prunedNodes }
342
348
  }
@@ -8,7 +8,7 @@ import type { StyleGraph } from "../schema"
8
8
  export const BODY_GRAPH: StyleGraph = {
9
9
  version: 1,
10
10
  name: "Body",
11
- slot: "body",
11
+ tags: ["body"],
12
12
  nodes: [
13
13
  { id: "tex", type: "texture" },
14
14
  { id: "geo", type: "geometry" },
@@ -8,7 +8,7 @@ import type { StyleGraph } from "../schema"
8
8
  export const CLOTH_ROUGH_GRAPH: StyleGraph = {
9
9
  version: 1,
10
10
  name: "Rough Cloth",
11
- slot: "cloth_rough",
11
+ tags: ["cloth_rough"],
12
12
  nodes: [
13
13
  { id: "tex", type: "texture" },
14
14
  { id: "geo", type: "geometry" },
@@ -8,7 +8,7 @@ import type { StyleGraph } from "../schema"
8
8
  export const CLOTH_SMOOTH_GRAPH: StyleGraph = {
9
9
  version: 1,
10
10
  name: "Smooth Cloth",
11
- slot: "cloth_smooth",
11
+ tags: ["cloth_smooth"],
12
12
  nodes: [
13
13
  { id: "tex", type: "texture" },
14
14
  { id: "geo", type: "geometry" },
@@ -9,7 +9,7 @@ import type { StyleGraph } from "../schema"
9
9
  export const DEFAULT_GRAPH: StyleGraph = {
10
10
  version: 1,
11
11
  name: "Principled BSDF",
12
- slot: "default",
12
+ tags: ["default"],
13
13
  nodes: [
14
14
  { id: "tex", type: "texture" },
15
15
  {
@@ -0,0 +1,33 @@
1
+ // Eye as a StyleGraph — port of shaders/materials/eye.ts. The published preset
2
+ // author's instruction: "keep eyes in the default nodegraph, add emission 1.5".
3
+ // So it's the default Principled BSDF plus an Emission of the diffuse texture at
4
+ // 1.5× (Blender's Principled Emission socket, decomposed as a separate Emission +
5
+ // Add Shader — the emission feeds bloom pre-tonemap).
6
+ //
7
+ // The rear-view gate and the see-through stencil stamp are slot-owned (built-in eye
8
+ // behavior, see EYE_TEMPLATE in slots.ts + createSlotPipeline) — not in this graph.
9
+
10
+ import type { StyleGraph } from "../schema"
11
+
12
+ export const EYE_GRAPH: StyleGraph = {
13
+ version: 1,
14
+ name: "Eye",
15
+ tags: ["eye"],
16
+ nodes: [
17
+ { id: "tex", type: "texture" },
18
+ {
19
+ id: "principled",
20
+ type: "principled",
21
+ inputs: { metallic: 0.0, specular: 0.5, roughness: 0.5, spec_clamp: 1e30, sheen: 0.0, sheen_tint: 0.0 },
22
+ },
23
+ { id: "emission", type: "emission", inputs: { strength: 1.5 } },
24
+ { id: "add", type: "add_shader" },
25
+ ],
26
+ links: [
27
+ { from: { node: "tex", socket: "color" }, to: { node: "principled", socket: "base" } },
28
+ { from: { node: "tex", socket: "color" }, to: { node: "emission", socket: "color" } },
29
+ { from: { node: "principled", socket: "color" }, to: { node: "add", socket: "a" } },
30
+ { from: { node: "emission", socket: "color" }, to: { node: "add", socket: "b" } },
31
+ ],
32
+ output: { node: "add", socket: "color" },
33
+ }
@@ -0,0 +1,136 @@
1
+ // M_Face as a StyleGraph — port of shaders/materials/face.ts (仿深空之眼 "M_Face").
2
+ // Toon + warm rim + dual fresnel rim + BT.601 bright-tex gate, mixed 50/50 against a
3
+ // Principled BSDF with noise bump. Structurally close to body, but: the toon uses the
4
+ // edge-AA ramp, hue tints are 0.46 (full hue_sat, not the 0.5 _id specialization),
5
+ // rim2 is the hair-style fresnel×layer_weight_fresnel rim, and there's an extra
6
+ // near-white texture gate that emits (freckle/highlight paint). Plain material — no
7
+ // built-in slot effect, so it uses the default prelude/epilogue.
8
+
9
+ import type { StyleGraph } from "../schema"
10
+
11
+ export const FACE_GRAPH: StyleGraph = {
12
+ version: 1,
13
+ name: "Face",
14
+ tags: ["face"],
15
+ nodes: [
16
+ { id: "tex", type: "texture" },
17
+ { id: "geo", type: "geometry" },
18
+
19
+ // ── toon shading ──
20
+ { id: "str", type: "shader_to_rgb_diffuse" },
21
+ { id: "toon", type: "ramp_constant_aa", inputs: { edge: 0.2966 } },
22
+ { id: "shadow_tint", type: "hue_sat", inputs: { hue: 0.46000000834465027, saturation: 2.0, value: 0.3499999940395355, fac: 1.0 } },
23
+ { id: "lit_tint", type: "hue_sat", inputs: { hue: 0.46000000834465027, saturation: 1.600000023841858, value: 1.5, fac: 1.0 } },
24
+ { id: "toon_color", type: "mix/blend" },
25
+ { id: "bc", type: "bright_contrast", inputs: { bright: 0.1, contrast: 0.2 } },
26
+ { id: "emission3", type: "emission", inputs: { strength: 2.5 } },
27
+
28
+ // ── warm rim (toon*0.5+0.5 → cardinal ramp) ──
29
+ { id: "warm_mul", type: "math/multiply", inputs: { b: 0.5 } },
30
+ { id: "warm_add", type: "math/add", inputs: { b: 0.5 } },
31
+ { id: "warm_clamp", type: "math/clamp01" },
32
+ {
33
+ id: "warm_ramp",
34
+ type: "ramp_cardinal",
35
+ inputs: {
36
+ pos0: 0.2409,
37
+ color0: [0.2426, 0.068, 0.0588, 1.0],
38
+ pos1: 0.4663,
39
+ color1: [0.6677, 0.5024, 0.5126, 1.0],
40
+ },
41
+ },
42
+ { id: "warm_emit", type: "emission", inputs: { strength: 0.30000001192092896 } },
43
+
44
+ // ── rim1: fresnel × facing ──
45
+ { id: "rim1_fres", type: "fresnel", inputs: { ior: 2.0 } },
46
+ { id: "rim1_lw", type: "layer_weight/facing", inputs: { blend: 0.24 } },
47
+ { id: "rim1_str", type: "math/multiply" },
48
+ { id: "rim1", type: "emission", inputs: { color: [0.984157919883728, 0.6110184788703918, 0.5736401677131653] } },
49
+
50
+ // ── rim2: fresnel × fresnel-layer-weight, powered (hair-style) ──
51
+ { id: "rim2_fres", type: "fresnel", inputs: { ior: 1.45 } },
52
+ { id: "rim2_lw", type: "layer_weight/fresnel", inputs: { blend: 0.61 } },
53
+ { id: "rim2_raw", type: "math/multiply" },
54
+ { id: "rim2_pow", type: "math/power", inputs: { b: 0.6300000548362732 } },
55
+ { id: "rim2_mix", type: "mix_shader", inputs: { b: [1.0, 0.4684903025627136, 0.3698573112487793] } },
56
+
57
+ // ── near-white texture gate emission ──
58
+ { id: "gate", type: "math/greater_than", inputs: { b: 0.9300000071525574 } },
59
+ { id: "gate_scale", type: "math/multiply", inputs: { b: 3.0 } },
60
+
61
+ // ── npr stack (rim1 + rim2 + bright_emit + warm) ──
62
+ { id: "npr_add1", type: "add_shader" },
63
+ { id: "npr_add2", type: "add_shader" },
64
+ { id: "npr_stack", type: "add_shader" },
65
+
66
+ // ── principled with noise bump ──
67
+ { id: "map", type: "mapping", inputs: { scl: [1.0, 1.0, 1.5] } },
68
+ { id: "noise", type: "tex_noise", inputs: { scale: 1.0 } },
69
+ { id: "noise_ramp", type: "ramp_linear", inputs: { pos0: 0.0, pos1: 1.0 } },
70
+ { id: "bump", type: "bump", inputs: { strength: 0.324644535779953 } },
71
+ { id: "principled_base", type: "mix/blend", inputs: { b: [0.6832, 0.1947, 0.1373] } },
72
+ { id: "p_emit", type: "emission", inputs: { strength: 0.2 } },
73
+ {
74
+ id: "principled",
75
+ type: "principled",
76
+ inputs: { metallic: 0.0, specular: 0.5, roughness: 0.3, spec_clamp: 10.0, sheen: 0.0, sheen_tint: 0.0 },
77
+ },
78
+ { id: "p_sum", type: "add_shader" },
79
+
80
+ { id: "mix_shader_001", type: "mix_shader", inputs: { fac: 0.5 } },
81
+ ],
82
+ links: [
83
+ // toon
84
+ { from: { node: "str", socket: "value" }, to: { node: "toon", socket: "fac" } },
85
+ { from: { node: "tex", socket: "color" }, to: { node: "shadow_tint", socket: "color" } },
86
+ { from: { node: "tex", socket: "color" }, to: { node: "lit_tint", socket: "color" } },
87
+ { from: { node: "toon", socket: "fac_out" }, to: { node: "toon_color", socket: "fac" } },
88
+ { from: { node: "shadow_tint", socket: "color" }, to: { node: "toon_color", socket: "a" } },
89
+ { from: { node: "lit_tint", socket: "color" }, to: { node: "toon_color", socket: "b" } },
90
+ { from: { node: "toon_color", socket: "color" }, to: { node: "bc", socket: "color" } },
91
+ { from: { node: "bc", socket: "color" }, to: { node: "emission3", socket: "color" } },
92
+ // warm rim
93
+ { from: { node: "toon", socket: "fac_out" }, to: { node: "warm_mul", socket: "a" } },
94
+ { from: { node: "warm_mul", socket: "value" }, to: { node: "warm_add", socket: "a" } },
95
+ { from: { node: "warm_add", socket: "value" }, to: { node: "warm_clamp", socket: "a" } },
96
+ { from: { node: "warm_clamp", socket: "value" }, to: { node: "warm_ramp", socket: "fac" } },
97
+ { from: { node: "warm_ramp", socket: "color" }, to: { node: "warm_emit", socket: "color" } },
98
+ // rim1
99
+ { from: { node: "rim1_fres", socket: "value" }, to: { node: "rim1_str", socket: "a" } },
100
+ { from: { node: "rim1_lw", socket: "value" }, to: { node: "rim1_str", socket: "b" } },
101
+ { from: { node: "rim1_str", socket: "value" }, to: { node: "rim1", socket: "strength" } },
102
+ // rim2
103
+ { from: { node: "rim2_fres", socket: "value" }, to: { node: "rim2_raw", socket: "a" } },
104
+ { from: { node: "rim2_lw", socket: "value" }, to: { node: "rim2_raw", socket: "b" } },
105
+ { from: { node: "rim2_raw", socket: "value" }, to: { node: "rim2_pow", socket: "a" } },
106
+ { from: { node: "emission3", socket: "color" }, to: { node: "rim2_mix", socket: "a" } },
107
+ { from: { node: "rim2_pow", socket: "value" }, to: { node: "rim2_mix", socket: "fac" } },
108
+ // gate
109
+ { from: { node: "tex", socket: "color" }, to: { node: "gate", socket: "a" } },
110
+ { from: { node: "gate", socket: "value" }, to: { node: "gate_scale", socket: "a" } },
111
+ // npr stack
112
+ { from: { node: "rim1", socket: "color" }, to: { node: "npr_add1", socket: "a" } },
113
+ { from: { node: "rim2_mix", socket: "color" }, to: { node: "npr_add1", socket: "b" } },
114
+ { from: { node: "npr_add1", socket: "color" }, to: { node: "npr_add2", socket: "a" } },
115
+ { from: { node: "gate_scale", socket: "value" }, to: { node: "npr_add2", socket: "b" } },
116
+ { from: { node: "npr_add2", socket: "color" }, to: { node: "npr_stack", socket: "a" } },
117
+ { from: { node: "warm_emit", socket: "color" }, to: { node: "npr_stack", socket: "b" } },
118
+ // principled + noise bump
119
+ { from: { node: "geo", socket: "rest_pos" }, to: { node: "map", socket: "vector" } },
120
+ { from: { node: "map", socket: "vector" }, to: { node: "noise", socket: "vector" } },
121
+ { from: { node: "noise", socket: "value" }, to: { node: "noise_ramp", socket: "fac" } },
122
+ { from: { node: "noise_ramp", socket: "fac_out" }, to: { node: "bump", socket: "height" } },
123
+ { from: { node: "geo", socket: "normal" }, to: { node: "bump", socket: "normal" } },
124
+ { from: { node: "noise_ramp", socket: "fac_out" }, to: { node: "principled_base", socket: "fac" } },
125
+ { from: { node: "bc", socket: "color" }, to: { node: "principled_base", socket: "a" } },
126
+ { from: { node: "bc", socket: "color" }, to: { node: "p_emit", socket: "color" } },
127
+ { from: { node: "principled_base", socket: "color" }, to: { node: "principled", socket: "base" } },
128
+ { from: { node: "bump", socket: "vector" }, to: { node: "principled", socket: "normal" } },
129
+ { from: { node: "principled", socket: "color" }, to: { node: "p_sum", socket: "a" } },
130
+ { from: { node: "p_emit", socket: "color" }, to: { node: "p_sum", socket: "b" } },
131
+ // final mix
132
+ { from: { node: "npr_stack", socket: "color" }, to: { node: "mix_shader_001", socket: "a" } },
133
+ { from: { node: "p_sum", socket: "color" }, to: { node: "mix_shader_001", socket: "b" } },
134
+ ],
135
+ output: { node: "mix_shader_001", socket: "color" },
136
+ }
@@ -15,7 +15,7 @@ import type { StyleGraph } from "../schema"
15
15
  export const HAIR_GRAPH: StyleGraph = {
16
16
  version: 1,
17
17
  name: "Hair",
18
- slot: "hair",
18
+ tags: ["hair"],
19
19
  nodes: [
20
20
  { id: "tex", type: "texture" },
21
21
  { id: "geo", type: "geometry" },
@@ -9,7 +9,7 @@ import type { StyleGraph } from "../schema"
9
9
  export const METAL_GRAPH: StyleGraph = {
10
10
  version: 1,
11
11
  name: "Metal",
12
- slot: "metal",
12
+ tags: ["metal"],
13
13
  nodes: [
14
14
  { id: "tex", type: "texture" },
15
15
  { id: "geo", type: "geometry" },
@@ -11,7 +11,7 @@ import type { StyleGraph } from "../schema"
11
11
  export const STOCKINGS_GRAPH: StyleGraph = {
12
12
  version: 1,
13
13
  name: "Stockings",
14
- slot: "stockings",
14
+ tags: ["stockings"],
15
15
  nodes: [
16
16
  { id: "tex", type: "texture" },
17
17
  { id: "geo", type: "geometry" },
@@ -0,0 +1,33 @@
1
+ // Render-class + alpha-mode: the small, closed, engine-owned vocabularies that carry
2
+ // pass integration for a style group. A group's node graph is pure shading; these select
3
+ // how the engine wires that shading into the passes. Not user-extensible — this is where
4
+ // rendering correctness lives (stencil, cull, draw order, alpha test). See
5
+ // docs/style-groups-spec.md §5.
6
+
7
+ /** Pass-integration class. Selects stencil/cull/draw-order behavior. */
8
+ export type RenderClass = "auto" | "eye" | "hair"
9
+
10
+ /** Alpha-handling axis, orthogonal to RenderClass. "hashed" = Wyman & McGuire object-space
11
+ * hashed alpha test (stockings); "opaque" = the standard near-zero threshold discard. */
12
+ export type AlphaMode = "opaque" | "hashed"
13
+
14
+ /** Descriptive manifest for hosts (reze-design) so the render-class picker is data-driven
15
+ * instead of hardcoding strings. The effect implementations stay engine-side; this only
16
+ * describes them. `pairsWith` = the effect needs a counterpart class present to show. */
17
+ export type RenderClassInfo = {
18
+ id: RenderClass
19
+ label: string
20
+ description: string
21
+ pairsWith?: RenderClass
22
+ }
23
+
24
+ export const RENDER_CLASSES: readonly RenderClassInfo[] = [
25
+ { id: "auto", label: "Standard", description: "Opaque or transparent, derived from material alpha." },
26
+ { id: "eye", label: "Eye", description: "Stamps the see-through stencil; visible through hair.", pairsWith: "hair" },
27
+ {
28
+ id: "hair",
29
+ label: "Hair",
30
+ description: "Reads the eye stencil so eyes show through the silhouette.",
31
+ pairsWith: "eye",
32
+ },
33
+ ]
@@ -3,8 +3,6 @@
3
3
  // time (Blender Normal Z → engine Y), the compiler never sees Blender conventions.
4
4
  // See docs/graph-compiler-spec.md.
5
5
 
6
- import type { MaterialPreset } from "../engine"
7
-
8
6
  export type SocketValue = number | [number, number, number] | [number, number, number, number]
9
7
 
10
8
  export type GraphNode = {
@@ -39,15 +37,15 @@ export type ExposedParam = {
39
37
  export type StyleGraph = {
40
38
  version: 1
41
39
  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
40
  nodes: GraphNode[]
47
41
  links: GraphLink[]
48
42
  /** Must resolve to a color (vec3f) or float (auto-splatted) socket. */
49
43
  output: { node: string; socket: string }
50
44
  params?: ExposedParam[]
45
+ /** Soft, host-only hints for library filtering and smart default-group / render-class
46
+ * matching (e.g. ["hair"]). Ignored by the compiler; round-tripped. A graph is pure
47
+ * shading — pass integration lives on the style group's renderClass, not here. */
48
+ tags?: string[]
51
49
  }
52
50
 
53
51
  export type Diagnostic = {
@@ -1,52 +1,24 @@
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.
1
+ // fs() shell assembly. The graph computes only `final_color`; everything around it —
2
+ // texture sample, alpha discard, lighting context, built-in gates, and the FSOut/mask
3
+ // write is composed here from two orthogonal, engine-owned axes:
4
+ // RenderClass ("auto" | "eye" | "hair") — stencil/cull/draw-order + eye rear-gate +
5
+ // hair over-eyes alpha. See render-class.ts.
6
+ // AlphaMode ("opaque" | "hashed") — standard threshold discard vs Wyman hashed.
7
+ // A style group supplies both; this file turns them into the WGSL shell around fsBody.
8
+ // See docs/style-groups-spec.md §5, §7.
5
9
 
6
10
  import { NODES_WGSL } from "../shaders/materials/nodes"
7
11
  import { COMMON_MATERIAL_PRELUDE_WGSL } from "../shaders/materials/common"
8
- import type { MaterialPreset } from "../engine"
12
+ import type { AlphaMode, RenderClass } from "./render-class"
9
13
 
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
- }
14
+ // ── Module-scope declarations ──
15
+ // hair: the over-eyes pipeline-override constant (a second pipeline is compiled with
16
+ // IS_OVER_EYES=1). hashed: the Wyman & McGuire object-space hashed-alpha helpers.
17
+ const HAIR_OVER_EYES_DECL = `override IS_OVER_EYES: bool = false;
18
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
19
  `
24
20
 
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 {
21
+ const HASHED_ALPHA_DECLS = `fn _hash3d_wm(a: vec3f) -> f32 {
50
22
  return _hash33(a).x * 0.5 + 0.5;
51
23
  }
52
24
  fn hashed_alpha_threshold(co: vec3f) -> f32 {
@@ -71,56 +43,48 @@ fn hashed_alpha_threshold(co: vec3f) -> f32 {
71
43
  return clamp(threshold, 1e-6, 1.0);
72
44
  }
73
45
 
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
- export const SLOT_TEMPLATES: Partial<Record<MaterialPreset, SlotTemplate>> = {
98
- hair: HAIR_TEMPLATE,
99
- stockings: STOCKINGS_TEMPLATE,
100
- }
46
+ `
101
47
 
102
- export function slotTemplate(slot: MaterialPreset): SlotTemplate {
103
- return SLOT_TEMPLATES[slot] ?? DEFAULT_TEMPLATE
104
- }
48
+ // Eye's built-in rear-view gate (open-shell PMX heads don't occlude the eye from behind,
49
+ // so gate by camera-vs-face hemisphere via the 頭 bone; discard drops color + depth +
50
+ // the stencil stamp together). Injected between the view vectors and the lighting setup.
51
+ const EYE_REAR_GATE = `
52
+ if (material.headBoneIndex >= 0.0) {
53
+ let hm = skinMats[u32(material.headBoneIndex)];
54
+ let faceDir = -normalize(hm[2].xyz);
55
+ if (dot(faceDir, v) < -0.15) { discard; }
56
+ }
57
+ `
105
58
 
106
- // Adjust-tier uniforms — fixed 16-vec4f block (256 B) so the single shared material
107
- // bind group layout serves every graph; non-graph presets bind a zero buffer.
59
+ // ── Adjust-tier uniforms — fixed 16-vec4f block (256 B) shared by the single material
60
+ // bind group layout; non-graph draws bind a zero buffer. ──
108
61
  export const STYLE_UNIFORMS_WGSL = `struct StyleUniforms { p: array<vec4f, 16> };
109
62
  @group(2) @binding(4) var<uniform> style: StyleUniforms;
110
63
 
111
64
  `
112
65
 
113
- // fs() prelude identical to the hand-written materials so template locals keep the
114
- // exact names the registry's context nodes and emit functions reference.
115
- const FS_PRELUDE = `@fragment fn fs(input: VertexOutput) -> FSOut {
66
+ function decls(renderClass: RenderClass, alphaMode: AlphaMode): string {
67
+ return (alphaMode === "hashed" ? HASHED_ALPHA_DECLS : "") + (renderClass === "hair" ? HAIR_OVER_EYES_DECL : "")
68
+ }
69
+
70
+ // fs() header up to and including the graph body's context locals. Composed so the
71
+ // hand-written material shaders' local names (tex_color, n, v, l, sun, amb, shadow) are
72
+ // preserved exactly — the registry's context nodes and emit functions reference them.
73
+ function prelude(renderClass: RenderClass, alphaMode: AlphaMode): string {
74
+ const discard =
75
+ alphaMode === "hashed"
76
+ ? " if (alpha < hashed_alpha_threshold(input.restPos)) { discard; }"
77
+ : " if (alpha < 0.001) { discard; }"
78
+ const gate = renderClass === "eye" ? EYE_REAR_GATE : ""
79
+ return `@fragment fn fs(input: VertexOutput) -> FSOut {
116
80
  let tex_s = textureSample(diffuseTexture, diffuseSampler, input.uv);
117
- // MMD alpha semantics: material alpha × texture alpha (hair/lace textures cut
118
- // their shapes in the alpha channel).
81
+ // MMD alpha semantics: material alpha × texture alpha.
119
82
  let alpha = material.alpha * tex_s.a;
120
- if (alpha < 0.001) { discard; }
83
+ ${discard}
121
84
 
122
85
  let n = safe_normal(input.normal);
123
86
  let v = normalize(camera.viewPos - input.worldPos);
87
+ ${gate}
124
88
  let l = -light.lights[0].direction.xyz;
125
89
  let sun = light.lights[0].color.xyz * light.lights[0].color.w;
126
90
  let amb = light.ambientColor.xyz;
@@ -128,18 +92,50 @@ const FS_PRELUDE = `@fragment fn fs(input: VertexOutput) -> FSOut {
128
92
  let tex_color = tex_s.rgb;
129
93
 
130
94
  `
95
+ }
96
+
97
+ // Tail of fs(): consumes `final_color` + locals, writes FSOut. hashed forces output
98
+ // alpha to 1 (the discard already did the cutout); hair scales alpha for the over-eyes
99
+ // pass when IS_OVER_EYES is compiled true.
100
+ function epilogue(renderClass: RenderClass, alphaMode: AlphaMode): string {
101
+ const alphaBase = alphaMode === "hashed" ? "1.0" : "alpha"
102
+ if (renderClass === "hair") {
103
+ return ` var outAlpha = ${alphaBase};
104
+ if (IS_OVER_EYES) { outAlpha = ${alphaBase} * 0.25; }
105
+
106
+ var out: FSOut;
107
+ out.color = vec4f(final_color, outAlpha);
108
+ out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
109
+ return out;
110
+ `
111
+ }
112
+ return ` var out: FSOut;
113
+ out.color = vec4f(final_color, ${alphaBase});
114
+ out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
115
+ return out;
116
+ `
117
+ }
131
118
 
132
- export function assembleModule(slot: MaterialPreset, fsBody: string, includeStyleUniforms: boolean): string {
133
- const tmpl = slotTemplate(slot)
119
+ /**
120
+ * Assemble a full WGSL module: shared node library + bindings/skinning prelude, optional
121
+ * StyleUniforms, render-class/alpha-mode declarations, the composed fs() shell, and the
122
+ * graph's node body.
123
+ */
124
+ export function assembleModule(
125
+ renderClass: RenderClass,
126
+ alphaMode: AlphaMode,
127
+ fsBody: string,
128
+ includeStyleUniforms: boolean,
129
+ ): string {
134
130
  return (
135
131
  NODES_WGSL +
136
132
  COMMON_MATERIAL_PRELUDE_WGSL +
137
133
  (includeStyleUniforms ? STYLE_UNIFORMS_WGSL : "") +
138
- tmpl.decls +
139
- (tmpl.prelude ?? FS_PRELUDE) +
134
+ decls(renderClass, alphaMode) +
135
+ prelude(renderClass, alphaMode) +
140
136
  fsBody +
141
137
  "\n" +
142
- tmpl.epilogue +
138
+ epilogue(renderClass, alphaMode) +
143
139
  "}\n"
144
140
  )
145
141
  }
@@ -0,0 +1,39 @@
1
+ // Style groups — the user-facing unit of material styling. A group binds a set of
2
+ // materials to a node graph (pure shading) plus the engine-owned pass-integration axes
3
+ // (renderClass + alphaMode). Each material belongs to at most one group; grouped
4
+ // materials render via the group's compiled-graph pipeline, ungrouped ones fall back to
5
+ // the hand-written preset path. See docs/style-groups-spec.md.
6
+
7
+ import type { Diagnostic, StyleGraph } from "./schema"
8
+ import type { AlphaMode, RenderClass } from "./render-class"
9
+ import type { StyleSlot } from "./compile"
10
+
11
+ export type StyleGroup = {
12
+ /** Stable, unique within a model; /^[a-z0-9_-]+$/. The engine keys installs by this. */
13
+ id: string
14
+ /** Human display name ("Skirt", "Visor"); host-owned, round-tripped, never interpreted
15
+ * by the engine. Defaults to `id`. */
16
+ label?: string
17
+ /** Material names in this group; each material is in AT MOST one group. */
18
+ materials: string[]
19
+ /** Pure shading (StyleGraph has no `slot` — integration lives here on the group). */
20
+ graph: StyleGraph
21
+ /** Pass-integration class: stencil/cull/draw-order. Default "auto". */
22
+ renderClass?: RenderClass
23
+ /** Alpha-handling axis, orthogonal to renderClass. Default "opaque". */
24
+ alphaMode?: AlphaMode
25
+ }
26
+
27
+ export type GroupDiagnostic = { groupId: string; diagnostics: Diagnostic[]; ok: boolean }
28
+
29
+ export type ApplyStyleGroupsResult = {
30
+ /** Every group compiled + swapped. */
31
+ ok: boolean
32
+ groups: GroupDiagnostic[]
33
+ /** Names in a group that no model material matches (warning). */
34
+ unknownMaterials: string[]
35
+ /** Materials claimed by >1 group; the last group in array order wins (logged). */
36
+ conflicts: string[]
37
+ }
38
+
39
+ export type ApplyStyleGroupResult = { ok: boolean; diagnostics: Diagnostic[]; slotMap: StyleSlot[] }
package/src/index.ts CHANGED
@@ -10,7 +10,6 @@ export {
10
10
  type MaterialPreset,
11
11
  type ResolvedMaterialPreset,
12
12
  type MaterialPresetMap,
13
- type ApplyStyleResult,
14
13
  type GizmoDragEvent,
15
14
  type GizmoDragCallback,
16
15
  type GizmoDragKind,
@@ -33,6 +32,13 @@ export type {
33
32
  Diagnostic,
34
33
  } from "./graph/schema"
35
34
  export { NODE_REGISTRY, type NodeSpec, type SockT } from "./graph/registry"
35
+ export { RENDER_CLASSES, type RenderClass, type AlphaMode, type RenderClassInfo } from "./graph/render-class"
36
+ export type {
37
+ StyleGroup,
38
+ GroupDiagnostic,
39
+ ApplyStyleGroupsResult,
40
+ ApplyStyleGroupResult,
41
+ } from "./graph/style-group"
36
42
  export { HAIR_GRAPH } from "./graph/presets/hair"
37
43
  export { DEFAULT_GRAPH } from "./graph/presets/default"
38
44
  export { CLOTH_SMOOTH_GRAPH } from "./graph/presets/cloth_smooth"
@@ -40,6 +46,8 @@ export { CLOTH_ROUGH_GRAPH } from "./graph/presets/cloth_rough"
40
46
  export { METAL_GRAPH } from "./graph/presets/metal"
41
47
  export { BODY_GRAPH } from "./graph/presets/body"
42
48
  export { STOCKINGS_GRAPH } from "./graph/presets/stockings"
49
+ export { EYE_GRAPH } from "./graph/presets/eye"
50
+ export { FACE_GRAPH } from "./graph/presets/face"
43
51
  export { Model } from "./model"
44
52
  export { Vec3, Quat, Mat4 } from "./math"
45
53
  export type {