@tinypoly/gpu-renderer 0.0.0-stage → 0.0.1

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