rayzee 10.0.0 → 11.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +252 -44
  2. package/dist/ActiveColor-YvuQq44O.js +13 -0
  3. package/dist/ArchiveImporter-BRQvLggb.js +4300 -0
  4. package/dist/AssetConfig-DSEntE65.js +30 -0
  5. package/dist/BidirectionalIntegrator-9lZQxFVH.js +934 -0
  6. package/dist/BufferLayout-D_IBo2ci.js +150 -0
  7. package/dist/ColorManagement-oeEk7oSu.js +1488 -0
  8. package/dist/EngineEvents-Ci8dVWCP.js +73 -0
  9. package/dist/EngineIssues-CIZnXHsF.js +72 -0
  10. package/dist/FileFormats-sPGJeCRc.js +102 -0
  11. package/dist/Logger-dDDGj9Av.js +97 -0
  12. package/dist/{NeuralRetouch-CYgkQY8Z.js → NeuralRetouch-BCXylThH.js} +8 -6
  13. package/dist/{NeuralSuperRes-PkgDLQcD.js → NeuralSuperRes-sZAeS142.js} +55 -120
  14. package/dist/PathTracer-CQPW06Mm.js +29971 -0
  15. package/dist/PhysicalSky-BvqCw_dy.js +1219 -0
  16. package/dist/RayzeeRenderer-COpwotpU.js +3551 -0
  17. package/dist/SolarLimb-CPaeZVwz.js +721 -0
  18. package/dist/ToneMapGPU-CjU3osDd.js +177 -0
  19. package/dist/ViewTransforms-CYN7bq2w.js +255 -0
  20. package/dist/WorkingMatrix-C6y233hT.js +27 -0
  21. package/dist/ZipReader-gDSWr0I5.js +98 -0
  22. package/dist/addons/archives.es.js +3 -0
  23. package/dist/addons/bidirectional.es.js +2 -0
  24. package/dist/addons/color.es.js +3 -0
  25. package/dist/addons/formats.es.js +2 -0
  26. package/dist/addons/physical-sky.es.js +2 -0
  27. package/dist/addons/storage.es.js +3 -0
  28. package/dist/archiveFormats-BZw7TiDp.js +75 -0
  29. package/dist/identity-DrQsGT2o.js +39 -0
  30. package/dist/openStorage-DwcBRUj7.js +820 -0
  31. package/dist/rayzee-core.es.js +12 -0
  32. package/dist/rayzee.es.js +2551 -44024
  33. package/dist/rayzee.umd.js +528 -487
  34. package/dist/sceneCachePolicy-BSSsKY98.js +443 -0
  35. package/dist/shared-BGBcSxBQ.js +17 -0
  36. package/package.json +23 -2
  37. package/src/Color/ActiveColor.js +34 -0
  38. package/src/Color/BasicColor.js +147 -0
  39. package/src/Color/ColorManagement.js +7 -31
  40. package/src/Color/ViewTransforms.js +5 -1
  41. package/src/EngineDefaults.js +62 -1029
  42. package/src/EngineEvents.js +2 -0
  43. package/src/EngineIssues.js +2 -0
  44. package/src/Headless.js +6 -5
  45. package/src/Passes/AIUpscaler.js +58 -141
  46. package/src/Passes/OIDNDenoiser.js +92 -65
  47. package/src/Passes/OIDNTemporalHistory.js +1 -1
  48. package/src/PathTracerApp.js +620 -4026
  49. package/src/Pipeline/PipelineContext.js +0 -4
  50. package/src/Platform.js +36 -0
  51. package/src/Processor/ArchiveCache.js +1 -1
  52. package/src/Processor/ArchiveImporter.js +1259 -0
  53. package/src/Processor/AssetLoader.js +327 -1763
  54. package/src/Processor/AtmosphereModel.js +1 -2
  55. package/src/Processor/BVHBuilder.js +14 -126
  56. package/src/Processor/BVHLeafFold.js +4 -6
  57. package/src/Processor/BVHRefitter.js +67 -46
  58. package/src/Processor/BufferLayout.js +291 -0
  59. package/src/Processor/EmissiveTriangleBuilder.js +1 -1
  60. package/src/Processor/EnvironmentExactTable.js +187 -0
  61. package/src/Processor/EquirectHDRInfo.js +30 -223
  62. package/src/Processor/FileFormats.js +116 -0
  63. package/src/Processor/GLTFDecoders.js +85 -0
  64. package/src/Processor/GLTFDiffuseTransmission.js +63 -0
  65. package/src/Processor/GeometryExtractor.js +15 -5
  66. package/src/Processor/HostMemory.js +7 -1
  67. package/src/Processor/IESParser.js +47 -35
  68. package/src/Processor/InstanceTable.js +1 -1
  69. package/src/Processor/KernelManager.js +35 -2
  70. package/src/Processor/MaterialPacking.js +10 -8
  71. package/src/Processor/PBRT/PBRTMaterials.js +129 -41
  72. package/src/Processor/PBRT/PBRTParser.js +30 -17
  73. package/src/Processor/PBRT/PBRTSceneBuilder.js +548 -37
  74. package/src/Processor/PBRT/PBRTTextureBake.js +154 -0
  75. package/src/Processor/PBRT/PFM.js +59 -0
  76. package/src/Processor/PackedRayBuffer.js +116 -9
  77. package/src/Processor/ParallelBVHBuilder.js +3 -6
  78. package/src/Processor/PhysicalSky.js +5 -4
  79. package/src/Processor/PlatformImageLoader.js +50 -6
  80. package/src/Processor/QueueManager.js +39 -5
  81. package/src/Processor/ReinsertionOptimizer.js +2 -2
  82. package/src/Processor/SceneProcessor.js +53 -62
  83. package/src/Processor/ShaderBuilder.js +6 -16
  84. package/src/Processor/SimpleSky.js +1 -62
  85. package/src/Processor/SolarLimb.js +3 -0
  86. package/src/Processor/SplitBLAS.js +1 -1
  87. package/src/Processor/StorageTexturePool.js +22 -1
  88. package/src/Processor/TLASBuilder.js +1 -3
  89. package/src/Processor/TextureBuckets.js +237 -0
  90. package/src/Processor/TextureCreator.js +181 -8
  91. package/src/Processor/ToneMapGPU.js +64 -15
  92. package/src/Processor/Workers/BVHRefitWorker.js +5 -2
  93. package/src/Processor/Workers/BVHSubtreeWorker.js +1 -34
  94. package/src/Processor/Workers/BVHWorker.js +5 -18
  95. package/src/Processor/Workers/CDFWorker.js +6 -183
  96. package/src/Processor/Workers/TexturesWorker.js +103 -18
  97. package/src/Processor/archiveFormats.js +7 -0
  98. package/src/Processor/utils.js +2 -1
  99. package/src/RayzeeRenderer.js +3899 -0
  100. package/src/RenderSettings.js +68 -62
  101. package/src/Stages/ASVGF.js +13 -14
  102. package/src/Stages/AutoExposure.js +17 -17
  103. package/src/Stages/BilateralFilter.js +2 -1
  104. package/src/Stages/Compositor.js +8 -23
  105. package/src/Stages/DenoiserSettings.js +182 -0
  106. package/src/Stages/EdgeFilter.js +2 -1
  107. package/src/Stages/MotionVector.js +1 -1
  108. package/src/Stages/NRD.js +11 -5
  109. package/src/Stages/NormalDepth.js +12 -8
  110. package/src/Stages/PathTracer.js +338 -43
  111. package/src/Stages/PathTracerStage.js +69 -66
  112. package/src/Stages/Variance.js +1 -1
  113. package/src/Storage/BLASCache.js +1 -1
  114. package/src/Storage/CDFCache.js +18 -17
  115. package/src/Storage/DownloadCache.js +1 -1
  116. package/src/Storage/SceneGraphCodec.js +46 -3
  117. package/src/Storage/SpillStore.js +1 -1
  118. package/src/Storage/StorageManager.js +45 -9
  119. package/src/Storage/areas.js +4 -0
  120. package/src/TSL/BVHTraversal.js +50 -25
  121. package/src/TSL/Bidirectional.js +204 -0
  122. package/src/TSL/BidirectionalLamps.js +132 -0
  123. package/src/TSL/Clearcoat.js +15 -6
  124. package/src/TSL/Common.js +20 -15
  125. package/src/TSL/ConnectKernel.js +147 -0
  126. package/src/TSL/Debugger.js +1 -1
  127. package/src/TSL/Displacement.js +4 -3
  128. package/src/TSL/EmissiveSampling.js +65 -18
  129. package/src/TSL/Environment.js +77 -106
  130. package/src/TSL/EnvironmentCDF.js +85 -39
  131. package/src/TSL/ExtendKernel.js +8 -3
  132. package/src/TSL/FinalWriteKernel.js +4 -4
  133. package/src/TSL/GenerateKernel.js +18 -4
  134. package/src/TSL/HitFacet.js +35 -5
  135. package/src/TSL/LightBVHSampling.js +40 -9
  136. package/src/TSL/LightGenerateKernel.js +293 -0
  137. package/src/TSL/LightGuide.js +198 -0
  138. package/src/TSL/LightSplatKernel.js +165 -0
  139. package/src/TSL/LightVisibility.js +111 -0
  140. package/src/TSL/LightsCore.js +13 -38
  141. package/src/TSL/LightsDirect.js +28 -29
  142. package/src/TSL/LightsIndirect.js +3 -1
  143. package/src/TSL/LightsSampling.js +239 -51
  144. package/src/TSL/MaterialEvaluation.js +113 -31
  145. package/src/TSL/MaterialProperties.js +44 -17
  146. package/src/TSL/MaterialTransmission.js +52 -47
  147. package/src/TSL/MergeKernel.js +218 -0
  148. package/src/TSL/PathTracerCore.js +65 -29
  149. package/src/TSL/Random.js +5 -1
  150. package/src/TSL/SceneResources.js +51 -0
  151. package/src/TSL/ShadeKernel.js +964 -101
  152. package/src/TSL/Struct.js +10 -0
  153. package/src/TSL/Sun.js +1 -1
  154. package/src/TSL/TextureSampling.js +66 -61
  155. package/src/addons/archives.js +14 -0
  156. package/src/addons/bidirectional.js +13 -0
  157. package/src/addons/color.js +32 -0
  158. package/src/addons/formats.js +16 -0
  159. package/src/addons/physicalSky.js +14 -0
  160. package/src/addons/storage.js +19 -0
  161. package/src/core.js +31 -0
  162. package/src/index.js +13 -23
  163. package/src/integrators/BidirectionalIntegrator.js +833 -0
  164. package/src/managers/AnimationManager.js +88 -6
  165. package/src/managers/CameraManager.js +103 -46
  166. package/src/managers/DenoisingManager.js +14 -10
  167. package/src/managers/EnvironmentManager.js +125 -103
  168. package/src/managers/GoboManager.js +9 -0
  169. package/src/managers/MaterialDataManager.js +48 -37
  170. package/src/managers/UniformManager.js +32 -10
  171. package/dist/ToneMapGPU-B6XIe3DU.js +0 -544
  172. package/dist/rolldown-runtime-CAFD8bLK.js +0 -11
  173. package/src/Processor/TreeletOptimizer.js +0 -574
@@ -1,736 +1,93 @@
1
- import { sunPosition, dayOfYearForMonth } from './Processor/SunPosition.js';
2
-
3
- /** The default sun as a time of day (solar hours), month and latitude; north lies along −Z. */
4
- export const DEFAULT_SUN_PATH = Object.freeze( { time: 10, month: 3, latitude: 40 } );
5
-
6
- const DEFAULT_SUN = sunPosition( { hours: DEFAULT_SUN_PATH.time, dayOfYear: dayOfYearForMonth( DEFAULT_SUN_PATH.month ), latitude: DEFAULT_SUN_PATH.latitude } );
7
-
8
- /**
9
- * Engine-owned default values and configuration constants.
10
- * These are used exclusively by the rendering engine (src/core/)
11
- * and should not depend on any UI framework or external modules.
12
- */
13
-
14
- /**
15
- * Product decisions for a real-time viewer, not physical constants. Both are invisible from
16
- * outside and both move output away from a reference renderer. `viewer` is the default.
17
- */
18
- export const RENDER_PROFILES = Object.freeze( {
19
- viewer: Object.freeze( {
20
- areaLightIntensityScale: 0.1, // scales glTF placeholder area-light power (viewer tuning)
21
- environmentRotation: 0.0, // degrees — the HDRI as authored, as Blender shows it
22
- toneMapping: 6, // AgXToneMapping
23
- saturation: 1.0, // no grade
24
- dofMode: 'look', // depth of field set by how blurry it looks, not by a lens — right at any scene scale
25
- } ),
26
- physical: Object.freeze( {
27
- areaLightIntensityScale: 1.0,
28
- environmentRotation: 0.0,
29
- // AgX, matching Blender/Cycles' default view transform. ACES crushes shadows on this
30
- // engine's own corpus (shade 1.48 vs AgX 3.01 from identical radiance).
31
- toneMapping: 6,
32
- saturation: 1.0, // no grade
33
- dofMode: 'physical',
34
- } ),
35
- } );
1
+ /** Default values of the render settings and materials, and the presets built on them. */
36
2
 
37
3
  /**
38
- * @param {string} [name] - a RENDER_PROFILES key
39
- * @returns {{areaLightIntensityScale: number, environmentRotation: number}}
40
- * @throws {Error} on an unknown name — a typo must not silently select viewer tuning
4
+ * The default of every render setting, under the setting's own name: each key is a RenderSettings route
5
+ * and nothing else lives here. A viewer piece keeps its own defaults beside its code.
41
6
  */
42
- export function getRenderProfile( name = 'viewer' ) {
7
+ export const ENGINE_DEFAULTS = deepFreeze( {
43
8
 
44
- const profile = RENDER_PROFILES[ name ];
45
- if ( ! profile ) {
46
-
47
- throw new Error( `unknown render profile "${name}" — expected one of ${Object.keys( RENDER_PROFILES ).join( ', ' )}` );
48
-
49
- }
50
-
51
- return profile;
52
-
53
- }
54
-
55
- export const ENGINE_DEFAULTS = {
56
- // Canvas output
57
- resolution: 512,
58
- canvasWidth: 512,
59
- canvasHeight: 512,
9
+ // Light paths
10
+ maxBounces: 3,
11
+ transmissiveBounces: 5,
12
+ maxSubsurfaceSteps: 8, // a low cap on the subsurface random walk; the final tier raises it
13
+ maxTransparentBounces: 32, // alpha skips are free bounces; uncapped, cutout foliage eats the loop
14
+ integrator: 'path', // 'path' | 'bidirectional' | 'vcm'
15
+ shadowRays: 'two', // 'all' | 'two' | 'one' — shadow rays a hit (path integrator)
16
+ samplingTechnique: 2, // 0 PCG, 1 Halton, 2 Sobol
17
+ enableEmissiveTriangleSampling: false,
18
+ emissiveBoost: 1.0,
19
+ enableAlphaShadows: false,
20
+ fireflyThreshold: 3.0,
21
+ shadowTerminatorOffset: 0.1, // Cycles' Geometry Offset at Blender's default; 0 turns it off
22
+ globalIlluminationIntensity: 1,
23
+ wavefrontSortMaterials: true, // sort rays by material before Shade (above 8 materials); read when kernels build
60
24
 
61
- // Max material-texture dimension (longest edge) used when processing a scene's
62
- // textures into GPU arrays. Larger = sharper textures but ~quadratic VRAM. Clamped
63
- // to TEXTURE_CONSTANTS.MAX_TEXTURE_SIZE (hardware ceiling). Applied at scene load.
64
- maxTextureSize: 4096,
25
+ // Sampling and convergence
26
+ maxSamples: 60,
27
+ renderLimitMode: 'frames',
28
+ renderTimeLimit: 30,
29
+ renderMode: 0, // 0 preview, 1 production; configureForMode() sets it
30
+ useAdaptiveSampling: true,
31
+ noiseThreshold: 0.1, // a pixel under this √-luminance noise has converged; the interactive tier's value
32
+ adaptiveMinSamples: 8,
33
+ adaptiveStopFraction: 0.90, // a calibration, not a dial: the share of converged pixels that retires a frame
34
+ usePixelFreeze: true,
35
+ // Stricter than noiseThreshold on purpose: it tests plain relative error, and a pixel frozen before it
36
+ // passes the frame's own test never will.
37
+ pixelFreezeThreshold: 0.02,
38
+ pixelFreezeStability: 8, // candidate frames in a row before a pixel freezes
39
+ convergenceOverlay: false, // display only; never alters the render
40
+ interactionModeEnabled: true,
65
41
 
66
- toneMapping: RENDER_PROFILES.viewer.toneMapping,
42
+ // Output
67
43
  exposure: 1,
68
- saturation: RENDER_PROFILES.viewer.saturation,
69
- enableEnvironment: true,
70
- showBackground: true,
44
+ saturation: 1.0, // no grade
71
45
  transparentBackground: false,
46
+ backgroundColor: '#000000', // what a camera miss shows when the backdrop is a solid colour
47
+
48
+ // Environment and background
49
+ enableEnvironment: true,
72
50
  environmentIntensity: 1,
51
+ environmentRotation: 0.0, // degrees; 0 is the HDRI as authored, as Blender shows it
52
+ showBackground: true,
73
53
  backgroundIntensity: 1,
74
- // Solid backdrop color shown on camera-ray misses in 'color' background mode
75
- // (showBackground=false, transparentBackground=false). Black = legacy hidden-backdrop look.
76
- backgroundColor: '#000000',
77
- // Backdrop blur (env background only). 0 = sharp/off (no cost). Cone-jitter blur of the
78
- // primary-ray env lookup; lighting/reflections stay sharp. Samples = taps/frame (noise vs cost).
79
- backgroundBlurriness: 0,
54
+ backgroundBlurriness: 0, // blurs the backdrop only; lighting and reflections stay sharp
80
55
  backgroundBlurSamples: 8,
81
- environmentRotation: RENDER_PROFILES.viewer.environmentRotation,
82
56
  groundProjectionEnabled: false,
83
57
  groundProjectionRadius: 100,
84
58
  groundProjectionHeight: 15,
85
- // World Y of the projected ground plane; auto-seeded to the scene floor (min-Y) on model
86
- // load so models that aren't authored at y=0 sit ON the ground instead of sinking into it.
87
- groundProjectionLevel: 0,
88
- // Analytic ground-plane shadow catcher (primary-ray holdout; no geometry)
59
+ groundProjectionLevel: 0, // world Y of the projected ground; seeded to the scene's floor on load
89
60
  enableGroundCatcher: false,
90
61
  groundCatcherHeight: 0,
91
- globalIlluminationIntensity: 1,
92
62
 
93
- // Environment Mode System
94
- environmentMode: 'hdri', // 'hdri' | 'procedural' | 'gradient' | 'color'
95
-
96
- // Gradient Sky Colors
97
- gradientZenithColor: '#0077BE',
98
- gradientHorizonColor: '#87CEEB',
99
- gradientGroundColor: '#654321',
100
-
101
- // Solid Color Sky
102
- solidSkyColor: '#87CEEB',
103
-
104
- // Physical sky (Processor/PhysicalSky.js) — the Clear Day preset
105
- skySunAzimuth: 180 - DEFAULT_SUN.azimuth,
106
- skySunElevation: DEFAULT_SUN.elevation,
107
- skySunStrength: 1, // artistic multiplier on the disc and its light; 1 is physical
108
- skySunSize: 0.53, // angular diameter, degrees
109
- skyTurbidity: 2, // 1 is aerosol-free air, ~2 a clear day, 6+ hazy
110
- skyOzone: 300, // Dobson units
111
- skyAirDensity: 1,
112
- skyGroundAlbedo: '#959595', // 0.3 linear
113
- skyAltitude: 50, // metres
114
- skyPreset: 'clearDay',
115
-
116
- // Camera projection — 'perspective' | 'orthographic' | 'equirectangular'. Panorama ranges are UI-facing degrees.
117
- cameraProjection: 'perspective',
118
- panoramaLonRange: [ - 180, 180 ],
63
+ // Camera
64
+ cameraProjection: 'perspective', // 'perspective' | 'orthographic' | 'equirectangular'
65
+ panoramaLonRange: [ - 180, 180 ], // degrees
119
66
  panoramaLatRange: [ - 90, 90 ],
120
67
  panoramaLevelHorizon: true,
121
-
122
68
  enableDOF: false,
123
- fov: 55,
69
+ dofMode: 'look', // 'look': set by how blurry it looks, right at any scene scale; 'physical': a real lens
70
+ dofBlur: 0.05, // look mode: a far background's blur, as a fraction of the image height
124
71
  focusDistance: 0.8,
125
72
  aperture: 5.6,
126
73
  focalLength: 50,
127
74
  apertureScale: 1.0,
128
75
  anamorphicRatio: 1.0,
129
- unitsPerMetre: 1, // scene units per real metre; the lens is specified in mm
130
- dofMode: RENDER_PROFILES.viewer.dofMode, // 'look' | 'physical'
131
- dofBlur: 0.05, // look mode: a far background's blur, as a fraction of the image height
76
+ unitsPerMetre: 1, // scene units per real metre; the lens is in mm
132
77
 
133
- // Auto-focus
134
- autoFocusMode: 'auto', // 'manual' | 'auto'
135
- afScreenPoint: { x: 0.5, y: 0.5 },
136
- afSmoothingFactor: 0.15,
78
+ // Read when a model loads
79
+ maxTextureSize: 4096, // longest edge of a material texture; clamped to the hardware ceiling
80
+ areaLightIntensityScale: 0.1, // power of a glTF model's placeholder area lights (RectAreaLight extras)
137
81
 
138
- enablePathTracer: true,
139
- enableAccumulation: true,
140
- pauseRendering: false,
141
- maxSamples: 60,
142
- bounces: 3,
143
- transmissiveBounces: 5,
144
- maxSubsurfaceSteps: 8, // interactive default: low cap (bounded random-walk SSS)
145
-
146
- maxTransparentBounces: 32, // guard: alpha skips are free bounces, else cutout foliage eats the loop budget
147
-
148
- // Adaptive sampling (Blender-style): stop the frame once enough pixels drop below the noise threshold.
149
- useAdaptiveSampling: true,
150
- // √-luminance-normalized per-pixel noise below which a pixel counts as converged. Base tracks the
151
- // interactive tier — that is what a freshly booted engine renders before configureForMode runs.
152
- noiseThreshold: 0.1,
153
- adaptiveMinSamples: 8, // min samples before adaptive sampling can trigger
154
- // Fraction of pixels that must pass the 3×3-eroded convergence count before the frame retires; the
155
- // geometry-only fraction has to clear the same bar (PathTracer._isConvergedComplete).
156
- // ENGINE-INTERNAL: a calibration constant rather than a quality dial — it only means anything against
157
- // those two counts. configureForMode supplies the per-tier value.
158
- adaptiveStopFraction: 0.90,
159
- // Per-pixel freeze: skip tracing pixels that individually converged (noise threshold only — no dark floor,
160
- // which would bake dim regions too dark). Naturally engages only on static/idle views.
161
- // Set together with useAdaptiveSampling by the single UI switch — two tiers of one feature.
162
- usePixelFreeze: true,
163
- // ENGINE-INTERNAL, and not the sibling of noiseThreshold it looks like: freeze bars on plain relErr where
164
- // the frame test uses the √-normalized error, so the same number is 2-5× stricter here. Deriving it from
165
- // noiseThreshold was measured and rejected — a pixel frozen before it satisfies the frame test can never
166
- // satisfy it afterwards, so a looser bar delays the early stop rather than hastening it.
167
- pixelFreezeThreshold: 0.02,
168
- pixelFreezeStability: 8, // ENGINE-INTERNAL: consecutive candidate frames before a pixel freezes
169
- convergenceOverlay: false, // display-only Compositor overlay; never alters the render
170
-
171
- samplingTechnique: 2,
172
- enableEmissiveTriangleSampling: false,
173
- emissiveBoost: 1.0,
174
-
175
- fireflyThreshold: 3.0,
176
- // Cycles' Shadow Terminator → Geometry Offset, and its default: light shadow rays leave a
177
- // smooth-shaded triangle from the smooth surface near the terminator. 0 disables.
178
- shadowTerminatorOffset: 0.1,
179
- // Wavefront material-coherence sort: global counting-sort of entering rays by material before
180
- // Shade (material-pure workgroups), under dynamic dispatch. Measured −8% at 1024²/8b. Gated on
181
- // material count > 8; the histogram bin count is sized per-scene to the material count.
182
- wavefrontSortMaterials: true,
183
- renderLimitMode: 'frames',
184
- renderTimeLimit: 30,
185
- renderMode: 0,
186
- enableAlphaShadows: false,
187
- tilesHelper: true, // show OIDN denoise / AI upscale tile progress overlay
188
- showLightHelper: false,
189
-
190
- directionalLightIntensity: 0,
191
- directionalLightColor: "#ffffff",
192
- directionalLightPosition: [ 1, 1, 1 ],
193
- directionalLightAngle: 0.0,
194
-
195
- // EdgeAware denoiser (spatial-only SVGF à-trous). filterStrength: final blend
196
- // (0 = raw, 1 = filtered). edgeAtrousIterations: à-trous passes (step 1,2,4,8,16).
197
- // edgePhiLuminance: variance-scaled luminance edge-stop. edgePhiNormal: normal cone
198
- // exponent. edgePhiDepth: RELATIVE depth tolerance (fraction of ray distance).
199
- filterStrength: 1.0,
200
- edgeAtrousIterations: 5,
201
- edgePhiLuminance: 4.0,
202
- edgePhiNormal: 64.0,
203
- edgePhiDepth: 0.1,
204
-
205
- enableOIDN: false,
206
- oidnQuality: 'fast',
207
- // OIDN as the live-view denoiser, refreshing the accumulating image. Set by choosing 'oidn' in
208
- // the real-time denoiser list, so that only ever one thing denoises the live view — not by
209
- // `enableOIDN`, which is the separate question of whether the finished image gets a pass.
210
- continuousDenoise: false,
211
- // Lower bound on the gap between cadence denoises. DenoisingManager also floors that gap at a
212
- // multiple of what the last denoise actually cost, and above ~1024² that is what binds — this
213
- // value only governs where a denoise is cheap. 8 ms measured 31 refreshes/sec at 512² at an
214
- // unchanged sample rate; 50 ms measured 18/sec for nothing in return.
215
- continuousDenoiseInterval: 8,
216
- // While the view moves, feed OIDN the reprojected history of restarted frames, not one sample.
217
- oidnTemporalHistory: true,
218
-
219
- enableUpscaler: false,
220
- upscalerScale: 2,
221
- upscalerQuality: 'fast',
222
- upscalerHdr: true,
223
-
224
- debugMode: 0,
225
- debugThreshold: 100,
226
- debugModel: 0,
227
-
228
- enableBloom: false,
229
- bloomStrength: 0.2,
230
- bloomRadius: 0.15,
231
- bloomThreshold: 0.85,
232
- interactionModeEnabled: true,
233
- // Per-axis render scale while the camera moves (0.5 = a quarter of the pixels); 1 turns it off.
234
- interactionRenderScale: 0.5,
82
+ // Debug
83
+ visMode: 0, // a debug view (TSL/Debugger.js); 0 renders normally
235
84
  debugVisScale: 100,
236
85
 
237
- // Denoising strategy
238
- denoiserStrategy: 'none',
239
-
240
- enableASVGF: false,
241
- asvgfTemporalAlpha: 0.1,
242
- asvgfAtrousIterations: 8,
243
- asvgfPhiColor: 10.0,
244
- asvgfPhiNormal: 128.0,
245
- asvgfPhiDepth: 1.0,
246
- asvgfVarianceBoost: 1.0,
247
- asvgfMaxAccumFrames: 32,
248
- // Must be > 0: the gradient also rejects fireflies before the EMA smears them across
249
- // ~1/alpha frames. At 0 the denoiser measures ~3x worse than no denoiser at 1 spp.
250
- asvgfGradientStrength: 1.0,
251
- asvgfGradientSigmaScale: 2.0,
252
- asvgfGradientNoiseFloor: 0.0,
253
- asvgfDebugMode: 0,
254
- asvgfQualityPreset: 'medium',
255
- showAsvgfHeatmap: false,
256
-
257
- // NRD (ReBLUR port) real-time denoiser — see NRD_DEFAULTS / NRD_QUALITY_PRESETS.
258
- nrdQualityPreset: 'medium',
259
- nrdDebugMode: 0,
260
-
261
- // Auto-exposure settings
262
- autoExposure: false,
263
- autoExposureKeyValue: 0.18,
264
- autoExposureMinExposure: 0.1,
265
- autoExposureMaxExposure: 20.0,
266
- autoExposureAdaptSpeedBright: 3.0,
267
- autoExposureAdaptSpeedDark: 0.5,
268
- };
86
+ } );
269
87
 
270
88
  /** The `cameraProjection` uniform's value for each setting value. */
271
89
  export const CAMERA_PROJECTION_IDS = Object.freeze( { perspective: 0, equirectangular: 1, orthographic: 2 } );
272
90
 
273
- // Ray distance NormalDepth writes on a miss. Max finite half-float, so miss−miss diffs stay 0
274
- // rather than Inf−Inf=NaN.
275
- export const GBUFFER_MISS_DEPTH = 65504.0;
276
- export const GBUFFER_MISS_THRESHOLD = 6e4;
277
-
278
- // normHitDist = hitDist / (A + B·viewZ). A constant, not a uniform: the Shade write and the NRD
279
- // decode must agree, and the normalization is a pure round trip.
280
- export const NRD_HIT_DIST_A = 3.0;
281
- export const NRD_HIT_DIST_B = 0.1;
282
-
283
- // Albedo demodulation safety floor. ASVGF and BilateralFilter MUST use the
284
- // same value — demod (`color / safeAlbedo`) and remod (`lighting * safeAlbedo`)
285
- // only round-trip exactly when both sides agree.
286
- export const ALBEDO_EPS = 0.01;
287
-
288
- // Hard ceiling the engine supports for the reserved (pre-allocated) render size. 4K (3840×2160)
289
- // fits within 4096². Raising the reserved size to this pins ~1.5 GB of MRT textures — opt-in only.
290
- export const MAX_RESERVABLE_RENDER_SIZE = 4096;
291
-
292
- // Reserved render size: every per-resolution compute StorageTexture + aux buffer is pre-allocated at
293
- // this SQUARE size and never resized at runtime (works around three.js StorageTexture-resize bugs —
294
- // see TSL/patches history). Render resolution must not exceed it; the engine warns + ignores larger.
295
- // It is a LIVE binding (mutable) so it can be raised (e.g. for 4K) via setReservedRenderSize(); consumers
296
- // read it inside their constructors, and stages already built at a lower value are re-initialised in place
297
- // by PathTracerApp.setReservedRenderResolution() — which is the device-gated API hosts should call, at any
298
- // point in the lifecycle. Default 2048 (zero VRAM regression). See docs/internal/specs/wavefront-chunked-pool.md.
299
- export let MAX_STORAGE_TEXTURE_SIZE = 2048;
300
-
301
- // Set the reserved render size. MUST be called before pipeline construction (stages pre-allocate at
302
- // this value and cannot resize). Clamped to [256, MAX_RESERVABLE_RENDER_SIZE]. Returns the applied value.
303
- export function setReservedRenderSize( px ) {
304
-
305
- MAX_STORAGE_TEXTURE_SIZE = Math.max( 256, Math.min( MAX_RESERVABLE_RENDER_SIZE, Math.floor( px ) || 256 ) );
306
- return MAX_STORAGE_TEXTURE_SIZE;
307
-
308
- }
309
-
310
- export const ASVGF_QUALITY_PRESETS = {
311
- // phiColor / phiDepth are RELATIVE tolerances (fractions). Bigger = more
312
- // permissive. The adaptive temporal gradient (gradientStrength > 0) is always
313
- // on: it measures real change in units of noise σ (gradientSigmaScale), so a
314
- // static scene reads ~0 (no convergence penalty) and only moving lights / anim
315
- // / disocclusion drop history. See ASVGF._buildGradientCompute.
316
- low: {
317
- temporalAlpha: 0.1,
318
- gradientStrength: 0.8,
319
- gradientSigmaScale: 2.5,
320
- gradientNoiseFloor: 0.05,
321
- atrousIterations: 3,
322
- phiColor: 1.0,
323
- phiNormal: 64.0,
324
- phiDepth: 0.1,
325
- phiLuminance: 6.0,
326
- maxAccumFrames: 16,
327
- varianceBoost: 0.5
328
- },
329
- medium: {
330
- temporalAlpha: 0.03,
331
- gradientStrength: 1.0,
332
- gradientSigmaScale: 2.5,
333
- gradientNoiseFloor: 0.05,
334
- atrousIterations: 4,
335
- phiColor: 0.5,
336
- phiNormal: 128.0,
337
- phiDepth: 0.05,
338
- phiLuminance: 4.0,
339
- maxAccumFrames: 64,
340
- varianceBoost: 1.0
341
- },
342
- high: {
343
- temporalAlpha: 0.0,
344
- gradientStrength: 1.0,
345
- gradientSigmaScale: 2.5,
346
- gradientNoiseFloor: 0.05,
347
- atrousIterations: 6,
348
- phiColor: 0.3,
349
- phiNormal: 256.0,
350
- phiDepth: 0.02,
351
- phiLuminance: 2.0,
352
- maxAccumFrames: 128,
353
- varianceBoost: 1.5
354
- }
355
- };
356
-
357
- // NRD ReBLUR port (Stages/NRD.js). Names follow nrd::ReblurSettings so NVIDIA's tuning notes apply.
358
- export const NRD_DEFAULTS = {
359
- maxAccumulatedFrameNum: 30,
360
- maxFastAccumulatedFrameNum: 6,
361
- maxStabilizedFrameNum: 63,
362
- historyFixFrameNum: 3,
363
- historyFixBasePixelStride: 14,
364
- prepassBlurRadius: 30,
365
- minBlurRadius: 1,
366
- maxBlurRadius: 30,
367
- lobeAngleFraction: 0.15,
368
- roughnessFraction: 0.15,
369
- planeDistanceSensitivity: 0.02,
370
- minHitDistanceWeight: 0.1,
371
- fastHistoryClampingSigmaScale: 2.0,
372
- fireflySuppressorMinRelativeScale: 2.0,
373
- enableAntiFirefly: true,
374
- antilagLuminanceSigmaScale: 2.0,
375
- antilagLuminanceSensitivity: 3.0,
376
- disocclusionThreshold: 0.01,
377
- convergenceS: 1.0,
378
- convergenceB: 0.2,
379
- convergenceP: 0.8,
380
- // Share of the lobe the normal weight accepts before any history exists. NRD's own constant is
381
- // 0.75, which its source flags as probably too much; at that width it smears curved surfaces.
382
- lobeVolumePercent: 0.1,
383
- // Progressive handover: input sample count at which the denoiser passes the render through untouched.
384
- // 0 = 2 · maxAccumulatedFrameNum.
385
- handoverFrames: 0,
386
- };
387
-
388
- // Keys a preset may override. Applying one resets every key to its default first, so a preset only
389
- // states its deltas and `medium` can be empty.
390
- export const NRD_PRESET_KEYS = [
391
- 'maxAccumulatedFrameNum', 'maxFastAccumulatedFrameNum', 'maxStabilizedFrameNum',
392
- 'historyFixFrameNum', 'prepassBlurRadius', 'maxBlurRadius', 'enableAntiFirefly',
393
- ];
394
-
395
- export const NRD_QUALITY_PRESETS = {
396
- // Short history + no pre-pass: most responsive, noisiest.
397
- low: {
398
- maxAccumulatedFrameNum: 16,
399
- maxFastAccumulatedFrameNum: 4,
400
- maxStabilizedFrameNum: 16,
401
- historyFixFrameNum: 2,
402
- prepassBlurRadius: 0,
403
- maxBlurRadius: 20,
404
- enableAntiFirefly: false,
405
- },
406
- // nrd::ReblurSettings defaults.
407
- medium: {},
408
- // Longer history and wider kernels: smoother, more lag on lighting change.
409
- high: {
410
- maxAccumulatedFrameNum: 45,
411
- maxFastAccumulatedFrameNum: 8,
412
- historyFixFrameNum: 4,
413
- prepassBlurRadius: 40,
414
- maxBlurRadius: 40,
415
- },
416
- };
417
-
418
- export const CAMERA_RANGES = {
419
- fov: {
420
- min: 10,
421
- max: 90,
422
- default: ENGINE_DEFAULTS.fov
423
- },
424
- focusDistance: {
425
- min: 0.3,
426
- max: 100.0,
427
- default: ENGINE_DEFAULTS.focusDistance
428
- },
429
- aperture: {
430
- options: [ 1.4, 2.0, 2.8, 4.0, 5.6, 8.0, 11.0, 16.0 ],
431
- default: ENGINE_DEFAULTS.aperture
432
- },
433
- focalLength: {
434
- min: 0,
435
- max: 200,
436
- default: ENGINE_DEFAULTS.focalLength
437
- }
438
- };
439
-
440
- // `exposure` (EV) makes up about two thirds of the light a lower sun loses, as a photographer
441
- // would: a sunset still reads darker than noon.
442
- export const SKY_PRESETS = {
443
- clearDay: { name: 'Clear Day', sunAzimuth: 180 - DEFAULT_SUN.azimuth, sunElevation: DEFAULT_SUN.elevation, turbidity: 2, exposure: 0 },
444
- clearMorning: { name: 'Clear Morning', sunAzimuth: 90, sunElevation: 15, turbidity: 2, exposure: 1.4 },
445
- clearNoon: { name: 'Clear Noon', sunAzimuth: 180, sunElevation: 70, turbidity: 2, exposure: 0 },
446
- hazyAfternoon: { name: 'Hazy Afternoon', sunAzimuth: 225, sunElevation: 35, turbidity: 4.5, exposure: 0.4 },
447
- goldenHour: { name: 'Golden Hour', sunAzimuth: 270, sunElevation: 6, turbidity: 2.5, exposure: 2.7 },
448
- sunset: { name: 'Sunset', sunAzimuth: 270, sunElevation: 1, turbidity: 3, exposure: 4.2 },
449
- blueHour: { name: 'Blue Hour', sunAzimuth: 270, sunElevation: - 4, turbidity: 2, exposure: 7 },
450
- mountain: { name: 'Mountain Air', sunAzimuth: 135, sunElevation: 40, turbidity: 1.3, altitude: 3000, exposure: 0 },
451
- };
452
-
453
- export const CAMERA_PRESETS = {
454
- portrait: {
455
- name: "Portrait",
456
- description: "Shallow depth of field, background blur",
457
- dofBlur: 0.10,
458
- aperture: 1.4,
459
- focalLength: 135,
460
- apertureScale: 1.0
461
- },
462
- landscape: {
463
- name: "Landscape",
464
- description: "Maximum depth of field, everything in focus",
465
- dofBlur: 0.001,
466
- aperture: 16.0,
467
- focalLength: 24,
468
- apertureScale: 0.5
469
- },
470
- macro: {
471
- name: "Macro",
472
- description: "Very thin focus plane for close-ups",
473
- dofBlur: 0.14,
474
- aperture: 1.4,
475
- focalLength: 100,
476
- apertureScale: 2.0
477
- },
478
- product: {
479
- name: "Product",
480
- description: "Sharp detail with subtle background separation",
481
- dofBlur: 0.013,
482
- aperture: 2.8,
483
- focalLength: 85,
484
- apertureScale: 0.4
485
- },
486
- architectural: {
487
- name: "Architectural",
488
- description: "Deep focus for rooms and buildings",
489
- dofBlur: 0.001,
490
- aperture: 11.0,
491
- focalLength: 16,
492
- apertureScale: 0.5
493
- },
494
- cinematic: {
495
- name: "Cinematic",
496
- description: "Dramatic depth separation with anamorphic bokeh",
497
- dofBlur: 0.13,
498
- aperture: 1.4,
499
- focalLength: 200,
500
- apertureScale: 1.0,
501
- anamorphicRatio: 1.5
502
- }
503
- };
504
-
505
- export const AUTO_FOCUS_MODES = {
506
- MANUAL: 'manual',
507
- AUTO: 'auto',
508
- };
509
-
510
- export const AF_DEFAULTS = {
511
- SMOOTHING_FACTOR: 0.15,
512
- RESET_THRESHOLD: 0.05,
513
- FALLBACK_DISTANCE: 10.0,
514
- SNAP_THRESHOLD: 0.5,
515
- };
516
-
517
- /**
518
- * Triangle record: 5 uvec4 lanes (80 bytes). The buffer is declared `uvec4` so packed lanes
519
- * keep their exact bit pattern — a packSnorm2x16 result can land in the f32 NaN range, and an
520
- * f32 lane may canonicalise it (the same reason the G-buffer is uvec4). Positions and UVs are
521
- * written as f32 through a Float32Array view of the same memory and read back with
522
- * `uintBitsToFloat`, so they carry full precision; only normals are compressed.
523
- *
524
- * Was 32 floats (128 B) with 4 dead padding lanes. At 128 B the 2 GB V8 ArrayBuffer cap put a
525
- * hard ceiling of 16.7M triangles on the scene — 80 B lifts that to 26.8M and cuts geometry
526
- * VRAM by the same 37.5%. Each normal now rides in its position's spare .w lane, so the three
527
- * vec4 loads the intersection test already does carry the normals with them.
528
- */
529
- export const TRIANGLE_DATA_LAYOUT = {
530
- FLOATS_PER_TRIANGLE: 20,
531
-
532
- POSITION_A_OFFSET: 0, // f32 xyz + packed normal A
533
- POSITION_B_OFFSET: 4,
534
- POSITION_C_OFFSET: 8,
535
-
536
- NORMAL_A_PACKED_OFFSET: 3, // oct16 in each position's .w lane
537
- NORMAL_B_PACKED_OFFSET: 7,
538
- NORMAL_C_PACKED_OFFSET: 11,
539
-
540
- UV_AB_OFFSET: 12, // f32 uvA.xy, uvB.xy
541
- UV_C_OFFSET: 16, // f32 uvC.xy
542
- MATERIAL_FLAGS_OFFSET: 18, // materialIndex | side << 24 | shadowBlockerBits << 26 (two bits)
543
- MESH_INDEX_OFFSET: 19
544
- };
545
-
546
- export const TRI_MATERIAL_MASK = 0xffffff;
547
- export const TRI_SIDE_SHIFT = 24; // 0 front, 1 back, 2 double, 3 cutaway
548
- // Front only for every ray, shadow rays too: a cut-away wall or roof. Set by `material.userData.cutaway`
549
- // at extraction, or `app.setMaterialsSide( indices, MATERIAL_SIDE_CUTAWAY )` on a built scene.
550
- export const MATERIAL_SIDE_CUTAWAY = 3;
551
- export const TRI_BLOCKER_SHIFT = 26; // 1 = blocks shadow rays whatever the settings
552
- export const TRI_BLOCKER_ALPHA_SHIFT = 27; // 1 = blocks them unless alpha-cutout shadows are on
553
-
554
- /**
555
- * How a shadow ray settles on this material without fetching it, mirroring traceShadowRay:
556
- * bit 0 set = always a blocker; bit 1 set = a blocker while alpha-cutout shadows are off
557
- * (MASK/BLEND with nothing else letting light through); 0 = light may pass, so the shadow
558
- * traversal has to find the nearest such surface.
559
- */
560
- export function shadowBlockerBits( material ) {
561
-
562
- if ( ! material ) return 0;
563
- const solid = ( material.transmission || 0 ) === 0
564
- && ( ( material.transparent | 0 ) === 0 || ( material.opacity ?? 1 ) >= 1 );
565
- if ( ! solid ) return 0;
566
- return ( material.alphaMode | 0 ) === 0 ? 1 : 2;
567
-
568
- }
569
-
570
- /**
571
- * Material index plus the per-triangle flags the shader reads without touching the
572
- * material buffer: `side` for inline culling and the two shadow-blocker bits.
573
- */
574
- export function packTriangleFlags( materialIndex, material ) {
575
-
576
- return ( ( materialIndex & TRI_MATERIAL_MASK )
577
- | ( ( material?.side ?? 0 ) << TRI_SIDE_SHIFT )
578
- | ( shadowBlockerBits( material ) << TRI_BLOCKER_SHIFT ) ) >>> 0;
579
-
580
- }
581
-
582
- /**
583
- * Octahedral-encode a unit normal into one u32 (two snorm16). Worst-case error is ~0.03°,
584
- * well under what normal maps and barycentric interpolation already contribute.
585
- * A degenerate (zero-length) normal encodes as +Z rather than NaN.
586
- */
587
- export function packNormalOct( x, y, z ) {
588
-
589
- const len = Math.sqrt( x * x + y * y + z * z );
590
- if ( len > 0 ) {
591
-
592
- x /= len; y /= len; z /= len;
593
-
594
- } else {
595
-
596
- x = 0; y = 0; z = 1;
597
-
598
- }
599
-
600
- const sum = Math.abs( x ) + Math.abs( y ) + Math.abs( z );
601
- let u = x / sum, v = y / sum;
602
- if ( z < 0 ) {
603
-
604
- const au = u, av = v;
605
- u = ( 1 - Math.abs( av ) ) * ( au >= 0 ? 1 : - 1 );
606
- v = ( 1 - Math.abs( au ) ) * ( av >= 0 ? 1 : - 1 );
607
-
608
- }
609
-
610
- const qu = Math.round( Math.min( 1, Math.max( - 1, u ) ) * 32767 ) & 0xffff;
611
- const qv = Math.round( Math.min( 1, Math.max( - 1, v ) ) * 32767 ) & 0xffff;
612
- return ( ( qv << 16 ) | qu ) >>> 0;
613
-
614
- }
615
-
616
- /** Inverse of packNormalOct; writes into `out` (length >= 3) and returns it. */
617
- export function unpackNormalOct( packed, out ) {
618
-
619
- const u = ( ( packed << 16 ) >> 16 ) / 32767;
620
- const v = ( packed >> 16 ) / 32767;
621
- let x = u, y = v, z = 1 - Math.abs( u ) - Math.abs( v );
622
- if ( z < 0 ) {
623
-
624
- const ax = x, ay = y;
625
- x = ( 1 - Math.abs( ay ) ) * ( ax >= 0 ? 1 : - 1 );
626
- y = ( 1 - Math.abs( ax ) ) * ( ay >= 0 ? 1 : - 1 );
627
-
628
- }
629
-
630
- const len = Math.sqrt( x * x + y * y + z * z ) || 1;
631
- out[ 0 ] = x / len; out[ 1 ] = y / len; out[ 2 ] = z / len;
632
- return out;
633
-
634
- }
635
-
636
- // Material data layout constants — single source of truth for material buffer offsets.
637
- // Shared between CPU writers (TextureCreator, MaterialDataManager) and GPU readers (Common.js getMaterial).
638
- export const MATERIAL_DATA_LAYOUT = {
639
-
640
- SLOTS_PER_MATERIAL: 33, // vec4 slots per material
641
- FLOATS_PER_MATERIAL: 132, // total floats per material (33 × 4)
642
-
643
- // ── Flat float offsets (CPU side) ────────────────────────────────
644
- // Used as: data[ materialIndex * FLOATS_PER_MATERIAL + offset ]
645
- // Ordered for cache-line coherence: shadow/culling → BxDF core → maps → extended → transforms
646
-
647
- // Slot 0: ior + transmission + thickness + emissiveIntensity [shadow]
648
- IOR: 0, TRANSMISSION: 1, THICKNESS: 2, EMISSIVE_INTENSITY: 3,
649
- // Slot 1: attenuationColor.rgb + attenuationDistance [shadow]
650
- ATTENUATION_COLOR: 4, ATTENUATION_DISTANCE: 7,
651
- // Slot 2: opacity + side + transparent + alphaTest [shadow + culling]
652
- OPACITY: 8, SIDE: 9, TRANSPARENT: 10, ALPHA_TEST: 11,
653
- // Slot 3: alphaMode + depthWrite + normalScale [shadow]
654
- ALPHA_MODE: 12, DEPTH_WRITE: 13, NORMAL_SCALE: 14,
655
- // Slot 4: color.rgb + metalness [BxDF core]
656
- COLOR: 16, METALNESS: 19,
657
- // Slot 5: emissive.rgb + roughness [BxDF core]
658
- EMISSIVE: 20, ROUGHNESS: 23,
659
- // Slot 6: map indices (albedo, normal, roughness, metalness) [maps]
660
- ALBEDO_MAP_INDEX: 24, NORMAL_MAP_INDEX: 25, ROUGHNESS_MAP_INDEX: 26, METALNESS_MAP_INDEX: 27,
661
- // Slot 7: map indices (emissive, bump) + clearcoat [maps]
662
- EMISSIVE_MAP_INDEX: 28, BUMP_MAP_INDEX: 29, CLEARCOAT: 30, CLEARCOAT_ROUGHNESS: 31,
663
- // Slot 8: dispersion + visible + sheen + sheenRoughness [extended BxDF]
664
- DISPERSION: 32, VISIBLE: 33, SHEEN: 34, SHEEN_ROUGHNESS: 35,
665
- // Slot 9: sheenColor.rgb + (reserved) [extended BxDF]
666
- SHEEN_COLOR: 36,
667
- // Slot 10: specularIntensity + specularColor.rgb [extended BxDF]
668
- SPECULAR_INTENSITY: 40, SPECULAR_COLOR: 41,
669
- // Slot 11: iridescence + iridescenceIOR + iridescenceThicknessRange [extended BxDF]
670
- IRIDESCENCE: 44, IRIDESCENCE_IOR: 45, IRIDESCENCE_THICKNESS_RANGE: 46,
671
- // Slot 12: bumpScale + displacementScale + displacementMapIndex + (padding)
672
- BUMP_SCALE: 48, DISPLACEMENT_SCALE: 49, DISPLACEMENT_MAP_INDEX: 50,
673
-
674
- // ── Transform float offsets (8 floats each: 7 matrix values + 1 padding) ──
675
- ALBEDO_TRANSFORM: 52,
676
- NORMAL_TRANSFORM: 60,
677
- ROUGHNESS_TRANSFORM: 68,
678
- METALNESS_TRANSFORM: 76,
679
- EMISSIVE_TRANSFORM: 84,
680
- BUMP_TRANSFORM: 92,
681
- DISPLACEMENT_TRANSFORM: 100,
682
-
683
- // ── Subsurface scattering (3 slots appended after transforms) ────
684
- // Slot 27: subsurfaceColor.rgb (scatter albedo) + subsurface weight
685
- SUBSURFACE_COLOR: 108, SUBSURFACE: 111,
686
- // Slot 28: subsurfaceRadius.rgb (mean free path) + radius scale
687
- SUBSURFACE_RADIUS: 112, SUBSURFACE_RADIUS_SCALE: 115,
688
- // Slot 29: subsurfaceAnisotropy g (116) + surface anisotropy (strength 117, rotation 118, map index 119)
689
- SUBSURFACE_ANISOTROPY: 116, ANISOTROPY: 117, ANISOTROPY_ROTATION: 118, ANISOTROPY_MAP_INDEX: 119,
690
- // Slot 30: extension-texture map indices A (transmission, clearcoat, clearcoatRoughness, sheenColor)
691
- TRANSMISSION_MAP_INDEX: 120, CLEARCOAT_MAP_INDEX: 121, CLEARCOAT_ROUGHNESS_MAP_INDEX: 122, SHEEN_COLOR_MAP_INDEX: 123,
692
- // Slot 31: extension-texture map indices B (sheenRoughness, iridescence, iridescenceThickness, specularIntensity)
693
- SHEEN_ROUGHNESS_MAP_INDEX: 124, IRIDESCENCE_MAP_INDEX: 125, IRIDESCENCE_THICKNESS_MAP_INDEX: 126, SPECULAR_INTENSITY_MAP_INDEX: 127,
694
- // Slot 32: extension-texture map indices C (specularColor + 3 reserved)
695
- SPECULAR_COLOR_MAP_INDEX: 128,
696
-
697
- // ── Vec4 slot indices (GPU/TSL side) ─────────────────────────────
698
- // Used with getDatafromStorageBuffer( buf, matIdx, int(slot), int(SLOTS_PER_MATERIAL) )
699
- SLOT: {
700
- IOR_TRANSMISSION: 0, // [shadow] ior, transmission, thickness, emissiveIntensity
701
- ATTENUATION: 1, // [shadow] attenuationColor, attenuationDistance
702
- OPACITY_ALPHA: 2, // [shadow+culling] opacity, side, transparent, alphaTest
703
- ALPHA_MODE: 3, // [shadow] alphaMode, depthWrite, normalScale
704
- COLOR_METALNESS: 4, // [BxDF] color.rgb, metalness
705
- EMISSIVE_ROUGHNESS: 5, // [BxDF] emissive.rgb, roughness
706
- MAP_INDICES_A: 6, // [maps] albedo, normal, roughness, metalness
707
- MAP_INDICES_B: 7, // [maps] emissive, bump, clearcoat, clearcoatRoughness
708
- DISPERSION_SHEEN: 8, // [extended] dispersion, visible, sheen, sheenRoughness
709
- SHEEN_COLOR: 9, // [extended] sheenColor, reserved
710
- SPECULAR: 10, // [extended] specularIntensity, specularColor
711
- IRIDESCENCE: 11, // [extended] iridescence, iridescenceIOR, iridescenceThicknessRange
712
- BUMP_DISPLACEMENT: 12, // bumpScale, displacementScale, displacementMapIndex
713
- ALBEDO_TRANSFORM_A: 13, ALBEDO_TRANSFORM_B: 14,
714
- NORMAL_TRANSFORM_A: 15, NORMAL_TRANSFORM_B: 16,
715
- ROUGHNESS_TRANSFORM_A: 17, ROUGHNESS_TRANSFORM_B: 18,
716
- METALNESS_TRANSFORM_A: 19, METALNESS_TRANSFORM_B: 20,
717
- EMISSIVE_TRANSFORM_A: 21, EMISSIVE_TRANSFORM_B: 22,
718
- BUMP_TRANSFORM_A: 23, BUMP_TRANSFORM_B: 24,
719
- DISPLACEMENT_TRANSFORM_A: 25, DISPLACEMENT_TRANSFORM_B: 26,
720
- SUBSURFACE_A: 27, // subsurfaceColor.rgb, subsurface weight
721
- SUBSURFACE_B: 28, // subsurfaceRadius.rgb, subsurfaceRadiusScale
722
- SUBSURFACE_C: 29, // subsurfaceAnisotropy g, anisotropy, anisotropyRotation, anisotropyMapIndex
723
- EXT_MAP_INDICES_A: 30, // transmission, clearcoat, clearcoatRoughness, sheenColor map indices
724
- EXT_MAP_INDICES_B: 31, // sheenRoughness, iridescence, iridescenceThickness, specularIntensity map indices
725
- EXT_MAP_INDICES_C: 32, // specularColor map index + 3 reserved
726
- },
727
-
728
- };
729
-
730
- // glTF/three.js spell "no volume absorption" as attenuationDistance = Infinity, while the GPU
731
- // contract is `> 0 = on` (calculateBeerLawAbsorption). Every writer into ATTENUATION_DISTANCE
732
- // collapses both spellings to 0 so no shader divides by Inf.
733
- export const normalizeAttenuationDistance = d => ( Number.isFinite( d ) && d > 0 ? d : 0 );
734
91
 
735
92
  // The only fallback for a property a three.js material doesn't carry: MeshPhysicalMaterial's own
736
93
  // default (glTF with the extension absent), or the value that turns an engine-only feature off.
@@ -772,6 +129,9 @@ export const MATERIAL_DEFAULTS = deepFreeze( {
772
129
  subsurfaceAnisotropy: 0,
773
130
  anisotropy: 0,
774
131
  anisotropyRotation: 0,
132
+ // KHR_materials_diffuse_transmission: the share of the diffuse lobe passed through to the other side.
133
+ diffuseTransmission: 0,
134
+ diffuseTransmissionColor: [ 1, 1, 1 ],
775
135
  } );
776
136
 
777
137
  function deepFreeze( object ) {
@@ -781,324 +141,6 @@ function deepFreeze( object ) {
781
141
 
782
142
  }
783
143
 
784
- // BVH node leaf markers
785
- /**
786
- * Node tags, written into slot [3] of a BVH node as a raw u32 bit pattern.
787
- *
788
- * Node indices, triangle offsets and counts are integers living inside a Float32Array. Written as
789
- * float *values* they round silently past 2^24 (16,777,216): in a 24M-node scene half of every
790
- * BLAS pointer landed on a neighbouring node and that geometry vanished from the render with no
791
- * error at all. Every one of those fields is written as a u32 bit pattern instead, exact to 2^30.
792
- *
793
- * Tags sit above {@link BVH_MAX_INDEX} so a single unsigned compare separates a leaf from an inner
794
- * node's left-child index. All three are ordinary finite floats — nothing lands in the NaN range,
795
- * which an f32 storage buffer is free to canonicalise.
796
- */
797
- export const BVH_MAX_INDEX = 0x40000000; // 2^30
798
-
799
- /**
800
- * Slot [1] of a BLAS-pointer leaf holds its placement index, which {@link BVH_MAX_INDEX} keeps
801
- * below 2^30. Bit 30 is therefore free to say the leaf's matrix is identity: geometry no other
802
- * placement shares is baked to world space at extraction, and a ray reaching it needs no
803
- * transform at all. Mask the bit off before using the slot as an index.
804
- */
805
- export const TLAS_LEAF_IDENTITY = 0x40000000;
806
- export const TLAS_PLACEMENT_MASK = 0x3fffffff;
807
-
808
- export const BVH_LEAF_MARKERS = {
809
- TRIANGLE_LEAF: 0x40000000, // leaf containing triangle references
810
- BLAS_POINTER_LEAF: 0x40000001, // TLAS leaf pointing to a BLAS root node
811
- FRONTIER: 0x40000002, // parallel-build placeholder, overwritten during assembly
812
- };
813
-
814
- /**
815
- * A triangle leaf of at most this many triangles is folded into its parent: the child's slot ([3]
816
- * or [7]) holds `~( first << 4 | count )`, the very value traversal pushes, so an inner node is
817
- * read exactly as before and only popping a negative entry differs. Node indices stay below
818
- * {@link BVH_MAX_INDEX}, leaf tags between it and 2^31, folded leaves from 2^31 up — so the count
819
- * has four bits and the first triangle must stay below {@link BVH_FOLDED_FIRST_LIMIT}.
820
- */
821
- export const BVH_FOLDED_LEAF_MAX = 15;
822
- export const BVH_FOLDED_FIRST_LIMIT = 1 << 27;
823
-
824
- /** A u32 view over a float buffer, for writing index fields as exact bit patterns. */
825
- export function bvhIndexView( f32 ) {
826
-
827
- return new Uint32Array( f32.buffer, f32.byteOffset, f32.length );
828
-
829
- }
830
-
831
- /**
832
- * Refuse to build a BVH whose indices would collide with the leaf tags.
833
- *
834
- * Throws rather than degrades: the failure this replaces was a scene that rendered with half its
835
- * geometry silently missing, which is far worse than a scene that refuses to load. Guarding the
836
- * totals covers every individual write, since no index can exceed the count it indexes into.
837
- *
838
- * @param {number} count - node or triangle total about to be indexed
839
- * @param {string} what - what the count is, for the message
840
- * @throws {RangeError}
841
- */
842
- export function assertBVHIndexFits( count, what ) {
843
-
844
- if ( count >= BVH_MAX_INDEX ) {
845
-
846
- throw new RangeError(
847
- `${what} is ${count.toLocaleString()}, at or past the BVH index limit of ` +
848
- `${BVH_MAX_INDEX.toLocaleString()}. Node indices are stored as u32 bit patterns and the ` +
849
- 'leaf tags occupy everything above that.'
850
- );
851
-
852
- }
853
-
854
- return count;
855
-
856
- }
857
-
858
- // Texture processing constants
859
- export const TEXTURE_CONSTANTS = {
860
- PIXELS_PER_MATERIAL: 30,
861
- RGBA_COMPONENTS: 4,
862
- VEC4_PER_TRIANGLE: 8,
863
- VEC4_PER_BVH_NODE: 4,
864
- FLOATS_PER_VEC4: 4,
865
- MIN_TEXTURE_WIDTH: 4,
866
- MAX_CONCURRENT_WORKERS: Math.min( typeof navigator !== 'undefined' ? navigator.hardwareConcurrency || 4 : 4, 6 ),
867
- BUFFER_POOL_SIZE: 20,
868
- CANVAS_POOL_SIZE: 12,
869
- CACHE_SIZE_LIMIT: 50,
870
- // Hardware ceiling for a single texture-array dimension (WebGPU maxTextureDimension2D
871
- // guaranteed minimum). The configurable maxTextureSize setting is clamped to this.
872
- MAX_TEXTURE_SIZE: 8192,
873
- // Default cap applied when no maxTextureSize is supplied (engine standalone use).
874
- DEFAULT_MAX_TEXTURE_SIZE: 4096,
875
- // Per-map-type ceiling before bucketing; the real cap is per (pool, bucket) in _bucketTextures.
876
- MAX_TEXTURES_LIMIT: 4 * 256,
877
- // Array shapes per colorSpace pool. Material maps are grouped into this many
878
- // (width, height) classes so a small or oddly-shaped map no longer pays a large
879
- // neighbour's footprint. 4 → ~8 bound material arrays (4 sRGB + 4 linear).
880
- MATERIAL_BUCKET_COUNT: 4,
881
- // Packing stride: a map's stored index encodes bucketId * BUCKET_LAYER_STRIDE + layer.
882
- // Also the per-bucket layer cap. Kept at the WebGPU portable maxTextureArrayLayers floor (256)
883
- // so a consolidated bucket (which merges several map types of one size) stays portable.
884
- BUCKET_LAYER_STRIDE: 256,
885
- };
886
-
887
- // An RGBA8 row upload must be a multiple of 256 bytes, so bucket widths land on 64 texels.
888
- const BUCKET_WIDTH_ALIGN = 64;
889
-
890
- export function alignBucketWidth( width, maxTextureSize ) {
891
-
892
- const aligned = Math.ceil( Math.max( 1, width ) / BUCKET_WIDTH_ALIGN ) * BUCKET_WIDTH_ALIGN;
893
- return Math.min( maxTextureSize, Math.max( TEXTURE_CONSTANTS.MIN_TEXTURE_WIDTH, aligned ) );
894
-
895
- }
896
-
897
- function bucketOf( units ) {
898
-
899
- let width = 0, height = 0, count = 0;
900
- for ( const unit of units ) {
901
-
902
- width = Math.max( width, unit.width );
903
- height = Math.max( height, unit.height );
904
- count += unit.count;
905
-
906
- }
907
-
908
- return { units, width, height, count };
909
-
910
- }
911
-
912
- // Footprint times layer count.
913
- function bucketCost( bucket, maxTextureSize ) {
914
-
915
- return alignBucketWidth( bucket.width, maxTextureSize ) * bucket.height * bucket.count * 4;
916
-
917
- }
918
-
919
- function totalCost( buckets, maxTextureSize ) {
920
-
921
- return buckets.reduce( ( sum, b ) => sum + bucketCost( b, maxTextureSize ), 0 );
922
-
923
- }
924
-
925
- // Repeatedly fuse the pair of buckets whose union wastes least, until `count` remain.
926
- function mergeDown( buckets, count, maxTextureSize ) {
927
-
928
- let current = buckets.slice();
929
-
930
- while ( current.length > count ) {
931
-
932
- let best = null;
933
-
934
- for ( let i = 0; i < current.length; i ++ ) {
935
-
936
- for ( let j = i + 1; j < current.length; j ++ ) {
937
-
938
- const fused = bucketOf( current[ i ].units.concat( current[ j ].units ) );
939
- const delta = bucketCost( fused, maxTextureSize )
940
- - bucketCost( current[ i ], maxTextureSize ) - bucketCost( current[ j ], maxTextureSize );
941
- if ( ! best || delta < best.delta ) best = { i, j, delta, fused };
942
-
943
- }
944
-
945
- }
946
-
947
- current = current.filter( ( _, k ) => k !== best.i && k !== best.j ).concat( [ best.fused ] );
948
-
949
- }
950
-
951
- return current;
952
-
953
- }
954
-
955
- // The pre-shape-aware layout, kept as a rival opening: square scenes still do well on it.
956
- function ladderBuckets( units, count, maxTextureSize ) {
957
-
958
- const ladder = [];
959
- for ( let i = count - 1; i >= 0; i -- ) {
960
-
961
- ladder.push( Math.max( TEXTURE_CONSTANTS.MIN_TEXTURE_WIDTH, Math.round( maxTextureSize / Math.pow( 2, i ) ) ) );
962
-
963
- }
964
-
965
- const bins = ladder.map( () => [] );
966
-
967
- for ( const unit of units ) {
968
-
969
- const longest = Math.pow( 2, Math.ceil( Math.log2( Math.max( unit.width, unit.height ) ) ) );
970
- let slot = ladder.findIndex( size => longest <= size );
971
- if ( slot < 0 ) slot = ladder.length - 1;
972
- bins[ slot ].push( unit );
973
-
974
- }
975
-
976
- return bins.filter( bin => bin.length ).map( bucketOf );
977
-
978
- }
979
-
980
- // Both openings above are greedy and stop short on mixed aspects; moving one shape at a time
981
- // to whichever bucket makes the pool cheapest recovers the difference.
982
- function refine( buckets, maxTextureSize ) {
983
-
984
- let current = buckets;
985
-
986
- for ( let pass = 0; pass < 8; pass ++ ) {
987
-
988
- let improved = false;
989
-
990
- for ( const unit of current.flatMap( b => b.units ) ) {
991
-
992
- const from = current.findIndex( b => b.units.includes( unit ) );
993
- if ( from < 0 ) continue;
994
-
995
- for ( let to = 0; to < current.length; to ++ ) {
996
-
997
- if ( to === from ) continue;
998
-
999
- const moved = [];
1000
- for ( let i = 0; i < current.length; i ++ ) {
1001
-
1002
- if ( i === from ) {
1003
-
1004
- const rest = current[ i ].units.filter( u => u !== unit );
1005
- if ( rest.length ) moved.push( bucketOf( rest ) );
1006
-
1007
- } else if ( i === to ) moved.push( bucketOf( current[ i ].units.concat( [ unit ] ) ) );
1008
- else moved.push( current[ i ] );
1009
-
1010
- }
1011
-
1012
- if ( totalCost( moved, maxTextureSize ) < totalCost( current, maxTextureSize ) ) {
1013
-
1014
- current = moved;
1015
- improved = true;
1016
- break;
1017
-
1018
- }
1019
-
1020
- }
1021
-
1022
- }
1023
-
1024
- if ( ! improved ) break;
1025
-
1026
- }
1027
-
1028
- return current;
1029
-
1030
- }
1031
-
1032
- /**
1033
- * Choose up to `count` array shapes for one colorSpace pool. Grouping by longest edge alone
1034
- * forces a 2000x453 banner into a 2048x2048 array; grouping on both axes does not.
1035
- *
1036
- * @param {Array<{width: number, height: number}>} sizes - every texture in the pool
1037
- * @param {number} maxTextureSize
1038
- * @param {number} [count]
1039
- * @returns {Array<{width: number, height: number}>} ascending by footprint
1040
- */
1041
- export function planTextureBuckets( sizes, maxTextureSize, count = TEXTURE_CONSTANTS.MATERIAL_BUCKET_COUNT ) {
1042
-
1043
- const distinct = new Map();
1044
-
1045
- for ( const { width, height } of sizes ) {
1046
-
1047
- const w = Math.min( maxTextureSize, Math.max( 1, width || 1 ) );
1048
- const h = Math.min( maxTextureSize, Math.max( 1, height || 1 ) );
1049
- const key = `${w}x${h}`;
1050
- const seen = distinct.get( key );
1051
- if ( seen ) seen.count ++;
1052
- else distinct.set( key, { width: w, height: h, count: 1 } );
1053
-
1054
- }
1055
-
1056
- const units = [ ...distinct.values() ];
1057
- let buckets = units.map( unit => bucketOf( [ unit ] ) );
1058
-
1059
- if ( buckets.length > count ) {
1060
-
1061
- const ladder = ladderBuckets( units, count, maxTextureSize );
1062
- const merged = mergeDown( buckets, count, maxTextureSize );
1063
- buckets = totalCost( ladder, maxTextureSize ) < totalCost( merged, maxTextureSize ) ? ladder : merged;
1064
- buckets = refine( buckets, maxTextureSize );
1065
-
1066
- }
1067
-
1068
- return buckets
1069
- .map( b => ( { width: alignBucketWidth( b.width, maxTextureSize ), height: b.height } ) )
1070
- .sort( ( a, b ) => a.width * a.height - b.width * b.height );
1071
-
1072
- }
1073
-
1074
- /**
1075
- * Index of the cheapest planned bucket that can hold a texture at its native size.
1076
- * A texture larger than every bucket lands in the largest and is downscaled there.
1077
- *
1078
- * @param {number} width
1079
- * @param {number} height
1080
- * @param {Array<{width: number, height: number}>} shapes - from {@link planTextureBuckets}
1081
- * @returns {number}
1082
- */
1083
- export function getTextureBucketId( width, height, shapes ) {
1084
-
1085
- const w = Math.max( 1, width || 1 );
1086
- const h = Math.max( 1, height || 1 );
1087
- for ( let i = 0; i < shapes.length; i ++ ) if ( shapes[ i ].width >= w && shapes[ i ].height >= h ) return i;
1088
- return Math.max( 0, shapes.length - 1 );
1089
-
1090
- }
1091
-
1092
- // Pack (bucketId, layer) into the single int slot a material map index occupies.
1093
- export function packTextureIndex( bucketId, layer ) {
1094
-
1095
- return bucketId * TEXTURE_CONSTANTS.BUCKET_LAYER_STRIDE + layer;
1096
-
1097
- }
1098
-
1099
- // Default texture matrix for materials
1100
- export const DEFAULT_TEXTURE_MATRIX = [ 0, 0, 1, 1, 0, 0, 0, 1 ];
1101
-
1102
144
  // Render quality configurations.
1103
145
  // 'interactive' — low-sample, bounded bounces, no offline denoising, controls enabled.
1104
146
  // 'production' — high-sample, deep bounces, OIDN enabled, controls disabled.
@@ -1119,14 +161,14 @@ export const PRODUCTION_RENDER_CONFIG = {
1119
161
  };
1120
162
 
1121
163
  export const INTERACTIVE_RENDER_CONFIG = {
1122
- maxSamples: ENGINE_DEFAULTS.maxSamples, bounces: ENGINE_DEFAULTS.bounces,
164
+ maxSamples: ENGINE_DEFAULTS.maxSamples, bounces: ENGINE_DEFAULTS.maxBounces,
1123
165
  renderMode: ENGINE_DEFAULTS.renderMode, enableAlphaShadows: ENGINE_DEFAULTS.enableAlphaShadows,
1124
166
  // 12, not 5: a spent budget leaves black glass and costs MORE — the ray then bounces diffusely.
1125
167
  transmissiveBounces: 12,
1126
168
  maxSubsurfaceSteps: ENGINE_DEFAULTS.maxSubsurfaceSteps,
1127
169
  // On, like production. The final pass costs one cheap denoise when the preview settles, and both
1128
170
  // neural passes need a denoised frame to be worth running — on Monte-Carlo noise the upscaler
1129
- // measured worse than a plain resize. ⚠️ `ENGINE_DEFAULTS.enableOIDN` stays false: the bench
171
+ // measured worse than a plain resize. ⚠️ `DENOISER_DEFAULTS.enableOIDN` stays false: the bench
1130
172
  // renders against it and every quality golden would move.
1131
173
  enableOIDN: true, oidnQuality: 'fast',
1132
174
  interactionModeEnabled: true,
@@ -1158,12 +200,3 @@ export function modePresetSettings( config ) {
1158
200
  };
1159
201
 
1160
202
  }
1161
-
1162
- // Memory management constants
1163
- export const MEMORY_CONSTANTS = {
1164
- MAX_BUFFER_MEMORY: 1024 * 1024 * 1024,
1165
- MAX_TEXTURE_MEMORY: 2048 * 1024 * 1024,
1166
- CLEANUP_THRESHOLD: 0.8,
1167
- CHUNK_SIZE_THRESHOLD: 64 * 1024 * 1024,
1168
- STREAM_BATCH_SIZE: 4
1169
- };