@meimu/mutracer 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/CHANGELOG.md +222 -0
  2. package/LICENSES/FidelityFX-FSR.txt +19 -0
  3. package/LICENSES/pbrt-v4-Apache-2.0.txt +202 -0
  4. package/THIRD_PARTY_NOTICES.md +18 -4
  5. package/dist/assets/bvh.worker.js +1 -1
  6. package/dist/mutracer.umd.js +6260 -5580
  7. package/docs/api.md +45 -18
  8. package/docs/materials.md +4 -2
  9. package/docs/performance.md +7 -0
  10. package/docs/post-processing.md +62 -9
  11. package/docs/progressive-rendering.md +3 -0
  12. package/docs/sampling.md +4 -0
  13. package/docs/technical/acceleration.md +52 -0
  14. package/docs/technical/aovs.md +79 -0
  15. package/docs/technical/caustics.md +109 -0
  16. package/docs/technical/continuous-spectrum.md +29 -0
  17. package/docs/technical/display-upscale.md +82 -0
  18. package/docs/technical/openpbr.md +99 -0
  19. package/docs/technical/pbrt-import.md +168 -0
  20. package/docs/technical/tiling.md +74 -0
  21. package/docs/technical/webgl.md +53 -0
  22. package/docs/viewer.md +11 -1
  23. package/lib/Display.d.ts +19 -5
  24. package/lib/Display.js +10 -7
  25. package/lib/Display.js.map +1 -1
  26. package/lib/Engine.d.ts +2 -1
  27. package/lib/Engine.js +30 -2
  28. package/lib/Engine.js.map +1 -1
  29. package/lib/Render.d.ts +20 -11
  30. package/lib/Render.js +32 -7
  31. package/lib/Render.js.map +1 -1
  32. package/lib/main.d.ts +3 -1
  33. package/lib/main.js +1 -0
  34. package/lib/main.js.map +1 -1
  35. package/lib/oidn.d.ts +1 -1
  36. package/lib/renderer/AovPrimitiveCache.js +39 -8
  37. package/lib/renderer/AovPrimitiveCache.js.map +1 -1
  38. package/lib/renderer/DisplayPostProcess.d.ts +8 -3
  39. package/lib/renderer/DisplayPostProcess.js +51 -18
  40. package/lib/renderer/DisplayPostProcess.js.map +1 -1
  41. package/lib/renderer/MaterialData.js +7 -2
  42. package/lib/renderer/MaterialData.js.map +1 -1
  43. package/lib/renderer/RenderingPipeline.d.ts +7 -1
  44. package/lib/renderer/RenderingPipeline.js.map +1 -1
  45. package/lib/renderer/TileRender.d.ts +2 -2
  46. package/lib/renderer/TileRender.js +128 -7
  47. package/lib/renderer/TileRender.js.map +1 -1
  48. package/lib/renderer/WGPUAov.d.ts +6 -1
  49. package/lib/renderer/WGPUAov.js +108 -14
  50. package/lib/renderer/WGPUAov.js.map +1 -1
  51. package/lib/renderer/WGPUCanvasSurface.js +12 -1
  52. package/lib/renderer/WGPUCanvasSurface.js.map +1 -1
  53. package/lib/renderer/WGPUEnvMapDistribution.js +19 -19
  54. package/lib/renderer/WGPUFullQuadPass.js +3 -3
  55. package/lib/renderer/WGPURayTraceOutput.js +12 -12
  56. package/lib/renderer/WGPURayTracePass.d.ts +17 -4
  57. package/lib/renderer/WGPURayTracePass.js +194 -71
  58. package/lib/renderer/WGPURayTracePass.js.map +1 -1
  59. package/lib/renderer/WGPURenderingPipeline.js +46 -40
  60. package/lib/renderer/WGPURenderingPipeline.js.map +1 -1
  61. package/lib/renderer/WebGLAov.js +3 -3
  62. package/lib/renderer/WebGLAov.js.map +1 -1
  63. package/lib/renderer/WebGLFullQuadPass.d.ts +1 -1
  64. package/lib/renderer/WebGLFullQuadPass.js +8 -2
  65. package/lib/renderer/WebGLFullQuadPass.js.map +1 -1
  66. package/lib/renderer/WebGLJNLMDenoiser.js +66 -66
  67. package/lib/renderer/WebGLJNLMDenoiser.js.map +1 -1
  68. package/lib/renderer/WebGLRayTraceOutputPass.js +4 -4
  69. package/lib/renderer/WebGLRayTracePass.d.ts +14 -10
  70. package/lib/renderer/WebGLRayTracePass.js +115 -111
  71. package/lib/renderer/WebGLRayTracePass.js.map +1 -1
  72. package/lib/renderer/WebGLRenderingPipeline.js +10 -3
  73. package/lib/renderer/WebGLRenderingPipeline.js.map +1 -1
  74. package/lib/renderer/WebGLSkyBaker.js +57 -57
  75. package/lib/renderer/aov.d.ts +13 -1
  76. package/lib/renderer/aov.js +21 -2
  77. package/lib/renderer/aov.js.map +1 -1
  78. package/lib/renderer/aovDisplay.js +2 -2
  79. package/lib/renderer/aovDisplay.js.map +1 -1
  80. package/lib/renderer/binnedSahBvh.d.ts +2 -1
  81. package/lib/renderer/binnedSahBvh.js +24 -17
  82. package/lib/renderer/binnedSahBvh.js.map +1 -1
  83. package/lib/renderer/bvhAccel.d.ts +3 -1
  84. package/lib/renderer/bvhAccel.js +36 -6
  85. package/lib/renderer/bvhAccel.js.map +1 -1
  86. package/lib/renderer/cameraUniform.d.ts +1 -1
  87. package/lib/renderer/cameraUniform.js +6 -2
  88. package/lib/renderer/cameraUniform.js.map +1 -1
  89. package/lib/renderer/compactVertices.d.ts +14 -0
  90. package/lib/renderer/compactVertices.js +124 -0
  91. package/lib/renderer/compactVertices.js.map +1 -0
  92. package/lib/renderer/easu.d.ts +11 -0
  93. package/lib/renderer/easu.js +156 -0
  94. package/lib/renderer/easu.js.map +1 -0
  95. package/lib/renderer/geometryUpdateState.d.ts +1 -0
  96. package/lib/renderer/geometryUpdateState.js.map +1 -1
  97. package/lib/renderer/geometryUpdates.js +3 -2
  98. package/lib/renderer/geometryUpdates.js.map +1 -1
  99. package/lib/renderer/glsl/aovDisplay.frag.js +5 -5
  100. package/lib/renderer/glsl/aovReflectance.glsl.js +35 -35
  101. package/lib/renderer/glsl/aovReflectanceSamples.glsl.js +1 -1
  102. package/lib/renderer/glsl/aovTrace.frag.js +44 -36
  103. package/lib/renderer/glsl/aovTrace.frag.js.map +1 -1
  104. package/lib/renderer/glsl/chunks/cameraProjection.glsl.d.ts +2 -0
  105. package/lib/renderer/glsl/chunks/cameraProjection.glsl.js +16 -0
  106. package/lib/renderer/glsl/chunks/cameraProjection.glsl.js.map +1 -0
  107. package/lib/renderer/glsl/chunks/cameraRay.glsl.d.ts +1 -1
  108. package/lib/renderer/glsl/chunks/cameraRay.glsl.js +9 -16
  109. package/lib/renderer/glsl/chunks/cameraRay.glsl.js.map +1 -1
  110. package/lib/renderer/glsl/chunks/emissiveLight.glsl.js +8 -8
  111. package/lib/renderer/glsl/chunks/envMap.glsl.js +67 -67
  112. package/lib/renderer/glsl/chunks/intersect.glsl.js +390 -375
  113. package/lib/renderer/glsl/chunks/intersect.glsl.js.map +1 -1
  114. package/lib/renderer/glsl/chunks/materialBuffer.glsl.js +120 -120
  115. package/lib/renderer/glsl/chunks/openPbrBsdf.glsl.js +562 -562
  116. package/lib/renderer/glsl/chunks/rand.glsl.js +3 -3
  117. package/lib/renderer/glsl/chunks/random.glsl.js +4 -4
  118. package/lib/renderer/glsl/chunks/rayTraceCore.glsl.js +37 -35
  119. package/lib/renderer/glsl/chunks/rayTraceCore.glsl.js.map +1 -1
  120. package/lib/renderer/glsl/chunks/sampleLight.glsl.js +70 -74
  121. package/lib/renderer/glsl/chunks/sampleLight.glsl.js.map +1 -1
  122. package/lib/renderer/glsl/chunks/sampleMaterial.glsl.d.ts +1 -1
  123. package/lib/renderer/glsl/chunks/sampleMaterial.glsl.js +107 -88
  124. package/lib/renderer/glsl/chunks/sampleMaterial.glsl.js.map +1 -1
  125. package/lib/renderer/glsl/chunks/sampleShadowCatcher.glsl.js +11 -11
  126. package/lib/renderer/glsl/chunks/sampling.glsl.js +77 -77
  127. package/lib/renderer/glsl/chunks/sobol.glsl.js +70 -70
  128. package/lib/renderer/glsl/chunks/spectralPrefix.glsl.d.ts +6 -0
  129. package/lib/renderer/glsl/chunks/spectralPrefix.glsl.js +47 -0
  130. package/lib/renderer/glsl/chunks/spectralPrefix.glsl.js.map +1 -0
  131. package/lib/renderer/glsl/chunks/textureLinear.glsl.js +5 -5
  132. package/lib/renderer/glsl/chunks/uvProjection.glsl.js +11 -11
  133. package/lib/renderer/glsl/depthQuery.frag.js +12 -6
  134. package/lib/renderer/glsl/depthQuery.frag.js.map +1 -1
  135. package/lib/renderer/glsl/normalizeRayTraceOutput.frag.js +10 -10
  136. package/lib/renderer/glsl/rayTrace.frag.js +233 -170
  137. package/lib/renderer/glsl/rayTrace.frag.js.map +1 -1
  138. package/lib/renderer/glsl/shadowCatcher.frag.js +10 -9
  139. package/lib/renderer/glsl/shadowCatcher.frag.js.map +1 -1
  140. package/lib/renderer/glsl/webglPipeline.frag.js +58 -58
  141. package/lib/renderer/gpuMemory.js +1 -1
  142. package/lib/renderer/gpuMemory.js.map +1 -1
  143. package/lib/renderer/openPbrEnergyLut.d.ts +2 -1
  144. package/lib/renderer/openPbrEnergyLut.js +18 -1
  145. package/lib/renderer/openPbrEnergyLut.js.map +1 -1
  146. package/lib/renderer/pathLimits.d.ts +5 -4
  147. package/lib/renderer/pathLimits.js +6 -5
  148. package/lib/renderer/pathLimits.js.map +1 -1
  149. package/lib/renderer/prepareMeshBVH.d.ts +2 -0
  150. package/lib/renderer/prepareMeshBVH.js +5 -3
  151. package/lib/renderer/prepareMeshBVH.js.map +1 -1
  152. package/lib/renderer/progressiveScheduler.js +1 -1
  153. package/lib/renderer/progressiveScheduler.js.map +1 -1
  154. package/lib/renderer/renderPipeline.d.ts +4 -0
  155. package/lib/renderer/renderPipeline.js +1 -0
  156. package/lib/renderer/renderPipeline.js.map +1 -1
  157. package/lib/renderer/researchTransport.d.ts +1 -1
  158. package/lib/renderer/researchTransport.js.map +1 -1
  159. package/lib/renderer/sceneAssembly.d.ts +1 -1
  160. package/lib/renderer/sceneAssembly.js +4 -3
  161. package/lib/renderer/sceneAssembly.js.map +1 -1
  162. package/lib/renderer/sceneChangeDetector.d.ts +1 -1
  163. package/lib/renderer/sceneChangeDetector.js +3 -1
  164. package/lib/renderer/sceneChangeDetector.js.map +1 -1
  165. package/lib/renderer/sceneLayoutSchema.d.ts +1 -0
  166. package/lib/renderer/sceneLayoutSchema.js +2 -2
  167. package/lib/renderer/sceneLayoutSchema.js.map +1 -1
  168. package/lib/renderer/scenePreparation.d.ts +3 -2
  169. package/lib/renderer/scenePreparation.js +18 -2
  170. package/lib/renderer/scenePreparation.js.map +1 -1
  171. package/lib/renderer/scratchSampling.js +6 -6
  172. package/lib/renderer/spectrum.js +2 -2
  173. package/lib/renderer/staticBvhPreparation.d.ts +9 -0
  174. package/lib/renderer/staticBvhPreparation.js +8 -2
  175. package/lib/renderer/staticBvhPreparation.js.map +1 -1
  176. package/lib/renderer/surfaceHit.d.ts +10 -0
  177. package/lib/renderer/surfaceHit.js +22 -0
  178. package/lib/renderer/surfaceHit.js.map +1 -0
  179. package/lib/renderer/tangentRequirements.d.ts +2 -0
  180. package/lib/renderer/tangentRequirements.js +7 -0
  181. package/lib/renderer/tangentRequirements.js.map +1 -0
  182. package/lib/renderer/textureAtlas.d.ts +4 -0
  183. package/lib/renderer/textureAtlas.js +48 -2
  184. package/lib/renderer/textureAtlas.js.map +1 -1
  185. package/lib/renderer/webgl.js +4 -0
  186. package/lib/renderer/webgl.js.map +1 -1
  187. package/lib/renderer/webglVertexAttributes.d.ts +17 -0
  188. package/lib/renderer/webglVertexAttributes.js +44 -0
  189. package/lib/renderer/webglVertexAttributes.js.map +1 -0
  190. package/lib/renderer/wgpuBindlessSource.js +2 -2
  191. package/lib/renderer/wgpuMaterialConstants.js +4 -3
  192. package/lib/renderer/wgpuMaterialConstants.js.map +1 -1
  193. package/lib/renderer/wgpuSceneUniform.js +2 -2
  194. package/lib/renderer/wgpuSceneUniform.js.map +1 -1
  195. package/lib/renderer/wgpuShaderSource.d.ts +15 -0
  196. package/lib/renderer/wgpuShaderSource.js +1 -1
  197. package/lib/renderer/wgpuShaderSource.js.map +1 -1
  198. package/lib/renderer/wgsl/aovCompact.wgsl.d.ts +4 -0
  199. package/lib/renderer/wgsl/aovCompact.wgsl.js +26 -0
  200. package/lib/renderer/wgsl/aovCompact.wgsl.js.map +1 -0
  201. package/lib/renderer/wgsl/aovDisplay.wgsl.js +9 -9
  202. package/lib/renderer/wgsl/aovReflectance.wgsl.js +39 -39
  203. package/lib/renderer/wgsl/aovReflectanceSamples.wgsl.js +1 -1
  204. package/lib/renderer/wgsl/aovResolve.wgsl.d.ts +4 -0
  205. package/lib/renderer/wgsl/aovResolve.wgsl.js +21 -2
  206. package/lib/renderer/wgsl/aovResolve.wgsl.js.map +1 -1
  207. package/lib/renderer/wgsl/aovSnapshot.wgsl.js +2 -2
  208. package/lib/renderer/wgsl/aovSnapshot.wgsl.js.map +1 -1
  209. package/lib/renderer/wgsl/aovTrace.wgsl.js +31 -31
  210. package/lib/renderer/wgsl/aovTraceDisplay.wgsl.d.ts +2 -0
  211. package/lib/renderer/wgsl/aovTraceDisplay.wgsl.js +56 -0
  212. package/lib/renderer/wgsl/aovTraceDisplay.wgsl.js.map +1 -0
  213. package/lib/renderer/wgsl/bloom.wgsl.js +18 -18
  214. package/lib/renderer/wgsl/causticLight.compute.d.ts +1 -1
  215. package/lib/renderer/wgsl/causticLight.compute.js +121 -119
  216. package/lib/renderer/wgsl/causticLight.compute.js.map +1 -1
  217. package/lib/renderer/wgsl/chunks/background.wgsl.js +46 -46
  218. package/lib/renderer/wgsl/chunks/causticAdjoint.wgsl.js +4 -4
  219. package/lib/renderer/wgsl/chunks/causticAtomic.wgsl.js +3 -3
  220. package/lib/renderer/wgsl/chunks/caustics.wgsl.js +3 -3
  221. package/lib/renderer/wgsl/chunks/common.wgsl.js +4 -4
  222. package/lib/renderer/wgsl/chunks/envMap.wgsl.js +30 -30
  223. package/lib/renderer/wgsl/chunks/intersect.wgsl.d.ts +6 -2
  224. package/lib/renderer/wgsl/chunks/intersect.wgsl.js +550 -542
  225. package/lib/renderer/wgsl/chunks/intersect.wgsl.js.map +1 -1
  226. package/lib/renderer/wgsl/chunks/intersectionAlpha.wgsl.d.ts +1 -1
  227. package/lib/renderer/wgsl/chunks/intersectionAlpha.wgsl.js +18 -16
  228. package/lib/renderer/wgsl/chunks/intersectionAlpha.wgsl.js.map +1 -1
  229. package/lib/renderer/wgsl/chunks/materialBuffer.wgsl.d.ts +2 -1
  230. package/lib/renderer/wgsl/chunks/materialBuffer.wgsl.js +70 -69
  231. package/lib/renderer/wgsl/chunks/materialBuffer.wgsl.js.map +1 -1
  232. package/lib/renderer/wgsl/chunks/openPbrBsdf.wgsl.js +593 -593
  233. package/lib/renderer/wgsl/chunks/pdm_displacement.wgsl.js +118 -118
  234. package/lib/renderer/wgsl/chunks/rand.wgsl.js +3 -3
  235. package/lib/renderer/wgsl/chunks/rayTraceCore.wgsl.d.ts +4 -1
  236. package/lib/renderer/wgsl/chunks/rayTraceCore.wgsl.js +32 -26
  237. package/lib/renderer/wgsl/chunks/rayTraceCore.wgsl.js.map +1 -1
  238. package/lib/renderer/wgsl/chunks/sampleLight.wgsl.js +114 -114
  239. package/lib/renderer/wgsl/chunks/sampleMaterial.wgsl.d.ts +1 -1
  240. package/lib/renderer/wgsl/chunks/sampleMaterial.wgsl.js +159 -137
  241. package/lib/renderer/wgsl/chunks/sampleMaterial.wgsl.js.map +1 -1
  242. package/lib/renderer/wgsl/chunks/sampleShadowCatcher.wgsl.js +8 -8
  243. package/lib/renderer/wgsl/chunks/sampling.wgsl.js +74 -74
  244. package/lib/renderer/wgsl/chunks/sobol.wgsl.js +74 -74
  245. package/lib/renderer/wgsl/chunks/spectralPrefix.wgsl.d.ts +6 -0
  246. package/lib/renderer/wgsl/chunks/spectralPrefix.wgsl.js +66 -0
  247. package/lib/renderer/wgsl/chunks/spectralPrefix.wgsl.js.map +1 -0
  248. package/lib/renderer/wgsl/chunks/uvProjection.wgsl.js +11 -11
  249. package/lib/renderer/wgsl/chunks/vertexAttributes.wgsl.d.ts +2 -0
  250. package/lib/renderer/wgsl/chunks/vertexAttributes.wgsl.js +61 -0
  251. package/lib/renderer/wgsl/chunks/vertexAttributes.wgsl.js.map +1 -0
  252. package/lib/renderer/wgsl/compose.frag.wgsl.d.ts +2 -0
  253. package/lib/renderer/wgsl/compose.frag.wgsl.js +99 -97
  254. package/lib/renderer/wgsl/compose.frag.wgsl.js.map +1 -1
  255. package/lib/renderer/wgsl/customDenoise.wgsl.js +10 -10
  256. package/lib/renderer/wgsl/depthQuery.wgsl.d.ts +1 -1
  257. package/lib/renderer/wgsl/depthQuery.wgsl.js +11 -2
  258. package/lib/renderer/wgsl/depthQuery.wgsl.js.map +1 -1
  259. package/lib/renderer/wgsl/easu.wgsl.d.ts +2 -0
  260. package/lib/renderer/wgsl/easu.wgsl.js +80 -0
  261. package/lib/renderer/wgsl/easu.wgsl.js.map +1 -0
  262. package/lib/renderer/wgsl/easuDetail.wgsl.d.ts +21 -0
  263. package/lib/renderer/wgsl/easuDetail.wgsl.js +230 -0
  264. package/lib/renderer/wgsl/easuDetail.wgsl.js.map +1 -0
  265. package/lib/renderer/wgsl/jnlm.compute.js +74 -74
  266. package/lib/renderer/wgsl/jnlm.compute.js.map +1 -1
  267. package/lib/renderer/wgsl/offlineAdaptive.compute.js +26 -26
  268. package/lib/renderer/wgsl/oidnAuxiliary.wgsl.js +3 -3
  269. package/lib/renderer/wgsl/photonMap.compute.js +89 -89
  270. package/lib/renderer/wgsl/proceduralSky.compute.js +61 -61
  271. package/lib/renderer/wgsl/rayTrace.compute.d.ts +14 -0
  272. package/lib/renderer/wgsl/rayTrace.compute.js +395 -333
  273. package/lib/renderer/wgsl/rayTrace.compute.js.map +1 -1
  274. package/lib/scene/Scene.js.map +1 -1
  275. package/lib/settings/render.d.ts +11 -0
  276. package/lib/settings/render.js +1 -0
  277. package/lib/settings/render.js.map +1 -1
  278. package/lib/util/fromTHREE.js +21 -14
  279. package/lib/util/fromTHREE.js.map +1 -1
  280. package/lib/version.d.ts +1 -1
  281. package/lib/version.js +1 -1
  282. package/lib/version.js.map +1 -1
  283. package/package.json +5 -2
  284. package/lib/oidnBundledWeights.cdn.d.ts +0 -2
  285. package/lib/oidnBundledWeights.cdn.js +0 -5
  286. package/lib/oidnBundledWeights.cdn.js.map +0 -1
  287. package/lib/renderer/bvh.worker.d.ts +0 -1
  288. package/lib/renderer/bvh.worker.js +0 -16
  289. package/lib/renderer/bvh.worker.js.map +0 -1
  290. package/lib/renderer/bvhWorkerBuild.d.ts +0 -13
  291. package/lib/renderer/bvhWorkerBuild.js +0 -17
  292. package/lib/renderer/bvhWorkerBuild.js.map +0 -1
  293. package/lib/renderer/lightCore.d.ts +0 -12
  294. package/lib/renderer/lightCore.js +0 -13
  295. package/lib/renderer/lightCore.js.map +0 -1
  296. package/lib/renderer/materialCore.d.ts +0 -8
  297. package/lib/renderer/materialCore.js +0 -9
  298. package/lib/renderer/materialCore.js.map +0 -1
  299. package/lib/scene/PixelTexture.d.ts +0 -6
  300. package/lib/scene/PixelTexture.js +0 -10
  301. package/lib/scene/PixelTexture.js.map +0 -1
  302. package/lib/util/studioLighting.d.ts +0 -18
  303. package/lib/util/studioLighting.js +0 -36
  304. package/lib/util/studioLighting.js.map +0 -1
@@ -0,0 +1,82 @@
1
+ # Display upscale
2
+
3
+ `createEasuPass` enlarges a reduced-resolution render to the canvas, and with
4
+ `albedoDetail` it restores surface texture detail from full-resolution color AOVs.
5
+ Usage, costs and limits are in [post-processing](../post-processing.md); the AOV
6
+ side is in [surface color AOVs](./aovs.md). This note explains the algorithm. The
7
+ code is `src/renderer/easu.ts` with `wgsl/easu.wgsl.ts` and
8
+ `wgsl/easuDetail.wgsl.ts`.
9
+
10
+ ## Pipeline position
11
+
12
+ The pass is a display post-process: it runs after composition, tone mapping and
13
+ denoising, on display-encoded RGB. A pass can change the image size through
14
+ `outputSize`, so the upscale is ordinary chain state and needs no special case in
15
+ composition, export or denoising. Beauty, its denoising and the AOVs keep their own
16
+ resolutions; only the displayed image is enlarged.
17
+
18
+ An input at the output size is copied, and a larger input is area-averaged, so one
19
+ registration serves both a reduced preview and a full-resolution final render.
20
+
21
+ ## EASU
22
+
23
+ The enlargement is AMD FidelityFX FSR 1 EASU, without RCAS sharpening. It estimates
24
+ a local edge direction and filters along it with a 12-tap lobe. FSR expects inputs
25
+ in [0, 1], so extended-range output normalizes the neighborhood only for the
26
+ direction estimate and filters the original values. EASU cannot invent detail that
27
+ the reduced render does not contain.
28
+
29
+ ## Detail compensation
30
+
31
+ Texture detail lost at render resolution is still present in the display-resolution
32
+ `diffuseColor` and `specularColor` AOVs. Compensation transfers it:
33
+
34
+ 1. **Low guides.** The full-resolution colors are area-averaged over each render
35
+ pixel's exact footprint, including noninteger scales, so the low guides match
36
+ what beauty integrated. No second AOV allocation is needed.
37
+ 2. **Local model.** For each render pixel, a guided-filter regression over a 3×3
38
+ window fits scene-linear radiance as `slope × color + intercept` per channel.
39
+ Diffuse shading follows color (slope), while highlights and reflections do not
40
+ (intercept). Flat colors give a slope near zero, so they are left alone.
41
+ 3. **Upsampling.** The low guides are enlarged with exactly the same EASU taps and
42
+ weights as beauty, so beauty and guides share one footprint.
43
+ 4. **Ratio.** At each output pixel, the full-resolution color replaces the
44
+ upsampled one inside the model: `(slope × hi + intercept) / (slope × low +
45
+ intercept)`, clamped per channel to [0.7, 1.43]. Without a valid model it falls
46
+ back to the plain color ratio `hi / low`.
47
+ 5. **Display transform.** The ratio is scene-linear, but the image is tone-mapped.
48
+ A lookup table built from the current tone mapping and grading maps displayed
49
+ RGB back to scene-linear per channel, applies the ratio and maps it forward, so
50
+ curve shoulders and toes respond as the tone mapper would. Only the change is
51
+ added to the displayed value, which keeps it exact where the ratio is 1.
52
+
53
+ Compensation is skipped, leaving EASU alone, where it would be wrong:
54
+
55
+ - AOV validity or coverage below 0.95 (transmission, cutouts, background,
56
+ unsupported materials);
57
+ - silhouettes, detected from a change in hit coverage or a break in the planarity
58
+ of inverse depth (which is linear in screen space on any plane, so sloped
59
+ surfaces are not silhouettes);
60
+ - color below 10⁻³, treated as black.
61
+
62
+ Clipped highlights cannot be recovered: the inverse tone mapping has no
63
+ information above the display maximum.
64
+
65
+ ## Retained alternatives
66
+
67
+ `createEasuPassImpl` keeps three internal variants for comparisons:
68
+
69
+ - **Plain color ratio.** Scaling all displayed radiance by `hi / low` also scaled
70
+ highlights and other light that does not follow color, which limited the usable
71
+ ratio range. The guided model widened it to the current default.
72
+ - **Separate lobes.** Diffuse and specular ratios weighted by lobe shares estimated
73
+ from the low colors; beauty has no separate lobes to weight them exactly.
74
+ - **Silhouette reconstruction.** Front and back colors from depth-matched
75
+ low-resolution neighbors, mixed by full-resolution coverage. It sharpens
76
+ backgrounds but loses detail on thin structures, glass and reflections
77
+ compared with EASU alone.
78
+
79
+ Ratios were first applied to encoded display RGB, which over-darkened dark decals;
80
+ applying them in linear and then through the display transform let the clamp range
81
+ grow. The public tuning options were removed; `EASU_DETAIL` holds the shared
82
+ defaults, which native also uses.
@@ -0,0 +1,99 @@
1
+ # OpenPBR implementation
2
+
3
+ `OpenPBRMaterial` follows the OpenPBR Surface specification with a compact GPU
4
+ BSDF. Parameters, units and user-facing limits are in the
5
+ [materials guide](../materials.md); this note describes how the implementation
6
+ is organized and the rules for changing it.
7
+
8
+ ## Data flow
9
+
10
+ - **Layout table.** `materialLayoutSchema.ts` lists every material buffer row
11
+ (one `vec4` each) with its source field and type. The same table generates the
12
+ TypeScript packing, the WGSL struct (`materialBufferWgsl`) and the GLSL row
13
+ constants (`materialRowsGlsl`), so the three cannot drift. Reserved rows keep
14
+ their legacy contents; reusing one is an intentional migration.
15
+ - **Texture slots.** `materialTextureSlots.ts` lists every map with its target
16
+ parameters, channels, color space, shader define and feature bit. Slot order is
17
+ the atlas index contract. Public map names live in the scene layer
18
+ (`materialTextureNames.ts`).
19
+ - **Feature specialization.** Features that a scene does not use, such as
20
+ anisotropy directions or coat normals, are compiled out by generating different
21
+ WGSL source, not by patching strings after generation.
22
+
23
+ ## BSDF
24
+
25
+ The BSDF (`openPbrBsdf.wgsl.ts` and its GLSL counterpart) follows the
26
+ prepare/eval/sample/pdf organization of Adobe's
27
+ [openpbr-bsdf](https://github.com/adobe/openpbr-bsdf) and the
28
+ [OpenPBR viewer](https://github.com/portsmouth/OpenPBR-viewer):
29
+
30
+ - `openpbr_prepare` resolves the layered material once per path vertex. NEE, BSDF
31
+ sampling and MIS reuse the result, which made WebGPU samples about 11–15% faster
32
+ than preparing per evaluation.
33
+ - The lobes are fuzz, coat, metal, dielectric specular, transmission, diffuse and
34
+ subsurface. Their weights and probabilities are stored as arrays indexed by lobe,
35
+ rather than one variable per lobe.
36
+ - Metal uses the F82-tint Fresnel; specular anisotropy uses the specification's
37
+ roughness mapping. The coat roughens the base, changes the IOR ratio below it,
38
+ absorbs, and darkens through internal reflection (`coatDarkening`, default 1).
39
+ - Fuzz uses an analytic approximation. The specification's LTC fuzz (Zeltner et al.
40
+ 2022) is not implemented.
41
+
42
+ ## Energy compensation
43
+
44
+ GGX loses energy to single scattering as roughness grows. The renderer uses Adobe's
45
+ tables for opaque dielectric base coupling, ideal-metal multiple scattering and
46
+ solid ideal-dielectric reflection and transmission (`openPbrEnergyLut.ts`). Before
47
+ these tables, rough glass lost 5–36% and rough metal 1.5–20% of its energy in white
48
+ furnace tests.
49
+
50
+ The tables are stored as 8-bit samples normalized per row, with 16-bit row bounds,
51
+ differences between consecutive rows and exact 16-bit averages. This keeps the
52
+ module about 44 KB gzipped while adding the multiple-scattering tables. On the GPU
53
+ they occupy about 280 KB.
54
+
55
+ Thin-walled rough transmission has no compensation, as in the reference.
56
+
57
+ ## Import decisions
58
+
59
+ - Three.js and glTF clearcoat import with `coatDarkening = 0`, because glTF's
60
+ clearcoat does not darken the base. `userData.mutracerOpenPBR` overrides it.
61
+ - Specification names (`specularRoughnessAnisotropy`, `coatRoughnessAnisotropy`,
62
+ `subsurfaceScatterAnisotropy`, `geometryOpacity`, `geometryThinWalled`) are the
63
+ primary names; older names remain deprecated aliases.
64
+ - glTF anisotropy is converted after texture sampling, selected by
65
+ `specularAnisotropyModel: 'gltf'`.
66
+
67
+ ## Not implemented
68
+
69
+ - Statistical mixing of two materials.
70
+ - LTC fuzz.
71
+ - Resampled direct lighting on WebGL. It waits for the cause of a small diffuse
72
+ residual with 8 or 16 WebGPU light candidates; the default of 4 is unaffected.
73
+
74
+ ## Known accuracy limits
75
+
76
+ - Rough solid glass remains slightly dark in white furnace tests, under 1% without
77
+ a coat and under 1.5% with one at roughness 0.3. With compensation off it is
78
+ about 2 percentage points darker than the reference, so the remaining loss is in
79
+ the transmission integration, not the tables.
80
+ - Strongly tilted coat normals lose about 5% of their energy, because lobes are
81
+ clipped to the geometric hemisphere. The materials guide documents this.
82
+ - Strong anisotropic metal can retain a small energy error, because the tables use
83
+ isotropic roughness.
84
+ - A coat with a higher IOR than the base can produce spurious total internal
85
+ reflection in the thin film, as in the reference.
86
+
87
+ ## Acceptance rules
88
+
89
+ These maintainer rules apply to material and BSDF changes:
90
+
91
+ - A white furnace may deviate from 1 by at most 0.5%, and a coated material from
92
+ its uncoated control by at most 0.5%.
93
+ - Re-encoding the tables may change any material on either backend by at most 0.1%.
94
+ - Measure cold shader compilation with caches invalidated; increases above 10% need
95
+ a maintainer decision.
96
+ - Sparse pixel differences of at most one half-float step from compiler
97
+ reassociation are acceptable.
98
+ - Refactors keep default materials (base weight 1, no coat, isotropic, specular
99
+ color 1) pixel-identical on both backends.
@@ -0,0 +1,168 @@
1
+ # PBRT import mapping
2
+
3
+ This contributor note describes the viewer/tool importer. Usage and limits are
4
+ in [the tool guide](../../tools/pbrt/README.md).
5
+
6
+ ## Coordinates and state
7
+
8
+ The reference is `mmp/pbrt-v4@b0bdf461ba5dd6504b70ebf0932acc0fe7aa744f`.
9
+ PBRT's camera looks along local +Z. With `M = diag(1,1,-1,1)`, geometry uses
10
+ `M * objectToWorld` and the Three camera uses `M * cameraToWorld * M`.
11
+ The world reflection is baked into geometry so emissive triangles have consistent
12
+ world-space winding and normals. The winding changes when the baked transform
13
+ changes handedness, XOR `ReverseOrientation`; explicit normals only change sign
14
+ for `ReverseOrientation`. Triangle winding is then face-forwarded to shading
15
+ normals, matching PBRT meshes with authored N. Instances retain shared geometry
16
+ and use `M * T * M`.
17
+ Loop shapes refine the control mesh and push vertices to the limit surface;
18
+ area-weighted normals approximate the reference limit-surface tangents.
19
+ A reflection is absorbed into a geometry variant for negative instance transforms.
20
+
21
+ `LookAt` sets world-to-camera, so its inverse is captured at `Camera`. `WorldBegin`
22
+ resets the object transform. Include filenames and all asset filenames resolve
23
+ against the main scene directory, as `SetSearchDirectory`/`ResolveFilename` do.
24
+ `Import` inherits graphics state but restores it afterward; definitions and scene
25
+ objects remain. Deprecated transform blocks follow PBRT-v4's attribute-block
26
+ rewrite. Only the start transform is retained for animated scenes.
27
+
28
+ ## Camera and microfacets
29
+
30
+ For aspect `a = width/height`, the vertical field of view is `fov` when `a >= 1`,
31
+ otherwise `2 atan(tan(fov/2)/a)`. Lens conversion uses a 24 mm sensor height:
32
+ `focalLength = .024/(2 tan(verticalFov/2))` metres and
33
+ `fStop = focalLength/(2 lensRadius)` for metre scene units.
34
+ The focus distance stays in scene units. The linear exposure multiplier is
35
+ `ISO/100 * (shutterClose-shutterOpen)`. Film exposuretime is not a PBRT-v4
36
+ parameter and is warned and ignored, as is whitebalance. Unsupported sensors
37
+ produce a separate warning.
38
+
39
+ The pinned `TrowbridgeReitzDistribution::RoughnessToAlpha` returns `sqrt(r)`.
40
+ Since OpenPBR uses `alpha = roughness²`, the imported roughness is `r^(1/4)` with
41
+ remapping and `sqrt(r)` without it. For anisotropic alphas, the scalar roughness
42
+ is `((alphaU²+alphaV²)/2)^(1/4)` and anisotropy is
43
+ `1-min(alphaU,alphaV)/max(alphaU,alphaV)` (zero for two zero axes). This inverts
44
+ OpenPBR's RMS-preserving formula for both base and coat. The larger alpha is
45
+ assigned to the tangent axis; rotation is π/2 when v is larger, so u stays dpdu.
46
+ Anisotropic meshes also carry UV-derived tangents for the zero-rotation path.
47
+
48
+ ## Colors and light units
49
+
50
+ RGB inputs are linear sRGB. RGB illumination preserves the RGB multiplier;
51
+ PBRT normalizes the illuminant separately in `SpectrumToPhotometric`. The pinned
52
+ implementation does **not** multiply this integral by 683. The Film's CIE sensor
53
+ integrates the emitted spectrum; this cancels the illuminant normalization for
54
+ white light. Consequently a point light's RGB intensity maps directly to the
55
+ zero-radius SphereLight intensity, and an area light's RGB radiance maps to mesh
56
+ emission. Spot cone angles are half angles and use cosine smoothstep on both sides.
57
+ Distant intensity is irradiance facing the light. Sphere emitters retain meshes:
58
+ MuTracer analytic sphere lights currently do not supply visible primary-ray
59
+ emission, and adding a second emissive sampler would count the source twice.
60
+
61
+ Power multiplies point intensity by `power/(4π)`. For a spot it multiplies by
62
+ `power/[2π(1-(cos(inner)+cos(outer))/2)]`. Area-light power divides by
63
+ `π * area * (twosided ? 2 : 1)`. Image emission also requires texture-average
64
+ normalization; that case remains an approximation. Constant environment
65
+ illuminance multiplies radiance by `illuminance/π`. Image illuminance follows
66
+ the pinned renderer's `2π/N * sum(Y(pixel)*max(z,0))` normalization in light space.
67
+
68
+ Named optical data and CIE/D65 tables come from the pinned Apache-2.0 source.
69
+ Conductor normal and edge reflectances integrate Fresnel at cosines 1 and 1/7
70
+ under D65, then convert XYZ to linear sRGB. OpenPBR takes an F82 **tint**,
71
+ so the edge value is divided by `F0+(1-F0)*(6/7)^5` and limited to [0,1]. Glass uses interpolated eta at
72
+ 587.56 nm and Abbe `(etaD-1)/(etaF-etaC)`, at 486.13 and 656.27 nm.
73
+ Blackbody emission is normalized to unit luminance before light scaling.
74
+ An RGB renderer and PBRT's spectral renderer can retain material-dependent
75
+ biases; comparisons report them without changing either renderer's algorithms.
76
+ Coated conductor inputs keep `conductor.*` substrate parameters separate from
77
+ `interface.*` coat parameters. Both roughness layers use the same remap rule;
78
+ optical constants are read as curves from named data or SPD files before Fresnel
79
+ integration. Coat IOR and anisotropy use the existing OpenPBR metadata fields.
80
+
81
+ For coefficient-based subsurface materials, each RGB extinction mean free path is
82
+ `1 / (scale * (sigma_a + sigma_s))`. The largest channel becomes the OpenPBR
83
+ radius and channel ratios become radius scales. Single-scattering albedo
84
+ `sigma_s / (sigma_a + sigma_s)` is converted to the artist-facing subsurface color
85
+ by inverting the existing material packer's mapping. Authored diffuse reflectance
86
+ is already an effective scattering color. These mappings preserve the RGB medium
87
+ coefficients but do not reproduce PBRT's spectral diffusion model.
88
+ Interior homogeneous media encode total extinction, not absorption, in
89
+ transmission color: choose depth `d=1/max(1,max(sigma_t))`, set color
90
+ `exp(-sigma_t*d)` and scatter color `sigma_s/sigma_t`, with scaled coefficients.
91
+ This preserves the existing packer's coefficients without hitting its color floor.
92
+ `MediumInterface` orders inside then outside. The geometric normal points toward
93
+ outside, after ReverseOrientation and handedness. The importer rejects scenes
94
+ with a camera medium. If only an outside medium remains, conversion flips
95
+ winding and normals together and encodes it as the material interior. Two
96
+ media cannot be represented: conversion warns and uses inside only.
97
+ Material variants include both names to avoid sharing different boundaries.
98
+
99
+ The extra medium-side reversal reciprocates thick dielectric IOR: PBRT's
100
+ DielectricBxDF exchanges eta across its final normal. ReverseOrientation and
101
+ coordinate handedness already affect both normals and do not independently
102
+ reciprocate IOR. ThinDielectricBxDF uses AbsCosTheta and keeps eta unchanged.
103
+ Base specular IOR below one survives Three conversion and material packing;
104
+ both backends use the ratio for Fresnel, total internal reflection and refraction.
105
+ Coat IOR is clamped to at least one and the coat is exterior-only, so reversed
106
+ coats and coat IOR below one receive an ior-orientation warning and are excluded
107
+ from strict comparison. Reciprocal spectral dispersion retains the original
108
+ Abbe number, but a reciprocal Cauchy curve is not itself the same Cauchy fit;
109
+ this receives an ior-dispersion approximation warning and comparison exclusion.
110
+
111
+ Ordinary PBRT surfaces accept primary back-face hits. One-sided area emission
112
+ uses FrontSide because MuTracer couples primary visibility and emission sidedness;
113
+ its missing back-side surface visibility is warned about.
114
+
115
+ PBRT image textures evaluate `(s,1-t)`. Decoded images use top-down rows and
116
+ Three textures set `flipY=true`. Infinite lights use the equal-area square map,
117
+ reprojected to world-space latitude/longitude. The direct importer uses nearest
118
+ source lookup to match ImageInfiniteLight. Each target texel averages 4×4
119
+ jittered samples uniform in azimuth and cos(theta), then per-channel spherical
120
+ integrals correct residual quadrature energy error. Source texels use area 4π/N;
121
+ target bands use exact cosine differences. This preserves small bright features'
122
+ total energy while approximating their subpixel distribution. The existing glTF
123
+ metadata adapter retains its previous bilinear reprojection.
124
+
125
+ Material displacement uses forward UV finite differences with PBRT's `.0005`
126
+ fallback step, including scalar texture scale and UV repeat. The image-row
127
+ v derivative changes sign for `(s,1-t)`. The result is a tangent normal
128
+ `normalize(-dh/du,-dh/dv,1)`, never a geometric height field. This static bake
129
+ assumes unit tangents and omits footprint-dependent steps and curvature terms;
130
+ it is warned and excluded from strict comparison. Only shape displacement uses
131
+ the height-field mapping. In the pinned `interaction.cpp`, NormalMap and BumpMap
132
+ are alternatives (`if/else`): normalmap takes precedence when both exist.
133
+
134
+ ## Comparison and memory
135
+
136
+ Ordinary byte color maps retain their original data and declared color space;
137
+ unmodified linear HDR maps also share decoded pixel data. Scale graphs carry
138
+ separate multipliers. Scalar and transformed maps retain
139
+ floating-point values. Rectangular/non-power-of-two height fields are resampled
140
+ to the existing displacement layout, with a warning.
141
+
142
+ The comparison sets PBRT's pixel filter to box with radius 0.5 to match MuTracer's
143
+ uniform pixel jitter. It overrides spp and output resolution, removes Film crop
144
+ windows and pixel bounds, preserves maxdepth,
145
+ sets an opaque black background when no environment exists, removes Film sensor exposure from the reference HDR and never tone maps numerical
146
+ inputs. AOV material IDs identify approximate surface regions. This primary-hit
147
+ mask does not isolate indirect paths that touch approximate materials. PBRT uses
148
+ seeds 0/1; MuTracer uses disjoint sample blocks starting at 0/N with the same Sobol
149
+ scramble. Their difference gives a diagnostic noise estimate, not independent
150
+ Monte Carlo replicas or statistical proof of equivalence.
151
+ Whole-frame RMSE retains every comparable pixel and its noise margin. Regional
152
+ material/tile tests exclude discontinuities in object, instance or material IDs,
153
+ dilated by one pixel. Their statistic is absolute channel mean bias / standard
154
+ error, with threshold 6. Standard error is sqrt(sum of per-pixel replica-difference
155
+ variances) / pixel count; squaring a region-sum difference instead gives an
156
+ unstable single observation. Whole-frame and regional statuses are separate.
157
+ Each MuTracer run uses a fresh Chrome process to avoid retaining both scenes' caches
158
+ and allocations during memory measurement.
159
+
160
+ Text and gzip Includes are decoded by statements to avoid the single-string size
161
+ limit. The viewer parses them and PLY data in a Worker. Inline P/N/UV/index
162
+ arrays become
163
+ transferable typed views before leaving the Worker; statement locations share
164
+ the same parameter objects. Binary assets are prefetched six at a time before
165
+ conversion; progress sums completed and in-flight bytes. The download preflight
166
+ uses unique encoded input sizes and an 8x expansion estimate against an 8 GiB
167
+ working budget. This estimate is deliberately conservative and cannot predict
168
+ total process/GPU memory or prevent every browser allocation failure.
@@ -0,0 +1,74 @@
1
+ # Tile scheduling
2
+
3
+ A full-frame path-tracing sample can take longer than a display frame and, on slow
4
+ scenes, long enough to stall the browser or trigger a GPU timeout. Renders therefore
5
+ trace each sample as a grid of tiles, sized so that one tile fits `timeBudgetMs`.
6
+ Usage and the related options are in
7
+ [sample batching](../progressive-rendering.md#sample-batching); this note explains how
8
+ the tile grid is chosen. The code is `src/renderer/TileRender.ts`.
9
+
10
+ ## Two predictors
11
+
12
+ - **WebGPU with timestamp queries** uses `makeTileRenderWithPredict`, which reads
13
+ the GPU time of each submitted grid.
14
+ - **WebGL 2, and WebGPU without timestamp queries,** use `makeTileRender`, adapted
15
+ from ray-tracing-renderer. It adjusts a pixels-per-tile count from wall-clock
16
+ feedback, never below 8192 pixels. WebGL timer queries are not reliable enough to
17
+ replace it; see [WebGL 2 backend](./webgl.md).
18
+
19
+ The rest of this note describes the WebGPU predictor.
20
+
21
+ ## Prediction
22
+
23
+ The first sample uses a probe tile shaped like the frame, large enough that dispatch
24
+ overhead does not dominate it. Each later measurement scales the tile area by the
25
+ ratio of the budget to the measured tile time, limited to 4× (2× per dimension) per
26
+ update so the grid converges without jumping from a small probe to a blocking
27
+ full-frame tile. Tiles are never narrower than one compute workgroup (8 pixels):
28
+ a smaller dispatch occupies the same workgroups and repeats their fixed cost.
29
+
30
+ The grid is interleaved, so each sample still covers every pixel once. When the
31
+ predicted tile exceeds the frame, several samples are traced per frame instead.
32
+
33
+ ## Why area-proportional prediction is not enough
34
+
35
+ Tile time is not proportional to tile area. A tile pays a fixed cost for dispatch
36
+ and for its longest path, and small frames leave most of the GPU idle. On a heavy
37
+ scene at a small resolution, 16 tiles can each take more than half the time of the
38
+ whole frame. An area-proportional predictor keeps subdividing toward the budget and
39
+ multiplies the cost of every sample.
40
+
41
+ Two rules handle this:
42
+
43
+ - **Refinement check.** When the predictor subdivides, it compares the next
44
+ measurement with the grid that motivated the change. If the new tile time
45
+ improves by less than half of the area ratio, and still misses the budget by more
46
+ than 10%, the predictor returns to the previous grid and uses its tile size as a
47
+ floor until prediction is invalidated.
48
+ - **Unattainable budgets.** From two measurements at sufficiently different tile
49
+ areas, the predictor fits tile time as `fixed + perPixel × area`. If the fixed
50
+ part alone exceeds the budget by more than 10%, no grid can meet it. The predictor then picks the
51
+ fewest tiles that keep a tile below 350 ms (over 5× below a 2 s GPU timeout) and,
52
+ where possible, keep the repeated fixed cost below one fifth of the sample. This
53
+ decision is retained until prediction is invalidated, because refitting on its
54
+ own grids lets path-cost noise alternate between tile counts.
55
+
56
+ `maxTiles` still caps the grid in both cases. A long tile is a deliberate trade:
57
+ fewer updates while interacting, in exchange for not multiplying the cost of
58
+ every sample. The guide documents the resulting pauses.
59
+
60
+ ## Measurements
61
+
62
+ Several samples may be submitted before their timestamps resolve. Each measurement
63
+ is matched to the grid it was submitted with and consumed once, so stale results
64
+ cannot shrink tiles repeatedly. Scene, view and setting changes invalidate the
65
+ prediction (`needsUpdatePredicting`). Photon and light-tracing batches run with the
66
+ first and last tile and are timed separately.
67
+
68
+ ## Changing this code
69
+
70
+ - Scheduling changes must update `tests/fixtures/display-schedule.json` and the
71
+ schedule tests, and pass `npm run test:parity`; see `AGENTS.md`.
72
+ - Judge changes by total sample time and by tile time, at both small and normal
73
+ resolutions on a heavy scene. Fixed-seed HDR must stay identical, because tiling
74
+ changes only the order of work.
@@ -0,0 +1,53 @@
1
+ # WebGL 2 backend
2
+
3
+ The WebGL 2 backend runs the same path tracer as fragment shaders. It is the
4
+ fallback where WebGPU is unavailable, so it favors working everywhere over speed.
5
+ This note lists the constraints that shape it and the open issues known to
6
+ contributors. Feature differences visible to users are documented with each
7
+ feature.
8
+
9
+ ## Constraints
10
+
11
+ - **Texture units.** WebGL 2 guarantees 16 fragment texture units. Per-vertex
12
+ attributes (normals, UVs, tangents, colors, material indices) share one
13
+ interleaved RGBA32F texture, positions keep their own for triangle tests, and
14
+ instance records follow the BVH in its texture. `webglSamplerBudget.test.ts`
15
+ keeps every trace pass at or below 14 referenced samplers, leaving two for new
16
+ features. Add a sampler only after checking that budget.
17
+ - **Uniforms.** Large scenes can exceed `MAX_FRAGMENT_UNIFORM_VECTORS` (1024 on
18
+ common devices) and fail to render. The renderer does not yet fail soft there.
19
+ - **Path length.** `bounces` and path events stop at 20 on WebGL, and Intel Macs use
20
+ a shorter event limit. WebGPU allows 50.
21
+ - **Compilation.** ANGLE defers native compilation, so a first draw can block for
22
+ seconds, much longer than WebGPU. Trace programs request parallel compilation
23
+ and are polled before use. Per-program timings do not measure that program's
24
+ compile time.
25
+ - **Timing.** Timer queries can report totals longer than the wall-clock time and
26
+ cannot drive scheduling or performance claims. Tiles are sized from wall-clock
27
+ feedback; see [tile scheduling](./tiling.md). Measure WebGL performance by wall
28
+ time until the HDR readback completes.
29
+ - **WebGPU-only features.** Caustic estimators, display post-processing,
30
+ display-resolution AOVs and custom denoisers fail soft on WebGL. Caustic paths
31
+ fall back to path tracing, so the expected image is unchanged.
32
+
33
+ ## Open issues
34
+
35
+ - **Background in some scenes.** On at least one scene, WebGL terminates many
36
+ paths at depth 0 or on a miss where WebGPU continues, and part of the background
37
+ renders black. Primary-miss and environment behavior should be aligned before
38
+ WebGL is used as a cross-backend reference for such scenes.
39
+ - **Long single draws.** With dispersion, one fragment invocation traces all three
40
+ channel suffixes ([shared prefixes](./continuous-spectrum.md#shared-prefixes)).
41
+ Large renders with a single tile have lost the context, likely from a GPU
42
+ timeout. Options are to size tiles for three times the work or to fall back to
43
+ three draws on large images.
44
+ - **Context loss.** Some driver failures lose the context and later reads return
45
+ zeros. Treat an all-black WebGL capture as a failed run, not a result.
46
+
47
+ ## Changing this code
48
+
49
+ - Implement material, light and sampling changes in both WGSL and GLSL; see
50
+ `AGENTS.md`.
51
+ - Run GPU comparisons headless, one context at a time. When a WebGL capture fails
52
+ with context loss, rerun it before drawing conclusions, and do not compare its
53
+ output.
package/docs/viewer.md CHANGED
@@ -34,7 +34,17 @@ and frames the model.
34
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
- into view. Material rows start collapsed; expand a mesh to see its materials.
37
+ into view. Selection follows the rendered geometry, including
38
+ [displacement on WebGPU](materials.md#displacement) and alpha-cutout holes;
39
+ it becomes available once the scene is ready to render.
40
+ Material rows start collapsed; expand a mesh to see its materials.
41
+
42
+ The viewer caches prepared BVHs to speed up reopening unchanged models.
43
+ Models still need to be loaded and decoded. The cache has a 4 GiB browser-storage
44
+ budget; entries are removed as space is needed. Cached data is reused only when
45
+ geometry, displacement and BVH settings match. Loading still works if storage
46
+ is unavailable or full.
47
+ Clear the site's browser storage to remove the cache.
38
48
 
39
49
  ## Sharing viewer settings
40
50
 
package/lib/Display.d.ts CHANGED
@@ -36,11 +36,25 @@ export interface PostProcessContext {
36
36
  /** Engine device; may change after recovery. */ device: GPUDevice;
37
37
  /** Record commands here; the library finishes and submits it. */ encoder: GPUCommandEncoder;
38
38
  /** Read-only composed display image, top row first; no additional color conversion. */ input: GPUTexture;
39
- /** Fully write this image. Same format and dimensions as input; supports render, storage and copy writes. */ output: GPUTexture;
40
- /** Actual render width. */ width: number;
41
- /** Actual render height. */ height: number;
39
+ /** Fully write this image. Same format as input; dimensions follow outputSize; supports render, storage and copy writes. */ output: GPUTexture;
40
+ /** Actual render width; pass dimensions are input.width and output.width. */ width: number;
41
+ /** Actual render height; pass dimensions are input.height and output.height. */ height: number;
42
42
  /** Selected complete render, including a selected fallback. */ render: Render;
43
43
  }
44
+ /** Optional dimensions for a display pass. */
45
+ export interface PostProcessOptions {
46
+ /** Output dimensions per invocation, bounded by maxTextureDimension2D. Default undefined preserves input size. Display is the current canvas size, including during export. */
47
+ outputSize?: (input: {
48
+ width: number;
49
+ height: number;
50
+ }, display: {
51
+ width: number;
52
+ height: number;
53
+ }) => {
54
+ width: number;
55
+ height: number;
56
+ };
57
+ }
44
58
  /** Synchronous display pass. Images and encoder are borrowed only during the call; do not retain or destroy them. */
45
59
  export type PostProcess = (context: PostProcessContext) => void;
46
60
  /** Canvas presentation and shared display settings; obtained from engine.display. */
@@ -73,11 +87,11 @@ export default class Display {
73
87
  private readonly _postProcesses;
74
88
  private _warnedPostProcess;
75
89
  /** 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. */
76
- addPostProcess(pass: PostProcess): () => void;
90
+ addPostProcess(pass: PostProcess, options?: PostProcessOptions): () => void;
77
91
  private warnPostProcess;
78
92
  private compose;
79
93
  /** Presents a complete image or fallback. Keeps fallback visible until the first intermediate filter finishes. Does not trace. */
80
94
  present(render: Render): Render | undefined;
81
- /** Encodes this render at its actual dimensions using these settings; leaves the display canvas unchanged. */
95
+ /** Encodes the display chain at its final dimensions (default render dimensions), cropping fixed renders before passes using these settings; leaves the display canvas unchanged. */
82
96
  toBlob(render: Render, options?: BlobOptions): Promise<Blob>;
83
97
  }
package/lib/Display.js CHANGED
@@ -68,8 +68,8 @@ export default class Display {
68
68
  _postProcesses = [];
69
69
  _warnedPostProcess = false;
70
70
  /** 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. */
71
- addPostProcess(pass) {
72
- const entry = { pass };
71
+ addPostProcess(pass, options = {}) {
72
+ const entry = { pass, options: { ...options } };
73
73
  this._postProcesses.push(entry);
74
74
  if (this.engine.backend === 'webgl')
75
75
  this.warnPostProcess();
@@ -82,7 +82,7 @@ export default class Display {
82
82
  console.warn('Display post-processing requires WebGPU; passes are ignored on WebGL.');
83
83
  }
84
84
  }
85
- compose(render) {
85
+ compose(render, exporting = false) {
86
86
  if (render._engine !== this.engine)
87
87
  throw new Error('Render belongs to another engine.');
88
88
  const target = render._output;
@@ -100,9 +100,11 @@ export default class Display {
100
100
  if (this._postProcesses.length && this.engine.backend === 'webgl')
101
101
  this.warnPostProcess();
102
102
  try {
103
- target.applyPostProcesses?.(this._postProcesses.map(entry => entry.pass), render);
103
+ target.applyPostProcesses?.(this._postProcesses.map(entry => entry.pass), render, this._postProcesses.map(entry => entry.options), exporting ? render._exportCrop : undefined);
104
104
  }
105
105
  catch (error) {
106
+ // Preserve the pre-chain export crop even when a callback or size fails.
107
+ target.applyPostProcesses?.([], render, [], exporting ? render._exportCrop : undefined);
106
108
  this.engine._notifyError(error);
107
109
  }
108
110
  return target;
@@ -122,12 +124,13 @@ export default class Display {
122
124
  this.engine._present(target);
123
125
  return selected;
124
126
  }
125
- /** Encodes this render at its actual dimensions using these settings; leaves the display canvas unchanged. */
127
+ /** Encodes the display chain at its final dimensions (default render dimensions), cropping fixed renders before passes using these settings; leaves the display canvas unchanged. */
126
128
  async toBlob(render, options = {}) {
127
- const target = this.compose(render);
129
+ const target = this.compose(render, true);
128
130
  if (!target)
129
131
  throw new Error('Render has no complete image.');
130
- const image = render._cropOutput(await target.readPresentation());
132
+ const presentation = await target.readPresentation();
133
+ const image = this.engine.backend === 'webgpu' ? presentation : render._cropOutput(presentation);
131
134
  const canvas = createCanvas(image.width, image.height, 'convertToBlob' in this.canvas);
132
135
  const context = canvas.getContext('2d');
133
136
  if (!context)