rayzee 11.7.0 → 11.9.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 (79) hide show
  1. package/README.md +31 -6
  2. package/dist/{ArchiveImporter-DYcRXeKE.js → ArchiveImporter-BJxszU_k.js} +2353 -1356
  3. package/dist/{BidirectionalIntegrator-BvjRpbTP.js → BidirectionalIntegrator-DWg66vAd.js} +302 -301
  4. package/dist/{BufferLayout-xPRH9jYD.js → BufferLayout-Bzgccg1o.js} +1 -0
  5. package/dist/{ColorManagement-oeEk7oSu.js → ColorManagement-B_HjAiy2.js} +1 -1
  6. package/dist/{EngineIssues-CIZnXHsF.js → EngineIssues-CfJiBaNj.js} +19 -8
  7. package/dist/{NeuralRetouch-ClaMiKYg.js → NeuralRetouch-BD9E-Yht.js} +2 -2
  8. package/dist/{NeuralSuperRes-WNEXun9s.js → NeuralSuperRes-C8Mf3tJX.js} +2 -2
  9. package/dist/{PathTracer-B9wZPPVL.js → PathTracer-DeE9UJ1n.js} +3530 -3402
  10. package/dist/{PhysicalSky-DcjItqKi.js → PhysicalSky-cKy75-oR.js} +45 -45
  11. package/dist/{RayzeeRenderer-p-NeE6E2.js → RayzeeRenderer-CzLyBA3G.js} +378 -374
  12. package/dist/SceneMetadata-C17UfOEp.js +183 -0
  13. package/dist/{SolarLimb-bZE6y63S.js → SolarLimb-DRYy8tln.js} +184 -172
  14. package/dist/{ToneMapGPU-sAs8wdgI.js → ToneMapGPU-4GPwoG_c.js} +13 -6
  15. package/dist/addons/archives.es.js +1 -1
  16. package/dist/addons/bidirectional.es.js +1 -1
  17. package/dist/addons/color.es.js +1 -1
  18. package/dist/addons/physical-sky.es.js +1 -1
  19. package/dist/addons/storage.es.js +1 -1
  20. package/dist/{openStorage-Fe9BoS1_.js → openStorage-CdyVjmps.js} +1 -1
  21. package/dist/rayzee-core.es.js +7 -7
  22. package/dist/rayzee.es.js +821 -794
  23. package/dist/rayzee.umd.js +112 -103
  24. package/dist/{sceneCachePolicy-B9w3BnaP.js → sceneCachePolicy-ywbGKHD3.js} +9 -6
  25. package/package.json +1 -1
  26. package/src/EngineDefaults.js +1 -0
  27. package/src/EngineIssues.js +21 -6
  28. package/src/Passes/AIUpscaler.js +4 -0
  29. package/src/Passes/OIDNDenoiser.js +8 -6
  30. package/src/Passes/OIDNTemporalHistory.js +1 -1
  31. package/src/PathTracerApp.js +10 -1
  32. package/src/Processor/ArchiveImporter.js +106 -27
  33. package/src/Processor/AssetLoader.js +22 -0
  34. package/src/Processor/BufferLayout.js +3 -3
  35. package/src/Processor/GeometryExtractor.js +12 -4
  36. package/src/Processor/LightSerializer.js +6 -2
  37. package/src/Processor/MaterialPacking.js +2 -2
  38. package/src/Processor/PBRT/PBRTMaterials.js +49 -13
  39. package/src/Processor/PBRT/PBRTParser.js +144 -22
  40. package/src/Processor/PBRT/PBRTSceneBuilder.js +287 -67
  41. package/src/Processor/PBRT/PBRTTextureBake.js +191 -9
  42. package/src/Processor/PBRT/index.js +19 -4
  43. package/src/Processor/PlatformImageLoader.js +9 -0
  44. package/src/Processor/SceneProcessor.js +42 -26
  45. package/src/Processor/TextureCreator.js +44 -4
  46. package/src/Processor/ToneMapCPU.js +34 -5
  47. package/src/Processor/ToneMapGPU.js +15 -9
  48. package/src/Processor/USD/USDScene.js +981 -17
  49. package/src/RayzeeRenderer.js +85 -28
  50. package/src/Stages/AutoExposure.js +21 -8
  51. package/src/Stages/Compositor.js +39 -1
  52. package/src/Stages/DenoiserSettings.js +0 -4
  53. package/src/Stages/LocalExposure.js +44 -18
  54. package/src/Stages/NRD.js +53 -24
  55. package/src/Stages/NormalDepth.js +12 -6
  56. package/src/Stages/PathTracer.js +27 -1
  57. package/src/Stages/PathTracerStage.js +14 -2
  58. package/src/Storage/SceneGraphCodec.js +6 -3
  59. package/src/TSL/BVHTraversal.js +33 -20
  60. package/src/TSL/CameraRay.js +12 -1
  61. package/src/TSL/Common.js +25 -8
  62. package/src/TSL/DebugKernel.js +3 -2
  63. package/src/TSL/Displacement.js +66 -113
  64. package/src/TSL/FinalWriteKernel.js +4 -7
  65. package/src/TSL/GenerateKernel.js +6 -2
  66. package/src/TSL/LightSplatKernel.js +7 -4
  67. package/src/TSL/LightsCore.js +6 -2
  68. package/src/TSL/LightsDirect.js +33 -11
  69. package/src/TSL/LightsSampling.js +45 -12
  70. package/src/TSL/SceneResources.js +19 -0
  71. package/src/TSL/ShadeKernel.js +3 -4
  72. package/src/TSL/Struct.js +1 -0
  73. package/src/integrators/BidirectionalIntegrator.js +1 -0
  74. package/src/managers/CameraManager.js +9 -0
  75. package/src/managers/DenoisingManager.js +1 -0
  76. package/src/managers/InteractionManager.js +6 -0
  77. package/src/managers/MaterialDataManager.js +11 -0
  78. package/src/managers/UniformManager.js +3 -1
  79. package/dist/SceneMetadata-D2Sh-0e-.js +0 -110
package/src/Stages/NRD.js CHANGED
@@ -8,7 +8,7 @@ import { createStorageTexture } from '../Processor/StorageTexturePool.js';
8
8
  import { sanitizeRGB, sanitize1, FP16_MAX } from '../TSL/Common.js';
9
9
  import { cameraRayPoint, cameraRayUniforms, isOrthographic } from '../TSL/CameraRay.js';
10
10
  import {
11
- NRD_DEFAULTS, NRD_HIT_DIST_A, NRD_HIT_DIST_B, ALBEDO_EPS, GBUFFER_MISS_THRESHOLD as MISS_THRESHOLD,
11
+ NRD_DEFAULTS, NRD_HIT_DIST_A, NRD_HIT_DIST_B, GBUFFER_MISS_THRESHOLD as MISS_THRESHOLD,
12
12
  } from './DenoiserSettings.js';
13
13
 
14
14
  const NRD_EPS = 1e-6;
@@ -25,6 +25,12 @@ const FIREFLY_RADIUS_SCALE = 0.1;
25
25
  const FIREFLY_FAST_RELATIVE_INTENSITY = 4.0;
26
26
  const ALMOST_ZERO_ANGLE_COS = Math.cos( 89.0 * Math.PI / 180.0 );
27
27
  const CATROM_SHARPNESS = 0.5;
28
+ // The input fuses diffuse and specular, so it is demodulated by both lobes' albedo (NRD_MaterialFactors summed).
29
+ const SPECULAR_F0 = 0.04;
30
+ // A sample this dark reflects nothing diffusely: it carries emission, the environment or specular alone.
31
+ const BLACK_ALBEDO = 0.1 * SPECULAR_F0;
32
+ // Carried past the pre-pass in dataT.z, above the four occlusion bits.
33
+ const BLACK_BIT = 16;
28
34
  const WG_SIZE = 16;
29
35
 
30
36
  const PASS = {
@@ -111,6 +117,12 @@ const clampNegativeToZero = ( v ) => vec4( linearToYCoCg( yCoCgToLinear( v.xyz )
111
117
  const stdDev = ( m1, m2 ) => sqrt( m2.sub( m1.mul( m1 ) ).abs() );
112
118
  // rotator = (cos, sin, -sin, cos)
113
119
  const rotate2 = ( rot, v ) => vec2( rot.x.mul( v.x ).add( rot.y.mul( v.y ) ), rot.z.mul( v.x ).add( rot.w.mul( v.y ) ) );
120
+ const demodAlbedo = ( a ) => a.mul( 1.0 - SPECULAR_F0 ).add( SPECULAR_F0 );
121
+ const isBlack = ( a ) => max( a.x, max( a.y, a.z ) ).lessThan( BLACK_ALBEDO );
122
+ const isBlackData = ( data ) => data.z.greaterThan( BLACK_BIT - 0.5 );
123
+ // NormalDepth negates an emitter's roughness, as NRD packs a material ID into its normal-roughness guide.
124
+ const isEmitter = ( guideW ) => guideW.lessThan( 0.0 );
125
+ const guideRoughness = ( guideW ) => guideW.abs().clamp( 0.05, 1.0 );
114
126
  const denanify = ( w, s ) => select( w.equal( 0.0 ), vec4( 0.0 ), s );
115
127
 
116
128
  function hash01( n ) {
@@ -256,7 +268,7 @@ export class NRD extends RenderStage {
256
268
  this._geomPrev = tex();
257
269
  this._stabA = tex(); // .x stabilized luma
258
270
  this._stabB = tex();
259
- this._dataT = tex(); // .x fast luma, .y accumSpeed, .z occlusion bits, .w catrom ok
271
+ this._dataT = tex(); // .x fast luma, .y accumSpeed, .z occlusion bits (+ BLACK_BIT), .w catrom ok
260
272
 
261
273
  }
262
274
 
@@ -382,6 +394,13 @@ export class NRD extends RenderStage {
382
394
 
383
395
  }
384
396
 
397
+ // Taps whose radiance is no estimate of the centre's lighting.
398
+ _excluded( zs, guideW, tapBlack, centerBlack ) {
399
+
400
+ return zs.greaterThanEqual( MISS_THRESHOLD ).or( isEmitter( guideW ) ).or( tapBlack.and( centerBlack.not() ) );
401
+
402
+ }
403
+
385
404
  _bilinear( uv ) {
386
405
 
387
406
  const t = uv.mul( vec2( this.resW, this.resH ) ).sub( 0.5 );
@@ -524,11 +543,9 @@ export class NRD extends RenderStage {
524
543
  const inputFrames = this.inputFrames;
525
544
 
526
545
  const readSignal = isPre
527
- ? ( c ) => {
546
+ ? ( c, alb ) => {
528
547
 
529
- const col = textureLoad( colorTex, c );
530
- const alb = textureLoad( albedoTex, c );
531
- const lighting = sanitizeRGB( col.xyz.div( max( alb.xyz, vec3( ALBEDO_EPS ) ) ) );
548
+ const lighting = sanitizeRGB( textureLoad( colorTex, c ).xyz.div( demodAlbedo( alb.xyz ) ) );
532
549
  return vec4( linearToYCoCg( lighting ), sanitize1( alb.w, FP16_MAX ).min( 1.0 ) );
533
550
 
534
551
  }
@@ -547,10 +564,12 @@ export class NRD extends RenderStage {
547
564
  const isSky = dist.greaterThanEqual( MISS_THRESHOLD );
548
565
  const sn = textureLoad( snTex, coord ).toVar();
549
566
  const N = normalize( sn.xyz.mul( 2.0 ).sub( 1.0 ) ).toVar();
550
- const roughness = sn.w.clamp( 0.05, 1.0 ).toVar();
567
+ const roughness = guideRoughness( sn.w ).toVar();
551
568
  const viewZ = float( MISS_THRESHOLD ).toVar();
552
569
 
553
- const result = readSignal( coord ).toVar();
570
+ const albedo = isPre ? textureLoad( albedoTex, coord ) : null;
571
+ const black = ( isPre ? isBlack( albedo ) : isBlackData( textureLoad( dataTex, coord ) ) ).toVar();
572
+ const result = readSignal( coord, albedo ).toVar();
554
573
 
555
574
  If( isSky.not(), () => {
556
575
 
@@ -558,7 +577,7 @@ export class NRD extends RenderStage {
558
577
  const X = this._worldPosAt( pixelUv, dist, cwm, cpi ).toVar();
559
578
  viewZ.assign( camView.mul( vec4( X, 1.0 ) ).z.abs() );
560
579
 
561
- If( maxRadiusU.greaterThan( 0.0 ), () => {
580
+ If( maxRadiusU.greaterThan( 0.0 ).and( isEmitter( sn.w ).not() ), () => {
562
581
 
563
582
  const V = this._viewVector( X, camPos, this.viewVector );
564
583
  const NoV = dot( N, V ).abs().toVar();
@@ -625,11 +644,13 @@ export class NRD extends RenderStage {
625
644
 
626
645
  const angle = acosApproxPositive( dot( N, Ns ) );
627
646
  w.mulAssign( weightSmooth( angle, normalParam, float( 0.0 ) ) );
628
- w.mulAssign( weightSmooth( sns.w, roughParams.x, roughParams.y ) );
647
+ w.mulAssign( weightSmooth( guideRoughness( sns.w ), roughParams.x, roughParams.y ) );
629
648
  w.mulAssign( weightSmooth( dot( N, Xs ), geomParams.x, geomParams.y ) );
630
- w.assign( select( zs.greaterThanEqual( MISS_THRESHOLD ), float( 0.0 ), w ) );
649
+ const albS = isPre ? textureLoad( albedoTex, pos ) : null;
650
+ const blackS = isPre ? isBlack( albS ) : isBlackData( textureLoad( dataTex, pos ) );
651
+ w.assign( select( this._excluded( zs, sns.w, blackS, black ), float( 0.0 ), w ) );
631
652
 
632
- const s = denanify( w, readSignal( pos ) );
653
+ const s = denanify( w, readSignal( pos, albS ) );
633
654
  w.mulAssign( minHitDistWeight.add( weightExp( s.w, hdParams.x, hdParams.y ) ) );
634
655
 
635
656
  sum.addAssign( w );
@@ -675,6 +696,7 @@ export class NRD extends RenderStage {
675
696
  const geomTex = this._nGeom;
676
697
  const outTex = this._tmpB;
677
698
  const dataOut = this._dataT;
699
+ const albedoTex = this._albedoTexNode;
678
700
  const camView = this.camView;
679
701
  const prevCamView = this.prevCamView;
680
702
  const camPos = this.camPos;
@@ -709,7 +731,7 @@ export class NRD extends RenderStage {
709
731
 
710
732
  const sn = textureLoad( snTex, coord );
711
733
  const N = normalize( sn.xyz.mul( 2.0 ).sub( 1.0 ) ).toVar();
712
- const roughness = sn.w.clamp( 0.05, 1.0 ).toVar();
734
+ const roughness = guideRoughness( sn.w ).toVar();
713
735
 
714
736
  const pixelUv = this._pixelUv( gx, gy );
715
737
  const X = this._worldPosAt( pixelUv, dist, cwm, cpi ).toVar();
@@ -806,7 +828,8 @@ export class NRD extends RenderStage {
806
828
  const fastClamped = min( fast, history.x.mul( maxRel ).mul( FIREFLY_FAST_RELATIVE_INTENSITY ) );
807
829
  fast.assign( mix( fast, fastClamped, aff ) );
808
830
 
809
- textureStore( dataOut, uvec, vec4( fast, accumSpeed, fbits, select( allowCatRom, float( 1.0 ), float( 0.0 ) ) ) ).toWriteOnly();
831
+ const blackBit = select( isBlack( textureLoad( albedoTex, coord ) ), float( BLACK_BIT ), float( 0.0 ) );
832
+ textureStore( dataOut, uvec, vec4( fast, accumSpeed, fbits.add( blackBit ), select( allowCatRom, float( 1.0 ), float( 0.0 ) ) ) ).toWriteOnly();
810
833
 
811
834
  } );
812
835
 
@@ -862,7 +885,7 @@ export class NRD extends RenderStage {
862
885
 
863
886
  const sn = textureLoad( snTex, coord );
864
887
  const N = normalize( sn.xyz.mul( 2.0 ).sub( 1.0 ) ).toVar();
865
- const roughness = sn.w.clamp( 0.05, 1.0 ).toVar();
888
+ const roughness = guideRoughness( sn.w ).toVar();
866
889
  const pixelUv = this._pixelUv( gx, gy );
867
890
  const X = this._worldPosAt( pixelUv, dist, cwm, cpi ).toVar();
868
891
  const viewZ = camView.mul( vec4( X, 1.0 ) ).z.abs();
@@ -881,13 +904,16 @@ export class NRD extends RenderStage {
881
904
  .mul( mix( float( 0.25 ).add( sqrt( hitDistFactor ).mul( 0.75 ) ), float( 1.0 ), nonLinear ) );
882
905
  const strideR = round( stride ).toVar();
883
906
 
884
- If( strideR.notEqual( 0.0 ), () => {
907
+ const emitter = isEmitter( sn.w ).toVar();
908
+
909
+ If( strideR.notEqual( 0.0 ).and( emitter.not() ), () => {
885
910
 
886
911
  const normalParam = normalWeightParam( nonLinear, u.lobeAngleFraction, roughness, u.lobeVolumePercent ).toVar();
887
912
  const geomParams = geometryWeightParams( u.planeDistSensitivity, frustumSize, X, N ).toVar();
888
913
  const hdParams = hitDistWeightParams( centerH, nonLinear ).toVar();
889
914
  const relaxedRough = relaxedRoughnessWeightParams( roughness.mul( roughness ), sqrt( u.roughnessFraction ), ROUGHNESS_SENSITIVITY ).toVar();
890
915
 
916
+ const black = isBlackData( data ).toVar();
891
917
  const sumd = float( 1.0 ).add( frameNum ).toVar();
892
918
  diff.mulAssign( sumd );
893
919
 
@@ -909,10 +935,12 @@ export class NRD extends RenderStage {
909
935
  const angle = acosApproxPositive( dot( Ns, N ) );
910
936
 
911
937
  const w = weightExp( angle, normalParam, float( 0.0 ) ).toVar();
912
- w.mulAssign( weightExp( sns.w.mul( sns.w ), relaxedRough.x, relaxedRough.y ) );
913
- w.mulAssign( float( 1.0 ).add( textureLoad( dataTex, pos ).y ) );
938
+ const rs = guideRoughness( sns.w );
939
+ w.mulAssign( weightExp( rs.mul( rs ), relaxedRough.x, relaxedRough.y ) );
940
+ const dataS = textureLoad( dataTex, pos );
941
+ w.mulAssign( float( 1.0 ).add( dataS.y ) );
914
942
  w.mulAssign( weightSmooth( dot( N, Xs ), geomParams.x, geomParams.y ) );
915
- w.assign( select( zs.greaterThanEqual( MISS_THRESHOLD ), float( 0.0 ), w ) );
943
+ w.assign( select( this._excluded( zs, sns.w, isBlackData( dataS ), black ), float( 0.0 ), w ) );
916
944
 
917
945
  const s = denanify( w, textureLoad( inTex, pos ) );
918
946
  w.mulAssign( weightExp( s.w, hdParams.x, hdParams.y ) );
@@ -1014,7 +1042,7 @@ export class NRD extends RenderStage {
1014
1042
 
1015
1043
  }
1016
1044
 
1017
- textureStore( outTex, uvec, changeLuma( diff, diffLuma ) ).toWriteOnly();
1045
+ textureStore( outTex, uvec, select( emitter, diff, changeLuma( diff, diffLuma ) ) ).toWriteOnly();
1018
1046
 
1019
1047
  } );
1020
1048
 
@@ -1067,7 +1095,8 @@ export class NRD extends RenderStage {
1067
1095
 
1068
1096
  } );
1069
1097
 
1070
- const roughness = textureLoad( snTex, coord ).w.clamp( 0.05, 1.0 );
1098
+ const guideW = textureLoad( snTex, coord ).w;
1099
+ const roughness = guideRoughness( guideW );
1071
1100
  const pixelUv = this._pixelUv( gx, gy );
1072
1101
  const mv = textureLoad( motionTex, coord );
1073
1102
  const smbUv = pixelUv.sub( mv.xy ).toVar();
@@ -1107,7 +1136,7 @@ export class NRD extends RenderStage {
1107
1136
  m2.divAssign( 9.0 );
1108
1137
  const sigma = stdDev( m1, m2 ).toVar();
1109
1138
 
1110
- If( accumSpeed.lessThan( u.historyFixFrameNum ), () => {
1139
+ If( accumSpeed.lessThan( u.historyFixFrameNum ).and( isEmitter( guideW ).not() ), () => {
1111
1140
 
1112
1141
  diffLuma.assign( min( diffLuma, m1.mul( float( 1.2 ).add( float( 1.0 ).div( accumSpeed.add( 1.0 ) ) ) ) ) );
1113
1142
 
@@ -1145,7 +1174,7 @@ export class NRD extends RenderStage {
1145
1174
 
1146
1175
  // Alpha stays the path tracer's coverage — Compositor reads it under transparentBackground.
1147
1176
  const albedo = textureLoad( albedoTex, coord );
1148
- const lighting = yCoCgToLinear( diff.xyz ).mul( max( albedo.xyz, vec3( ALBEDO_EPS ) ) );
1177
+ const lighting = yCoCgToLinear( diff.xyz ).mul( demodAlbedo( albedo.xyz ) );
1149
1178
  const out = vec4( lighting, color.w ).toVar();
1150
1179
 
1151
1180
  If( debugMode.equal( int( 1 ) ), () => {
@@ -1168,7 +1197,7 @@ export class NRD extends RenderStage {
1168
1197
 
1169
1198
  } ).ElseIf( debugMode.equal( int( 5 ) ), () => {
1170
1199
 
1171
- out.assign( vec4( vec3( data.z.div( 15.0 ) ), 1.0 ) );
1200
+ out.assign( vec4( vec3( float( bits.bitAnd( uint( 15 ) ) ).div( 15.0 ) ), 1.0 ) );
1172
1201
 
1173
1202
  } );
1174
1203
 
@@ -1,4 +1,4 @@
1
- import { Fn, vec2, vec4, float, int, uint, uvec2, uvec4, uniform, min, storage, If,
1
+ import { Fn, vec2, vec4, float, int, uint, uvec2, uvec4, uniform, min, max, select, storage, If,
2
2
  textureStore, workgroupId, localId } from 'three/tsl';
3
3
  import { RenderTarget, StorageTexture } from 'three/webgpu';
4
4
  import { HalfFloatType, RGBAFormat, RedIntegerFormat, UnsignedIntType, NearestFilter, Matrix4, Box2, Vector2 } from 'three';
@@ -7,7 +7,7 @@ import { MAX_STORAGE_TEXTURE_SIZE } from '../Processor/StorageTexturePool.js';
7
7
  import { GBUFFER_MISS_DEPTH } from './DenoiserSettings.js';
8
8
  import { Ray, HitInfo, RayTracingMaterial, UVCache } from '../TSL/Struct.js';
9
9
  import { traverseBVH } from '../TSL/BVHTraversal.js';
10
- import { cameraRayOf, cameraRayUniforms } from '../TSL/CameraRay.js';
10
+ import { cameraRayOf, cameraRayUniforms, cameraClipDistance } from '../TSL/CameraRay.js';
11
11
  import { getMaterial } from '../TSL/Common.js';
12
12
  import { computeUVCache, processNormal, processBump, processMetalnessRoughness, triangleUVTangent, buildBucketTextureNodes, refreshBucketTextureNodes } from '../TSL/TextureSampling.js';
13
13
  import { withSceneResources } from '../TSL/SceneResources.js';
@@ -31,10 +31,10 @@ import { withSceneResources } from '../TSL/SceneResources.js';
31
31
  * map, recomputed from the SAME deterministic hit — no extra ray) so the
32
32
  * spatial denoiser's edge-stop can see normal-map detail the flat geometric
33
33
  * normal hides. Deterministic ⇒ jitter-free, so it's safe for the gates.
34
- * Its .w is the sampled material roughness (NRD's normal+roughness guide).
34
+ * Its .w is the sampled material roughness (NRD's normal+roughness guide), negated on an emitter.
35
35
  *
36
36
  * Publishes: pathtracer:normalDepth, pathtracer:prevNormalDepth,
37
- * pathtracer:shadingNormal (rgb = shading normal·0.5+0.5, a = roughness),
37
+ * pathtracer:shadingNormal (rgb = shading normal·0.5+0.5, a = roughness, negative on an emitter),
38
38
  * pathtracer:instanceLeaf (r32uint: the hit's transformed TLAS leaf + 1, 0 = none)
39
39
  */
40
40
  export class NormalDepth extends RenderStage {
@@ -233,14 +233,18 @@ export class NormalDepth extends RenderStage {
233
233
  // No jitter — deterministic per-pixel ray so the temporal gate
234
234
  // sees stable per-pixel normals across frames.
235
235
  const uv = vec2( float( gx ).add( 0.5 ).div( resW ), float( gy ).add( 0.5 ).div( resH ) );
236
- const ray = Ray( cameraRayOf( uv, camWorldMat, camProjInvMat, this.cameraRay ) );
236
+ const primary = cameraRayOf( uv, camWorldMat, camProjInvMat, this.cameraRay );
237
+ const direction = primary.direction.toVar();
238
+ // From the camera's clip plane, as the path tracer's rays start; the depth stays measured from the camera.
239
+ const clipAlong = cameraClipDistance( direction, this.cameraRay.clipStart, camWorldMat, this.cameraRay.cameraProjection ).toVar();
240
+ const ray = Ray( { origin: primary.origin.add( direction.mul( clipAlong ) ), direction } );
237
241
  const hit = HitInfo.wrap( traverseBVH( ray, bvhStorage, triStorage ) );
238
242
 
239
243
  const encodedNormal = hit.normal.mul( 0.5 ).add( 0.5 );
240
244
  // Clamp to the max finite HalfFloat: a hit farther than 65504 units would otherwise
241
245
  // round to +Inf in the f16 texture (on backends that don't clamp), reintroducing the
242
246
  // Inf-Inf=NaN in the denoiser depth weights that the finite miss-sentinel avoids.
243
- const depth = min( hit.dst, float( 65504.0 ) );
247
+ const depth = min( hit.dst.add( clipAlong ), float( 65504.0 ) );
244
248
 
245
249
  const result = hit.didHit.select(
246
250
  vec4( encodedNormal, depth ),
@@ -289,6 +293,8 @@ export class NormalDepth extends RenderStage {
289
293
  shadingNormal.assign( processBump( mapped, material, uvCache ) );
290
294
  // Same floor the Shade kernel applies before shading.
291
295
  roughness.assign( processMetalnessRoughness( material, uvCache ).y.clamp( 0.05, 1.0 ) );
296
+ const emits = max( material.emissive.x, max( material.emissive.y, material.emissive.z ) ).mul( material.emissiveIntensity ).greaterThan( 0.0 );
297
+ roughness.assign( select( emits, roughness.negate(), roughness ) );
292
298
 
293
299
  } );
294
300
 
@@ -27,6 +27,7 @@ import {
27
27
  SORT_GLOBAL_WG_SIZE, SORT_GLOBAL_MAX_BINS,
28
28
  } from '../TSL/SortGlobalKernels.js';
29
29
  import { ENGINE_DEFAULTS } from '../EngineDefaults.js';
30
+ import { LIGHT_FLOATS } from '../managers/UniformManager.js';
30
31
  import { MAX_STORAGE_TEXTURE_SIZE } from '../Processor/StorageTexturePool.js';
31
32
  import { readPixels } from '../Processor/TextureReadback.js';
32
33
  import { ISSUE_CODES } from '../EngineIssues.js';
@@ -380,6 +381,26 @@ export class PathTracer extends PathTracerStage {
380
381
 
381
382
  }
382
383
 
384
+ /**
385
+ * Whether some point or spot light has a radius: only then is disc sampling compiled in, and never for an integrator
386
+ * of its own (a bidirectional light path leaves a light's centre).
387
+ */
388
+ _lightRadiusNeeded() {
389
+
390
+ if ( this._integrator ) return false;
391
+ const sized = ( type, count, slot ) => {
392
+
393
+ if ( ! count ) return false;
394
+ const data = this[ `${type}LightsBufferNode` ].array;
395
+ for ( let i = 0; i < count; i ++ ) if ( data[ i * LIGHT_FLOATS[ type ] + slot ] > 0 ) return true;
396
+ return false;
397
+
398
+ };
399
+
400
+ return sized( 'point', this.numPointLights.value, 9 ) || sized( 'spot', this.numSpotLights.value, 18 );
401
+
402
+ }
403
+
383
404
  get integrator() {
384
405
 
385
406
  return this._integrator?.name ?? 'path';
@@ -406,10 +427,12 @@ export class PathTracer extends PathTracerStage {
406
427
  this._materialFeaturesChanged = false;
407
428
  const needed = this._layersToCompile();
408
429
  if ( MATERIAL_LAYERS.some( ( layer ) => needed[ layer ] && ! this._compiledLayers?.[ layer ] ) ) this._outputsChanged = true;
430
+ if ( ! this._compiledTransmissionMaps && this.materialData.hasTransmissionMaps() ) this._outputsChanged = true;
409
431
 
410
432
  }
411
433
 
412
434
  if ( PICKED_SHADOW_RAYS.includes( this._shadowRays ) && this._pickedShadowRays() !== this._compiledPick ) this._outputsChanged = true;
435
+ if ( this._lightRadiusNeeded() !== this._compiledLightRadius ) this._outputsChanged = true;
413
436
 
414
437
  if ( this._lightBufferRealloc || this._outputsChanged ) {
415
438
 
@@ -1636,6 +1659,7 @@ export class PathTracer extends PathTracerStage {
1636
1659
  anamorphicRatio: this.anamorphicRatio,
1637
1660
  dofMode: this.dofMode,
1638
1661
  dofBlur: this.dofBlur,
1662
+ cameraClipStart: this.cameraClipStart,
1639
1663
  renderWidth: this._wfRenderWidth,
1640
1664
  renderHeight: this._wfRenderHeight,
1641
1665
  chunkRowBase: this._wfChunkRowBase,
@@ -1851,6 +1875,8 @@ export class PathTracer extends PathTracerStage {
1851
1875
  iesProfiles: texNodes.iesProfilesTex,
1852
1876
  alphaShadows: this.uniforms.get( 'enableAlphaShadows' ),
1853
1877
  materialLayers: this._compiledLayers = this._layersToCompile(),
1878
+ lightRadius: this._compiledLightRadius = this._lightRadiusNeeded(),
1879
+ transmissionMaps: this._compiledTransmissionMaps = this.materialData.hasTransmissionMaps(),
1854
1880
  };
1855
1881
  const own = ( call ) => withSceneResources( call, resources );
1856
1882
  const freshEnvTex = _env.environmentTexture ? texture( _env.environmentTexture ) : texNodes.envTex;
@@ -2089,7 +2115,6 @@ export class PathTracer extends PathTracerStage {
2089
2115
  hasPreviousAux: this.hasPreviousAux,
2090
2116
  auxAccumulationAlpha: this.auxAccumulationAlpha,
2091
2117
  cameraIsMoving: this.cameraIsMoving,
2092
- transparentBackground: this.transparentBackground,
2093
2118
  prevAccumTexture: prevColor,
2094
2119
  prevAlbedoTexture: prevAlbedo,
2095
2120
  prevNormalDepthTexture: prevNormalDepth,
@@ -2146,6 +2171,7 @@ export class PathTracer extends PathTracerStage {
2146
2171
  anamorphicRatio: this.anamorphicRatio,
2147
2172
  dofMode: this.dofMode,
2148
2173
  dofBlur: this.dofBlur,
2174
+ cameraClipStart: this.cameraClipStart,
2149
2175
  bvhBuffer: freshBvh,
2150
2176
  triangleBuffer: freshTri,
2151
2177
  materialBuffer: freshMat,
@@ -1018,7 +1018,11 @@ export class PathTracerStage extends RenderStage {
1018
1018
 
1019
1019
  this._whenTLASResident( () => {
1020
1020
 
1021
- if ( ! this._patchTLASLeafVisibility( meshIndex, visible ) ) return;
1021
+ // Leaves are per placement: an InstancedMesh holds a run of them, and shifts every index after it.
1022
+ const run = this._instanceTable?.placementRunOf( meshIndex );
1023
+ let patched = false;
1024
+ for ( let p = run?.start; run && p < run.start + run.count; p ++ ) patched = this._patchTLASLeafVisibility( p, visible ) || patched;
1025
+ if ( ! patched ) return;
1022
1026
  this._patchGroupNodes();
1023
1027
  this._flushBVHEdits();
1024
1028
 
@@ -1437,13 +1441,18 @@ export class PathTracerStage extends RenderStage {
1437
1441
  */
1438
1442
  _updateCameraUniforms() {
1439
1443
 
1444
+ // Only an imported camera's clip plane: `near` is the viewer's own for its default camera.
1445
+ const clipStart = Math.max( this.camera.userData?.clipStart ?? 0, 0 );
1446
+
1440
1447
  if ( ! this._matricesApproxEqual( this.lastCameraMatrix, this.camera.matrixWorld ) ||
1441
- ! this._matricesApproxEqual( this.lastProjectionMatrix, this.camera.projectionMatrixInverse ) ) {
1448
+ ! this._matricesApproxEqual( this.lastProjectionMatrix, this.camera.projectionMatrixInverse ) ||
1449
+ clipStart !== this.cameraClipStart.value ) {
1442
1450
 
1443
1451
  this.cameraWorldMatrix.value.copy( this.camera.matrixWorld );
1444
1452
  this.cameraViewMatrix.value.copy( this.camera.matrixWorldInverse );
1445
1453
  this.cameraProjectionMatrix.value.copy( this.camera.projectionMatrix );
1446
1454
  this.cameraProjectionMatrixInverse.value.copy( this.camera.projectionMatrixInverse );
1455
+ this.cameraClipStart.value = clipStart;
1447
1456
 
1448
1457
  this.lastCameraMatrix.copy( this.camera.matrixWorld );
1449
1458
  this.lastProjectionMatrix.copy( this.camera.projectionMatrixInverse );
@@ -1503,6 +1512,9 @@ export class PathTracerStage extends RenderStage {
1503
1512
  // freezes when the frame retires or the camera moves, which is what a denoiser needs.
1504
1513
  context.setState( 'pathtracer:samples', this.frameCount );
1505
1514
  context.setState( 'renderMode', this.renderMode.value );
1515
+ // What a camera ray that sees nothing shows; the colour texture's alpha is 0 there in every case.
1516
+ context.setState( 'pathtracer:backdrop', this.uniforms.get( 'transparentBackground' ).value ? 'transparent'
1517
+ : this.uniforms.get( 'showBackground' ).value ? 'environment' : 'color' );
1506
1518
 
1507
1519
  }
1508
1520
 
@@ -284,10 +284,11 @@ export function encodeSceneGraph( root, { environment = null, animations = [], s
284
284
 
285
285
  /**
286
286
  * Writes an encoded graph into a storage entry: `graph.json` and `data.bin`.
287
- * @param {{release?: boolean}} [options] - drop each section's array once written, so a graph
288
- * written during a build does not keep arrays the build has since replaced
287
+ * @param {{release?: boolean, signal?: AbortSignal}} [options] - `release` drops each section's array once
288
+ * written, so a graph written during a build does not keep arrays the build has since replaced; `signal`
289
+ * stops the write between pieces
289
290
  */
290
- export async function writeSceneGraph( writer, { manifest, sections }, { release = false } = {} ) {
291
+ export async function writeSceneGraph( writer, { manifest, sections }, { release = false, signal = null } = {} ) {
291
292
 
292
293
  for ( const section of sections ) {
293
294
 
@@ -295,6 +296,7 @@ export async function writeSceneGraph( writer, { manifest, sections }, { release
295
296
  const bytes = new Uint8Array( array.buffer, array.byteOffset, array.byteLength );
296
297
  for ( let at = 0; at < bytes.length; at += PIECE_BYTES ) {
297
298
 
299
+ signal?.throwIfAborted();
298
300
  await writer.write( 'data.bin', bytes.subarray( at, Math.min( bytes.length, at + PIECE_BYTES ) ), { at: offset + at } );
299
301
 
300
302
  }
@@ -304,6 +306,7 @@ export async function writeSceneGraph( writer, { manifest, sections }, { release
304
306
  }
305
307
 
306
308
  if ( sections.length === 0 ) await writer.write( 'data.bin', new Uint8Array( 0 ) );
309
+ signal?.throwIfAborted();
307
310
  await writer.writeJSON( 'graph.json', manifest );
308
311
 
309
312
  }
@@ -32,7 +32,7 @@ import {
32
32
  } from '../Processor/BufferLayout.js';
33
33
  import { HitInfo } from './Struct.js';
34
34
  import {
35
- getDatafromStorageBuffer, instanceRows, instanceNormalToWorld, unpackTriangleNormal, triangleRow, shadowFlagsSettle
35
+ getDatafromStorageBuffer, storageRows, instanceRows, instanceNormalToWorld, unpackTriangleNormal, triangleRow, shadowFlagsSettle
36
36
  } from './Common.js';
37
37
 
38
38
  const MAX_STACK_DEPTH = 32;
@@ -51,6 +51,15 @@ const HUGE_VAL = 1e8;
51
51
 
52
52
  const createStack = () => array( 'int', MAX_STACK_DEPTH ).toVar();
53
53
 
54
+ // Row k of BVH node `nodeIndex`. One buffer reads each row where it is used; over parts, `rows` are read in one branch.
55
+ const nodeRows = ( bvhBuffer, nodeIndex, rows = [ 0, 1, 2, 3 ] ) => {
56
+
57
+ if ( ! bvhBuffer.isSplitStorage ) return ( k ) => getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( k ), int( BVH_STRIDE ) );
58
+ const read = storageRows( bvhBuffer, int( nodeIndex ).mul( BVH_STRIDE ), rows );
59
+ return ( k ) => read[ rows.indexOf( k ) ];
60
+
61
+ };
62
+
54
63
  // A negative stack entry is a leaf folded into its parent, `~( first << 4 | count )` as the parent
55
64
  // stores it, so child slots are pushed as they are and a leaf needs no fetch.
56
65
  const foldedFirst = ( entry ) => int( uint( entry.bitNot() ).shiftRight( uint( 4 ) ) );
@@ -91,10 +100,10 @@ const toObjectDir = ( rows, d ) => vec3(
91
100
  // Compare-swaps sorting up to four ascending.
92
101
  const SORT_NETWORKS = [[], [], [[ 0, 1 ]], [[ 0, 1 ], [ 1, 2 ], [ 0, 1 ]], [[ 0, 1 ], [ 2, 3 ], [ 0, 2 ], [ 1, 3 ], [ 1, 2 ]]];
93
102
 
94
- const pushHitCopies = ( bvhBuffer, nodeIndex, nodeData0, origin, invDir, stack, stackPtr, limit ) => {
103
+ const pushHitCopies = ( node, nodeIndex, nodeData0, origin, invDir, stack, stackPtr, limit ) => {
95
104
 
96
- const d1 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 1 ), int( BVH_STRIDE ) );
97
- const d2 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 2 ), int( BVH_STRIDE ) );
105
+ const d1 = node( 1 );
106
+ const d2 = node( 2 );
98
107
  const base = vec3( nodeData0.z, d1.x, d1.y ).toVar();
99
108
  const exps = floatBitsToUint( nodeData0.y ).toVar();
100
109
  const stepOf = ( a ) => uintBitsToFloat( exps.shiftRight( uint( a * 8 ) ).bitAnd( uint( 255 ) ).shiftLeft( uint( 23 ) ) );
@@ -363,8 +372,9 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
363
372
 
364
373
  const leaf = int( uint( entry ).bitAnd( uint( ~ COPY_ENTRY >>> 0 ) ).shiftRight( uint( 2 ) ) );
365
374
  const k = uint( entry ).bitAnd( uint( 3 ) ).toVar();
366
- const head = getDatafromStorageBuffer( bvhBuffer, leaf, int( 0 ), int( BVH_STRIDE ) );
367
- const words = floatBitsToUint( getDatafromStorageBuffer( bvhBuffer, leaf, int( 3 ), int( BVH_STRIDE ) ) ).toVar();
375
+ const node = nodeRows( bvhBuffer, leaf, [ 0, 3 ] );
376
+ const head = node( 0 );
377
+ const words = floatBitsToUint( node( 3 ) ).toVar();
368
378
  const word = select( k.equal( uint( 0 ) ), words.x, select( k.equal( uint( 1 ) ), words.y, select( k.equal( uint( 2 ) ), words.z, words.w ) ) ).toVar();
369
379
 
370
380
  If( word.bitAnd( uint( CLUSTER_COPY_HIDDEN ) ).equal( uint( 0 ) ), () => {
@@ -483,12 +493,13 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
483
493
  const visitNode = () => {
484
494
 
485
495
  const nodeIndex = entry;
496
+ const node = nodeRows( bvhBuffer, nodeIndex );
486
497
 
487
498
  // 4 vec4 per node
488
499
  // Leaf: vec4(0) = [triOffset, triCount, 0, TRIANGLE_LEAF]
489
500
  // Inner: vec4(0) = [leftMin.xyz, leftChild], vec4(1) = [leftMax.xyz, rightChild],
490
501
  // vec4(2) = [rightMin.xyz, 0], vec4(3) = [rightMax.xyz, 0]
491
- const nodeData0 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 0 ), int( BVH_STRIDE ) );
502
+ const nodeData0 = node( 0 );
492
503
  if ( trackStats ) closestHit.boxTests.addAssign( 1 );
493
504
 
494
505
  // Slot [3] carries a u32 bit pattern: an inner node's left child, or a leaf tag. Stored as
@@ -504,7 +515,7 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
504
515
 
505
516
  } ).Else( clustered ? () => {
506
517
 
507
- pushHitCopies( bvhBuffer, nodeIndex, nodeData0, worldOrigin, worldInvDir, stack, stackPtr, closestHit.dst );
518
+ pushHitCopies( node, nodeIndex, nodeData0, worldOrigin, worldInvDir, stack, stackPtr, closestHit.dst );
508
519
 
509
520
  } : () => {
510
521
 
@@ -516,7 +527,7 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
516
527
  // A baked placement already sits in world space; only a transformed one moves the ray.
517
528
  If( floatBitsToUint( nodeData0.y ).bitAnd( uint( TLAS_LEAF_IDENTITY ) ).equal( uint( 0 ) ), () => {
518
529
 
519
- const rows = instanceRows( bvhBuffer, nodeIndex );
530
+ const rows = [ node( 1 ), node( 2 ), node( 3 ) ];
520
531
  const localDir = toObjectDir( rows, worldDirection ).toVar();
521
532
 
522
533
  rayOrigin.assign( toObjectPoint( rows, worldOrigin ) );
@@ -547,9 +558,9 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
547
558
  } ).Else( () => {
548
559
 
549
560
  // Inner node — child AABBs stored in this node (4 reads total, no child fetches)
550
- const nodeData1 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 1 ), int( BVH_STRIDE ) );
551
- const nodeData2 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 2 ), int( BVH_STRIDE ) );
552
- const nodeData3 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 3 ), int( BVH_STRIDE ) );
561
+ const nodeData1 = node( 1 );
562
+ const nodeData2 = node( 2 );
563
+ const nodeData3 = node( 3 );
553
564
 
554
565
  const leftChild = int( nodeTag ).toVar();
555
566
  const rightChild = int( floatBitsToUint( nodeData1.w ) ).toVar();
@@ -705,8 +716,9 @@ const makeTraverseBVHShadow = ( cameraCulled ) => Fn( ( [
705
716
 
706
717
  const leaf = int( uint( entry ).bitAnd( uint( ~ COPY_ENTRY >>> 0 ) ).shiftRight( uint( 2 ) ) );
707
718
  const k = uint( entry ).bitAnd( uint( 3 ) ).toVar();
708
- const head = getDatafromStorageBuffer( bvhBuffer, leaf, int( 0 ), int( BVH_STRIDE ) );
709
- const words = floatBitsToUint( getDatafromStorageBuffer( bvhBuffer, leaf, int( 3 ), int( BVH_STRIDE ) ) ).toVar();
719
+ const node = nodeRows( bvhBuffer, leaf, [ 0, 3 ] );
720
+ const head = node( 0 );
721
+ const words = floatBitsToUint( node( 3 ) ).toVar();
710
722
  const word = select( k.equal( uint( 0 ) ), words.x, select( k.equal( uint( 1 ) ), words.y, select( k.equal( uint( 2 ) ), words.z, words.w ) ) ).toVar();
711
723
 
712
724
  If( word.bitAnd( uint( CLUSTER_COPY_HIDDEN ) ).equal( uint( 0 ) ), () => {
@@ -818,7 +830,8 @@ const makeTraverseBVHShadow = ( cameraCulled ) => Fn( ( [
818
830
  const visitNode = () => {
819
831
 
820
832
  const nodeIndex = entry;
821
- const nodeData0 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 0 ), int( BVH_STRIDE ) );
833
+ const node = nodeRows( bvhBuffer, nodeIndex );
834
+ const nodeData0 = node( 0 );
822
835
 
823
836
  // Slot [3] carries a u32 bit pattern: an inner node's left child, or a leaf tag. Stored as
824
837
  // float VALUES these rounded past 2^24 and sent the ray to a neighbouring node, which
@@ -833,7 +846,7 @@ const makeTraverseBVHShadow = ( cameraCulled ) => Fn( ( [
833
846
 
834
847
  } ).Else( clustered ? () => {
835
848
 
836
- pushHitCopies( bvhBuffer, nodeIndex, nodeData0, worldOrigin, worldInvDir, stack, stackPtr, closestHit.dst );
849
+ pushHitCopies( node, nodeIndex, nodeData0, worldOrigin, worldInvDir, stack, stackPtr, closestHit.dst );
837
850
 
838
851
  } : () => {
839
852
 
@@ -843,7 +856,7 @@ const makeTraverseBVHShadow = ( cameraCulled ) => Fn( ( [
843
856
  // A baked placement already sits in world space; only a transformed one moves the ray.
844
857
  If( floatBitsToUint( nodeData0.y ).bitAnd( uint( TLAS_LEAF_IDENTITY ) ).equal( uint( 0 ) ), () => {
845
858
 
846
- const rows = instanceRows( bvhBuffer, nodeIndex );
859
+ const rows = [ node( 1 ), node( 2 ), node( 3 ) ];
847
860
  const localDir = toObjectDir( rows, worldDirection ).toVar();
848
861
 
849
862
  rayOrigin.assign( toObjectPoint( rows, worldOrigin ) );
@@ -874,9 +887,9 @@ const makeTraverseBVHShadow = ( cameraCulled ) => Fn( ( [
874
887
  } ).Else( () => {
875
888
 
876
889
  // Inner node — child AABBs stored in this node
877
- const nodeData1 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 1 ), int( BVH_STRIDE ) );
878
- const nodeData2 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 2 ), int( BVH_STRIDE ) );
879
- const nodeData3 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 3 ), int( BVH_STRIDE ) );
890
+ const nodeData1 = node( 1 );
891
+ const nodeData2 = node( 2 );
892
+ const nodeData3 = node( 3 );
880
893
 
881
894
  const leftChild = int( nodeTag ).toVar();
882
895
  const rightChild = int( floatBitsToUint( nodeData1.w ) ).toVar();
@@ -14,7 +14,7 @@
14
14
 
15
15
  import {
16
16
  Fn, vec3, vec4, float, int, uniform,
17
- If, normalize, mat3, mix, sin, cos, cross, dot, select,
17
+ If, normalize, mat3, mix, sin, cos, cross, dot, select, max,
18
18
  } from 'three/tsl';
19
19
  import { Vector2 } from 'three';
20
20
 
@@ -39,10 +39,12 @@ export function cameraRayUniforms() {
39
39
  panoLonRange: uniform( new Vector2(), 'vec2' ),
40
40
  panoLatRange: uniform( new Vector2(), 'vec2' ),
41
41
  panoLevelHorizon: uniform( 1, 'int' ),
42
+ clipStart: uniform( 0, 'float' ),
42
43
  /** @param {import('../managers/UniformManager.js').UniformManager} source */
43
44
  sync( source ) {
44
45
 
45
46
  u.cameraProjection.value = source.get( 'cameraProjection' ).value;
47
+ u.clipStart.value = source.get( 'cameraClipStart' ).value;
46
48
  u.panoLonRange.value.copy( source.get( 'panoLonRange' ).value );
47
49
  u.panoLatRange.value.copy( source.get( 'panoLatRange' ).value );
48
50
  u.panoLevelHorizon.value = source.get( 'panoLevelHorizon' ).value;
@@ -70,6 +72,15 @@ export const cameraRayPoint = ( uv01, distance, cameraWorldMatrix, cameraProject
70
72
 
71
73
  };
72
74
 
75
+ /**
76
+ * How far along a camera ray its clip plane lies: Cycles' clip start, a plane `clipStart` in front of the camera (a
77
+ * sphere for a panorama). Camera rays start there, so a camera can see through a wall behind it.
78
+ */
79
+ export const cameraClipDistance = ( direction, clipStart, cameraWorldMatrix, cameraProjection ) => select(
80
+ cameraProjection.equal( PANORAMA ), clipStart,
81
+ clipStart.div( max( dot( direction, normalize( vec3( cameraWorldMatrix[ 2 ] ) ) ).negate(), 1e-4 ) )
82
+ );
83
+
73
84
  /** World-space primary ray origin for pixel uv: the camera, or its image plane when orthographic. */
74
85
  export const cameraRayOrigin = Fn( ( [ uv01, cameraWorldMatrix, cameraProjectionMatrixInverse, cameraProjection ] ) => {
75
86