rayzee 7.17.0 → 7.19.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 (47) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/BVHSubtreeWorker-D7KMbrQ-.js +2 -0
  3. package/dist/assets/BVHSubtreeWorker-D7KMbrQ-.js.map +1 -0
  4. package/dist/assets/BVHWorker-DOaGn6uN.js +2 -0
  5. package/dist/assets/BVHWorker-DOaGn6uN.js.map +1 -0
  6. package/dist/rayzee.es.js +2388 -2138
  7. package/dist/rayzee.es.js.map +1 -1
  8. package/dist/rayzee.umd.js +49 -49
  9. package/dist/rayzee.umd.js.map +1 -1
  10. package/package.json +1 -1
  11. package/src/EngineDefaults.js +7 -6
  12. package/src/Passes/OIDNDenoiser.js +17 -14
  13. package/src/PathTracerApp.js +63 -56
  14. package/src/Processor/BVHBuilder.js +11 -7
  15. package/src/Processor/BuildTimer.js +20 -6
  16. package/src/Processor/EmissiveTriangleBuilder.js +11 -7
  17. package/src/Processor/GeometryExtractor.js +11 -5
  18. package/src/Processor/KernelManager.js +48 -1
  19. package/src/Processor/PackedRayBuffer.js +20 -12
  20. package/src/Processor/ParallelBVHBuilder.js +9 -6
  21. package/src/Processor/QueueManager.js +6 -8
  22. package/src/Processor/SceneProcessor.js +47 -21
  23. package/src/Processor/ShaderBuilder.js +4 -1
  24. package/src/Processor/StorageTexturePool.js +4 -1
  25. package/src/Processor/Workers/BVHWorker.js +12 -6
  26. package/src/Processor/utils.js +30 -0
  27. package/src/RenderSettings.js +1 -1
  28. package/src/SceneHelpers.js +11 -1
  29. package/src/Stages/PathTracer.js +15 -4
  30. package/src/Stages/PathTracerStage.js +17 -14
  31. package/src/TSL/FinalWriteKernel.js +11 -6
  32. package/src/TSL/ShadeKernel.js +46 -24
  33. package/src/TSL/TextureSampling.js +12 -2
  34. package/src/index.js +3 -0
  35. package/src/managers/EnvironmentManager.js +12 -8
  36. package/src/managers/MaterialDataManager.js +12 -9
  37. package/src/managers/OverlayManager.js +105 -48
  38. package/src/managers/UniformManager.js +1 -1
  39. package/src/managers/helpers/OutlineHelper.js +42 -40
  40. package/src/managers/helpers/TransformGizmoHelper.js +61 -0
  41. package/src/managers/helpers/ViewOverlayRenderer.js +140 -0
  42. package/src/managers/helpers/overlaySurface.js +53 -0
  43. package/src/utils/Logger.js +291 -0
  44. package/dist/assets/BVHSubtreeWorker-sNzvxn66.js +0 -2
  45. package/dist/assets/BVHSubtreeWorker-sNzvxn66.js.map +0 -1
  46. package/dist/assets/BVHWorker-CiVdFrwe.js +0 -2
  47. package/dist/assets/BVHWorker-CiVdFrwe.js.map +0 -1
@@ -1,5 +1,8 @@
1
1
  import { Vector3, Vector2, Color, Matrix3, Matrix4, FrontSide, BackSide, DoubleSide, RGBAFormat } from "three";
2
2
  import { TRIANGLE_DATA_LAYOUT } from '../EngineDefaults.js';
3
+ import { createLogger, fmt, warnOnce } from '../utils/Logger.js';
4
+
5
+ const log = createLogger( 'geometry' );
3
6
 
4
7
  const MAX_TEXTURES_LIMIT = 128;
5
8
 
@@ -169,7 +172,8 @@ export class GeometryExtractor {
169
172
  if ( material.depthWrite === false ) {
170
173
 
171
174
  material.depthWrite = true;
172
- console.warn( "Depth write is disabled in material, enabling it for rastered rendering" );
175
+ // Fires per offending material; the fix is identical every time, so warn once.
176
+ warnOnce( log, 'depthWrite', 'material had depthWrite disabled — enabled for rastered rendering' );
173
177
 
174
178
  }
175
179
 
@@ -758,10 +762,12 @@ export class GeometryExtractor {
758
762
  logStats() {
759
763
 
760
764
  const usedBytes = this.currentTriangleIndex * TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE * 4;
761
- console.log( "materials:", this.materials.length );
762
- console.log( "triangles:", this.currentTriangleIndex );
763
- console.log( "triangle data size (MB):", ( usedBytes / ( 1024 * 1024 ) ).toFixed( 2 ) );
764
- console.log( "maps:", this.maps.length );
765
+
766
+ log.debug( fmt.list( [
767
+ `${fmt.count( this.currentTriangleIndex, 'tri' )} (${fmt.mb( usedBytes )})`,
768
+ fmt.count( this.materials.length, 'material' ),
769
+ fmt.count( this.maps.length, 'map' ),
770
+ ] ) );
765
771
 
766
772
  }
767
773
 
@@ -8,6 +8,10 @@
8
8
  * dispatch interface that wraps `renderer.compute(node)`.
9
9
  */
10
10
 
11
+ import { createLogger, fmt } from '../utils/Logger.js';
12
+
13
+ const log = createLogger( 'wavefront' );
14
+
11
15
  /** Default workgroup sizes per kernel type */
12
16
  const WORKGROUP_SIZES = {
13
17
  generate: [ 16, 16, 1 ], // 2D screen-space
@@ -62,6 +66,14 @@ export class KernelManager {
62
66
  */
63
67
  this.profile = new Map();
64
68
 
69
+ /**
70
+ * First-dispatch (shader compilation) timings, buffered so a build reports
71
+ * one line instead of one per kernel. See _recordFirstDispatch.
72
+ * @type {Array<{name: string, ms: number}>}
73
+ */
74
+ this._firstDispatches = [];
75
+ this._firstDispatchFlush = null;
76
+
65
77
  // Initialize workgroup sizes from defaults
66
78
  for ( const [ name, wgSize ] of Object.entries( WORKGROUP_SIZES ) ) {
67
79
 
@@ -71,6 +83,38 @@ export class KernelManager {
71
83
 
72
84
  }
73
85
 
86
+ /**
87
+ * Buffers a kernel's first-dispatch cost and flushes the batch on the next macrotask,
88
+ * so the kernels compiled during one frame collapse into a single summary line.
89
+ */
90
+ _recordFirstDispatch( name, ms ) {
91
+
92
+ this._firstDispatches.push( { name, ms } );
93
+
94
+ clearTimeout( this._firstDispatchFlush );
95
+ this._firstDispatchFlush = setTimeout( () => this._flushFirstDispatches(), 0 );
96
+
97
+ }
98
+
99
+ _flushFirstDispatches() {
100
+
101
+ const batch = this._firstDispatches;
102
+ this._firstDispatches = [];
103
+ this._firstDispatchFlush = null;
104
+ if ( batch.length === 0 ) return;
105
+
106
+ const total = batch.reduce( ( sum, k ) => sum + k.ms, 0 );
107
+ const slowest = [ ...batch ].sort( ( a, b ) => b.ms - a.ms );
108
+
109
+ log.info( fmt.list( [
110
+ `${batch.length} kernel${batch.length === 1 ? '' : 's'} compiled in ${fmt.ms( total )}`,
111
+ `slowest ${slowest.slice( 0, 2 ).map( k => `${k.name} ${fmt.ms( k.ms )}` ).join( ', ' )}`,
112
+ ] ) );
113
+
114
+ batch.forEach( k => log.debug( `kernel '${k.name}' first dispatch (incl. compilation) ${fmt.ms( k.ms )}` ) );
115
+
116
+ }
117
+
74
118
  /**
75
119
  * Register a pre-built compute node.
76
120
  * @param {string} name - Kernel name (e.g. 'generate', 'extend')
@@ -106,7 +150,7 @@ export class KernelManager {
106
150
  const t1 = performance.now();
107
151
  timingEntry.compiledOnce = true;
108
152
  timingEntry.lastDispatchMs = t1 - t0;
109
- console.log( `[Wavefront] Kernel '${name}' first dispatch (includes compilation): ${( t1 - t0 ).toFixed( 1 )}ms` );
153
+ this._recordFirstDispatch( name, t1 - t0 );
110
154
 
111
155
  } else if ( this.profiling ) {
112
156
 
@@ -266,6 +310,9 @@ export class KernelManager {
266
310
 
267
311
  dispose() {
268
312
 
313
+ clearTimeout( this._firstDispatchFlush );
314
+ this._firstDispatchFlush = null;
315
+ this._firstDispatches = [];
269
316
  this.kernels.clear();
270
317
  this.timing.clear();
271
318
  this.profile.clear();
@@ -8,6 +8,9 @@ import {
8
8
  packSnorm2x16, packUnorm2x16, unpackSnorm2x16, unpackUnorm2x16,
9
9
  } from 'three/tsl';
10
10
  import { StorageInstancedBufferAttribute } from 'three/webgpu';
11
+ import { createLogger, fmt } from '../utils/Logger.js';
12
+
13
+ const log = createLogger( 'gpu' );
11
14
 
12
15
  export const RAY_STRIDE = 7;
13
16
  export const HIT_STRIDE = 2;
@@ -19,7 +22,7 @@ export const HIT_STRIDE = 2;
19
22
  export const GBUFFER_STRIDE = 1;
20
23
 
21
24
  export const RAY = {
22
- ORIGIN_META: 0, // vec4(origin.xyz, uintBitsToFloat(perRayBounces | sssSteps<<8)); pixelIndex == rayID
25
+ ORIGIN_META: 0, // vec4(origin.xyz, uintBitsToFloat(perRayBounces | sssSteps<<8 | lobeCounts<<16)); pixelIndex == rayID
23
26
  DIR_FLAGS: 1, // vec4(direction.xyz, uintBitsToFloat(bounceFlags))
24
27
  THROUGHPUT_PDF: 2, // vec4(throughput.xyz, pdf)
25
28
  RADIANCE_ALPHA: 3, // vec4(radiance.xyz, alpha)
@@ -100,14 +103,12 @@ export class PackedRayBuffer {
100
103
  ro: storage( hitAttr, 'vec4' ).toReadOnly(),
101
104
  };
102
105
 
103
- const totalMB = (
104
- rayCount * 16 + capacity * 4 + hitCount * 16
105
- ) / ( 1024 * 1024 );
106
+ // Kept for the [gpu] startup summary in PathTracer._buildWavefrontKernels.
107
+ this.rayBytes = rayCount * 16 + capacity * 4;
108
+ this.hitBytes = hitCount * 16;
109
+ this.totalBytes = this.rayBytes + this.hitBytes;
106
110
 
107
- console.log(
108
- `PackedRayBuffer: capacity=${capacity}, total=${totalMB.toFixed( 1 )} MB ` +
109
- `(ray=${( rayCount * 16 / 1048576 ).toFixed( 0 )}MB hit=${( hitCount * 16 / 1048576 ).toFixed( 0 )}MB) [SoA ray/hit]`
110
- );
111
+ log.debug( `ray pool capacity ${fmt.n( capacity )} · ${fmt.mb( this.totalBytes )} (rays ${fmt.mb( this.rayBytes )} · hits ${fmt.mb( this.hitBytes )})` );
111
112
 
112
113
  }
113
114
 
@@ -182,12 +183,15 @@ export const gbDecodeNormalDepth = ( packed ) => {
182
183
  export const gbDecodeAlbedo = ( packed ) =>
183
184
  vec3( unpackUnorm2x16( packed.z ), unpackUnorm2x16( packed.w ).x );
184
185
 
185
- // .w packs per-ray bounce state: perRayBounces (bits 0-7) | sssSteps (bits 8-15). pixelIndex is
186
- // NOT stored — it equals rayID (one ray per pixel).
187
- export const writeRayOriginMeta = ( buf, id, origin, bounces, sssSteps ) =>
186
+ // .w packs per-ray bounce state: perRayBounces (bits 0-7) | sssSteps (bits 8-15) | transparentCount
187
+ // (bits 16-21), each masked so an overrun can't bleed into its neighbour. pixelIndex is NOT stored —
188
+ // it equals rayID (one ray per pixel).
189
+ export const writeRayOriginMeta = ( buf, id, origin, bounces, sssSteps, transparentCount = uint( 0 ) ) =>
188
190
  buf.element( soa( id, RAY.ORIGIN_META ) )
189
191
  .assign( vec4( origin, uintBitsToFloat(
190
- uint( bounces ).bitOr( uint( sssSteps ).shiftLeft( 8 ) )
192
+ uint( bounces ).bitAnd( uint( 0xFF ) )
193
+ .bitOr( uint( sssSteps ).bitAnd( uint( 0xFF ) ).shiftLeft( 8 ) )
194
+ .bitOr( uint( transparentCount ).bitAnd( uint( 0x3F ) ).shiftLeft( 16 ) )
191
195
  ) ) );
192
196
 
193
197
  export const writeRayDirFlags = ( buf, id, direction, bounceFlags ) =>
@@ -293,11 +297,15 @@ export const writeFeatureThroughput = ( buf, id, tp ) => {
293
297
  // Per-ray bounce state packed into ORIGIN_META.w (written by writeRayOriginMeta alongside the origin):
294
298
  // perRayBounces = bits 0-7 (camera-bounce depth; the loop index can't track it once free bounces decouple it)
295
299
  // sssSteps = bits 8-15 (SSS random-walk step counter)
300
+ // transparentCount = bits 16-21 (alpha-passthrough counter, max 63; see maxTransparentBounces)
296
301
  export const readPathBounces = ( buf, id ) =>
297
302
  int( floatBitsToUint( buf.element( soa( id, RAY.ORIGIN_META ) ).w ).bitAnd( 0xFF ) );
298
303
  export const readSssSteps = ( buf, id ) =>
299
304
  int( floatBitsToUint( buf.element( soa( id, RAY.ORIGIN_META ) ).w ).shiftRight( 8 ).bitAnd( 0xFF ) );
300
305
 
306
+ export const readTransparentCount = ( buf, id ) =>
307
+ int( floatBitsToUint( buf.element( soa( id, RAY.ORIGIN_META ) ).w ).shiftRight( 16 ).bitAnd( uint( 0x3F ) ) );
308
+
301
309
  // Region 9: SSS sigmaS + Henyey-Greenstein g. sigmaS==0 marks glass (Beer-Lambert path, not random walk).
302
310
  export const readSSSMedium = ( buf, id ) => {
303
311
 
@@ -10,6 +10,9 @@
10
10
  import { fetchAsWorker } from './Workers/fetchAsWorker.js';
11
11
  import BVH_WORKER_URL from './Workers/BVHWorker.js?worker&url';
12
12
  import BVH_SUBTREE_WORKER_URL from './Workers/BVHSubtreeWorker.js?worker&url';
13
+ import { createLogger, fmt } from '../utils/Logger.js';
14
+
15
+ const log = createLogger( 'bvh' );
13
16
 
14
17
  const FPT = 32; // FLOATS_PER_TRIANGLE
15
18
  const PARALLEL_THRESHOLD = 50000;
@@ -34,7 +37,7 @@ export function buildBVHParallel( triangles, depth, progressCallback, config ) {
34
37
  const numWorkers = Math.min( navigator.hardwareConcurrency || 4, MAX_PARALLEL_WORKERS );
35
38
  const parallelDepth = Math.ceil( Math.log2( numWorkers * 2.5 + 1 ) );
36
39
 
37
- console.log( `[ParallelBVH] Parallel build: ${triangleCount.toLocaleString()} triangles, ${numWorkers} workers, parallelDepth=${parallelDepth}` );
40
+ log.debug( `parallel build ${fmt.n( triangleCount )} triangles · ${numWorkers} workers · parallelDepth ${parallelDepth}` );
38
41
 
39
42
  return new Promise( ( resolve, reject ) => {
40
43
 
@@ -98,7 +101,7 @@ export function buildBVHParallel( triangles, depth, progressCallback, config ) {
98
101
 
99
102
  if ( settled ) return;
100
103
  settled = true;
101
- console.warn( `[ParallelBVH] Parallel build failed (${reason}), falling back to single worker` );
104
+ log.warn( `parallel build failed (${reason}), falling back to single worker` );
102
105
  cleanup();
103
106
  // Copy from SharedArrayBuffer to regular ArrayBuffer for transfer
104
107
  const restoredBuffer = new ArrayBuffer( sharedTriangleData.byteLength );
@@ -182,7 +185,7 @@ export function buildBVHParallel( triangles, depth, progressCallback, config ) {
182
185
 
183
186
  } )().catch( ( error ) => {
184
187
 
185
- console.warn( '[ParallelBVH] Parallel build setup failed:', error );
188
+ log.warn( 'parallel build setup failed:', error );
186
189
  reject( error );
187
190
 
188
191
  } );
@@ -209,7 +212,7 @@ async function handlePhase2(
209
212
 
210
213
  // No frontier tasks — top-level tree is the complete BVH
211
214
  // Still need to reorder triangles via coordinator
212
- console.log( '[ParallelBVH] No frontier tasks, assembling with top-level tree only' );
215
+ log.debug( 'no frontier tasks, assembling with top-level tree only' );
213
216
  coordinatorWorker.postMessage( {
214
217
  type: 'assemble',
215
218
  topFlatData,
@@ -225,7 +228,7 @@ async function handlePhase2(
225
228
 
226
229
  }
227
230
 
228
- console.log( `[ParallelBVH] Phase 2: distributing ${frontierTasks.length} tasks across ${numWorkers} workers` );
231
+ log.debug( `phase 2 distributing ${frontierTasks.length} tasks across ${numWorkers} workers` );
229
232
 
230
233
  // Distribute tasks using greedy least-loaded assignment
231
234
  const sortedTasks = [ ...frontierTasks ].sort( ( a, b ) => ( b.end - b.start ) - ( a.end - a.start ) );
@@ -493,7 +496,7 @@ function buildSingleWorker( triangles, depth, progressCallback, config ) {
493
496
 
494
497
  } )().catch( ( error ) => {
495
498
 
496
- console.warn( '[ParallelBVH] Single worker fallback failed:', error );
499
+ log.warn( 'single worker fallback failed:', error );
497
500
  reject( error );
498
501
 
499
502
  } );
@@ -5,6 +5,9 @@
5
5
  import { storage } from 'three/tsl';
6
6
  import { StorageInstancedBufferAttribute } from 'three/webgpu';
7
7
  import { freeStorageAttribute } from './PackedRayBuffer.js';
8
+ import { createLogger, fmt } from '../utils/Logger.js';
9
+
10
+ const log = createLogger( 'gpu' );
8
11
 
9
12
  /** Counter indices — must match ResetCounters kernel */
10
13
  export const COUNTER = {
@@ -109,15 +112,10 @@ export class QueueManager {
109
112
 
110
113
  this.pingPong = 0;
111
114
 
112
- const totalBytes = (
113
- COUNTER.COUNT * 4 +
114
- capacity * 4 * 3
115
- );
115
+ // Kept for the [gpu] startup summary in PathTracer._buildWavefrontKernels.
116
+ this.totalBytes = COUNTER.COUNT * 4 + capacity * 4 * 3;
116
117
 
117
- console.log(
118
- `QueueManager: Allocated capacity=${capacity}, ` +
119
- `total=${( totalBytes / ( 1024 * 1024 ) ).toFixed( 1 )} MB`
120
- );
118
+ log.debug( `queues capacity ${fmt.n( capacity )} · ${fmt.mb( this.totalBytes )}` );
121
119
 
122
120
  }
123
121
 
@@ -9,12 +9,15 @@ import { GeometryExtractor } from './GeometryExtractor.js';
9
9
  import { EmissiveTriangleBuilder } from './EmissiveTriangleBuilder.js';
10
10
  import { updateLoading } from '../Processor/utils.js';
11
11
  import { BuildTimer } from './BuildTimer.js';
12
+ import { createLogger, fmt, workerLogLevel } from '../utils/Logger.js';
12
13
  import { SRGBColorSpace } from 'three';
13
14
  import { TRIANGLE_DATA_LAYOUT, TEXTURE_CONSTANTS, getTextureBucketId, packTextureIndex } from '../EngineDefaults.js';
14
15
  import { fetchAsWorker } from './Workers/fetchAsWorker.js';
15
16
  import BVH_WORKER_URL from './Workers/BVHWorker.js?worker&url';
16
17
  import BVH_REFIT_WORKER_URL from './Workers/BVHRefitWorker.js?worker&url';
17
18
 
19
+ const log = createLogger( 'scene' );
20
+
18
21
  /**
19
22
  * SceneProcessor - Processes scene geometry into GPU-ready data:
20
23
  * BVH acceleration, texture atlas, material buffers.
@@ -170,11 +173,9 @@ export class SceneProcessor {
170
173
  */
171
174
  _log( message, data ) {
172
175
 
173
- if ( this.config.verbose ) {
174
-
175
- console.log( `[SceneProcessor] ${message}`, data || '' );
176
-
177
- }
176
+ // Visibility is the log level's job now; config.verbose no longer gates this.
177
+ if ( data !== undefined ) log.debug( message, data );
178
+ else log.debug( message );
178
179
 
179
180
  }
180
181
 
@@ -194,7 +195,7 @@ export class SceneProcessor {
194
195
  this.isProcessing = true;
195
196
  this.processingStage = 'init';
196
197
 
197
- const timer = new BuildTimer( `SceneProcessor (${object.name || 'scene'})` );
198
+ const timer = new BuildTimer( object.name ?? '', { namespace: 'scene' } );
198
199
 
199
200
  try {
200
201
 
@@ -273,7 +274,7 @@ export class SceneProcessor {
273
274
  } catch ( error ) {
274
275
 
275
276
  this.processingStage = 'error';
276
- console.error( '[SceneProcessor] Processing error:', error );
277
+ log.error( 'processing failed:', error );
277
278
  updateLoading( {
278
279
  status: `Error: ${error.message}`,
279
280
  progress: 100
@@ -357,7 +358,7 @@ export class SceneProcessor {
357
358
 
358
359
  } catch ( error ) {
359
360
 
360
- console.error( '[SceneProcessor] Geometry extraction error:', error );
361
+ log.error( 'geometry extraction failed:', error );
361
362
  updateLoading( {
362
363
  status: `Extraction error: ${error.message}`,
363
364
  progress: 25
@@ -449,6 +450,7 @@ export class SceneProcessor {
449
450
  numBins: this.bvhBuilder.numBins,
450
451
  maxBins: this.bvhBuilder.maxBins,
451
452
  minBins: this.bvhBuilder.minBins,
453
+ logLevel: workerLogLevel(),
452
454
  };
453
455
 
454
456
  const totalTasks = poolTasks.length + parallelTasks.length;
@@ -487,7 +489,9 @@ export class SceneProcessor {
487
489
  Promise.all( parallelPromises )
488
490
  ] );
489
491
 
490
- // Store all results
492
+ // Store all results, summing per-mesh split stats for one aggregate BVH line
493
+ const blasStats = { sah: 0, objMed: 0, spatMed: 0, failed: 0, treeletsImproved: 0, treeletsProcessed: 0 };
494
+
491
495
  for ( const { m, range, result } of [ ...poolResults, ...parallelResults ] ) {
492
496
 
493
497
  if ( result.reorderedTriangles ) {
@@ -496,6 +500,18 @@ export class SceneProcessor {
496
500
 
497
501
  }
498
502
 
503
+ const st = result.splitStats;
504
+ if ( st ) {
505
+
506
+ blasStats.sah += st.sahSplits ?? 0;
507
+ blasStats.objMed += st.objectMedianSplits ?? 0;
508
+ blasStats.spatMed += st.spatialMedianSplits ?? 0;
509
+ blasStats.failed += st.failedSplits ?? 0;
510
+ blasStats.treeletsImproved += st.treeletsImproved ?? 0;
511
+ blasStats.treeletsProcessed += st.treeletsProcessed ?? 0;
512
+
513
+ }
514
+
499
515
  this.instanceTable.setEntry( {
500
516
  meshIndex: m,
501
517
  blasNodeCount: result.bvhData.length / 16,
@@ -552,7 +568,15 @@ export class SceneProcessor {
552
568
  this._disposeRefitWorker();
553
569
 
554
570
  const duration = performance.now() - startTime;
555
- this._log( `BVH complete: ${validEntries.length} mesh(es), ${this.bvhData.length / 16} nodes (${duration.toFixed( 2 )}ms)` );
571
+ // One aggregate line for the whole two-level build; the workers' per-mesh
572
+ // detail sits a level below at `debug`.
573
+ log.debug( fmt.list( [
574
+ `${fmt.n( validEntries.length )} BLASes + TLAS`,
575
+ `${fmt.n( this.bvhData.length / 16 )} nodes`,
576
+ `SAH ${fmt.n( blasStats.sah )} · objMed ${blasStats.objMed} · spatMed ${blasStats.spatMed} · failed ${blasStats.failed}`,
577
+ blasStats.treeletsProcessed ? `treelets ${blasStats.treeletsImproved}/${blasStats.treeletsProcessed} improved` : null,
578
+ fmt.ms( duration ),
579
+ ] ) );
556
580
 
557
581
  updateLoading( {
558
582
  status: "BVH construction complete",
@@ -561,7 +585,7 @@ export class SceneProcessor {
561
585
 
562
586
  } catch ( error ) {
563
587
 
564
- console.error( '[SceneProcessor] BVH building error:', error );
588
+ log.error( 'BVH build failed:', error );
565
589
  updateLoading( {
566
590
  status: `BVH error: ${error.message}`,
567
591
  progress: 75
@@ -660,6 +684,7 @@ export class SceneProcessor {
660
684
  numBins: opts.numBins,
661
685
  maxBins: opts.maxBins,
662
686
  minBins: opts.minBins,
687
+ logLevel: opts.logLevel,
663
688
  }, [ meshTriData.buffer ] );
664
689
 
665
690
  };
@@ -686,6 +711,7 @@ export class SceneProcessor {
686
711
  bvhData: data.bvhData,
687
712
  reorderedTriangles: data.triangles || null,
688
713
  originalToBvh: data.originalToBvh || null,
714
+ splitStats: data.treeletStats || null,
689
715
  }
690
716
  } );
691
717
 
@@ -835,7 +861,7 @@ export class SceneProcessor {
835
861
 
836
862
  } catch ( error ) {
837
863
 
838
- console.error( '[SceneProcessor] Texture creation error:', error );
864
+ log.error( 'texture creation failed:', error );
839
865
  throw error;
840
866
 
841
867
  }
@@ -877,7 +903,7 @@ export class SceneProcessor {
877
903
  if ( seen !== undefined ) return packTextureIndex( bucket, seen );
878
904
  if ( lists[ bucket ].length >= STRIDE ) {
879
905
 
880
- console.warn( `[SceneProcessor] Texture bucket ${bucket} full (${STRIDE}); dropping a map.` );
906
+ log.warn( `texture bucket ${bucket} full (${STRIDE}); dropping a map` );
881
907
  return - 1;
882
908
 
883
909
  }
@@ -1089,7 +1115,7 @@ export class SceneProcessor {
1089
1115
 
1090
1116
  } catch ( error ) {
1091
1117
 
1092
- console.warn( `[SceneProcessor] Error disposing ${prop}:`, error );
1118
+ log.warn( `error disposing ${prop}:`, error );
1093
1119
 
1094
1120
  }
1095
1121
 
@@ -1157,7 +1183,7 @@ export class SceneProcessor {
1157
1183
 
1158
1184
  } catch ( error ) {
1159
1185
 
1160
- console.error( '[SceneProcessor] Material rebuild error:', error );
1186
+ log.error( 'material rebuild failed:', error );
1161
1187
  throw error;
1162
1188
 
1163
1189
  } finally {
@@ -1492,7 +1518,7 @@ export class SceneProcessor {
1492
1518
 
1493
1519
  if ( ! this.triangleData ) {
1494
1520
 
1495
- console.error( 'SceneProcessor: Failed to get triangle data' );
1521
+ log.error( 'failed to get triangle data' );
1496
1522
  return false;
1497
1523
 
1498
1524
  }
@@ -1501,7 +1527,7 @@ export class SceneProcessor {
1501
1527
 
1502
1528
  if ( ! this.bvhData ) {
1503
1529
 
1504
- console.error( 'SceneProcessor: Failed to get BVH data' );
1530
+ log.error( 'failed to get BVH data' );
1505
1531
  return false;
1506
1532
 
1507
1533
  }
@@ -1515,7 +1541,7 @@ export class SceneProcessor {
1515
1541
 
1516
1542
  } else {
1517
1543
 
1518
- console.warn( 'SceneProcessor: No material data, using defaults' );
1544
+ log.warn( 'no material data, using defaults' );
1519
1545
 
1520
1546
  }
1521
1547
 
@@ -1862,7 +1888,7 @@ export class SceneProcessor {
1862
1888
 
1863
1889
  if ( data.error ) {
1864
1890
 
1865
- console.error( `Background BLAS rebuild error (mesh ${meshIdx}):`, data.error );
1891
+ log.error( `background BLAS rebuild failed (mesh ${meshIdx}):`, data.error );
1866
1892
  return;
1867
1893
 
1868
1894
  }
@@ -1876,7 +1902,7 @@ export class SceneProcessor {
1876
1902
 
1877
1903
  worker.onerror = ( err ) => {
1878
1904
 
1879
- console.error( `Background BLAS rebuild worker error (mesh ${meshIdx}):`, err );
1905
+ log.error( `background BLAS rebuild worker failed (mesh ${meshIdx}):`, err );
1880
1906
  worker.terminate();
1881
1907
  this._pendingRebuilds.delete( meshIdx );
1882
1908
 
@@ -1948,7 +1974,7 @@ export class SceneProcessor {
1948
1974
  // for the same triangle set. If it differs, the buffer layout is invalid.
1949
1975
  if ( newNodeCount !== entry.blasNodeCount ) {
1950
1976
 
1951
- console.warn( `Background rebuild: node count mismatch for mesh ${meshIdx} (${newNodeCount} vs ${entry.blasNodeCount}), skipping swap` );
1977
+ log.warn( `background rebuild node count mismatch for mesh ${meshIdx} (${newNodeCount} vs ${entry.blasNodeCount}), skipping swap` );
1952
1978
  return;
1953
1979
 
1954
1980
  }
@@ -11,6 +11,9 @@ import { texture } from 'three/tsl';
11
11
  import { LinearFilter, DataArrayTexture } from 'three';
12
12
  import { setAlphaShadowsUniform } from '../TSL/LightsDirect.js';
13
13
  import { setGoboMapsTexture, setIESProfilesTexture } from '../TSL/LightsCore.js';
14
+ import { createLogger } from '../utils/Logger.js';
15
+
16
+ const log = createLogger( 'shader' );
14
17
 
15
18
  export class ShaderBuilder {
16
19
 
@@ -43,7 +46,7 @@ export class ShaderBuilder {
43
46
  if ( stage.goboMaps && nodes.goboMapsTex ) nodes.goboMapsTex.value = stage.goboMaps;
44
47
  if ( stage.iesProfiles && nodes.iesProfilesTex ) nodes.iesProfilesTex.value = stage.iesProfiles;
45
48
 
46
- console.log( 'ShaderBuilder: Scene textures updated in-place' );
49
+ log.debug( 'scene textures updated in-place' );
47
50
 
48
51
  }
49
52
 
@@ -15,6 +15,9 @@
15
15
  import { StorageTexture, RenderTarget } from 'three/webgpu';
16
16
  import { RGBAFormat, FloatType, LinearFilter, NearestFilter, Box2, Vector2 } from 'three';
17
17
  import { MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
18
+ import { createLogger, fmt } from '../utils/Logger.js';
19
+
20
+ const log = createLogger( 'gpu' );
18
21
 
19
22
  function createWriteStorageTex() {
20
23
 
@@ -83,7 +86,7 @@ export class StorageTexturePool {
83
86
  this.readTarget.textures[ 1 ].name = 'gNormalDepth';
84
87
  this.readTarget.textures[ 2 ].name = 'gAlbedo';
85
88
 
86
- console.log( `StorageTexturePool: Created ${width}x${height} (3 write StorageTextures + 1 MRT RenderTarget)` );
89
+ log.debug( `storage texture pool ${fmt.px( width, height )} — 3 write StorageTextures + 1 MRT RenderTarget` );
87
90
 
88
91
  }
89
92
 
@@ -1,4 +1,7 @@
1
1
  import { BVHBuilder } from '../BVHBuilder.js';
2
+ import { createLogger, fmt, applyWorkerLogLevel } from '../../utils/Logger.js';
3
+
4
+ const log = createLogger( 'bvh' );
2
5
 
3
6
  const FPT = 32; // FLOATS_PER_TRIANGLE
4
7
 
@@ -9,6 +12,9 @@ self.onmessage = function ( e ) {
9
12
  const data = e.data;
10
13
  const type = data.type;
11
14
 
15
+ // Workers get their own globals, so the main thread forwards its level per message.
16
+ applyWorkerLogLevel( data.logLevel );
17
+
12
18
  if ( type === 'buildPhase1' ) {
13
19
 
14
20
  handlePhase1( data );
@@ -100,7 +106,7 @@ function handlePhase1( data ) {
100
106
  const flattenTime = performance.now() - flattenStart;
101
107
 
102
108
  const totalTime = performance.now() - startTime;
103
- console.log( `[BVHWorker] Phase 1: ${Math.round( totalTime )}ms (init: ${Math.round( builder.splitStats.initTime )}ms, morton: ${Math.round( builder.splitStats.mortonSortTime )}ms, SAH: ${Math.round( builder.splitStats.sahBuildTime )}ms, flatten: ${Math.round( flattenTime )}ms), ${builder.frontierTasks.length} frontier tasks` );
109
+ log.debug( `phase 1 ${fmt.ms( totalTime )} (init ${fmt.ms( builder.splitStats.initTime )} · morton ${fmt.ms( builder.splitStats.mortonSortTime )} · SAH ${fmt.ms( builder.splitStats.sahBuildTime )} · flatten ${fmt.ms( flattenTime )}) · ${builder.frontierTasks.length} frontier tasks` );
104
110
 
105
111
  self.postMessage( {
106
112
  type: 'phase1Result',
@@ -113,7 +119,7 @@ function handlePhase1( data ) {
113
119
 
114
120
  } catch ( error ) {
115
121
 
116
- console.error( '[BVHWorker] Phase 1 error:', error );
122
+ log.error( 'phase 1 failed:', error );
117
123
  self.postMessage( { type: 'error', error: error.message } );
118
124
 
119
125
  }
@@ -162,7 +168,7 @@ function handleAssemble( data ) {
162
168
  }
163
169
 
164
170
  const totalTime = performance.now() - startTime;
165
- console.log( `[BVHWorker] Phase 3 (assemble + reorder): ${Math.round( totalTime )}ms (${( bvhData.byteLength / 1024 / 1024 ).toFixed( 1 )}MB BVH)` );
171
+ log.debug( `phase 3 assemble + reorder ${fmt.ms( totalTime )} (${fmt.mb( bvhData.byteLength )} BVH)` );
166
172
 
167
173
  self.postMessage( {
168
174
  type: 'assembleResult',
@@ -173,7 +179,7 @@ function handleAssemble( data ) {
173
179
 
174
180
  } catch ( error ) {
175
181
 
176
- console.error( '[BVHWorker] Assembly error:', error );
182
+ log.error( 'assembly failed:', error );
177
183
  self.postMessage( { type: 'error', error: error.message } );
178
184
 
179
185
  }
@@ -225,7 +231,7 @@ function handleFullBuild( data ) {
225
231
  const flattenStart = performance.now();
226
232
  const bvhData = builder.flattenBVH( bvhRoot );
227
233
  const flattenTime = performance.now() - flattenStart;
228
- console.log( `[BVHWorker] Flatten BVH: ${Math.round( flattenTime )}ms (${( bvhData.byteLength / 1024 / 1024 ).toFixed( 1 )}MB)` );
234
+ log.debug( `flatten ${fmt.ms( flattenTime )} (${fmt.mb( bvhData.byteLength )})` );
229
235
 
230
236
  const originalToBvh = builder.originalToBvhMap || null;
231
237
 
@@ -261,7 +267,7 @@ function handleFullBuild( data ) {
261
267
 
262
268
  } catch ( error ) {
263
269
 
264
- console.error( '[BVHWorker] Error:', error );
270
+ log.error( 'build failed:', error );
265
271
  self.postMessage( { error: error.message } );
266
272
 
267
273
  }
@@ -102,6 +102,36 @@ export function distroyBuffers( { material, geometry, children } ) {
102
102
 
103
103
  }
104
104
 
105
+ /**
106
+ * Disposes a renderer without leaking it.
107
+ *
108
+ * Three.js 0.184–0.185: `Renderer.dispose()` does not remove the 'resize' listener
109
+ * it installs on `_canvasTarget`. The bound handler closes over the renderer,
110
+ * pinning the entire WebGPU graph (Backend, Nodes, Bindings, Pipelines, GPUDevice,
111
+ * every TSL node) alive indefinitely — confirmed via heap-snapshot retainer
112
+ * analysis. See three/src/renderers/common/Renderer.js:292 (attach) and :2503
113
+ * (dispose — missing removal).
114
+ *
115
+ * Renderers constructed with an external `device` do not destroy it here; three
116
+ * only destroys a device it created itself.
117
+ *
118
+ * @param {import('three/webgpu').WebGPURenderer} renderer
119
+ */
120
+ export function disposeRenderer( renderer ) {
121
+
122
+ if ( ! renderer ) return;
123
+
124
+ if ( renderer._canvasTarget && renderer._onCanvasTargetResize ) {
125
+
126
+ renderer._canvasTarget.removeEventListener( 'resize', renderer._onCanvasTargetResize );
127
+
128
+ }
129
+
130
+ renderer.dispose();
131
+ renderer._canvasTarget = null;
132
+
133
+ }
134
+
105
135
  export function disposeObjectFromMemory( object, exeptions = [] ) {
106
136
 
107
137
  if ( ! object ) return;
@@ -18,6 +18,7 @@ const SETTING_ROUTES = {
18
18
  maxBounces: { uniform: 'maxBounces', reset: true },
19
19
  transmissiveBounces: { uniform: 'transmissiveBounces', reset: true },
20
20
  maxSubsurfaceSteps: { uniform: 'maxSubsurfaceSteps', reset: true },
21
+ maxTransparentBounces: { uniform: 'maxTransparentBounces', reset: true },
21
22
  environmentIntensity: { uniform: 'environmentIntensity', reset: true },
22
23
  backgroundIntensity: { uniform: 'backgroundIntensity', reset: true },
23
24
  backgroundBlurriness: { uniform: 'backgroundBlurriness', reset: true },
@@ -43,7 +44,6 @@ const SETTING_ROUTES = {
43
44
  // Adaptive sampling — whole-frame early-stop (useAdaptiveSampling) + per-pixel freeze (usePixelFreeze)
44
45
  useAdaptiveSampling: { uniform: 'useAdaptiveSampling', reset: true },
45
46
  noiseThreshold: { uniform: 'noiseThreshold', reset: true },
46
- darkNoiseFloor: { uniform: 'darkNoiseFloor', reset: true },
47
47
  adaptiveMinSamples: { uniform: 'adaptiveMinSamples', reset: true },
48
48
  adaptiveStopFraction: { uniform: 'adaptiveStopFraction', reset: true },
49
49
  usePixelFreeze: { uniform: 'usePixelFreeze', reset: true },