rayzee 7.17.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 (40) 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 +2773 -2615
  7. package/dist/rayzee.es.js.map +1 -1
  8. package/dist/rayzee.umd.js +48 -48
  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 +32 -12
  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/RenderSettings.js +1 -1
  27. package/src/Stages/PathTracer.js +15 -4
  28. package/src/Stages/PathTracerStage.js +17 -14
  29. package/src/TSL/FinalWriteKernel.js +11 -6
  30. package/src/TSL/ShadeKernel.js +46 -24
  31. package/src/TSL/TextureSampling.js +12 -2
  32. package/src/index.js +3 -0
  33. package/src/managers/EnvironmentManager.js +12 -8
  34. package/src/managers/MaterialDataManager.js +12 -9
  35. package/src/managers/UniformManager.js +1 -1
  36. package/src/utils/Logger.js +291 -0
  37. package/dist/assets/BVHSubtreeWorker-sNzvxn66.js +0 -2
  38. package/dist/assets/BVHSubtreeWorker-sNzvxn66.js.map +0 -1
  39. package/dist/assets/BVHWorker-CiVdFrwe.js +0 -2
  40. package/dist/assets/BVHWorker-CiVdFrwe.js.map +0 -1
@@ -25,6 +25,9 @@ import { LightSerializer } from '../Processor/LightSerializer';
25
25
  // Constants
26
26
  import { ENGINE_DEFAULTS as DEFAULT_STATE } from '../EngineDefaults.js';
27
27
  import { getAssetConfig } from '../AssetConfig.js';
28
+ import { createLogger, fmt } from '../utils/Logger.js';
29
+
30
+ const log = createLogger( 'pathtracer' );
28
31
 
29
32
  /**
30
33
  * Data layout constants
@@ -377,7 +380,7 @@ export class PathTracerStage extends RenderStage {
377
380
 
378
381
  this.stbnScalarTexture = configure( tex );
379
382
  stbnScalarTextureNode.value = tex;
380
- console.log( `PathTracer: STBN scalar atlas loaded ${tex.image.width}x${tex.image.height}` );
383
+ log.debug( `STBN scalar atlas ${fmt.px( tex.image.width, tex.image.height )}` );
381
384
 
382
385
  } );
383
386
 
@@ -385,7 +388,7 @@ export class PathTracerStage extends RenderStage {
385
388
 
386
389
  this.stbnVec2Texture = configure( tex );
387
390
  stbnVec2TextureNode.value = tex;
388
- console.log( `PathTracer: STBN vec2 atlas loaded ${tex.image.width}x${tex.image.height}` );
391
+ log.debug( `STBN vec2 atlas ${fmt.px( tex.image.width, tex.image.height )}` );
389
392
 
390
393
  } );
391
394
 
@@ -548,7 +551,7 @@ export class PathTracerStage extends RenderStage {
548
551
 
549
552
  this.directionalLightsData = mockMaterial.uniforms.directionalLights.value;
550
553
 
551
- console.log( `Sun added as directional light (intensity: ${scaledSunIntensity.toFixed( 2 )})` );
554
+ log.debug( `sun added as directional light · intensity ${scaledSunIntensity.toFixed( 2 )}` );
552
555
 
553
556
  }
554
557
 
@@ -716,7 +719,7 @@ export class PathTracerStage extends RenderStage {
716
719
 
717
720
  this.triangleCount = triangleCount;
718
721
 
719
- console.log( `PathTracer: ${this.triangleCount} triangles (storage buffer)` );
722
+ log.debug( `${fmt.n( this.triangleCount )} triangles (storage buffer)` );
720
723
 
721
724
  }
722
725
 
@@ -744,7 +747,7 @@ export class PathTracerStage extends RenderStage {
744
747
  }
745
748
 
746
749
  this.bvhNodeCount = Math.floor( vec4Count / BVH_VEC4_PER_NODE );
747
- console.log( `PathTracer: ${this.bvhNodeCount} BVH nodes (storage buffer)` );
750
+ log.debug( `${fmt.n( this.bvhNodeCount )} BVH nodes (storage buffer)` );
748
751
 
749
752
  }
750
753
 
@@ -983,14 +986,14 @@ export class PathTracerStage extends RenderStage {
983
986
 
984
987
  if ( ! this.triangleStorageNode ) {
985
988
 
986
- console.error( 'PathTracer: Triangle data required' );
989
+ log.error( 'triangle data required' );
987
990
  return;
988
991
 
989
992
  }
990
993
 
991
994
  if ( ! this.bvhStorageNode ) {
992
995
 
993
- console.error( 'PathTracer: BVH data required' );
996
+ log.error( 'BVH data required' );
994
997
  return;
995
998
 
996
999
  }
@@ -1348,7 +1351,7 @@ export class PathTracerStage extends RenderStage {
1348
1351
  this.emissiveTriangleCount.value = count;
1349
1352
  this.emissiveTotalPower.value = totalPower;
1350
1353
  this._rebuildLightBuffer();
1351
- console.log( `PathTracer: ${count} emissive triangles, totalPower=${totalPower.toFixed( 4 )} (storage buffer)` );
1354
+ log.debug( `${fmt.n( count )} emissive triangles · totalPower ${totalPower.toFixed( 4 )} (storage buffer)` );
1352
1355
 
1353
1356
  }
1354
1357
 
@@ -1359,7 +1362,7 @@ export class PathTracerStage extends RenderStage {
1359
1362
  this._lbvhDataCache = nodeData;
1360
1363
  this.lightBVHNodeCount.value = nodeCount;
1361
1364
  this._rebuildLightBuffer();
1362
- console.log( `PathTracer: Light BVH ${nodeCount} nodes` );
1365
+ log.debug( `light BVH ${fmt.n( nodeCount )} nodes` );
1363
1366
 
1364
1367
  }
1365
1368
 
@@ -1402,7 +1405,7 @@ export class PathTracerStage extends RenderStage {
1402
1405
 
1403
1406
  try {
1404
1407
 
1405
- console.log( 'PathTracer: Starting material rebuild...' );
1408
+ log.debug( 'material rebuild started' );
1406
1409
 
1407
1410
  await this.sdfs.rebuildMaterials( scene );
1408
1411
  this.updateSceneUniforms();
@@ -1410,20 +1413,20 @@ export class PathTracerStage extends RenderStage {
1410
1413
  this.updateLights();
1411
1414
  this.reset();
1412
1415
 
1413
- console.log( 'PathTracer materials rebuilt successfully' );
1416
+ log.debug( 'material rebuild complete' );
1414
1417
 
1415
1418
  } catch ( error ) {
1416
1419
 
1417
- console.error( 'Error rebuilding PathTracer materials:', error );
1420
+ log.error( 'material rebuild failed:', error );
1418
1421
 
1419
1422
  try {
1420
1423
 
1421
- console.warn( 'Attempting recovery by resetting path tracer...' );
1424
+ log.warn( 'attempting recovery by resetting the path tracer' );
1422
1425
  this.reset();
1423
1426
 
1424
1427
  } catch ( recoveryError ) {
1425
1428
 
1426
- console.error( 'Recovery failed:', recoveryError );
1429
+ log.error( 'recovery failed:', recoveryError );
1427
1430
 
1428
1431
  }
1429
1432
 
@@ -50,7 +50,8 @@ export function buildFinalWriteKernel( params ) {
50
50
  // OIDN models (calb_cnrm/high, alb_nrm/balanced); off for fast/ASVGF which want the bump normal.
51
51
  cleanAuxNormalEnabled,
52
52
  // Tier-1 convergence early-stop: per-pixel Welford luminance second moment + converged-pixel counter.
53
- counters, m2BufferRW, useAdaptiveSampling, noiseThreshold, darkNoiseFloor, adaptiveMinSamples,
53
+ counters, m2BufferRW, useAdaptiveSampling, noiseThreshold, adaptiveMinSamples,
54
+
54
55
  // Tier-2 per-pixel freeze: streakBufferRW = per-pixel freeze-candidate streak (stamped here);
55
56
  // frozenMaskRO = dilated frozen mask from buildActivePixels (pass-through gates on it, not streak).
56
57
  usePixelFreeze, pixelFreezeThreshold, pixelFreezeStability, streakBufferRW, frozenMaskRO,
@@ -141,7 +142,7 @@ export function buildFinalWriteKernel( params ) {
141
142
  // Tier-1 convergence: per-pixel running second moment of LUMINANCE (Welford). luminance() is linear,
142
143
  // so luminance(running-mean color) == running-mean luminance == E[L]; m2 tracks E[L²] under the SAME
143
144
  // global 1/(frame+1) alpha (NO per-pixel alpha). sampleVar = E[L²]-E[L]²; varOfMean = sampleVar/(N);
144
- // relErr = SE(mean)/mean. A pixel counts as converged once frame>=minSamples AND relErr<threshold.
145
+ // absSE = SE(mean); a pixel converges once frame>=minSamples AND the √-normalized error < threshold.
145
146
  // The m2 write runs every frame (incl. frame 0, where alpha==1 self-inits it → no explicit clear).
146
147
  // Sits AFTER the accumulation mix (finalColor is the mean) and BEFORE the visMode-11 mutation.
147
148
  If( convOn, () => {
@@ -158,11 +159,15 @@ export function buildFinalWriteKernel( params ) {
158
159
  // Combined criterion: bright pixels converge on relErr<threshold; dark/dim pixels (where relErr
159
160
  // stays high forever) converge on absSE<absFloor, since their absolute noise is imperceptible.
160
161
  const absSE = varOfMean.sqrt().toVar();
161
- const relErr = absSE.div( meanLum.add( float( 1e-4 ) ) );
162
+ const relErr = absSE.div( meanLum.add( float( 1e-4 ) ) ); // Tier-2 freeze predicate reads this
163
+
164
+ // Cycles-style convergence: normalize the standard error by √luminance for dim pixels (lum<1),
165
+ // by luminance for bright ones. The √ loosens the relative bar in shadows so dark pixels converge
166
+ // on their small absolute noise instead of stalling forever on a pure relative bar.
167
+ const convNorm = select( meanLum.lessThan( float( 1.0 ) ), meanLum.sqrt(), meanLum );
168
+ const converged = absSE.div( convNorm.add( float( 1e-4 ) ) ).lessThan( noiseThreshold );
162
169
 
163
- If( frame.greaterThanEqual( uint( adaptiveMinSamples ) ).and(
164
- relErr.lessThan( noiseThreshold ).or( absSE.lessThan( darkNoiseFloor ) )
165
- ), () => {
170
+ If( frame.greaterThanEqual( uint( adaptiveMinSamples ) ).and( converged ), () => {
166
171
 
167
172
  atomicAdd( counters.element( uint( COUNTER.CONVERGED_COUNT ) ), uint( 1 ) );
168
173
 
@@ -53,6 +53,7 @@ import {
53
53
  readRayOrigin, readRayDirection, readRayBounceFlags, readRayThroughput, readRayPdf,
54
54
  readMediumStack, writeMediumStack, readMediumSigmaA, writeMediumSigmaA,
55
55
  readPathBounces, readSssSteps, readSSSMedium, writeSSSMedium,
56
+ readTransparentCount,
56
57
  readHitDistance, readHitBarycentrics, readHitNormal,
57
58
  readHitMaterialIndex, readHitTriangleIndex,
58
59
  writeRayOriginMeta, writeRayDirFlags, writeRayThroughputPdf, writeRayRadiance,
@@ -88,6 +89,7 @@ export function buildShadeKernel( params ) {
88
89
  pointLightsBuffer, numPointLights,
89
90
  spotLightsBuffer, numSpotLights,
90
91
  maxBounceCount, maxSubsurfaceSteps,
92
+ maxTransparentBounces, // guard on alpha-skip depth; counter in ORIGIN_META.w
91
93
  currentBounce, // loop iteration = path length (advances on free bounces); drives RR/firefly/giScale
92
94
  transparentBackground, backgroundIntensity, backgroundColor, backgroundBlurriness, backgroundBlurSamples, showBackground,
93
95
  globalIlluminationIntensity,
@@ -204,6 +206,7 @@ export function buildShadeKernel( params ) {
204
206
  // path length = loop iteration (advances every bounce incl. transmissive/SSS); drives RR/firefly/giScale/MIS. Megakernel: loop counter i.
205
207
  const bounceIndex = int( currentBounce ).toVar();
206
208
  const sssSteps = readSssSteps( rayBufferRW, rayID ).toVar();
209
+ const transparentCount = readTransparentCount( rayBufferRW, rayID ).toVar();
207
210
 
208
211
  // ── Analytic ground-plane shadow catcher (primary ray only, no geometry) ──
209
212
  // A horizontal plane at y = groundCatcherHeight. For a bounce-0 ray that crosses it
@@ -546,7 +549,7 @@ export function buildShadeKernel( params ) {
546
549
  // free-bounce continuation: ray stays in the same medium, so medium stack + coeffs persist
547
550
  // SSS scatter changes direction → no longer the direct backdrop view.
548
551
  flags.assign( flags.bitOr( uint( RAY_FLAG.REDIRECTED ) ) );
549
- writeRayOriginMeta( rayBufferRW, rayID, scatterPoint, cameraDepth, sssSteps );
552
+ writeRayOriginMeta( rayBufferRW, rayID, scatterPoint, cameraDepth, sssSteps, transparentCount );
550
553
  writeRayDirFlags( rayBufferRW, rayID, newDir, flags );
551
554
  // Free bounce: preserve prevBouncePdf (megakernel leaves it untouched across SSS scatter,
552
555
  // PathTracerCore.js:1272 sets it only after an opaque scatter). Writing 1.0 here spuriously
@@ -666,6 +669,29 @@ export function buildShadeKernel( params ) {
666
669
  ).greaterThanEqual( float( 0.25 ) ).toVar();
667
670
  const featPrefix = featCarry.toVar();
668
671
 
672
+ // DDFA terminal fallback: a ray dying while still deferring (e.g. a mirror maze) commits its last
673
+ // surface instead of leaving a black guide OIDN would demod-amplify.
674
+ // Plain JS inliner, not an Fn — it closes over albedo/featPrefix/flags .toVar()s.
675
+ const commitDeferredAux = ( normal ) => {
676
+
677
+ If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
678
+
679
+ const primaryDepth = gbDecodeNormalDepth( readGBuffer( gBufferRW, pixelIndex ) ).w;
680
+ writeGBuffer( gBufferRW, pixelIndex, normal, primaryDepth, clamp( albedo.mul( featPrefix ), vec3( 0.0 ), vec3( 1.0 ) ) );
681
+
682
+ } );
683
+
684
+ };
685
+
686
+ // Deactivate the ray, keeping the radiance already gathered at this vertex.
687
+ const terminatePath = () => {
688
+
689
+ writeRayRadiance( rayBufferRW, rayID, currentRadiance );
690
+ writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
691
+ rngBufferRW.element( rayID ).assign( rngState );
692
+
693
+ };
694
+
669
695
  // first-hit MRT data (bounce 0 only): write the primary DEPTH now with the default normal/albedo.
670
696
  // The real normal/albedo are committed by the DDFA decision blocks below (which re-pack this depth);
671
697
  // they may defer through smooth glass/mirror and commit at the first diffuse-enough surface or the env.
@@ -874,8 +900,21 @@ export function buildShadeKernel( params ) {
874
900
 
875
901
  flags.assign( flags.bitOr( uint( RAY_FLAG.REDIRECTED ) ) );
876
902
 
903
+ } ).Else( () => {
904
+
905
+ // Alpha passthrough: charge the guard. N is faceforwarded for the aux commit (a ray
906
+ // exiting a back-facing skip would otherwise write an inverted normal).
907
+ transparentCount.addAssign( 1 );
908
+ If( transparentCount.greaterThan( maxTransparentBounces ), () => {
909
+
910
+ commitDeferredAux( select( dot( N, direction.negate() ).lessThan( 0.0 ), N.negate(), N ) );
911
+ terminatePath();
912
+ Return();
913
+
914
+ } );
915
+
877
916
  } );
878
- writeRayOriginMeta( rayBufferRW, rayID, newOrigin, cameraDepth, sssSteps );
917
+ writeRayOriginMeta( rayBufferRW, rayID, newOrigin, cameraDepth, sssSteps, transparentCount );
879
918
  writeRayDirFlags( rayBufferRW, rayID, interaction.direction, flags );
880
919
  // Free bounce: preserve prevBouncePdf (megakernel keeps the last opaque-scatter pdf across
881
920
  // transmission/alpha-skip/SSS-boundary). Writing 1.0 corrupts the next bounce's env/emissive MIS,
@@ -1230,17 +1269,8 @@ export function buildShadeKernel( params ) {
1230
1269
  ).toVar();
1231
1270
  If( rrSurvival.lessThanEqual( 0.0 ), () => {
1232
1271
 
1233
- // DDFA terminal fallback: a ray dying while still deferring (e.g. a mirror maze) commits its last
1234
- // surface instead of leaving a black guide OIDN would demod-amplify. N is viewer-facing here.
1235
- If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
1236
-
1237
- const primaryDepth = gbDecodeNormalDepth( readGBuffer( gBufferRW, pixelIndex ) ).w;
1238
- writeGBuffer( gBufferRW, pixelIndex, N, primaryDepth, clamp( albedo.mul( featPrefix ), vec3( 0.0 ), vec3( 1.0 ) ) );
1239
-
1240
- } );
1241
- writeRayRadiance( rayBufferRW, rayID, currentRadiance );
1242
- writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
1243
- rngBufferRW.element( rayID ).assign( rngState );
1272
+ commitDeferredAux( N );
1273
+ terminatePath();
1244
1274
  Return();
1245
1275
 
1246
1276
  } );
@@ -1249,16 +1279,8 @@ export function buildShadeKernel( params ) {
1249
1279
  // Terminate on CAMERA depth (opaque scatter count), not path length — glass/SSS free bounces no longer burn the maxBounces budget (gap #4).
1250
1280
  If( cameraDepth.greaterThanEqual( maxBounceCount ), () => {
1251
1281
 
1252
- // DDFA terminal fallback (see RR-kill above): commit the last surface if still deferring.
1253
- If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
1254
-
1255
- const primaryDepth = gbDecodeNormalDepth( readGBuffer( gBufferRW, pixelIndex ) ).w;
1256
- writeGBuffer( gBufferRW, pixelIndex, N, primaryDepth, clamp( albedo.mul( featPrefix ), vec3( 0.0 ), vec3( 1.0 ) ) );
1257
-
1258
- } );
1259
- writeRayRadiance( rayBufferRW, rayID, currentRadiance );
1260
- writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
1261
- rngBufferRW.element( rayID ).assign( rngState );
1282
+ commitDeferredAux( N );
1283
+ terminatePath();
1262
1284
  Return();
1263
1285
 
1264
1286
  } );
@@ -1266,7 +1288,7 @@ export function buildShadeKernel( params ) {
1266
1288
  const newOrigin = hitPoint.add( N.mul( 0.001 ) );
1267
1289
 
1268
1290
  // Opaque scatter: the only bounce that advances camera depth.
1269
- writeRayOriginMeta( rayBufferRW, rayID, newOrigin, cameraDepth.add( 1 ), sssSteps );
1291
+ writeRayOriginMeta( rayBufferRW, rayID, newOrigin, cameraDepth.add( 1 ), sssSteps, transparentCount );
1270
1292
  writeRayDirFlags( rayBufferRW, rayID, bounceDir, flags );
1271
1293
  writeRayThroughputPdf( rayBufferRW, rayID, throughput, bouncePdf );
1272
1294
  writeRayRadiance( rayBufferRW, rayID, currentRadiance );
@@ -47,10 +47,20 @@ const withBucket = ( buckets, packedIndex, fn ) => {
47
47
 
48
48
  const bucket = packedIndex.div( int( _STRIDE ) ).toVar();
49
49
  const layer = packedIndex.sub( bucket.mul( int( _STRIDE ) ) ).toVar();
50
- let chain = If( bucket.equal( int( 0 ) ), () => fn( buckets[ 0 ], layer ) );
50
+ // Block bodies, not concise arrows: an If() callback that yields a value makes TSL emit a
51
+ // `return` inside an inline Fn(), which it comments out and warns about on every arm.
52
+ let chain = If( bucket.equal( int( 0 ) ), () => {
53
+
54
+ fn( buckets[ 0 ], layer );
55
+
56
+ } );
51
57
  for ( let i = 1; i < buckets.length; i ++ ) {
52
58
 
53
- chain = chain.ElseIf( bucket.equal( int( i ) ), () => fn( buckets[ i ], layer ) );
59
+ chain = chain.ElseIf( bucket.equal( int( i ) ), () => {
60
+
61
+ fn( buckets[ i ], layer );
62
+
63
+ } );
54
64
 
55
65
  }
56
66
 
package/src/index.js CHANGED
@@ -17,6 +17,9 @@ export { EngineEvents } from './EngineEvents.js';
17
17
  // Asset URL / cache namespace overrides (call before constructing PathTracerApp)
18
18
  export { configureAssets, getAssetConfig } from './AssetConfig.js';
19
19
 
20
+ // Logging — leveled/namespaced console output shared with the workers
21
+ export { Logger, createLogger, fmt, LOG_LEVELS } from './utils/Logger.js';
22
+
20
23
  // Configuration defaults and presets
21
24
  export {
22
25
  ENGINE_DEFAULTS,
@@ -13,6 +13,9 @@ import {
13
13
  import { EquirectHDRInfo } from '../Processor/EquirectHDRInfo.js';
14
14
  import { ProceduralSky } from '../Processor/ProceduralSky.js';
15
15
  import { SimpleSky } from '../Processor/SimpleSky.js';
16
+ import { createLogger, fmt } from '../utils/Logger.js';
17
+
18
+ const log = createLogger( 'env' );
16
19
  import { ENGINE_DEFAULTS as DEFAULT_STATE } from '../EngineDefaults.js';
17
20
 
18
21
  export class EnvironmentManager {
@@ -231,8 +234,6 @@ export class EnvironmentManager {
231
234
  this.environmentTexture = envTex;
232
235
  this.envTexSize.set( envTex.image.width, envTex.image.height );
233
236
 
234
- console.log( `EnvironmentManager: Environment map ${envTex.image.width}x${envTex.image.height}` );
235
-
236
237
  }
237
238
 
238
239
  /**
@@ -317,11 +318,14 @@ export class EnvironmentManager {
317
318
 
318
319
  }
319
320
 
320
- console.log( `Environment CDF built in ${this.cdfBuildTime.toFixed( 2 )}ms (worker: ${useWorker})` );
321
+ log.info( fmt.list( [
322
+ fmt.px( this.envTexSize.x, this.envTexSize.y ),
323
+ `CDF ${fmt.ms( this.cdfBuildTime )}${useWorker ? '' : ' (main thread)'}`,
324
+ ] ) );
321
325
 
322
326
  } catch ( error ) {
323
327
 
324
- console.error( 'Error building environment CDF:', error );
328
+ log.error( 'CDF build failed:', error );
325
329
  this.uniforms.set( 'useEnvMapIS', 0 );
326
330
  this.uniforms.set( 'envTotalSum', 0.0 );
327
331
  this.uniforms.set( 'envCompensationDelta', 0.0 );
@@ -437,7 +441,7 @@ export class EnvironmentManager {
437
441
 
438
442
  } catch ( error ) {
439
443
 
440
- console.error( 'Error generating gradient sky:', error );
444
+ log.error( 'gradient sky generation failed:', error );
441
445
 
442
446
  }
443
447
 
@@ -467,7 +471,7 @@ export class EnvironmentManager {
467
471
 
468
472
  } catch ( error ) {
469
473
 
470
- console.error( 'Error generating solid color sky:', error );
474
+ log.error( 'solid color sky generation failed:', error );
471
475
 
472
476
  }
473
477
 
@@ -503,11 +507,11 @@ export class EnvironmentManager {
503
507
  this.uniforms.set( 'sunAngularSize', 0.0087 );
504
508
  this.uniforms.set( 'hasSun', 1 );
505
509
 
506
- console.log( `Sun parameters synced: dir=${this.envParams.skySunDirection.toArray().map( v => v.toFixed( 2 ) ).join( ',' )}` );
510
+ log.debug( `sun synced · dir ${this.envParams.skySunDirection.toArray().map( v => v.toFixed( 2 ) ).join( ',' )}` );
507
511
 
508
512
  } catch ( error ) {
509
513
 
510
- console.error( 'Error generating procedural sky:', error );
514
+ log.error( 'procedural sky generation failed:', error );
511
515
 
512
516
  }
513
517
 
@@ -10,6 +10,9 @@
10
10
  import { StorageInstancedBufferAttribute } from 'three/webgpu';
11
11
  import { storage } from 'three/tsl';
12
12
  import { MATERIAL_DATA_LAYOUT as M, TRIANGLE_DATA_LAYOUT as T } from '../EngineDefaults.js';
13
+ import { createLogger, fmt } from '../utils/Logger.js';
14
+
15
+ const log = createLogger( 'material' );
13
16
 
14
17
  const PIXELS_PER_MATERIAL = M.SLOTS_PER_MATERIAL;
15
18
  // Per-triangle float offsets used by _patchTriangleSideForMaterial / _patchTriangleBlockerForMaterial.
@@ -84,7 +87,7 @@ export class MaterialDataManager {
84
87
  }
85
88
 
86
89
  this.materialCount = Math.floor( vec4Count / PIXELS_PER_MATERIAL );
87
- console.log( `MaterialDataManager: ${this.materialCount} materials (storage buffer)` );
90
+ log.debug( `${fmt.n( this.materialCount )} materials (storage buffer)` );
88
91
 
89
92
  }
90
93
 
@@ -186,7 +189,7 @@ export class MaterialDataManager {
186
189
 
187
190
  if ( ! this.materialStorageAttr ) {
188
191
 
189
- console.warn( 'Material storage buffer not available' );
192
+ log.warn( 'material storage buffer not available' );
190
193
  return;
191
194
 
192
195
  }
@@ -364,7 +367,7 @@ export class MaterialDataManager {
364
367
 
365
368
  break;
366
369
  default:
367
- console.warn( `Unknown material property: ${property}` );
370
+ log.warn( `unknown material property: ${property}` );
368
371
  return;
369
372
 
370
373
  }
@@ -403,7 +406,7 @@ export class MaterialDataManager {
403
406
 
404
407
  if ( ! this.materialStorageAttr ) {
405
408
 
406
- console.warn( 'Material storage buffer not available' );
409
+ log.warn( 'material storage buffer not available' );
407
410
  return;
408
411
 
409
412
  }
@@ -630,7 +633,7 @@ export class MaterialDataManager {
630
633
 
631
634
  if ( ! this.materialStorageAttr ) {
632
635
 
633
- console.warn( 'Material storage buffer not available' );
636
+ log.warn( 'material storage buffer not available' );
634
637
  return;
635
638
 
636
639
  }
@@ -651,7 +654,7 @@ export class MaterialDataManager {
651
654
  const offset = transformOffsets[ textureName ];
652
655
  if ( offset === undefined ) {
653
656
 
654
- console.warn( `Unknown texture name for transform update: ${textureName}` );
657
+ log.warn( `unknown texture name for transform update: ${textureName}` );
655
658
  return;
656
659
 
657
660
  }
@@ -683,7 +686,7 @@ export class MaterialDataManager {
683
686
 
684
687
  if ( ! this.materialStorageAttr?.array ) {
685
688
 
686
- console.warn( '[MaterialDataManager] Material storage buffer not available for feature scanning' );
689
+ log.warn( 'material storage buffer not available for feature scanning' );
687
690
  return false;
688
691
 
689
692
  }
@@ -763,7 +766,7 @@ export class MaterialDataManager {
763
766
 
764
767
  if ( ! features ) {
765
768
 
766
- console.warn( '[MaterialDataManager] No sceneFeatures detected, skipping define injection' );
769
+ log.warn( 'no sceneFeatures detected, skipping define injection' );
767
770
  return;
768
771
 
769
772
  }
@@ -780,7 +783,7 @@ export class MaterialDataManager {
780
783
  // For TSL, we can't inject defines into the shader at runtime
781
784
  // Instead, we would need to conditionally generate the shader
782
785
  // For now, log the features for debugging
783
- console.log( '[MaterialDataManager] Material features:', features );
786
+ log.debug( 'material features:', features );
784
787
 
785
788
  this.compiledFeatures = featuresJSON;
786
789
 
@@ -164,13 +164,13 @@ export class UniformManager {
164
164
  u( 'maxSamples', DEFAULT_STATE.maxSamples, 'int' );
165
165
  u( 'transmissiveBounces', DEFAULT_STATE.transmissiveBounces, 'int' );
166
166
  u( 'maxSubsurfaceSteps', DEFAULT_STATE.maxSubsurfaceSteps, 'int' );
167
+ u( 'maxTransparentBounces', DEFAULT_STATE.maxTransparentBounces, 'int' );
167
168
  u( 'visMode', DEFAULT_STATE.debugMode, 'int' );
168
169
  u( 'debugVisScale', DEFAULT_STATE.debugVisScale, 'float' );
169
170
 
170
171
  // Tier-1 convergence early-stop (FinalWrite reads these; live-toggled, no shader rebuild)
171
172
  ub( 'useAdaptiveSampling', DEFAULT_STATE.useAdaptiveSampling );
172
173
  u( 'noiseThreshold', DEFAULT_STATE.noiseThreshold, 'float' );
173
- u( 'darkNoiseFloor', DEFAULT_STATE.darkNoiseFloor, 'float' );
174
174
  u( 'adaptiveMinSamples', DEFAULT_STATE.adaptiveMinSamples, 'int' );
175
175
  // CPU-only (read in PathTracer._isConvergedComplete, not bound to any kernel); registered here for
176
176
  // the settings/configureForMode plumbing + the _defineUniformGetters accessor.