@meimu/mutracer 0.2.0 → 0.3.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 (211) hide show
  1. package/README.md +3 -0
  2. package/dist/assets/bvh.worker.js +1 -1
  3. package/dist/mutracer.umd.js +8949 -7915
  4. package/docs/api.md +42 -21
  5. package/docs/caustics.md +80 -0
  6. package/docs/materials.md +5 -4
  7. package/docs/progressive-rendering.md +50 -9
  8. package/docs/technical/continuous-spectrum.md +93 -0
  9. package/docs/updates.md +3 -1
  10. package/docs/viewer-checklist.md +65 -0
  11. package/docs/viewer.md +7 -5
  12. package/lib/Display.js +1 -1
  13. package/lib/Display.js.map +1 -1
  14. package/lib/Engine.d.ts +2 -0
  15. package/lib/Engine.js +47 -12
  16. package/lib/Engine.js.map +1 -1
  17. package/lib/Render.d.ts +61 -13
  18. package/lib/Render.js +169 -19
  19. package/lib/Render.js.map +1 -1
  20. package/lib/RenderLoop.d.ts +7 -3
  21. package/lib/RenderLoop.js +68 -44
  22. package/lib/RenderLoop.js.map +1 -1
  23. package/lib/main.d.ts +1 -1
  24. package/lib/main.js.map +1 -1
  25. package/lib/oidn.d.ts +1 -1
  26. package/lib/oidnBundledWeights.cdn.d.ts +2 -2
  27. package/lib/renderer/GPUTraceQueue.d.ts +2 -1
  28. package/lib/renderer/GPUTraceQueue.js +3 -1
  29. package/lib/renderer/GPUTraceQueue.js.map +1 -1
  30. package/lib/renderer/RenderingPipeline.d.ts +4 -0
  31. package/lib/renderer/RenderingPipeline.js.map +1 -1
  32. package/lib/renderer/WGPUCausticLightPass.d.ts +48 -0
  33. package/lib/renderer/WGPUCausticLightPass.js +217 -0
  34. package/lib/renderer/WGPUCausticLightPass.js.map +1 -0
  35. package/lib/renderer/WGPUEnvMapDistribution.js +18 -18
  36. package/lib/renderer/WGPUFullQuadPass.js +3 -3
  37. package/lib/renderer/WGPUPhotonMap.d.ts +37 -0
  38. package/lib/renderer/WGPUPhotonMap.js +191 -0
  39. package/lib/renderer/WGPUPhotonMap.js.map +1 -0
  40. package/lib/renderer/WGPURayTraceOutput.d.ts +13 -1
  41. package/lib/renderer/WGPURayTraceOutput.js +122 -28
  42. package/lib/renderer/WGPURayTraceOutput.js.map +1 -1
  43. package/lib/renderer/WGPURayTracePass.d.ts +48 -17
  44. package/lib/renderer/WGPURayTracePass.js +538 -340
  45. package/lib/renderer/WGPURayTracePass.js.map +1 -1
  46. package/lib/renderer/WGPURenderingPipeline.js +83 -47
  47. package/lib/renderer/WGPURenderingPipeline.js.map +1 -1
  48. package/lib/renderer/WebGLFullQuadPass.d.ts +2 -1
  49. package/lib/renderer/WebGLFullQuadPass.js +4 -0
  50. package/lib/renderer/WebGLFullQuadPass.js.map +1 -1
  51. package/lib/renderer/WebGLJNLMDenoiser.js +66 -66
  52. package/lib/renderer/WebGLRayTraceOutputPass.js +4 -4
  53. package/lib/renderer/WebGLRayTracePass.d.ts +6 -1
  54. package/lib/renderer/WebGLRayTracePass.js +43 -6
  55. package/lib/renderer/WebGLRayTracePass.js.map +1 -1
  56. package/lib/renderer/WebGLRenderingPipeline.js +10 -14
  57. package/lib/renderer/WebGLRenderingPipeline.js.map +1 -1
  58. package/lib/renderer/WebGLSkyBaker.js +60 -60
  59. package/lib/renderer/accumulationTarget.d.ts +2 -1
  60. package/lib/renderer/accumulationTarget.js.map +1 -1
  61. package/lib/renderer/causticDiagnostics.d.ts +31 -0
  62. package/lib/renderer/causticDiagnostics.js +5 -0
  63. package/lib/renderer/causticDiagnostics.js.map +1 -0
  64. package/lib/renderer/causticEmission.d.ts +10 -0
  65. package/lib/renderer/causticEmission.js +40 -0
  66. package/lib/renderer/causticEmission.js.map +1 -0
  67. package/lib/renderer/causticUniform.d.ts +12 -0
  68. package/lib/renderer/causticUniform.js +22 -0
  69. package/lib/renderer/causticUniform.js.map +1 -0
  70. package/lib/renderer/caustics.d.ts +16 -0
  71. package/lib/renderer/caustics.js +93 -0
  72. package/lib/renderer/caustics.js.map +1 -0
  73. package/lib/renderer/glsl/aovDisplay.frag.js +5 -5
  74. package/lib/renderer/glsl/aovReflectance.glsl.js +53 -53
  75. package/lib/renderer/glsl/aovReflectanceSamples.glsl.js +1 -1
  76. package/lib/renderer/glsl/aovTrace.frag.js +33 -33
  77. package/lib/renderer/glsl/chunks/cameraRay.glsl.js +9 -9
  78. package/lib/renderer/glsl/chunks/emissiveLight.glsl.d.ts +2 -0
  79. package/lib/renderer/glsl/chunks/emissiveLight.glsl.js +35 -0
  80. package/lib/renderer/glsl/chunks/emissiveLight.glsl.js.map +1 -0
  81. package/lib/renderer/glsl/chunks/envMap.glsl.js +73 -73
  82. package/lib/renderer/glsl/chunks/intersect.glsl.js +426 -428
  83. package/lib/renderer/glsl/chunks/intersect.glsl.js.map +1 -1
  84. package/lib/renderer/glsl/chunks/materialBuffer.glsl.d.ts +1 -0
  85. package/lib/renderer/glsl/chunks/materialBuffer.glsl.js +156 -153
  86. package/lib/renderer/glsl/chunks/materialBuffer.glsl.js.map +1 -1
  87. package/lib/renderer/glsl/chunks/openPbrBsdf.glsl.js +771 -771
  88. package/lib/renderer/glsl/chunks/rand.glsl.js +3 -3
  89. package/lib/renderer/glsl/chunks/random.glsl.js +6 -6
  90. package/lib/renderer/glsl/chunks/rayTraceCore.glsl.d.ts +1 -0
  91. package/lib/renderer/glsl/chunks/rayTraceCore.glsl.js +68 -63
  92. package/lib/renderer/glsl/chunks/rayTraceCore.glsl.js.map +1 -1
  93. package/lib/renderer/glsl/chunks/sampleLight.glsl.js +94 -76
  94. package/lib/renderer/glsl/chunks/sampleLight.glsl.js.map +1 -1
  95. package/lib/renderer/glsl/chunks/sampleMaterial.glsl.d.ts +1 -0
  96. package/lib/renderer/glsl/chunks/sampleMaterial.glsl.js +140 -117
  97. package/lib/renderer/glsl/chunks/sampleMaterial.glsl.js.map +1 -1
  98. package/lib/renderer/glsl/chunks/sampleShadowCatcher.glsl.js +14 -14
  99. package/lib/renderer/glsl/chunks/sampling.glsl.js +89 -92
  100. package/lib/renderer/glsl/chunks/sampling.glsl.js.map +1 -1
  101. package/lib/renderer/glsl/chunks/sobol.glsl.js +79 -79
  102. package/lib/renderer/glsl/chunks/textureLinear.glsl.js +5 -5
  103. package/lib/renderer/glsl/chunks/uvProjection.glsl.js +11 -11
  104. package/lib/renderer/glsl/depthQuery.frag.js +2 -2
  105. package/lib/renderer/glsl/normalizeRayTraceOutput.frag.js +10 -10
  106. package/lib/renderer/glsl/rayTrace.frag.d.ts +1 -1
  107. package/lib/renderer/glsl/rayTrace.frag.js +260 -237
  108. package/lib/renderer/glsl/rayTrace.frag.js.map +1 -1
  109. package/lib/renderer/glsl/shadowCatcher.frag.js +26 -26
  110. package/lib/renderer/glsl/shadowCatcher.frag.js.map +1 -1
  111. package/lib/renderer/glsl/webglPipeline.frag.js +59 -59
  112. package/lib/renderer/lightSampling.d.ts +3 -2
  113. package/lib/renderer/lightSampling.js +14 -8
  114. package/lib/renderer/lightSampling.js.map +1 -1
  115. package/lib/renderer/progressiveScheduler.js +4 -3
  116. package/lib/renderer/progressiveScheduler.js.map +1 -1
  117. package/lib/renderer/renderPipeline.d.ts +1 -0
  118. package/lib/renderer/renderPipeline.js.map +1 -1
  119. package/lib/renderer/researchTransport.d.ts +31 -0
  120. package/lib/renderer/researchTransport.js +5 -0
  121. package/lib/renderer/researchTransport.js.map +1 -0
  122. package/lib/renderer/sceneLayoutSchema.d.ts +1 -0
  123. package/lib/renderer/sceneLayoutSchema.js +2 -2
  124. package/lib/renderer/sceneLayoutSchema.js.map +1 -1
  125. package/lib/renderer/scratchSampling.js +6 -6
  126. package/lib/renderer/spectrum.d.ts +21 -0
  127. package/lib/renderer/spectrum.js +154 -0
  128. package/lib/renderer/spectrum.js.map +1 -0
  129. package/lib/renderer/webglLightSampling.d.ts +39 -0
  130. package/lib/renderer/webglLightSampling.js +15 -0
  131. package/lib/renderer/webglLightSampling.js.map +1 -0
  132. package/lib/renderer/wgpuBindlessSource.js +3 -3
  133. package/lib/renderer/wgpuShaderSource.d.ts +7 -0
  134. package/lib/renderer/wgpuShaderSource.js +2 -0
  135. package/lib/renderer/wgpuShaderSource.js.map +1 -1
  136. package/lib/renderer/wgsl/aovDisplay.wgsl.js +9 -9
  137. package/lib/renderer/wgsl/aovReflectance.wgsl.js +55 -55
  138. package/lib/renderer/wgsl/aovReflectanceSamples.wgsl.js +1 -1
  139. package/lib/renderer/wgsl/aovResolve.wgsl.js +2 -2
  140. package/lib/renderer/wgsl/aovSnapshot.wgsl.js +1 -1
  141. package/lib/renderer/wgsl/aovTrace.wgsl.js +33 -33
  142. package/lib/renderer/wgsl/bloom.wgsl.js +18 -18
  143. package/lib/renderer/wgsl/causticLight.compute.d.ts +6 -0
  144. package/lib/renderer/wgsl/causticLight.compute.js +537 -0
  145. package/lib/renderer/wgsl/causticLight.compute.js.map +1 -0
  146. package/lib/renderer/wgsl/chunks/background.wgsl.d.ts +2 -0
  147. package/lib/renderer/wgsl/chunks/background.wgsl.js +102 -0
  148. package/lib/renderer/wgsl/chunks/background.wgsl.js.map +1 -0
  149. package/lib/renderer/wgsl/chunks/causticAdjoint.wgsl.d.ts +2 -0
  150. package/lib/renderer/wgsl/chunks/causticAdjoint.wgsl.js +12 -0
  151. package/lib/renderer/wgsl/chunks/causticAdjoint.wgsl.js.map +1 -0
  152. package/lib/renderer/wgsl/chunks/causticAtomic.wgsl.d.ts +2 -0
  153. package/lib/renderer/wgsl/chunks/causticAtomic.wgsl.js +16 -0
  154. package/lib/renderer/wgsl/chunks/causticAtomic.wgsl.js.map +1 -0
  155. package/lib/renderer/wgsl/chunks/caustics.wgsl.d.ts +1 -0
  156. package/lib/renderer/wgsl/chunks/caustics.wgsl.js +62 -0
  157. package/lib/renderer/wgsl/chunks/caustics.wgsl.js.map +1 -0
  158. package/lib/renderer/wgsl/chunks/common.wgsl.js +4 -4
  159. package/lib/renderer/wgsl/chunks/envMap.wgsl.js +37 -37
  160. package/lib/renderer/wgsl/chunks/intersect.wgsl.js +598 -592
  161. package/lib/renderer/wgsl/chunks/intersect.wgsl.js.map +1 -1
  162. package/lib/renderer/wgsl/chunks/intersectionAlpha.wgsl.js +14 -14
  163. package/lib/renderer/wgsl/chunks/materialBuffer.wgsl.d.ts +2 -1
  164. package/lib/renderer/wgsl/chunks/materialBuffer.wgsl.js +75 -73
  165. package/lib/renderer/wgsl/chunks/materialBuffer.wgsl.js.map +1 -1
  166. package/lib/renderer/wgsl/chunks/openPbrBsdf.wgsl.js +794 -794
  167. package/lib/renderer/wgsl/chunks/pdm_displacement.wgsl.js +139 -139
  168. package/lib/renderer/wgsl/chunks/rand.wgsl.js +3 -3
  169. package/lib/renderer/wgsl/chunks/rayTraceCore.wgsl.d.ts +2 -1
  170. package/lib/renderer/wgsl/chunks/rayTraceCore.wgsl.js +35 -25
  171. package/lib/renderer/wgsl/chunks/rayTraceCore.wgsl.js.map +1 -1
  172. package/lib/renderer/wgsl/chunks/sampleLight.wgsl.d.ts +1 -1
  173. package/lib/renderer/wgsl/chunks/sampleLight.wgsl.js +142 -138
  174. package/lib/renderer/wgsl/chunks/sampleLight.wgsl.js.map +1 -1
  175. package/lib/renderer/wgsl/chunks/sampleMaterial.wgsl.d.ts +1 -1
  176. package/lib/renderer/wgsl/chunks/sampleMaterial.wgsl.js +204 -187
  177. package/lib/renderer/wgsl/chunks/sampleMaterial.wgsl.js.map +1 -1
  178. package/lib/renderer/wgsl/chunks/sampleShadowCatcher.wgsl.d.ts +1 -1
  179. package/lib/renderer/wgsl/chunks/sampleShadowCatcher.wgsl.js +11 -11
  180. package/lib/renderer/wgsl/chunks/sampleShadowCatcher.wgsl.js.map +1 -1
  181. package/lib/renderer/wgsl/chunks/sampling.wgsl.d.ts +1 -1
  182. package/lib/renderer/wgsl/chunks/sampling.wgsl.js +89 -93
  183. package/lib/renderer/wgsl/chunks/sampling.wgsl.js.map +1 -1
  184. package/lib/renderer/wgsl/chunks/sobol.wgsl.js +83 -83
  185. package/lib/renderer/wgsl/chunks/uvProjection.wgsl.js +11 -11
  186. package/lib/renderer/wgsl/compose.frag.wgsl.js +98 -98
  187. package/lib/renderer/wgsl/customDenoise.wgsl.js +10 -10
  188. package/lib/renderer/wgsl/depthQuery.wgsl.js +2 -2
  189. package/lib/renderer/wgsl/jnlm.compute.js +74 -74
  190. package/lib/renderer/wgsl/offlineAdaptive.compute.js +25 -25
  191. package/lib/renderer/wgsl/oidnAuxiliary.wgsl.js +3 -3
  192. package/lib/renderer/wgsl/photonMap.compute.d.ts +29 -0
  193. package/lib/renderer/wgsl/photonMap.compute.js +352 -0
  194. package/lib/renderer/wgsl/photonMap.compute.js.map +1 -0
  195. package/lib/renderer/wgsl/proceduralSky.compute.js +64 -64
  196. package/lib/renderer/wgsl/rayTrace.compute.d.ts +9 -1
  197. package/lib/renderer/wgsl/rayTrace.compute.js +448 -522
  198. package/lib/renderer/wgsl/rayTrace.compute.js.map +1 -1
  199. package/lib/scene/MaterialState.d.ts +2 -2
  200. package/lib/scene/MaterialState.js +2 -2
  201. package/lib/scene/MaterialState.js.map +1 -1
  202. package/lib/settings/render.d.ts +11 -0
  203. package/lib/settings/render.js +1 -0
  204. package/lib/settings/render.js.map +1 -1
  205. package/lib/util/fromTHREE.js +2 -2
  206. package/lib/util/fromTHREE.js.map +1 -1
  207. package/lib/version.d.ts +1 -1
  208. package/lib/version.js +1 -1
  209. package/lib/version.js.map +1 -1
  210. package/llms.txt +2 -1
  211. package/package.json +3 -3
@@ -1,3 +1,11 @@
1
+ import { causticDiagnostics } from './causticDiagnostics.js';
2
+ import { WGPUPhotonMap, photonRadius } from './WGPUPhotonMap.js';
3
+ import { defaultPhotonMapPhotons } from './wgsl/photonMap.compute.js';
4
+ import { researchTransport } from './researchTransport.js';
5
+ import { causticEmissionTable } from './causticEmission.js';
6
+ import { WGPUCausticLightPass } from './WGPUCausticLightPass.js';
7
+ import { causticLightCompute } from './wgsl/causticLight.compute.js';
8
+ import { causticCasterSpheres, hasStochasticCausticCaster, causticCasterSphere, isCausticSpecular } from './caustics.js';
1
9
  import { AovPrimitiveCache } from './AovPrimitiveCache.js';
2
10
  import { packAovScene } from './aovScene.js';
3
11
  import { aovMask } from './aov.js';
@@ -46,7 +54,7 @@ import { updateMeshInstances, updateMeshInstanceTransforms } from './prepareMesh
46
54
  import { buildTLAS, concatBLASBuffers, getTraversalStackSizes, refitBLAS, refitTLAS } from './bvhAccel.js';
47
55
  import { isTextureDataSource } from '../scene/Texture.js';
48
56
  import { DEFAULT_BEVEL_SAMPLES, DEFAULT_DIRECT_LIGHT_CANDIDATES, normalizeBevelSamples, normalizeDirectLightCandidates } from './RenderingPipeline.js';
49
- import { EmissiveTriangleCache, LightChoiceBuffer, estimateEnvironmentPower, } from './lightSampling.js';
57
+ import { EmissiveTriangleCache, LightChoiceBuffer, estimateEnvironmentPower } from './lightSampling.js';
50
58
  import { getOpenPbrEnergyLutData, OPENPBR_ENERGY_LUT_HEIGHT, OPENPBR_ENERGY_LUT_WIDTH } from './openPbrEnergyLut.js';
51
59
  import { offlineAdaptiveCompute } from './wgsl/offlineAdaptive.compute.js';
52
60
  import { normalizeOfflineAdaptiveSamplingOptions } from './OfflineAdaptiveSampling.js';
@@ -78,6 +86,10 @@ function joinWebGPUErrors(...errors) {
78
86
  .filter(Boolean))
79
87
  ].join('\n');
80
88
  }
89
+ const MAX_TRACE_VARIANTS = 32;
90
+ // Requests are cheap but drivers are not, and a running compilation cannot be cancelled.
91
+ const MAX_CONCURRENT_COMPILES = 2;
92
+ const MAX_QUEUED_COMPILES = 4;
81
93
  function shaderCompilationNow() {
82
94
  return typeof performance === 'undefined' ? Date.now() : performance.now();
83
95
  }
@@ -113,6 +125,16 @@ class WGPURayTracePass {
113
125
  _width = 0;
114
126
  _height = 0;
115
127
  _meshes = [];
128
+ _causticKey = '';
129
+ _photonMapping = false;
130
+ _photonMap;
131
+ _causticLight;
132
+ _causticSettings;
133
+ _causticPerspective = true;
134
+ _causticCameraScale = 1;
135
+ _causticBounds = [0, 0, 0, 0];
136
+ _transport;
137
+ _lightTracing = false;
116
138
  _sceneEnvironment = new SceneEnvironmentCache();
117
139
  _rectLightTextures = new RectLightTextureTracker();
118
140
  _analyticLights = [];
@@ -147,6 +169,8 @@ class WGPURayTracePass {
147
169
  if (!output)
148
170
  return;
149
171
  this._targetOutputs.delete(target);
172
+ this._causticLight?.release(target);
173
+ this._photonMap?.release(target);
150
174
  for (const buffer of output.buffers)
151
175
  buffer.destroy();
152
176
  if (this._activeTarget === target) {
@@ -155,17 +179,23 @@ class WGPURayTracePass {
155
179
  }
156
180
  }
157
181
  getTargetMemoryBytes(target) {
158
- return this._targetOutputs.get(target)?.buffers.reduce((sum, buffer) => sum + buffer.size, 0) ?? 0;
182
+ return ((this._targetOutputs.get(target)?.buffers.reduce((sum, buffer) => sum + buffer.size, 0) ?? 0) +
183
+ (this._causticLight?.bytes(target) ?? 0) +
184
+ (this._photonMap?.bytes(target) ?? 0));
159
185
  }
160
186
  useTarget(target) {
187
+ if (this._photonMapping && this._activeTarget !== target)
188
+ this._needsPhotonTargetBindGroups = true;
161
189
  this._activeTarget = target;
162
190
  const output = this._targetOutputs.get(target);
163
- [this._outColorBuffer, this._outNormalBuffer, this._outAlbedoBuffer, this._outShadowCatcherBuffer] = output?.buffers ?? [];
191
+ [this._outColorBuffer, this._outNormalBuffer, this._outAlbedoBuffer, this._outShadowCatcherBuffer] =
192
+ output?.buffers ?? [];
164
193
  if (output?.bindGroup && output.pipeline === this._rayTracePipeline && this._rayTraceBindGroups) {
165
194
  this._rayTraceBindGroups[this._bindless ? 2 : 3] = output.bindGroup;
195
+ this._needsUpdateOutputBindGroup = false;
166
196
  }
167
197
  else {
168
- this._needsUpdateBindGroups = true;
198
+ this._needsUpdateOutputBindGroup = true;
169
199
  }
170
200
  }
171
201
  _outColorBuffer;
@@ -212,22 +242,25 @@ class WGPURayTracePass {
212
242
  _skyBaker;
213
243
  _backgroundImageSource;
214
244
  _backgroundTex;
215
- _rayTracePipelineLayout;
216
245
  _rayTraceBindGroups;
246
+ _photonEmissionGroups;
217
247
  _blasLayoutMask = 1;
218
- _compiledBlasLayoutMask = 0;
219
- _compiledAnisotropyDirections = false;
220
- _compiledCoatNormals = false;
221
- _compiledHasAlpha = true;
222
- _compiledSceneHasAlpha = true;
223
248
  _alphaBlendStrategy = 'stochastic';
224
249
  _constants = initialSceneConstants();
225
250
  _retainedShaderCapabilities;
226
251
  _retainedShaderStructure;
227
252
  _shaderCapabilities = new WGPUShaderCapabilities();
228
- _prevPipelineConstants = {};
229
- _prevDefinesCode = '';
230
- _prevPipelineInputs;
253
+ _targetTransport = new WeakMap();
254
+ _traceVariants = new Map();
255
+ _queuedCompiles = [];
256
+ _activeCompiles = 0;
257
+ _traceModules = new Map();
258
+ _requestedVariant;
259
+ _activeVariant;
260
+ _activeInputs;
261
+ _awaitingRetirement;
262
+ _resourceError;
263
+ _aovPipelinesFailed = false;
231
264
  _hasTransmission = false;
232
265
  _hasShadowCatcher = false;
233
266
  _hasDisplacement = false;
@@ -242,19 +275,20 @@ class WGPURayTracePass {
242
275
  _apertureRadius = 0;
243
276
  _focusDistance = 10;
244
277
  _needsUpdateBindGroups = true;
245
- _computeShaderModule;
278
+ // Photon maps are per target. Switching targets only rebinds them, which
279
+ // must not send steady frames through the asynchronous pipeline path.
280
+ _needsPhotonTargetBindGroups = false;
281
+ // Target selection is not a scene-resource edit. A stale fallback must not
282
+ // leave global bindings dirty after switching back to a prepared target.
283
+ _needsUpdateOutputBindGroup = false;
246
284
  _aovPipelines = new WeakMap();
247
285
  _depthCompute;
248
286
  _depthTracePipeline;
249
287
  _depthPipeline;
250
288
  _shaderCache;
251
289
  _debugBVHBuffer;
252
- _isCompilingShader = false;
253
- _shaderCompilationDiagnostics;
254
- _pipelineError;
255
290
  // Fold scene invariants before backend inlining; runtime edits use uniforms.
256
291
  _bakePipelineConstants = true;
257
- _failedPipelineSignature;
258
292
  constructor(device, _textureSelection = { requested: 'atlas' }) {
259
293
  this._textureSelection = _textureSelection;
260
294
  this._device = device;
@@ -298,8 +332,14 @@ class WGPURayTracePass {
298
332
  device.queue.writeTexture({ texture: this._adaptiveSamplingTexture }, new Float32Array([Number.MAX_SAFE_INTEGER]), { bytesPerRow: 4 }, [1, 1]);
299
333
  }
300
334
  getOutputBuffer() {
335
+ // Another render may have traced since this target's samples were written.
336
+ const transport = this._targetTransport.get(this._activeTarget) ?? 0;
301
337
  return {
302
338
  color: this._outColorBuffer,
339
+ ...(transport & 2 || ((this._transport?.debugGroup ?? -1) >= 0 || causticDiagnostics.debugGroup >= 0) ? { wideHdr: true } : {}),
340
+ ...(transport & 1 && this._causticLight?.get(this._activeTarget)
341
+ ? { caustic: this._causticLight.get(this._activeTarget) }
342
+ : {}),
303
343
  normal: this._outNormalBuffer,
304
344
  albedo: this._outAlbedoBuffer,
305
345
  shadowCatcher: this._outShadowCatcherBuffer
@@ -494,12 +534,26 @@ class WGPURayTracePass {
494
534
  varianceBuffer.destroy();
495
535
  }
496
536
  }
497
- _createPipelineLayout() {
537
+ _createPipelineLayout(bindless, photonMapping) {
498
538
  const descriptor = {
499
539
  label: 'Ray Trace Pipeline Layout',
500
- bindGroupLayouts: (this._bindless ? bindlessBindGroupLayouts() : rayTraceBindGroupLayouts()).map(layout => this._shaderCache.createBindGroupLayout(layout))
540
+ bindGroupLayouts: (bindless ? bindlessBindGroupLayouts() : rayTraceBindGroupLayouts()).map((layout, index) => {
541
+ if (photonMapping && index === (bindless ? 0 : 1))
542
+ layout = {
543
+ ...layout,
544
+ entries: [
545
+ ...layout.entries,
546
+ ...[11, 12].map((binding) => ({
547
+ binding: binding + (bindless ? 4 : 0),
548
+ visibility: GPUShaderStage.COMPUTE,
549
+ texture: { sampleType: 'uint' }
550
+ }))
551
+ ]
552
+ };
553
+ return this._shaderCache.createBindGroupLayout(layout);
554
+ })
501
555
  };
502
- this._rayTracePipelineLayout = this._shaderCache.createPipelineLayout(this._bindless ? resourceTableLayout(descriptor) : descriptor);
556
+ return this._shaderCache.createPipelineLayout(bindless ? resourceTableLayout(descriptor) : descriptor);
503
557
  }
504
558
  _updateGPUBuffer(label, gpuBuffer, data, minSize = WGPU_SCENE_BUFFER_MIN_BYTES) {
505
559
  const device = this._device;
@@ -527,22 +581,46 @@ class WGPURayTracePass {
527
581
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
528
582
  });
529
583
  }
530
- async updatePipeline() {
531
- if (this._isCompilingShader) {
584
+ _preparePhotonTarget() {
585
+ if (!this._photonMapping)
532
586
  return;
587
+ this._photonMap ??= new WGPUPhotonMap(this._device);
588
+ const before = this._photonMap.get(this._activeTarget)?.data;
589
+ const requested = this._causticSettings?.photonMapping;
590
+ const n = typeof requested === 'object' ? requested.photonsPerSample : undefined;
591
+ const after = this._photonMap.ensure(this._activeTarget, Number.isFinite(n) && n > 0 ? n : defaultPhotonMapPhotons(this._width, this._height, causticDiagnostics.photonCountDivisor));
592
+ if (before !== after.data)
593
+ this._needsUpdateBindGroups = true;
594
+ }
595
+ /**
596
+ * Prepares the pipeline for the settings currently written to this pass.
597
+ * Everything except compilation runs synchronously, and compilation only
598
+ * writes to its own variant. Renders with different settings can therefore
599
+ * interleave without invalidating each other. Returns a promise while the
600
+ * requested state waits on the GPU; undefined means nothing is outstanding.
601
+ */
602
+ updatePipeline() {
603
+ if (this._disposed)
604
+ return;
605
+ if (this._needsUpdateMeshData || this._needsUpdateMeshMembership || this._pendingGeometryUpdates.size ||
606
+ this._needsRefitTLAS || this._dirtyColorMeshes.size || this._pendingMaterialUpdate || this._needsUpdateLightSamplingData) {
607
+ // A failure can have the same shader signature as its repaired scene.
608
+ // Retry after authored updates, while keeping an unchanged failure idle.
609
+ this._resourceError = undefined;
610
+ for (const [signature, variant] of this._traceVariants)
611
+ if (variant.status === 'failed')
612
+ this._traceVariants.delete(signature);
613
+ if (this._aovPipelinesFailed) {
614
+ this._aovPipelines = new WeakMap();
615
+ this._aovPipelinesFailed = false;
616
+ }
533
617
  }
534
- // Set this before any synchronous preparation. updatePipeline is called
535
- // without awaiting it, so a preparation error must not let createPass run
536
- // with an undefined pipeline in the same frame.
537
- this._isCompilingShader = true;
538
- let attemptedPipelineSignature;
539
- let compilationStartedAt;
540
- let pipelineCompilationStartedAt;
618
+ this._awaitingRetirement = this._bindless?.retirement;
619
+ if (this._awaitingRetirement)
620
+ return this._awaitingRetirement;
621
+ if (this._resourceError)
622
+ return;
541
623
  try {
542
- if (this._bindless?.retirement)
543
- await this._bindless.retirement;
544
- if (this._disposed)
545
- return;
546
624
  if (this._needsUpdateMeshData) {
547
625
  this._updateMeshData();
548
626
  }
@@ -557,29 +635,49 @@ class WGPURayTracePass {
557
635
  this._updateMeshColors();
558
636
  if (this._pendingMaterialUpdate)
559
637
  this._updateMaterialData();
638
+ this._setCaustics(this._causticSettings);
560
639
  if (this._needsUpdateLightSamplingData) {
561
640
  this._updateLightSamplingData();
562
641
  }
563
- const device = this._device;
564
- const pipelineInputs = JSON.stringify([this._constants, this._hasDisplacement,
565
- this._alphaBlendStrategy, this._blasLayoutMask, this._bakePipelineConstants]);
642
+ this._setCaustics(this._causticSettings);
643
+ if (this._lightTracing || this._photonMapping)
644
+ this._causticBounds = causticCasterSphere(this._meshes);
645
+ this._targetTransport.set(this._activeTarget, (+this._lightTracing) | (+this._photonMapping << 1));
646
+ this._preparePhotonTarget();
647
+ const pipelineInputs = JSON.stringify([
648
+ this._constants,
649
+ this._hasDisplacement,
650
+ this._alphaBlendStrategy,
651
+ this._blasLayoutMask,
652
+ this._bakePipelineConstants,
653
+ this._causticKey,
654
+ !!this._bindless
655
+ ]);
566
656
  // Steady rendering must not repeat capability selection and cache-key
567
657
  // preparation. Dirty resources still take the normal bind-group path.
568
- if (pipelineInputs === this._prevPipelineInputs && this._rayTracePipeline &&
569
- this._rayTraceBindGroups && !this._needsUpdateBindGroups)
658
+ if (pipelineInputs === this._activeInputs &&
659
+ this._rayTracePipeline &&
660
+ this._rayTraceBindGroups &&
661
+ !this._needsUpdateBindGroups) {
662
+ if (this._needsPhotonTargetBindGroups || this._needsUpdateOutputBindGroup) {
663
+ this._needsPhotonTargetBindGroups = false;
664
+ this._needsUpdateOutputBindGroup = false;
665
+ this._updateBindGroups();
666
+ }
667
+ this._requestedVariant = this._activeVariant;
570
668
  return;
669
+ }
571
670
  this._rtUniform.views.sceneFeatureFlags[0] = wgpuSceneFeatureFlags(this._constants);
572
671
  const requiredCapabilities = retainedShaderCapabilities(this._constants);
573
672
  const constants = { ...this._constants, ...requiredCapabilities };
574
- const hasAlpha = Boolean(constants.HAS_ALPHA_BLEND) &&
575
- this._alphaBlendStrategy === 'deterministic-source-over';
673
+ const hasAlpha = Boolean(constants.HAS_ALPHA_BLEND) && this._alphaBlendStrategy === 'deterministic-source-over';
576
674
  const hasSceneAlpha = Boolean(constants.HAS_ALPHA);
577
675
  const pipelineConstantsKeys = wgpuPipelineConstantKeys(constants);
578
- const defineCode = WGPU_SHADER_DEFINE_KEYS
579
- .map((key) => `const ${key} = ${constants[key]};`)
580
- .join('\n');
676
+ const defineCode = WGPU_SHADER_DEFINE_KEYS.map((key) => `const ${key} = ${constants[key]};`).join('\n');
581
677
  const structure = {
582
678
  hasDisplacement: this._hasDisplacement,
679
+ caustics: this._causticKey,
680
+ bindless: !!this._bindless,
583
681
  hasAlpha,
584
682
  hasSceneAlpha,
585
683
  hasAnisotropyDirections: Boolean(constants.USE_ANISOTROPY_DIRECTIONS),
@@ -588,10 +686,11 @@ class WGPURayTracePass {
588
686
  bakePipelineConstants: this._bakePipelineConstants
589
687
  };
590
688
  const structureKey = JSON.stringify({
591
- ...structure, defineCode,
689
+ ...structure,
690
+ defineCode,
592
691
  constants: pipelineConstantsKeys
593
- .filter(key => !(key in requiredCapabilities))
594
- .map(key => [key, constants[key]])
692
+ .filter((key) => !(key in requiredCapabilities))
693
+ .map((key) => [key, constants[key]])
595
694
  });
596
695
  // Prefer an existing compatible pipeline, including an earlier lean
597
696
  // variant. Only grow the current variant when it cannot cover an edit;
@@ -599,265 +698,255 @@ class WGPURayTracePass {
599
698
  const capabilities = this._shaderCapabilities.find(structureKey, requiredCapabilities) ??
600
699
  retainedShaderCapabilities(this._constants, structureKey === this._retainedShaderStructure ? this._retainedShaderCapabilities : undefined);
601
700
  Object.assign(constants, capabilities);
602
- const pipelineSignature = JSON.stringify({
701
+ // Runtime feature values live in uniforms, so edits can share a pipeline
702
+ // as long as its compiled defines and overrides are the same.
703
+ const signature = JSON.stringify({
603
704
  ...structure,
604
- constants: Object.keys(constants).sort().map(key => [key, constants[key]])
705
+ defineCode,
706
+ constants: pipelineConstantsKeys.map((key) => [key, constants[key]])
605
707
  });
606
- attemptedPipelineSignature = pipelineSignature;
607
- if (pipelineSignature === this._failedPipelineSignature) {
708
+ const variant = this._traceVariants.get(signature) ??
709
+ this._startTraceVariant(signature, constants, { hasAlpha, hasSceneAlpha, defineCode, pipelineConstantsKeys, structureKey, capabilities });
710
+ this._requestedVariant = variant;
711
+ if (variant.status === 'pending') {
712
+ this._requestQueuedCompile(variant);
713
+ return variant.settled;
714
+ }
715
+ if (variant.status === 'failed')
608
716
  return;
717
+ this._installTraceVariant(variant, pipelineInputs);
718
+ }
719
+ catch (error) {
720
+ this._resourceError = error;
721
+ this._needsUpdateBindGroups = true;
722
+ console.error('Failed to update WebGPU ray tracing resources.', error);
723
+ }
724
+ }
725
+ /** Runs only for a ready variant, without yielding, so tracing can start in this call. */
726
+ _installTraceVariant(variant, pipelineInputs) {
727
+ if (this._activeVariant !== variant) {
728
+ this._activeVariant = variant;
729
+ this._rayTracePipeline = variant.pipeline;
730
+ this._depthCompute = variant.depthCompute;
731
+ this._needsUpdateBindGroups = true;
732
+ }
733
+ const emission = () => causticEmissionTable(this._analyticLights, this._emissiveTriangles, this._causticBounds[3], this._envImageSource && 'sky' in this._envImageSource
734
+ ? proceduralEnvironmentPower(this._envImageSource)
735
+ : estimateEnvironmentPower(this._envImageSource));
736
+ const casters = () => causticDiagnostics.aggregateCasters ? [this._causticBounds] : causticCasterSpheres(this._meshes);
737
+ if (this._lightTracing && this._causticLight) {
738
+ this._causticLight.setDistribution(casters(), emission());
739
+ this._causticLight.use(variant.lightEmitter);
740
+ }
741
+ if (this._photonMapping && this._photonMap) {
742
+ this._photonMap.emission.setDistribution(casters(), emission());
743
+ this._photonMap.emission.use(variant.photonEmitter);
744
+ }
745
+ // TODO rebind when lights, materials, bvh changed.
746
+ if (this._needsUpdateBindGroups || this._needsPhotonTargetBindGroups || this._needsUpdateOutputBindGroup) {
747
+ this._needsUpdateBindGroups = false;
748
+ this._needsPhotonTargetBindGroups = false;
749
+ this._needsUpdateOutputBindGroup = false;
750
+ this._updateBindGroups();
751
+ }
752
+ this._retainedShaderCapabilities = variant.capabilities;
753
+ this._retainedShaderStructure = variant.structureKey;
754
+ this._shaderCapabilities.remember(variant.structureKey, variant.capabilities);
755
+ this._activeInputs = pipelineInputs;
756
+ }
757
+ /** Snapshots everything compilation needs. The task must not read mutable pass state afterwards. */
758
+ _startTraceVariant(signature, constants, shader) {
759
+ const bake = this._bakePipelineConstants, bindless = this._bindless;
760
+ const hasDisplacement = this._hasDisplacement, blasLayoutMask = this._blasLayoutMask;
761
+ const lightTracing = this._lightTracing, photonMapping = this._photonMapping;
762
+ const causticKey = this._causticKey, transport = this._transport;
763
+ const { hasAlpha, hasSceneAlpha, defineCode, pipelineConstantsKeys } = shader;
764
+ const emitterSource = (mode) => {
765
+ const code = causticLightCompute(constants, blasLayoutMask, hasDisplacement, mode, '', causticDiagnostics.fullEmissionRandom !== false);
766
+ return bindless ? bindlessShaderSource(code, this._textureSelection.support.enable) : code;
767
+ };
768
+ const lightEmitter = lightTracing ? this._causticLight ??= new WGPUCausticLightPass(this._device) : undefined;
769
+ const photonEmitter = photonMapping ? this._photonMap?.emission : undefined;
770
+ const pipelineDiagnostic = {
771
+ label: 'Ray Trace', entryPoint: 'main', firstFrameRequired: true, status: 'compiling',
772
+ startedAtMs: 0, concurrentCompilationCount: 1
773
+ };
774
+ const variant = {
775
+ status: 'pending', capabilities: shader.capabilities, structureKey: shader.structureKey,
776
+ settled: undefined,
777
+ diagnostics: {
778
+ status: 'compiling', sourceBytes: 0, sourceLines: 0, pipelineCount: 1,
779
+ completedPipelineCount: 0, sourceGenerationTimeMs: 0, moduleCreationTimeMs: 0,
780
+ modules: [], pipelines: [{ ...pipelineDiagnostic }], maximumConcurrentPipelineCount: 1, peakConcurrentPipelineCount: 1
609
781
  }
610
- this._pipelineError = undefined;
611
- const previousPipelineConstantsKeys = wgpuPipelineConstantKeys(this._prevPipelineConstants);
612
- const needsCreateShaderModule = !this._rayTracePipelineLayout ||
613
- !this._computeShaderModule ||
614
- pipelineConstantsKeys.join(',') !==
615
- previousPipelineConstantsKeys.join(',') ||
616
- defineCode !== this._prevDefinesCode ||
617
- hasAlpha !== this._compiledHasAlpha ||
618
- hasSceneAlpha !== this._compiledSceneHasAlpha ||
619
- Boolean(constants.USE_ANISOTROPY_DIRECTIONS) !== this._compiledAnisotropyDirections ||
620
- Boolean(constants.USE_COAT_NORMAL_FRAMES) !== this._compiledCoatNormals ||
621
- this._blasLayoutMask !== this._compiledBlasLayoutMask ||
622
- (this._bakePipelineConstants &&
623
- pipelineConstantsKeys.some((key) => constants[key] !== this._prevPipelineConstants[key]));
624
- const needsCreatePipeline = needsCreateShaderModule ||
625
- pipelineConstantsKeys.some((key) => constants[key] !== this._prevPipelineConstants[key]);
626
- compilationStartedAt = needsCreatePipeline
627
- ? shaderCompilationNow()
628
- : undefined;
629
- let sourceGenerationTimeMs = 0;
630
- let moduleCreationTimeMs = 0;
631
- let sourceBytes = this._shaderCompilationDiagnostics?.sourceBytes ?? 0;
632
- let sourceLines = this._shaderCompilationDiagnostics?.sourceLines ?? 0;
633
- let sourceFingerprint = this._shaderCompilationDiagnostics?.modules[0]?.sourceFingerprint ?? '';
634
- if (needsCreateShaderModule) {
635
- const sourceGenerationStartedAt = shaderCompilationNow();
636
- let rayTraceShaderCode = wgpuShaderSource(constants, {
637
- hasDisplacement: this._hasDisplacement, hasAlpha, hasSceneAlpha,
782
+ };
783
+ const device = this._device, groupIndices = bindless ? [0, 1] : [0, 1, 2];
784
+ let startedAt = 0;
785
+ const compile = async () => {
786
+ startedAt = shaderCompilationNow();
787
+ // Override values are not part of the WGSL unless they are baked, so a
788
+ // quality edit can share the module that the driver has already parsed.
789
+ const moduleKey = JSON.stringify([causticKey, !!bindless, hasAlpha, hasSceneAlpha, hasDisplacement,
790
+ constants.USE_ANISOTROPY_DIRECTIONS, constants.USE_COAT_NORMAL_FRAMES, blasLayoutMask, defineCode, pipelineConstantsKeys,
791
+ bake && pipelineConstantsKeys.map((key) => constants[key])]);
792
+ let source = this._traceModules.get(moduleKey);
793
+ if (!source) {
794
+ let code = wgpuShaderSource(constants, {
795
+ hasDisplacement,
796
+ hasAlpha,
797
+ hasSceneAlpha,
798
+ ...transport?.features,
799
+ lightTracing,
800
+ photonMapping,
801
+ photonMaxDepth: causticDiagnostics.photonMaxDepth ?? 20,
802
+ photonDirectSubset: causticDiagnostics.photonDirectSubset,
803
+ debugGroup: causticDiagnostics.debugGroup,
804
+ workgroupSize: transport?.workgroupSize ?? WORKGROUP_SIZE,
638
805
  hasAnisotropyDirections: Boolean(constants.USE_ANISOTROPY_DIRECTIONS),
639
806
  hasCoatNormals: Boolean(constants.USE_COAT_NORMAL_FRAMES),
640
- blasLayoutMask: this._blasLayoutMask,
807
+ blasLayoutMask,
641
808
  dynamicEnvironment: true
642
- }, this._bakePipelineConstants);
643
- if (this._bindless)
644
- rayTraceShaderCode = bindlessShaderSource(rayTraceShaderCode, this._textureSelection.support.enable);
645
- sourceBytes = rayTraceShaderCode.length;
646
- sourceLines = shaderSourceLineCount(rayTraceShaderCode);
647
- sourceFingerprint = shaderSourceFingerprint(rayTraceShaderCode);
648
- sourceGenerationTimeMs =
649
- shaderCompilationNow() - sourceGenerationStartedAt;
650
- const moduleCreationStartedAt = shaderCompilationNow();
651
- this._computeShaderModule = this._shaderCache.createShaderModule({
652
- label: 'Ray Trace',
653
- code: rayTraceShaderCode
654
- });
655
- moduleCreationTimeMs =
656
- shaderCompilationNow() - moduleCreationStartedAt;
657
- this._compiledHasAlpha = hasAlpha;
658
- this._compiledSceneHasAlpha = hasSceneAlpha;
659
- this._compiledAnisotropyDirections = Boolean(constants.USE_ANISOTROPY_DIRECTIONS);
660
- this._compiledCoatNormals = Boolean(constants.USE_COAT_NORMAL_FRAMES);
661
- this._compiledBlasLayoutMask = this._blasLayoutMask;
809
+ }, bake);
810
+ if (bindless)
811
+ code = bindlessShaderSource(code, this._textureSelection.support.enable);
812
+ variant.diagnostics.sourceGenerationTimeMs = shaderCompilationNow() - startedAt;
813
+ const moduleStartedAt = shaderCompilationNow();
814
+ source = {
815
+ module: this._shaderCache.createShaderModule({ label: 'Ray Trace', code }),
816
+ bytes: code.length, lines: shaderSourceLineCount(code), fingerprint: shaderSourceFingerprint(code)
817
+ };
818
+ variant.diagnostics.moduleCreationTimeMs = shaderCompilationNow() - moduleStartedAt;
819
+ this._traceModules.set(moduleKey, source);
820
+ if (this._traceModules.size > 8)
821
+ this._traceModules.delete(this._traceModules.keys().next().value);
662
822
  }
663
- if (needsCreatePipeline) {
664
- if (!this._rayTracePipelineLayout) {
665
- device.pushErrorScope('validation');
666
- let layoutScopeOpen = true;
667
- try {
668
- this._createPipelineLayout();
669
- const layoutError = await device.popErrorScope();
670
- layoutScopeOpen = false;
671
- if (layoutError)
672
- throw layoutError;
673
- }
674
- catch (error) {
675
- const scopedError = layoutScopeOpen
676
- ? await device.popErrorScope().catch(() => null)
677
- : null;
678
- this._rayTracePipelineLayout = undefined;
679
- const detail = joinWebGPUErrors(error, scopedError);
680
- throw new Error(`Unable to create the ray tracing pipeline layout: ${detail}`);
681
- }
682
- }
683
- // NOTE must before creating pipeline.
684
- // Because createComputePipelineAsync is async.
685
- // The constants may be changed after the pipeline is created(in setMesh).
686
- this._prevPipelineConstants = { ...constants };
687
- this._prevDefinesCode = defineCode;
688
- this._needsUpdateBindGroups = true;
689
- device.pushErrorScope('validation');
690
- let pipelineScopeOpen = true;
691
- try {
692
- const pipelineConstants = !this._bakePipelineConstants
693
- ? pipelineConstantsKeys.reduce((obj, key) => {
694
- obj[key] = +constants[key];
695
- return obj;
696
- }, {})
697
- : undefined;
698
- const diagnostic = {
699
- label: 'Ray Trace',
700
- entryPoint: 'main',
701
- firstFrameRequired: true,
702
- status: 'compiling'
703
- };
704
- pipelineCompilationStartedAt = shaderCompilationNow();
705
- diagnostic.startedAtMs = 0;
706
- diagnostic.concurrentCompilationCount = 1;
707
- this._shaderCompilationDiagnostics = {
708
- status: 'compiling',
709
- sourceBytes,
710
- sourceLines,
711
- pipelineCount: 1,
712
- completedPipelineCount: 0,
713
- sourceGenerationTimeMs,
714
- moduleCreationTimeMs,
715
- modules: [
716
- {
717
- label: 'Ray Trace',
718
- sourceBytes,
719
- sourceLines,
720
- sourceFingerprint
721
- }
722
- ],
723
- pipelines: [{ ...diagnostic }],
724
- maximumConcurrentPipelineCount: 1,
725
- peakConcurrentPipelineCount: 1
726
- };
727
- let rayTracePipeline;
728
- try {
729
- rayTracePipeline = await this._shaderCache.createComputePipelineAsync({
730
- label: 'Ray Trace',
731
- layout: this._rayTracePipelineLayout,
732
- compute: {
733
- module: this._computeShaderModule,
734
- entryPoint: 'main',
735
- constants: pipelineConstants
736
- }
737
- });
738
- diagnostic.status = 'ready';
739
- }
740
- catch (error) {
741
- diagnostic.status = 'failed';
742
- diagnostic.error = getWebGPUErrorMessage(error);
743
- // Pipeline creation already reports WGSL failures. Pull the much
744
- // heavier module diagnostics only on that failure path so normal
745
- // startup does not schedule a second full-module inspection.
746
- const info = await this._computeShaderModule
747
- .getCompilationInfo()
748
- .catch(() => undefined);
749
- const shaderErrors = (info?.messages ?? [])
750
- .filter((message) => message.type === 'error');
751
- if (shaderErrors.length) {
752
- throw new Error(shaderErrors
753
- .map((message) => `Ray Trace WGSL ${message.lineNum}:${message.linePos} ${message.message}`)
754
- .join('\n'));
755
- }
756
- throw error;
757
- }
758
- finally {
759
- diagnostic.endedAtMs =
760
- shaderCompilationNow() - pipelineCompilationStartedAt;
761
- diagnostic.durationMs = diagnostic.endedAtMs;
762
- }
763
- this._rayTracePipeline = rayTracePipeline;
764
- this._depthCompute = { module: this._computeShaderModule, constants: pipelineConstants };
765
- const pipelineError = await device.popErrorScope();
766
- pipelineScopeOpen = false;
767
- if (pipelineError)
768
- throw pipelineError;
769
- const compilationCompletedAt = shaderCompilationNow();
770
- this._shaderCompilationDiagnostics = {
771
- ...this._shaderCompilationDiagnostics,
772
- status: 'ready',
773
- completedPipelineCount: 1,
774
- pipelines: [{ ...diagnostic }],
775
- pipelineCompilationTimeMs: compilationCompletedAt - pipelineCompilationStartedAt,
776
- totalTimeMs: compilationCompletedAt - compilationStartedAt
777
- };
778
- }
779
- catch (error) {
780
- const scopedError = pipelineScopeOpen
781
- ? await device.popErrorScope().catch(() => null)
782
- : null;
783
- const detail = joinWebGPUErrors(error, scopedError);
784
- if (!this._bakePipelineConstants &&
785
- isWebGPUPipelineSpecializationFailure(error, scopedError)) {
786
- const failedAt = shaderCompilationNow();
787
- if (this._shaderCompilationDiagnostics?.status === 'compiling') {
788
- this._shaderCompilationDiagnostics = {
789
- ...this._shaderCompilationDiagnostics,
790
- status: 'failed',
791
- pipelineCompilationTimeMs: pipelineCompilationStartedAt == null
792
- ? undefined
793
- : failedAt - pipelineCompilationStartedAt,
794
- totalTimeMs: compilationStartedAt == null
795
- ? undefined
796
- : failedAt - compilationStartedAt,
797
- error: detail
798
- };
799
- }
800
- // WebKit can accept a large WGSL module but fail while translating
801
- // its specialization-constant variant to Metal. Baking the values
802
- // into WGSL lets its front end eliminate disabled feature branches
803
- // before creating the Metal library.
804
- this._bakePipelineConstants = true;
805
- this._rayTracePipeline = undefined;
806
- this._rayTraceBindGroups = undefined;
807
- this._computeShaderModule = undefined;
808
- this._prevPipelineConstants = {};
809
- console.warn('WebGPU pipeline specialization failed; retrying with constants baked into WGSL.');
810
- return;
811
- }
812
- throw new Error(detail);
823
+ Object.assign(variant.diagnostics, {
824
+ sourceBytes: source.bytes, sourceLines: source.lines,
825
+ modules: [{ label: 'Ray Trace', sourceBytes: source.bytes, sourceLines: source.lines, sourceFingerprint: source.fingerprint }]
826
+ });
827
+ const { module } = source;
828
+ const layout = this._createPipelineLayout(!!bindless, photonMapping);
829
+ const pipelineConstants = !bake
830
+ ? pipelineConstantsKeys.reduce((obj, key) => {
831
+ obj[key] = +constants[key];
832
+ return obj;
833
+ }, {})
834
+ : undefined;
835
+ const lightSource = lightEmitter && emitterSource('lightTracing');
836
+ const photonSource = photonEmitter && emitterSource('photonMapping');
837
+ // Scopes capture all device errors while open, so one must never span an
838
+ // await: frames presented meanwhile would be blamed on this pipeline.
839
+ device.pushErrorScope('validation');
840
+ const request = this._shaderCache.createComputePipelineAsync({
841
+ label: 'Ray Trace', layout,
842
+ compute: { module, entryPoint: 'main', constants: pipelineConstants }
843
+ });
844
+ const scope = device.popErrorScope().catch(() => null);
845
+ let pipeline;
846
+ try {
847
+ pipeline = await request;
848
+ const scopedError = await scope;
849
+ if (scopedError)
850
+ throw scopedError;
851
+ }
852
+ catch (error) {
853
+ const scopedError = await scope;
854
+ // Pipeline creation already reports WGSL failures. Pull the much
855
+ // heavier module diagnostics only on that failure path so normal
856
+ // startup does not schedule a second full-module inspection.
857
+ const info = await module.getCompilationInfo().catch(() => undefined);
858
+ const shaderErrors = (info?.messages ?? []).filter((message) => message.type === 'error');
859
+ if (shaderErrors.length) {
860
+ throw new Error(shaderErrors
861
+ .map((message) => `Ray Trace WGSL ${message.lineNum}:${message.linePos} ${message.message}`)
862
+ .join('\n'));
813
863
  }
864
+ if (isWebGPUPipelineSpecializationFailure(error, scopedError))
865
+ throw error;
866
+ throw new Error(joinWebGPUErrors(error, scopedError));
814
867
  }
815
- // Compilation can complete after Engine disposal/device loss has released
816
- // the material table and its textures. Never bind that retired generation.
868
+ const groups = lightEmitter || photonEmitter ? groupIndices.map((i) => pipeline.getBindGroupLayout(i)) : [];
869
+ const [light, photon] = await Promise.all([
870
+ lightEmitter?.prepare(lightSource, groups, !!bindless),
871
+ photonEmitter?.prepare(photonSource, groups, !!bindless)
872
+ ]);
873
+ Object.assign(variant, { pipeline, lightEmitter: light, photonEmitter: photon,
874
+ depthCompute: { module, constants: pipelineConstants } });
875
+ };
876
+ const finishDiagnostics = (status, error) => {
877
+ const totalTimeMs = shaderCompilationNow() - startedAt;
878
+ Object.assign(variant.diagnostics, {
879
+ status, completedPipelineCount: status === 'ready' ? 1 : 0, error,
880
+ pipelines: [{ ...pipelineDiagnostic, status, error, endedAtMs: totalTimeMs, durationMs: totalTimeMs }],
881
+ pipelineCompilationTimeMs: totalTimeMs, totalTimeMs
882
+ });
883
+ };
884
+ const run = () => compile().then(() => {
885
+ variant.status = 'ready';
886
+ finishDiagnostics('ready');
887
+ }, (error) => {
888
+ finishDiagnostics('failed', getWebGPUErrorMessage(error));
817
889
  if (this._disposed)
818
890
  return;
819
- // TODO rebind when lights, materials, bvh changed.
820
- if (this._needsUpdateBindGroups) {
821
- this._needsUpdateBindGroups = false;
822
- this._updateBindGroups();
891
+ if (!bake && isWebGPUPipelineSpecializationFailure(error)) {
892
+ // WebKit can accept a large WGSL module but fail while translating
893
+ // its specialization-constant variant to Metal. Baking the values
894
+ // into WGSL lets its front end eliminate disabled feature branches
895
+ // before creating the Metal library. The next request has a new
896
+ // signature, so this one stays pending until it is replaced.
897
+ this._bakePipelineConstants = true;
898
+ this._traceVariants.delete(signature);
899
+ console.warn('WebGPU pipeline specialization failed; retrying with constants baked into WGSL.');
900
+ return;
823
901
  }
824
- this._retainedShaderCapabilities = capabilities;
825
- this._retainedShaderStructure = structureKey;
826
- this._shaderCapabilities.remember(structureKey, capabilities);
827
- this._prevPipelineInputs = pipelineInputs;
828
- this._failedPipelineSignature = undefined;
829
- }
830
- catch (error) {
831
- if (this._disposed)
902
+ // Nobody needs an obsolete result, and a later request should try again.
903
+ if (this._requestedVariant !== variant) {
904
+ this._traceVariants.delete(signature);
905
+ variant.status = 'failed';
832
906
  return;
833
- this._pipelineError = error;
834
- const failedAt = shaderCompilationNow();
835
- if (this._shaderCompilationDiagnostics?.status === 'compiling') {
836
- this._shaderCompilationDiagnostics = {
837
- ...this._shaderCompilationDiagnostics,
838
- status: 'failed',
839
- pipelineCompilationTimeMs: pipelineCompilationStartedAt == null
840
- ? undefined
841
- : failedAt - pipelineCompilationStartedAt,
842
- totalTimeMs: compilationStartedAt == null
843
- ? undefined
844
- : failedAt - compilationStartedAt,
845
- error: getWebGPUErrorMessage(error)
846
- };
847
907
  }
848
- this._failedPipelineSignature = attemptedPipelineSignature;
849
- // Do not allow the render loop to submit the previous/partial state.
850
- // The failed signature prevents retrying the same shader every frame;
851
- // changing a pipeline-affecting setting permits a new attempt.
852
- this._rayTracePipeline = undefined;
853
- this._rayTraceBindGroups = undefined;
854
- this._prevPipelineConstants = {};
855
- this._needsUpdateBindGroups = true;
908
+ variant.status = 'failed';
909
+ variant.error = error;
856
910
  console.error('Failed to update WebGPU ray tracing pipeline.', error);
911
+ });
912
+ variant.settled = new Promise((resolve) => {
913
+ this._queuedCompiles.push({ variant, signature, run, release: resolve });
914
+ });
915
+ this._traceVariants.set(signature, variant);
916
+ if (this._queuedCompiles.length > MAX_QUEUED_COMPILES) {
917
+ // A dropped variant stays pending, because a render that still wants it
918
+ // replaces it with a new request on its next frame.
919
+ const obsolete = this._queuedCompiles.shift();
920
+ if (this._traceVariants.get(obsolete.signature) === obsolete.variant)
921
+ this._traceVariants.delete(obsolete.signature);
922
+ obsolete.release();
923
+ }
924
+ for (const [key, retained] of this._traceVariants) {
925
+ if (this._traceVariants.size <= MAX_TRACE_VARIANTS)
926
+ break;
927
+ if (retained.status !== 'pending' && retained !== this._activeVariant)
928
+ this._traceVariants.delete(key);
929
+ }
930
+ this._pumpCompiles();
931
+ return variant;
932
+ }
933
+ /** Runs the most recently requested variants first: a dragged slider's last value is the one in use. */
934
+ _pumpCompiles() {
935
+ while (this._activeCompiles < MAX_CONCURRENT_COMPILES && this._queuedCompiles.length) {
936
+ const job = this._queuedCompiles.pop();
937
+ this._activeCompiles++;
938
+ void job.run().finally(() => {
939
+ this._activeCompiles--;
940
+ job.release();
941
+ if (!this._disposed)
942
+ this._pumpCompiles();
943
+ });
857
944
  }
858
- finally {
859
- this._isCompilingShader = false;
860
- }
945
+ }
946
+ _requestQueuedCompile(variant) {
947
+ const index = this._queuedCompiles.findIndex((job) => job.variant === variant);
948
+ if (index >= 0)
949
+ this._queuedCompiles.push(...this._queuedCompiles.splice(index, 1));
861
950
  }
862
951
  _prepareAovPipelines(kinds) {
863
952
  const trace = this._rayTracePipeline;
@@ -890,7 +979,7 @@ class WGPURayTracePass {
890
979
  void Promise.all([
891
980
  this._shaderCache.createComputePipelineAsync({ label: 'AOV accumulation', layout, compute: { ...compute, entryPoint: 'aovTrace' } }),
892
981
  this._shaderCache.createComputePipelineAsync({ label: 'AOV center', layout, compute: { ...compute, entryPoint: 'aovTraceCenter' } })
893
- ]).then(([accumulation, center]) => { pending.accumulation = accumulation; pending.center = center; }, error => { pending.error = error; });
982
+ ]).then(([accumulation, center]) => { pending.accumulation = accumulation; pending.center = center; }, error => { pending.error = error; this._aovPipelinesFailed = true; });
894
983
  }
895
984
  if (pipelines.error)
896
985
  throw pipelines.error;
@@ -959,7 +1048,7 @@ class WGPURayTracePass {
959
1048
  const tracePipeline = this._rayTracePipeline;
960
1049
  const groups = this._rayTraceBindGroups;
961
1050
  const textures = this._bindless, textureRevision = textures?.revision;
962
- if (!tracePipeline || !groups || !this._depthCompute || this._isCompilingShader)
1051
+ if (!tracePipeline || !groups || !this._depthCompute || this.isCompilingShader())
963
1052
  return;
964
1053
  const packed = packDepthSampleArea(area, aspect ?? this._width / this._height);
965
1054
  if (this._depthTracePipeline !== tracePipeline || !this._depthPipeline) {
@@ -1023,15 +1112,17 @@ class WGPURayTracePass {
1023
1112
  params.destroy();
1024
1113
  }
1025
1114
  }
1115
+ /** Whether the latest request waits on GPU work that will settle on its own. */
1026
1116
  isCompilingShader() {
1027
- return this._isCompilingShader;
1117
+ return !!this._awaitingRetirement || this._requestedVariant?.status === 'pending';
1028
1118
  }
1029
1119
  isPipelineReady() {
1030
1120
  return ((!!this._rayTracePipeline) &&
1031
1121
  !!this._rayTraceBindGroups);
1032
1122
  }
1123
+ /** Failure of the latest request. A failed variant does not affect other renders. */
1033
1124
  getPipelineError() {
1034
- return this._pipelineError;
1125
+ return this._resourceError ?? this._requestedVariant?.error;
1035
1126
  }
1036
1127
  _updateBindGroups() {
1037
1128
  const { _device } = this;
@@ -1054,14 +1145,18 @@ class WGPURayTracePass {
1054
1145
  }
1055
1146
  }));
1056
1147
  const textureEntries = [
1057
- ...(this._bindless ? [] : [{
1058
- binding: 0,
1059
- resource: this._mapsSampler
1060
- },
1061
- {
1062
- binding: 1,
1063
- resource: this._maps.createView()
1064
- }]),
1148
+ ...(this._bindless
1149
+ ? []
1150
+ : [
1151
+ {
1152
+ binding: 0,
1153
+ resource: this._mapsSampler
1154
+ },
1155
+ {
1156
+ binding: 1,
1157
+ resource: this._maps.createView()
1158
+ }
1159
+ ]),
1065
1160
  {
1066
1161
  binding: 3,
1067
1162
  resource: {
@@ -1071,6 +1166,12 @@ class WGPURayTracePass {
1071
1166
  { binding: 4, resource: this._envTex.createView() },
1072
1167
  { binding: 5, resource: this._envDistributionTex.createView() },
1073
1168
  { binding: 6, resource: this._backgroundTex.createView() },
1169
+ ...(this._photonMapping
1170
+ ? [
1171
+ { binding: 11, resource: this._photonMap.get(this._activeTarget).data.createView() },
1172
+ { binding: 12, resource: this._photonMap.get(this._activeTarget).headers.createView() }
1173
+ ]
1174
+ : []),
1074
1175
  { binding: 8, resource: this._lightSamplingTexture.createView() },
1075
1176
  { binding: 9, resource: this._openPbrEnergyLut.createView() },
1076
1177
  {
@@ -1078,11 +1179,18 @@ class WGPURayTracePass {
1078
1179
  resource: this._adaptiveSamplingTexture.createView()
1079
1180
  }
1080
1181
  ];
1081
- const bufferBindGroup = _device.createBindGroup({ layout: bindGroupPipeline.getBindGroupLayout(0),
1082
- entries: this._bindless ? bufferEntries.concat(textureEntries.map(e => ({ ...e, binding: e.binding + 4 }))) : bufferEntries });
1083
- const texturesBindGroup = this._bindless ? undefined : _device.createBindGroup({
1084
- layout: bindGroupPipeline.getBindGroupLayout(1), entries: textureEntries
1182
+ const bufferBindGroup = _device.createBindGroup({
1183
+ layout: bindGroupPipeline.getBindGroupLayout(0),
1184
+ entries: this._bindless
1185
+ ? bufferEntries.concat(textureEntries.map((e) => ({ ...e, binding: e.binding + 4 })))
1186
+ : bufferEntries
1085
1187
  });
1188
+ const texturesBindGroup = this._bindless
1189
+ ? undefined
1190
+ : _device.createBindGroup({
1191
+ layout: bindGroupPipeline.getBindGroupLayout(1),
1192
+ entries: textureEntries
1193
+ });
1086
1194
  const uniformBindGroup = _device.createBindGroup({
1087
1195
  layout: bindGroupPipeline.getBindGroupLayout(this._bindless ? 1 : 2),
1088
1196
  entries: Object.keys(this._uniforms).map((key, i) => ({
@@ -1092,19 +1200,36 @@ class WGPURayTracePass {
1092
1200
  }
1093
1201
  }))
1094
1202
  });
1203
+ if (this._photonMapping) {
1204
+ // Explicit layouts still track unused textures. Bind a placeholder while
1205
+ // emission writes the real packed map through its storage view.
1206
+ const emissionEntries = textureEntries.map((entry) => entry.binding === 11 || entry.binding === 12
1207
+ ? { ...entry, resource: this._photonMap.placeholder.createView() }
1208
+ : entry);
1209
+ const sceneGroup = _device.createBindGroup({
1210
+ layout: bindGroupPipeline.getBindGroupLayout(this._bindless ? 0 : 1),
1211
+ entries: this._bindless
1212
+ ? bufferEntries.concat(emissionEntries.map((e) => ({ ...e, binding: e.binding + 4 })))
1213
+ : emissionEntries
1214
+ });
1215
+ this._photonEmissionGroups = this._bindless
1216
+ ? [sceneGroup, uniformBindGroup]
1217
+ : [bufferBindGroup, sceneGroup, uniformBindGroup];
1218
+ }
1095
1219
  const target = this._targetOutputs.get(this._activeTarget);
1096
- const outputBindGroup = target.pipeline === bindGroupPipeline && target.bindGroup || _device.createBindGroup({
1097
- layout: bindGroupPipeline.getBindGroupLayout(this._bindless ? 2 : 3),
1098
- entries: [
1099
- this._outColorBuffer,
1100
- this._outNormalBuffer,
1101
- this._outAlbedoBuffer,
1102
- this._outShadowCatcherBuffer
1103
- ].map((buffer, i) => ({
1104
- binding: i,
1105
- resource: { buffer }
1106
- }))
1107
- });
1220
+ const outputBindGroup = (target.pipeline === bindGroupPipeline && target.bindGroup) ||
1221
+ _device.createBindGroup({
1222
+ layout: bindGroupPipeline.getBindGroupLayout(this._bindless ? 2 : 3),
1223
+ entries: [
1224
+ this._outColorBuffer,
1225
+ this._outNormalBuffer,
1226
+ this._outAlbedoBuffer,
1227
+ this._outShadowCatcherBuffer
1228
+ ].map((buffer, i) => ({
1229
+ binding: i,
1230
+ resource: { buffer }
1231
+ }))
1232
+ });
1108
1233
  target.bindGroup = outputBindGroup;
1109
1234
  target.pipeline = bindGroupPipeline;
1110
1235
  this._rayTraceBindGroups = [
@@ -1131,6 +1256,36 @@ class WGPURayTracePass {
1131
1256
  encoder.clearBuffer(this._outAlbedoBuffer);
1132
1257
  encoder.clearBuffer(this._outShadowCatcherBuffer);
1133
1258
  }
1259
+ const offset = this._rtUniform.views.scaleOffset;
1260
+ const causticBatch = this._lightTracing && offset[2] === tiles[0] - 1 && offset[3] === tiles[1] - 1;
1261
+ const photonBatch = this._photonMapping && offset[2] === 0 && offset[3] === 0;
1262
+ if (photonBatch && this._photonMap) {
1263
+ const requested = this._causticSettings?.photonMapping, initial = typeof requested === 'object' ? requested.initialRadius : undefined;
1264
+ const radius = Number.isFinite(initial) && initial > 0 ? initial : Math.max((causticDiagnostics.photonPerCasterRadius
1265
+ ? causticCasterSpheres(this._meshes).reduce((maximum, s) => Math.max(maximum, s[3]), 0)
1266
+ : this._causticBounds[3]) * 0.03, 1e-4);
1267
+ const batch = Math.max(1, this._rtUniform.views.sampleCount[0] - this._rtUniform.views.sampleSequenceOffset[0] + 1);
1268
+ const photonViewProjection = mat4.create();
1269
+ mat4.invert(photonViewProjection, this._viewProjInv);
1270
+ this._photonMap.encode(encoder, this._photonEmissionGroups, this._activeTarget, photonRadius(radius, causticDiagnostics.photonDebugBatch ?? batch, causticDiagnostics.photonAlpha ?? 2 / 3,
1271
+ // Allocation limits may reduce the requested count.
1272
+ this._photonMap.get(this._activeTarget).count, causticDiagnostics.photonRadiusScaling), {
1273
+ querySet: timing?.querySet,
1274
+ resourceTable: this._bindless?.table,
1275
+ orthographic: !this._causticPerspective,
1276
+ progressiveBatch: batch,
1277
+ width: this._width,
1278
+ height: this._height,
1279
+ clear,
1280
+ run: true,
1281
+ photons: 1,
1282
+ batch: Math.floor(this._rtUniform.views.sampleCount[0]),
1283
+ ...this._transport?.uniform,
1284
+ viewProjection: photonViewProjection,
1285
+ casterSphere: this._causticBounds,
1286
+ cameraScale: this._causticCameraScale
1287
+ });
1288
+ }
1134
1289
  const descriptor = {};
1135
1290
  const querySet = timing?.querySet;
1136
1291
  if (querySet) {
@@ -1150,8 +1305,32 @@ class WGPURayTracePass {
1150
1305
  this._rayTraceBindGroups.forEach((bindGroup, idx) => {
1151
1306
  pass.setBindGroup(idx, bindGroup);
1152
1307
  });
1153
- pass.dispatchWorkgroups(Math.ceil(dispatchWidth / WORKGROUP_SIZE), Math.ceil(dispatchHeight / WORKGROUP_SIZE), 1);
1308
+ researchTransport()?.capture(this);
1309
+ pass.dispatchWorkgroups(Math.ceil(dispatchWidth / (this._transport?.workgroupSize ?? WORKGROUP_SIZE)), Math.ceil(dispatchHeight / (this._transport?.workgroupSize ?? WORKGROUP_SIZE)), 1);
1154
1310
  pass.end();
1311
+ if (this._lightTracing && this._causticLight) {
1312
+ const viewProjection = mat4.create();
1313
+ mat4.invert(viewProjection, this._viewProjInv);
1314
+ const requested = this._causticSettings?.lightTracing;
1315
+ const count = typeof requested === 'object' ? requested.photonsPerSample : undefined;
1316
+ const photons = Math.min(this._device.limits.maxComputeWorkgroupsPerDimension * 64, Math.max(1, Math.round(Number.isFinite(count) ? count : (this._width * this._height) / 4)));
1317
+ this._causticLight.encode(encoder, this._rayTraceBindGroups.slice(0, this._bindless ? 2 : 3), this._activeTarget, {
1318
+ querySet: causticBatch ? querySet : undefined,
1319
+ width: this._width,
1320
+ height: this._height,
1321
+ clear,
1322
+ run: !!causticBatch,
1323
+ photons,
1324
+ batch: Math.floor(this._rtUniform.views.sampleCount[0]),
1325
+ ...this._transport?.uniform,
1326
+ resourceTable: this._bindless?.table,
1327
+ orthographic: !this._causticPerspective,
1328
+ viewProjection,
1329
+ casterSphere: this._causticBounds,
1330
+ cameraScale: this._causticCameraScale
1331
+ });
1332
+ }
1333
+ return (causticBatch ? 1 : 0) | (photonBatch ? 2 : 0);
1155
1334
  }
1156
1335
  async getDebugBVHData() {
1157
1336
  if (!this._debugBVHBuffer) {
@@ -1174,7 +1353,7 @@ class WGPURayTracePass {
1174
1353
  scale: atlas?.scale ?? 1, metadataBytes: 0, resourceTableCapacity: 0, retiredBytes: 0
1175
1354
  })
1176
1355
  },
1177
- shaderCompilation: this._shaderCompilationDiagnostics,
1356
+ shaderCompilation: this._requestedVariant?.diagnostics,
1178
1357
  lightSampling: {
1179
1358
  emissiveTriangles: this._emissiveTriangles.length,
1180
1359
  activeChoices: this._rtUniform.views.lightChoiceCount[0],
@@ -1194,15 +1373,6 @@ class WGPURayTracePass {
1194
1373
  if (next === this._alphaBlendStrategy)
1195
1374
  return false;
1196
1375
  this._alphaBlendStrategy = next;
1197
- if (this._constants.HAS_ALPHA_BLEND) {
1198
- this._computeShaderModule = undefined;
1199
- this._rayTracePipeline = undefined;
1200
- this._rayTracePipelineLayout = undefined;
1201
- this._rayTraceBindGroups = undefined;
1202
- this._prevPipelineConstants = {};
1203
- this._failedPipelineSignature = undefined;
1204
- this._needsUpdateBindGroups = true;
1205
- }
1206
1376
  return true;
1207
1377
  }
1208
1378
  hasDepthOfField() {
@@ -1266,8 +1436,28 @@ class WGPURayTracePass {
1266
1436
  this._device.queue.writeTexture({ texture: this._adaptiveSamplingTexture }, data, { bytesPerRow: width * Float32Array.BYTES_PER_ELEMENT }, [width, height]);
1267
1437
  }
1268
1438
  setQuality(options) {
1439
+ WGPURayTracePass.prototype._setCaustics.call(this, options.caustics);
1269
1440
  writeTraceQuality(this._rtUniform.views, options);
1270
1441
  }
1442
+ _setCaustics(settings) {
1443
+ this._causticSettings = settings;
1444
+ const transport = researchTransport()?.configure(this, 'webgpu');
1445
+ if (!!transport?.enabled !== !!this._transport?.enabled)
1446
+ this._needsUpdateLightSamplingData = true;
1447
+ this._transport = transport;
1448
+ const hasCaster = !!(settings?.lightTracing || settings?.photonMapping) &&
1449
+ this._meshes.some((mesh) => mesh.materials.some(isCausticSpecular));
1450
+ const photonSupported = !(settings?.lightTracing || settings?.photonMapping) ||
1451
+ !hasCaster ||
1452
+ !hasStochasticCausticCaster(this._meshes);
1453
+ this._lightTracing = !!settings?.lightTracing && hasCaster && photonSupported;
1454
+ const photonMapping = !!settings?.photonMapping && hasCaster && photonSupported;
1455
+ this._photonMapping = photonMapping;
1456
+ this._causticKey =
1457
+ this._transport?.enabled || this._lightTracing || this._photonMapping
1458
+ ? `${this._lightTracing}/${this._photonMapping}/${causticDiagnostics.fullEmissionRandom !== false}/${causticDiagnostics.photonMaxDepth ?? 20}/${causticDiagnostics.debugGroup}/${causticDiagnostics.photonDirectSubset !== false}/${this._transport?.key ?? ''}`
1459
+ : '';
1460
+ }
1271
1461
  _setEnvMap(envImage) {
1272
1462
  const device = this._device;
1273
1463
  // Always create env tex
@@ -1621,14 +1811,19 @@ class WGPURayTracePass {
1621
1811
  this._needsUpdateBindGroups ||= resized;
1622
1812
  }
1623
1813
  _updateLightSamplingData() {
1624
- this._constants.LIGHT_CAPABILITIES = lightShaderCapabilities(this._analyticLights.map(light => light.type), this._emissiveTriangles.length);
1625
- const lighting = prepareSceneLightSampling(this._analyticLights, this._emissiveTriangles, this._envImageSource && 'sky' in this._envImageSource ? proceduralEnvironmentPower(this._envImageSource) : estimateEnvironmentPower(this._envImageSource), this._device.limits.maxTextureDimension2D, this._lightChoices, this._lightSamplingScratch, this._shadowCatcherLightChoices);
1814
+ this._constants.LIGHT_CAPABILITIES = lightShaderCapabilities(this._analyticLights.map((light) => light.type), this._emissiveTriangles.length);
1815
+ const lighting = prepareSceneLightSampling(this._analyticLights, this._emissiveTriangles, this._envImageSource && 'sky' in this._envImageSource
1816
+ ? proceduralEnvironmentPower(this._envImageSource)
1817
+ : estimateEnvironmentPower(this._envImageSource), this._device.limits.maxTextureDimension2D, this._lightChoices, this._lightSamplingScratch, this._shadowCatcherLightChoices);
1818
+ researchTransport()?.prepare(this, 'webgpu', lighting);
1626
1819
  this._analyticLightSelectionPmfs = lighting.analyticPmfs;
1627
1820
  this._iesProfileTexelOffsets = lighting.profiles.offsets;
1628
1821
  this._lightDomainDiagnostics = lighting.domains;
1629
1822
  const { lightWidth: width, lightHeight: height, lightData: textureData } = lighting;
1630
1823
  writeTraceLighting(this._rtUniform.views, lighting);
1631
- const resized = !this._lightSamplingTexture || width !== this._lightSamplingTextureSize[0] || height !== this._lightSamplingTextureSize[1];
1824
+ const resized = !this._lightSamplingTexture ||
1825
+ width !== this._lightSamplingTextureSize[0] ||
1826
+ height !== this._lightSamplingTextureSize[1];
1632
1827
  if (!this._lightSamplingTexture ||
1633
1828
  width !== this._lightSamplingTextureSize[0] ||
1634
1829
  height !== this._lightSamplingTextureSize[1]) {
@@ -1770,11 +1965,7 @@ class WGPURayTracePass {
1770
1965
  }
1771
1966
  this._hasShadowCatcher = materials.some((m) => m.shadowCatcher);
1772
1967
  const hasDisplacement = materialData.getDisplacementMaps().length > 0;
1773
- if (hasDisplacement !== this._hasDisplacement) {
1774
- this._hasDisplacement = hasDisplacement;
1775
- // Needs to recreate pipeline layout
1776
- this._rayTracePipelineLayout = undefined;
1777
- }
1968
+ this._hasDisplacement = hasDisplacement;
1778
1969
  Object.assign(this._constants, wgpuMaterialConstants(materials, materialData, mergedGeometry, this._bevelSamples, Number(this._constants.NUM_MAPS ?? 0)));
1779
1970
  // Runtime storage metadata has no texture-count specialization.
1780
1971
  if (this._bindless)
@@ -1945,8 +2136,6 @@ class WGPURayTracePass {
1945
2136
  this._retiredBindless.add(retired);
1946
2137
  void this._device.queue.onSubmittedWorkDone().catch(() => { }).then(() => { retired.dispose(); this._retiredBindless.delete(retired); });
1947
2138
  this._bindless = undefined;
1948
- this._computeShaderModule = undefined;
1949
- this._rayTracePipelineLayout = undefined;
1950
2139
  }
1951
2140
  return new MaterialTable(...args);
1952
2141
  }
@@ -2058,6 +2247,10 @@ class WGPURayTracePass {
2058
2247
  }
2059
2248
  // TODO reuse the code of RayTraceNode
2060
2249
  setCamera(camera) {
2250
+ this._causticPerspective = camera.projection === 'perspective';
2251
+ const projection = camera.getProjectionMatrix(this._targetAspect);
2252
+ this._causticCameraScale = Math.abs(projection[0] * projection[5]) / 4;
2253
+ this._setCaustics(this._causticSettings);
2061
2254
  const viewProjectionInvMat = this._viewProjInv;
2062
2255
  this._apertureRadius = cameraApertureRadius(camera);
2063
2256
  this._focusDistance = camera.focusDistance;
@@ -2147,12 +2340,14 @@ class WGPURayTracePass {
2147
2340
  this._envDistribution?.dispose();
2148
2341
  this._skyBaker?.dispose();
2149
2342
  this._bindless?.dispose();
2150
- this._retiredBindless.forEach(textures => textures.dispose());
2343
+ this._retiredBindless.forEach((textures) => textures.dispose());
2151
2344
  this._retiredBindless.clear();
2345
+ this._causticLight?.dispose();
2346
+ this._photonMap?.dispose();
2152
2347
  this._shaderCache.clear();
2153
2348
  this._shaderCapabilities.clear();
2154
2349
  const buffers = new Set([
2155
- ...Array.from(this._targetOutputs.values()).flatMap(target => target.buffers),
2350
+ ...Array.from(this._targetOutputs.values()).flatMap((target) => target.buffers),
2156
2351
  this._mapInfoUniform,
2157
2352
  this._attrBuffers,
2158
2353
  this._materialsBuffer,
@@ -2172,6 +2367,9 @@ class WGPURayTracePass {
2172
2367
  this._adaptiveSamplingTexture
2173
2368
  ]);
2174
2369
  textures.forEach((texture) => texture?.destroy());
2370
+ for (const job of this._queuedCompiles.splice(0))
2371
+ job.release();
2372
+ this._traceVariants.clear();
2175
2373
  this._rayTracePipeline = undefined;
2176
2374
  this._rayTraceBindGroups = undefined;
2177
2375
  }