@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
package/docs/api.md CHANGED
@@ -130,38 +130,40 @@ other sequences retain their completed samples.
130
130
  | `aovs` / `aovSamples` | `[]` / `16` | Optional [surface outputs](#surface-outputs) / independent positive integer sample cap |
131
131
  | `bounces` / `clamping` | `3` / `0` | Scattering depth / per-sample radiance clamp; 0 disables clamping |
132
132
  | `samplingPattern` | `'sobol'` | Sobol, ZSobol or random; see [sampling](./sampling.md) |
133
+ | `caustics` | `undefined` | Optional caustics; all estimators default to off. See [caustics](#caustics) |
133
134
  | `directLightCandidates` | `4` | WebGPU light candidates, clamped 1–16; ignored on WebGL |
134
135
  | `bevel` / `bevelSamples` | `true` / `8` | Bevel shading / probes, clamped 1–32 |
135
- | `dispersionWavelengths` | `3` | 3 wavelengths, or one randomly chosen wavelength |
136
+ | `dispersionWavelengths` | `3` | One continuous wavelength per RGB channel, or one randomly chosen channel |
136
137
  | `alphaBlendStrategy` | `'stochastic'` | WebGPU primary BLEND policy; ignored on WebGL |
137
138
  | `transmissiveOutputAlphaMin` | `1` | Path-depth alpha rule over transparent backgrounds; changes restart this render |
138
139
  | `resolutionScale` | `1` | Relative to display canvas; exclusive with fixed width/height |
139
- | `width` / `height` | undefined | Paired fixed constructor dimensions; getters report actual output dimensions |
140
+ | `width` / `height` | undefined | Paired fixed constructor dimensions; `toBlob`, `readPixels` and the getters return exactly this size |
140
141
  | `adaptiveResolution` | `false` | `{targetFrameMs, minScale}` for scale-based renders; both required, positive, minScale ≤ resolutionScale |
141
- | `timeBudgetMs` / `maxTiles` | `21` / `Infinity` | Tile time budget / tiles per call |
142
- | `batchSamples` | `false` | Let one refine submit several samples within timeBudgetMs; see [sample batching](./progressive-rendering.md#sample-batching). `allowMultipleSamplesPerFrame` is a deprecated alias |
142
+ | `timeBudgetMs` / `maxTiles` | `21` / `Infinity` | Target tile time / maximum tile grid size per sample (rounded up to a rectangular grid); fewer tiles make larger tiles, without limiting work per frame or call |
143
+ | `batchSamples` | `false` | Let one refine submit several samples within an estimated timeBudgetMs; one sample or tile can exceed it; see [sample batching](./progressive-rendering.md#sample-batching). `allowMultipleSamplesPerFrame` is a deprecated alias |
143
144
  | `fallback` | `null` | Single-level Render on the same engine |
144
145
  | `denoiser` | `null` | Per-render configuration; read back through the frozen `denoiser` getter and change with `setDenoiser()`; see [denoising](./denoising.md) |
145
146
 
146
147
  `bounces` is rounded and clamped to [1, 20] in both backends; NaN becomes 1.
147
- The path loop allows at most `min(bounces * 3, 20)` events, including transmission,
148
- alpha skips and escape checks. Opaque paths also have their own scattering-depth
149
- termination. This event budget can truncate deep transparent paths even when
150
- the requested scattering depth is larger. Intel Mac WebGL retains its driver
151
- workaround: `min(bounces, 3)` loop events, doubled for scenes with transmission
152
- or alpha, plus its existing early depth termination.
148
+ Paths allow at most `min(bounces * 3, 20)` events, including transmission,
149
+ alpha skips and escape checks. Opaque paths also terminate at their scattering
150
+ limit. The event budget can truncate deep transparent paths before the requested
151
+ scattering depth. On Intel Mac WebGL, the event limit is `min(bounces, 3)`, doubled
152
+ for scenes with transmission or alpha, with earlier scattering termination.
153
+ See [caustic path-length limits](./caustics.md#limits) when comparing renderers.
153
154
 
154
155
  | Method / property | Meaning |
155
156
  | --- | --- |
156
157
  | `refine(channel?, options?)` | Trace beauty or an enabled AOV within this render's budget; returns status |
158
+ | `refineUntil(deadline, {signal}?)` | For custom frame loops: refine once synchronously, then keep sampling until the `performance.now()` deadline or cancellation; returns `Promise<void>`; see [sample batching](progressive-rendering.md#sample-batching) |
157
159
  | `converge(options)` | Deterministic offline image; see below |
158
160
  | `readPixels(kind = 'hdr')` | Read this render; see [pixel outputs](#pixel-outputs) |
159
161
  | `engine` | Owning engine |
160
162
  | `denoising` | Queued/running inference, including intermediate updates; default false |
161
163
  | `on('sample', listener)` | Complete samples: `{samples, isFirstFrame, frameTimeMs}`; returns unsubscribe |
162
164
  | `samples` | Complete samples since restart; stale engine inputs report 0 |
163
- | `gpuSampleTimeMs` | Latest WebGPU full beauty sample estimate in ms; undefined without timestamps, for stale inputs or during converge |
164
- | `whenReadyToRefine()` | Wait for the next release of WebGPU sampling queue capacity; resolves immediately if none is queued or on WebGL; submits no work |
165
+ | `gpuSampleTimeMs` | Deprecated; use `refineUntil` for custom frame loops. Latest WebGPU full beauty sample estimate in ms, extrapolated from tile GPU time when tiled; undefined without timestamps, for stale inputs or during converge |
166
+ | `whenReadyToRefine()` | Deprecated; use `refineUntil`. Wait for WebGPU sampling capacity and return `true` if sampling was queued; return `false` immediately with no sampling, on WebGL, or for only denoising, display or offline convergence work; submits no work; throws after disposal |
165
167
  | `converged` | `samples >= maxSamples`, independently of filtering |
166
168
  | `status` | Preparing, compiling, converging, denoising, finished or error |
167
169
  | `setDenoiser(options)` | Replace the denoiser configuration; resolves when the filter is ready |
@@ -175,13 +177,10 @@ completed filtering. Synchronous execution failures return `'error'` and set
175
177
  engine.lastError. Invalid lifecycle operations (disposed render or refine during
176
178
  converge) throw.
177
179
 
178
- `refine(undefined, {batchSamples, targetBatchTimeMs, deferResolve})`
179
- overrides batching for one call without changing render settings or discarding
180
- samples. The first two options default to the render's execution settings.
181
- `targetBatchTimeMs` must be positive and finite, defaults to `timeBudgetMs`, is
182
- capped by that budget and is ignored on WebGL. `deferResolve` (default false,
183
- WebGPU only) leaves the displayed image unchanged for calls between display
184
- frames; the next presentation or pixel read updates it once with every new sample.
180
+ `refine(undefined, {batchSamples})` overrides batching for one call without
181
+ changing render settings or discarding samples; it defaults to the render's
182
+ `batchSamples`. The `targetBatchTimeMs` and `deferResolve` refine options are
183
+ deprecated; use `refineUntil`.
185
184
 
186
185
  A fallback gets one sample when the requested render cannot finish its first
187
186
  sample within budget and the fallback has no current complete image. A ready
@@ -206,6 +205,25 @@ Resolves `{samples, averageSamplesPerPixel, pilotSamples, adaptiveSampling,
206
205
  varianceHistory?}` after filtering. It rejects if engine input revisions change.
207
206
  The same render cannot refine concurrently; other renders remain usable.
208
207
 
208
+ ### Caustics
209
+
210
+ ```ts
211
+ render.caustics = {lightTracing: {photonsPerSample: 32768}};
212
+ render.caustics = undefined; // Disable every estimator.
213
+ ```
214
+
215
+ Changing any caustic option restarts accumulation. All estimators are experimental;
216
+ see the [caustics guide](./caustics.md) for choosing estimators, supported paths,
217
+ rendering cost and image-quality limitations.
218
+
219
+ | Option | Default | Meaning |
220
+ | --- | --- | --- |
221
+ | `lightTracing` | `false` | `true` or an object enables directly visible caustic light paths on WebGPU; ignored on WebGL 2 |
222
+ | `lightTracing.photonsPerSample` | pixel count / 4 | Emitted photons per sample, rounded to a positive integer and capped by the device dispatch limit |
223
+ | `photonMapping` | `false` | `true` or an object enables progressive caustic photon mapping on WebGPU; ignored on WebGL 2. See [PM-only denoising limits](./caustics.md#limits) |
224
+ | `photonMapping.photonsPerSample` | pixel count / 4, at most 262144 | Emitted photons per batch; 28 bytes each plus up to 13 MiB of overhead. Positive finite values are rounded to at least 1 and reduced to fit device limits and a 128 MiB batch budget; other values use the default |
225
+ | `photonMapping.initialRadius` | 3% of the caster bounding sphere radius | Positive first-batch gather radius in scene units, independent of `photonsPerSample`; shrinks as batches accumulate. Smaller radii preserve detail but require more photons |
226
+
209
227
  ## RenderLoop and Sync
210
228
 
211
229
  `start()` uses requestAnimationFrame when available and a 16 ms setTimeout
@@ -226,6 +244,7 @@ otherwise. Worker visibility and control forwarding are described in
226
244
 
227
245
  `engine`, `display`, `preview` (possibly undefined) and `final` are read-only.
228
246
  `changing` defaults false; `samples` counts final beauty samples and reports 0 during preview; `status` starts preparing.
247
+ An exception during a tick, such as from a sync, sets `status` to error and `lastError`; the next successful tick clears them.
229
248
  `select({changing})` defaults to changing ? preview : final. `addSync`/`removeSync`
230
249
  manage integrations without disposing them. `tick` executes a manual frame;
231
250
  `start`/`stop` manage animation frames. `converge` accepts the same options as
@@ -251,7 +270,7 @@ Use `engine.display`; creation of additional displays is outside this API.
251
270
  their next refine; fixed renders retain their dimensions.
252
271
  `present(render)` only presents: it returns the render or its ready fallback,
253
272
  or undefined without changing the canvas when neither is ready.
254
- `toBlob(render, {type?, quality?})` composes that render at its actual dimensions,
273
+ `toBlob(render, {type?, quality?})` composes that render at its output dimensions,
255
274
  without resizing or changing the current canvas. MIME defaults to image/png;
256
275
  quality defaults to the browser encoder value.
257
276
 
@@ -580,7 +599,9 @@ see [denoising](./denoising.md#custom-oidn-networks). The main entry exports
580
599
 
581
600
  `render.getOutputTexture(kind)` borrows native HDR or the latest display image;
582
601
  `display.addPostProcess(pass)` returns unregister and runs on every present/export.
583
- Both are WebGPU-only. See [formats, lifecycle and overlay example](./post-processing.md).
602
+ Both are WebGPU-only. Textures are rounded up to multiples of 16 pixels, so a
603
+ fixed render's texture can include extra pixels around the requested image. See
604
+ [formats, lifecycle and overlay example](./post-processing.md).
584
605
 
585
606
  Anisotropy direction options: `anisotropyMap` (default `undefined`) reads linear RG as a tangent direction and B as a multiplier for base specular strength. `specularAnisotropyRotation` (default `0`) rotates that direction counter-clockwise in radians; `coatAnisotropyRotation` (default `0`) rotates the coat relative to the geometry tangent. See [materials](./materials.md#anisotropy-directions) for frame and fallback conventions.
586
607
 
@@ -0,0 +1,80 @@
1
+ # Caustics
2
+
3
+ Enable caustics on the render that needs them:
4
+
5
+ ```ts
6
+ render.caustics = {lightTracing: true, photonMapping: true};
7
+ ```
8
+
9
+ Choose the estimators for the visible receiver:
10
+
11
+ | Scene | Settings |
12
+ | --- | --- |
13
+ | Glass or metal casting onto a directly visible surface | `{lightTracing: true}` |
14
+ | Water or glass between the camera and the caustic receiver | `{photonMapping: true}` |
15
+ | A mixture of direct, indirect and glass-covered receivers | `{lightTracing: true, photonMapping: true}` |
16
+
17
+ Light tracing preserves narrow, directly visible caustics without a gather
18
+ radius. Photon mapping covers indirect caustics and surfaces seen through glass,
19
+ with additional memory and gather cost. Combined estimators assign path coverage
20
+ automatically. The viewer's Cornell caustic preset enables both estimators.
21
+
22
+ Set `render.caustics = undefined` to disable every estimator. See
23
+ [API options and defaults](./api.md#caustics).
24
+
25
+ ## Options and cost
26
+
27
+ ```ts
28
+ render.caustics = {
29
+ lightTracing: {photonsPerSample: 32768},
30
+ photonMapping: {photonsPerSample: 32768, initialRadius: 0.1}
31
+ };
32
+ ```
33
+
34
+ Both estimators require WebGPU; WebGL ignores those options. HDR readback,
35
+ display and denoising include the combined result. Changes to any option restart
36
+ accumulation. The native SDK provides the same choices through Rust
37
+ `CausticSettings` and the C `MT_CAUSTIC_*` flags.
38
+
39
+ More emitted photons can reduce noise but cost more time and memory. Photon-map
40
+ storage adds 28 bytes per emitted photon plus up to 13 MiB of overhead. Requests
41
+ above the device limits or the 128 MiB batch budget use fewer photons. Photon
42
+ batches cover the full image even when path tracing uses tiles or adaptive
43
+ sampling. Their time counts toward the frame budget, but adaptive variance measures
44
+ only the path-traced component.
45
+
46
+ ## Limits
47
+
48
+ Light tracing follows surfaces with no diffuse, subsurface, fuzz or coat
49
+ contribution and specular roughness at most 0.1. It supports emissive triangles,
50
+ rectangles, spheres, spotlights, directional lights and environment lighting,
51
+ perspective and orthographic cameras, depth of field and dispersion. Photon
52
+ mapping adds caustics throughout the allowed camera path. Volume scattering
53
+ events and shadow catchers remain with ordinary path tracing.
54
+
55
+ Complete caustic light-to-camera paths can be longer than `bounces`. Align total
56
+ path-length limits when comparing radiance with another renderer.
57
+
58
+ Photon mapping starts blurred and shrinks its radius as samples accumulate. A
59
+ small initial radius can be noisy; a large one can blur nearby surfaces and details.
60
+ Distant smooth casters enlarge the automatic radius, so set `initialRadius` if deep
61
+ caustics look too blurred. At the first diffuse surface, the gather radius is capped
62
+ to about two projected pixels to reduce patches after denoising; reflections and
63
+ refractions use an approximate footprint. More photons can help, but not every
64
+ batch improves the image, and more samples do not remove the precision loss in
65
+ stored photon data.
66
+
67
+ With photon mapping alone, directly visible receivers can show fewer cloud-like
68
+ OIDN blotches but grainier raw images. Receivers seen through reflection or
69
+ refraction can still show blotches. Enable light tracing as well for the cleanest
70
+ directly visible receivers.
71
+
72
+ A denoiser can soften narrow caustic lines, so compare raw HDR with denoised output
73
+ when judging detail. Use raw HDR for energy comparisons: displayed or denoised
74
+ brightness can differ even when regional HDR energy agrees.
75
+
76
+ Both estimators fall back to path tracing for the whole scene when a smooth caster
77
+ uses stochastic blend coverage. Cutout coverage remains supported. Environment
78
+ caustics follow path tracing's visible-background behavior through the first smooth
79
+ transmission; an explicit black background suppresses that contribution.
80
+ Quantitative energy agreement with other renderers is not guaranteed.
package/docs/materials.md CHANGED
@@ -97,10 +97,11 @@ the substrate through the attenuation term.
97
97
  **Interior media.** Transmission describes a homogeneous volume:
98
98
  `transmissionColor` is the color that survives one `transmissionDepth`, and
99
99
  `transmissionScatter` is the scattering albedo. With `transmissionDepth = 0`
100
- the tint is applied at the surface. Subsurface is a separate layer driven by its
101
- color and per-channel radius. When both are active, their coefficients combine
102
- into one volume. Dispersion (`transmissionDispersionScale > 0`) samples
103
- wavelengths spectrally.
100
+ the tint is applied at the surface. Subsurface has its own color and per-channel
101
+ radius. A positive `transmissionDispersionScale` enables
102
+ wavelength-dependent refraction; values above `1` increase color separation,
103
+ and `0` disables it. Lower `transmissionDispersionAbbeNumber` values also
104
+ increase color separation.
104
105
 
105
106
  ## Compatibility names
106
107
 
@@ -87,24 +87,65 @@ are refined that frame. See [scene updates](updates.md) for notifications.
87
87
 
88
88
  ## Sample batching
89
89
 
90
- Without batching, a still image gets one sample per display frame, so a fast GPU
91
- idles for most of each frame. With `batchSamples`, sampling works on two levels:
90
+ Without batching, each call advances one sample or tile, so a fast GPU can idle
91
+ between display frames. With `batchSamples`, sampling works on two levels:
92
92
 
93
- - **Per call.** One `refine()` may submit several samples while at most
94
- `timeBudgetMs` of GPU work is queued, and returns without waiting for them.
93
+ - **Per call.** One `refine()` may submit several samples within an estimated
94
+ `timeBudgetMs` of GPU work, or one longer sample or tile, and returns without waiting for them.
95
95
  Without `batchSamples`, a call traces one sample or tile.
96
96
  - **Between frames.** RenderLoop also refines its final render each time the GPU
97
97
  finishes queued work, until the next display frame. It keeps about 4 ms of work
98
- queued, or one sample if that is longer, so a camera edit waits behind at most
99
- that much work at any refresh rate.
98
+ queued, or one sample or tile if that is longer. Expensive tiled samples can
99
+ continue between display frames when each measured tile fits `timeBudgetMs`;
100
+ longer tiles wait for display frames to bound input delay. Presentation still
101
+ follows the display frame rate.
100
102
 
101
103
  Batching is off by default. Enable it on the loop's final render with
102
104
  `final: {batchSamples: true}`, or on a Render you supply. RenderLoop stops
103
105
  batching while the view is changing.
104
106
 
105
- In your own loop, `batchSamples` gives the first level. For the second, call
106
- `render.refine(undefined, {targetBatchTimeMs: 4, deferResolve: true})` after each
107
- `await render.whenReadyToRefine()` until your next frame; see [Render](./api.md#render).
107
+ Prefer RenderLoop for between-frame refilling. For a custom loop, use
108
+ `refineUntil(deadline, {signal})`. It synchronously calls `refine()` once, so
109
+ `present()` belongs immediately after the call. On WebGPU it then refills short
110
+ batches until the deadline, cancellation, a hidden page or sampling/display
111
+ conditions prevent more work. WebGL performs only the synchronous call.
112
+
113
+ This example assumes `engine` and `render` are created with
114
+ `render.batchSamples = true` and the final beauty channel selected. Adjust the
115
+ 60 Hz deadline to your application's cadence and update controls before
116
+ `engine.update()`.
117
+
118
+ <!-- own-loop-refill -->
119
+ ```js
120
+ let refill;
121
+ const stop = () => refill?.abort();
122
+ for (const type of ['pointerdown', 'pointermove', 'pointerup', 'wheel', 'keydown', 'input', 'change'])
123
+ addEventListener(type, stop, {capture: true, passive: true});
124
+
125
+ function frame(timestamp) {
126
+ requestAnimationFrame(frame);
127
+ stop();
128
+ engine.update();
129
+ refill = new AbortController();
130
+ void render.refineUntil(timestamp + 1000 / 60, {signal: refill.signal});
131
+ engine.display.present(render);
132
+ }
133
+ requestAnimationFrame(frame);
134
+ ```
135
+
136
+ Call `stop()` before pausing, exporting or switching to another render, and skip
137
+ `refineUntil()` while paused. A new call automatically ends the previous refill.
138
+ The signal cancels future submissions; work already queued on the GPU completes.
139
+ The returned Promise resolves when refilling stops, including after an
140
+ asynchronous error (reported through `render.status` and `engine.lastError`).
141
+
142
+ The library automatically continues the remaining tiles of the current sample
143
+ between presentations. `samples` increases only after the whole sample;
144
+ presentation keeps the previous complete sample until then. Committed scene or
145
+ camera edits discard an unfinished sample and restart against the new inputs.
146
+ `maxTiles` caps the sample's tile grid (rounded up to a rectangular grid), so a
147
+ smaller value makes larger tiles. It does not cap work per frame or per call;
148
+ use `timeBudgetMs` and the between-frame deadline to control queued work.
108
149
 
109
150
  ## Reading and exporting
110
151
 
@@ -0,0 +1,93 @@
1
+ # Continuous-wavelength dispersion
2
+
3
+ MuTracer renders dispersion with continuous wavelengths inside an RGB renderer.
4
+ Wavelength decides how light refracts and how its energy divides between RGB
5
+ channels. Lights, materials and media stay RGB, so this is not spectral rendering.
6
+ The implementation is in `src/renderer/spectrum.ts`, and it runs only when a material
7
+ has `transmissionDispersionScale > 0`.
8
+
9
+ ## Spectral response
10
+
11
+ `spectrumRgb` converts a wavelength in 400–700 nm to linear sRGB weights:
12
+
13
+ 1. Evaluate the CIE 1931 matching functions with the multi-lobe fit from Wyman, Sloan
14
+ and Shirley, "Simple Analytic Approximations to the CIE XYZ Color Matching
15
+ Functions", JCGT 2(2), 2013.
16
+ 2. Convert XYZ to linear sRGB and clamp negative values to zero.
17
+ 3. Scale each channel so its mean over a uniform wavelength is 1.
18
+
19
+ The scale keeps white light white on average. The clamp keeps out-of-gamut colors
20
+ saturated instead of colorimetrically exact, and it leaves red with a small violet
21
+ lobe, which is why the short end of a rainbow looks violet instead of blue.
22
+
23
+ ## Sampling
24
+
25
+ Both integrators sample from 5 nm inverse CDF tables. They use different estimators
26
+ because they carry different quantities.
27
+
28
+ | | Camera paths | Caustic photons |
29
+ |---|---|---|
30
+ | Distribution | The response of the path's RGB channel | The sum of the RGB responses |
31
+ | Carries | One RGB channel | RGB flux |
32
+ | Energy weight | None; the distribution is the response | `spectrumRgb(λ) / (pdf(λ) · 300)` |
33
+ | Random number | `CAMERA_WAVELENGTH`, shared by a sample's three paths | Photon emission dimension 69 |
34
+
35
+ Camera paths evaluate no response, so their effective response is a 5 nm histogram of
36
+ the fit. Photons apply the weight exactly. Sharing one random number across a camera
37
+ sample's channels keeps their error correlated with the other common random numbers.
38
+
39
+ `dispersionWavelengths: 3` traces one path per channel in each sample. With
40
+ `dispersionWavelengths: 1`, a dispersive primary hit selects one channel at random
41
+ and multiplies the result by 3; other primary hits trace full RGB. This is not hero
42
+ wavelength sampling, which carries several wavelengths along one path and combines
43
+ them with MIS.
44
+
45
+ ## Channels and gathering
46
+
47
+ A path stores its channel and its refracting wavelength separately:
48
+ `spectralChannel` selects the radiance it keeps, and `wavelength` only drives
49
+ `sampling_cauchy_ior`. A channel of 0 keeps full RGB. Red paths can sample violet, so
50
+ the channel cannot be derived from the wavelength.
51
+
52
+ Photon gathering filters photon flux by the camera path's channel. This splits RGB
53
+ across the camera paths and applies no second spectral weight, so energy is shared
54
+ between path tracing and caustics the same way as without dispersion.
55
+
56
+ ## Short-wavelength IOR limit
57
+
58
+ Cauchy IOR rises quickly at short wavelengths. At strong dispersion scales, violet
59
+ light reaches IORs that reflect or trap much of it, and wide caustics turn yellow.
60
+ `spectrumIorWavelength` therefore eases the refracting wavelength below 500 nm toward
61
+ 475 nm, while color keeps the sampled wavelength. The mapping is monotonic and
62
+ continuous, so the gradient has no hard edge.
63
+
64
+ This limit is an artistic choice and not physically based. It narrows the violet end
65
+ of the spectrum, and strong dispersion scales still lose some blue.
66
+
67
+ ## Differences from spectral rendering
68
+
69
+ | Area | Spectral rendering | MuTracer |
70
+ |---|---|---|
71
+ | Lights | Emission varies with wavelength | RGB, with an implicit broad spectrum; a sodium lamp still disperses into a full rainbow |
72
+ | Materials and textures | Reflectance varies with wavelength, often upsampled from RGB | RGB, so metamerism cannot appear |
73
+ | Media | Absorption and scattering vary with wavelength | Three RGB coefficients |
74
+ | Thin film | Interference is integrated over wavelength | Evaluated at 620, 540 and 450 nm |
75
+ | Refractive index | Often measured curves or Sellmeier equations | Cauchy from IOR and Abbe number, with the short-wavelength limit |
76
+ | Wavelengths per path | Often several, combined with MIS | One per path or photon |
77
+ | Output color | Integrated from the spectrum into the output color space | Fixed clamped sRGB weights, normalized per channel |
78
+
79
+ These differences matter most for narrow-band lights, colored absorption over long
80
+ paths, metamerism and predictive color. Clear glass and crystal under broad lights are
81
+ closer to the spectral result, because their color comes mainly from refraction.
82
+
83
+ Full spectral rendering would change the light, material, texture and medium data
84
+ models, and requires RGB-to-spectrum upsampling in both shader backends and native.
85
+
86
+ ## Changing this code
87
+
88
+ - Update WGSL and GLSL together. Both are generated from `spectrum.ts`; caustics are
89
+ WGSL only.
90
+ - `spectralChannel` must be set wherever a path's wavelength is set.
91
+ - `tests/unit/spectrum.test.ts` covers white balance, weight bounds, channel
92
+ distributions and the IOR limit. Check the image result on a real GPU in both
93
+ backends, as described in `AGENTS.md`.
package/docs/updates.md CHANGED
@@ -154,7 +154,9 @@ do not recompile the path tracer. Neither do changes to `bounces`,
154
154
  bevel switch, sample count or seed. Data uploads, atlas/BVH work and accumulation
155
155
  restarts may still be needed.
156
156
 
157
- Returning to a cached compatible combination reuses its pipeline. Prepared
157
+ A render waiting for a shader keeps its samples and image, and other renders keep
158
+ sampling. Returning to a cached compatible combination reuses its pipeline, even
159
+ while another combination is still compiling. Prepared
158
160
  combinations may be discarded after many different edits; recreating the Engine
159
161
  or recovering a lost device also requires preparing shaders again. Compilation time depends
160
162
  on the feature combination, BVH layout, browser and GPU driver. Denoising and
@@ -0,0 +1,65 @@
1
+ # Viewer feature checklist
2
+
3
+ Use this checklist with the [Web viewer](https://mutracer.meimu-studio.com/viewer/)
4
+ and [native viewer](../native/viewer/README.md). Start with material spheres,
5
+ then use a glTF with nested objects, cameras, lights and animation. A checked
6
+ item means you exercised the behavior in the version being reviewed; it does
7
+ not promise identical rendered pixels between devices or backends.
8
+
9
+ ## Workflow
10
+
11
+ | Check | Action and expected result |
12
+ | --- | --- |
13
+ | [ ] Open | Open a GLB, a glTF with external buffers/images, a folder and a model URL; geometry and textures load. Drop files on the viewport. On Web, a fresh load uses the collection's curated view or the file's first camera, including orthographic cameras; restoring a saved view takes precedence. Failed or cancelled loads leave the previous scene usable. |
14
+ | [ ] Examples | Open **Examples** in the toolbar; switch between MuTracer collection and glTF Research Scenes, load a thumbnail and check its camera and authored lighting. |
15
+ | [ ] Scene | Search objects or materials and expand nested groups and meshes; select a material child to open its editor. Select an object from the tree or viewport; selection reveals its ancestors. Hide/show a mesh and a group. Select an authored camera. |
16
+ | [ ] Navigation | Drag to orbit, right-drag to pan, scroll to zoom, **F** to fit. Moving an authored camera continues in Orbit. |
17
+ | [ ] History | Undo/redo material, visibility, lighting and render edits; one continuous drag is one step. Reset all restores defaults. On Web, it also restores the example's render choices and initial camera, or fits the model when no camera is provided. |
18
+ | [ ] Panels | **1–4** opens Lighting, Material, Camera, Render respectively. **[**, **]** toggle scene/inspector; click the active rail tab to close it. **H** hides the interface. Text input must not trigger shortcuts. |
19
+ | [ ] Playback | **Space** plays/pauses animation when present, otherwise pauses/resumes rendering. Choose another clip and seek the timeline. |
20
+ | [ ] Render dock | Change sample target, viewport resolution and denoising; pause/resume and inspect sample progress, elapsed time and completion. **D** toggles denoising. |
21
+ | [ ] Export | **E** opens a preview; save the displayed viewport PNG. Render Viewport/1080p/2K/4K/Square PNG with a separate sample budget, denoising and optional transparency. Cancel an export and continue using the original image. |
22
+ | [ ] Code/help | Open Code and copy a setup for the corresponding SDK; open keyboard help. **⌘O / Ctrl+O** opens files. |
23
+
24
+ ## Lighting, materials, camera and rendering
25
+
26
+ | Check | Controls and expected result |
27
+ | --- | --- |
28
+ | [ ] Environment | Switch HDRI / Studio and model lighting where available. Load a local HDR or browse Poly Haven; change HDR resolution, intensity and rotation. Failed downloads keep the previous lighting. |
29
+ | [ ] Studio | Choose each rig; adjust intensity, rotation, size, ambient strength and color. |
30
+ | [ ] Background | Switch environment, color, transparent and grounded modes where offered. Adjust projection height/radius; verify auto values and transparency in exported PNG. |
31
+ | [ ] Ground | Enable ground shadow and change opacity; inspect contact with the model. |
32
+ | [ ] Material selection | Pick a mesh/material, check shared-material edits, apply a preset and reset to loaded values. |
33
+ | [ ] Material layers | Exercise base color/weight, metalness, roughness, diffuse roughness, IOR/specular, anisotropy, transmission/volume/thin wall, subsurface, coat, fuzz, thin film, emission, opacity/alpha and double-sided/unlit controls where offered. |
34
+ | [ ] Textures | Upload, replace and remove each exposed slot; change Wrap S/T and Repeat S/T, including negative repeats. Apply a Poly Haven set with optional displacement; cancel/replace downloads and switch materials during download. |
35
+ | [ ] Displacement | Change scale, bias, subdivision and smoothing; verify silhouettes and return to zero displacement. |
36
+ | [ ] MaterialX (Web) | Import a .zip package or .mtlx with images; check converted layers and textures. |
37
+ | [ ] Camera | Switch Perspective/Orthographic, adjust FOV, auto-rotate and Fit. Authored cameras keep their projection/FOV until Orbit resumes. |
38
+ | [ ] Depth of field | Enable DOF, adjust f-number, area autofocus and focus frame; move the frame, manually edit focus or pick a point; choose automatic or explicit scene units. Orthographic views disable DOF. |
39
+ | [ ] Quality | Change max samples, bounces, resolution, sampling pattern and clamping. Web also offers a BVH builder. |
40
+ | [ ] Caustics | Toggle light tracing and photon mapping; compare each alone and together on a glass scene. Web's master Caustics switch gates both. |
41
+ | [ ] Denoiser | Toggle JNLM/OIDN; adjust JNLM strength, search/patch radius, albedo/normal tolerance and passes; choose OIDN Standard/Small/Large and tile size. Reset selected parameters. |
42
+ | [ ] Color | Change AgX/ACES/Cineon/Reinhard/Linear, exposure, contrast, saturation, temperature and tint; verify exported colors. |
43
+ | [ ] Effects | Enable bloom, threshold/radius and vignette; turn them off and verify the image returns. |
44
+ | [ ] Debug | Expand/collapse Debug; inspect Final, Albedo, Normal, Depth, Position, Roughness, Diffuse/Specular reflectance, ID and Motion. Adjust channel ranges and ID component; toggle interactive preview and max preview scale. Overlay gestures must not orbit/select the scene. |
45
+ | [ ] Window size | Check wide and narrow windows, hidden panels and long names; controls remain readable and the viewport remains usable. Web also provides the mobile bottom sheet. |
46
+
47
+ ## Platform options and limits
48
+
49
+ | Area | Web | Native |
50
+ | --- | --- | --- |
51
+ | Rendering device | WebGPU; WebGL 2 fallback reloads the page | HWRT / Software BVH; device availability is reported in the UI |
52
+ | Denoising | WebGL 2 supports JNLM; OIDN needs WebGPU | JNLM/OIDN; optional Windows DLSS RR build |
53
+ | HDR display | WebGPU plus compatible browser/display; falls back to SDR | Viewer window is SDR; HDR toggle is disabled |
54
+ | BVH builder | tinybvh / builtin | No builder selector |
55
+ | MaterialX import | .zip or .mtlx with images | No MaterialX import UI |
56
+ | Caustics controls | Master switch plus two methods; methods require WebGPU | Independent method switches; both off disables caustics |
57
+ | File access | Multi-file/folder picker; URLs need CORS | File/folder picker; folder Open selects a top-level glTF/GLB; resources resolve beside the model |
58
+ | Code | JavaScript setup | Rust SDK setup |
59
+ | Examples browser | Popover with optional pinned panel | Toolbar opens a docked panel |
60
+ | Extra native controls | — | Fixed-pixel render sizes, directional key light |
61
+ | Small screens | Mobile sheet and responsive toolbar | Desktop panels; scene list hides when space is limited |
62
+
63
+ Native and Web use different UI toolkits and fonts. Check icon meaning,
64
+ label order, alignment and control availability; file dialogs, window controls,
65
+ font rasterization and responsive behavior follow each platform.
package/docs/viewer.md CHANGED
@@ -21,17 +21,17 @@ resampled to at most 2K width. If a source map is missing, the model still opens
21
21
  and the Lighting panel explains which map is unavailable; choose an environment
22
22
  there to replace it.
23
23
 
24
- glTF Research Scenes automatically use their first camera when available.
25
- For other glTF/GLB imports with cameras, a toast offers **Apply camera** to
26
- use the first camera; dismiss it to keep the Orbit view. You can select any
24
+ MuTracer collection examples open with a curated view, under the procedural
25
+ sky or a Poly Haven HDRI. A few also set their own tone mapping and bloom, which
26
+ return to the defaults when you leave them. Other models with cameras open in their
27
+ first camera. You can select any
27
28
  imported camera in the separate camera toolbar at the bottom right; it appears
28
29
  only for models with cameras. Drag, pan or zoom after selecting a camera to
29
30
  continue in **Orbit** from that view.
30
31
  Hover over a camera in the scene tree and click its camera action to apply it
31
32
  directly. **Fit to model** in the bottom toolbar (or **F**) returns to Orbit
32
33
  and frames the model.
33
- Reloading the current model restores your saved camera selection without
34
- showing the prompt again.
34
+ Reloading the current model restores your saved camera selection.
35
35
 
36
36
  Picking an object in the viewport reveals it in the scene tree and scrolls it
37
37
  into view. Material rows start collapsed; expand a mesh to see its materials.
@@ -54,6 +54,8 @@ https://mutracer.meimu-studio.com/viewer/?hdrOutputEnabled=true&maxSamples=128#m
54
54
  The viewer updates the URL as lighting, render and display settings change.
55
55
  Copy the address bar to share those settings and the selected example model.
56
56
  Settings also work in the hash, such as `#model=dragon&hdrOutputEnabled=true`.
57
+ A link with only `#model=…` opens the example with its own render settings, as
58
+ the Examples menu does. Settings in a link win over the example's.
57
59
 
58
60
  ### Options
59
61
 
package/lib/Display.js CHANGED
@@ -127,7 +127,7 @@ export default class Display {
127
127
  const target = this.compose(render);
128
128
  if (!target)
129
129
  throw new Error('Render has no complete image.');
130
- const image = await target.readPresentation();
130
+ const image = render._cropOutput(await target.readPresentation());
131
131
  const canvas = createCanvas(image.width, image.height, 'convertToBlob' in this.canvas);
132
132
  const context = canvas.getContext('2d');
133
133
  if (!context)
@@ -1 +1 @@
1
- {"version":3,"file":"Display.js","sourceRoot":"","sources":["../src/Display.ts"],"names":[],"mappings":"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","sourcesContent":["import {displayAovKind, validateAovDisplay, type AovIdComponent} from './renderer/aovDisplay.js';\nexport type {AovIdComponent} from './renderer/aovDisplay.js';\nimport {createCanvas} from './util/canvas.js';\nimport type Engine from './Engine.js';\nimport type Render from './Render.js';\nimport type { ToneMapping, DisplayChannel } from './renderer/RenderingPipeline.js';\nimport type { ColorRGB } from './types.js';\nimport { displaySettingsKey, displayFilterSettingsKey, applyDisplaySettings } from './settings/display.js';\n/** Display image channels. Default final. */\nexport type {DisplayChannel} from './renderer/RenderingPipeline.js';\n/** Image encoding options for Display.toBlob. */\nexport interface BlobOptions {\n /** Browser image MIME type. Default image/png. */ type?: string;\n /** Encoder quality in 0–1. Default undefined uses the browser default. */ quality?: number;\n}\n/** Optional shared display settings; defaults match Display. */\nexport interface DisplaySettings {\n /** Tone mapping transform. Default agx. */ toneMapping?: ToneMapping;\n /** Linear exposure multiplier. Default 1. */ toneMappingExposure?: number;\n /** Contrast multiplier. Default 1. */ contrast?: number;\n /** Saturation multiplier. Default 1. */ saturation?: number;\n /** White balance temperature shift in -1–1. Default 0. */ temperature?: number;\n /** Green/magenta tint in -1–1. Default 0. */ tint?: number;\n /** Vignette strength in 0–1. Default 0. */ vignette?: number;\n /** Bloom intensity; 0 disables it. Default 0. */ bloomIntensity?: number;\n /** Bloom luminance threshold. Default 1. */ bloomThreshold?: number;\n /** Bloom radius in 0–1. Default 0.6. */ bloomRadius?: number;\n /** Extended-range WebGPU presentation, ignored on WebGL. Default false. */ hdrOutputEnabled?: boolean;\n /** Display image channel. Default final. AOV channels require explicit Render.aovs. */ channel?: DisplayChannel;\n /** Finite float32 scene-unit depths mapped to black/white; 0 <= near < far. Default [0, 10]. */ aovDepthRange?: readonly [number, number];\n /** Finite positive float32 scene-unit half-range around the origin for position RGB. Default 1. */ aovPositionScale?: number;\n /** Finite positive float32 input-pixel displacement mapped to full motion brightness. Default 16. */ aovMotionScale?: number;\n /** Stable ID lane to colorize. Default object. */ aovIdComponent?: AovIdComponent;\n /** Linear shadow-catcher color. Default black. */ shadowCatcherColor?: ColorRGB;\n /** Shadow-catcher opacity multiplier. Default 1. */ shadowCatcherAlpha?: number;\n}\n/** Borrowed WebGPU images for one synchronous display pass. */\nexport interface PostProcessContext {\n /** Engine device; may change after recovery. */ device: GPUDevice;\n /** Record commands here; the library finishes and submits it. */ encoder: GPUCommandEncoder;\n /** Read-only composed display image, top row first; no additional color conversion. */ input: GPUTexture;\n /** Fully write this image. Same format and dimensions as input; supports render, storage and copy writes. */ output: GPUTexture;\n /** Actual render width. */ width: number;\n /** Actual render height. */ height: number;\n /** Selected complete render, including a selected fallback. */ render: Render;\n}\n/** Synchronous display pass. Images and encoder are borrowed only during the call; do not retain or destroy them. */\nexport type PostProcess = (context: PostProcessContext) => void;\n/** Canvas presentation and shared display settings; obtained from engine.display. */\nexport default class Display {\n /** Display canvas. Default the supplied Engine canvas, or a new canvas (OffscreenCanvas without document). */ readonly canvas: HTMLCanvasElement | OffscreenCanvas;\n /** Tone mapping transform. Default agx. */ toneMapping: ToneMapping = 'agx';\n /** Linear exposure multiplier. Default 1. */ toneMappingExposure = 1;\n /** Contrast multiplier. Default 1. */ contrast = 1;\n /** Saturation multiplier. Default 1. */ saturation = 1;\n /** White balance temperature shift in -1–1. Default 0. */ temperature = 0;\n /** Green/magenta tint in -1–1. Default 0. */ tint = 0;\n /** Vignette strength in 0–1. Default 0. */ vignette = 0;\n /** Bloom intensity; 0 disables it. Default 0. */ bloomIntensity = 0;\n /** Bloom luminance threshold. Default 1. */ bloomThreshold = 1;\n /** Bloom radius in 0–1. Default 0.6. */ bloomRadius = 0.6;\n /** Extended-range WebGPU presentation, ignored on WebGL. Default false. */ hdrOutputEnabled = false;\n /** Display image channel. Default final. AOV channels require explicit Render.aovs. */ channel: DisplayChannel = 'final';\n /** Finite float32 scene-unit depths mapped to black/white; 0 <= near < far. Default [0, 10]. */ aovDepthRange: readonly [number, number] = [0, 10];\n /** Finite positive float32 scene-unit half-range around the origin for position RGB. Default 1. */ aovPositionScale = 1;\n /** Finite positive float32 input-pixel displacement mapped to full motion brightness. Default 16. */ aovMotionScale = 16;\n /** Stable ID lane to colorize. Default object. */ aovIdComponent: AovIdComponent = 'object';\n /** Linear shadow-catcher color. Default black. */ shadowCatcherColor: ColorRGB = [0, 0, 0];\n /** Shadow-catcher opacity multiplier. Default 1. */ shadowCatcherAlpha = 1;\n private constructor(private readonly engine: Engine, canvas: HTMLCanvasElement | OffscreenCanvas) { this.canvas = canvas; }\n /** @internal */ static _create(engine: Engine, canvas: HTMLCanvasElement | OffscreenCanvas) { return new Display(engine, canvas); }\n /** Resizes the drawing buffer; scale-based renders resize at their next refine. */\n setSize(width: number, height: number) {\n if (!Number.isFinite(width) || !Number.isFinite(height) || width < 1 || height < 1) throw new Error('Display dimensions must be positive.');\n width = Math.round(width); height = Math.round(height);\n if (this.canvas.width !== width) this.canvas.width = width;\n if (this.canvas.height !== height) this.canvas.height = height;\n }\n private readonly _filterSettings = new WeakMap<Render, string>();\n private readonly settings = new WeakMap<Render, string>();\n /** @internal */ _configure(render: Render) {\n if (render._engine !== this.engine) throw new Error('Render belongs to another engine.');\n const target = render._output;\n if (!target) return;\n const kind = displayAovKind(this.channel);\n if (!kind && !['final','normal','albedo','shadow','shadowCatcher'].includes(this.channel)) throw new RangeError('Unknown display channel');\n if (kind && !render.aovs.includes(kind)) throw new Error(`AOV ${kind} is not enabled on the selected render`);\n validateAovDisplay({depthRange: this.aovDepthRange, positionScale: this.aovPositionScale, motionScale: this.aovMotionScale, idComponent: this.aovIdComponent});\n const key = displaySettingsKey(this);\n const filterKey = displayFilterSettingsKey(this);\n const previous = this.settings.get(render);\n if (previous !== key) {\n this.settings.set(render, key);\n if (previous !== undefined && this._filterSettings.get(render) !== filterKey) render._displayChanged();\n this._filterSettings.set(render, filterKey);\n }\n target.activate();\n applyDisplaySettings(this, target);\n }\n private readonly _postProcesses: {pass: PostProcess}[] = [];\n private _warnedPostProcess = false;\n /** Registers a display pass in order and returns an idempotent removal function. Default no passes. WebGPU only; WebGL skips passes with one warning. Runs after composition on every present/toBlob, without tracing or invalidating denoising. A thrown error discards the whole chain, presents the original image and emits engine error. */\n addPostProcess(pass: PostProcess): () => void {\n const entry = {pass};\n this._postProcesses.push(entry);\n if (this.engine.backend === 'webgl') this.warnPostProcess();\n return () => { const index = this._postProcesses.indexOf(entry); if (index >= 0) this._postProcesses.splice(index, 1); };\n }\n private warnPostProcess() {\n if (!this._warnedPostProcess) { this._warnedPostProcess = true; console.warn('Display post-processing requires WebGPU; passes are ignored on WebGL.'); }\n }\n private compose(render: Render) {\n if (render._engine !== this.engine) throw new Error('Render belongs to another engine.');\n const target = render._output;\n if (!target) return;\n const kind = displayAovKind(this.channel);\n if (!kind && !target.presentationComplete) return;\n if (kind && !render.aovs.includes(kind)) throw new Error(`AOV ${kind} is not enabled on the selected render`);\n if (kind && target.aovReady && !target.aovReady(kind)) return;\n this._configure(render);\n target.drawPostProcess();\n if (this._postProcesses.length && this.engine.backend === 'webgl') this.warnPostProcess();\n try { target.applyPostProcesses?.(this._postProcesses.map(entry => entry.pass), render); }\n catch (error) { this.engine._notifyError(error); }\n return target;\n }\n /** Presents a complete image or fallback. Keeps fallback visible until the first intermediate filter finishes. Does not trace. */\n present(render: Render): Render | undefined {\n if (render._engine !== this.engine) throw new Error('Render belongs to another engine.');\n const ready = this.channel === 'final' && render.fallback ? render._presentable : render._completeFor(this.channel);\n const fallback = render.fallback;\n const selected = ready ? render : fallback && (this.channel === 'final' ? fallback._presentable : fallback._completeFor(this.channel)) ? fallback : undefined;\n if (!selected) return;\n const target = this.compose(selected);\n if (!target) return;\n this.engine._present(target);\n return selected;\n }\n /** Encodes this render at its actual dimensions using these settings; leaves the display canvas unchanged. */\n async toBlob(render: Render, options: BlobOptions = {}): Promise<Blob> {\n const target = this.compose(render);\n if (!target) throw new Error('Render has no complete image.');\n const image = await target.readPresentation();\n const canvas = createCanvas(image.width, image.height, 'convertToBlob' in this.canvas);\n const context = canvas.getContext('2d') as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D | null;\n if (!context) throw new Error('The image encoding context is not available.');\n const bytes = new Uint8ClampedArray(image.data.length);\n for (let i = 0; i < bytes.length; i++) bytes[i] = Math.round(Math.min(1, Math.max(0, image.data[i])) * 255);\n context.putImageData(new ImageData(bytes, image.width, image.height), 0, 0);\n if ('convertToBlob' in canvas) return canvas.convertToBlob({type: options.type ?? 'image/png', quality: options.quality});\n return new Promise((resolve, reject) => canvas.toBlob(blob => blob ? resolve(blob) : reject(new Error('The image could not be encoded')), options.type ?? 'image/png', options.quality));\n }\n}\n"]}
1
+ {"version":3,"file":"Display.js","sourceRoot":"","sources":["../src/Display.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,cAAc,EAAE,kBAAkB,EAAsB,MAAM,0BAA0B,CAAC;AAEjG,OAAO,EAAC,YAAY,EAAC,MAAM,kBAAkB,CAAC;AAK9C,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAyC3G,qFAAqF;AACrF,MAAM,CAAC,OAAO,OAAO,OAAO;IAoBW;IAnBrC,8GAA8G,CAAU,MAAM,CAAsC;IACpK,2CAA2C,CAAC,WAAW,GAAgB,KAAK,CAAC;IAC7E,6CAA6C,CAAC,mBAAmB,GAAG,CAAC,CAAC;IACtE,sCAAsC,CAAC,QAAQ,GAAG,CAAC,CAAC;IACpD,wCAAwC,CAAC,UAAU,GAAG,CAAC,CAAC;IACxD,0DAA0D,CAAC,WAAW,GAAG,CAAC,CAAC;IAC3E,6CAA6C,CAAC,IAAI,GAAG,CAAC,CAAC;IACvD,2CAA2C,CAAC,QAAQ,GAAG,CAAC,CAAC;IACzD,iDAAiD,CAAC,cAAc,GAAG,CAAC,CAAC;IACrE,4CAA4C,CAAC,cAAc,GAAG,CAAC,CAAC;IAChE,wCAAwC,CAAC,WAAW,GAAG,GAAG,CAAC;IAC3D,2EAA2E,CAAC,gBAAgB,GAAG,KAAK,CAAC;IACrG,uFAAuF,CAAC,OAAO,GAAmB,OAAO,CAAC;IAC1H,gGAAgG,CAAC,aAAa,GAA8B,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACpJ,mGAAmG,CAAC,gBAAgB,GAAG,CAAC,CAAC;IACzH,qGAAqG,CAAC,cAAc,GAAG,EAAE,CAAC;IAC1H,kDAAkD,CAAC,cAAc,GAAmB,QAAQ,CAAC;IAC7F,kDAAkD,CAAC,kBAAkB,GAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAC5F,oDAAoD,CAAC,kBAAkB,GAAG,CAAC,CAAC;IAC5E,YAAqC,MAAc,EAAE,MAA2C;QAA3D,WAAM,GAAN,MAAM,CAAQ;QAAiD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IAAC,CAAC;IAC3H,gBAAgB,CAAC,MAAM,CAAC,OAAO,CAAC,MAAc,EAAE,MAA2C,IAAI,OAAO,IAAI,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;IACpI,mFAAmF;IACnF,OAAO,CAAC,KAAa,EAAE,MAAc;QACnC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,MAAM,GAAG,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;QAC5I,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACvD,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,KAAK,KAAK;YAAE,IAAI,CAAC,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC;QAC3D,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,MAAM;YAAE,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC;IACjE,CAAC;IACgB,eAAe,GAAG,IAAI,OAAO,EAAkB,CAAC;IAChD,QAAQ,GAAG,IAAI,OAAO,EAAkB,CAAC;IAC1D,gBAAgB,CAAC,UAAU,CAAC,MAAc;QACxC,IAAI,MAAM,CAAC,OAAO,KAAK,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;QACzF,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC;QAC9B,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1C,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,OAAO,EAAC,QAAQ,EAAC,QAAQ,EAAC,QAAQ,EAAC,eAAe,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC;YAAE,MAAM,IAAI,UAAU,CAAC,yBAAyB,CAAC,CAAC;QAC3I,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,OAAO,IAAI,wCAAwC,CAAC,CAAC;QAC9G,kBAAkB,CAAC,EAAC,UAAU,EAAE,IAAI,CAAC,aAAa,EAAE,aAAa,EAAE,IAAI,CAAC,gBAAgB,EAAE,WAAW,EAAE,IAAI,CAAC,cAAc,EAAE,WAAW,EAAE,IAAI,CAAC,cAAc,EAAC,CAAC,CAAC;QAC/J,MAAM,GAAG,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;QACrC,MAAM,SAAS,GAAG,wBAAwB,CAAC,IAAI,CAAC,CAAC;QACjD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC3C,IAAI,QAAQ,KAAK,GAAG,EAAE,CAAC;YACrB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;YAC/B,IAAI,QAAQ,KAAK,SAAS,IAAI,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,SAAS;gBAAE,MAAM,CAAC,eAAe,EAAE,CAAC;YACvG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;QAC9C,CAAC;QACD,MAAM,CAAC,QAAQ,EAAE,CAAC;QAClB,oBAAoB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACrC,CAAC;IACgB,cAAc,GAA0B,EAAE,CAAC;IACpD,kBAAkB,GAAG,KAAK,CAAC;IACnC,iVAAiV;IACjV,cAAc,CAAC,IAAiB;QAC9B,MAAM,KAAK,GAAG,EAAC,IAAI,EAAC,CAAC;QACrB,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAChC,IAAI,IAAI,CAAC,MAAM,CAAC,OAAO,KAAK,OAAO;YAAE,IAAI,CAAC,eAAe,EAAE,CAAC;QAC5D,OAAO,GAAG,EAAE,GAAG,MAAM,KAAK,GAAG,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC;YAAE,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3H,CAAC;IACO,eAAe;QACrB,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC;YAAC,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC;YAAC,OAAO,CAAC,IAAI,CAAC,uEAAuE,CAAC,CAAC;QAAC,CAAC;IAC1J,CAAC;IACO,OAAO,CAAC,MAAc;QAC5B,IAAI,MAAM,CAAC,OAAO,KAAK,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;QACzF,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC;QAC9B,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1C,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,oBAAoB;YAAE,OAAO;QAClD,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,OAAO,IAAI,wCAAwC,CAAC,CAAC;QAC9G,IAAI,IAAI,IAAI,MAAM,CAAC,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,OAAO;QAC9D,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QACxB,MAAM,CAAC,eAAe,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,cAAc,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,OAAO,KAAK,OAAO;YAAE,IAAI,CAAC,eAAe,EAAE,CAAC;QAC1F,IAAI,CAAC;YAAC,MAAM,CAAC,kBAAkB,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;QAAC,CAAC;QAC1F,OAAO,KAAK,EAAE,CAAC;YAAC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;QAAC,CAAC;QAClD,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,kIAAkI;IAClI,OAAO,CAAC,MAAc;QACpB,IAAI,MAAM,CAAC,OAAO,KAAK,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;QACzF,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,KAAK,OAAO,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACpH,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;QACjC,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;QAC9J,IAAI,CAAC,QAAQ;YAAE,OAAO;QACtB,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACtC,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC7B,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,8GAA8G;IAC9G,KAAK,CAAC,MAAM,CAAC,MAAc,EAAE,UAAuB,EAAE;QACpD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACpC,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;QAC9D,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,MAAM,CAAC,gBAAgB,EAAE,CAAC,CAAC;QAClE,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,eAAe,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC;QACvF,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAwE,CAAC;QAC/G,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;QAC9E,MAAM,KAAK,GAAG,IAAI,iBAAiB,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACvD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE;YAAE,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC;QAC5G,OAAO,CAAC,YAAY,CAAC,IAAI,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QAC5E,IAAI,eAAe,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC,aAAa,CAAC,EAAC,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,WAAW,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAC,CAAC,CAAC;QAC1H,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC,EAAE,OAAO,CAAC,IAAI,IAAI,WAAW,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;IAC3L,CAAC;CACF","sourcesContent":["import {displayAovKind, validateAovDisplay, type AovIdComponent} from './renderer/aovDisplay.js';\nexport type {AovIdComponent} from './renderer/aovDisplay.js';\nimport {createCanvas} from './util/canvas.js';\nimport type Engine from './Engine.js';\nimport type Render from './Render.js';\nimport type { ToneMapping, DisplayChannel } from './renderer/RenderingPipeline.js';\nimport type { ColorRGB } from './types.js';\nimport { displaySettingsKey, displayFilterSettingsKey, applyDisplaySettings } from './settings/display.js';\n/** Display image channels. Default final. */\nexport type {DisplayChannel} from './renderer/RenderingPipeline.js';\n/** Image encoding options for Display.toBlob. */\nexport interface BlobOptions {\n /** Browser image MIME type. Default image/png. */ type?: string;\n /** Encoder quality in 0–1. Default undefined uses the browser default. */ quality?: number;\n}\n/** Optional shared display settings; defaults match Display. */\nexport interface DisplaySettings {\n /** Tone mapping transform. Default agx. */ toneMapping?: ToneMapping;\n /** Linear exposure multiplier. Default 1. */ toneMappingExposure?: number;\n /** Contrast multiplier. Default 1. */ contrast?: number;\n /** Saturation multiplier. Default 1. */ saturation?: number;\n /** White balance temperature shift in -1–1. Default 0. */ temperature?: number;\n /** Green/magenta tint in -1–1. Default 0. */ tint?: number;\n /** Vignette strength in 0–1. Default 0. */ vignette?: number;\n /** Bloom intensity; 0 disables it. Default 0. */ bloomIntensity?: number;\n /** Bloom luminance threshold. Default 1. */ bloomThreshold?: number;\n /** Bloom radius in 0–1. Default 0.6. */ bloomRadius?: number;\n /** Extended-range WebGPU presentation, ignored on WebGL. Default false. */ hdrOutputEnabled?: boolean;\n /** Display image channel. Default final. AOV channels require explicit Render.aovs. */ channel?: DisplayChannel;\n /** Finite float32 scene-unit depths mapped to black/white; 0 <= near < far. Default [0, 10]. */ aovDepthRange?: readonly [number, number];\n /** Finite positive float32 scene-unit half-range around the origin for position RGB. Default 1. */ aovPositionScale?: number;\n /** Finite positive float32 input-pixel displacement mapped to full motion brightness. Default 16. */ aovMotionScale?: number;\n /** Stable ID lane to colorize. Default object. */ aovIdComponent?: AovIdComponent;\n /** Linear shadow-catcher color. Default black. */ shadowCatcherColor?: ColorRGB;\n /** Shadow-catcher opacity multiplier. Default 1. */ shadowCatcherAlpha?: number;\n}\n/** Borrowed WebGPU images for one synchronous display pass. */\nexport interface PostProcessContext {\n /** Engine device; may change after recovery. */ device: GPUDevice;\n /** Record commands here; the library finishes and submits it. */ encoder: GPUCommandEncoder;\n /** Read-only composed display image, top row first; no additional color conversion. */ input: GPUTexture;\n /** Fully write this image. Same format and dimensions as input; supports render, storage and copy writes. */ output: GPUTexture;\n /** Actual render width. */ width: number;\n /** Actual render height. */ height: number;\n /** Selected complete render, including a selected fallback. */ render: Render;\n}\n/** Synchronous display pass. Images and encoder are borrowed only during the call; do not retain or destroy them. */\nexport type PostProcess = (context: PostProcessContext) => void;\n/** Canvas presentation and shared display settings; obtained from engine.display. */\nexport default class Display {\n /** Display canvas. Default the supplied Engine canvas, or a new canvas (OffscreenCanvas without document). */ readonly canvas: HTMLCanvasElement | OffscreenCanvas;\n /** Tone mapping transform. Default agx. */ toneMapping: ToneMapping = 'agx';\n /** Linear exposure multiplier. Default 1. */ toneMappingExposure = 1;\n /** Contrast multiplier. Default 1. */ contrast = 1;\n /** Saturation multiplier. Default 1. */ saturation = 1;\n /** White balance temperature shift in -1–1. Default 0. */ temperature = 0;\n /** Green/magenta tint in -1–1. Default 0. */ tint = 0;\n /** Vignette strength in 0–1. Default 0. */ vignette = 0;\n /** Bloom intensity; 0 disables it. Default 0. */ bloomIntensity = 0;\n /** Bloom luminance threshold. Default 1. */ bloomThreshold = 1;\n /** Bloom radius in 0–1. Default 0.6. */ bloomRadius = 0.6;\n /** Extended-range WebGPU presentation, ignored on WebGL. Default false. */ hdrOutputEnabled = false;\n /** Display image channel. Default final. AOV channels require explicit Render.aovs. */ channel: DisplayChannel = 'final';\n /** Finite float32 scene-unit depths mapped to black/white; 0 <= near < far. Default [0, 10]. */ aovDepthRange: readonly [number, number] = [0, 10];\n /** Finite positive float32 scene-unit half-range around the origin for position RGB. Default 1. */ aovPositionScale = 1;\n /** Finite positive float32 input-pixel displacement mapped to full motion brightness. Default 16. */ aovMotionScale = 16;\n /** Stable ID lane to colorize. Default object. */ aovIdComponent: AovIdComponent = 'object';\n /** Linear shadow-catcher color. Default black. */ shadowCatcherColor: ColorRGB = [0, 0, 0];\n /** Shadow-catcher opacity multiplier. Default 1. */ shadowCatcherAlpha = 1;\n private constructor(private readonly engine: Engine, canvas: HTMLCanvasElement | OffscreenCanvas) { this.canvas = canvas; }\n /** @internal */ static _create(engine: Engine, canvas: HTMLCanvasElement | OffscreenCanvas) { return new Display(engine, canvas); }\n /** Resizes the drawing buffer; scale-based renders resize at their next refine. */\n setSize(width: number, height: number) {\n if (!Number.isFinite(width) || !Number.isFinite(height) || width < 1 || height < 1) throw new Error('Display dimensions must be positive.');\n width = Math.round(width); height = Math.round(height);\n if (this.canvas.width !== width) this.canvas.width = width;\n if (this.canvas.height !== height) this.canvas.height = height;\n }\n private readonly _filterSettings = new WeakMap<Render, string>();\n private readonly settings = new WeakMap<Render, string>();\n /** @internal */ _configure(render: Render) {\n if (render._engine !== this.engine) throw new Error('Render belongs to another engine.');\n const target = render._output;\n if (!target) return;\n const kind = displayAovKind(this.channel);\n if (!kind && !['final','normal','albedo','shadow','shadowCatcher'].includes(this.channel)) throw new RangeError('Unknown display channel');\n if (kind && !render.aovs.includes(kind)) throw new Error(`AOV ${kind} is not enabled on the selected render`);\n validateAovDisplay({depthRange: this.aovDepthRange, positionScale: this.aovPositionScale, motionScale: this.aovMotionScale, idComponent: this.aovIdComponent});\n const key = displaySettingsKey(this);\n const filterKey = displayFilterSettingsKey(this);\n const previous = this.settings.get(render);\n if (previous !== key) {\n this.settings.set(render, key);\n if (previous !== undefined && this._filterSettings.get(render) !== filterKey) render._displayChanged();\n this._filterSettings.set(render, filterKey);\n }\n target.activate();\n applyDisplaySettings(this, target);\n }\n private readonly _postProcesses: {pass: PostProcess}[] = [];\n private _warnedPostProcess = false;\n /** Registers a display pass in order and returns an idempotent removal function. Default no passes. WebGPU only; WebGL skips passes with one warning. Runs after composition on every present/toBlob, without tracing or invalidating denoising. A thrown error discards the whole chain, presents the original image and emits engine error. */\n addPostProcess(pass: PostProcess): () => void {\n const entry = {pass};\n this._postProcesses.push(entry);\n if (this.engine.backend === 'webgl') this.warnPostProcess();\n return () => { const index = this._postProcesses.indexOf(entry); if (index >= 0) this._postProcesses.splice(index, 1); };\n }\n private warnPostProcess() {\n if (!this._warnedPostProcess) { this._warnedPostProcess = true; console.warn('Display post-processing requires WebGPU; passes are ignored on WebGL.'); }\n }\n private compose(render: Render) {\n if (render._engine !== this.engine) throw new Error('Render belongs to another engine.');\n const target = render._output;\n if (!target) return;\n const kind = displayAovKind(this.channel);\n if (!kind && !target.presentationComplete) return;\n if (kind && !render.aovs.includes(kind)) throw new Error(`AOV ${kind} is not enabled on the selected render`);\n if (kind && target.aovReady && !target.aovReady(kind)) return;\n this._configure(render);\n target.drawPostProcess();\n if (this._postProcesses.length && this.engine.backend === 'webgl') this.warnPostProcess();\n try { target.applyPostProcesses?.(this._postProcesses.map(entry => entry.pass), render); }\n catch (error) { this.engine._notifyError(error); }\n return target;\n }\n /** Presents a complete image or fallback. Keeps fallback visible until the first intermediate filter finishes. Does not trace. */\n present(render: Render): Render | undefined {\n if (render._engine !== this.engine) throw new Error('Render belongs to another engine.');\n const ready = this.channel === 'final' && render.fallback ? render._presentable : render._completeFor(this.channel);\n const fallback = render.fallback;\n const selected = ready ? render : fallback && (this.channel === 'final' ? fallback._presentable : fallback._completeFor(this.channel)) ? fallback : undefined;\n if (!selected) return;\n const target = this.compose(selected);\n if (!target) return;\n this.engine._present(target);\n return selected;\n }\n /** Encodes this render at its actual dimensions using these settings; leaves the display canvas unchanged. */\n async toBlob(render: Render, options: BlobOptions = {}): Promise<Blob> {\n const target = this.compose(render);\n if (!target) throw new Error('Render has no complete image.');\n const image = render._cropOutput(await target.readPresentation());\n const canvas = createCanvas(image.width, image.height, 'convertToBlob' in this.canvas);\n const context = canvas.getContext('2d') as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D | null;\n if (!context) throw new Error('The image encoding context is not available.');\n const bytes = new Uint8ClampedArray(image.data.length);\n for (let i = 0; i < bytes.length; i++) bytes[i] = Math.round(Math.min(1, Math.max(0, image.data[i])) * 255);\n context.putImageData(new ImageData(bytes, image.width, image.height), 0, 0);\n if ('convertToBlob' in canvas) return canvas.convertToBlob({type: options.type ?? 'image/png', quality: options.quality});\n return new Promise((resolve, reject) => canvas.toBlob(blob => blob ? resolve(blob) : reject(new Error('The image could not be encoded')), options.type ?? 'image/png', options.quality));\n }\n}\n"]}
package/lib/Engine.d.ts CHANGED
@@ -67,6 +67,8 @@ export default class Engine {
67
67
  private _bvhWorkerPool?;
68
68
  private _bvhPreparationRevision;
69
69
  private _bvhPreparation?;
70
+ private _bvhPreparationController?;
71
+ private _bvhFailedRevision;
70
72
  private _bvhPreparationTail;
71
73
  private _tinyBvhUnavailable;
72
74
  private _bvhWorkerUnavailable;