rayzee 6.4.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 (58) hide show
  1. package/README.md +24 -5
  2. package/dist/rayzee.es.js +4953 -4225
  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 +29 -13
  8. package/src/PathTracerApp.js +119 -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 +22 -1
  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 -316
  19. package/src/Processor/StorageTexturePool.js +29 -15
  20. package/src/Processor/TextureCreator.js +6 -0
  21. package/src/Processor/VRAMTracker.js +169 -0
  22. package/src/Processor/utils.js +11 -110
  23. package/src/RenderSettings.js +1 -3
  24. package/src/Stages/ASVGF.js +76 -20
  25. package/src/Stages/BilateralFilter.js +34 -10
  26. package/src/Stages/EdgeFilter.js +2 -3
  27. package/src/Stages/MotionVector.js +16 -9
  28. package/src/Stages/NormalDepth.js +17 -5
  29. package/src/Stages/PathTracer.js +671 -1456
  30. package/src/Stages/PathTracerStage.js +1451 -0
  31. package/src/Stages/SSRC.js +32 -15
  32. package/src/Stages/Variance.js +35 -12
  33. package/src/TSL/BVHTraversal.js +7 -1
  34. package/src/TSL/Common.js +12 -2
  35. package/src/TSL/CompactKernel.js +110 -0
  36. package/src/TSL/DebugKernel.js +98 -0
  37. package/src/TSL/Environment.js +13 -11
  38. package/src/TSL/ExtendKernel.js +75 -0
  39. package/src/TSL/FinalWriteKernel.js +121 -0
  40. package/src/TSL/GenerateKernel.js +109 -0
  41. package/src/TSL/LightsSampling.js +2 -2
  42. package/src/TSL/MaterialTransmission.js +32 -2
  43. package/src/TSL/PathTracerCore.js +43 -912
  44. package/src/TSL/ShadeKernel.js +873 -0
  45. package/src/TSL/Struct.js +5 -0
  46. package/src/TSL/Subsurface.js +232 -0
  47. package/src/TSL/patches.js +81 -4
  48. package/src/index.js +3 -0
  49. package/src/managers/CameraManager.js +1 -1
  50. package/src/managers/DenoisingManager.js +40 -75
  51. package/src/managers/EnvironmentManager.js +30 -39
  52. package/src/managers/MaterialDataManager.js +60 -1
  53. package/src/managers/OverlayManager.js +7 -22
  54. package/src/managers/UniformManager.js +1 -3
  55. package/src/managers/helpers/TileHelper.js +2 -2
  56. package/src/Stages/AdaptiveSampling.js +0 -483
  57. package/src/TSL/PathTracer.js +0 -384
  58. package/src/managers/TileManager.js +0 -298
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "6.4.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,
@@ -65,18 +69,14 @@ export const ENGINE_DEFAULTS = {
65
69
  bounces: 3,
66
70
  samplesPerPixel: 1,
67
71
  transmissiveBounces: 5,
72
+ maxSubsurfaceSteps: 8, // interactive default: low cap (bounded random-walk SSS)
68
73
  samplingTechnique: 3,
69
74
  enableEmissiveTriangleSampling: false,
70
75
  emissiveBoost: 1.0,
71
76
 
72
- adaptiveSampling: false,
73
- adaptiveSamplingMin: 1,
74
- adaptiveSamplingMax: 8,
75
- adaptiveSamplingVarianceThreshold: 0.1,
76
77
  temporalVarianceWeight: 0.6,
77
78
  enableEarlyTermination: true,
78
79
  earlyTerminationThreshold: 0.002,
79
- showAdaptiveSamplingHelper: false,
80
80
  performanceModeAdaptive: 'medium',
81
81
 
82
82
  fireflyThreshold: 3.0,
@@ -84,8 +84,7 @@ export const ENGINE_DEFAULTS = {
84
84
  renderTimeLimit: 30,
85
85
  renderMode: 0,
86
86
  enableAlphaShadows: false,
87
- tiles: 3,
88
- tilesHelper: false,
87
+ tilesHelper: true, // show OIDN denoise / AI upscale tile progress overlay
89
88
  showLightHelper: false,
90
89
 
91
90
  directionalLightIntensity: 0,
@@ -151,6 +150,12 @@ export const ENGINE_DEFAULTS = {
151
150
  // only round-trip exactly when both sides agree.
152
151
  export const ALBEDO_EPS = 0.01;
153
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
+
154
159
  export const ASVGF_QUALITY_PRESETS = {
155
160
  // phiColor / phiDepth are RELATIVE tolerances (fractions). Bigger = more
156
161
  // permissive. gradientStrength = 0 keeps the adaptive-α boost off; the
@@ -356,8 +361,8 @@ export const TRIANGLE_DATA_LAYOUT = {
356
361
  // Shared between CPU writers (TextureCreator, MaterialDataManager) and GPU readers (Common.js getMaterial).
357
362
  export const MATERIAL_DATA_LAYOUT = {
358
363
 
359
- SLOTS_PER_MATERIAL: 27, // vec4 slots per material
360
- FLOATS_PER_MATERIAL: 108, // total floats per material (27 × 4)
364
+ SLOTS_PER_MATERIAL: 30, // vec4 slots per material
365
+ FLOATS_PER_MATERIAL: 120, // total floats per material (30 × 4)
361
366
 
362
367
  // ── Flat float offsets (CPU side) ────────────────────────────────
363
368
  // Used as: data[ materialIndex * FLOATS_PER_MATERIAL + offset ]
@@ -399,6 +404,14 @@ export const MATERIAL_DATA_LAYOUT = {
399
404
  BUMP_TRANSFORM: 92,
400
405
  DISPLACEMENT_TRANSFORM: 100,
401
406
 
407
+ // ── Subsurface scattering (3 slots appended after transforms) ────
408
+ // Slot 27: subsurfaceColor.rgb (scatter albedo) + subsurface weight
409
+ SUBSURFACE_COLOR: 108, SUBSURFACE: 111,
410
+ // Slot 28: subsurfaceRadius.rgb (mean free path) + radius scale
411
+ SUBSURFACE_RADIUS: 112, SUBSURFACE_RADIUS_SCALE: 115,
412
+ // Slot 29: anisotropy g (floats 117-119 reserved for future SSS)
413
+ SUBSURFACE_ANISOTROPY: 116,
414
+
402
415
  // ── Vec4 slot indices (GPU/TSL side) ─────────────────────────────
403
416
  // Used with getDatafromStorageBuffer( buf, matIdx, int(slot), int(SLOTS_PER_MATERIAL) )
404
417
  SLOT: {
@@ -422,6 +435,9 @@ export const MATERIAL_DATA_LAYOUT = {
422
435
  EMISSIVE_TRANSFORM_A: 21, EMISSIVE_TRANSFORM_B: 22,
423
436
  BUMP_TRANSFORM_A: 23, BUMP_TRANSFORM_B: 24,
424
437
  DISPLACEMENT_TRANSFORM_A: 25, DISPLACEMENT_TRANSFORM_B: 26,
438
+ SUBSURFACE_A: 27, // subsurfaceColor.rgb, subsurface weight
439
+ SUBSURFACE_B: 28, // subsurfaceRadius.rgb, subsurfaceRadiusScale
440
+ SUBSURFACE_C: 29, // subsurfaceAnisotropy, reserved
425
441
  },
426
442
 
427
443
  };
@@ -434,7 +450,7 @@ export const BVH_LEAF_MARKERS = {
434
450
 
435
451
  // Texture processing constants
436
452
  export const TEXTURE_CONSTANTS = {
437
- PIXELS_PER_MATERIAL: 27,
453
+ PIXELS_PER_MATERIAL: 30,
438
454
  RGBA_COMPONENTS: 4,
439
455
  VEC4_PER_TRIANGLE: 8,
440
456
  VEC4_PER_BVH_NODE: 4,
@@ -454,8 +470,8 @@ export const DEFAULT_TEXTURE_MATRIX = [ 0, 0, 1, 1, 0, 0, 0, 1 ];
454
470
  // 'interactive' — low-sample, bounded bounces, no offline denoising, controls enabled.
455
471
  // 'production' — high-sample, deep bounces, OIDN enabled, controls disabled.
456
472
  export const PRODUCTION_RENDER_CONFIG = {
457
- maxSamples: 30, bounces: 20, transmissiveBounces: 8, samplesPerPixel: 1,
458
- renderMode: 1, enableAlphaShadows: true, tiles: 3, tilesHelper: true,
473
+ maxSamples: 30, bounces: 20, transmissiveBounces: 8, maxSubsurfaceSteps: 64, samplesPerPixel: 1,
474
+ renderMode: 1, enableAlphaShadows: true,
459
475
  enableOIDN: true, oidnQuality: 'balance',
460
476
  interactionModeEnabled: false,
461
477
  };
@@ -464,7 +480,7 @@ export const INTERACTIVE_RENDER_CONFIG = {
464
480
  maxSamples: ENGINE_DEFAULTS.maxSamples, bounces: ENGINE_DEFAULTS.bounces,
465
481
  samplesPerPixel: ENGINE_DEFAULTS.samplesPerPixel, renderMode: ENGINE_DEFAULTS.renderMode, enableAlphaShadows: ENGINE_DEFAULTS.enableAlphaShadows,
466
482
  transmissiveBounces: ENGINE_DEFAULTS.transmissiveBounces,
467
- tiles: ENGINE_DEFAULTS.tiles, tilesHelper: ENGINE_DEFAULTS.tilesHelper,
483
+ maxSubsurfaceSteps: ENGINE_DEFAULTS.maxSubsurfaceSteps,
468
484
  enableOIDN: false, oidnQuality: 'fast',
469
485
  interactionModeEnabled: true,
470
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 );
@@ -922,19 +948,11 @@ export class PathTracerApp extends EventDispatcher {
922
948
  maxBounces: config.bounces,
923
949
  samplesPerPixel: config.samplesPerPixel,
924
950
  transmissiveBounces: config.transmissiveBounces,
951
+ maxSubsurfaceSteps: config.maxSubsurfaceSteps,
925
952
  }, { silent: true } );
926
953
 
927
954
  this.stages.pathTracer?.setUniform( 'renderMode', parseInt( config.renderMode ) );
928
955
  this.stages.pathTracer?.setUniform( 'enableAlphaShadows', config.enableAlphaShadows ?? false );
929
- this.stages.pathTracer?.tileManager?.setTileCount( config.tiles );
930
-
931
- const tileHelper = this.overlayManager?.getHelper( 'tiles' );
932
- if ( tileHelper ) {
933
-
934
- tileHelper.enabled = config.tilesHelper;
935
- if ( ! config.tilesHelper ) tileHelper.hide();
936
-
937
- }
938
956
 
939
957
  this.stages.pathTracer?.updateCompletionThreshold?.();
940
958
 
@@ -957,6 +975,20 @@ export class PathTracerApp extends EventDispatcher {
957
975
 
958
976
  this.needsReset = false;
959
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
+
960
992
  this.reset();
961
993
 
962
994
  }
@@ -1066,6 +1098,69 @@ export class PathTracerApp extends EventDispatcher {
1066
1098
 
1067
1099
  }
1068
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
+
1069
1164
  // ═══════════════════════════════════════════════════════════════
1070
1165
  // Materials (absorbed from MaterialsAPI)
1071
1166
  // ═══════════════════════════════════════════════════════════════
@@ -1206,6 +1301,7 @@ export class PathTracerApp extends EventDispatcher {
1206
1301
  maxBufferSize: adapterLimits.maxBufferSize,
1207
1302
  maxStorageBufferBindingSize: adapterLimits.maxStorageBufferBindingSize,
1208
1303
  maxColorAttachmentBytesPerSample: 128,
1304
+ maxStorageBuffersPerShaderStage: Math.min( adapterLimits.maxStorageBuffersPerShaderStage, 10 ),
1209
1305
  }
1210
1306
  } );
1211
1307
 
@@ -1264,7 +1360,6 @@ export class PathTracerApp extends EventDispatcher {
1264
1360
  this.pipeline.addStage( this.stages.asvgf );
1265
1361
  this.pipeline.addStage( this.stages.variance );
1266
1362
  this.pipeline.addStage( this.stages.bilateralFilter );
1267
- this.pipeline.addStage( this.stages.adaptiveSampling );
1268
1363
  this.pipeline.addStage( this.stages.edgeFilter );
1269
1364
  this.pipeline.addStage( this.stages.autoExposure );
1270
1365
  this.pipeline.addStage( this.stages.compositor );
@@ -1468,9 +1563,6 @@ export class PathTracerApp extends EventDispatcher {
1468
1563
 
1469
1564
  _createStages() {
1470
1565
 
1471
- const adaptiveSamplingMax = this.settings.get( 'adaptiveSamplingMax' );
1472
- const useAdaptiveSampling = this.settings.get( 'useAdaptiveSampling' );
1473
-
1474
1566
  this.stages.pathTracer = new PathTracer( this.renderer, this.scene, this.cameraManager.camera );
1475
1567
  this.stages.normalDepth = new NormalDepth( this.renderer, {
1476
1568
  pathTracer: this.stages.pathTracer
@@ -1482,10 +1574,6 @@ export class PathTracerApp extends EventDispatcher {
1482
1574
  this.stages.asvgf = new ASVGF( this.renderer, { enabled: false } );
1483
1575
  this.stages.variance = new Variance( this.renderer, { enabled: false } );
1484
1576
  this.stages.bilateralFilter = new BilateralFilter( this.renderer, { enabled: false } );
1485
- this.stages.adaptiveSampling = new AdaptiveSampling( this.renderer, {
1486
- adaptiveSamplingMax,
1487
- enabled: useAdaptiveSampling,
1488
- } );
1489
1577
  this.stages.edgeFilter = new EdgeFilter( this.renderer, { enabled: false } );
1490
1578
  this.stages.autoExposure = new AutoExposure( this.renderer, { enabled: DEFAULT_STATE.autoExposure ?? false } );
1491
1579
 
@@ -1504,10 +1592,11 @@ export class PathTracerApp extends EventDispatcher {
1504
1592
  camera: this.cameraManager.camera,
1505
1593
  stages: {
1506
1594
  pathTracer: this.stages.pathTracer,
1595
+ normalDepth: this.stages.normalDepth,
1596
+ motionVector: this.stages.motionVector,
1507
1597
  asvgf: this.stages.asvgf,
1508
1598
  variance: this.stages.variance,
1509
1599
  bilateralFilter: this.stages.bilateralFilter,
1510
- adaptiveSampling: this.stages.adaptiveSampling,
1511
1600
  edgeFilter: this.stages.edgeFilter,
1512
1601
  ssrc: this.stages.ssrc,
1513
1602
  autoExposure: this.stages.autoExposure,
@@ -1522,6 +1611,10 @@ export class PathTracerApp extends EventDispatcher {
1522
1611
  this.denoisingManager.setupDenoiser();
1523
1612
  this.denoisingManager.setupUpscaler();
1524
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
+
1525
1618
  // Set initial render resolution
1526
1619
  const initW = this.canvas.clientWidth || 1;
1527
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
@@ -160,6 +160,7 @@ export class GeometryExtractor {
160
160
  if ( newMaterial.iridescence > 0 ) this.sceneFeatures.hasIridescence = true;
161
161
  if ( newMaterial.sheen > 0 ) this.sceneFeatures.hasSheen = true;
162
162
  if ( newMaterial.transparent || newMaterial.opacity < 1.0 || newMaterial.alphaTest > 0 ) this.sceneFeatures.hasTransparency = true;
163
+ if ( newMaterial.subsurface > 0 ) this.sceneFeatures.hasSubsurface = true;
163
164
 
164
165
  // Detect multi-lobe materials (require multi-lobe MIS for optimal sampling)
165
166
  const featureCount = [
@@ -259,7 +260,13 @@ export class GeometryExtractor {
259
260
  normalScale: { x: 1, y: 1 },
260
261
  bumpScale: 1.0,
261
262
  displacementScale: 1.0,
262
- alphaTest: 0.0
263
+ alphaTest: 0.0,
264
+ // Subsurface scattering (no native MeshPhysicalMaterial equivalent)
265
+ subsurface: 0.0,
266
+ subsurfaceColor: new Color( 0xffffff ),
267
+ subsurfaceRadius: [ 1.0, 0.2, 0.1 ], // skin-like: red travels furthest
268
+ subsurfaceRadiusScale: 1.0,
269
+ subsurfaceAnisotropy: 0.0
263
270
  };
264
271
 
265
272
  }
@@ -390,6 +397,13 @@ export class GeometryExtractor {
390
397
  iridescenceIOR: material.iridescenceIOR ?? defaults.iridescenceIOR,
391
398
  iridescenceThicknessRange: material.iridescenceThicknessRange ?? defaults.iridescenceThicknessRange,
392
399
 
400
+ // Subsurface scattering (custom props; MeshPhysicalMaterial has none)
401
+ subsurface: material.subsurface ?? defaults.subsurface,
402
+ subsurfaceColor: material.subsurfaceColor ?? defaults.subsurfaceColor,
403
+ subsurfaceRadius: material.subsurfaceRadius ?? defaults.subsurfaceRadius,
404
+ subsurfaceRadiusScale: material.subsurfaceRadiusScale ?? defaults.subsurfaceRadiusScale,
405
+ subsurfaceAnisotropy: material.subsurfaceAnisotropy ?? defaults.subsurfaceAnisotropy,
406
+
393
407
  // Specular properties (for compatibility)
394
408
  specularIntensity: legacyMapping.specularIntensity ?? material.specularIntensity ?? defaults.specularIntensity,
395
409
  specularColor: legacyMapping.specularColor ?? material.specularColor ?? defaults.specularColor,
@@ -522,6 +536,10 @@ export class GeometryExtractor {
522
536
  // triangle extraction that stores directly in texture format
523
537
  extractTrianglesInBatch( positions, normals, uvs, indices, triangleCount, materialIndex, meshIndex ) {
524
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
+
525
543
  // Pre-allocate objects for positions, normals, and UVs
526
544
  const posA = this._getVec3( 0 );
527
545
  const posB = this._getVec3( 1 );
@@ -765,6 +783,7 @@ export class GeometryExtractor {
765
783
 
766
784
  // Reset other arrays
767
785
  this.materials = [];
786
+ this.materialTriangleCounts = []; // Per-material triangle count (for sort-bin remap, item 41)
768
787
  this.meshes = [];
769
788
  this.meshTriangleRanges = []; // Per-mesh { start, count } for TLAS/BLAS
770
789
  this.maps = [];
@@ -789,6 +808,7 @@ export class GeometryExtractor {
789
808
  hasIridescence: false,
790
809
  hasSheen: false,
791
810
  hasTransparency: false,
811
+ hasSubsurface: false,
792
812
  hasMultiLobeMaterials: false, // Materials with 2+ BRDF lobes
793
813
  hasMRTOutputs: true // Always enabled for ASVGF/adaptive sampling support
794
814
  };
@@ -801,6 +821,7 @@ export class GeometryExtractor {
801
821
  triangleData: this.getTriangleData(), // Texture-ready Float32Array format
802
822
  triangleCount: this.getTriangleCount(),
803
823
  materials: this.materials,
824
+ materialTriangleCounts: this.materialTriangleCounts,
804
825
  meshes: this.meshes,
805
826
  meshTriangleRanges: this.meshTriangleRanges, // Per-mesh { start, count } for TLAS/BLAS
806
827
  maps: this.maps,