@aardworx/wombat.rendering 0.7.2 → 0.8.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.
@@ -0,0 +1,185 @@
1
+ // heapShader — bridge from wombat.shader effects to heap-scene
2
+ // fragment WGSL.
3
+ //
4
+ // `buildHeapScene` requires a fragment-stage WGSL string per group
5
+ // key (see runtime/heapScene.ts). Writing those strings by hand
6
+ // works but is the wrong long-term shape — users want to write the
7
+ // FS in the wombat.shader DSL and get the same effect-composition,
8
+ // type-checking, and DCE the per-RO path enjoys.
9
+ //
10
+ // `compileHeapFragment(effect)` runs wombat.shader's emit on a
11
+ // fragment Effect and rewrites the resulting WGSL to plug into the
12
+ // heap-scene's `VsOut` shape:
13
+ //
14
+ // ┌─ heap-scene's VsOut ┌─ DSL fragment input must use
15
+ // │ worldPos: vec3<f32> │ matching field names:
16
+ // │ normal: vec3<f32> │ worldPos, normal, color,
17
+ // │ color: vec4<f32> │ lightLoc
18
+ // │ lightLoc: vec3<f32> │
19
+ // └─ └─
20
+ //
21
+ // The DSL effect's fragment OUTPUT must be a single field; its name
22
+ // is irrelevant (the adapter rewraps to bare `@location(0) vec4<f32>`).
23
+ // `uniform.X` references inside the fragment body are redirected to
24
+ // `in.X` — the contract is that any uniform the FS reads must have
25
+ // been thread through the VS into the corresponding VsOut field.
26
+ //
27
+ // What this DOES NOT do: rewrite the vertex stage. The heap-scene's
28
+ // VS is fixed (storage-buffer uniform reads, vertex pulling from
29
+ // position/normal slabs); user effects only contribute the fragment.
30
+
31
+ import type { Effect } from "@aardworx/wombat.shader";
32
+
33
+ /**
34
+ * Compile a wombat.shader fragment `Effect` into a WGSL string
35
+ * usable as a `buildHeapScene` `fragmentShaders` value.
36
+ *
37
+ * The effect's fragment input field names must match the
38
+ * heap-scene `VsOut` fields: `worldPos`, `normal`, `color`,
39
+ * `lightLoc`. The fragment output must be a single `vec4<f32>`
40
+ * field (any name).
41
+ */
42
+ export function compileHeapFragment(effect: Effect): string {
43
+ const compiled = effect.compile({ target: "wgsl" });
44
+ const fs = compiled.stages.find(s => s.stage === "fragment");
45
+ if (fs === undefined) {
46
+ throw new Error("compileHeapFragment: effect has no fragment stage");
47
+ }
48
+ return rewriteFragment(fs.source);
49
+ }
50
+
51
+ function rewriteFragment(wgsl: string): string {
52
+ let s = wgsl;
53
+
54
+ // 1. Drop the auto-emitted UBO struct + binding. Heap-scene FS
55
+ // doesn't bind a UBO at all (uniforms ride on VsOut from VS).
56
+ s = s.replace(/struct\s+_UB_uniform\s*\{[\s\S]*?\};?\s*/g, "");
57
+ s = s.replace(/@group\(\d+\)\s*@binding\(\d+\)\s*var<uniform>\s+_w_uniform\s*:\s*_UB_uniform\s*;?\s*/g, "");
58
+
59
+ // 2. Redirect uniform reads to inter-stage. _w_uniform.LightLocation
60
+ // → in.lightLoc.
61
+ s = s.replace(/_w_uniform\.(\w+)/g, (_m, name: string) => "in." + uniformToVsOut(name));
62
+
63
+ // 3. Locate the @fragment function signature; capture the input
64
+ // struct name + output struct name from the signature itself.
65
+ // Two emit shapes seen in practice:
66
+ // - inline-marker: `wombat_fragment_<hash>` + `Wombat_fragment_<hash>{Input,Output}`
67
+ // - direct stage(): `<fnName>` + `<FnName>{Input,Output}`
68
+ // Detecting from the signature works for both.
69
+ const fnSigMatch = s.match(
70
+ /@fragment\s+fn\s+(\w+)\s*\(\s*(\w+)\s*:\s*(\w+)\s*\)\s*->\s*(\w+)\s*\{/,
71
+ );
72
+ if (fnSigMatch === null) {
73
+ throw new Error("compileHeapFragment: emitted WGSL has no @fragment function header");
74
+ }
75
+ const fnHeader = fnSigMatch[0];
76
+ const paramName = fnSigMatch[2]!;
77
+ const inputStructName = fnSigMatch[3]!;
78
+ const outputStructName = fnSigMatch[4]!;
79
+
80
+ // 4. Drop the input struct.
81
+ const inputStructRe = new RegExp(`struct\\s+${escapeRegExp(inputStructName)}\\s*\\{[\\s\\S]*?\\};?`);
82
+ s = s.replace(inputStructRe, "");
83
+
84
+ // 5. Capture + drop the output struct, recording its (single) field name.
85
+ const outputStructRe = new RegExp(`struct\\s+${escapeRegExp(outputStructName)}\\s*\\{([\\s\\S]*?)\\};?`);
86
+ const outputMatch = s.match(outputStructRe);
87
+ if (outputMatch === null) {
88
+ throw new Error(`compileHeapFragment: cannot locate output struct '${outputStructName}'`);
89
+ }
90
+ const outputBody = outputMatch[1]!;
91
+ const fieldRe = /@location\(0\)\s+(\w+)\s*:\s*vec4<f32>\s*,?/g;
92
+ const fieldMatches = [...outputBody.matchAll(fieldRe)];
93
+ if (fieldMatches.length !== 1) {
94
+ throw new Error(
95
+ `compileHeapFragment: fragment output must be a single @location(0) vec4<f32> field; got ${fieldMatches.length} field(s)`,
96
+ );
97
+ }
98
+ const outputFieldName = fieldMatches[0]![1]!;
99
+ s = s.replace(outputMatch[0], "");
100
+
101
+ // 6. Rewrite the function header to the heap-scene shape. Any parameter
102
+ // name (in, input, …) is renormalised to `in` so the user shader's
103
+ // field references work uniformly downstream.
104
+ s = s.replace(fnHeader, "@fragment\nfn fs(in: VsOut) -> @location(0) vec4<f32> {");
105
+ if (paramName !== "in") {
106
+ // Rewrite `paramName.X` references to `in.X`.
107
+ const paramRe = new RegExp(`\\b${escapeRegExp(paramName)}\\.`, "g");
108
+ s = s.replace(paramRe, "in.");
109
+ }
110
+
111
+ // 7. Rewrite the body's `out: <Output>; ... out.<field> = expr; return out;` to
112
+ // return expr directly. wombat.shader's emit always uses this pattern; we
113
+ // transform it via two passes (capture the assignment, drop the var/return).
114
+ const fieldRef = `out\\.${escapeRegExp(outputFieldName)}`;
115
+ const assignRe = new RegExp(
116
+ `var\\s+out\\s*:\\s*${escapeRegExp(outputStructName)}\\s*;([\\s\\S]*?)${fieldRef}\\s*=\\s*([\\s\\S]*?);([\\s\\S]*?)return\\s+out\\s*;`,
117
+ );
118
+ const assignMatch = s.match(assignRe);
119
+ if (assignMatch === null) {
120
+ // Fall back: if user wrote a more exotic body we surface a clear error.
121
+ throw new Error(
122
+ "compileHeapFragment: emitted body doesn't follow the var-out / out." + outputFieldName +
123
+ " = ... / return out pattern (got unsupported emit shape)",
124
+ );
125
+ }
126
+ const beforeAssign = assignMatch[1]!; // statements between var-out decl and the field assignment
127
+ const expr = assignMatch[2]!;
128
+ const afterAssign = assignMatch[3]!; // statements between the assignment and the return
129
+ s = s.replace(assignMatch[0], beforeAssign + afterAssign + `return ${expr};`);
130
+
131
+ // 8. Map references `in.<DslField>` to the corresponding heap VsOut
132
+ // field name (a 1:1 mapping when the user's input struct uses
133
+ // the canonical field names; identity otherwise).
134
+ s = s.replace(/in\.(\w+)/g, (_m, name: string) => "in." + dslFieldToVsOut(name));
135
+
136
+ // 9. Tidy: collapse 3+ blank lines to one.
137
+ s = s.replace(/\n{3,}/g, "\n\n").trimStart();
138
+
139
+ return s;
140
+ }
141
+
142
+ // Map a DSL uniform name to the heap-scene VsOut field that carries
143
+ // it. Today only `LightLocation` rides on VsOut; everything else
144
+ // would need to be added to the VS prelude in heapScene.ts.
145
+ function escapeRegExp(s: string): string {
146
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
147
+ }
148
+
149
+ function uniformToVsOut(name: string): string {
150
+ switch (name) {
151
+ case "LightLocation": return "lightLoc";
152
+ default:
153
+ throw new Error(
154
+ `compileHeapFragment: uniform '${name}' is not carried on VsOut. Either thread it through the VS or use a fragment that doesn't read it.`,
155
+ );
156
+ }
157
+ }
158
+
159
+ // Map an inter-stage name from the DSL convention (PascalCase, e.g.
160
+ // 'Normals', 'WorldPositions') or our preferred direct names
161
+ // ('normal', 'worldPos') to the canonical VsOut field name.
162
+ function dslFieldToVsOut(name: string): string {
163
+ switch (name) {
164
+ // canonical heap-scene names — pass through.
165
+ case "worldPos":
166
+ case "normal":
167
+ case "color":
168
+ case "lightLoc":
169
+ return name;
170
+ // legacy PascalCase from the wombat.dom convention — auto-translate
171
+ // so users can copy DSL effects from the per-RO path with minimal edits.
172
+ case "WorldPositions": return "worldPos";
173
+ case "WorldPosition": return "worldPos";
174
+ case "Normals": return "normal";
175
+ case "Normal": return "normal";
176
+ case "Colors": return "color";
177
+ case "Color": return "color";
178
+ case "LightLocation": return "lightLoc";
179
+ default:
180
+ throw new Error(
181
+ `compileHeapFragment: input field '${name}' has no mapping to VsOut. ` +
182
+ `Use one of: worldPos, normal, color, lightLoc.`,
183
+ );
184
+ }
185
+ }
@@ -22,3 +22,15 @@ export {
22
22
  ScenePass,
23
23
  type WalkerStats,
24
24
  } from "./scenePass.js";
25
+
26
+ export {
27
+ buildHeapScene,
28
+ type HeapScene,
29
+ type HeapSceneStats,
30
+ type HeapDrawSpec,
31
+ type HeapGeometry,
32
+ type HeapTextureSet,
33
+ type BuildHeapSceneOptions,
34
+ } from "./heapScene.js";
35
+
36
+ export { compileHeapFragment } from "./heapShader.js";