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