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.
- package/README.md +22 -20
- package/dist/{ArchiveImporter-xuUJiklP.js → ArchiveImporter-BRQvLggb.js} +12 -13
- package/dist/{BidirectionalIntegrator-C5PKjJoT.js → BidirectionalIntegrator-9lZQxFVH.js} +87 -87
- package/dist/BufferLayout-D_IBo2ci.js +150 -0
- package/dist/{ColorManagement-CheBCPaW.js → ColorManagement-oeEk7oSu.js} +171 -171
- package/dist/{NeuralRetouch-CW11yLZ_.js → NeuralRetouch-BCXylThH.js} +3 -3
- package/dist/{NeuralSuperRes-BekaecYg.js → NeuralSuperRes-sZAeS142.js} +3 -3
- package/dist/{PathTracer-Ccl9KOuF.js → PathTracer-CQPW06Mm.js} +2911 -2669
- package/dist/{PhysicalSky-Dv2PMFGW.js → PhysicalSky-BvqCw_dy.js} +2 -2
- package/dist/{RayzeeRenderer-BQxnzCTD.js → RayzeeRenderer-COpwotpU.js} +338 -329
- package/dist/{SolarLimb-ken-JbDK.js → SolarLimb-CPaeZVwz.js} +10 -10
- package/dist/{ToneMapGPU-BHi5JzIq.js → ToneMapGPU-CjU3osDd.js} +1 -1
- package/dist/{ViewTransforms-D0rTLBsX.js → ViewTransforms-CYN7bq2w.js} +2 -2
- package/dist/addons/archives.es.js +1 -1
- package/dist/addons/bidirectional.es.js +1 -1
- package/dist/addons/color.es.js +1 -1
- package/dist/addons/physical-sky.es.js +1 -1
- package/dist/rayzee-core.es.js +11 -11
- package/dist/rayzee.es.js +734 -599
- package/dist/rayzee.umd.js +102 -102
- package/dist/sceneCachePolicy-BSSsKY98.js +443 -0
- package/package.json +1 -1
- package/src/Color/ViewTransforms.js +5 -1
- package/src/EngineDefaults.js +59 -1032
- package/src/Headless.js +6 -5
- package/src/Passes/OIDNTemporalHistory.js +1 -1
- package/src/PathTracerApp.js +5 -3
- package/src/Processor/AssetLoader.js +6 -5
- package/src/Processor/BVHBuilder.js +10 -31
- package/src/Processor/BVHLeafFold.js +4 -6
- package/src/Processor/BVHRefitter.js +3 -46
- package/src/Processor/BufferLayout.js +291 -0
- package/src/Processor/EmissiveTriangleBuilder.js +1 -1
- package/src/Processor/GeometryExtractor.js +3 -3
- package/src/Processor/HostMemory.js +7 -1
- package/src/Processor/InstanceTable.js +1 -1
- package/src/Processor/MaterialPacking.js +2 -1
- package/src/Processor/PBRT/PBRTSceneBuilder.js +1 -1
- package/src/Processor/QueueManager.js +28 -3
- package/src/Processor/SceneProcessor.js +4 -2
- package/src/Processor/SplitBLAS.js +1 -1
- package/src/Processor/StorageTexturePool.js +22 -1
- package/src/Processor/TLASBuilder.js +1 -3
- package/src/Processor/TextureBuckets.js +237 -0
- package/src/Processor/TextureCreator.js +10 -1
- package/src/Processor/Workers/TexturesWorker.js +5 -6
- package/src/RayzeeRenderer.js +29 -32
- package/src/RenderSettings.js +26 -28
- package/src/Stages/ASVGF.js +8 -7
- package/src/Stages/AutoExposure.js +14 -5
- package/src/Stages/BilateralFilter.js +2 -1
- package/src/Stages/DenoiserSettings.js +182 -0
- package/src/Stages/EdgeFilter.js +2 -1
- package/src/Stages/MotionVector.js +1 -1
- package/src/Stages/NRD.js +2 -3
- package/src/Stages/NormalDepth.js +3 -2
- package/src/Stages/PathTracer.js +72 -4
- package/src/Stages/PathTracerStage.js +2 -1
- package/src/Stages/Variance.js +1 -1
- package/src/TSL/BVHTraversal.js +4 -1
- package/src/TSL/Common.js +1 -1
- package/src/TSL/EmissiveSampling.js +1 -1
- package/src/TSL/LightVisibility.js +111 -0
- package/src/TSL/LightsSampling.js +151 -15
- package/src/TSL/Random.js +1 -1
- package/src/TSL/ShadeKernel.js +88 -3
- package/src/TSL/TextureSampling.js +1 -1
- package/src/core.js +5 -9
- package/src/index.js +10 -21
- package/src/integrators/BidirectionalIntegrator.js +2 -1
- package/src/managers/CameraManager.js +17 -7
- package/src/managers/DenoisingManager.js +14 -10
- package/src/managers/EnvironmentManager.js +32 -13
- package/src/managers/MaterialDataManager.js +3 -3
- package/src/managers/UniformManager.js +2 -2
- package/dist/EngineDefaults-DH3bI5t-.js +0 -844
- package/dist/sceneCachePolicy-BmwYOYDu.js +0 -183
- /package/dist/{Logger-BnHghprG.js → Logger-dDDGj9Av.js} +0 -0
package/src/EngineDefaults.js
CHANGED
|
@@ -1,739 +1,93 @@
|
|
|
1
|
-
|
|
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
|
-
*
|
|
39
|
-
*
|
|
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
|
|
7
|
+
export const ENGINE_DEFAULTS = deepFreeze( {
|
|
43
8
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
42
|
+
// Output
|
|
73
43
|
exposure: 1,
|
|
74
|
-
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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.
|
|
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. ⚠️ `
|
|
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
|
-
};
|