@tinypoly/gpu-renderer 0.0.0-stage → 0.0.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 (194) hide show
  1. package/ARCHITECTURE.md +334 -0
  2. package/LICENSE +21 -0
  3. package/README.md +258 -2
  4. package/dist/accumulator.d.ts +27 -0
  5. package/dist/accumulator.d.ts.map +1 -0
  6. package/dist/accumulator.js +57 -0
  7. package/dist/accumulator.js.map +1 -0
  8. package/dist/bvh.d.ts +26 -0
  9. package/dist/bvh.d.ts.map +1 -0
  10. package/dist/bvh.js +207 -0
  11. package/dist/bvh.js.map +1 -0
  12. package/dist/camera.d.ts +29 -0
  13. package/dist/camera.d.ts.map +1 -0
  14. package/dist/camera.js +48 -0
  15. package/dist/camera.js.map +1 -0
  16. package/dist/canvas.d.ts +9 -0
  17. package/dist/canvas.d.ts.map +1 -0
  18. package/dist/canvas.js +11 -0
  19. package/dist/canvas.js.map +1 -0
  20. package/dist/denoise.d.ts +69 -0
  21. package/dist/denoise.d.ts.map +1 -0
  22. package/dist/denoise.js +455 -0
  23. package/dist/denoise.js.map +1 -0
  24. package/dist/device.d.ts +37 -0
  25. package/dist/device.d.ts.map +1 -0
  26. package/dist/device.js +52 -0
  27. package/dist/device.js.map +1 -0
  28. package/dist/dispatchScheduler.d.ts +43 -0
  29. package/dist/dispatchScheduler.d.ts.map +1 -0
  30. package/dist/dispatchScheduler.js +49 -0
  31. package/dist/dispatchScheduler.js.map +1 -0
  32. package/dist/environment.d.ts +39 -0
  33. package/dist/environment.d.ts.map +1 -0
  34. package/dist/environment.js +168 -0
  35. package/dist/environment.js.map +1 -0
  36. package/dist/frameLayout.d.ts +4 -0
  37. package/dist/frameLayout.d.ts.map +1 -0
  38. package/dist/frameLayout.js +58 -0
  39. package/dist/frameLayout.js.map +1 -0
  40. package/dist/geometry.d.ts +75 -0
  41. package/dist/geometry.d.ts.map +1 -0
  42. package/dist/geometry.js +671 -0
  43. package/dist/geometry.js.map +1 -0
  44. package/dist/index.d.ts +25 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +17 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/lights.d.ts +64 -0
  49. package/dist/lights.d.ts.map +1 -0
  50. package/dist/lights.js +264 -0
  51. package/dist/lights.js.map +1 -0
  52. package/dist/materialLayout.d.ts +143 -0
  53. package/dist/materialLayout.d.ts.map +1 -0
  54. package/dist/materialLayout.js +297 -0
  55. package/dist/materialLayout.js.map +1 -0
  56. package/dist/math.d.ts +10 -0
  57. package/dist/math.d.ts.map +1 -0
  58. package/dist/math.js +40 -0
  59. package/dist/math.js.map +1 -0
  60. package/dist/pathTracePass.d.ts +27 -0
  61. package/dist/pathTracePass.d.ts.map +1 -0
  62. package/dist/pathTracePass.js +83 -0
  63. package/dist/pathTracePass.js.map +1 -0
  64. package/dist/planarCapture.d.ts +38 -0
  65. package/dist/planarCapture.d.ts.map +1 -0
  66. package/dist/planarCapture.js +163 -0
  67. package/dist/planarCapture.js.map +1 -0
  68. package/dist/presentPass.d.ts +31 -0
  69. package/dist/presentPass.d.ts.map +1 -0
  70. package/dist/presentPass.js +78 -0
  71. package/dist/presentPass.js.map +1 -0
  72. package/dist/renderer.d.ts +196 -0
  73. package/dist/renderer.d.ts.map +1 -0
  74. package/dist/renderer.js +839 -0
  75. package/dist/renderer.js.map +1 -0
  76. package/dist/sceneAssets.d.ts +24 -0
  77. package/dist/sceneAssets.d.ts.map +1 -0
  78. package/dist/sceneAssets.js +77 -0
  79. package/dist/sceneAssets.js.map +1 -0
  80. package/dist/sceneUpload.d.ts +61 -0
  81. package/dist/sceneUpload.d.ts.map +1 -0
  82. package/dist/sceneUpload.js +98 -0
  83. package/dist/sceneUpload.js.map +1 -0
  84. package/dist/shaderModule.d.ts +7 -0
  85. package/dist/shaderModule.d.ts.map +1 -0
  86. package/dist/shaderModule.js +19 -0
  87. package/dist/shaderModule.js.map +1 -0
  88. package/dist/shaderProgram.d.ts +124 -0
  89. package/dist/shaderProgram.d.ts.map +1 -0
  90. package/dist/shaderProgram.js +1030 -0
  91. package/dist/shaderProgram.js.map +1 -0
  92. package/dist/structLayout.d.ts +28 -0
  93. package/dist/structLayout.d.ts.map +1 -0
  94. package/dist/structLayout.js +58 -0
  95. package/dist/structLayout.js.map +1 -0
  96. package/dist/texturePack.d.ts +54 -0
  97. package/dist/texturePack.d.ts.map +1 -0
  98. package/dist/texturePack.js +86 -0
  99. package/dist/texturePack.js.map +1 -0
  100. package/dist/upstream.d.ts +7 -0
  101. package/dist/upstream.d.ts.map +1 -0
  102. package/dist/upstream.js +11 -0
  103. package/dist/upstream.js.map +1 -0
  104. package/dist/volumetric.d.ts +10 -0
  105. package/dist/volumetric.d.ts.map +1 -0
  106. package/dist/volumetric.js +34 -0
  107. package/dist/volumetric.js.map +1 -0
  108. package/dist/wgsl/bsdf.wgsl.d.ts +13 -0
  109. package/dist/wgsl/bsdf.wgsl.d.ts.map +1 -0
  110. package/dist/wgsl/bsdf.wgsl.js +619 -0
  111. package/dist/wgsl/bsdf.wgsl.js.map +1 -0
  112. package/dist/wgsl/bvh.wgsl.d.ts +3 -0
  113. package/dist/wgsl/bvh.wgsl.d.ts.map +1 -0
  114. package/dist/wgsl/bvh.wgsl.js +218 -0
  115. package/dist/wgsl/bvh.wgsl.js.map +1 -0
  116. package/dist/wgsl/common.wgsl.d.ts +3 -0
  117. package/dist/wgsl/common.wgsl.d.ts.map +1 -0
  118. package/dist/wgsl/common.wgsl.js +193 -0
  119. package/dist/wgsl/common.wgsl.js.map +1 -0
  120. package/dist/wgsl/environment.wgsl.d.ts +10 -0
  121. package/dist/wgsl/environment.wgsl.d.ts.map +1 -0
  122. package/dist/wgsl/environment.wgsl.js +145 -0
  123. package/dist/wgsl/environment.wgsl.js.map +1 -0
  124. package/dist/wgsl/lights.wgsl.d.ts +10 -0
  125. package/dist/wgsl/lights.wgsl.d.ts.map +1 -0
  126. package/dist/wgsl/lights.wgsl.js +434 -0
  127. package/dist/wgsl/lights.wgsl.js.map +1 -0
  128. package/dist/wgsl/pathTrace.wgsl.d.ts +14 -0
  129. package/dist/wgsl/pathTrace.wgsl.d.ts.map +1 -0
  130. package/dist/wgsl/pathTrace.wgsl.js +1020 -0
  131. package/dist/wgsl/pathTrace.wgsl.js.map +1 -0
  132. package/dist/wgsl/present.wgsl.d.ts +7 -0
  133. package/dist/wgsl/present.wgsl.d.ts.map +1 -0
  134. package/dist/wgsl/present.wgsl.js +154 -0
  135. package/dist/wgsl/present.wgsl.js.map +1 -0
  136. package/dist/wgsl/random.wgsl.d.ts +9 -0
  137. package/dist/wgsl/random.wgsl.d.ts.map +1 -0
  138. package/dist/wgsl/random.wgsl.js +91 -0
  139. package/dist/wgsl/random.wgsl.js.map +1 -0
  140. package/dist/wgsl/sampling.wgsl.d.ts +3 -0
  141. package/dist/wgsl/sampling.wgsl.d.ts.map +1 -0
  142. package/dist/wgsl/sampling.wgsl.js +31 -0
  143. package/dist/wgsl/sampling.wgsl.js.map +1 -0
  144. package/dist/wgsl/shaderSupport.wgsl.d.ts +6 -0
  145. package/dist/wgsl/shaderSupport.wgsl.d.ts.map +1 -0
  146. package/dist/wgsl/shaderSupport.wgsl.js +120 -0
  147. package/dist/wgsl/shaderSupport.wgsl.js.map +1 -0
  148. package/dist/wgsl/surface.wgsl.d.ts +10 -0
  149. package/dist/wgsl/surface.wgsl.d.ts.map +1 -0
  150. package/dist/wgsl/surface.wgsl.js +794 -0
  151. package/dist/wgsl/surface.wgsl.js.map +1 -0
  152. package/dist/wgsl/texture.wgsl.d.ts +11 -0
  153. package/dist/wgsl/texture.wgsl.d.ts.map +1 -0
  154. package/dist/wgsl/texture.wgsl.js +132 -0
  155. package/dist/wgsl/texture.wgsl.js.map +1 -0
  156. package/package.json +76 -3
  157. package/src/accumulator.ts +64 -0
  158. package/src/bvh.ts +272 -0
  159. package/src/camera.ts +77 -0
  160. package/src/canvas.ts +13 -0
  161. package/src/denoise.ts +514 -0
  162. package/src/device.ts +81 -0
  163. package/src/dispatchScheduler.ts +71 -0
  164. package/src/environment.ts +233 -0
  165. package/src/frameLayout.ts +59 -0
  166. package/src/geometry.ts +899 -0
  167. package/src/index.ts +61 -0
  168. package/src/lights.ts +365 -0
  169. package/src/materialLayout.ts +408 -0
  170. package/src/math.ts +48 -0
  171. package/src/pathTracePass.ts +97 -0
  172. package/src/planarCapture.ts +225 -0
  173. package/src/presentPass.ts +99 -0
  174. package/src/renderer.ts +1117 -0
  175. package/src/sceneAssets.ts +118 -0
  176. package/src/sceneUpload.ts +185 -0
  177. package/src/shaderModule.ts +19 -0
  178. package/src/shaderProgram.ts +1370 -0
  179. package/src/structLayout.ts +93 -0
  180. package/src/texturePack.ts +152 -0
  181. package/src/upstream.ts +56 -0
  182. package/src/volumetric.ts +42 -0
  183. package/src/wgsl/bsdf.wgsl.ts +618 -0
  184. package/src/wgsl/bvh.wgsl.ts +217 -0
  185. package/src/wgsl/common.wgsl.ts +193 -0
  186. package/src/wgsl/environment.wgsl.ts +144 -0
  187. package/src/wgsl/lights.wgsl.ts +433 -0
  188. package/src/wgsl/pathTrace.wgsl.ts +1019 -0
  189. package/src/wgsl/present.wgsl.ts +153 -0
  190. package/src/wgsl/random.wgsl.ts +90 -0
  191. package/src/wgsl/sampling.wgsl.ts +30 -0
  192. package/src/wgsl/shaderSupport.wgsl.ts +119 -0
  193. package/src/wgsl/surface.wgsl.ts +793 -0
  194. package/src/wgsl/texture.wgsl.ts +131 -0
@@ -0,0 +1,1030 @@
1
+ import { ShaderChunk } from "three";
2
+ import { NO_TEXTURE } from "./texturePack.js";
3
+ import { CpuShader } from "./upstream.js";
4
+ /** The chunks a shader may include, with the output transforms the renderer applies to the image itself. */
5
+ export const SHADER_CHUNKS = {
6
+ ...ShaderChunk,
7
+ tonemapping_fragment: "",
8
+ colorspace_fragment: "",
9
+ encodings_fragment: "",
10
+ // Three's fog chunks are honoured by the integrator for every material that accepts fog, shaders included.
11
+ fog_fragment: "",
12
+ };
13
+ /** What a sampler becomes in the rewritten GLSL: a texture table index, typed so the overloads below can tell them apart. */
14
+ const SAMPLER_TYPES = {
15
+ sampler2D: "uint",
16
+ isampler2D: "uint",
17
+ usampler2D: "uint",
18
+ sampler2DShadow: "uint",
19
+ samplerExternalOES: "uint",
20
+ samplerCube: "int",
21
+ samplerCubeShadow: "int",
22
+ sampler3D: "uvec2",
23
+ sampler2DArray: "uvec2",
24
+ isampler3D: "uvec2",
25
+ usampler3D: "uvec2",
26
+ sampler2DArrayShadow: "uvec2",
27
+ };
28
+ /** The texture calls the kernel provides, by the name the rewrite gives them. */
29
+ const TEXTURE_CALLS = [
30
+ [/\b(texture2DLodEXT|texture2DLod|textureCubeLodEXT|textureCubeLod|textureLod)\s*\(/g, "tp_texLod("],
31
+ [/\b(texture2DGradEXT|textureCubeGradEXT|textureGrad)\s*\(/g, "tp_texGrad("],
32
+ [/\b(texture2DProj|textureProj)\s*\(/g, "tp_texProj("],
33
+ [/\btexelFetch\s*\(/g, "tp_texelFetch("],
34
+ [/\btextureSize\s*\(/g, "tp_texSize("],
35
+ [/\b(textureCube|texture2D|texture)\s*\(/g, "tp_tex("],
36
+ ];
37
+ /** Built-in variables, which become globals the kernel sets. */
38
+ const BUILTINS = [
39
+ [/\bgl_FragData\s*\[\s*0\s*\]/g, "tp_FragColor"],
40
+ [/\bgl_FragColor\b/g, "tp_FragColor"],
41
+ [/\bgl_FragCoord\b/g, "tp_FragCoord"],
42
+ [/\bgl_FrontFacing\b/g, "tp_FrontFacing"],
43
+ [/\bgl_PointCoord\b/g, "tp_PointCoord"],
44
+ [/\bgl_FragDepth(EXT)?\b/g, "tp_FragDepth"],
45
+ ];
46
+ /**
47
+ * Stand-ins for everything the kernel replaces. naga keeps their signatures, which is how the WGSL that comes back is
48
+ * matched to the kernel's own implementation; their bodies are thrown away.
49
+ */
50
+ const INTERNAL_PRELUDE = `
51
+ vec4 tp_FragColor;
52
+ vec4 tp_FragCoord;
53
+ bool tp_FrontFacing;
54
+ vec2 tp_PointCoord;
55
+ float tp_FragDepth;
56
+ bool tp_discarded;
57
+ int tp_lane;
58
+ void tp_discard() { tp_discarded = true; }
59
+ vec4 tp_tex(uint s, vec2 uv) { return vec4(0.0); }
60
+ vec4 tp_tex(uint s, vec2 uv, float bias) { return vec4(0.0); }
61
+ vec4 tp_tex(int s, vec3 direction) { return vec4(0.0); }
62
+ vec4 tp_tex(int s, vec3 direction, float bias) { return vec4(0.0); }
63
+ vec4 tp_tex(uvec2 s, vec3 uvw) { return vec4(0.0); }
64
+ vec4 tp_tex(uvec2 s, vec3 uvw, float bias) { return vec4(0.0); }
65
+ vec4 tp_texLod(uint s, vec2 uv, float lod) { return vec4(0.0); }
66
+ vec4 tp_texLod(int s, vec3 direction, float lod) { return vec4(0.0); }
67
+ vec4 tp_texLod(uvec2 s, vec3 uvw, float lod) { return vec4(0.0); }
68
+ vec4 tp_texGrad(uint s, vec2 uv, vec2 dx, vec2 dy) { return vec4(0.0); }
69
+ vec4 tp_texGrad(int s, vec3 direction, vec3 dx, vec3 dy) { return vec4(0.0); }
70
+ vec4 tp_texProj(uint s, vec3 uv) { return vec4(0.0); }
71
+ vec4 tp_texProj(uint s, vec4 uv) { return vec4(0.0); }
72
+ vec4 tp_texelFetch(uint s, ivec2 texel, int lod) { return vec4(0.0); }
73
+ ivec2 tp_texSize(uint s, int lod) { return ivec2(0); }
74
+ ivec2 tp_texSize(int s, int lod) { return ivec2(0); }
75
+ `;
76
+ /** What Three declares in front of every `ShaderMaterial` fragment shader, minus its output transforms. */
77
+ const SHADER_MATERIAL_PRELUDE = `
78
+ mat4 viewMatrix;
79
+ vec3 cameraPosition;
80
+ bool isOrthographic;
81
+ vec4 linearToOutputTexel(vec4 value) { return value; }
82
+ vec3 toneMapping(vec3 color) { return color; }
83
+ `;
84
+ /** The surface state a built-in material's hooks read and edit, as Three's own fragment shader names it. */
85
+ const PATCHED_CONTEXT = [
86
+ ["vec4", "diffuseColor"],
87
+ ["vec3", "normal"],
88
+ ["float", "roughnessFactor"],
89
+ ["float", "metalnessFactor"],
90
+ ["vec3", "totalEmissiveRadiance"],
91
+ ["vec3", "vViewPosition"],
92
+ ["vec2", "vUv"],
93
+ ["vec3", "outgoingLight"],
94
+ ["vec3", "geometryNormal"],
95
+ ["vec3", "nonPerturbedNormal"],
96
+ ["vec3", "geometryPosition"],
97
+ ["vec3", "geometryViewDir"],
98
+ ["float", "faceDirection"],
99
+ ["vec3", "vNormal"],
100
+ ];
101
+ /** Light helpers Three's hooks expect from its lighting chunks, which a patched shader no longer includes. */
102
+ const PATCHED_HELPERS = `
103
+ float getDistanceAttenuation(float lightDistance, float cutoffDistance, float decayExponent) {
104
+ float falloff = 1.0 / max(pow(lightDistance, decayExponent), 0.01);
105
+ if (cutoffDistance > 0.0) falloff *= pow(clamp(1.0 - pow(lightDistance / cutoffDistance, 4.0), 0.0, 1.0), 2.0);
106
+ return falloff;
107
+ }
108
+ float getSpotAttenuation(float coneCosine, float penumbraCosine, float angleCosine) {
109
+ return smoothstep(coneCosine, penumbraCosine, angleCosine);
110
+ }
111
+ `;
112
+ /** Expands `#include <chunk>` the way Three does, recursively. */
113
+ export function expandIncludes(source, chunks = SHADER_CHUNKS, depth = 0) {
114
+ if (depth > 16)
115
+ throw new Error("GPU renderer: shader includes nest too deeply.");
116
+ return source.replace(/^[ \t]*#include +<([\w\d./]+)>/gm, (_match, name) => {
117
+ const chunk = chunks[name];
118
+ if (chunk === undefined)
119
+ throw new Error(`GPU renderer: unknown shader include <${name}>.`);
120
+ return expandIncludes(chunk, chunks, depth + 1);
121
+ });
122
+ }
123
+ /** Three's unrollLoops, for the loops whose bounds are plain numbers; others stay loops. */
124
+ function unrollLoops(source) {
125
+ const pattern = /#pragma unroll_loop_start\s+for\s*\(\s*int\s+i\s*=\s*(\d+)\s*;\s*i\s*<\s*(\d+)\s*;\s*i\s*\+\+\s*\)\s*{([\s\S]+?)}\s+#pragma unroll_loop_end/g;
126
+ return source
127
+ .replace(pattern, (_match, start, end, body) => {
128
+ let unrolled = "";
129
+ for (let i = Number(start); i < Number(end); i++)
130
+ unrolled += body.replace(/\[\s*i\s*\]/g, `[ ${i} ]`).replace(/UNROLLED_LOOP_INDEX/g, String(i));
131
+ return unrolled;
132
+ })
133
+ .replace(/#pragma unroll_loop_(start|end)/g, "");
134
+ }
135
+ function stripComments(source) {
136
+ return source.replace(/\/\*[\s\S]*?\*\//g, match => match.replace(/[^\n]/g, " ")).replace(/\/\/[^\n]*/g, "");
137
+ }
138
+ const QUALIFIERS = new Set([
139
+ "uniform", "varying", "attribute", "in", "out", "flat", "smooth", "noperspective", "centroid", "invariant",
140
+ "highp", "mediump", "lowp", "const"
141
+ ]);
142
+ /**
143
+ * Rewrites the declarations at global scope: uniforms and varyings lose their storage qualifier and become plain
144
+ * globals, which naga keeps as module scope variables the kernel can fill in; samplers become texture indices.
145
+ */
146
+ function rewriteGlobals(source, stage) {
147
+ const uniforms = [];
148
+ const varyings = [];
149
+ const outputs = [];
150
+ let output = "";
151
+ let depth = 0;
152
+ let statement = "";
153
+ let lineStart = true;
154
+ const declarators = (rest) => rest
155
+ .split(",")
156
+ .map(part => /^\s*(\w+)/.exec(part)?.[1])
157
+ .filter((name) => !!name);
158
+ const flush = (terminator) => {
159
+ const text = statement;
160
+ statement = "";
161
+ const layout = /^(\s*)layout\s*\([^)]*\)\s*/.exec(text);
162
+ const body = layout ? text.slice(layout[0].length) : text.trimStart();
163
+ const leading = layout ? layout[1] : text.slice(0, text.length - text.trimStart().length);
164
+ const words = body.split(/\s+/);
165
+ let index = 0;
166
+ const qualifiers = [];
167
+ while (index < words.length && QUALIFIERS.has(words[index]))
168
+ qualifiers.push(words[index++]);
169
+ const storage = qualifiers.find(q => q === "uniform" || q === "varying" || q === "attribute" || q === "in" || q === "out");
170
+ if (!storage) {
171
+ output += text + terminator;
172
+ return;
173
+ }
174
+ const remainder = words.slice(index).join(" ");
175
+ const type = /^(\w+)/.exec(remainder)?.[1] ?? "";
176
+ const names = declarators(remainder.slice(type.length));
177
+ // An interface block: `uniform Block { ... } name;` becomes a struct with that declarator.
178
+ if (terminator === "{") {
179
+ output += `${leading}struct ${type} {`;
180
+ depth++;
181
+ return;
182
+ }
183
+ if (storage === "uniform")
184
+ uniforms.push(...names);
185
+ else if (stage === "fragment" && (storage === "varying" || storage === "in"))
186
+ varyings.push(...names.map(name => ({ name, flat: qualifiers.includes("flat") })));
187
+ else if (storage === "out")
188
+ outputs.push(...names);
189
+ output += `${leading}${remainder}${terminator}`;
190
+ };
191
+ for (let i = 0; i < source.length; i++) {
192
+ const char = source[i];
193
+ // Preprocessor lines pass through whole, wherever they appear.
194
+ if (lineStart && char === "#") {
195
+ const end = source.indexOf("\n", i);
196
+ const line = end < 0 ? source.slice(i) : source.slice(i, end + 1);
197
+ if (depth === 0) {
198
+ output += statement + line;
199
+ statement = "";
200
+ }
201
+ else {
202
+ output += line;
203
+ }
204
+ i += line.length - 1;
205
+ lineStart = true;
206
+ continue;
207
+ }
208
+ if (char === "\n")
209
+ lineStart = true;
210
+ else if (char !== " " && char !== "\t" && char !== "\r")
211
+ lineStart = false;
212
+ if (depth > 0) {
213
+ output += char;
214
+ if (char === "{")
215
+ depth++;
216
+ if (char === "}")
217
+ depth--;
218
+ continue;
219
+ }
220
+ if (char === ";") {
221
+ flush(";");
222
+ continue;
223
+ }
224
+ if (char === "{") {
225
+ flush("{");
226
+ if (!output.endsWith("{"))
227
+ output += "{";
228
+ depth = Math.max(depth, 1);
229
+ continue;
230
+ }
231
+ if (char === "}") {
232
+ output += statement + char;
233
+ statement = "";
234
+ continue;
235
+ }
236
+ statement += char;
237
+ }
238
+ output += statement;
239
+ return { source: output, uniforms, varyings, outputs };
240
+ }
241
+ /**
242
+ * Turns one of Three's fragment shaders into the GLSL 450 naga reads, with every hook the kernel needs. Exported for
243
+ * the tests, which check the rewrite without a device.
244
+ */
245
+ export function prepareFragmentShader(shader, kind, options = {}) {
246
+ const defines = Object.entries(shader.defines ?? {})
247
+ .filter(([, value]) => value !== false)
248
+ .map(([name, value]) => `#define ${name} ${value === true ? "" : value}`)
249
+ .join("\n");
250
+ let body = shader.fragment;
251
+ if (kind === "patched" || kind === "after")
252
+ body = `${SHADER_CHUNKS.common}\n${body}`;
253
+ body = unrollLoops(expandIncludes(body));
254
+ body = stripComments(body)
255
+ .replace(/^[ \t]*#version[^\n]*$/gm, "")
256
+ .replace(/^[ \t]*#extension[^\n]*$/gm, "")
257
+ .replace(/^[ \t]*precision\s+\w+\s+\w+\s*;/gm, "")
258
+ .replace(/\b(highp|mediump|lowp)\b/g, "");
259
+ const rewritten = rewriteGlobals(body, "fragment");
260
+ body = rewritten.source;
261
+ for (const [type, replacement] of Object.entries(SAMPLER_TYPES))
262
+ body = body.replace(new RegExp(`\\b${type}\\b`, "g"), replacement);
263
+ for (const [pattern, replacement] of [...BUILTINS, ...TEXTURE_CALLS])
264
+ body = body.replace(pattern, replacement);
265
+ body = body
266
+ .replace(/\bdiscard\s*;/g, "tp_discard();")
267
+ .replace(/\bconst\s+(?=(in|inout|out)\b)/g, "")
268
+ .replace(/([(,]\s*)const\s+(?=\w)/g, "$1");
269
+ // Every derivative call site gets its own slot, which the neighbouring runs fill in and the centre one reads.
270
+ let sites = 0;
271
+ const siteFunctions = [];
272
+ body = body.replace(/\b(dFdx|dFdy|fwidth)\s*\(/g, (_match, operation) => {
273
+ const site = sites++;
274
+ siteFunctions.push(derivativeSite(site, operation));
275
+ return `tp_${operation}_${site}(`;
276
+ });
277
+ // Only declarations at global scope count: a parameter or a local of the same name declares nothing for main().
278
+ const global = globalScope(body);
279
+ const declared = (name) => new RegExp(`\\b(vec[234]|float|int|bool|mat[234])\\s+${name}\\s*[;=\\[,]`).test(global);
280
+ const patched = kind === "patched" || kind === "after"
281
+ ? PATCHED_CONTEXT.filter(([, name]) => !declared(name)).map(([type, name]) => `${type} ${name};`).join("\n")
282
+ + PATCHED_HELPERS
283
+ : "";
284
+ const shaderPrelude = !options.raw ? SHADER_MATERIAL_PRELUDE
285
+ .split("\n")
286
+ .filter(line => {
287
+ const name = /^\w+\s+(\w+)\s*;/.exec(line)?.[1];
288
+ return !name || !declared(name);
289
+ })
290
+ .join("\n") : "";
291
+ const source = [
292
+ "#version 450",
293
+ defines,
294
+ INTERNAL_PRELUDE,
295
+ siteFunctions.join("\n"),
296
+ shaderPrelude,
297
+ patched,
298
+ body,
299
+ ].join("\n");
300
+ return {
301
+ source,
302
+ uniforms: [...new Set(rewritten.uniforms)],
303
+ varyings: rewritten.varyings,
304
+ outputs: rewritten.outputs,
305
+ derivativeSites: sites,
306
+ };
307
+ }
308
+ /** The source with every block and parenthesis emptied, which leaves the declarations at global scope. */
309
+ function globalScope(source) {
310
+ let depth = 0;
311
+ let out = "";
312
+ for (const char of source) {
313
+ if (char === "{" || char === "(")
314
+ depth++;
315
+ if (depth === 0)
316
+ out += char;
317
+ if ((char === "}" || char === ")") && depth > 0) {
318
+ depth--;
319
+ if (depth === 0)
320
+ out += ";";
321
+ }
322
+ }
323
+ return out;
324
+ }
325
+ /** One derivative call site, overloaded for every float type, with the slot its neighbours write. */
326
+ function derivativeSite(site, operation) {
327
+ const types = [["float", ".x", "0.0"], ["vec2", ".xy", "vec2(0.0)"],
328
+ ["vec3", ".xyz", "vec3(0.0)"], ["vec4", "", "vec4(0.0)"]];
329
+ const slots = `vec4 tp_sx_${site};\nvec4 tp_sy_${site};\n`;
330
+ return slots + types.map(([type, swizzle, zero]) => {
331
+ const write = (slot) => swizzle ? `${slot}${swizzle} = v;` : `${slot} = v;`;
332
+ const read = (slot) => `${slot}${swizzle}`;
333
+ if (operation === "dFdx")
334
+ return `${type} tp_dFdx_${site}(${type} v) { if (tp_lane == 1) { ${write(`tp_sx_${site}`)} }`
335
+ + ` return tp_lane == 0 ? ${read(`tp_sx_${site}`)} - v : ${zero}; }`;
336
+ if (operation === "dFdy")
337
+ return `${type} tp_dFdy_${site}(${type} v) { if (tp_lane == 2) { ${write(`tp_sy_${site}`)} }`
338
+ + ` return tp_lane == 0 ? ${read(`tp_sy_${site}`)} - v : ${zero}; }`;
339
+ return `${type} tp_fwidth_${site}(${type} v) { if (tp_lane == 1) { ${write(`tp_sx_${site}`)} }`
340
+ + ` if (tp_lane == 2) { ${write(`tp_sy_${site}`)} }`
341
+ + ` return tp_lane == 0 ? abs(${read(`tp_sx_${site}`)} - v) + abs(${read(`tp_sy_${site}`)} - v) : ${zero}; }`;
342
+ }).join("\n");
343
+ }
344
+ /** Splits a module into its top level declarations. naga's output is regular enough for a brace counting scan. */
345
+ export function splitWgsl(source) {
346
+ const items = [];
347
+ let depth = 0;
348
+ let start = 0;
349
+ const push = (end) => {
350
+ const text = source.slice(start, end).trim();
351
+ start = end;
352
+ if (!text)
353
+ return;
354
+ const entry = /^@(fragment|vertex|compute)/.test(text);
355
+ const fn = /^(?:@[\w]+(?:\([^)]*\))?\s*)*fn\s+(\w+)\s*\(([\s\S]*?)\)\s*(?:->\s*[^{]+)?\{/.exec(text);
356
+ if (fn) {
357
+ const params = fn[2].split(",").map(p => p.trim()).filter(Boolean).map(p => p.slice(p.indexOf(":") + 1).trim());
358
+ items.push({ text, kind: entry ? "entry" : "fn", name: fn[1], params });
359
+ return;
360
+ }
361
+ const struct = /^struct\s+(\w+)/.exec(text);
362
+ if (struct) {
363
+ items.push({ text, kind: "struct", name: struct[1] });
364
+ return;
365
+ }
366
+ const variable = /^var<private>\s+(\w+)\s*:\s*([^=;]+)/.exec(text);
367
+ if (variable) {
368
+ items.push({ text, kind: "var", name: variable[1], type: variable[2].trim() });
369
+ return;
370
+ }
371
+ const constant = /^(?:const|override)\s+(\w+)/.exec(text);
372
+ if (constant) {
373
+ items.push({ text, kind: "const", name: constant[1] });
374
+ return;
375
+ }
376
+ const alias = /^alias\s+(\w+)/.exec(text);
377
+ items.push({ text, kind: alias ? "alias" : "other", name: alias?.[1] ?? "" });
378
+ };
379
+ for (let i = 0; i < source.length; i++) {
380
+ const char = source[i];
381
+ if (char === "{")
382
+ depth++;
383
+ if (char === "}") {
384
+ depth--;
385
+ if (depth === 0)
386
+ push(i + 1);
387
+ continue;
388
+ }
389
+ if (char === ";" && depth === 0)
390
+ push(i + 1);
391
+ }
392
+ push(source.length);
393
+ return items;
394
+ }
395
+ /** Prefixes every module scope name, leaving member accesses and struct member declarations alone. */
396
+ function renameItems(items, rename) {
397
+ const replace = (text, insideStruct) => {
398
+ let result = "";
399
+ const identifier = /[A-Za-z_][A-Za-z0-9_]*/g;
400
+ let last = 0;
401
+ let match;
402
+ while ((match = identifier.exec(text))) {
403
+ const name = match[0];
404
+ const before = text.slice(0, match.index).trimEnd();
405
+ const after = text.slice(match.index + name.length).trimStart();
406
+ const member = before.endsWith(".");
407
+ // Inside a struct, a name followed by a colon is a member being declared.
408
+ const declaration = insideStruct && after.startsWith(":") && !/[<(,]$/.test(before);
409
+ const next = rename.get(name);
410
+ result += text.slice(last, match.index) + (next && !member && !declaration ? next : name);
411
+ last = match.index + name.length;
412
+ }
413
+ return result + text.slice(last);
414
+ };
415
+ return items.map(item => ({
416
+ ...item,
417
+ text: replace(item.text, item.kind === "struct"),
418
+ name: rename.get(item.name) ?? item.name,
419
+ }));
420
+ }
421
+ function splitTopLevel(text) {
422
+ const parts = [];
423
+ let depth = 0, start = 0;
424
+ for (let i = 0; i < text.length; i++) {
425
+ if (text[i] === "<")
426
+ depth++;
427
+ if (text[i] === ">")
428
+ depth--;
429
+ if (text[i] === "," && depth === 0) {
430
+ parts.push(text.slice(start, i).trim());
431
+ start = i + 1;
432
+ }
433
+ }
434
+ parts.push(text.slice(start).trim());
435
+ return parts.filter(Boolean);
436
+ }
437
+ function parseType(text, structs) {
438
+ const type = text.trim();
439
+ if (type === "f32" || type === "i32" || type === "u32" || type === "bool")
440
+ return { kind: "scalar", scalar: type };
441
+ const vector = /^vec([234])<(f32|i32|u32|bool)>$/.exec(type);
442
+ if (vector)
443
+ return { kind: "vector", size: Number(vector[1]), scalar: vector[2] };
444
+ const shorthand = /^vec([234])([fiu])$/.exec(type);
445
+ if (shorthand)
446
+ return { kind: "vector", size: Number(shorthand[1]), scalar: { f: "f32", i: "i32", u: "u32" }[shorthand[2]] };
447
+ const matrix = /^mat([234])x([234])(?:<f32>|f)$/.exec(type);
448
+ if (matrix)
449
+ return { kind: "matrix", columns: Number(matrix[1]), rows: Number(matrix[2]) };
450
+ const array = /^array<([\s\S]+)>$/.exec(type);
451
+ if (array) {
452
+ const [element, count] = splitTopLevel(array[1]);
453
+ const parsed = parseType(element, structs);
454
+ const size = Number(count);
455
+ return parsed && Number.isInteger(size) ? { kind: "array", element: parsed, count: size } : null;
456
+ }
457
+ const members = structs.get(type);
458
+ if (members) {
459
+ const parsed = members.map(member => ({ name: member.name, type: parseType(member.type, structs) }));
460
+ if (parsed.some(member => !member.type))
461
+ return null;
462
+ return { kind: "struct", name: type, members: parsed };
463
+ }
464
+ return null;
465
+ }
466
+ function wordsOf(type) {
467
+ switch (type.kind) {
468
+ case "scalar": return 1;
469
+ case "vector": return type.size;
470
+ case "matrix": return type.columns * type.rows;
471
+ case "array": return type.count * wordsOf(type.element);
472
+ case "struct": return type.members.reduce((sum, member) => sum + wordsOf(member.type), 0);
473
+ }
474
+ }
475
+ const scalarRead = (scalar, word) => {
476
+ switch (scalar) {
477
+ case "f32": return `bitcast<f32>(${word})`;
478
+ case "i32": return `bitcast<i32>(${word})`;
479
+ case "bool": return `(${word} != 0u)`;
480
+ default: return word;
481
+ }
482
+ };
483
+ /** WGSL that assigns a value of `type` to `target` from the words at `base + offset`. */
484
+ function loadCode(target, type, offset, indent, loop = 0) {
485
+ const word = (k) => `textures[data + ${offset} + ${k}u]`;
486
+ switch (type.kind) {
487
+ case "scalar":
488
+ return `${indent}${target} = ${scalarRead(type.scalar, word(0))};\n`;
489
+ case "vector": {
490
+ const parts = Array.from({ length: type.size }, (_, k) => scalarRead(type.scalar, word(k)));
491
+ return `${indent}${target} = vec${type.size}<${type.scalar}>(${parts.join(", ")});\n`;
492
+ }
493
+ case "matrix": {
494
+ const parts = Array.from({ length: type.columns * type.rows }, (_, k) => scalarRead("f32", word(k)));
495
+ return `${indent}${target} = mat${type.columns}x${type.rows}<f32>(${parts.join(", ")});\n`;
496
+ }
497
+ case "array": {
498
+ const index = `i${loop}`;
499
+ const stride = wordsOf(type.element);
500
+ return `${indent}for (var ${index} = 0u; ${index} < ${type.count}u; ${index} = ${index} + 1u) {\n`
501
+ + loadCode(`${target}[${index}]`, type.element, `${offset} + ${index} * ${stride}u`, `${indent} `, loop + 1)
502
+ + `${indent}}\n`;
503
+ }
504
+ case "struct": {
505
+ let code = "";
506
+ let at = 0;
507
+ for (const member of type.members) {
508
+ code += loadCode(`${target}.${member.name}`, member.type, `${offset} + ${at}u`, indent, loop);
509
+ at += wordsOf(member.type);
510
+ }
511
+ return code;
512
+ }
513
+ }
514
+ }
515
+ /** Numbers out of a serialized uniform value: a colour, a vector, a matrix, an array or a plain number. */
516
+ function numbersOf(value) {
517
+ if (typeof value === "number")
518
+ return [value];
519
+ if (typeof value === "boolean")
520
+ return [value ? 1 : 0];
521
+ if (Array.isArray(value))
522
+ return value.flatMap(entry => numbersOf(entry));
523
+ if (value && typeof value === "object") {
524
+ if ("matrix" in value)
525
+ return numbersOf(value.matrix);
526
+ if ("elements" in value)
527
+ return numbersOf(value.elements);
528
+ }
529
+ return [];
530
+ }
531
+ /** Writes a value of `type` into words, as `loadCode` reads it back. */
532
+ function packValue(type, value, out, floats, ints, at, textures) {
533
+ const scalar = (kind, raw, index) => {
534
+ // A texture becomes its place in the table, whatever integer type the sampler was rewritten to.
535
+ if (raw && typeof raw === "object" && !Array.isArray(raw) && typeof raw.texture === "string") {
536
+ out[index] = textures.get(raw.texture) ?? NO_TEXTURE;
537
+ return;
538
+ }
539
+ const number = typeof raw === "boolean" ? Number(raw) : typeof raw === "number" ? raw : 0;
540
+ if (kind === "f32")
541
+ floats[index] = number;
542
+ else if (kind === "i32")
543
+ ints[index] = Math.trunc(number);
544
+ else if (kind === "bool")
545
+ out[index] = number ? 1 : 0;
546
+ else
547
+ out[index] = Math.max(0, Math.trunc(number)) >>> 0;
548
+ };
549
+ switch (type.kind) {
550
+ case "scalar":
551
+ scalar(type.scalar, Array.isArray(value) ? value[0] : value, at);
552
+ break;
553
+ case "vector": {
554
+ const numbers = typeof value === "number" ? Array(type.size).fill(value) : numbersOf(value);
555
+ for (let k = 0; k < type.size; k++)
556
+ scalar(type.scalar, numbers[k] ?? 0, at + k);
557
+ break;
558
+ }
559
+ case "matrix": {
560
+ const numbers = numbersOf(value);
561
+ const sourceSize = Math.round(Math.sqrt(numbers.length));
562
+ // Three hands mat3 uniforms over as nine numbers and mat4 as sixteen, both column-major.
563
+ for (let column = 0; column < type.columns; column++)
564
+ for (let row = 0; row < type.rows; row++) {
565
+ const square = sourceSize * sourceSize === numbers.length;
566
+ const index = square ? column * sourceSize + row : column * type.rows + row;
567
+ floats[at + column * type.rows + row] = numbers[index] ?? (column === row ? 1 : 0);
568
+ }
569
+ break;
570
+ }
571
+ case "array": {
572
+ const stride = wordsOf(type.element);
573
+ const flat = !Array.isArray(value) ? [] : value;
574
+ // A float array may arrive flat (Float32Array becomes numbers) for vector elements.
575
+ const elements = type.element.kind !== "scalar" && flat.every(entry => typeof entry === "number")
576
+ ? Array.from({ length: type.count }, (_, k) => flat.slice(k * stride, (k + 1) * stride))
577
+ : flat;
578
+ for (let k = 0; k < type.count; k++)
579
+ packValue(type.element, elements[k], out, floats, ints, at + k * stride, textures);
580
+ break;
581
+ }
582
+ case "struct": {
583
+ const record = value && typeof value === "object" && !Array.isArray(value) ? value : {};
584
+ let offset = 0;
585
+ for (const member of type.members) {
586
+ packValue(member.type, record[member.name], out, floats, ints, at + offset, textures);
587
+ offset += wordsOf(member.type);
588
+ }
589
+ break;
590
+ }
591
+ }
592
+ }
593
+ /** The kernel's implementation of each texture stand-in, chosen by its name and the types of its parameters. */
594
+ function textureImplementation(base, params) {
595
+ const sampler = params[0] ?? "";
596
+ const unsupported = "return vec4<f32>(0.0);";
597
+ if (base === "tp_texSize")
598
+ return `return shaderTextureSize(u32(${"s"}));`;
599
+ if (base === "tp_texelFetch")
600
+ return "return shaderTexelFetch(s, a1);";
601
+ if (sampler.startsWith("vec2"))
602
+ return unsupported;
603
+ if (base === "tp_texProj")
604
+ return params[1]?.startsWith("vec4") ? "return shaderSample2D(s, a1.xy / a1.w);" : "return shaderSample2D(s, a1.xy / a1.z);";
605
+ if (sampler === "i32")
606
+ return "return shaderSampleCube(u32(s), a1);";
607
+ return "return shaderSample2D(s, a1);";
608
+ }
609
+ let naga = null;
610
+ /** naga is only loaded for a scene that has shaders; its WebAssembly has to be instantiated once in a browser. */
611
+ async function loadNaga() {
612
+ naga ??= import("naga-wasm").then(async (module) => {
613
+ // In Node the module instantiates itself on import; in a browser, init() fetches the binary next to it.
614
+ if (typeof window !== "undefined")
615
+ await module.init();
616
+ return module;
617
+ });
618
+ return naga;
619
+ }
620
+ /** naga's GLSL to WGSL, with its own error text on failure. */
621
+ async function translate(source) {
622
+ const module = await loadNaga();
623
+ const parsed = module.parseGlsl(source, { stage: "fragment" });
624
+ try {
625
+ const info = module.validate(parsed);
626
+ try {
627
+ return module.writeWgsl(parsed, info);
628
+ }
629
+ finally {
630
+ info.free();
631
+ }
632
+ }
633
+ finally {
634
+ parsed.free();
635
+ }
636
+ }
637
+ /** Builds a program from naga's WGSL: renamed, its stand-ins replaced, with a loader, its varyings and an entry. */
638
+ export function buildProgram(id, kind, wgsl, prepared) {
639
+ const prefix = `p${id}_`;
640
+ const items = splitWgsl(wgsl).filter(item => item.kind !== "entry");
641
+ const names = new Map();
642
+ for (const item of items)
643
+ if (item.name)
644
+ names.set(item.name, prefix + item.name);
645
+ const structSource = new Map();
646
+ for (const item of items.filter(entry => entry.kind === "struct")) {
647
+ const body = item.text.slice(item.text.indexOf("{") + 1, item.text.lastIndexOf("}"));
648
+ const members = splitTopLevel(body.replace(/@\w+(\([^)]*\))?/g, "")).map(member => {
649
+ const colon = member.indexOf(":");
650
+ return { name: member.slice(0, colon).trim(), type: member.slice(colon + 1).trim() };
651
+ });
652
+ structSource.set(item.name, members);
653
+ }
654
+ const globals = new Map(items.filter(item => item.kind === "var").map(item => [item.name, item.type]));
655
+ /** naga keeps a global's name unless it collides with a WGSL keyword, in which case it suffixes it. */
656
+ const globalNamed = (name) => {
657
+ if (globals.has(name))
658
+ return name;
659
+ // naga's namer suffixes a global whose name a parameter or a keyword already took: name_, name_1, name_2...
660
+ const pattern = new RegExp(`^${name}_\\d*$`);
661
+ for (const candidate of globals.keys())
662
+ if (pattern.test(candidate))
663
+ return candidate;
664
+ return null;
665
+ };
666
+ const renamed = renameItems(items, names);
667
+ // The stand-ins: their WGSL bodies are replaced by calls into the kernel.
668
+ const stand = /^tp_(tex|texLod|texGrad|texProj|texelFetch|texSize)(_\d+)?$/;
669
+ const output = [];
670
+ renamed.forEach((item, index) => {
671
+ const original = items[index];
672
+ if (original.kind === "fn" && stand.test(original.name)) {
673
+ const base = `tp_${stand.exec(original.name)?.[1]}`;
674
+ const params = original.params ?? [];
675
+ const signature = params.map((type, k) => `${k === 0 ? "s" : `a${k}`}: ${type}`).join(", ");
676
+ const returns = base === "tp_texSize" ? "vec2<i32>" : "vec4<f32>";
677
+ output.push(`fn ${item.name}(${signature}) -> ${returns} {\n ${textureImplementation(base, params)}\n}`);
678
+ return;
679
+ }
680
+ output.push(item.text);
681
+ });
682
+ // The uniform loader.
683
+ const uniformTypes = [];
684
+ for (const name of prepared.uniforms) {
685
+ const global = globalNamed(name);
686
+ const type = global ? parseType(globals.get(global), structSource) : null;
687
+ if (global && type)
688
+ uniformTypes.push({ global, name, type });
689
+ }
690
+ // The shader prelude's built-ins are uniforms too, filled the same way.
691
+ for (const name of ["viewMatrix", "cameraPosition", "isOrthographic"]) {
692
+ const global = globalNamed(name);
693
+ if (global && !uniformTypes.some(entry => entry.global === global)) {
694
+ const type = parseType(globals.get(global), structSource);
695
+ if (type)
696
+ uniformTypes.push({ global, name, type });
697
+ }
698
+ }
699
+ let loader = `fn ${prefix}load(data: u32) {\n`;
700
+ let words = 0;
701
+ for (const uniform of uniformTypes) {
702
+ loader += loadCode(prefix + uniform.global, uniform.type, `${words}u`, " ");
703
+ words += wordsOf(uniform.type);
704
+ }
705
+ loader += "}\n";
706
+ // Varyings, laid out per corner in declaration order.
707
+ const varyings = [];
708
+ let stride = 0;
709
+ let varyingCode = "";
710
+ const sourceVaryings = kind === "foreground" ? [] : prepared.varyings;
711
+ for (const varying of sourceVaryings) {
712
+ const global = globalNamed(varying.name);
713
+ const type = global ? parseType(globals.get(global), structSource) : null;
714
+ if (!global || !type || (type.kind !== "scalar" && type.kind !== "vector"))
715
+ continue;
716
+ const size = type.kind === "scalar" ? 1 : type.size;
717
+ const scalar = type.scalar;
718
+ const flat = varying.flat || scalar !== "f32";
719
+ varyings.push({ name: varying.name, size, offset: stride, flat });
720
+ const component = (k) => {
721
+ const read = `shaderVarying(at, ${stride + k}u, bary, ${flat ? "true" : "false"})`;
722
+ return scalar === "f32" ? read : scalar === "bool" ? `(${read} != 0.0)` : `${scalar}(round(${read}))`;
723
+ };
724
+ const value = size === 1 ? component(0)
725
+ : `vec${size}<${scalar}>(${Array.from({ length: size }, (_, k) => component(k)).join(", ")})`;
726
+ varyingCode += ` ${prefix}${global} = ${value};\n`;
727
+ stride += size;
728
+ }
729
+ const set = (name, value) => {
730
+ const global = globalNamed(name);
731
+ return global ? ` ${prefix}${global} = ${value};\n` : "";
732
+ };
733
+ const usesDerivatives = prepared.derivativeSites > 0;
734
+ const outputName = prepared.outputs.map(globalNamed).find((name) => !!name) ?? globalNamed("tp_FragColor");
735
+ let setup = `fn ${prefix}setup(input: ShaderInput, lane: i32) {\n`;
736
+ setup += set("tp_lane", "lane");
737
+ setup += " let bary = select(select(input.bary, input.baryDy, lane == 2), input.baryDx, lane == 1);\n";
738
+ setup += " var coord = input.fragCoord;\n if (lane == 1) { coord.x = coord.x + 1.0; }\n"
739
+ + " if (lane == 2) { coord.y = coord.y + 1.0; }\n";
740
+ setup += set("tp_FragCoord", "coord");
741
+ setup += set("tp_FrontFacing", "input.frontFacing");
742
+ setup += set("tp_PointCoord", "input.pointCoord");
743
+ setup += set("tp_discarded", "false");
744
+ // After <opaque_fragment>, gl_FragColor already holds the surface's colour.
745
+ if (outputName)
746
+ setup += ` ${prefix}${outputName} = ${kind === "after" ? "input.fragColor" : "vec4<f32>(0.0, 0.0, 0.0, 1.0)"};\n`;
747
+ if (kind === "foreground") {
748
+ setup += set("vDirection", "input.direction");
749
+ }
750
+ else if (varyingCode) {
751
+ setup += " let at = shaderVaryingAddress(input.material, input.triangle);\n" + varyingCode;
752
+ }
753
+ if (kind === "patched" || kind === "after") {
754
+ setup += set("diffuseColor", "input.diffuseColor");
755
+ setup += set("normal", "input.normal");
756
+ setup += set("roughnessFactor", "input.roughness");
757
+ setup += set("metalnessFactor", "input.metalness");
758
+ setup += set("totalEmissiveRadiance", "input.emissive");
759
+ setup += set("vViewPosition", "input.viewPosition");
760
+ setup += set("vUv", "input.uv");
761
+ setup += set("outgoingLight", "input.diffuseColor.rgb");
762
+ setup += set("geometryNormal", "input.geometryNormal");
763
+ setup += set("nonPerturbedNormal", "input.geometryNormal");
764
+ setup += set("geometryPosition", "-input.viewPosition");
765
+ setup += set("geometryViewDir", "normalize(input.viewPosition)");
766
+ setup += set("faceDirection", "select(-1.0, 1.0, input.frontFacing)");
767
+ setup += set("vNormal", "input.geometryNormal");
768
+ }
769
+ setup += "}\n";
770
+ // naga renames GLSL's main to main_1 and gives the name to its entry point, which was dropped above.
771
+ const functions = new Set(items.filter(item => item.kind === "fn").map(item => item.name));
772
+ const main = names.get(functions.has("main_1") ? "main_1" : "main");
773
+ if (!main || !functions.has(functions.has("main_1") ? "main_1" : "main"))
774
+ throw new Error("GPU renderer: the translated shader has no main function.");
775
+ let run = `fn ${prefix}run(input: ShaderInput) -> ShaderOutput {\n ${prefix}load(input.data);\n`;
776
+ if (usesDerivatives)
777
+ run += ` ${prefix}setup(input, 1);\n ${main}();\n ${prefix}setup(input, 2);\n ${main}();\n`;
778
+ run += ` ${prefix}setup(input, 0);\n ${main}();\n var result = noShaderOutput(input);\n`;
779
+ if (outputName)
780
+ run += ` result.color = ${prefix}${outputName};\n`;
781
+ run += globalNamed("tp_discarded") ? ` result.discarded = ${prefix}${globalNamed("tp_discarded")};\n` : "";
782
+ if (kind === "patched") {
783
+ const read = (field, name) => (globalNamed(name) ? ` result.${field} = ${prefix}${globalNamed(name)};\n` : "");
784
+ run += read("diffuseColor", "diffuseColor") + read("normal", "normal") + read("roughness", "roughnessFactor")
785
+ + read("metalness", "metalnessFactor") + read("emissive", "totalEmissiveRadiance");
786
+ }
787
+ run += " return result;\n}\n";
788
+ const totalWords = words;
789
+ return {
790
+ id,
791
+ kind,
792
+ wgsl: `// program ${id} (${kind})\n${output.join("\n\n")}\n\n${loader}\n${setup}\n${run}`,
793
+ varyings,
794
+ varyingStride: stride,
795
+ usesDerivatives,
796
+ uniformWords: totalWords,
797
+ packUniforms(values, textures) {
798
+ const out = new Uint32Array(Math.max(1, totalWords));
799
+ const floats = new Float32Array(out.buffer);
800
+ const ints = new Int32Array(out.buffer);
801
+ let at = 0;
802
+ for (const uniform of uniformTypes) {
803
+ packValue(uniform.type, values[uniform.name], out, floats, ints, at, textures);
804
+ at += wordsOf(uniform.type);
805
+ }
806
+ return out;
807
+ },
808
+ };
809
+ }
810
+ /** Translates one fragment shader into a program, or explains why it could not be. */
811
+ export async function compileProgram(id, kind, shader, raw = false) {
812
+ const prepared = prepareFragmentShader(shader, kind, { raw });
813
+ let wgsl;
814
+ try {
815
+ wgsl = await translate(prepared.source);
816
+ }
817
+ catch (error) {
818
+ // WGSL has no fallthrough. Only then are the shader's switches rewritten, so every other shader keeps naga's own.
819
+ if (!/fall-through/.test(error.formatted ?? error.message))
820
+ throw error;
821
+ prepared.source = rewriteSwitches(prepared.source);
822
+ wgsl = await translate(prepared.source);
823
+ }
824
+ return buildProgram(id, kind, wgsl, prepared);
825
+ }
826
+ /** Where the bracket opened at `open` closes, counting the same kind of bracket. */
827
+ function closing(source, open) {
828
+ const pair = { "(": ")", "{": "}" };
829
+ const opener = source[open], closer = pair[opener];
830
+ let depth = 0;
831
+ for (let i = open; i < source.length; i++) {
832
+ if (source[i] === opener)
833
+ depth++;
834
+ if (source[i] === closer && --depth === 0)
835
+ return i;
836
+ }
837
+ return -1;
838
+ }
839
+ let switchCount = 0;
840
+ /**
841
+ * GLSL switches turned into what WGSL can say, for a shader whose cases fall through: one pass of a loop, so `break`
842
+ * still leaves it, and each case an `if` that is entered when its label matches or the case before it fell through.
843
+ * `default` is entered when no label matches. A `continue` inside such a case leaves the switch instead of continuing
844
+ * the loop around it, which is the one thing this does not keep.
845
+ */
846
+ export function rewriteSwitches(source) {
847
+ const pattern = /\bswitch\s*\(/g;
848
+ let match;
849
+ while ((match = pattern.exec(source))) {
850
+ const open = source.indexOf("(", match.index);
851
+ const close = closing(source, open);
852
+ const brace = source.indexOf("{", close);
853
+ const end = closing(source, brace);
854
+ if (close < 0 || brace < 0 || end < 0)
855
+ break;
856
+ const selector = source.slice(open + 1, close);
857
+ const body = rewriteSwitches(source.slice(brace + 1, end));
858
+ const labels = [];
859
+ const label = /\b(case\s+([^:]+?)|default)\s*:/g;
860
+ let found;
861
+ while ((found = label.exec(body))) {
862
+ // Only labels of this switch: at its own nesting depth, not inside a block or a call.
863
+ const before = body.slice(0, found.index);
864
+ const depth = (before.match(/[{(]/g)?.length ?? 0) - (before.match(/[})]/g)?.length ?? 0);
865
+ if (depth === 0)
866
+ labels.push({ at: found.index, length: found[0].length, value: found[2] ?? null });
867
+ }
868
+ const n = switchCount++;
869
+ const values = labels.filter(entry => entry.value !== null).map(entry => `tp_sv${n} == int(${entry.value})`);
870
+ let rewritten = `for (int tp_sw${n} = 0; tp_sw${n} < 1; tp_sw${n}++) {\n`
871
+ + `int tp_sv${n} = int(${selector});\nbool tp_sf${n} = false;\n`;
872
+ labels.forEach((entry, k) => {
873
+ const statements = body.slice(entry.at + entry.length, labels[k + 1]?.at ?? body.length);
874
+ const condition = entry.value !== null
875
+ ? `tp_sv${n} == int(${entry.value})`
876
+ : `!(${values.join(" || ") || "false"})`;
877
+ rewritten += `if (tp_sf${n} || ${condition}) { tp_sf${n} = true;\n${statements}\n}\n`;
878
+ });
879
+ rewritten += "}";
880
+ source = source.slice(0, match.index) + rewritten + source.slice(end + 1);
881
+ pattern.lastIndex = match.index + rewritten.length;
882
+ }
883
+ return source;
884
+ }
885
+ /** The CPU renderer's foreground wrapper, with the sky texture it samples as a plain 2D lookup. */
886
+ export function foregroundSource(source) {
887
+ const replaced = source.replace(/\btextureCube\s*\(\s*uSkyMap\s*,/g, "cpuSkyMap(");
888
+ return `
889
+ varying vec3 vDirection;
890
+ uniform sampler2D uCpuSky;
891
+ vec4 cpuSkyMap(vec3 direction) {
892
+ vec3 d = normalize(direction);
893
+ return texture2D(uCpuSky, vec2(atan(d.z, d.x) / 6.283185307179586 + 0.5, acos(clamp(d.y, -1.0, 1.0)) / 3.141592653589793));
894
+ }
895
+ ${replaced}
896
+ void main() { gl_FragColor = vec4(skyForeground(normalize(vDirection)), 1.0); }
897
+ `;
898
+ }
899
+ const NO_PROGRAMS = {
900
+ programs: [],
901
+ fragment: () => null,
902
+ vertex: () => null,
903
+ after: () => null,
904
+ foreground: null,
905
+ failures: [],
906
+ key: "",
907
+ };
908
+ /**
909
+ * Translates every shader a scene uses. A shader that fails to translate is reported and its material falls back to
910
+ * its colour, as before this existed, rather than failing the frame.
911
+ */
912
+ export async function compileShaderPrograms(scene, foreground) {
913
+ const materials = new Set();
914
+ for (const mesh of scene.meshes)
915
+ for (const material of mesh.materials)
916
+ if (material?.shader)
917
+ materials.add(material);
918
+ if (!materials.size && !foreground)
919
+ return NO_PROGRAMS;
920
+ const programs = [];
921
+ const byKey = new Map();
922
+ // Keyed by what decides the program, not by the material object: a material copied to bind other uniforms (a
923
+ // planar capture's, for one) runs the same shaders.
924
+ const fragments = new Map();
925
+ const vertices = new Map();
926
+ const afters = new Map();
927
+ const vertexByKey = new Map();
928
+ const failures = [];
929
+ const counts = lightCounts(scene);
930
+ const programFor = (key, compile, label) => {
931
+ let pending = byKey.get(key);
932
+ if (!pending) {
933
+ const id = byKey.size;
934
+ pending = compile(id).then(program => {
935
+ programs[id] = program;
936
+ return program;
937
+ }, (error) => {
938
+ failures.push(`${label}: ${error.formatted ?? error.message ?? error}`);
939
+ return null;
940
+ });
941
+ byKey.set(key, pending);
942
+ }
943
+ return pending;
944
+ };
945
+ for (const material of materials) {
946
+ const shader = material.shader;
947
+ const kind = shader.patched ? "patched" : "surface";
948
+ const raw = material.type === "RawShaderMaterial";
949
+ const defines = { ...shader.defines, ...counts };
950
+ const key = `${kind}|${raw}|${JSON.stringify(defines)}|${shader.fragment}`;
951
+ const program = await programFor(key, id => compileProgram(id, kind, { fragment: shader.fragment, defines }, raw), material.name);
952
+ if (program)
953
+ fragments.set(shaderKey(material), program);
954
+ // The CPU renderer's snapshot keeps what a hook does after <opaque_fragment> as a second shader, often empty.
955
+ const after = shader.patched ? shader.fragmentAfter : undefined;
956
+ if (after && hasStatements(after)) {
957
+ const afterProgram = await programFor(`after|${JSON.stringify(defines)}|${after}`, id => compileProgram(id, "after", { fragment: after, defines }), `${material.name} (after opaque)`);
958
+ if (afterProgram)
959
+ afters.set(shaderKey(material), afterProgram);
960
+ }
961
+ const vertexKey = `${JSON.stringify(defines)}|${shader.vertex}`;
962
+ try {
963
+ let vertex = vertexByKey.get(vertexKey);
964
+ if (!vertex) {
965
+ vertex = new CpuShader(shader.vertex, defines, SHADER_CHUNKS);
966
+ vertexByKey.set(vertexKey, vertex);
967
+ }
968
+ vertices.set(shaderKey(material), vertex);
969
+ }
970
+ catch (error) {
971
+ failures.push(`${material.name} (vertex): ${error.message}`);
972
+ }
973
+ }
974
+ const sky = foreground
975
+ ? await programFor(`foreground|${foreground}`, id => compileProgram(id, "foreground", { fragment: foregroundSource(foreground), defines: {} }, true), "sky foreground")
976
+ : null;
977
+ const compiled = programs.filter(Boolean);
978
+ for (const failure of failures)
979
+ console.warn(`GPU renderer: a shader could not be translated and renders as its colour. ${failure}`);
980
+ return {
981
+ programs: compiled,
982
+ fragment: material => (material.shader ? fragments.get(shaderKey(material)) ?? null : null),
983
+ vertex: material => (material.shader ? vertices.get(shaderKey(material)) ?? null : null),
984
+ after: material => (material.shader ? afters.get(shaderKey(material)) ?? null : null),
985
+ foreground: sky,
986
+ failures,
987
+ key: compiled.map(program => program.wgsl).join("\n"),
988
+ };
989
+ }
990
+ /** What decides a material's programs: its type and the source and defines of its shaders. */
991
+ function shaderKey(material) {
992
+ const shader = material.shader;
993
+ return JSON.stringify([material.type, shader.patched, shader.vertex, shader.fragment, shader.fragmentAfter ?? "",
994
+ shader.defines]);
995
+ }
996
+ /** Whether a shader's main does anything: a hook template with nothing added after a chunk is only empty lines. */
997
+ function hasStatements(source) {
998
+ const start = source.indexOf("void main()");
999
+ const open = start < 0 ? -1 : source.indexOf("{", start);
1000
+ if (open < 0)
1001
+ return false;
1002
+ const end = closing(source, open);
1003
+ return stripComments(source.slice(open + 1, end < 0 ? undefined : end)).replace(/#[^\n]*/g, "").trim().length > 0;
1004
+ }
1005
+ /** The light counts Three defines for a patched shader, as the CPU renderer passes them. */
1006
+ function lightCounts(scene) {
1007
+ return {
1008
+ NUM_DIR_LIGHTS: scene.lights.filter(light => light.type === "DirectionalLight").length,
1009
+ NUM_POINT_LIGHTS: scene.lights.filter(light => light.type === "PointLight").length,
1010
+ NUM_SPOT_LIGHTS: scene.lights.filter(light => light.type === "SpotLight").length,
1011
+ };
1012
+ }
1013
+ /** The WGSL every program relies on, and the dispatcher the surface code calls. */
1014
+ export function programsWgsl(programs) {
1015
+ const list = programs?.programs ?? [];
1016
+ const cases = list.map(program => ` case ${program.id}u: { return p${program.id}_run(input); }`).join("\n");
1017
+ return `${list.map(program => program.wgsl).join("\n\n")}
1018
+
1019
+ /** Runs the translated shader with this id; an unknown one leaves the surface as it was. */
1020
+ fn runProgram(id: u32, input: ShaderInput) -> ShaderOutput {
1021
+ switch id {
1022
+ ${cases}
1023
+ default: { }
1024
+ }
1025
+
1026
+ return noShaderOutput(input);
1027
+ }
1028
+ `;
1029
+ }
1030
+ //# sourceMappingURL=shaderProgram.js.map