rayzee 7.16.0 → 7.18.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 +2817 -2537
  7. package/dist/rayzee.es.js.map +1 -1
  8. package/dist/rayzee.umd.js +55 -55
  9. package/dist/rayzee.umd.js.map +1 -1
  10. package/package.json +1 -1
  11. package/src/EngineDefaults.js +27 -11
  12. package/src/Passes/OIDNDenoiser.js +17 -14
  13. package/src/PathTracerApp.js +86 -13
  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 +69 -35
  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/RenderSettings.js +1 -1
  27. package/src/Stages/ASVGF.js +38 -0
  28. package/src/Stages/BilateralFilter.js +23 -0
  29. package/src/Stages/EdgeFilter.js +18 -0
  30. package/src/Stages/MotionVector.js +23 -0
  31. package/src/Stages/NormalDepth.js +22 -0
  32. package/src/Stages/PathTracer.js +324 -169
  33. package/src/Stages/PathTracerStage.js +17 -14
  34. package/src/Stages/Variance.js +23 -0
  35. package/src/TSL/FinalWriteKernel.js +30 -18
  36. package/src/TSL/GenerateKernel.js +28 -17
  37. package/src/TSL/ShadeKernel.js +186 -87
  38. package/src/TSL/TextureSampling.js +12 -2
  39. package/src/index.js +3 -0
  40. package/src/managers/EnvironmentManager.js +12 -8
  41. package/src/managers/MaterialDataManager.js +12 -9
  42. package/src/managers/UniformManager.js +1 -1
  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
@@ -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();
@@ -4,10 +4,13 @@
4
4
  */
5
5
 
6
6
  import {
7
- storage, uintBitsToFloat, floatBitsToUint, vec2, vec3, vec4, uvec4, uint, int,
7
+ storage, uintBitsToFloat, floatBitsToUint, vec2, vec3, vec4, uvec4, uint, int, float, clamp,
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,12 +22,12 @@ 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)
26
29
  MEDIUM_STACK: 4, // vec4(uintBitsToFloat(stackDepth|transTraversals<<8|wavelength<<16), ior1, ior2, ior3)
27
- MEDIUM_SIGMA_A: 5, // vec4(sigmaA.xyz, _) — Beer-Lambert absorption coeff of the active medium (KHR_materials_volume + SSS)
30
+ MEDIUM_SIGMA_A: 5, // vec4(sigmaA.xyz, featTP.w) — Beer-Lambert absorption (KHR_materials_volume + SSS) + DDFA feature-throughput tint
28
31
  SSS_SIGMA_S: 6, // vec4(sigmaS.xyz, g) — SSS scattering coeff + Henyey-Greenstein anisotropy (sigmaS==0 ⇒ glass)
29
32
  };
30
33
 
@@ -50,16 +53,11 @@ export function freeStorageAttribute( renderer, attr ) {
50
53
 
51
54
  export class PackedRayBuffer {
52
55
 
53
- // Capacity maxRays would allocate (mirrors allocate()/resize()). 1.25× headroom, NO pow2 rounding —
54
- // the pow2 jump nearly doubled VRAM (e.g. 2048²: 5.24M→8.39M) for no realloc benefit: the app's
55
- // discrete resolution presets always exceed the 1.25× margin on a tier change, so they rebuild anyway.
56
- static requiredCapacity( maxRays ) {
57
-
58
- return Math.ceil( maxRays * 1.25 );
59
-
60
- }
61
-
62
- constructor( maxRays = 0, renderer = null ) {
56
+ // `capacity` is the exact paths-in-flight budget B (Blender-style path pool). It is DECOUPLED from
57
+ // image resolution and fixed for the device — the image is streamed through the pool in row bands
58
+ // (see docs/internal/specs/wavefront-chunked-pool.md), so the SoA stride _cap never changes with
59
+ // resolution and the wavefront kernels are built once (no resize-triggered WGSL regen).
60
+ constructor( capacity = 0, renderer = null ) {
63
61
 
64
62
  this.capacity = 0;
65
63
  this._renderer = renderer;
@@ -70,15 +68,14 @@ export class PackedRayBuffer {
70
68
  this.rngBuffer = null;
71
69
  this.hitBuffer = null;
72
70
 
73
- if ( maxRays > 0 ) this.allocate( maxRays );
71
+ if ( capacity > 0 ) this.allocate( capacity );
74
72
 
75
73
  }
76
74
 
77
- allocate( maxRays ) {
75
+ allocate( capacity ) {
78
76
 
79
77
  this.dispose();
80
78
 
81
- const capacity = Math.ceil( maxRays * 1.25 );
82
79
  this.capacity = capacity;
83
80
  _cap = capacity;
84
81
 
@@ -106,23 +103,20 @@ export class PackedRayBuffer {
106
103
  ro: storage( hitAttr, 'vec4' ).toReadOnly(),
107
104
  };
108
105
 
109
- const totalMB = (
110
- rayCount * 16 + capacity * 4 + hitCount * 16
111
- ) / ( 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;
112
110
 
113
- console.log(
114
- `PackedRayBuffer: capacity=${capacity}, total=${totalMB.toFixed( 1 )} MB ` +
115
- `(ray=${( rayCount * 16 / 1048576 ).toFixed( 0 )}MB hit=${( hitCount * 16 / 1048576 ).toFixed( 0 )}MB) [SoA ray/hit]`
116
- );
111
+ log.debug( `ray pool capacity ${fmt.n( capacity )} · ${fmt.mb( this.totalBytes )} (rays ${fmt.mb( this.rayBytes )} · hits ${fmt.mb( this.hitBytes )})` );
117
112
 
118
113
  }
119
114
 
120
- // Reallocates only if maxRays needs more capacity; returns true if it did.
121
- resize( maxRays ) {
115
+ // Reallocates only if a larger budget capacity is requested; returns true if it did.
116
+ resize( capacity ) {
122
117
 
123
- const needed = Math.ceil( maxRays * 1.25 );
124
- if ( needed <= this.capacity && this.capacity > 0 ) return false;
125
- this.allocate( maxRays );
118
+ if ( capacity <= this.capacity && this.capacity > 0 ) return false;
119
+ this.allocate( capacity );
126
120
  return true;
127
121
 
128
122
  }
@@ -189,12 +183,15 @@ export const gbDecodeNormalDepth = ( packed ) => {
189
183
  export const gbDecodeAlbedo = ( packed ) =>
190
184
  vec3( unpackUnorm2x16( packed.z ), unpackUnorm2x16( packed.w ).x );
191
185
 
192
- // .w packs per-ray bounce state: perRayBounces (bits 0-7) | sssSteps (bits 8-15). pixelIndex is
193
- // NOT stored — it equals rayID (one ray per pixel).
194
- 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 ) ) =>
195
190
  buf.element( soa( id, RAY.ORIGIN_META ) )
196
191
  .assign( vec4( origin, uintBitsToFloat(
197
- 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 ) )
198
195
  ) ) );
199
196
 
200
197
  export const writeRayDirFlags = ( buf, id, direction, bounceFlags ) =>
@@ -258,20 +255,57 @@ export const writeMediumStack = ( buf, id, stackDepth, transTraversals, ior1, io
258
255
  stackDepth.bitOr( transTraversals.shiftLeft( 8 ) ).bitOr( wavelength.shiftLeft( 16 ) )
259
256
  ), ior1, ior2, ior3 ) );
260
257
 
261
- // Region 7: Beer-Lambert sigmaA of the active medium; single-slot, absorption gated on stackDepth>0.
258
+ // Region 7: Beer-Lambert sigmaA (.xyz) of the active medium + DDFA feature-throughput (.w).
259
+ // .xyz absorption is gated on stackDepth>0; .w carries the OIDN/ASVGF see-through aux tint (see below).
260
+ // Both writers RMW-preserve the other half so sigmaA (live in glass) and featTP never clobber each other.
262
261
  export const readMediumSigmaA = ( buf, id ) => buf.element( soa( id, RAY.MEDIUM_SIGMA_A ) ).xyz;
263
262
 
264
- export const writeMediumSigmaA = ( buf, id, sigmaA ) =>
265
- buf.element( soa( id, RAY.MEDIUM_SIGMA_A ) ).assign( vec4( sigmaA, 0.0 ) );
263
+ export const writeMediumSigmaA = ( buf, id, sigmaA ) => {
264
+
265
+ const keepW = buf.element( soa( id, RAY.MEDIUM_SIGMA_A ) ).w.toVar();
266
+ buf.element( soa( id, RAY.MEDIUM_SIGMA_A ) ).assign( vec4( sigmaA, keepW ) );
267
+
268
+ };
269
+
270
+ // DDFA (Deferred Denoising-Feature Aux) feature-throughput: a clean per-ray tint carried through smooth
271
+ // glass/mirror, committed into the OIDN/ASVGF albedo guide at the first diffuse-enough surface. Rides the
272
+ // otherwise-dead MEDIUM_SIGMA_A.w, packed 10/10/10 into 30 bits (top 2 bits zero ⇒ the float bit-pattern is
273
+ // always finite — never NaN/Inf). LDR [0,1] per channel; final albedo re-quantizes to unorm16 anyway.
274
+ const pack30 = ( tp ) => {
275
+
276
+ const c = clamp( tp, vec3( 0.0 ), vec3( 1.0 ) ).mul( 1023.0 ).add( 0.5 );
277
+ return uint( c.x ).bitOr( uint( c.y ).shiftLeft( 10 ) ).bitOr( uint( c.z ).shiftLeft( 20 ) );
278
+
279
+ };
280
+
281
+ const unpack30 = ( u ) => vec3(
282
+ float( u.bitAnd( uint( 1023 ) ) ),
283
+ float( u.shiftRight( 10 ).bitAnd( uint( 1023 ) ) ),
284
+ float( u.shiftRight( 20 ).bitAnd( uint( 1023 ) ) ),
285
+ ).div( 1023.0 );
286
+
287
+ export const readFeatureThroughput = ( buf, id ) =>
288
+ unpack30( floatBitsToUint( buf.element( soa( id, RAY.MEDIUM_SIGMA_A ) ).w ) );
289
+
290
+ export const writeFeatureThroughput = ( buf, id, tp ) => {
291
+
292
+ const keepXYZ = buf.element( soa( id, RAY.MEDIUM_SIGMA_A ) ).xyz.toVar();
293
+ buf.element( soa( id, RAY.MEDIUM_SIGMA_A ) ).assign( vec4( keepXYZ, uintBitsToFloat( pack30( tp ) ) ) );
294
+
295
+ };
266
296
 
267
297
  // Per-ray bounce state packed into ORIGIN_META.w (written by writeRayOriginMeta alongside the origin):
268
298
  // perRayBounces = bits 0-7 (camera-bounce depth; the loop index can't track it once free bounces decouple it)
269
299
  // sssSteps = bits 8-15 (SSS random-walk step counter)
300
+ // transparentCount = bits 16-21 (alpha-passthrough counter, max 63; see maxTransparentBounces)
270
301
  export const readPathBounces = ( buf, id ) =>
271
302
  int( floatBitsToUint( buf.element( soa( id, RAY.ORIGIN_META ) ).w ).bitAnd( 0xFF ) );
272
303
  export const readSssSteps = ( buf, id ) =>
273
304
  int( floatBitsToUint( buf.element( soa( id, RAY.ORIGIN_META ) ).w ).shiftRight( 8 ).bitAnd( 0xFF ) );
274
305
 
306
+ export const readTransparentCount = ( buf, id ) =>
307
+ int( floatBitsToUint( buf.element( soa( id, RAY.ORIGIN_META ) ).w ).shiftRight( 16 ).bitAnd( uint( 0x3F ) ) );
308
+
275
309
  // Region 9: SSS sigmaS + Henyey-Greenstein g. sigmaS==0 marks glass (Beer-Lambert path, not random walk).
276
310
  export const readSSSMedium = ( buf, id ) => {
277
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