rayzee 11.14.0 → 11.15.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 (286) hide show
  1. package/README.md +8 -1952
  2. package/dist/{ActiveColor-7CZdmrsz.js → ActiveColor-DakLCifA.js} +1 -1
  3. package/dist/{sceneCachePolicy-DcEWCTKO.js → ArchiveFormats-DGypbJbi.js} +364 -301
  4. package/dist/{ArchiveImporter-DBMmDG98.js → ArchiveImporter-DO52X2Wv.js} +267 -269
  5. package/dist/BidirectionalIntegrator-BDAVOhHt.js +960 -0
  6. package/dist/{BufferLayout-SvkefbAs.js → BufferLayout-ny3WneEc.js} +1 -1
  7. package/dist/CameraRay-BrivdtGO.js +181 -0
  8. package/dist/{ColorManagement-D9eYhssc.js → ColorManagement-m9mZF5u4.js} +166 -138
  9. package/dist/CompactKernel-CGteMt2T.js +19414 -0
  10. package/dist/DepthPass-CucSh6We.js +69 -0
  11. package/dist/{EngineEvents-CLckhYbL.js → EngineEvents-oQGofgRf.js} +16 -2
  12. package/dist/{EngineIssues-B14FGIHX.js → EngineIssues-t5p7TEjb.js} +1 -0
  13. package/dist/FileIdentity-BXmPNvJP.js +69 -0
  14. package/dist/LocalExposure-2gts3VQL.js +344 -0
  15. package/dist/{Logger-dDDGj9Av.js → Logger-D7hic8LA.js} +1 -1
  16. package/dist/{NeuralRetouch-Cyh0AwUT.js → NeuralRetouch-XHVjbYAw.js} +5 -5
  17. package/dist/{NeuralSuperRes-BPp05Ipu.js → NeuralSuperRes-BvBV4gN4.js} +14 -14
  18. package/dist/{openStorage-D6DBv9_I.js → OpenStorage-Brn8HYNA.js} +81 -84
  19. package/dist/{PackedRayBuffer-BtJJxs43.js → PackedRayBuffer-BhqeNEaL.js} +56 -71
  20. package/dist/PhysicalSky-CE3UvfuW.js +1220 -0
  21. package/dist/PictureView-DXa3ED9l.js +96 -0
  22. package/dist/{PathTracer-BLV2bVqi.js → RayzeeRenderer-DPsWdbUI.js} +9155 -24022
  23. package/dist/{SolarLimb-DSig_M0f.js → SolarLimb-IayvafJP.js} +1 -1
  24. package/dist/{TextureBuckets-CcSAhgVc.js → TextureBuckets-BOZzBXSD.js} +1 -1
  25. package/dist/TextureSampling-D81EJ9uO.js +732 -0
  26. package/dist/{ToneMapGPU-l_79riZd.js → ToneMapGPU-tJZo_-7S.js} +2 -2
  27. package/dist/{ViewTransforms-Crdom11t.js → ViewTransforms-Bh7hSjSb.js} +1 -1
  28. package/dist/{WorkingMatrix-DWJV2JM2.js → WorkingMatrix-CDGicYGE.js} +1 -1
  29. package/dist/{ZipReader-Bva4oiva.js → ZipReader-D6LbMsfZ.js} +1 -1
  30. package/dist/addons/archives.es.js +2 -2
  31. package/dist/addons/bidirectional.es.js +1 -1
  32. package/dist/addons/color.es.js +3 -3
  33. package/dist/addons/formats.es.js +102 -2
  34. package/dist/addons/physical-sky.es.js +1 -1
  35. package/dist/addons/storage.es.js +2 -2
  36. package/dist/encoders.es.js +1 -1
  37. package/dist/rayzee-core.es.js +11 -13
  38. package/dist/rayzee.es.js +793 -1057
  39. package/dist/rayzee.umd.js +121 -154
  40. package/package.json +2 -2
  41. package/src/AssetConfig.js +1 -1
  42. package/src/EngineDefaults.js +1 -1
  43. package/src/EngineEvents.js +26 -0
  44. package/src/EngineIssues.js +1 -0
  45. package/src/Platform.js +40 -0
  46. package/src/RayzeeRenderer.js +188 -1345
  47. package/src/RenderSettings.js +3 -3
  48. package/src/{Processor → addons/archives}/ArchiveCache.js +4 -4
  49. package/src/{Processor → addons/archives}/ArchiveImporter.js +16 -17
  50. package/src/addons/{archives.js → archives/index.js} +2 -2
  51. package/src/{Processor/PBRT → addons/archives/pbrt}/PBRTCurves.js +1 -1
  52. package/src/{Processor/PBRT → addons/archives/pbrt}/PBRTMaterials.js +1 -1
  53. package/src/{Processor/PBRT → addons/archives/pbrt}/PBRTParser.js +1 -1
  54. package/src/{Processor/PBRT → addons/archives/pbrt}/PBRTSceneBuilder.js +3 -4
  55. package/src/{Processor/PBRT → addons/archives/pbrt}/PBRTTextureBake.js +1 -1
  56. package/src/{Processor/PBRT → addons/archives/pbrt}/index.js +2 -2
  57. package/src/{Processor/USD → addons/archives/usd}/USDLayer.js +3 -3
  58. package/src/{Processor/USD → addons/archives/usd}/USDScene.js +1 -1
  59. package/src/{Processor/USD → addons/archives/usd}/USDStage.js +2 -2
  60. package/src/{Processor/USD → addons/archives/usd}/index.js +1 -1
  61. package/src/{TSL → addons/bidirectional}/Bidirectional.js +4 -4
  62. package/src/{integrators → addons/bidirectional}/BidirectionalIntegrator.js +25 -44
  63. package/src/{TSL → addons/bidirectional}/BidirectionalLamps.js +6 -6
  64. package/src/{TSL → addons/bidirectional}/ConnectKernel.js +9 -9
  65. package/src/{TSL → addons/bidirectional}/LightGenerateKernel.js +11 -11
  66. package/src/{TSL → addons/bidirectional}/LightGuide.js +2 -2
  67. package/src/{TSL → addons/bidirectional}/LightSplatKernel.js +8 -8
  68. package/src/{TSL → addons/bidirectional}/MergeKernel.js +8 -8
  69. package/src/addons/{bidirectional.js → bidirectional/index.js} +1 -1
  70. package/src/{Color → addons/color}/BakedViews.js +1 -1
  71. package/src/{Color → addons/color}/ColorManagement.js +91 -45
  72. package/src/{Color → addons/color}/ColorSpaces.js +1 -1
  73. package/src/{Color → addons/color}/LutBake.js +1 -1
  74. package/src/{Color → addons/color}/OcioRuntime.js +2 -9
  75. package/src/{Color → addons/color}/OcioViews.js +2 -2
  76. package/src/addons/{color.js → color/index.js} +6 -6
  77. package/src/addons/formats.js +112 -3
  78. package/src/{TSL → addons/physical-sky}/Atmosphere.js +1 -1
  79. package/src/{Processor → addons/physical-sky}/AtmosphereModel.js +1 -1
  80. package/src/{TSL → addons/physical-sky}/EnvironmentCDF.js +1 -1
  81. package/src/{Processor → addons/physical-sky}/PhysicalSky.js +5 -5
  82. package/src/addons/{physicalSky.js → physical-sky/index.js} +1 -1
  83. package/src/{Storage/openStorage.js → addons/storage/OpenStorage.js} +63 -2
  84. package/src/{Storage → addons/storage}/StorageManager.js +58 -6
  85. package/src/addons/{storage.js → storage/index.js} +3 -3
  86. package/src/{Processor → bvh}/BVHBuilder.js +2 -2
  87. package/src/{Processor → bvh}/BVHLeafFold.js +1 -1
  88. package/src/{Processor/Workers → bvh}/BVHRefitWorker.js +2 -2
  89. package/src/{Processor → bvh}/BVHRefitter.js +1 -1
  90. package/src/{Processor/Workers → bvh}/BVHSubtreeWorker.js +2 -2
  91. package/src/{Processor/Workers → bvh}/BVHWorker.js +3 -3
  92. package/src/{Processor → bvh}/ParallelBVHBuilder.js +3 -3
  93. package/src/{Processor → bvh}/SplitBLAS.js +2 -2
  94. package/src/{Processor → bvh}/TLASBuilder.js +4 -4
  95. package/src/{Processor/Workers → bvh}/TLASWorker.js +2 -2
  96. package/src/core.js +10 -10
  97. package/src/{Processor/Workers → environment}/CDFWorker.js +1 -1
  98. package/src/{Processor → environment}/EnvironmentExactTable.js +2 -2
  99. package/src/{managers → environment}/EnvironmentManager.js +10 -10
  100. package/src/{Processor → environment}/EquirectHDRInfo.js +1 -1
  101. package/src/{Processor → environment}/SolarLimb.js +1 -1
  102. package/src/index.js +52 -50
  103. package/src/{Processor → lights}/EmissiveTriangleBuilder.js +5 -5
  104. package/src/{managers → lights}/LightManager.js +4 -4
  105. package/src/{Processor → lights}/LightSerializer.js +76 -2
  106. package/src/{Processor → loaders}/AssetLoader.js +115 -11
  107. package/src/loaders/SceneLoading.js +730 -0
  108. package/src/{managers → materials}/MaterialDataManager.js +8 -8
  109. package/src/{Processor → materials}/MaterialPacking.js +1 -1
  110. package/src/{Processor/Workers → materials}/PackWorker.js +1 -1
  111. package/src/{Processor → materials}/TextureCreator.js +383 -7
  112. package/src/{Processor/Workers → materials}/TexturesWorker.js +1 -1
  113. package/src/{Processor → memory}/HostMemory.js +1 -1
  114. package/src/memory/SceneSpill.js +477 -0
  115. package/src/node/{nodePlatform.js → NodePlatform.js} +4 -2
  116. package/src/node/index.js +1 -1
  117. package/src/output/Pictures.js +460 -0
  118. package/src/{Processor → output}/ToneMapCPU.js +2 -12
  119. package/src/{Processor → output}/ToneMapGPU.js +2 -2
  120. package/src/{Processor → pathtracer}/KernelManager.js +29 -4
  121. package/src/{Processor → pathtracer}/PackedRayBuffer.js +3 -3
  122. package/src/{Stages → pathtracer}/PathTracer.js +286 -398
  123. package/src/{Stages → pathtracer}/PathTracerStage.js +21 -25
  124. package/src/{Processor → pathtracer}/QueueManager.js +42 -13
  125. package/src/{Processor → pathtracer}/ShaderBuilder.js +1 -1
  126. package/src/{Processor → pathtracer}/StorageTexturePool.js +2 -2
  127. package/src/{managers → pathtracer}/UniformManager.js +2 -2
  128. package/src/{Pipeline → pipeline}/BindingAudit.js +0 -6
  129. package/src/{Pipeline → pipeline}/CompletionTracker.js +8 -1
  130. package/src/{Stages → pipeline}/Compositor.js +23 -10
  131. package/src/{Processor → scene}/BuildTimer.js +1 -1
  132. package/src/{Processor → scene}/GeometryExtractor.js +47 -7
  133. package/src/{Processor → scene}/InstanceTable.js +2 -2
  134. package/src/scene/SceneEdits.js +260 -0
  135. package/src/{Processor → scene}/SceneProcessor.js +370 -2004
  136. package/src/scene/SceneRefit.js +994 -0
  137. package/src/{TSL → shaders}/BVHTraversal.js +1 -1
  138. package/src/{TSL → shaders}/CameraRay.js +3 -3
  139. package/src/{TSL → shaders}/Clearcoat.js +1 -5
  140. package/src/{TSL → shaders}/Common.js +3 -55
  141. package/src/{TSL/MaterialProperties.js → shaders/DFGTable.js} +5 -640
  142. package/src/{TSL → shaders}/Displacement.js +1 -1
  143. package/src/{TSL → shaders}/EmissiveSampling.js +4 -5
  144. package/src/{TSL → shaders}/Environment.js +3 -3
  145. package/src/{TSL → shaders}/LightBVHSampling.js +1 -1
  146. package/src/{TSL → shaders}/LightVisibility.js +1 -1
  147. package/src/{TSL → shaders}/LightsCore.js +1 -1
  148. package/src/{TSL → shaders}/LightsSampling.js +1 -1
  149. package/src/shaders/MaterialProperties.js +637 -0
  150. package/src/{TSL → shaders}/MaterialTransmission.js +5 -26
  151. package/src/{TSL → shaders}/Random.js +7 -7
  152. package/src/{TSL → shaders}/Struct.js +81 -12
  153. package/src/{TSL → shaders}/Subsurface.js +1 -1
  154. package/src/{TSL → shaders}/Sun.js +1 -1
  155. package/src/{TSL → shaders}/TextureSampling.js +6 -23
  156. package/src/{TSL/patches.js → shaders/ThreePatches.js} +4 -159
  157. package/src/shaders/kernels/CompactKernel.js +261 -0
  158. package/src/{TSL → shaders/kernels}/DebugKernel.js +4 -4
  159. package/src/{TSL → shaders/kernels}/DepthPass.js +11 -11
  160. package/src/{TSL → shaders/kernels}/ExtendKernel.js +5 -5
  161. package/src/{TSL → shaders/kernels}/FinalWriteKernel.js +172 -10
  162. package/src/{TSL → shaders/kernels}/GenerateKernel.js +5 -5
  163. package/src/{TSL → shaders/kernels}/ShadeKernel.js +27 -27
  164. package/src/{TSL → shaders/kernels}/SortGlobalKernels.js +3 -3
  165. package/src/storage/BLASCache.js +329 -0
  166. package/src/{Storage → storage}/CDFCache.js +1 -2
  167. package/src/{Storage → storage}/DownloadCache.js +12 -3
  168. package/src/{Storage/identity.js → storage/FileIdentity.js} +1 -1
  169. package/src/{Storage → storage}/SpillStore.js +2 -2
  170. package/src/storage/StorageAreas.js +27 -0
  171. package/src/{Headless.js → viewer/Headless.js} +15 -3
  172. package/src/{PathTracerApp.js → viewer/PathTracerApp.js} +54 -32
  173. package/src/{SceneState → viewer}/SceneState.js +4 -4
  174. package/src/{managers → viewer/animation}/AnimationManager.js +1 -1
  175. package/src/{managers/timeline → viewer/animation}/CameraTrack.js +1 -1
  176. package/src/{managers/timeline → viewer/animation}/TimelineManager.js +1 -1
  177. package/src/{managers → viewer/animation}/VideoRenderManager.js +4 -4
  178. package/src/{managers → viewer/camera}/CameraManager.js +10 -10
  179. package/src/{managers → viewer/camera}/InteractionManager.js +4 -4
  180. package/src/{Stages → viewer/denoise}/DenoiserSettings.js +1 -1
  181. package/src/{managers → viewer/denoise}/DenoisingManager.js +27 -17
  182. package/src/{Stages → viewer/denoise}/MotionVector.js +3 -3
  183. package/src/{Stages → viewer/denoise}/NRD.js +5 -5
  184. package/src/{Stages → viewer/denoise}/NormalDepth.js +8 -8
  185. package/src/{Passes → viewer/denoise}/OIDNDenoiser.js +94 -25
  186. package/src/{Passes → viewer/denoise}/OIDNTemporalHistory.js +2 -2
  187. package/src/{managers → viewer/lights}/GoboManager.js +3 -3
  188. package/src/{managers → viewer/lights}/IESManager.js +4 -4
  189. package/src/{Processor → viewer/lights}/IESParser.js +1 -1
  190. package/src/{managers → viewer/overlays}/OverlayManager.js +9 -10
  191. package/src/{managers/helpers → viewer/overlays}/TransformGizmoHelper.js +1 -1
  192. package/src/{managers → viewer/overlays}/TransformManager.js +1 -1
  193. package/src/{managers/helpers → viewer/overlays}/ViewOverlayRenderer.js +56 -13
  194. package/src/{Passes → viewer/picture}/AIUpscaler.js +3 -3
  195. package/src/{Processor/Workers → viewer/picture}/AIUpscalerWorker.js +2 -2
  196. package/src/{Stages → viewer/picture}/AutoExposure.js +3 -3
  197. package/src/{Stages → viewer/picture}/LocalExposure.js +3 -3
  198. package/src/{neural → viewer/picture}/NeuralRetouch.js +3 -3
  199. package/src/{neural → viewer/picture}/NeuralSuperRes.js +5 -5
  200. package/src/viewer/picture/PictureView.js +205 -0
  201. package/dist/BidirectionalIntegrator-D6rcU7N1.js +0 -956
  202. package/dist/DepthPass-D4Zh108s.js +0 -68
  203. package/dist/FileFormats-B_VqIVDq.js +0 -102
  204. package/dist/PhysicalSky-VonV_lAD.js +0 -1220
  205. package/dist/RayzeeRenderer-D1NeGoVA.js +0 -4051
  206. package/dist/SceneMetadata-BBx6k2EY.js +0 -183
  207. package/dist/TextureSampling-C4Sg4Yw_.js +0 -901
  208. package/dist/identity-CtOwEU7N.js +0 -49
  209. package/dist/shared-ubP8eJlr.js +0 -17
  210. package/src/Processor/FileFormats.js +0 -116
  211. package/src/Processor/blackbody.js +0 -74
  212. package/src/Processor/generateMaterialSpheres.js +0 -71
  213. package/src/Processor/utils.js +0 -613
  214. package/src/Storage/BLASCache.js +0 -190
  215. package/src/Storage/areas.js +0 -4
  216. package/src/Storage/events.js +0 -29
  217. package/src/Storage/index.js +0 -9
  218. package/src/Storage/inlineTransport.js +0 -24
  219. package/src/Storage/sceneCachePolicy.js +0 -12
  220. package/src/Storage/shared.js +0 -9
  221. package/src/Storage/transport.js +0 -62
  222. package/src/TSL/CompactKernel.js +0 -123
  223. package/src/managers/helpers/overlaySurface.js +0 -53
  224. /package/dist/{AssetConfig-BY4jDfTH.js → AssetConfig-C8BsprCp.js} +0 -0
  225. /package/src/{utils/Logger.js → Logger.js} +0 -0
  226. /package/src/{SceneState/portable.js → Portable.js} +0 -0
  227. /package/src/{version.js → Version.js} +0 -0
  228. /package/src/{Processor → addons/archives}/ArchiveReader.js +0 -0
  229. /package/src/{Storage → addons/archives}/SceneGraphCodec.js +0 -0
  230. /package/src/{Processor → addons/archives}/ZipReader.js +0 -0
  231. /package/src/{Processor/PBRT/buffers.js → addons/archives/pbrt/ArrayMemory.js} +0 -0
  232. /package/src/{Processor/PBRT → addons/archives/pbrt}/EqualAreaOctahedral.js +0 -0
  233. /package/src/{Processor/PBRT → addons/archives/pbrt}/LoopSubdivision.js +0 -0
  234. /package/src/{Processor/PBRT → addons/archives/pbrt}/PBRTAnimation.js +0 -0
  235. /package/src/{Processor/PBRT → addons/archives/pbrt}/PBRTMath.js +0 -0
  236. /package/src/{Processor/PBRT → addons/archives/pbrt}/PBRTScattering.js +0 -0
  237. /package/src/{Processor/PBRT → addons/archives/pbrt}/PBRTTokenizer.js +0 -0
  238. /package/src/{Processor/PBRT → addons/archives/pbrt}/PFM.js +0 -0
  239. /package/src/{Processor/USD → addons/archives/usd}/USDText.js +0 -0
  240. /package/src/{Color → addons/color}/Displays.js +0 -0
  241. /package/src/{Color → addons/color}/InputColorSpaces.js +0 -0
  242. /package/src/{Storage/locks.js → addons/storage/Locks.js} +0 -0
  243. /package/src/{Storage → addons/storage}/StorageOps.js +0 -0
  244. /package/src/{Storage → addons/storage}/StorageWorker.js +0 -0
  245. /package/src/{Processor → bvh}/ReinsertionOptimizer.js +0 -0
  246. /package/src/{Color → color}/ActiveColor.js +0 -0
  247. /package/src/{Color → color}/BasicColor.js +0 -0
  248. /package/src/{Color → color}/BuiltinViews.js +0 -0
  249. /package/src/{Color → color}/ViewTransforms.js +0 -0
  250. /package/src/{Color → color}/WorkingMatrix.js +0 -0
  251. /package/src/{Processor → environment}/SimpleSky.js +0 -0
  252. /package/src/{Processor → environment}/SunPosition.js +0 -0
  253. /package/src/{Processor → lights}/LightBVHBuilder.js +0 -0
  254. /package/src/{Processor/archiveFormats.js → loaders/ArchiveFormats.js} +0 -0
  255. /package/src/{Processor → loaders}/GLTFDecoders.js +0 -0
  256. /package/src/{Processor → loaders}/GLTFDiffuseTransmission.js +0 -0
  257. /package/src/{Processor → loaders}/PlatformImageLoader.js +0 -0
  258. /package/src/{Processor → loaders}/SceneMetadata.js +0 -0
  259. /package/src/{Processor → materials}/ResampleRGBA8.js +0 -0
  260. /package/src/{Processor → materials}/TextureBuckets.js +0 -0
  261. /package/src/{Processor → memory}/ChunkedRecords.js +0 -0
  262. /package/src/{Processor → memory}/VRAMTracker.js +0 -0
  263. /package/src/{Processor → output}/ImageEncoders.js +0 -0
  264. /package/src/{Processor → output}/TextureReadback.js +0 -0
  265. /package/src/{Processor → pathtracer}/CameraOptimizer.js +0 -0
  266. /package/src/{Pipeline → pipeline}/EventDispatcher.js +0 -0
  267. /package/src/{Pipeline → pipeline}/PipelineContext.js +0 -0
  268. /package/src/{Pipeline → pipeline}/RenderPipeline.js +0 -0
  269. /package/src/{Pipeline → pipeline}/RenderStage.js +0 -0
  270. /package/src/{Processor → scene}/BufferLayout.js +0 -0
  271. /package/src/{SceneHelpers.js → scene/SceneHelpers.js} +0 -0
  272. /package/src/{managers → scene}/ViewCamera.js +0 -0
  273. /package/src/{TSL → shaders}/Debugger.js +0 -0
  274. /package/src/{TSL → shaders}/Fresnel.js +0 -0
  275. /package/src/{TSL → shaders}/HitFacet.js +0 -0
  276. /package/src/{TSL → shaders}/LightsDirect.js +0 -0
  277. /package/src/{TSL → shaders}/LightsIndirect.js +0 -0
  278. /package/src/{TSL → shaders}/MaterialEvaluation.js +0 -0
  279. /package/src/{TSL → shaders}/MaterialSampling.js +0 -0
  280. /package/src/{TSL → shaders}/PathTracerCore.js +0 -0
  281. /package/src/{TSL → shaders}/SceneResources.js +0 -0
  282. /package/src/{TSL → shaders}/ShadowTerminator.js +0 -0
  283. /package/src/{Storage → storage}/GeometrySpill.js +0 -0
  284. /package/src/{managers → viewer/camera}/WalkControls.js +0 -0
  285. /package/src/{managers/helpers → viewer/overlays}/OutlineHelper.js +0 -0
  286. /package/src/{managers/helpers → viewer/overlays}/TileHelper.js +0 -0
package/README.md CHANGED
@@ -9,50 +9,14 @@ A real-time WebGPU path tracing engine built on Three.js. Framework-agnostic —
9
9
 
10
10
  🌐 **[Live Demo](https://atul-mourya.github.io/rayzee-renderer/)** — the same demo app linked from the root monorepo README, built on this engine.
11
11
 
12
- ## Table of Contents
12
+ ## Documentation
13
13
 
14
- - [Installation](#installation)
15
- - [Getting Started](#getting-started)
16
- - [Vanilla JS with Vite](#vanilla-js-with-vite)
17
- - [Vanilla JS (no bundler)](#vanilla-js-no-bundler)
18
- - [React](#react)
19
- - [Integrating Alongside an Existing Three.js App](#integrating-alongside-an-existing-threejs-app)
20
- - [Vite tip](#vite-tip)
21
- - [API Reference](#api-reference)
22
- - [Configuring Assets (CDN URLs & cache namespace)](#configuring-assets-cdn-urls--cache-namespace)
23
- - [PathTracerApp](#pathtracerapp)
24
- - [Renderer core (`rayzee/core`)](#renderer-core-rayzeecore)
25
- - [Add-ons](#add-ons)
26
- - [engine.cameraManager](#enginecameramanager)
27
- - [Camera Projection (Orthographic, 360° Panorama)](#camera-projection-orthographic-360-panorama)
28
- - [engine.lightManager](#enginelightmanager)
29
- - [engine.animationManager](#engineanimationmanager)
30
- - [engine.timeline](#enginetimeline)
31
- - [Materials](#materials)
32
- - [Colour Management](#colour-management)
33
- - [engine.environmentManager](#engineenvironmentmanager)
34
- - [engine.denoisingManager](#enginedenoisingmanager)
35
- - [engine.interactionManager](#engineinteractionmanager)
36
- - [engine.transformManager](#enginetransformmanager)
37
- - [Moving and Deforming Objects](#moving-and-deforming-objects)
38
- - [Degradation contract](#degradation-contract)
39
- - [Output Methods](#output-methods)
40
- - [Render Resolution Reserve](#render-resolution-reserve)
41
- - [Memory Monitoring](#memory-monitoring)
42
- - [Logging](#logging)
43
- - [Deterministic & Headless Rendering](#deterministic--headless-rendering)
44
- - [On-disk Storage (OPFS)](#on-disk-storage-opfs)
45
- - [Saving Scene State](#saving-scene-state)
46
- - [Render Checkpoints](#render-checkpoints)
47
- - [Events](#events)
48
- - [Advanced: Custom Pipeline Stages](#advanced-custom-pipeline-stages)
49
- - [All Exports](#all-exports)
50
- - [Browser Requirements](#browser-requirements)
51
- - [Optional Dependencies](#optional-dependencies)
52
- - [Enabling OIDN (Intel Open Image Denoise)](#enabling-oidn-intel-open-image-denoise)
53
- - [Enabling the AI Upscaler](#enabling-the-ai-upscaler)
54
- - [Troubleshooting](#troubleshooting)
55
- - [License](#license)
14
+ - **[Getting started](https://github.com/atul-mourya/rayzee-renderer/blob/main/docs/guide/getting-started.md)** — set-ups without a bundler, with React or beside a three.js app; asset URLs; the viewer and the renderer core; add-ons; troubleshooting
15
+ - **[Scenes](https://github.com/atul-mourya/rayzee-renderer/blob/main/docs/guide/scenes.md)** — loading models, folders and archives; camera, lights, animation, timeline, materials, environment; moving objects
16
+ - **[Rendering](https://github.com/atul-mourya/rayzee-renderer/blob/main/docs/guide/rendering.md)** — settings and modes, colour management, denoising, local exposure, saving pictures, the render reserve, OIDN and the upscaler
17
+ - **[Headless rendering and Node](https://github.com/atul-mourya/rayzee-renderer/blob/main/docs/guide/headless.md)** — deterministic and production renders, batch rendering, Node, GPU timing
18
+ - **[Storage and memory](https://github.com/atul-mourya/rayzee-renderer/blob/main/docs/guide/storage.md)** — on-disk caches, scene state, render checkpoints, memory monitoring
19
+ - **[API](https://github.com/atul-mourya/rayzee-renderer/blob/main/docs/guide/api.md)** — events, the degradation contract, logging, custom pipeline stages, every export
56
20
 
57
21
  ## Installation
58
22
 
@@ -62,7 +26,7 @@ npm install rayzee three
62
26
 
63
27
  `three` (>=0.186.0) is a required peer dependency.
64
28
 
65
- ## Getting Started
29
+ ## Quick start
66
30
 
67
31
  ### Vanilla JS with Vite
68
32
 
@@ -130,1919 +94,11 @@ npm install rayzee three
130
94
  npm run dev
131
95
  ```
132
96
 
133
- ### Vanilla JS (no bundler)
134
-
135
- A single HTML file — no Node.js, no build step. Uses [ES module import maps](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) to resolve the pre-built ESM bundle and its dependencies from a CDN.
136
-
137
- ```html
138
- <!DOCTYPE html>
139
- <html>
140
- <head>
141
- <title>Rayzee Path Tracer</title>
142
- <style>body { margin: 0; overflow: hidden; background: #111; }</style>
143
- <script type="importmap">
144
- {
145
- "imports": {
146
- "three": "https://cdn.jsdelivr.net/npm/three@0.186.0/build/three.webgpu.js",
147
- "three/tsl": "https://cdn.jsdelivr.net/npm/three@0.186.0/build/three.tsl.js",
148
- "three/webgpu": "https://cdn.jsdelivr.net/npm/three@0.186.0/build/three.webgpu.js",
149
- "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.0/examples/jsm/",
150
- "oidn-web/": "https://cdn.jsdelivr.net/npm/oidn-web@0.5.0/",
151
- "rayzee": "https://cdn.jsdelivr.net/npm/rayzee/dist/rayzee.es.js"
152
- }
153
- }
154
- </script>
155
- </head>
156
- <body>
157
- <canvas id="viewport"></canvas>
158
- <script type="module">
159
- import { PathTracerApp } from 'rayzee';
160
-
161
- const canvas = document.getElementById('viewport');
162
- canvas.width = window.innerWidth;
163
- canvas.height = window.innerHeight;
164
-
165
- const engine = new PathTracerApp(canvas);
166
- await engine.init();
167
- // Replace with your own model URL
168
- await engine.loadModel('https://your-cdn.com/scene.glb');
169
- // Light it: a model alone renders black. The physical sky needs no file.
170
- await engine.environmentManager.setMode('procedural');
171
- engine.animate();
172
-
173
- window.addEventListener('resize', () => {
174
- canvas.width = window.innerWidth;
175
- canvas.height = window.innerHeight;
176
- engine.onResize();
177
- });
178
- </script>
179
- </body>
180
- </html>
181
- ```
182
-
183
- Serve with any static server (ES modules require HTTP, not `file://`):
184
-
185
- ```bash
186
- npx serve .
187
- ```
188
-
189
- > **Note**: The import map approach loads dependencies from a CDN, so initial load is slower than a bundled build. For production, use the Vite setup above.
190
-
191
- ### React
192
-
193
- ```jsx
194
- import { useRef, useEffect } from 'react';
195
- import { PathTracerApp } from 'rayzee';
196
-
197
- export default function Viewport({ modelUrl }) {
198
- const canvasRef = useRef(null);
199
- const engineRef = useRef(null);
200
-
201
- useEffect(() => {
202
- const canvas = canvasRef.current;
203
- canvas.width = canvas.clientWidth;
204
- canvas.height = canvas.clientHeight;
205
-
206
- const engine = new PathTracerApp(canvas);
207
- engineRef.current = engine;
208
-
209
- (async () => {
210
- await engine.init();
211
- if (modelUrl) await engine.loadModel(modelUrl);
212
- engine.animate();
213
- })();
214
-
215
- return () => engine.dispose();
216
- }, [modelUrl]);
217
-
218
- return <canvas ref={canvasRef} style={{ width: '100%', height: '100vh' }} />;
219
- }
220
- ```
221
-
222
- No special build config is needed — models and HDRs are loaded via URL at runtime.
223
-
224
- ### Integrating Alongside an Existing Three.js App
225
-
226
- If your app already has a WebGL/WebGPU rasterized view and you want to add a path-traced mode on demand, run rayzee on its own **separate canvas** (WebGL and WebGPU can't share one) and toggle visibility.
227
-
228
- ```js
229
- import { PathTracerApp } from 'rayzee';
230
-
231
- // 1. WebGPU detection
232
- if (!navigator.gpu || !(await navigator.gpu.requestAdapter())) return;
233
-
234
- // 2. Overlay canvas (hidden until toggled on)
235
- const ptCanvas = document.createElement('canvas');
236
- Object.assign(ptCanvas.style, { position: 'absolute', inset: 0, display: 'none' });
237
- container.appendChild(ptCanvas);
238
-
239
- let engine = null;
240
- async function togglePathTrace(on) {
241
- if (on && !engine) {
242
- ptCanvas.width = container.clientWidth;
243
- ptCanvas.height = container.clientHeight;
244
- engine = new PathTracerApp(ptCanvas, { autoResize: false });
245
- await engine.init();
246
- await engine.loadEnvironment('/env.hdr'); // required for realistic lighting
247
- await engine.loadObject3D(yourScene); // rayzee renders its own copy — yourScene is left untouched
248
- engine.animate();
249
- }
250
- ptCanvas.style.display = on ? 'block' : 'none';
251
- hostCanvas.style.display = on ? 'none' : 'block';
252
- on ? engine?.resume() : engine?.pause(); // pause the inactive renderer to avoid GPU contention
253
- }
254
- ```
255
-
256
- Key constraints:
257
-
258
- - **`loadObject3D` copies the passed `Object3D`.** The engine never reparents, rewrites or disposes your tree, so handing it a subtree of a scene your host still renders is safe — no clone needed on your side. The copy shares geometry, material and texture data by reference, so it costs scene-graph nodes, not GPU memory, and any ancestor transform is baked in so the model renders where your host sees it. The flip side: later edits to the object you passed do not reach the render. Mutate `engine.sceneModel` (the copy) and call `refitBVH()`/`refitBLASes()` instead.
259
- - **Rayzee ignores `onBeforeCompile`.** It reads PBR material properties (albedo, roughness, metalness, …) directly into its own GPU buffers; custom shader injection on the host material has no effect on the path-traced view.
260
- - **Always load an environment.** Path tracing without an env map produces a black background and no indirect lighting.
261
- - **`three` is a peer dep on both sides.** Vite/webpack dedupe automatically. For script-tag setups, load one copy of `three` globally.
262
-
263
- ### Vite tip
264
-
265
- When rayzee is installed from npm, its pre-built `dist/rayzee.es.js` uses worker and `import.meta.url` patterns that Vite's dep pre-bundler re-parses incorrectly. Exclude it:
266
-
267
- ```js
268
- // vite.config.js
269
- export default defineConfig({
270
- optimizeDeps: { exclude: ['rayzee'] },
271
- });
272
- ```
273
-
274
- ## API Reference
275
-
276
- ### Configuring Assets (CDN URLs & cache namespace)
277
-
278
- By default, the engine loads GLTF Draco/KTX2 decoders, OIDN denoiser weights, ONNX upscaler models, and the onnxruntime-web bundle from upstream CDNs. If you're self-hosting, embedding the engine alongside a different consumer of the same caches, or operating offline, override them **once before constructing `PathTracerApp`**:
279
-
280
- ```js
281
- import { configureAssets } from 'rayzee';
282
-
283
- configureAssets({
284
- // onnxruntime-web (loaded by AI upscaler worker via dynamic import)
285
- ortRuntimeUrl: '/ort/ort.webgpu.bundle.min.mjs',
286
- ortWasmPaths: '/ort/',
287
-
288
- // GLTFLoader extension decoders
289
- dracoDecoderPath: '/draco/',
290
- ktx2TranscoderPath: '/basis/',
291
-
292
- // Denoiser & upscaler weights
293
- oidnWeightsBaseUrl: '/oidn-tzas/',
294
- upscalerModelBaseUrl: '/upscaler-onnx/',
295
-
296
- // OpenColorIO runtime (~6 MB of WebAssembly) — the engine never names the package, so a host
297
- // that loads colour configs supplies it, bundled or served. Unset, colour management stays inert.
298
- ocioRuntimeFactory: () => import('@bb-studio/ocio'), // or: ocioRuntimeUrl: '/vendor/ocio/index.js'
299
- ocioWasmUrl: '/vendor/ocio/ocio-wasm.wasm', // optional override for the .wasm
300
-
301
- // Names the engine's on-disk storage (an OPFS directory). Set a unique value if several
302
- // apps embed the engine on the same origin, so their caches stay apart.
303
- cacheNamespace: 'my-app',
304
-
305
- // On-disk storage: 'auto' (default) where the browser has it, false for none.
306
- storage: 'auto',
307
- });
308
-
309
- const engine = new PathTracerApp(canvas);
310
- await engine.init();
311
- ```
312
-
313
- All keys are optional — only what you pass is overridden. Call `getAssetConfig()` to read the current values.
314
-
315
- ### PathTracerApp
316
-
317
- The main engine class. Extends `RayzeeRenderer`, the [renderer core](#renderer-core-rayzeecore), which extends three.js's `EventDispatcher`. Related functionality is grouped into **namespaced managers** accessed via `engine.cameraManager`, `engine.lightManager`, etc., or as direct methods on the engine instance.
318
-
319
- ```js
320
- const engine = new PathTracerApp(canvas, options?)
321
- ```
322
-
323
- | Parameter | Type | Description |
324
- |---|---|---|
325
- | `canvas` | `HTMLCanvasElement` | Rendering target |
326
- | `canvas` may be `null` | | Headless: the engine makes its own canvas and runs no render loop — see [Running in Node](#running-in-node) |
327
- | `options.headless` | `boolean` | Headless with a canvas of your own (default: `true` when `canvas` is `null`) |
328
- | `options.autoResize` | `boolean` | Auto-resize on window resize (default: `true`; always off headless) |
329
- | `options.container` | `HTMLElement` | Single DOM parent the engine mounts auxiliary elements into — HUD overlay (tile borders, helpers) and denoiser canvas. Defaults to `canvas.parentNode`. |
330
- | `options.strict` | `boolean` | Throw an `EngineIssueError` where the engine would otherwise degrade and carry on (default: `false`). See [Degradation contract](#degradation-contract). |
331
- | `options.maxSceneBytes` | `number` | Raise or lower the CPU memory ceiling a scene may need before the engine refuses it (default 9,216 MB). See [Memory monitoring](#memory-monitoring). |
332
- | `options.hostMemoryGB` | `number` | The host's memory, for runtimes without Chrome's `navigator.deviceMemory` (which then read as 4 GB and cap the render reserve at 2048). Sizes the reserve and the path pool. |
333
- | `options.storage` | `false \| 'auto' \| StorageManager` | On-disk storage (default: `configureAssets( { storage } )`; off under `strict` unless you set it there or here). A manager you pass stays yours to dispose. See [On-disk storage](#on-disk-storage-opfs). |
334
- | `options.memorySpill` | `'auto' \| boolean` | Default `'auto'`: build a static scene through disk when it would not fit in memory — triangle records, BLAS nodes and the three.js geometry are written out as the build finishes with them, and the instance matrix lists, the top-level tree and the triangle order maps once the load is done. `'auto'` does so only for a scene whose in-memory estimate passes the safe line (7 GB); `true` for every static scene; `false` never. Unless `false`, the pbrt and USD triangle and placement caps rise to 120M / 60M. Needs storage. Change it later with `engine.setMemorySpill( mode )`; `engine.sceneSpilled` says whether the current scene went to disk. See [On-disk storage](#on-disk-storage-opfs). |
335
-
336
- The engine creates and mounts everything it needs (denoiser canvas, tile/HUD overlay) into a single parent on `init()`. Performance HUDs (e.g. `stats-gl`) are not bundled — listen to `EngineEvents.FRAME` and tick your own panel.
337
-
338
- #### Lifecycle
339
-
340
- ```js
341
- await engine.init() // Initialize WebGPU renderer and pipeline
342
- engine.animate() // Start the render loop
343
- engine.pause() // Pause rendering
344
- engine.resume() // Resume rendering
345
- engine.reset() // Reset accumulation (restart from sample 0)
346
- engine.reset(false, { motion: true }) // Same, when only placements or geometry moved: keeps OIDN's motion history
347
- engine.dispose() // Clean up all resources
348
- engine.wake() // Resume render loop if idle
349
- ```
350
-
351
- Constructing a new `PathTracerApp` on a canvas that already has an active instance auto-disposes the prior one — safe under React StrictMode and HMR even without explicit cleanup, though `engine.dispose()` remains the recommended teardown path.
352
-
353
- #### Loading Assets
354
-
355
- ```js
356
- await engine.loadModel(url) // Load a GLB/GLTF by URL
357
- await engine.loadFile(fileOrUrl) // Any supported file: GLB/GLTF/FBX/OBJ/STL/PLY/DAE/3MF/USD/USDZ, archives, HDR/EXR
358
- await engine.loadFile({ files }) // A folder of files — see Loading a folder
359
- await engine.loadObject3D(object3d, name?) // Load a Three.js Object3D directly (name is optional, defaults to 'object3d')
360
- await engine.loadEnvironment(url) // Load HDR/EXR environment map
361
- engine.cancelLoad() // Abort an in-flight download (network phase only; no-op once processing starts)
362
- ```
363
-
364
- On `rayzee/core`, formats other than glTF and `.hdr` need the `rayzee/addons/formats` add-on — see [Add-ons](#add-ons).
365
-
366
- `loadModel` / `loadObject3D` **replace** the current scene. A model that carries cameras opens through its first one; `cameraManager.switchCamera(0)` goes back to the default view, fitted to the model. To add or remove objects from a live scene without a full reload (and without reframing the camera):
367
-
368
- ```js
369
- const id = await engine.addModel(url, { name }) // Append a model, rebuild in place
370
- const id = await engine.addModelFromObject3D(object3d, { name }) // Append a copy of a caller-owned Object3D (yours is untouched)
371
- engine.getSceneObject(id) // Resolve an id to the rendered root (the copy)
372
- await engine.removeSceneObject(id) // Remove by id — returns false if not found
373
- engine.setSceneObjectVisibility(id, visible) // Toggle visibility with an O(1) BVH-leaf patch, no rebuild
374
- ```
375
-
376
- `engine.sceneModel` is the root of what is actually being rendered — for `loadObject3D` that is the engine's copy, and it is the object to mutate before `refitBVH()`.
377
-
378
- `id` is the appended root's `Object3D.uuid`, returned by `addModel`/`addModelFromObject3D`. For `addModelFromObject3D` the engine carries your object's uuid onto its copy, so the id matches the object you passed. The built-in ground plane is permanent and can't be removed.
379
-
380
- ##### Loading a folder
381
-
382
- A model that comes as several files — a `.gltf` with its `.bin` and textures, an `.obj` with its `.mtl`, a pbrt
383
- scene — loads from its folder as the same folder zipped would, with nothing unpacked or copied: each file is read
384
- only when the scene asks for it. On `rayzee/core` this needs the `rayzee/addons/archives` add-on.
385
-
386
- ```js
387
- await engine.loadFile({ files: input.files }); // <input type="file" webkitdirectory>: paths from webkitRelativePath
388
- await engine.loadFile({ name: 'Sponza', files: [ // or path + file pairs: a drop, a directory handle, fs.openAsBlob in Node
389
- { path: 'Sponza/sponza.gltf', file: gltf },
390
- { path: 'Sponza/sponza.bin', file: bin },
391
- ] });
392
- ```
393
-
394
- Loose files picked together load the same way. Hidden files and folders (`.DS_Store`, `.git/`) are left out. The model
395
- that loads is a `scene` / `model` / `main` / `asset` glTF when there is one, else the shallowest model, glTF first; the
396
- others are named in an `asset.ambiguous_entry` warning. A large pbrt or USD folder of several parts asks which to load,
397
- as an archive does (below).
398
-
399
- A USD scene — a folder or archive whose main model is a `.usd` / `.usda` / `.usdc` layer, or a loose layer — loads
400
- through the engine's own USD importer, part of the archives add-on: composition (sublayers, references, payloads,
401
- inherits, specializes, variants), meshes, curves, point instancers, PxrDisneyBsdf / PxrSurface / UsdPreviewSurface
402
- materials, cameras, lights and the dome light as the environment. Each layer is read only when composition reaches it. A
403
- `.usdz` stays with three.js's loader (`usdFormat`). Ptex is not read: a Ptex input takes the mesh's mean
404
- `displayColor`. `sceneSource` is `{ kind: 'local-folder', folder: { name, size, lastModified, sample, files }, key }`
405
- — `sample` hashes every file's path, size and date, so any change inside the folder changes it — and
406
- `sceneSourceFolder` hands the files back, as `{ name, files: [{ path, file }] }`.
407
-
408
- ##### Loading part of a scene archive
409
-
410
- On `rayzee/core`, archives and pbrt need the `rayzee/addons/archives` add-on — see [Renderer core](#renderer-core-rayzeecore).
411
-
412
- A pbrt-v4 scene archive (`.tar`, `.tar.gz`, `.zip`) is usually a root `.pbrt` file that includes one
413
- subtree per element, and the whole thing rarely fits in a browser tab — Moana is 29 GB unpacked.
414
- The archive can be inspected without retaining any of it, then loaded one element at a time:
415
-
416
- ```js
417
- const { kind, root, elements, entryCount, totalBytes } = await engine.inspectArchive(file);
418
-
419
- await engine.loadFile(file, { element: elements[0].prefix }); // one element
420
- await engine.loadFile(file, { element: [ a.prefix, b.prefix ] }); // several together
421
- ```
422
-
423
- Everything above a chosen element comes along — the root scene file, the material library, an
424
- ancestor's `textures` folder — and an `Include` pointing at an element you left out only warns,
425
- which is what makes a partial load work. Selecting every element is a valid answer and loads the
426
- whole scene.
427
-
428
- Past 4 GB unpacked (a USD scene: past 1 GB of layers), a multi-element archive or folder **throws**
429
- `ARCHIVE_NEEDS_ELEMENT` rather than taking all of it. The error carries the element list — each
430
- `{ name, prefix, files, bytes, scenes }` — so a host can turn it into a picker:
431
-
432
- ```js
433
- try {
434
- await engine.loadFile(file);
435
- } catch (err) {
436
- if (err.code === 'ARCHIVE_NEEDS_ELEMENT') showPicker(err.elements, err.root, err.totalBytes);
437
- else throw err;
438
- }
439
- ```
440
-
441
- Per-load options for pbrt and USD scenes:
442
-
443
- | Option | Default | Effect |
444
- |---|---|---|
445
- | `promptBytes` | 4 GB | moves the line past which a multi-element archive asks for elements |
446
- | `maxTriangles` | 45M (120M with storage, unless `memorySpill: false`) | past it, placements are skipped and the build reports itself truncated (a USD scene thins curve sets and point instancers first, and never cuts a mesh short unless meshes alone are over) |
447
- | `maxPlacements` | 6M (60M with storage, unless `memorySpill: false`) | the same, for placements |
448
- | `curveTolerance` | 0.05 | how far a curve segment may stray, × the curve's half-width; curves become strips with adaptive segments. `0` gives the old uniform strips bit for bit |
449
- | `instanceIncludes` | `true` | a file included again under the same material, with no side effects, is placed as an instance of its first reading instead of being read and stored again |
450
-
451
- 45M is the highest rung measured to survive without the spill, which is why the caps rise only where the build can
452
- spill; the preflight still refuses what cannot fit. With the spill, the whole Moana USD island — 111.6M triangles, 51.0M
453
- placed copies — loads and renders in Chrome on a 24 GB Apple M-series in about 4½ minutes. Past
454
- WebGPU's 4 GB buffer limit the triangle and node stores go up in parts, so buffer size no longer caps a
455
- scene. Past 1M placed copies the top-level tree is built over clusters of four neighbouring copies
456
- (about 80 bytes a copy instead of 128), which costs such a scene 17–20 % frame time and nothing below
457
- that size.
458
-
459
- #### Settings
460
-
461
- ```js
462
- engine.settings.set('maxBounces', 8) // Set a single parameter
463
- engine.settings.setMany({ // Set multiple parameters at once
464
- maxBounces: 8,
465
- maxSamples: 60,
466
- exposure: 1.0
467
- })
468
- engine.settings.get('maxBounces') // Read a parameter
469
- engine.settings.getAll() // Get all current settings
470
- ```
471
-
472
- Key settings:
473
-
474
- | Setting | Type | Default | Description |
475
- |---|---|---|---|
476
- | `maxBounces` | `number` | 3 | Max ray bounce depth |
477
- | `maxSamples` | `number` | 60 | Max accumulated samples before stopping |
478
- | `renderLimitMode` | `string` | 'frames' | What else can stop the render: `'frames'` only `maxSamples`; `'time'` `maxSamples` or `renderTimeLimit`, whichever comes first; `'timeOnly'` only `renderTimeLimit` (`maxSamples` is ignored while the limit is above 0). Convergence (`useAdaptiveSampling`) can stop it in every mode |
479
- | `renderTimeLimit` | `number` | 30 | Seconds of rendering before the time limit stops it, in `'time'` and `'timeOnly'`; `0` turns the limit off. Raising it after it stopped a render continues that render, unless another limit has been reached |
480
- | `exposure` | `number` | 1.0 | Exposure value |
481
- | `saturation` | `number` | 1.0 | Color saturation (1 = no grade) |
482
- | `enableEnvironment` | `boolean` | true | Use environment lighting |
483
- | `environmentIntensity` | `number` | 1.0 | Environment light strength |
484
- | `environmentRotation` | `number` | 0 | Environment Y-rotation (degrees); 0 shows the HDRI as authored, as Blender's unmapped world does |
485
- | `areaLightIntensityScale` | `number` | 0.1 | Power of a glTF model's placeholder area lights (RectAreaLight extras), read when the model loads, so set it first; 1 is the authored power |
486
- | `maxTextureSize` | `number` | 4096 | Longest edge of a material texture, read when a model loads (clamped to the hardware ceiling); `setMaxTextureSize()` applies it to the current scene too |
487
- | `wavefrontSortMaterials` | `boolean` | true | Sort rays by material before shading, above 8 materials; read when the shaders next build |
488
- | `showBackground` | `boolean` | true | Show the environment as a visible backdrop for camera-miss rays (vs. a solid/transparent background) |
489
- | `samplingTechnique` | `number` | 2 | Sampler: `0` PCG, `1` scrambled Halton, `2` Owen-scrambled Sobol |
490
- | `integrator` | `string` | 'path' | `'path'` \| `'bidirectional'` \| `'vcm'` (on the core, needs the bidirectional add-on). Bidirectional also traces light subpaths from every light — emissive surfaces, rect/disk, point, spot and directional lights, the sun and the environment — far faster for caustics and light through small openings, about 2× the cost per sample. On a lamp-lit interior with an HDRI it is ~20 % less noisy at equal time; a room lit only through a window stays better path traced. `'vcm'` adds photon merging, for caustics seen in mirrors and through glass. Switching rebuilds the kernels |
491
- | `fireflyThreshold` | `number` | 3.0 | Firefly clamping threshold |
492
- | `shadowTerminatorOffset` | `number` | 0.1 | Cycles' Shadow Terminator → Geometry Offset: near the light's terminator on a smooth-shaded low-poly mesh, light and environment shadow rays start on the smooth surface the vertex normals describe, not the flat facet. Blender's default; `0` disables |
493
- | `transmissiveBounces` | `number` | 5 | Max bounces for transmissive materials |
494
- | `maxSubsurfaceSteps` | `number` | 8 | Max random-walk steps for subsurface scattering (raised to 64 by `configureForMode('production')`) |
495
- | `enableAlphaShadows` | `boolean` | false | Alpha-tested shadow rays (enabled by `configureForMode('production')`) |
496
- | `enableDOF` | `boolean` | false | Enable depth of field |
497
- | `dofMode` | `string` | 'look' | `'look'`: the blur is set by `dofBlur`, the same at any scene scale; `'physical'`: a real lens set by `aperture`, `focalLength` and `unitsPerMetre`. |
498
- | `dofBlur` | `number` | 0.05 | Look mode: how far a distant background blurs, as a fraction of the image height |
499
- | `focusDistance` | `number` | 0.8 | DOF focus distance in scene units — depth along the view axis (the focal plane is flat) |
500
- | `aperture` | `number` | 5.6 | Physical mode: f-stop |
501
- | `focalLength` | `number` | 50 | Physical mode: focal length (mm) |
502
- | `unitsPerMetre` | `number` | 1 | Physical mode: scene units per real metre, for files whose units are not metres. It carries over between model loads — reset it when the new file's units differ |
503
- | `transparentBackground` | `boolean` | false | Transparent canvas background |
504
- | `interactionModeEnabled` | `boolean` | true | Render at lower resolution while the camera moves, keeping the full bounce budget ("Fast Navigation" in the app) |
505
- | `interactionRenderScale` | `number` | 0.5 | Per-axis render scale while the camera moves (0.5 = a quarter of the pixels); `1` turns the drop off. Ignored while OIDN is the live denoiser. `PathTracerApp` only |
506
- | `renderMode` | `number` | 0 | Internal preview(0)/production(1) flag driving accumulation & denoiser history — normally set via `configureForMode()`, not written directly |
507
- | `visMode` | `number` | 0 | Debug visualization mode (0 = off) |
508
- | `environmentMode` | `string` | 'hdri' | Sky mode: `'hdri'` \| `'procedural'` \| `'color'` — not routed through `engine.settings`; use `engine.environmentManager.setMode()` instead |
509
- | `cameraProjection` | `string` | 'perspective' | `'perspective'` \| `'orthographic'` \| `'equirectangular'` — see [Camera Projection](#camera-projection-orthographic-360-panorama) |
510
- | `panoramaLonRange` | `[number, number]` | `[-180, 180]` | Panorama longitude sweep, degrees, left→right |
511
- | `panoramaLatRange` | `[number, number]` | `[-90, 90]` | Panorama latitude sweep, degrees, bottom→top |
512
- | `panoramaLevelHorizon` | `boolean` | true | Yaw-only panorama basis, so orbit pitch/roll can't tilt the horizon |
513
- | `useAdaptiveSampling` | `boolean` | true | Whole-frame early-stop once convergence reaches `adaptiveStopFraction` |
514
- | `noiseThreshold` | `number` | 0.1 | √-luminance-normalized per-pixel noise below which a pixel counts as converged (the production preset uses 0.02) |
515
- | `adaptiveMinSamples` | `number` | 8 | Minimum samples before adaptive sampling can trigger |
516
- | `adaptiveStopFraction` | `number` | 0.90 | Fraction of pixels that must converge before the frame retires (the production preset uses 0.94) |
517
- | `usePixelFreeze` | `boolean` | true | Per-pixel freeze (Tier-2): skip individually-converged pixels via active-list compaction |
518
- | `pixelFreezeThreshold` | `number` | 0.02 | Relative-error threshold for a pixel to become a freeze candidate |
519
- | `pixelFreezeStability` | `number` | 8 | Consecutive candidate frames required before a pixel freezes |
520
-
521
- See `ENGINE_DEFAULTS` for the full list with default values — every key in it is a setting of the same name. The default look is AgX (`DEFAULT_VIEW`) at neutral saturation; tone mapping is chosen through [Colour Management](#colour-management) (`engine.color.setActiveView( id )`), not `settings`. On the core without the colour add-on, set `renderer.renderer.toneMapping`.
522
-
523
- #### Rendering Modes
524
-
525
- ```js
526
- engine.configureForMode('production') // High quality (full-frame, 20 bounces, OIDN, controls disabled)
527
- engine.configureForMode('interactive') // Real-time navigation (3 bounces, controls enabled)
528
- ```
529
-
530
- To pause rendering for image-viewing UI, set `engine.pauseRendering = true` and disable camera controls directly — the engine doesn't model viewport visibility.
531
-
532
- ---
533
-
534
- ### Renderer core (`rayzee/core`)
535
-
536
- `PathTracerApp` is built on `RayzeeRenderer`, the renderer without the viewer: a scene and camera in, path-traced samples accumulated, the image out. The denoisers, camera controls, gizmo, overlays, timeline and animation playback exist only in `PathTracerApp`; six capabilities come as add-ons — file formats beyond glTF and `.hdr`, the physical sky, scene archives, bidirectional path tracing, OpenColorIO colour and on-disk storage. Its entry point downloads about 30 % less at startup (277 KB against 404 KB compressed). It takes the same constructor options except `container`, and renders the same pixels.
537
-
538
- ```js
539
- import { RayzeeRenderer } from 'rayzee/core';
540
-
541
- const renderer = await new RayzeeRenderer(canvas).init();
542
- await renderer.loadModel('/models/scene.glb');
543
- renderer.camera.position.set(0, 2, 6); // the camera it renders from; reset() after moving it
544
- renderer.camera.lookAt(0, 0, 0);
545
- renderer.reset();
546
- await renderer.renderFrames(256);
547
- const image = await renderer.renderToBuffer({ colorSpace: 'srgb' });
548
- renderer.dispose();
549
- ```
550
-
551
- Runnable examples live in the repository's `rayzee/examples/`, each run against the engine in that checkout:
552
-
553
- | Example | What it shows | Run (from the repository root) |
554
- |---|---|---|
555
- | `core-node.mjs` | the core alone, in Node, writing a PNG | `npm run example:node` |
556
- | `core-browser/` | the core plus the physical sky, accumulating on a canvas | `npm run example:browser` |
557
- | `viewer-browser/` | the full engine in a page: a model, an HDRI, OIDN on the finished image | `npm run example:viewer` |
558
- | `render.mjs` | the full engine in Node: any model and sky, any size, denoised, saved as `render.png` (options at the top of the file) | `npm run example:render -- --model scene.glb` |
559
-
560
- `npm run bench:examples` runs all four and fails if any of them throws or logs an error or warning.
561
-
562
- #### Add-ons
563
-
564
- Six capabilities install on the core explicitly. `PathTracerApp` installs all six itself, so nothing in this
565
- section applies to it.
566
-
567
- | Add-on | Import | Install | When | Without it |
568
- |---|---|---|---|---|
569
- | File formats | `fbxFormat`, `objFormat`, `stlFormat`, `plyFormat`, `colladaFormat`, `threeMFFormat`, `usdFormat`, `exrFormat` or `allFormats` from `rayzee/addons/formats` | `renderer.assetLoader.registerFormat(objFormat, exrFormat)` | after `init()` | only glTF/GLB, `.hdr` and images load; any other file's error names the add-on |
570
- | Physical sky | `PhysicalSky` from `rayzee/addons/physical-sky` | `renderer.environmentManager.setProceduralSky(PhysicalSky)` | after `init()` | `'procedural'` mode records `capability.missing` (throws under `strict`) |
571
- | Scene archives, folders and pbrt | `ArchiveImporter` from `rayzee/addons/archives` | `renderer.assetLoader.setArchiveImporter(new ArchiveImporter(renderer.assetLoader))` | after `init()` | `.zip`, `.tar`, `.tgz` and folders do not load, and the error names the add-on |
572
- | Bidirectional and VCM | `BidirectionalIntegrator` from `rayzee/addons/bidirectional` | `renderer.stages.pathTracer.registerIntegrator(['bidirectional', 'vcm'], pt => new BidirectionalIntegrator(pt))` | after `init()` | choosing either integrator records `capability.missing` (throws under `strict`) and keeps the current one |
573
- | OpenColorIO colour | `ColorManagement` from `rayzee/addons/color` | `renderer.setColorManagement(ColorManagement)` | before or after `init()` | linear Rec.709 through three.js's own tone mappers; `loadColorConfig()` records `capability.missing` and rejects |
574
- | On-disk storage | `acquireSharedStorage` from `rayzee/addons/storage` | `renderer.setStorageOpener(acquireSharedStorage)` | **before** `init()` | downloads land in memory and nothing is cached between visits |
575
-
576
- `init()` creates the asset loader, the environment manager and the path tracer stage, which is why four of them come
577
- after it. Storage is opened during `init()`, so its opener has to be set first.
578
-
579
- All six, in that order:
580
-
581
- ```js
582
- import { RayzeeRenderer, configureAssets } from 'rayzee/core';
583
- import { acquireSharedStorage } from 'rayzee/addons/storage';
584
- import { ColorManagement } from 'rayzee/addons/color';
585
- import { ArchiveImporter } from 'rayzee/addons/archives';
586
- import { PhysicalSky } from 'rayzee/addons/physical-sky';
587
- import { BidirectionalIntegrator } from 'rayzee/addons/bidirectional';
588
- import { allFormats } from 'rayzee/addons/formats';
589
-
590
- configureAssets({ ocioRuntimeFactory: () => import('@bb-studio/ocio') });
591
-
592
- const renderer = new RayzeeRenderer(canvas);
593
- renderer.setStorageOpener(acquireSharedStorage); // before init()
594
- renderer.setColorManagement(ColorManagement);
595
- await renderer.init();
596
- renderer.assetLoader.registerFormat(...allFormats);
597
- renderer.assetLoader.setArchiveImporter(new ArchiveImporter(renderer.assetLoader));
598
- renderer.environmentManager.setProceduralSky(PhysicalSky);
599
- renderer.stages.pathTracer.registerIntegrator(['bidirectional', 'vcm'], pt => new BidirectionalIntegrator(pt));
600
- ```
601
-
602
- **Loading on first use.** The physical sky and the archive importer can be installed as loaders instead, so their code
603
- is fetched only the first time the sky is baked or an archive is read — `PathTracerApp` does this. The archive loader
604
- takes the formats it reads, which `rayzee/core` exports, so the loader recognises an archive before its code exists:
605
-
606
- ```js
607
- import { ARCHIVE_FORMATS } from 'rayzee/core';
608
-
609
- renderer.environmentManager.setProceduralSkyLoader(() => import('rayzee/addons/physical-sky').then((m) => m.PhysicalSky));
610
- renderer.assetLoader.setArchiveImporterLoader(
611
- () => import('rayzee/addons/archives').then((m) => new m.ArchiveImporter(renderer.assetLoader)),
612
- ARCHIVE_FORMATS,
613
- );
614
- ```
615
-
616
- Colour, storage and the integrators have no loader: colour and storage are used at startup, and an integrator applies
617
- the moment it is chosen (a lazy one would trace plain frames meanwhile and break reproducible renders).
618
-
619
- **File formats.** The core reads glTF/GLB (Draco, KTX2 and meshopt decoders are fetched only for a file that uses
620
- them), `.hdr` and LDR images. FBX, OBJ, STL, PLY, Collada, 3MF, USDZ and EXR are formats to register — import only
621
- those you read and a bundler leaves the rest out; each three.js loader is downloaded the first time its format is read
622
- (a `.usd` / `.usda` / `.usdc` layer is read by the engine's own USD importer, in the archives add-on below):
623
-
624
- ```js
625
- import { objFormat, usdFormat, exrFormat } from 'rayzee/addons/formats';
626
-
627
- renderer.assetLoader.registerFormat(objFormat, usdFormat, exrFormat);
628
- await renderer.loadFile(objFile);
629
- ```
630
-
631
- A format of your own registers the same way. A model format's `parse` gets the file and resolves to the model; an
632
- environment format's `createLoader` returns a three.js loader whose `loadAsync` resolves to a texture:
633
-
634
- ```js
635
- renderer.assetLoader.registerFormat({
636
- name: 'Point cloud (XYZ)', label: 'XYZ', type: 'model', extensions: ['xyz'],
637
- async parse(file, { filename }) {
638
- return { model: buildPoints(await file.text(), filename) };
639
- },
640
- });
641
- ```
642
-
643
- **Physical sky**, for `environmentMode: 'procedural'`:
644
-
645
- ```js
646
- renderer.environmentManager.setProceduralSky(PhysicalSky);
647
- await renderer.environmentManager.setMode('procedural');
648
- ```
649
-
650
- **Bidirectional path tracing and vertex merging**, for `integrator: 'bidirectional' | 'vcm'`:
651
-
652
- ```js
653
- renderer.stages.pathTracer.registerIntegrator(['bidirectional', 'vcm'], pt => new BidirectionalIntegrator(pt));
654
- renderer.settings.set('integrator', 'bidirectional');
655
- ```
656
-
657
- Its controls are on the active integrator (the same on `PathTracerApp`):
658
-
659
- ```js
660
- const integrator = renderer.stages.pathTracer.activeIntegrator;
661
- integrator.setMergeRadius(1); // 'vcm': gather radius in pixels
662
- integrator.setMergeTrust(0.25); // 'vcm': how far merging is trusted against the other strategies
663
- integrator.setLightGuiding(true); // learn where light paths from the sky and sun start
664
- integrator.setBidirectionalStrategy('connect', { alone: true }); // keep one strategy, for verification
665
- ```
666
-
667
- **Scene archives** (`.zip`, `.tar`, `.tar.gz`) and the pbrt scenes in them:
668
-
669
- ```js
670
- renderer.assetLoader.setArchiveImporter(new ArchiveImporter(renderer.assetLoader));
671
- await renderer.loadFile(archiveFile);
672
- ```
673
-
674
- **OpenColorIO colour management** — configs, their views and looks, working spaces and export spaces. The host supplies
675
- the OCIO runtime (the engine never names the package). Without the add-on, pick a tone mapper on the three.js renderer
676
- instead (`renderer.renderer.toneMapping = AgXToneMapping`):
677
-
678
- ```js
679
- configureAssets({ ocioRuntimeFactory: () => import('@bb-studio/ocio') });
680
- renderer.setColorManagement(ColorManagement);
681
- await renderer.loadColorConfig({ builtin: 'ocio://cg-config-v4.0.0_aces-v2.0_ocio-v2.5' });
682
- ```
683
-
684
- **On-disk storage** (the browser's origin private file system), for the download, environment and scene caches and
685
- the `memorySpill` option. Asking for it without the add-on (`storage: 'auto'`) records a `capability.missing` warning;
686
- `storage: false` turns it off either way:
687
-
688
- ```js
689
- const renderer = new RayzeeRenderer(canvas);
690
- renderer.setStorageOpener(acquireSharedStorage);
691
- await renderer.init();
692
- ```
693
-
694
- Several renderers, core or full, can live in one page; they share only the colour management and the on-disk storage,
695
- which are page-wide.
696
-
697
- Code of your own that adds a setting declares it on the renderer's settings, with its default, so it gets the same
698
- provenance, change events and session saving as the built-in ones:
699
-
700
- ```js
701
- renderer.settings.define('myGlowStrength', { default: 1, apply: (value) => glow.setStrength(value), reset: true });
702
- renderer.settings.set('myGlowStrength', 2);
703
- ```
704
-
705
- ### engine.cameraManager
706
-
707
- Camera switching, auto-focus, DOF, and direct Three.js access.
708
-
709
- ```js
710
- engine.cameraManager.active // The active camera: a PerspectiveCamera that can turn orthographic
711
- engine.cameraManager.controls // The OrbitControls instance
712
- engine.cameraManager.switchCamera(index) // Switch between scene cameras
713
- engine.cameraManager.getNames() // List available cameras
714
- engine.cameraManager.focusOn(center) // Focus orbit camera on a world-space point
715
- engine.cameraManager.setAutoFocusMode(mode) // 'auto' | 'manual'
716
- engine.cameraManager.setAFScreenPoint(x, y) // Set normalized AF screen point (0-1)
717
- engine.cameraManager.setNavigationMode(mode) // 'orbit' | 'walk'
718
- engine.cameraManager.walkControls.speed // Walk speed, scene units per second
719
- engine.cameraManager.orthoHeight // Orthographic view height, scene units, wheel zoom included
720
- engine.cameraManager.setOrthoHeight(height) // Set it
721
- ```
722
-
723
- **Walk mode** is first-person navigation: drag to look, W A S D or the arrow keys to walk level, E and Q to
724
- rise and sink, Shift faster, Alt slower. A new model resets `speed` so the walk crosses it in about eight
725
- seconds. Keys are ignored while focus is in a text field, list or menu, and when a focused control has
726
- already used the key. The mode obeys `controls.enabled`, so anything that locks the orbit camera locks walking
727
- too. Switching back to `'orbit'` circles the surface at the centre of the view.
728
-
729
- ### Camera Projection (Orthographic, 360° Panorama)
730
-
731
- Three camera models live behind the `cameraProjection` setting. All are compiled into the same kernel, so switching writes a uniform and resets accumulation — it never recompiles WGSL.
732
-
733
- #### Orthographic
734
-
735
- ```js
736
- engine.settings.set('cameraProjection', 'orthographic');
737
- engine.cameraManager.setOrthoHeight(12); // the view's height in scene units
738
- engine.addEventListener(EngineEvents.ORTHO_HEIGHT_UPDATED, ({ height }) => {}); // the wheel changed it
739
- ```
740
-
741
- Rays are parallel and start on the camera's image plane, so nothing behind the camera is seen and nothing shrinks with distance. Switching keeps what the view shows at the orbit target: turning orthographic sizes the view from the orbit distance and field of view, and turning back moves the camera to match. The wheel then zooms by changing the view's size rather than moving the camera. A new model is framed the same way.
742
-
743
- `engine.cameraManager.active` stays the same object — it switches its own projection and reports `isOrthographicCamera` — so picking, the transform gizmo and the overlays follow without anything being re-pointed. Imported orthographic cameras (glTF, and pbrt's `Camera "orthographic"`) switch the projection to orthographic at their own size, and a camera left orthographic comes back so; any other camera switches it back to perspective. Every denoiser, auto-focus and depth of field keep working. An environment at infinity is seen from a single direction, so the background is one colour.
744
-
745
- #### 360° Panorama
746
-
747
- ```js
748
- engine.settings.set('cameraProjection', 'equirectangular');
749
-
750
- // Optional: crop the sweep. Degrees, [min, max].
751
- engine.settings.set('panoramaLonRange', [-90, 90]); // VR180
752
- engine.settings.set('panoramaLatRange', [0, 90]); // upper hemisphere only
753
- engine.settings.set('panoramaLevelHorizon', true); // default — orbit pitch won't tilt the panorama
754
- ```
755
-
756
- The mapping puts camera-forward at the image centre, the zenith at the top row, and yaw-right at increasing u. Full-sphere output is 2:1 — **size the canvas accordingly** (`engine.setCanvasSize(w, w / 2)`); the engine renders whatever aspect you give it and will stretch the sphere otherwise. A cropped range changes the natural aspect to match `lonRange / latRange`.
757
-
758
- Depth of field still works: the lens plane is built from each ray's own frame, not the camera's, so bokeh stays round across the whole sweep.
759
-
760
- Two features are incompatible with a non-frustum camera and the engine switches them off for you when panorama is enabled:
761
-
762
- - **NRD** hands the view to OIDN — NRD's motion vectors unproject through the projection matrix, which is meaningless when every pixel is its own direction. OIDN keeps working (without its motion history). Asking for `'nrd'` while in a panorama runs `'oidn'` and records `denoiser.unavailable`.
763
- - **Auto-focus** pauses and focus holds its last distance — it raycasts via `Raycaster.setFromCamera`, which only understands a frustum. It resumes when you leave the panorama.
764
-
765
- Read the denoiser outcome back rather than duplicating the rule (`engine.denoisingManager.denoiserStrategy`); it is not restored automatically when you leave the panorama.
766
-
767
- ### engine.lightManager
768
-
769
- Light CRUD, visual helpers, and GPU sync.
770
-
771
- ```js
772
- engine.lightManager.add('PointLight') // Add a light (PointLight, SpotLight, DirectionalLight, RectAreaLight)
773
- engine.lightManager.remove(uuid) // Remove by UUID
774
- engine.lightManager.clear() // Remove all lights
775
- engine.lightManager.getAll() // Get all light descriptors
776
- engine.lightManager.setIntensity(uuid, 40) // Set one light's power and re-upload
777
- engine.lightManager.getLight(uuid) // The traced three.js light, to edit other properties
778
- engine.lightManager.sync() // Re-upload light data to GPU after editing a light
779
- engine.lightManager.showHelpers(true) // Toggle visual helpers
780
- ```
781
-
782
- The path tracer traces **copies** of a model's lights, made when the model loads. Changing a light
783
- inside the loaded model does nothing; change the copy — `getLight(uuid)` with a UUID from `getAll()` —
784
- and call `sync()`, or use `setIntensity()`.
785
-
786
- Light `intensity` follows Blender: radiant power in watts for point, spot and area lights, irradiance in W/m² for directional. Dividing power by area only means something in metres, so the engine assumes **one world unit is one metre**; scenes authored in cm or mm must carry that scale in their node transforms, as glTF exporters do. glTF `RectAreaLightPlaceholder` nodes author `intensity` as three.js radiance (their `power` field is `intensity · width · height · π`); the importer converts it to power through the light's world area so the authored radiance is reproduced exactly, then applies the profile's `areaLightIntensityScale`.
787
-
788
- ### engine.animationManager
789
-
790
- GLTF animation playback controls.
791
-
792
- ```js
793
- engine.animationManager.play(clipIndex) // Play an animation clip
794
- engine.animationManager.pause() // Pause playback
795
- engine.animationManager.resume() // Resume playback
796
- engine.animationManager.stop() // Stop and reset
797
- engine.animationManager.setSpeed(2) // Set playback speed multiplier
798
- engine.animationManager.setLoop(true) // Enable/disable looping
799
- engine.animationManager.clips // Get available animation clips
800
- ```
801
-
802
- ### engine.timeline
803
-
804
- Authored animation: keyframed tracks on one time axis, in seconds. The camera's is the first track; the timeline is where lights and objects will join it. A model's own clips stay with `engine.animationManager`, and saved cameras (`engine.addCamera()`) stay cameras — a keyframe is not a camera.
805
-
806
- ```js
807
- const camera = engine.timeline.camera // the camera's track
808
- const key = camera.addKey() // key the current view, 2 s after the last key
809
- camera.addKey(5) // …or at a time
810
- camera.updateKey(key.id) // give a key the current view
811
- camera.setTime(key.id, 3.5) // retime it; keys stay in time order
812
- camera.remove(key.id)
813
- camera.keys // [{ id, time, position, target, fov, orthoHeight }]
814
-
815
- engine.timeline.duration // seconds to the last key of any track
816
- engine.timeline.animates // a track has two keys or more
817
- engine.timeline.seek(2.5) // put the scene where the timeline has it at 2.5 s
818
- await engine.timeline.play() // run it in the viewport, controls locked
819
- engine.timeline.stop() // or stop early
820
-
821
- // Keys changed ({ track: 'camera' }) or playback started or stopped ({ track: undefined })
822
- engine.addEventListener(EngineEvents.TIMELINE_CHANGED, ({ track }) => {})
823
-
824
- // A video of the move through a still scene; pass clipIndex too to move the camera during a clip
825
- await new VideoRenderManager(engine).renderAnimation({ timeline: engine.timeline, fps: 30, onFrame });
826
- ```
827
-
828
- Between keys the camera glides along a smooth curve through their positions while looking along another through their targets, so a subject every key looks at stays in frame. It leaves the first key and reaches the last at rest, runs straight through the keys between, and holds still outside them. FOV, or an orthographic view's height, blends too, in the projection in use. The curves are three.js keyframe tracks (`InterpolateSmooth`). With auto-focus on, focus is measured again on every video frame, and the view is put back when the render ends. A new model clears the keys, as it clears saved cameras.
829
-
830
- ### Materials
831
-
832
- Material property updates and texture transforms — accessed as direct methods on the engine.
833
-
834
- ```js
835
- engine.setMaterialProperty(index, property, value) // Update a material property
836
- engine.setTextureTransform(index, name, transform) // Update texture transform
837
- engine.reset() // Re-upload all material data to GPU
838
- engine.stages.pathTracer.materialData.updateMaterial(index, mat) // Replace a material
839
- await engine.rebuildMaterials(scene) // Full rebuild (after texture changes)
840
-
841
- // Cap the longest edge of processed material textures (clamped to the hardware max).
842
- // Larger = sharper textures, ~quadratic VRAM. Reprocesses the current scene by default;
843
- // settings.set('maxTextureSize', n) stores it for the next load instead.
844
- await engine.setMaxTextureSize(2048)
845
- await engine.setMaxTextureSize(4096, { reprocess: false })
846
-
847
- // Per-mesh visibility — recommended UUID-based API (handles lookup + sync internally)
848
- engine.setMeshVisibilityByUuid(uuid, true) // explicit set
849
- engine.setMeshVisibilityByUuid(uuid, prev => !prev) // toggle via updater fn
850
- // Returns the new visibility state, or null if the mesh wasn't found.
851
-
852
- // Lower-level — for callers that already have a meshIndex or have mutated object.visible directly
853
- engine.setMeshVisibility(meshIndex, visible)
854
- engine.updateAllMeshVisibility() // re-sync after manual object.visible mutations
855
- // With memorySpill, an edit whose top-level tree is on disk lands once it is read back (a moment later).
856
-
857
- // Read access to the active scene (returns the mesh-bearing scene)
858
- engine.getScene()
859
-
860
- // Where a packed value came from: 'material' | 'mapped' | 'default' | 'host'
861
- engine.getMaterialPropertySource(index, 'ior')
862
- ```
863
-
864
- A property a three.js material lacks falls back to `MATERIAL_DEFAULTS` (exported) — MeshPhysicalMaterial's own values, so a glTF metallic material gets IOR 1.5, not a guess derived from its metalness. Weights and roughnesses (metalness, roughness, transmission, opacity, clearcoat, sheen, iridescence, …) are clamped to [0, 1] on upload and on `setMaterialProperty`.
865
-
866
- ### Colour Management
867
-
868
- On `rayzee/core` this is the `rayzee/addons/color` add-on — see [Renderer core](#renderer-core-rayzeecore).
869
-
870
- `engine.color` is an OpenColorIO pipeline: what textures and lights mean, what the render happens in, and what it is shown and saved as. **It is inert until a config is loaded** — the render stays linear Rec.709 and the view transforms are three.js's own seven, so a host that never loads one sees no change. The host supplies the runtime (`ocioRuntimeFactory` or `ocioRuntimeUrl` in [`configureAssets`](#configuring-assets-cdn-urls--cache-namespace)).
871
-
872
- ```js
873
- configureAssets({ ocioRuntimeFactory: () => import('@bb-studio/ocio') });
874
-
875
- await engine.loadColorConfig({ builtin: 'ocio://cg-config-v4.0.0_aces-v2.0_ocio-v2.5' }); // a runtime built-in
876
- await engine.loadColorConfig({ files, configPath: 'config.ocio', id: 'studio' }); // or a folder: [{ relativePath, data }]
877
-
878
- engine.color.setView({ display: 'sRGB - Display', view: 'ACES 2.0 - SDR 100 nits (Rec.709)', look: null });
879
- engine.color.setLook(look); // a look from status().config.looks, on the active view
880
- engine.color.setActiveView(id); // any registered view, OCIO or built-in (three.js constant)
881
- engine.color.setContext({ SHOT: '010' }); // $SHOT in the config resolves to this
882
- engine.color.status(); // config, working space, active view, registered views
883
-
884
- engine.color.setWorkingSpace('ACEScg'); // render in the config's space…
885
- await engine.applyColorWorkingSpace(); // …which rebuilds textures, materials and the environment
886
-
887
- await engine.setTextureColorSpace(texture, 'srgb'); // null (auto), 'srgb', 'linear' or a config space
888
- const { data } = await engine.renderToBuffer({ colorSpace: 'ACES2065-1', source: 'display' }); // float delivery buffer
889
- await engine.unloadColorConfig();
890
- ```
891
-
892
- - **Load and unload through `engine.loadColorConfig()` / `unloadColorConfig()`** once a scene exists: they undo an adopted working space while the config that converted the environment is still loaded.
893
- - A view is baked to a log2 shaper and a 65³ table, interpolated tetrahedrally — where that measures poorly, on linear Rec.709's axes and at 97³, whichever is closer (`entry.error` says what it cost); the same table drives the canvas, the GPU and CPU readbacks and the menu (`listViewTransforms()`, `onRegistryChange()`). An OCIO view returns display-encoded colour, so the engine switches the output pass to linear while one is active.
894
- - **Baked views.** `await engine.color.saveBakedView(id)` writes a view's table to a file (157 KB gzip for 65³, 426 KB for 97³), and `await engine.color.loadBakedView(bytes, { expect })` registers it with no runtime and no config — show the look on the first frame, load the config later. When that config loads, a baked view whose files match the fingerprint it was baked from is kept as is: no rebake, no new id, no restart.
895
- - `renderToBuffer({ colorSpace })` takes `'srgb'` (display bytes through the active view), `'linear'` (the working-space accumulation) or a config colour space; `source: 'display'` reads what the viewport shows, denoised, instead of the raw accumulation.
896
- - Degradations (a view that cannot bake, a display the canvas cannot show) are recorded as warnings in the [degradation contract](#degradation-contract).
897
-
898
- ### engine.environmentManager
899
-
900
- Environment maps, sky modes, and procedural generation.
901
-
902
- ```js
903
- engine.environmentManager.params // Current environment parameters
904
- engine.environmentManager.texture // The loaded environment texture
905
- await engine.loadEnvironment(url) // Load HDR/EXR environment map (method on engine)
906
- await engine.environmentManager.setEnvironmentMap(tex) // Set a custom environment texture
907
- await engine.environmentManager.setMode(mode) // 'hdri' | 'procedural' | 'color'
908
- await engine.environmentManager.generateProcedural() // Physical sky: spectral, multiple scattering, analytic sun (core: needs rayzee/addons/physical-sky)
909
- await engine.environmentManager.generateSolid() // Solid color sky
910
- engine.environmentManager.markDirty() // Flag environment for GPU re-upload
911
- ```
912
-
913
- The physical sky (mode `'procedural'`) is set through `params`, then baked. It is baked and
914
- importance-sampled entirely on the GPU (~1.6 ms per sun move), so bake on every slider event rather
915
- than debouncing: requests made in one task become one bake, and the promise resolves once the
916
- environment has caught up. The sun is drawn and sampled as a light of its own, not from the texture.
917
-
918
- ```js
919
- import { sunPosition, dayOfYearForMonth } from 'rayzee';
920
-
921
- const p = engine.environmentManager.params;
922
- const { azimuth, elevation } = sunPosition( { hours: 17.5, dayOfYear: dayOfYearForMonth( 6 ), latitude: 40 } );
923
- const az = ( 180 - azimuth ) * Math.PI / 180, el = elevation * Math.PI / 180; // north along −Z, east along +X
924
- p.skySunDirection.set( Math.cos( el ) * Math.sin( az ), Math.sin( el ), Math.cos( el ) * Math.cos( az ) );
925
- p.skyTurbidity = 3; // haze, 1–10; also skyOzone (Dobson units), skyAirDensity (× Earth's),
926
- // skyGroundAlbedo (Color), skyAltitude (m), skySunSize (°), skySunStrength
927
- await engine.environmentManager.generateProcedural();
928
- ```
929
-
930
- ### engine.denoisingManager
931
-
932
- Denoiser strategy, NRD, OIDN, upscaler, and auto-exposure. Auto exposure meters the accumulated image on the GPU
933
- and moves in stops; it follows a room level learned as the camera moves plus a damped share of each view, so a scene lit
934
- for its manual exposure stays near it while one lit far off is corrected in full. It eases while the image restarts
935
- every frame (a moving camera, playback) and lands within about a third of a second once the image builds up, so a
936
- finished render shows its own exposure at any sample count. While it is on, `exposure` is its compensation.
937
-
938
- ```js
939
- // Strategy
940
- engine.denoisingManager.setStrategy('nrd', 'medium') // DENOISER_STRATEGIES: 'none' | 'nrd' | 'oidn'
941
- engine.denoisingManager.denoiserStrategy // read back the active strategy (derived from stage state)
942
- engine.denoisingManager.applyNRDPreset('high') // 'low' | 'medium' | 'high'
943
- engine.denoisingManager.setAutoExposure(true)
944
-
945
- // Fine-grained parameters
946
- engine.denoisingManager.setNRDParams({ maxAccumulatedFrameNum: 16, maxBlurRadius: 20 })
947
- engine.denoisingManager.setAutoExposureParams({
948
- strength: 0.3, // 0..1, how far it follows each view; 1 averages every view to middle grey
949
- metering: 'center', // 'average' | 'center' | 'spot' (at the auto-focus point)
950
- minExposure: 2 ** -8, // how far it may go from the manual exposure (default ±8 stops)
951
- maxExposure: 2 ** 8,
952
- keyValue: 0.18, // the grey it aims the average at
953
- })
954
-
955
- // OIDN & Upscaler
956
- engine.denoisingManager.setOIDNEnabled(true)
957
- engine.denoisingManager.setOIDNQuality('high')
958
- engine.denoisingManager.setStrategy('oidn') // OIDN owns the live view; see below
959
- engine.denoisingManager.setTemporalHistory(false) // live OIDN without the motion history (default on)
960
- engine.denoisingManager.continuousDenoiseInterval = 250 // cap refreshes at 4/sec (default 8 = uncapped)
961
- engine.denoisingManager.setUpscalerEnabled(true)
962
- engine.denoisingManager.setUpscalerScaleFactor(2) // 2 or 4
963
- engine.denoisingManager.setUpscalerQuality('quality') // 'fast' | 'balanced' | 'quality'
964
- ```
965
-
966
- ### Local exposure
967
-
968
- Unreal Engine 5's local exposure: brings large bright (and, if asked, dark) areas toward middle grey while keeping their
969
- detail. It changes only what is shown — the canvas, `renderToBuffer( { colorSpace: 'srgb' } )`, the AI upscaler and the
970
- neural passes apply the same per-pixel gain; EXR and `'linear'` reads stay scene-referred. Off by default.
971
-
972
- ```js
973
- engine.setLocalExposure(true)
974
- engine.setLocalExposureParams({
975
- highlightContrast: 0.6, // contrast of the base layer above middle grey; 1 = unchanged
976
- shadowContrast: 1, // below middle grey; < 1 lifts shadows
977
- detailStrength: 1, // contrast of detail within the base layer
978
- })
979
- ```
980
-
981
- ### engine.interactionManager
982
-
983
- Object picking and interaction modes.
984
-
985
- ```js
986
- engine.interactionManager.select(object) // Programmatically select an object
987
- engine.interactionManager.deselect() // Deselect the current object
988
- engine.interactionManager.toggleSelectMode() // Toggle object selection mode
989
- engine.interactionManager.disableMode() // Disable selection mode and detach gizmo
990
- engine.interactionManager.toggleFocusMode() // Toggle click-to-focus DOF
991
- engine.interactionManager.on(type, handler) // Subscribe (returns unsubscribe function)
992
- ```
993
-
994
- ### engine.transformManager
995
-
996
- Transform gizmo controls.
997
-
998
- ```js
999
- engine.transformManager.setMode('translate') // 'translate' | 'rotate' | 'scale'
1000
- engine.transformManager.setSpace('world') // 'world' | 'local'
1001
- engine.transformManager.controls // Access the underlying TransformControls
1002
- ```
1003
-
1004
- ### Moving and Deforming Objects
1005
-
1006
- Three calls update a loaded scene without rebuilding it. They differ in how much work they do, and
1007
- picking the wrong one is the usual source of trouble.
1008
-
1009
- ```js
1010
- engine.updateMeshTransforms(meshIndices) // an object moved, rotated or scaled
1011
- engine.refitBLASes(meshIndices, positions) // specific objects' vertices changed
1012
- await engine.refitBVH(positions) // the whole scene is posed anew, e.g. animation
1013
- ```
1014
-
1015
- **`updateMeshTransforms` is the one a gizmo drag wants.** Triangles are stored in each object's own
1016
- space, so a rigid move only rewrites a matrix — no vertex pass, no geometry upload — and refits only the
1017
- tree nodes above what moved, uploading just those. Using `refitBLASes` for a move instead rewrites
1018
- vertices needlessly, and drags along any other object sharing the same geometry. It returns
1019
- `{ refitTimeMs, placements }`, or `null` on a `memorySpill` scene whose matrices or tree are on disk:
1020
- the move then lands once they are read back.
1021
-
1022
- All three take **indices into `engine.sceneMeshes`**, which is a depth-first walk of the rendered
1023
- scene and includes the engine's own hidden ground disk. Build your index list from that array, never
1024
- from your own model root, or the two orders silently disagree.
1025
-
1026
- Positions are **world space**, 9 floats per triangle (`ax,ay,az, bx,by,bz, cx,cy,cz`), triangles in
1027
- index order. Two shapes are accepted:
1028
-
1029
- ```js
1030
- // Preferred: a per-mesh callback, asked for one mesh at a time. You may hand back the same
1031
- // scratch buffer on every call.
1032
- await engine.refitBVH((meshIndex, triCount) => myPositionsFor(meshIndex));
1033
-
1034
- // Also works: one array for every triangle in the scene, meshes in sceneMeshes order.
1035
- // 1,030 MB at 30M triangles, and will not allocate at that size — prefer the callback.
1036
- await engine.refitBVH(sceneWideFloat32Array);
1037
- ```
1038
-
1039
- Both shapes are length-checked and throw on a mismatch. Before that check existed, a short buffer
1040
- wrote NaN through every bounding box with no error and the scene simply vanished.
1041
-
1042
- An object that shares its geometry with another cannot be deformed — writing its vertices would
1043
- move every copy. `refitBLASes` skips such a mesh and records a `refit.shared_geometry` issue.
1044
- Anything skinned or morphed is given triangles of its own at load, so this only fires when the
1045
- wrong mesh was handed over.
1046
-
1047
- ---
1048
-
1049
- ### Degradation contract
1050
-
1051
- The engine degrades rather than fails, which is right for a viewer and backwards for a batch
1052
- renderer, so one option decides which you get:
1053
-
1054
- ```js
1055
- const engine = new PathTracerApp(canvas, { strict: true }); // throw at the point of degradation
1056
- ```
1057
-
1058
- Lenient hosts read the log instead:
1059
-
1060
- ```js
1061
- engine.issues // every recorded issue, newest last
1062
- engine.issueErrors // just the ones a strict host would have thrown on
1063
- engine.addEventListener(EngineEvents.ISSUE, ({ issue }) => report(issue));
1064
- ```
1065
-
1066
- Each issue carries `{ code, message, detail, severity, at }`. `ISSUE_CODES` is **add-only API
1067
- surface** — pin a version and branch on the strings; they are never renamed or repurposed.
1068
-
1069
- | Code | Raised when |
1070
- |---|---|
1071
- | `adapter.software` | the GPU is a software rasteriser (SwiftShader, llvmpipe, lavapipe, WARP) |
1072
- | `asset.unreachable` / `asset.ambiguous_entry` | the asset could not be fetched, or an archive held several candidate models |
1073
- | `asset.archive_too_large` / `asset.entry_too_large` | an archive or one of its entries exceeded the byte budget |
1074
- | `asset.parameters_ignored` | a pbrt scene sets parameters the loader ignores (`detail.unused`) or only approximates (`detail.approximated`), each `directive type: name` with how many directives set it — a warning |
1075
- | `texture.build_failed` / `texture.processing_fallback` / `texture.limit_exceeded` | a texture could not be built, fell back to a slower path, or exceeded the per-map-type cap |
1076
- | `environment.load_failed` | the environment map failed to load |
1077
- | `setting.unknown_key` | a setting name reached no stage — how a typo becomes a wrong image |
1078
- | `render.size_declined` / `render.reserve_capped` | the requested render size or reserve exceeded device limits |
1079
- | `stage.render_failed` | a pipeline stage threw (recorded once per stage and phase) |
1080
- | `scene.memory_budget` | the scene needs more CPU memory than is safe, or more than is possible |
1081
- | `emissive.instances_collapsed` | an emissive instanced mesh was too large to expand, so its copies light the scene as one |
1082
- | `refit.shared_geometry` | a deform was asked for on a mesh that shares its triangles, and was skipped |
1083
- | `denoiser.unavailable` | a requested denoise or upscale produced nothing — the denoiser was not built, OIDN was off while accumulating, or the pass needs a canvas in a document |
1084
- | `output.source_fallback` | `renderToBuffer( { source: 'display' } )` found no denoised picture and returned the raw accumulation |
1085
- | `output.tonemap_fallback` | `renderToBuffer`'s `'srgb'` bytes were tone-mapped on the CPU, not the GPU — the picture is the same within a level, only slower (a warning: strict does not throw) |
1086
- | `output.pass_unavailable` | `exportImage` was asked for an EXR layer it cannot write, such as `albedo` or `normal` with no denoiser on (`detail.pass`); the file is written without it |
1087
- | `light.placeholder_skipped` | a `RectAreaLightPlaceholder` node lacked `userData.name` or `userData.type: 'RectAreaLight'`, so no light was made for it |
1088
- | `capability.missing` | a feature was asked for whose add-on is not installed on the core (procedural sky, an unregistered integrator, a colour config, storage) — names the add-on |
1089
- | `color.config_load_failed` / `viewTransform.bake_failed` / `viewTransform.display_mismatch` | a colour config failed to load (the previous one stays), a view could not be rebaked, or a view targets a display the canvas cannot show (a saved buffer is still right) — warnings |
1090
- | `storage.unavailable` / `storage.quota_exceeded` / `storage.write_failed` / `storage.read_failed` / `storage.entry_corrupt` / `storage.cache_mismatch` | on-disk storage is off or failed; the engine carries on in memory — warnings |
1091
-
1092
- `asset.unreachable` also covers what the engine fetches for itself: OIDN weights, IES profiles and
1093
- gobos (`detail.asset` says which).
1094
-
1095
- `init()` cannot degrade past a missing GPU, so these three always reject it with an
1096
- `EngineIssueError`, strict or not, and are recorded too. `error.cause` is the WebGPU error, when there was one:
1097
-
1098
- | Code | Raised when |
1099
- |---|---|
1100
- | `webgpu.unavailable` | there is no `navigator.gpu`, or three.js could start only WebGL2 on the canvas |
1101
- | `adapter.unavailable` | `requestAdapter()` returned no adapter or rejected — no GPU, or no driver for it |
1102
- | `device.unavailable` | the adapter was found but `requestDevice()` rejected |
1103
-
1104
- ```js
1105
- try { await engine.init(); } catch (error) { metrics.failure = error.code ?? 'unknown'; }
1106
- ```
1107
-
1108
- `settings.getEffective()` is the companion for the `setting.unknown_key` case: it returns every live
1109
- setting as `{ value, source, routed }`, and `routed: false` means stored but reaching no stage.
1110
-
1111
- ---
1112
-
1113
- ### Output Methods
1114
-
1115
- Canvas output, screenshots, saved files and scene statistics — accessed as direct methods on the engine.
1116
-
1117
- ```js
1118
- engine.getCanvas() // Get the canvas with the final rendered image
1119
- const blob = await engine.screenshot() // Capture frame as Blob (default 'image/png')
1120
- const jpg = await engine.screenshot({ type: 'image/jpeg', quality: 0.9 })
1121
- engine.getStatistics() // Triangle count, mesh count, etc.
1122
- engine.setCanvasSize(1920, 1080) // Set explicit canvas dimensions
1123
- engine.onResize() // Trigger manual resize recalculation
1124
- engine.isComplete() // Check if rendering has converged
1125
- engine.getFrameCount() // Get the current accumulated frame count
1126
- engine.getMemoryInfo() // GPU memory snapshot: { current, peak, byCategory } in bytes
1127
- ```
1128
-
1129
- `screenshot()` returns a `Blob` for the host to save, upload, or display. To trigger a browser download:
1130
-
1131
- ```js
1132
- const blob = await engine.screenshot();
1133
- const url = URL.createObjectURL(blob);
1134
- const a = Object.assign(document.createElement('a'), { href: url, download: 'render.png' });
1135
- a.click();
1136
- URL.revokeObjectURL(url);
1137
- ```
1138
-
1139
- #### Saving a file
1140
-
1141
- `exportImage()` writes the render as a file and returns `{ blob, extension, mimeType, width, height, colorSpace, … }`.
1142
- It works on the core too, and headless.
1143
-
1144
- | `format` | What the file holds |
1145
- |---|---|
1146
- | `'png'` (`bitDepth: 8` or `16`), `'jpeg'`, `'webp'` | the picture the canvas shows: exposure, view and denoising applied; at 8 bits, JPEG and WebP, the AI upscaler's picture while one shows (`upscaled: false` skips it) |
1147
- | `'heic'`, `'avif'` | the same picture, written by the browser's video encoder |
1148
- | `'exr'` (`precision: 'half'` or `'float'`), `'hdr'` (Radiance) | the light itself, before exposure and view, in the working space or `colorSpace`; with `asShown: true`, the light the screen gives off for the picture, so any viewer shows it as the screen does |
1149
-
1150
- ```js
1151
- const { blob, extension } = await engine.exportImage({ format: 'png', bitDepth: 16 });
1152
- const exr = await engine.exportImage({ format: 'exr', passes: ['albedo', 'normal', 'depth'] });
1153
- const photo = await engine.exportImage({ format: 'jpeg', gainMap: true }); // an HDR photo
1154
- engine.exportInfo(); // what can be written now: { width, height, upscaled, hdr, layers: { albedo, normal, depth } }
1155
- ```
1156
-
1157
- - **HDR photos.** `gainMap: true` on `'jpeg'`, `'heic'` or `'avif'` adds a gain map — a second, small picture that
1158
- says how much brighter each part of the HDR version is (ISO 21496-1; Ultra HDR in a JPEG). Photo viewers that read gain
1159
- maps show the file in HDR, others the normal picture. It needs the screen showing HDR
1160
- (`engine.color.setHdrOutput(true)` on an HDR display; `exportInfo().hdr`).
1161
- - **EXR layers.** `albedo` and `normal` are the denoisers' guide images, so they exist only while a denoiser is on;
1162
- `depth` is measured when asked for. A layer that cannot be written records `output.pass_unavailable`.
1163
- - **Where each works.** JPEG, WebP, HEIC and AVIF need a browser (its canvas or video encoder); PNG, EXR and HDR work in
1164
- Node too. `canEncodeStill('heic')` from `rayzee/encoders` says whether this browser can write HEIC or AVIF.
1165
- - Render details (samples, size, view, settings, versions) go into the file, except a WebP, unless `metadata: false`.
1166
- The file writers download on the first export, not at startup.
1167
-
1168
- The writers are also a module of their own, `rayzee/encoders`, for pixels a host already has. It is part of the
1169
- package, not an add-on: nothing to install, and importing it loads nothing else (9 KB compressed).
1170
-
1171
- ```js
1172
- import { encodePNG, encodeEXR, encodeHDR, IMAGE_FORMATS } from 'rayzee/encoders';
1173
-
1174
- const png = await encodePNG({ data, width, height }); // RGBA, Uint8Array (8 bits) or Uint16Array (16), top row first
1175
- const hdr = await encodeHDR({ data: floats, width, height }); // RGBA Float32Array
1176
- const exr = await encodeEXR({ width, height, channels: [
1177
- { name: 'R', data: floats, stride: 4, offset: 0 },
1178
- { name: 'G', data: floats, stride: 4, offset: 1 },
1179
- { name: 'B', data: floats, stride: 4, offset: 2 },
1180
- ] });
1181
- ```
1182
-
1183
- Chrome on macOS takes about 5 seconds to start its first hardware video encoder after the browser launches, and about
1184
- 2 ms after that. HEIC and MP4 both use it. `prepareStill('heic')` starts it ahead of time without encoding anything,
1185
- for example when an export menu opens.
1186
-
1187
- ---
1188
-
1189
- ### Render Resolution Reserve
1190
-
1191
- Every compute `StorageTexture` and aux buffer is pre-allocated at one square dimension — the *reserve* — and `setCanvasSize()` refuses anything larger. The default is 2048, so 4K output needs the reserve raised first.
1192
-
1193
- ```js
1194
- engine.setReservedRenderResolution(4096) // raise to 4K (longest edge)
1195
- engine.setReservedRenderResolution(2048, { allowLower: true }) // lower, paying a rebuild, to reclaim VRAM
1196
- engine.getReservedRenderResolution() // the reserve actually in force
1197
- ```
1198
-
1199
- The request is **device-capped**: a 4096 reserve pins roughly 1.5 GB of MRT textures, so it is only granted on hosts reporting ≥ 8 GB and a ≥ 1 GB `maxStorageBufferBindingSize`; weaker devices clamp to 2048, recorded as `render.reserve_capped`. The memory figure is `options.hostMemoryGB`, else `navigator.deviceMemory`, else an assumed 4 — so outside Chrome, pass it. `MAX_RESERVABLE_RENDER_SIZE` (4096) is the ceiling on any request.
1200
-
1201
- Raises are monotonic unless you pass `allowLower` — UI-driven callers ask for whatever the current view needs, and honouring every decrease made the reserve oscillate across preview↔render switches, paying a full kernel rebuild each time.
1202
-
1203
- Callable at any point in the lifecycle:
1204
-
1205
- - **Before `init()`** — recorded and applied during `init()`, after the device exists but before the stages are constructed, so they allocate at the raised size directly. The device gate cannot run without a device, so the return value here is the *request*, not the verdict.
1206
- - **After `init()`** — applied immediately, re-initialising the reserved GPU storage in place.
1207
-
1208
- Either way the verdict arrives as `EngineEvents.RESERVED_RENDER_SIZE_CHANGED`:
1209
-
1210
- ```js
1211
- engine.addEventListener(EngineEvents.RESERVED_RENDER_SIZE_CHANGED, e => console.log('reserve:', e.size));
1212
- engine.setReservedRenderResolution(4096);
1213
- await engine.init();
1214
- console.log(engine.getReservedRenderResolution()); // 4096, or 2048 if the device declined
1215
- ```
1216
-
1217
- ---
1218
-
1219
- ### Memory Monitoring
1220
-
1221
- Track GPU (VRAM) usage across the whole pipeline. Sizes are measured from live GPU resources (buffer `byteLength` + texture dimensions × format), so they are exact, not estimated.
1222
-
1223
- ```js
1224
- const { current, peak, byCategory } = engine.getMemoryInfo(); // bytes
1225
- // byCategory: { rays, queues, gbuffer, accum, geometry, materials, environment, stages, denoiser, canvas }
1226
-
1227
- engine.vram.resetPeak(); // reset the high-water mark to the current value
1228
- engine.vram.getReport(); // formatted one-line summary string
1229
- ```
1230
-
1231
- `peak` is a high-water mark, reset when a final render begins (`configureForMode('production')`). The engine's VRAM is largely monotonic — the ray pool only grows and the per-stage storage textures are fixed-size — so `peak` equals `current` during a steady render and only exceeds it after memory is released (lower resolution, a smaller scene, or removing the HDRI). The `stages` + `accum` categories (fixed 2048² storage textures) dominate the baseline.
1232
-
1233
- `denoiser` is what the engine allocates for OIDN — its three float inputs, its half-float output and
1234
- the motion history. `canvas` is one image per presented surface (a browser may keep one or two more)
1235
- plus the buffer three.js's output pass tone-maps through. Not counted: oidn-web's own network weights
1236
- and activations, which it reports by count, not by size; and the neural passes, which run on a
1237
- `GPUDevice` of their own.
1238
-
1239
- The React app surfaces this as a `Memory: … | Peak: …` readout in the on-canvas stats overlay.
1240
-
1241
- #### CPU memory
1242
-
1243
- The wall a large scene hits is not VRAM, it is contiguous `ArrayBuffer` address space on the CPU —
1244
- and how much of it a browser can still hand out falls as the tab stays up, so the same scene can
1245
- load after a restart and fail after a long session.
1246
-
1247
- ```js
1248
- const { preflight, allocatedBytes, peakLiveBytes, byPhase, samples } = engine.getHostMemoryInfo();
1249
- // null until a scene has been built
1250
- ```
1251
-
1252
- ⚠️ Do not use `performance.memory.usedJSHeapSize` for this. It does not count `SharedArrayBuffer`,
1253
- and the triangle and node stores are SAB-backed, so the browser's own reading under-reports a large
1254
- scene by gigabytes.
1255
-
1256
- Before extraction the engine prices the scene and applies two lines, both recording
1257
- `scene.memory_budget`:
1258
-
1259
- | Estimate | What happens |
1260
- |---|---|
1261
- | above ~7,040 MB | warns, and builds anyway |
1262
- | above ~9,216 MB | **throws** — past this the renderer process is killed rather than throwing an error you could catch, so refusing early is the only useful answer |
1263
-
1264
- Raise or lower the hard line with `new PathTracerApp(canvas, { maxSceneBytes })`. The estimate runs
1265
- low at the very top of its range, so the per-load `maxTriangles` cap (45M) is the more reliable
1266
- guard on a scene of that size. For a spilling build the estimate leaves out what the build keeps on
1267
- disk (the BVH, the three.js geometry, and triangle records past what a streamed build holds at once)
1268
- and prices the build by its larger phase — extraction, or the top-level tree — since the two never
1269
- hold their arrays at the same time. The whole Moana island estimates 8.6 GB that way.
1270
-
1271
- ---
1272
-
1273
- ### Logging
1274
-
1275
- Leveled, namespaced console output, shared with the engine's Web Workers. The default level is `info`, which hides per-mesh and per-texture detail; drop to `debug` to see it.
1276
-
1277
- ```js
1278
- import { Logger, createLogger, fmt, LOG_LEVELS } from 'rayzee';
1279
-
1280
- Logger.setLevel('debug'); // 'silent' | 'error' | 'warn' | 'info' | 'debug'
1281
- Logger.getLevel();
1282
- Logger.isEnabled('debug'); // gate expensive message construction
1283
- Logger.only('bvh', 'gpu'); // restrict debug to these namespaces (implies setLevel('debug'))
1284
- Logger.only(); // clear the namespace filter
1285
- Logger.refresh(); // re-read the level from globals/localStorage
1286
- ```
1287
-
1288
- The chosen level persists in `localStorage` under `rayzeeLogLevel` (namespace filter: `rayzeeLogNamespaces`), so it survives a reload. The engine does not install a global itself — expose one from your host if you want console access without an import; the demo app does `globalThis.rayzee = { log: Logger, ... }`, which is what makes `rayzee.log.setLevel('debug')` work there.
1289
-
1290
- `createLogger(namespace)` returns a channel with `error` / `warn` / `info` / `debug` plus `summary(headline, details)`, which prints one `info` line with the detail lines folded into a collapsed group. `fmt` holds the formatting helpers those summaries use — `n`, `ms`, `mb`, `px`, `count`, `list`. `LOG_LEVELS` is the name→severity map.
1291
-
1292
- ---
1293
-
1294
- ### Deterministic & Headless Rendering
1295
-
1296
- For offline rendering, regression testing, and benchmarking — drive accumulation yourself instead of the rAF loop, and get bit-reproducible output.
1297
-
1298
- ```js
1299
- engine.setDeterministicMode(true); // pin everything wall-clock- or readback-dependent
1300
- const samples = await engine.renderFrames(256, {
1301
- reset: true, // restart accumulation from sample 0
1302
- yieldEvery: 8, // yield to the event loop every N passes (0 disables)
1303
- onProgress: n => console.log(n),
1304
- });
1305
- const blob = await engine.screenshot();
1306
- engine.setDeterministicMode(false); // restore the previous configuration
1307
- ```
1308
-
1309
- The RNG is already pure — `hash(pixel, rayIndex, frame)`, no clock, no `Math.random()` in any shader. What varies run to run is *which* uniforms and dispatch grids are live on frame k, so `setDeterministicMode` disables adaptive sampling, per-pixel freeze, the readback-driven per-bounce early exit and dynamic dispatch sizing, interaction mode, auto-focus, and auto-exposure, and pins the sampler's seed axis to the accumulation frame. It also forces `renderLimitMode` to `'frames'` — a wall-clock render limit retires at a run-dependent sample count. It leaves the rAF loop stopped; `renderFrames` is the drive.
1310
-
1311
- - `engine.isDeterministic` — whether output is currently bit-reproducible.
1312
- - `setDeterministicMode(true, { pinDispatch: false })` keeps the two readback-driven dispatch heuristics active. Output is then *not* reproducible; this exists so performance measurements reflect shipping behaviour rather than a configuration production never runs.
1313
- - `renderFrames` raises `maxSamples` if needed, and throws if something retires the render early.
1314
-
1315
- #### Production renders, reproducibly
1316
-
1317
- Deterministic mode buys reproducibility by turning adaptive sampling off. To render the way the
1318
- product does — adaptive sampling, pixel freeze and the per-bounce early exit all on — and still get
1319
- the same image for the same input, drive the render with `renderUntilComplete`:
1320
-
1321
- ```js
1322
- engine.configureForMode('production');
1323
- const { samples, retiredBy, denoised } = await engine.renderUntilComplete({ signal });
1324
- // retiredBy: 'samples' | 'converged' | 'timeLimit'
1325
- ```
1326
-
1327
- It renders without `requestAnimationFrame` until the sample ceiling, convergence or the time limit —
1328
- whichever comes first — then runs the final OIDN denoise once, and fires `RENDER_COMPLETE` as the
1329
- loop does. Adaptive sampling decides from counts read back from the GPU; normally each is applied
1330
- whenever it lands, so the frame that retires depends on how fast frames are submitted. For its
1331
- duration `renderUntilComplete` runs those readbacks in **lockstep**: one issued at frame N is applied
1332
- at frame N + 4 exactly, and the render waits if it has not landed; every reset also starts from seed
1333
- 0 and from nothing the previous render measured. It turns interaction mode off meanwhile (a
1334
- wall-clock mode for camera drags), and keeps submissions at most 2 × `drainEvery` frames ahead of
1335
- the GPU. A time limit is wall-clock by nature, so that one stop is not reproducible. Auto exposure, if you turn it
1336
- on, lands on a metering of the finished image before the closing denoise.
1337
-
1338
- `engine.setLockstepReadbacks(true)` applies the same lockstep to the rAF loop and to `renderFrames`.
1339
-
1340
- #### Batch rendering
1341
-
1342
- The supported entry point for a render farm is `rayzee/src/Headless.js`, exported from the package.
1343
- Its defaults are a batch renderer's — `strict`, deterministic, storage off —
1344
- so a degraded render throws instead of shipping:
1345
-
1346
- ```js
1347
- import { openHeadless, captureHeadless } from 'rayzee';
1348
-
1349
- const app = await openHeadless({ canvas, model: url, width: 1920, height: 1080 });
1350
- try {
1351
- const frame = await captureHeadless(app, { samples: 256, denoise: true });
1352
- // frame.data: RGBA8 (colorSpace 'srgb') or Float32 (colorSpace 'linear')
1353
- // frame.source: what was read — 'oidn' here, 'accumulation' without denoise
1354
- // frame.issues: everything the engine survived, when not strict
1355
- } finally {
1356
- app.dispose();
1357
- }
1358
- ```
1359
-
1360
- `denoise: true` turns OIDN on before accumulating (it reads the albedo and normal buffers, which
1361
- are written only while it is on), runs **one** final denoise with `app.runFinalDenoise()`, and reads
1362
- it back with `renderToBuffer( { source: 'display' } )`. Driving it yourself is the same three calls.
1363
- `renderToBuffer` reports the picture it read as `source`, and records `output.source_fallback` when
1364
- `'display'` was asked for, a denoiser is in use, and nothing had published — so a strict host cannot
1365
- ship a noisy image by mistake. Its `'srgb'` bytes are tone-mapped on the GPU — 9 ms at 4096×2160,
1366
- where the CPU pass it replaced took 1.4 s on an M-series Mac and 10 s on a cloud host — and match the
1367
- canvas to within one level (it rounds half a level up); `'linear'` is exact. When the GPU pass is
1368
- unavailable or fails, the CPU does it instead: the result's `toneMappedOn` says `'cpu'` rather than
1369
- `'gpu'`, and `output.tonemap_fallback` is recorded with the reason.
1370
-
1371
- Constructing `PathTracerApp` yourself instead: pass `strict: true`; storage is then off unless you
1372
- set it. Outside Chrome, pass `hostMemoryGB`.
1373
-
1374
- The engine has one set of defaults, the viewer's. For lens-accurate depth of field and glTF placeholder area
1375
- lights at their authored power, set `dofMode: 'physical'` and `areaLightIntensityScale: 1` — through `settings`
1376
- here, or `engine.settings` before loading the model.
1377
-
1378
- **Provenance.** `engine.getProvenance()` — also `frame.provenance` from `captureHeadless` — is plain
1379
- JSON naming what produced the image: engine and three.js versions, the adapter, every live setting with its source, the colour pipeline, the render size and
1380
- samples, and whether it ran headless, strict, deterministic or lockstepped. `mode.lockstep` says
1381
- whether the current image was traced in lockstep, so it stays true after `renderUntilComplete` turns
1382
- lockstep back off. Store it beside each render and "what made this?" has an answer without rendering
1383
- again.
1384
-
1385
- **Pinning a look.** A change to what a render looks like when a host sets nothing — a default
1386
- setting, a mode preset, light units — is released as a **major**, with the
1387
- change described under BREAKING CHANGES in the release notes. Pin a major to pin a look; read the
1388
- notes before moving to the next one.
1389
-
1390
- #### Running in Node
1391
-
1392
- The published build renders in plain Node on Dawn — the `webgpu` package, the WebGPU implementation
1393
- inside Chrome — with no browser and no DOM shim:
1394
-
1395
- ```js
1396
- import { create, globals } from 'webgpu';
1397
- import sharp from 'sharp';
1398
- import { configurePlatform, openHeadless } from 'rayzee';
1399
- import { nodePlatform } from 'rayzee/node';
1400
-
1401
- Object.assign(globalThis, globals);
1402
- const gpu = create([]); // keep a reference: Dawn crashes if it is collected
1403
- Object.defineProperty(navigator, 'gpu', { value: gpu });
1404
-
1405
- const decodeImage = async (bytes) => {
1406
- const { data, info } = await sharp(bytes).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
1407
- return { data, width: info.width, height: info.height };
1408
- };
1409
- configurePlatform(nodePlatform({ decodeImage })); // after the webgpu globals, before any app
1410
- const app = await openHeadless({ width: 1920, height: 1080, hostMemoryGB: 16 }); // no canvas: headless
1411
- await app.loadModel('https://…/room.glb');
1412
- const { samples } = await app.renderUntilComplete();
1413
- const frame = await app.renderToBuffer({ source: 'display' });
1414
- app.dispose();
1415
- ```
1416
-
1417
- - **No canvas means headless.** `new PathTracerApp(null)` — or `{ headless: true }` — makes its own
1418
- canvas (a WebGPU context over a plain texture), runs no render loop (drive it with `renderFrames` or
1419
- `renderUntilComplete`), and builds neither the overlay renderer nor the transform gizmo.
1420
- - **`configurePlatform({ Worker, decodeImage })`** is where a host without a browser supplies what one
1421
- would. `nodePlatform()` from `rayzee/node` fills it: `NodeWorker`, the Web Worker API over
1422
- `worker_threads`, runs the engine's inlined workers; `decodeImage(bytes, mimeType)` is yours — PNG,
1423
- JPEG or WebP to RGBA8, straight alpha, top row first — since the engine carries no decoder. It decodes
1424
- every glTF image, embedded or not, and JPEG/PNG skies. PNGs with bytes after `IEND`, which browsers
1425
- accept and strict decoders refuse, are trimmed first; a texture that still fails is recorded as
1426
- `texture.build_failed` rather than dropped. Without `decodeImage`, each glTF image is recorded that way with what to
1427
- configure (a strict host's load throws), and the model loads untextured. KTX2 (Basis) textures need no
1428
- `decodeImage`: three's KTX2 loader decodes them, in a `NodeWorker` the engine lends it. Measured on a 91-texture model: `sharp` and
1429
- `@napi-rs/canvas` (`await loadImage()`, then a 2D canvas) both decode in parallel, ~0.3 s against
1430
- pngjs's 2.5 s serial. The canvas route premultiplies, which zeroes colour under alpha 0 and loses
1431
- it under low alpha; `sharp` is exact. `jpeg-js` differs from libjpeg-turbo by up to 63 levels.
1432
- - **`nodePlatform()` wraps the `webgpu` queue**, so install its globals first. dawn.node 0.6.1
1433
- segfaults on `writeBuffer` / `writeTexture` from a `SharedArrayBuffer`, which WebGPU allows and the
1434
- engine's triangle and BVH stores are once a scene is large; those uploads are copied out, 64 MB at
1435
- a time.
1436
- - **Textures are packed on the CPU** where there is no `createImageBitmap`: exact when a map fits its
1437
- bucket, bilinear otherwise (a browser's canvas filter differs slightly there). A bucket over 8 MB
1438
- packs in a worker, reading images decoded by `decodeImage` in place (they are kept in shared
1439
- memory): on the main thread it held up the BVH workers, and a 1.9M-triangle, 91-texture model
1440
- loaded in 4.1–4.9 s. It now loads in 2.1 s, against Chrome's 2.9 s on the same machine.
1441
- - `nodePlatform()` also defines `ProgressEvent`, which three.js's `FileLoader` constructs while
1442
- streaming; that is the only global it sets. three.js's own Draco and KTX2 loaders call the global
1443
- `Worker`; the engine lends them `NodeWorker` for the length of each glTF parse, so a Draco or KTX2
1444
- model loads with nothing set by the host.
1445
-
1446
- Measured on this bench's corpus: all 29 scenes match the Chrome goldens (`npm run bench:node`, RMSE
1447
- ≤ 0.0036, no pixel over 0.02 — the readback's tone map accounts for most of it), and a textured glTF with an
1448
- HDR or a PNG sky matches Chrome block for block within 0.05 of a level. Not available
1449
- without a browser: on-disk storage (OPFS), gobo libraries (they draw on a 2D canvas), and the AI
1450
- upscaler.
1451
-
1452
- #### GPU timing
1453
-
1454
- ```js
1455
- engine.enableGPUTiming(true); // off by default — the queries themselves cost time
1456
- const { compute, render, total } = await engine.getGPUTimings();
1457
- const { kernels, unattributed, frame } = await engine.getKernelGPUTimings();
1458
- ```
1459
-
1460
- WebGPU timestamp queries are the only true GPU metric here — `pipeline.getStats()` times command *encoding* on the CPU and stays flat while GPU cost doubles. Both methods return `null` when the device lacks `timestamp-query` or timing was never enabled.
1461
-
1462
- `getKernelGPUTimings()` attributes each compute pass of the last resolved frame back to a wavefront kernel name. Durations are **summed per kernel across the frame**, so `extend` reports its whole per-frame cost over every bounce iteration, not one bounce. `unattributed` collects passes belonging to no registered kernel (other stages, denoisers), so `sum(kernels) + unattributed` reconciles with `total`.
1463
-
1464
- Neither method can see the OIDN denoise — `oidn-web` submits on its own command encoders, outside
1465
- the stages three.js times. It carries its own profiler instead:
1466
-
1467
- ```js
1468
- engine.profileNextDenoise(); // arms one capture; per-denoise, not sticky
1469
- const { profile, runtime } = await engine.getDenoiseProfile();
1470
- ```
1471
-
1472
- `profile` is the per-layer GPU timing of that denoise; `runtime` reports the selected engine and
1473
- precision, the model, kernel capabilities, tile state and resource counts — which is the way to
1474
- confirm FP16 actually engaged on a given GPU rather than inferring it from the device's feature
1475
- list. Both need `timestamp-query`, and `getDenoiseProfile()` returns `null` when OIDN is not set up.
1476
-
1477
- ### On-disk Storage (OPFS)
1478
-
1479
- On `rayzee/core`, storage needs the `rayzee/addons/storage` add-on (`setStorageOpener`) — see [Renderer core](#renderer-core-rayzeecore).
1480
-
1481
- `engine.storage` is a `StorageManager` over the browser's origin private file system, or `null` where
1482
- there is none (a private window, Node) — everything works without it, only slower. The engine keeps
1483
- its caches there: downloads (models, skies, OIDN weights — revalidated at most daily with a 1-byte `Range` request, and served from the cache meanwhile),
1484
- unpacked `.tar.gz` archives and archive indexes, built scenes (reopened without rebuilding BVHs when
1485
- the first build took ≥ 10 s), and environment sampling tables. Caches share a budget of 30 % of the
1486
- quota and are evicted least-recently-used; `engine.storage.usage()` reports each area.
1487
-
1488
- A host keeps its own data beside them:
1489
-
1490
- ```js
1491
- import { STORAGE_KIND } from 'rayzee';
1492
-
1493
- engine.storage.defineArea( 'renders', { kind: STORAGE_KIND.USER } ); // never evicted
1494
- const writer = await engine.storage.area( 'renders' ).create( 'render:42', { label: 'Render 42' } );
1495
- await writer.writeFile( 'image.png', blob );
1496
- await writer.commit(); // meta.json last: all or nothing
1497
-
1498
- const entry = await engine.storage.area( 'renders' ).open( 'render:42' );
1499
- const file = await entry.file( 'image.png' );
1500
- entry.release();
1501
- ```
1502
-
1503
- `loadFile( url )` and `loadModel( url, { cacheKey } )` download through the cache — pass `cacheKey` for
1504
- a link that expires (a signed URL). `fetchFile( url, storage )` / `cachedObjectURL( url )` do the same
1505
- for a host's own assets. Failures record `storage.*` issues and fall back to memory; storage never
1506
- throws for being absent or full.
1507
-
1508
- **Memory spill.** A static scene too large for memory is built through disk, so its large arrays are never all in
1509
- memory at once. By default (`memorySpill: 'auto'`) a scene spills only when its in-memory estimate passes the safe
1510
- line (7 GB) — ordinary models never do and load exactly as before; `true` spills every static scene of more than one
1511
- 64 MB chunk, `false` none. The decision is made per load, and a spilling scene's environment is narrowed to 8192 texels
1512
- wide:
1513
-
1514
- - Extraction and BVH building run together. Each stored range of triangles goes to a BLAS worker as
1515
- soon as it is written, and extraction waits while more than 1 GB of triangle records are held.
1516
- On the 55.7M-triangle Moana subset the build peak fell from 6.6 to 4.1 GB.
1517
- - The three.js geometry goes to disk after the build last reads it. Past 1 GB of it, it **stays there**
1518
- until something needs it: `await engine.ensureSceneResident()` reads it back, and so does a rebuild of
1519
- the same model. Meanwhile clicking the model selects nothing and a selection draws no outline (its
1520
- arrays are empty). Less than 1 GB comes back when the build ends. On a 70M-triangle scene
1521
- the page held 0.84 GB after extraction instead of 4.07 GB.
1522
- - Each triangle chunk and BLAS goes to the GPU, then to disk. At 50M triangles the page settles at
1523
- 7.2 GB instead of 9.1 GB.
1524
- - Once the load is done, the instance matrix lists, the top-level tree with its copy records and the
1525
- triangle order maps go to disk too. An edit reads back what it needs (a visibility change the tree, a
1526
- move the matrices, the tree and that object's copy records) and lands once it has; 30 s after the
1527
- last edit it all goes back. On the Moana island the CPU data held at rest fell from 6.9 to 2.2 GB, and
1528
- a tab in Chrome from 5.2 to 3.6 GB.
1529
- - An environment wider than 8192 pixels is box-filtered down when it loads.
1530
-
1531
- With or without the spill, a mesh past 2M triangles is built as spatial pieces of ≤ 512k triangles
1532
- on a worker pool and joined under the tree that split them, so no build holds a second copy of it.
1533
-
1534
- The render is unchanged. Material edits that rewrite triangles (side, transparency, emission), and
1535
- refits, read the scene back first. `refitBLASes` throws on a spilled scene until
1536
- `await engine.ensureSceneResident()`.
1537
-
1538
- ### Saving Scene State
1539
-
1540
- ```js
1541
- const state = engine.exportSceneState(); // JSON-safe: settings, sky, colour, lights, cameras,
1542
- // timeline keys, material edits, hidden and moved objects
1543
- const source = engine.sceneSource; // { kind: 'url', url, cacheKey } | { kind: 'local-file', file } | { kind: 'local-folder', folder } | ...
1544
-
1545
- // later, after loading the same model again:
1546
- const { skipped } = await engine.importSceneState( state, {
1547
- resolve: async ( request ) => {
1548
- // { kind: 'environment', source } → a File, a URL or null (the engine loads URL skies itself)
1549
- // { kind: 'colorConfig', config } → true once you have loaded it (built-in configs load themselves)
1550
- return null;
1551
- },
1552
- } );
1553
- ```
1554
-
1555
- Objects, materials and cameras are matched by position in the scene plus a name check — UUIDs
1556
- change on every load — so a different model keeps its own objects and materials and the rest still
1557
- applies; `skipped` lists what could not be put back. Texture swaps on materials are not included yet.
1558
-
1559
- ### Render Checkpoints
1560
-
1561
- ```js
1562
- const checkpoint = await engine.captureRenderCheckpoint(); // accumulation + the counters that pick the next sample
1563
- // ...a reload later, the same scene state restored and one frame rendered at the same size:
1564
- engine.restoreRenderCheckpoint( checkpoint );
1565
- engine.wake(); // or await engine.renderFrames( n, { reset: false } )
1566
- ```
1567
-
1568
- In deterministic mode the continued render is bit-identical to one that never stopped. A checkpoint
1569
- is ~60 bytes a pixel (252 MB at 2048²): the accumulated colour and aux buffers as RGBA32F plus three
1570
- per-pixel convergence buffers.
1571
-
1572
- ---
1573
-
1574
- ### Events
1575
-
1576
- Subscribe to engine lifecycle events via `addEventListener`:
1577
-
1578
- ```js
1579
- import { EngineEvents } from 'rayzee';
1580
-
1581
- engine.addEventListener(EngineEvents.RENDER_COMPLETE, (e) => {
1582
- console.log('Render complete');
1583
- });
1584
- ```
1585
-
1586
- | Event | Fired when |
1587
- |---|---|
1588
- | `RENDER_COMPLETE` | Rendering has converged |
1589
- | `SHADERS_COMPILING` | The path tracer compiles its shaders in the background (`event.compiling` true, then false) — after a model load, a new material layer, a light transport switch. The page stays responsive; the canvas keeps its last frame until they are ready, and `renderFrames()` waits for them |
1590
- | `RENDER_RESET` | Accumulation buffer is reset |
1591
- | `FRAME` | Fires once per `animate()` tick — hook external instrumentation (stats panels, telemetry) here |
1592
- | `DENOISING_START` / `DENOISING_END` | Denoiser runs. `event.continuous` is `true` for a cadence denoise of the still-accumulating image, `false` for the one that ends a render |
1593
- | `DENOISER_LOADING` | The denoiser fetches and builds a network (`event.loading` true, then false) — on first use and on a quality change. A denoise asked for meanwhile starts once it is ready |
1594
- | `UPSCALING_START` / `UPSCALING_PROGRESS` / `UPSCALING_END` | AI upscaler runs |
1595
- | `LOADING_UPDATE` / `LOADING_RESET` | Asset loading progress. A failed load ends with `failed: true` and the error as `status`; an archive that asks which parts to load ends with `LOADING_RESET` |
1596
- | `STATS_UPDATE` | Performance stats updated |
1597
- | `OBJECT_SELECTED` / `OBJECT_DESELECTED` | Object selection changes |
1598
- | `OBJECT_DOUBLE_CLICKED` | Object double-clicked |
1599
- | `OBJECT_TRANSFORM_START` / `OBJECT_TRANSFORM_END` | Transform gizmo drag |
1600
- | `TRANSFORM_MODE_CHANGED` | Gizmo mode changed |
1601
- | `SELECT_MODE_CHANGED` | Selection mode toggled |
1602
- | `SETTING_CHANGED` | A render setting is modified |
1603
- | `AUTO_FOCUS_UPDATED` | Auto-focus recalculated — `worldDistance` in scene units, `distance` divided by the model's size |
1604
- | `ORTHO_HEIGHT_UPDATED` | An orthographic view's height changed — `height` in scene units |
1605
- | `AUTO_EXPOSURE_UPDATED` | Auto exposure moved — `exposure` (applied, with the manual exposure), `autoExposure` (its own part), `targetExposure`, `luminance` (metered) |
1606
- | `AF_POINT_PLACED` | Focus point placed on screen |
1607
- | `ANIMATION_STARTED` / `ANIMATION_PAUSED` / `ANIMATION_STOPPED` / `ANIMATION_FINISHED` | Animation lifecycle |
1608
- | `VIDEO_RENDER_PROGRESS` / `VIDEO_RENDER_COMPLETE` | Video export progress |
1609
- | `DEVICE_LOST` | The GPU device was lost (driver crash/reset) — rendering halts instead of throwing into a dead device |
1610
- | `DISPOSE` | Engine is being disposed (fires before teardown begins, so listeners can release their own references) |
1611
- | `ISSUE` | An issue was recorded — see [Degradation contract](#degradation-contract) |
1612
- | `MODEL_LOADED` / `OBJECT3D_LOADED` / `MODEL_ADDED` / `SCENE_OBJECT_REMOVED` / `SCENE_UNLOADED` | Scene content changed |
1613
- | `SCENE_REBUILD` / `SCENE_SPILLED` / `SCENE_METADATA_APPLIED` | The scene was rebuilt, moved to disk, or had its authored environment applied |
1614
- | `ENVIRONMENT_LOADED` / `TEXTURES_REPROCESSED` | An environment or the texture arrays were (re)built |
1615
- | `CAMERAS_UPDATED` / `CAMERA_SWITCHED` / `FOCUS_CHANGED` | Camera list, active camera, or focus distance changed |
1616
- | `RESOLUTION_CHANGED` / `RESERVED_RENDER_SIZE_CHANGED` | The render size or the reserve changed |
1617
- | `STORAGE_CHANGED` / `TIMELINE_CHANGED` | On-disk storage or timeline keys changed |
1618
-
1619
- Several of these used to be plain strings (`'ModelLoaded'`, `'RenderComplete'`, `'resolution_changed'`, …).
1620
- The engine still dispatches each under its old name as well, until the next major; `LEGACY_EVENT_NAMES`
1621
- maps new to old. Listening for a name the engine never dispatches logs a warning once, since such a
1622
- listener fails silently otherwise.
1623
-
1624
- ### Advanced: Custom Pipeline Stages
1625
-
1626
- Build custom rendering stages by extending `RenderStage`:
1627
-
1628
- ```js
1629
- import { RenderStage } from 'rayzee';
1630
-
1631
- class MyCustomStage extends RenderStage {
1632
- constructor() {
1633
- super('my-stage');
1634
- }
1635
-
1636
- render(context, writeBuffer) {
1637
- const input = context.getTexture('pathtracer:color');
1638
- // ... process input, write output
1639
- context.setTexture('my-stage:output', this.outputTexture);
1640
- }
1641
- }
1642
- ```
1643
-
1644
- To run between the path tracer and the compositor, subclass the renderer and return the stage from
1645
- `_createExtraStages()` (call the parent's and add yours); for the compositor to show its picture, list
1646
- `'my-stage:output'` in `_displaySources()`. `pipeline.addStage()` after `init()` appends after the
1647
- compositor. A stage that needs an extra path-tracer output asks for it with
1648
- `stages.pathTracer.requestOutput( name, options )` — `'gBuffer'` (the normal/depth and albedo a denoiser reads; the
1649
- viewer asks for it itself) or `'hitDistance'`. Each is compiled into the path tracer only while asked for.
1650
-
1651
- ### All Exports
1652
-
1653
- ```js
1654
- // The full engine (it also exports RayzeeRenderer)
1655
- import { PathTracerApp, EngineEvents, LEGACY_EVENT_NAMES } from 'rayzee';
1656
-
1657
- // The renderer core and its add-ons (see Renderer core)
1658
- import { RayzeeRenderer } from 'rayzee/core';
1659
- import { PhysicalSky } from 'rayzee/addons/physical-sky';
1660
- import { ArchiveImporter } from 'rayzee/addons/archives';
1661
- import { BidirectionalIntegrator } from 'rayzee/addons/bidirectional';
1662
- import { ColorManagement } from 'rayzee/addons/color';
1663
- import { acquireSharedStorage } from 'rayzee/addons/storage';
1664
-
1665
- // Headless rendering (see Deterministic & Headless Rendering)
1666
- import { renderHeadless, openHeadless, captureHeadless } from 'rayzee';
1667
-
1668
- // File writers behind exportImage(), for pixels a host already has (see Saving a file)
1669
- import {
1670
- IMAGE_FORMATS, encodePNG, encodeEXR, encodeHDR, encodeWithCanvas, encodeGainMapJPEG,
1671
- encodeStill, canEncodeStill, prepareStill,
1672
- } from 'rayzee/encoders';
1673
-
1674
- // Platform services for hosts without a browser (see Running in Node)
1675
- import { configurePlatform, getPlatform } from 'rayzee';
1676
- import { nodePlatform, NodeWorker } from 'rayzee/node';
1677
-
1678
- // Configuration & presets
1679
- import {
1680
- ENGINE_DEFAULTS,
1681
- DENOISER_DEFAULTS,
1682
- AUTO_EXPOSURE_DEFAULTS,
1683
- LOCAL_EXPOSURE_DEFAULTS,
1684
- AUTO_FOCUS_DEFAULTS,
1685
- DENOISER_STRATEGIES,
1686
- SKY_DEFAULTS,
1687
- DEFAULT_SUN_PATH,
1688
- DEFAULT_VIEW,
1689
- TRIANGLE_DATA_LAYOUT,
1690
- BVH_LEAF_MARKERS,
1691
- PRODUCTION_RENDER_CONFIG,
1692
- INTERACTIVE_RENDER_CONFIG,
1693
- MAX_RESERVABLE_RENDER_SIZE,
1694
- MATERIAL_DEFAULTS,
1695
- } from 'rayzee';
1696
-
1697
- // Colour management — engine.color is the instance a host normally uses; these build UI against
1698
- // it, or reach the view-transform registry without an app
1699
- import {
1700
- ColorManagement, getActiveColorManagement, isColorManaged, DEFAULT_WORKING_SPACE,
1701
- listViewTransforms, getViewTransform, addViewTransform, removeViewTransform, onRegistryChange,
1702
- buildOcioView, addOcioView, addAllOcioViews,
1703
- convertColor, convertPixelsF32, extractMatrix, hasColorSpace,
1704
- displayCanvasFit,
1705
- } from 'rayzee';
1706
-
1707
- // Where the sun stands for a solar time, date and latitude (degrees; azimuth clockwise from north)
1708
- import { sunPosition, timeForSunElevation, dayOfYearForMonth } from 'rayzee';
1709
-
1710
- // Leveled/namespaced logging, shared with the workers
1711
- import { Logger, createLogger, fmt, LOG_LEVELS } from 'rayzee';
1712
-
1713
- // Asset URL / cache namespace overrides
1714
- import { configureAssets, getAssetConfig } from 'rayzee';
1715
-
1716
- // On-disk storage (OPFS): caches, host areas, downloads, file identity
1717
- import {
1718
- openStorage, StorageManager, STORAGE_KIND, ENGINE_AREAS, acquireLock, heldLockNames,
1719
- fileIdentity, identityKey, sameIdentity,
1720
- DownloadCache, DOWNLOAD_POLICY, fetchFile, nameFromUrl, cachedObjectURL,
1721
- } from 'rayzee';
1722
-
1723
- // Archives read in place, one entry at a time
1724
- import { openZip, readZipDirectory } from 'rayzee';
1725
-
1726
- // Scene state as plain data (engine.exportSceneState), and the engine version as built
1727
- import { SCENE_STATE_VERSION, toPortable, fromPortable, VERSION } from 'rayzee';
1728
-
1729
- // Advanced: managers & pipeline
1730
- import {
1731
- RenderSettings,
1732
- CameraManager,
1733
- LightManager,
1734
- GoboManager,
1735
- IESManager,
1736
- DenoisingManager,
1737
- OverlayManager,
1738
- AnimationManager,
1739
- TransformManager,
1740
- VideoRenderManager,
1741
- TimelineManager,
1742
- CameraTrack,
1743
- InteractionManager,
1744
- RenderPipeline,
1745
- RenderStage,
1746
- StageExecutionMode,
1747
- PipelineContext,
1748
- } from 'rayzee';
1749
-
1750
- // VRAM accounting (VRAMTracker is also reachable as engine.vram)
1751
- import { VRAMTracker, bufferBytes, textureBytes } from 'rayzee';
1752
-
1753
- // Degradation contract — see above. ISSUE_CODES is add-only; pin a version and branch on it.
1754
- import { ISSUE_CODES, ISSUE_SEVERITY, IssueLog, EngineIssueError } from 'rayzee';
1755
-
1756
- // CPU memory: price a scene before loading it, or measure what can still be placed
1757
- import {
1758
- MemoryLedger,
1759
- estimateSceneBytes,
1760
- probeAddressSpace,
1761
- SAFE_SCENE_BYTES,
1762
- MAX_SCENE_BYTES,
1763
- } from 'rayzee';
1764
-
1765
- // Adapter description — flags software rasterisers (SwiftShader, llvmpipe, lavapipe, WARP)
1766
- import { describeAdapter } from 'rayzee';
1767
-
1768
- // Dev-only: texture-binding aliasing guard. Two TextureNodes still holding the default
1769
- // EmptyTexture when a kernel is first compiled can share one GPU binding — nothing throws,
1770
- // the aliased node just reads someone else's texture. Off by default; costs a per-stage
1771
- // snapshot when on. Intended for test harnesses, not production.
1772
- import { setBindingAudit, getBindingAuditFindings, clearBindingAuditFindings } from 'rayzee';
1773
- ```
1774
-
1775
97
  ## Browser Requirements
1776
98
 
1777
99
  - WebGPU support (Chrome 113+, Edge 113+, Safari 18+, Firefox 141+)
1778
100
  - Secure context (HTTPS or localhost)
1779
101
 
1780
- ## Optional Dependencies
1781
-
1782
- | Package | Purpose | Install needed? |
1783
- |---|---|---|
1784
- | `oidn-web` | Intel Open Image Denoise for high-quality final renders | Yes — `npm install oidn-web` (**>=0.5.0**) |
1785
- | `onnxruntime-web` | AI-powered upscaling | No — loaded from CDN at runtime |
1786
- | `@bb-studio/ocio` | OpenColorIO runtime for [Colour Management](#colour-management) (~6 MB WebAssembly) | Only to load colour configs — `npm install @bb-studio/ocio`, then pass it as `ocioRuntimeFactory` |
1787
-
1788
- > **Note:** `onnxruntime-web` is also listed in `package.json` under `optionalDependencies` for bundler compatibility, but the engine's own runtime path always fetches it from a CDN (see `ortRuntimeUrl` / `ortWasmPaths` in [Configuring Assets](#configuring-assets-cdn-urls--cache-namespace)) rather than importing the installed package — installing it locally has no effect unless you also override those URLs to point at your own copy.
1789
-
1790
- ### Enabling OIDN (Intel Open Image Denoise)
1791
-
1792
- OIDN provides high-quality AI denoising. It runs automatically once the render converges (reaches
1793
- `maxSamples`), and — in `'interactive'` mode — also on a cadence while the image is still
1794
- accumulating, so a preview shows a clean picture as it refines instead of only at the end. See
1795
- [When the denoiser runs](#when-the-denoiser-runs).
1796
-
1797
- 1. **Install the package**
1798
-
1799
- ```bash
1800
- npm install oidn-web
1801
- ```
1802
-
1803
- 2. **Enable in your app**
1804
-
1805
- ```js
1806
- // After engine.init() completes
1807
- engine.denoisingManager.setOIDNEnabled(true);
1808
- engine.denoisingManager.setOIDNQuality('balance'); // 'fast' | 'fast-clean' | 'balance' | 'high'
1809
- ```
1810
-
1811
- 3. **Listen for progress** (optional)
1812
-
1813
- ```js
1814
- engine.addEventListener(EngineEvents.DENOISING_START, () => {
1815
- console.log('Denoising started');
1816
- });
1817
- engine.addEventListener(EngineEvents.DENOISING_END, () => {
1818
- console.log('Denoising complete');
1819
- });
1820
- ```
1821
-
1822
- | Quality | Weights | Aux guide | Best for |
1823
- |---|---|---|---|
1824
- | `'fast'` | 0.6 MB | point-sampled | Low sample counts — the default |
1825
- | `'fast-clean'` | 0.6 MB | accumulated | Converged frames, at `'fast'`'s cost |
1826
- | `'balance'` | 1.8 MB | accumulated | General use |
1827
- | `'high'` | 7.3 MB | accumulated | Final renders — used by `configureForMode('production')` |
1828
-
1829
- #### When the denoiser runs
1830
-
1831
- Two independent decisions, because they answer different questions — *what cleans the view while the
1832
- render works*, and *does the finished image get a proper pass*:
1833
-
1834
- ```js
1835
- engine.denoisingManager.setStrategy('oidn'); // 'none' | 'nrd' | 'oidn'
1836
- engine.denoisingManager.setOIDNEnabled(false); // a full OIDN pass on the finished image
1837
- ```
1838
-
1839
- Exactly one denoiser owns the live view, which is why OIDN is an entry in that list rather than a
1840
- parallel switch — two of them would mean paying for a per-frame denoise whose result the OIDN
1841
- overlay immediately covers. But choosing OIDN there says nothing about the finished image, and
1842
- switching the final pass on says nothing about the live view. All six combinations are reachable:
1843
-
1844
- | `setStrategy` | `setOIDNEnabled` | Camera moving | Still, accumulating | Finished |
1845
- |---|---|---|---|---|
1846
- | `'none'` | `false` | raw | raw | raw |
1847
- | `'none'` | `true` | raw | raw | OIDN |
1848
- | `'nrd'` | `false` | NRD | NRD, then raw once it hands over | NRD's last frame, or raw past the handover |
1849
- | `'nrd'` | `true` | NRD | NRD, then raw once it hands over | OIDN |
1850
- | `'oidn'` | `false` | OIDN, from the motion history | OIDN, refreshing | one last refresh |
1851
- | `'oidn'` | `true` | OIDN, from the motion history | OIDN, refreshing | OIDN, full quality |
1852
-
1853
- `denoiser.enabled` is the union of the two — "OIDN is in use at all", which is what the aux G-buffer
1854
- wiring needs — so read the two decisions back from `denoisingManager.denoiserStrategy` and
1855
- `denoisingManager.finalDenoise`, not from it.
1856
-
1857
- With OIDN on the live view and the final pass off, the render still closes with one more refresh: the
1858
- cadence's last tick lands short of the end, and the picture should match the render that finished. On
1859
- a 150-sample render that gap measured **11 samples at 512²** and **1 at 1024²** — larger where the
1860
- renderer is fast, because more samples land between refreshes. It uses whatever model the refreshes
1861
- were already on: no reload for an image the user never asked to be denoised at full quality. A final render
1862
- (`configureForMode( 'production' )`) has no live view to match, so with the final pass off it finishes undenoised.
1863
-
1864
- In the app that is `Real-Time Denoiser` (None / NRD / **OIDN (AI)**, OIDN by default) and the
1865
- `Final Denoise (OIDN)` switch. Deterministic mode pins the live refreshes off, since which frame a
1866
- wall-clock cadence lands on is not reproducible.
1867
-
1868
- Leaving the live view raw is not just "denoising off" — it is the only way to see the true noise
1869
- level, which is how you judge whether a render has actually settled. That is row one and row two.
1870
-
1871
- #### While the view moves
1872
-
1873
- Every frame restarts while the camera or an object moves, so a refresh would otherwise denoise one
1874
- fresh sample with its own independent noise, and OIDN's guesses would jump from refresh to refresh
1875
- ("boiling"). Instead each restarted frame is blended into a per-pixel **motion history**, reprojected
1876
- into the current view, and the live refreshes denoise that. Measured on a 1.7M-triangle interior at
1877
- 512² against 128-sample references: blotchy flicker 2.12 → 0.90 while orbiting and 1.72 → 0.78 moving
1878
- forward, each frame 16 % closer to the clean render, refresh rate unchanged.
1879
-
1880
- - Reflections and a moving object's lighting do not travel with the surface, so shiny pixels and
1881
- pixels of moved objects keep only about 2 frames of history.
1882
- - When the view stops, the history is blended into the fresh accumulation and fades out over the
1883
- first 16 samples; the final denoise always reads the plain accumulation.
1884
- - A reset that changes what the scene looks like (a light, a material, a setting) drops the history.
1885
- Moves go through `reset(true)` (the camera) or `reset(false, { motion: true })`, which the engine's
1886
- own `updateMeshTransforms`, `refitBVH`, `refitBLASes` and animation playback already use.
1887
- - Moved placements are followed through their matrices when they move through
1888
- `updateMeshTransforms` or rigid animation; deformed geometry is rejected by its changed depth.
1889
- - Cost, only while OIDN owns the live view: +154 MB of VRAM at 512², +232 MB at 1024², and ~5 % GPU
1890
- per frame while moving, mostly the `NormalDepth` stage it switches on. At a size the refresh
1891
- cadence has proven too slow to denoise while moving (the raw render owns the view there), both are
1892
- released until the size changes.
1893
-
1894
- `setTemporalHistory(false)` returns to denoising the single fresh frame.
1895
-
1896
- #### Quality while it runs vs. quality when it finishes
1897
-
1898
- `oidnQuality` is **the quality of the finished image**. The refreshes along the way use the cheapest
1899
- model that reads the same kind of aux buffer, and the chosen model is put back for the last denoise:
1900
-
1901
- | `oidnQuality` | refreshes use | finished image |
1902
- |---|---|---|
1903
- | `fast` | `fast` | `fast` |
1904
- | `fast-clean` / `balance` / `high` | `fast-clean` | as chosen |
1905
-
1906
- Two constraints shape that table, and neither is optional:
1907
-
1908
- - **The aux kind must not change mid-render.** `setCleanAuxNormal()` throws away the accumulated
1909
- albedo/normal, so a refresh model that disagreed with the final one would leave the final denoise
1910
- reading an aux buffer one sample deep. That is why the cheap model is `fast-clean` and not `fast`.
1911
- - **The cheap model only takes over once a denoise has measured too slow to be a live view
1912
- (`> 120 ms`).** A tier a machine can afford is kept — at 512² that is every tier. The verdict
1913
- survives a camera move, because the device does not get faster between them; it is re-taken when
1914
- the tier or the resolution changes. Past it, each render swaps twice (to the cheap model when the
1915
- view moves, back for the finished image). A swap costs 10-20 ms on oidn-web 0.4.0, and each model's
1916
- weights are downloaded once and kept, so a swap never goes back to the network.
1917
-
1918
- #### What paces the refreshes
1919
-
1920
- Two knobs with two different jobs, and the gap between refreshes is whichever is larger:
1921
-
1922
- - **The cost floor protects the renderer.** The gap is at least twice what the last denoise actually
1923
- cost, so denoising never takes more than about half the wall clock, at any resolution, on any GPU.
1924
- This is not configurable, and it is what binds at the default.
1925
- - **`continuousDenoiseInterval` caps the refresh rate in absolute terms**, for a host that wants
1926
- fewer updates than the renderer could afford — a laptop on battery, or a viewport where 30 updates
1927
- a second is distracting. The default (8 ms) is below any real denoise cost, so it never binds:
1928
- refreshes run as often as the cost floor allows. Raising it gives a flat `1000 / interval` cap.
1929
-
1930
- Measured, `fast` model:
1931
-
1932
- | `continuousDenoiseInterval` | 8 | 50 | 100 | 200 | 400 |
1933
- |---|---|---|---|---|---|
1934
- | 512² (denoise 12 ms) | 35/sec | 18/sec | 9.6/sec | 4.9/sec | 2.6/sec |
1935
- | 1024² (denoise 50 ms) | 9/sec | 9/sec | 9/sec | 4.9/sec | 2.6/sec |
1936
-
1937
- The two columns converge once the interval is the larger of the two — below that the cost floor is
1938
- holding 1024² down to 9/sec regardless of what the interval says.
1939
-
1940
- A fixed millisecond interval cannot do this job: the same `fast` model measures 14 ms at 512², 48 ms
1941
- at 1024² and ~800 ms at 2048². Measured where the GPU is saturated (1536²), the multiplier is the
1942
- whole trade — 1x gives 1.6 refreshes/sec at 62 % of the sample rate, 2x gives 0.8/sec at 89 %, 3x
1943
- gives 0.6/sec at 97 %.
1944
-
1945
- This replaced a sample-growth gate (refresh only once the sample count had grown 1.4x), which was
1946
- written when a denoise cost 100-330 ms. Once the output pack moved to the GPU and a denoise got
1947
- cheap, that gate only cost refreshes: removing it took 512² from 2 to 31 refreshes/sec and 1024²
1948
- from 2 to 8.9, both at an unchanged sample rate, while 1536² and 2048² did not move at all because
1949
- the cost floor already bound there. Refreshing faster is also *smoother*, not shimmerier — less
1950
- changes underneath between refreshes.
1951
-
1952
- Nothing runs while the camera is moving; the raw frame shows during navigation.
1953
-
1954
- Cadence runs are tagged so a host can tell them apart from the denoise that ends a render:
1955
-
1956
- ```js
1957
- engine.addEventListener(EngineEvents.DENOISING_END, e => {
1958
- if (e.continuous) return; // a background refresh, not the final image
1959
- saveResult();
1960
- });
1961
- ```
1962
-
1963
- `'fast'` and `'fast-clean'` are the same network size and cost the same to run; they differ only in
1964
- which auxiliary guide their weights expect. That makes the ordering **not** a simple quality ladder:
1965
- at 1 spp the accumulated guide has one sample, so `'fast-clean'` is fed something it was not trained
1966
- for and measures materially worse than `'fast'` (on a transmission-heavy scene, more than double the
1967
- RMSE). Once the guide converges it wins by a few percent — but `'high'` beats it there anyway. Pick
1968
- `'fast'` for previews and `'high'` for output; `'fast-clean'` is for the narrow case of denoising a
1969
- converged frame on a budget.
1970
-
1971
- Denoise cost scales with frame area, and the tile tracks the frame so that a frame fitting inside one
1972
- tile pays no overlap padding — at 1024x1024 that is roughly 1.8x faster than tiling it. A cap
1973
- (default 1024) bounds the one-time activation allocation, so larger frames tile and stay
1974
- memory-bounded. Raise or lower it with
1975
- `engine.denoisingManager.denoiser.updateConfiguration({ tileSize: 2048 })`; the effective tile is
1976
- `min( max( width, height ), tileSize )`.
1977
-
1978
- > **Note:** The neural network model is downloaded on first use. Subsequent runs use the browser cache. OIDN also works with `configureForMode('production')`, which enables it automatically alongside high-quality render settings.
1979
-
1980
- ### Enabling the AI Upscaler
1981
-
1982
- The upscaler runs ONNX super-resolution models via `onnxruntime-web`. Unlike OIDN, `onnxruntime-web` is lazily fetched from a CDN inside a Web Worker — **no npm install or import map entry is needed**.
1983
-
1984
- ```js
1985
- engine.denoisingManager.setUpscalerEnabled(true);
1986
- engine.denoisingManager.setUpscalerQuality('fast'); // 'fast' | 'balanced' | 'quality'
1987
- engine.denoisingManager.setUpscalerScaleFactor(2); // 2 | 4
1988
-
1989
- engine.addEventListener(EngineEvents.UPSCALING_START, () => console.log('Upscaling started'));
1990
- engine.addEventListener(EngineEvents.UPSCALING_PROGRESS, (e) => console.log('Upscaling', e));
1991
- engine.addEventListener(EngineEvents.UPSCALING_END, () => console.log('Upscaling complete'));
1992
- ```
1993
-
1994
- | Quality | Model | 2× size | 4× size |
1995
- |---|---|---|---|
1996
- | `'fast'` | SPAN | 1.6 MB | 1.6 MB |
1997
- | `'balanced'` | SRVGGNetCompact | 2.4 MB | 4.9 MB |
1998
- | `'quality'` | RRDBNet / MoSR | 67 MB | 16.5 MB |
1999
-
2000
- **Chaining with OIDN:** Upscaling and OIDN **can** run together — on render completion, OIDN runs first, then its denoised output is fed into the upscaler. Enable both; no manual coordination required.
2001
-
2002
- ## Troubleshooting
2003
-
2004
- **OIDN: `Cannot find module './tza'` (webpack)**
2005
- The `oidn-web` package uses dynamic imports that webpack cannot resolve. This does not affect Vite or other ESM-native bundlers. Add `oidn-web` to your webpack externals:
2006
-
2007
- ```js
2008
- // webpack.config.js
2009
- module.exports = {
2010
- externals: {
2011
- 'oidn-web': 'oidn-web'
2012
- }
2013
- };
2014
- ```
2015
-
2016
- Then load it via a script tag or import map instead:
2017
-
2018
- ```html
2019
- <script type="importmap">
2020
- {
2021
- "imports": {
2022
- "oidn-web": "https://cdn.jsdelivr.net/npm/oidn-web@0.4.0/dist/oidn.js"
2023
- }
2024
- }
2025
- </script>
2026
- ```
2027
-
2028
- **OIDN from a CDN**
2029
- Load the self-bundled `/dist/oidn.js` path rather than `/+esm` or `esm.sh` — it is a single pre-bundled ESM with no external imports, and it is the path this engine is tested against.
2030
-
2031
- **Black screen / "WebGPU not supported"**
2032
- Your browser may not support WebGPU. Use Chrome 113+, Edge 113+, Safari 18+, or Firefox 141+. Ensure you're on HTTPS or localhost.
2033
-
2034
- **Models not loading**
2035
- If serving locally, place files in your `public/` folder and reference them with absolute paths (e.g., `/scene.glb`). For remote files, ensure the server allows CORS.
2036
-
2037
- **Workers blocked by Content-Security-Policy**
2038
- Rayzee's Web Workers are embedded in the bundle and spawned from a `blob:` URL, so a strict `worker-src` policy will block them — the symptom is BVH building, texture processing, or HDRI CDF generation silently failing. Allow `blob:`:
2039
-
2040
- ```
2041
- Content-Security-Policy: worker-src 'self' blob:
2042
- ```
2043
-
2044
- Only needed if you set an explicit `worker-src` (or fall back to a restrictive `default-src`). Pages without a CSP are unaffected.
2045
-
2046
102
  ## License
2047
103
 
2048
104
  MIT