rayzee 6.5.0 → 7.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 (51) hide show
  1. package/README.md +24 -5
  2. package/dist/rayzee.es.js +7554 -7014
  3. package/dist/rayzee.es.js.map +1 -1
  4. package/dist/rayzee.umd.js +157 -236
  5. package/dist/rayzee.umd.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/EngineDefaults.js +12 -9
  8. package/src/PathTracerApp.js +118 -26
  9. package/src/Pipeline/PipelineContext.js +1 -2
  10. package/src/Pipeline/RenderPipeline.js +1 -1
  11. package/src/Pipeline/RenderStage.js +1 -1
  12. package/src/Processor/CameraOptimizer.js +0 -5
  13. package/src/Processor/GeometryExtractor.js +6 -0
  14. package/src/Processor/KernelManager.js +277 -0
  15. package/src/Processor/PackedRayBuffer.js +265 -0
  16. package/src/Processor/QueueManager.js +173 -0
  17. package/src/Processor/SceneProcessor.js +1 -0
  18. package/src/Processor/ShaderBuilder.js +11 -317
  19. package/src/Processor/StorageTexturePool.js +29 -15
  20. package/src/Processor/VRAMTracker.js +169 -0
  21. package/src/Processor/utils.js +11 -110
  22. package/src/RenderSettings.js +0 -3
  23. package/src/Stages/ASVGF.js +76 -20
  24. package/src/Stages/BilateralFilter.js +34 -10
  25. package/src/Stages/EdgeFilter.js +2 -3
  26. package/src/Stages/MotionVector.js +16 -9
  27. package/src/Stages/NormalDepth.js +17 -5
  28. package/src/Stages/PathTracer.js +671 -1456
  29. package/src/Stages/PathTracerStage.js +1451 -0
  30. package/src/Stages/SSRC.js +32 -15
  31. package/src/Stages/Variance.js +35 -12
  32. package/src/TSL/CompactKernel.js +110 -0
  33. package/src/TSL/DebugKernel.js +98 -0
  34. package/src/TSL/Environment.js +13 -11
  35. package/src/TSL/ExtendKernel.js +75 -0
  36. package/src/TSL/FinalWriteKernel.js +121 -0
  37. package/src/TSL/GenerateKernel.js +109 -0
  38. package/src/TSL/LightsSampling.js +2 -2
  39. package/src/TSL/PathTracerCore.js +43 -1039
  40. package/src/TSL/ShadeKernel.js +873 -0
  41. package/src/TSL/patches.js +81 -4
  42. package/src/index.js +3 -0
  43. package/src/managers/CameraManager.js +1 -1
  44. package/src/managers/DenoisingManager.js +40 -75
  45. package/src/managers/EnvironmentManager.js +30 -39
  46. package/src/managers/OverlayManager.js +7 -22
  47. package/src/managers/UniformManager.js +0 -3
  48. package/src/managers/helpers/TileHelper.js +2 -2
  49. package/src/Stages/AdaptiveSampling.js +0 -483
  50. package/src/TSL/PathTracer.js +0 -384
  51. package/src/managers/TileManager.js +0 -298
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "6.5.0",
3
+ "version": "7.0.0",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -58,6 +58,10 @@ export const ENGINE_DEFAULTS = {
58
58
  afScreenPoint: { x: 0.5, y: 0.5 },
59
59
  afSmoothingFactor: 0.15,
60
60
 
61
+ // Multi-sample pool: S=samplesPerPixel rays/pixel/frame, FinalWrite averages them; interactive-only (renderMode 0, ≤ cap), else S=1.
62
+ // Pixel cap (768²) bounds pool memory; covers the 512² default, excludes ≥768².
63
+ wavefrontMultiSampleMaxPixels: 589824,
64
+
61
65
  enablePathTracer: true,
62
66
  enableAccumulation: true,
63
67
  pauseRendering: false,
@@ -70,14 +74,9 @@ export const ENGINE_DEFAULTS = {
70
74
  enableEmissiveTriangleSampling: false,
71
75
  emissiveBoost: 1.0,
72
76
 
73
- adaptiveSampling: false,
74
- adaptiveSamplingMin: 1,
75
- adaptiveSamplingMax: 8,
76
- adaptiveSamplingVarianceThreshold: 0.1,
77
77
  temporalVarianceWeight: 0.6,
78
78
  enableEarlyTermination: true,
79
79
  earlyTerminationThreshold: 0.002,
80
- showAdaptiveSamplingHelper: false,
81
80
  performanceModeAdaptive: 'medium',
82
81
 
83
82
  fireflyThreshold: 3.0,
@@ -85,8 +84,7 @@ export const ENGINE_DEFAULTS = {
85
84
  renderTimeLimit: 30,
86
85
  renderMode: 0,
87
86
  enableAlphaShadows: false,
88
- tiles: 3,
89
- tilesHelper: false,
87
+ tilesHelper: true, // show OIDN denoise / AI upscale tile progress overlay
90
88
  showLightHelper: false,
91
89
 
92
90
  directionalLightIntensity: 0,
@@ -152,6 +150,12 @@ export const ENGINE_DEFAULTS = {
152
150
  // only round-trip exactly when both sides agree.
153
151
  export const ALBEDO_EPS = 0.01;
154
152
 
153
+ // Per-resolution compute StorageTextures are pre-allocated at this size and never
154
+ // resized (works around three.js r184 StorageTexture-resize bugs — see TSL/patches
155
+ // history). Render resolution must not exceed this on either axis; the engine warns
156
+ // and ignores larger requests.
157
+ export const MAX_STORAGE_TEXTURE_SIZE = 2048;
158
+
155
159
  export const ASVGF_QUALITY_PRESETS = {
156
160
  // phiColor / phiDepth are RELATIVE tolerances (fractions). Bigger = more
157
161
  // permissive. gradientStrength = 0 keeps the adaptive-α boost off; the
@@ -467,7 +471,7 @@ export const DEFAULT_TEXTURE_MATRIX = [ 0, 0, 1, 1, 0, 0, 0, 1 ];
467
471
  // 'production' — high-sample, deep bounces, OIDN enabled, controls disabled.
468
472
  export const PRODUCTION_RENDER_CONFIG = {
469
473
  maxSamples: 30, bounces: 20, transmissiveBounces: 8, maxSubsurfaceSteps: 64, samplesPerPixel: 1,
470
- renderMode: 1, enableAlphaShadows: true, tiles: 3, tilesHelper: true,
474
+ renderMode: 1, enableAlphaShadows: true,
471
475
  enableOIDN: true, oidnQuality: 'balance',
472
476
  interactionModeEnabled: false,
473
477
  };
@@ -477,7 +481,6 @@ export const INTERACTIVE_RENDER_CONFIG = {
477
481
  samplesPerPixel: ENGINE_DEFAULTS.samplesPerPixel, renderMode: ENGINE_DEFAULTS.renderMode, enableAlphaShadows: ENGINE_DEFAULTS.enableAlphaShadows,
478
482
  transmissiveBounces: ENGINE_DEFAULTS.transmissiveBounces,
479
483
  maxSubsurfaceSteps: ENGINE_DEFAULTS.maxSubsurfaceSteps,
480
- tiles: ENGINE_DEFAULTS.tiles, tilesHelper: ENGINE_DEFAULTS.tilesHelper,
481
484
  enableOIDN: false, oidnQuality: 'fast',
482
485
  interactionModeEnabled: true,
483
486
  };
@@ -11,14 +11,13 @@ import { MotionVector } from './Stages/MotionVector.js';
11
11
  import { ASVGF } from './Stages/ASVGF.js';
12
12
  import { Variance } from './Stages/Variance.js';
13
13
  import { BilateralFilter } from './Stages/BilateralFilter.js';
14
- import { AdaptiveSampling } from './Stages/AdaptiveSampling.js';
15
14
  import { EdgeFilter } from './Stages/EdgeFilter.js';
16
15
  import { AutoExposure } from './Stages/AutoExposure.js';
17
16
  import { SSRC } from './Stages/SSRC.js';
18
17
  import { Compositor } from './Stages/Compositor.js';
19
18
  import { RenderPipeline } from './Pipeline/RenderPipeline.js';
20
19
  import { CompletionTracker } from './Pipeline/CompletionTracker.js';
21
- import { ENGINE_DEFAULTS as DEFAULT_STATE, PRODUCTION_RENDER_CONFIG, INTERACTIVE_RENDER_CONFIG } from './EngineDefaults.js';
20
+ import { ENGINE_DEFAULTS as DEFAULT_STATE, PRODUCTION_RENDER_CONFIG, INTERACTIVE_RENDER_CONFIG, MAX_STORAGE_TEXTURE_SIZE } from './EngineDefaults.js';
22
21
  import { updateStats, updateLoading, resetLoading, setStatusCallback, getDisplaySamples, disposeObjectFromMemory } from './Processor/utils.js';
23
22
  import { BuildTimer } from './Processor/BuildTimer.js';
24
23
  import { InteractionManager } from './managers/InteractionManager.js';
@@ -310,7 +309,14 @@ export class PathTracerApp extends EventDispatcher {
310
309
 
311
310
  }
312
311
 
313
- updateStats( { timeElapsed: this.completion.timeElapsed, samples: getDisplaySamples( this.stages.pathTracer ) } );
312
+ this._ensureVRAMWiring();
313
+ const mem = this.stages.pathTracer?.vramTracker?.measure();
314
+ updateStats( {
315
+ timeElapsed: this.completion.timeElapsed,
316
+ samples: getDisplaySamples( this.stages.pathTracer ),
317
+ memoryUsed: mem?.current ?? 0,
318
+ memoryPeak: mem?.peak ?? 0,
319
+ } );
314
320
 
315
321
  // Check time limit
316
322
  if ( this.completion.isTimeLimitReached( this.settings.get( 'renderLimitMode' ), this.settings.get( 'renderTimeLimit' ) ) ) {
@@ -740,12 +746,9 @@ export class PathTracerApp extends EventDispatcher {
740
746
  this.stages.pathTracer.setupMaterial();
741
747
  timer.end( 'Material setup (TSL compile)' );
742
748
 
743
- // Front-load GPU pipeline creation so the first animate frame is snappy:
744
- // - compute: Three.js has no async compute compile one dispatch at
745
- // build time moves the stall to this loading moment.
746
- // - raster fallback: compileAsync yields to main thread (r184+).
749
+ // Front-load raster pipeline creation (compileAsync yields to main thread, r184+) so the first
750
+ // animate frame is snappy. Wavefront compute kernels compile lazily on their first dispatch.
747
751
  timer.start( 'Pipeline precompile' );
748
- this.stages.pathTracer.shaderBuilder.forceCompile( this.renderer );
749
752
  try {
750
753
 
751
754
  await this.renderer.compileAsync( this.meshScene, this.cameraManager.camera );
@@ -845,11 +848,31 @@ export class PathTracerApp extends EventDispatcher {
845
848
  // Resize
846
849
  // ═══════════════════════════════════════════════════════════════
847
850
 
851
+ /**
852
+ * Guard against render resolutions the compute pipeline can't support.
853
+ * Per-resolution StorageTextures are pre-allocated at MAX_STORAGE_TEXTURE_SIZE
854
+ * and never resized, so a larger request would overflow them. Warn and skip.
855
+ * @returns {boolean} true if the size is renderable
856
+ */
857
+ _isRenderSizeSupported( width, height ) {
858
+
859
+ if ( width > MAX_STORAGE_TEXTURE_SIZE || height > MAX_STORAGE_TEXTURE_SIZE ) {
860
+
861
+ console.warn( `[Rayzee] Render resolution ${width}×${height} exceeds the ${MAX_STORAGE_TEXTURE_SIZE}px limit (compute storage textures are pre-allocated at ${MAX_STORAGE_TEXTURE_SIZE}px). Ignoring resize — use a resolution ≤ ${MAX_STORAGE_TEXTURE_SIZE}.` );
862
+ return false;
863
+
864
+ }
865
+
866
+ return true;
867
+
868
+ }
869
+
848
870
  onResize() {
849
871
 
850
872
  const width = this.canvas.clientWidth;
851
873
  const height = this.canvas.clientHeight;
852
874
  if ( width === 0 || height === 0 ) return;
875
+ if ( ! this._isRenderSizeSupported( width, height ) ) return;
853
876
 
854
877
  this.renderer.setPixelRatio( 1.0 );
855
878
  this.renderer.setSize( width, height, false );
@@ -878,6 +901,8 @@ export class PathTracerApp extends EventDispatcher {
878
901
 
879
902
  _applyRenderResize( renderWidth, renderHeight ) {
880
903
 
904
+ if ( ! this._isRenderSizeSupported( renderWidth, renderHeight ) ) return;
905
+
881
906
  this.pipeline?.setSize( renderWidth, renderHeight );
882
907
  this.denoisingManager?.setRenderSize( renderWidth, renderHeight );
883
908
  this.needsReset = true;
@@ -889,6 +914,7 @@ export class PathTracerApp extends EventDispatcher {
889
914
  setCanvasSize( width, height ) {
890
915
 
891
916
  if ( width === 0 || height === 0 ) return;
917
+ if ( ! this._isRenderSizeSupported( width, height ) ) return;
892
918
 
893
919
  this.renderer.setPixelRatio( 1.0 );
894
920
  this.renderer.setSize( width, height, false );
@@ -927,15 +953,6 @@ export class PathTracerApp extends EventDispatcher {
927
953
 
928
954
  this.stages.pathTracer?.setUniform( 'renderMode', parseInt( config.renderMode ) );
929
955
  this.stages.pathTracer?.setUniform( 'enableAlphaShadows', config.enableAlphaShadows ?? false );
930
- this.stages.pathTracer?.tileManager?.setTileCount( config.tiles );
931
-
932
- const tileHelper = this.overlayManager?.getHelper( 'tiles' );
933
- if ( tileHelper ) {
934
-
935
- tileHelper.enabled = config.tilesHelper;
936
- if ( ! config.tilesHelper ) tileHelper.hide();
937
-
938
- }
939
956
 
940
957
  this.stages.pathTracer?.updateCompletionThreshold?.();
941
958
 
@@ -958,6 +975,20 @@ export class PathTracerApp extends EventDispatcher {
958
975
 
959
976
  this.needsReset = false;
960
977
  this.pauseRendering = false;
978
+
979
+ // Entering a final render starts a fresh peak window (Blender per-render semantics).
980
+ if ( isProduction ) {
981
+
982
+ const tracker = this.stages.pathTracer?.vramTracker;
983
+ if ( tracker ) {
984
+
985
+ tracker.measure();
986
+ tracker.resetPeak();
987
+
988
+ }
989
+
990
+ }
991
+
961
992
  this.reset();
962
993
 
963
994
  }
@@ -1067,6 +1098,69 @@ export class PathTracerApp extends EventDispatcher {
1067
1098
 
1068
1099
  }
1069
1100
 
1101
+ /** The path tracer's VRAM tracker, or null before stages are built. */
1102
+ get vram() {
1103
+
1104
+ return this.stages.pathTracer?.vramTracker ?? null;
1105
+
1106
+ }
1107
+
1108
+ /**
1109
+ * On-demand current/peak GPU memory snapshot.
1110
+ * @returns {{ current: number, peak: number, byCategory: Object }} bytes
1111
+ */
1112
+ getMemoryInfo() {
1113
+
1114
+ return this.stages.pathTracer?.vramTracker?.measure() ?? { current: 0, peak: 0, byCategory: {} };
1115
+
1116
+ }
1117
+
1118
+ // Idempotent: registers the cross-stage texture provider and re-measures on
1119
+ // allocation events (scene/env load, resize) so peak is caught even while idle.
1120
+ _ensureVRAMWiring() {
1121
+
1122
+ if ( this._vramWired ) return;
1123
+ const tracker = this.stages.pathTracer?.vramTracker;
1124
+ if ( ! tracker ) return; // stages not ready yet
1125
+
1126
+ tracker.register( 'stages', () => this._collectStageTextures() );
1127
+
1128
+ const remeasure = () => tracker.measure();
1129
+ this._addTrackedListener( this, 'SceneRebuild', remeasure );
1130
+ this._addTrackedListener( this, 'EnvironmentLoaded', remeasure );
1131
+ this._addTrackedListener( this, 'resolution_changed', remeasure );
1132
+
1133
+ this._vramWired = true;
1134
+
1135
+ }
1136
+
1137
+ // Direct StorageTexture/RenderTarget properties of every non-pathTracer stage
1138
+ // (denoiser/G-buffer/filter targets). The pathTracer's own buffers/textures are
1139
+ // registered explicitly; measure() dedupes by identity so overlaps don't double-count.
1140
+ _collectStageTextures() {
1141
+
1142
+ const out = [];
1143
+ const stages = this.stages || {};
1144
+ const pt = stages.pathTracer;
1145
+
1146
+ for ( const key in stages ) {
1147
+
1148
+ const stage = stages[ key ];
1149
+ if ( ! stage || stage === pt || typeof stage !== 'object' ) continue;
1150
+
1151
+ for ( const prop in stage ) {
1152
+
1153
+ const v = stage[ prop ];
1154
+ if ( v && ( v.isTexture || v.isRenderTarget ) ) out.push( v );
1155
+
1156
+ }
1157
+
1158
+ }
1159
+
1160
+ return out;
1161
+
1162
+ }
1163
+
1070
1164
  // ═══════════════════════════════════════════════════════════════
1071
1165
  // Materials (absorbed from MaterialsAPI)
1072
1166
  // ═══════════════════════════════════════════════════════════════
@@ -1207,6 +1301,7 @@ export class PathTracerApp extends EventDispatcher {
1207
1301
  maxBufferSize: adapterLimits.maxBufferSize,
1208
1302
  maxStorageBufferBindingSize: adapterLimits.maxStorageBufferBindingSize,
1209
1303
  maxColorAttachmentBytesPerSample: 128,
1304
+ maxStorageBuffersPerShaderStage: Math.min( adapterLimits.maxStorageBuffersPerShaderStage, 10 ),
1210
1305
  }
1211
1306
  } );
1212
1307
 
@@ -1265,7 +1360,6 @@ export class PathTracerApp extends EventDispatcher {
1265
1360
  this.pipeline.addStage( this.stages.asvgf );
1266
1361
  this.pipeline.addStage( this.stages.variance );
1267
1362
  this.pipeline.addStage( this.stages.bilateralFilter );
1268
- this.pipeline.addStage( this.stages.adaptiveSampling );
1269
1363
  this.pipeline.addStage( this.stages.edgeFilter );
1270
1364
  this.pipeline.addStage( this.stages.autoExposure );
1271
1365
  this.pipeline.addStage( this.stages.compositor );
@@ -1469,9 +1563,6 @@ export class PathTracerApp extends EventDispatcher {
1469
1563
 
1470
1564
  _createStages() {
1471
1565
 
1472
- const adaptiveSamplingMax = this.settings.get( 'adaptiveSamplingMax' );
1473
- const useAdaptiveSampling = this.settings.get( 'useAdaptiveSampling' );
1474
-
1475
1566
  this.stages.pathTracer = new PathTracer( this.renderer, this.scene, this.cameraManager.camera );
1476
1567
  this.stages.normalDepth = new NormalDepth( this.renderer, {
1477
1568
  pathTracer: this.stages.pathTracer
@@ -1483,10 +1574,6 @@ export class PathTracerApp extends EventDispatcher {
1483
1574
  this.stages.asvgf = new ASVGF( this.renderer, { enabled: false } );
1484
1575
  this.stages.variance = new Variance( this.renderer, { enabled: false } );
1485
1576
  this.stages.bilateralFilter = new BilateralFilter( this.renderer, { enabled: false } );
1486
- this.stages.adaptiveSampling = new AdaptiveSampling( this.renderer, {
1487
- adaptiveSamplingMax,
1488
- enabled: useAdaptiveSampling,
1489
- } );
1490
1577
  this.stages.edgeFilter = new EdgeFilter( this.renderer, { enabled: false } );
1491
1578
  this.stages.autoExposure = new AutoExposure( this.renderer, { enabled: DEFAULT_STATE.autoExposure ?? false } );
1492
1579
 
@@ -1505,10 +1592,11 @@ export class PathTracerApp extends EventDispatcher {
1505
1592
  camera: this.cameraManager.camera,
1506
1593
  stages: {
1507
1594
  pathTracer: this.stages.pathTracer,
1595
+ normalDepth: this.stages.normalDepth,
1596
+ motionVector: this.stages.motionVector,
1508
1597
  asvgf: this.stages.asvgf,
1509
1598
  variance: this.stages.variance,
1510
1599
  bilateralFilter: this.stages.bilateralFilter,
1511
- adaptiveSampling: this.stages.adaptiveSampling,
1512
1600
  edgeFilter: this.stages.edgeFilter,
1513
1601
  ssrc: this.stages.ssrc,
1514
1602
  autoExposure: this.stages.autoExposure,
@@ -1523,6 +1611,10 @@ export class PathTracerApp extends EventDispatcher {
1523
1611
  this.denoisingManager.setupDenoiser();
1524
1612
  this.denoisingManager.setupUpscaler();
1525
1613
 
1614
+ // Seed G-buffer gating: NormalDepth/MotionVector start enabled (stage default)
1615
+ // but are only needed by real-time denoisers — idle them until one is active.
1616
+ this.denoisingManager._syncGBufferStages();
1617
+
1526
1618
  // Set initial render resolution
1527
1619
  const initW = this.canvas.clientWidth || 1;
1528
1620
  const initH = this.canvas.clientHeight || 1;
@@ -42,7 +42,7 @@ export class PipelineContext {
42
42
  accumulatedFrames: 0,
43
43
 
44
44
  // Render modes
45
- renderMode: 0, // 0 = progressive, 1 = tiled
45
+ renderMode: 0, // 0 = interactive, 1 = production (full-frame in both)
46
46
  interactionMode: false,
47
47
  isComplete: false,
48
48
 
@@ -65,7 +65,6 @@ export class PipelineContext {
65
65
 
66
66
  // Feature flags
67
67
  enableASVGF: false,
68
- enableAdaptiveSampling: false,
69
68
  enableEdgeFiltering: false,
70
69
  // Can be extended by stages as needed
71
70
  };
@@ -22,7 +22,7 @@ import { EventDispatcher } from './EventDispatcher.js';
22
22
  * // Add stages in execution order
23
23
  * pipeline.addStage(new PathTracer(...));
24
24
  * pipeline.addStage(new ASVGF(...));
25
- * pipeline.addStage(new AdaptiveSampling(...));
25
+ * pipeline.addStage(new EdgeFilter(...));
26
26
  *
27
27
  * // Render all stages
28
28
  * pipeline.render(writeBuffer);
@@ -28,7 +28,7 @@ export const StageExecutionMode = {
28
28
 
29
29
  /**
30
30
  * CONDITIONAL - Stage decides execution via shouldExecute() method
31
- * Use for: Stages with complex execution logic (AdaptiveSampling)
31
+ * Use for: Stages with complex execution logic
32
32
  */
33
33
  CONDITIONAL: 'conditional'
34
34
  };
@@ -25,7 +25,6 @@ export class CameraOptimizer {
25
25
  this.interactionQualitySettings = {
26
26
  maxBounceCount: 1,
27
27
  numRaysPerPixel: 1,
28
- useAdaptiveSampling: false,
29
28
  useEnvMapIS: false,
30
29
  // pixelRatio: 0.25,
31
30
  enableAccumulation: false,
@@ -306,7 +305,6 @@ export class CameraOptimizer {
306
305
  'ultra-low': {
307
306
  maxBounceCount: 1,
308
307
  numRaysPerPixel: 1,
309
- useAdaptiveSampling: false,
310
308
  useEnvMapIS: false,
311
309
  pixelRatio: 0.125,
312
310
  enableAccumulation: false
@@ -314,7 +312,6 @@ export class CameraOptimizer {
314
312
  'low': {
315
313
  maxBounceCount: 1,
316
314
  numRaysPerPixel: 1,
317
- useAdaptiveSampling: false,
318
315
  useEnvMapIS: false,
319
316
  pixelRatio: 0.25,
320
317
  enableAccumulation: false
@@ -322,7 +319,6 @@ export class CameraOptimizer {
322
319
  'medium': {
323
320
  maxBounceCount: 2,
324
321
  numRaysPerPixel: 1,
325
- useAdaptiveSampling: false,
326
322
  useEnvMapIS: true,
327
323
  pixelRatio: 0.5,
328
324
  enableAccumulation: false
@@ -330,7 +326,6 @@ export class CameraOptimizer {
330
326
  'high': {
331
327
  maxBounceCount: 3,
332
328
  numRaysPerPixel: 1,
333
- useAdaptiveSampling: true,
334
329
  useEnvMapIS: true,
335
330
  pixelRatio: 0.75,
336
331
  enableAccumulation: true
@@ -536,6 +536,10 @@ export class GeometryExtractor {
536
536
  // triangle extraction that stores directly in texture format
537
537
  extractTrianglesInBatch( positions, normals, uvs, indices, triangleCount, materialIndex, meshIndex ) {
538
538
 
539
+ // Track per-material triangle count for sort-bin remap (item 41)
540
+ while ( this.materialTriangleCounts.length <= materialIndex ) this.materialTriangleCounts.push( 0 );
541
+ this.materialTriangleCounts[ materialIndex ] += triangleCount;
542
+
539
543
  // Pre-allocate objects for positions, normals, and UVs
540
544
  const posA = this._getVec3( 0 );
541
545
  const posB = this._getVec3( 1 );
@@ -779,6 +783,7 @@ export class GeometryExtractor {
779
783
 
780
784
  // Reset other arrays
781
785
  this.materials = [];
786
+ this.materialTriangleCounts = []; // Per-material triangle count (for sort-bin remap, item 41)
782
787
  this.meshes = [];
783
788
  this.meshTriangleRanges = []; // Per-mesh { start, count } for TLAS/BLAS
784
789
  this.maps = [];
@@ -816,6 +821,7 @@ export class GeometryExtractor {
816
821
  triangleData: this.getTriangleData(), // Texture-ready Float32Array format
817
822
  triangleCount: this.getTriangleCount(),
818
823
  materials: this.materials,
824
+ materialTriangleCounts: this.materialTriangleCounts,
819
825
  meshes: this.meshes,
820
826
  meshTriangleRanges: this.meshTriangleRanges, // Per-mesh { start, count } for TLAS/BLAS
821
827
  maps: this.maps,