rayzee 9.0.0 → 9.1.1

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 (95) hide show
  1. package/README.md +139 -8
  2. package/dist/{NeuralRetouch-CYOJ21Wn.js → NeuralRetouch-BRquqfmZ.js} +1 -1
  3. package/dist/{NeuralSuperRes-CQLCLpcC.js → NeuralSuperRes-CbNvQjCw.js} +3 -2
  4. package/dist/{ToneMapGPU-BVE852iN.js → ToneMapGPU-DpUZDMbQ.js} +2 -1
  5. package/dist/rayzee.es.js +10353 -5337
  6. package/dist/rayzee.umd.js +265 -265
  7. package/package.json +1 -1
  8. package/src/AssetConfig.js +4 -0
  9. package/src/EngineDefaults.js +38 -56
  10. package/src/EngineEvents.js +3 -0
  11. package/src/EngineIssues.js +6 -0
  12. package/src/Headless.js +3 -1
  13. package/src/Passes/AIUpscaler.js +2 -1
  14. package/src/Passes/OIDNDenoiser.js +3 -6
  15. package/src/PathTracerApp.js +290 -19
  16. package/src/Processor/ArchiveCache.js +120 -0
  17. package/src/Processor/ArchiveReader.js +179 -32
  18. package/src/Processor/AssetLoader.js +514 -76
  19. package/src/Processor/AtmosphereModel.js +465 -0
  20. package/src/Processor/BVHBuilder.js +4 -1
  21. package/src/Processor/BVHLeafFold.js +100 -0
  22. package/src/Processor/BVHRefitter.js +98 -214
  23. package/src/Processor/ChunkedRecords.js +189 -4
  24. package/src/Processor/EmissiveTriangleBuilder.js +8 -0
  25. package/src/Processor/GeometryExtractor.js +238 -40
  26. package/src/Processor/InstanceTable.js +55 -37
  27. package/src/Processor/KernelManager.js +4 -1
  28. package/src/Processor/PBRT/PBRTCurves.js +104 -27
  29. package/src/Processor/PBRT/PBRTParser.js +132 -22
  30. package/src/Processor/PBRT/PBRTSceneBuilder.js +192 -49
  31. package/src/Processor/PBRT/buffers.js +27 -0
  32. package/src/Processor/PBRT/index.js +55 -15
  33. package/src/Processor/PackedRayBuffer.js +10 -13
  34. package/src/Processor/ParallelBVHBuilder.js +36 -15
  35. package/src/Processor/PhysicalSky.js +308 -0
  36. package/src/Processor/QueueManager.js +1 -0
  37. package/src/Processor/SceneProcessor.js +1132 -181
  38. package/src/Processor/SplitBLAS.js +249 -0
  39. package/src/Processor/SunPosition.js +41 -0
  40. package/src/Processor/TextureCreator.js +21 -0
  41. package/src/Processor/Workers/AIUpscalerWorker.js +79 -52
  42. package/src/Processor/Workers/BVHWorker.js +17 -13
  43. package/src/Processor/ZipReader.js +207 -0
  44. package/src/RenderSettings.js +38 -0
  45. package/src/SceneState/SceneState.js +367 -0
  46. package/src/SceneState/portable.js +52 -0
  47. package/src/Stages/NormalDepth.js +7 -6
  48. package/src/Stages/PathTracer.js +108 -4
  49. package/src/Stages/PathTracerStage.js +251 -104
  50. package/src/Storage/BLASCache.js +190 -0
  51. package/src/Storage/CDFCache.js +77 -0
  52. package/src/Storage/DownloadCache.js +266 -0
  53. package/src/Storage/GeometrySpill.js +217 -0
  54. package/src/Storage/SceneGraphCodec.js +503 -0
  55. package/src/Storage/SpillStore.js +112 -0
  56. package/src/Storage/StorageManager.js +934 -0
  57. package/src/Storage/StorageOps.js +137 -0
  58. package/src/Storage/StorageWorker.js +37 -0
  59. package/src/Storage/events.js +29 -0
  60. package/src/Storage/identity.js +77 -0
  61. package/src/Storage/index.js +9 -0
  62. package/src/Storage/inlineTransport.js +24 -0
  63. package/src/Storage/locks.js +99 -0
  64. package/src/Storage/openStorage.js +87 -0
  65. package/src/Storage/sceneCachePolicy.js +12 -0
  66. package/src/Storage/shared.js +9 -0
  67. package/src/Storage/transport.js +62 -0
  68. package/src/TSL/Atmosphere.js +684 -0
  69. package/src/TSL/BVHTraversal.js +288 -239
  70. package/src/TSL/Common.js +11 -2
  71. package/src/TSL/Displacement.js +6 -6
  72. package/src/TSL/EmissiveSampling.js +5 -5
  73. package/src/TSL/EnvironmentCDF.js +176 -0
  74. package/src/TSL/GenerateKernel.js +3 -3
  75. package/src/TSL/HitFacet.js +4 -4
  76. package/src/TSL/LightsDirect.js +8 -8
  77. package/src/TSL/LightsSampling.js +55 -37
  78. package/src/TSL/Random.js +1 -0
  79. package/src/TSL/ShadeKernel.js +75 -19
  80. package/src/TSL/Sun.js +40 -0
  81. package/src/TSL/TextureSampling.js +6 -7
  82. package/src/index.js +18 -0
  83. package/src/managers/CameraManager.js +169 -1
  84. package/src/managers/EnvironmentManager.js +263 -33
  85. package/src/managers/GoboManager.js +19 -8
  86. package/src/managers/IESManager.js +3 -3
  87. package/src/managers/LightManager.js +98 -5
  88. package/src/managers/MaterialDataManager.js +22 -2
  89. package/src/managers/TransformManager.js +26 -0
  90. package/src/managers/UniformManager.js +5 -3
  91. package/src/managers/VideoRenderManager.js +3 -1
  92. package/src/managers/timeline/CameraTrack.js +22 -0
  93. package/src/managers/timeline/TimelineManager.js +14 -0
  94. package/src/version.js +3 -0
  95. package/src/Processor/ProceduralSky.js +0 -291
package/src/TSL/Common.js CHANGED
@@ -329,8 +329,17 @@ export const getDatafromStorageBuffer = Fn( ( [ buffer, stride, sampleIndex, dat
329
329
 
330
330
  } );
331
331
 
332
- /** uvec4 lanes per triangle record — see TRIANGLE_DATA_LAYOUT. */
333
- export const TRI_STRIDE = 5;
332
+ /**
333
+ * The five uvec4 rows of a triangle record (TRIANGLE_DATA_LAYOUT) live in two GPU buffers: rows
334
+ * 0–2, positions with their packed normals, in `tris.geo`; rows 3–4, UVs, flags and mesh index,
335
+ * in `tris.shade`. One buffer of 80 B a triangle hit the 4 GB storage-buffer limit at 53.6M.
336
+ */
337
+ export const TRI_GEO_ROWS = 3;
338
+ export const TRI_SHADE_ROWS = 2;
339
+
340
+ export const triangleRow = ( tris, triIndex, row ) => row < TRI_GEO_ROWS
341
+ ? getDatafromStorageBuffer( tris.geo, triIndex, int( row ), int( TRI_GEO_ROWS ) )
342
+ : getDatafromStorageBuffer( tris.shade, triIndex, int( row - TRI_GEO_ROWS ), int( TRI_SHADE_ROWS ) );
334
343
 
335
344
  // Unit normal to an octahedral snorm16 pair; GPU twin of packNormalOct in EngineDefaults.
336
345
  export const packNormalOct = /*@__PURE__*/ wgslFn( `
@@ -1,7 +1,7 @@
1
1
  import { Fn, float, vec2, int, If, Loop, abs, normalize, dot, max, uintBitsToFloat } from 'three/tsl';
2
2
 
3
3
  import { struct } from './patches.js';
4
- import { getDatafromStorageBuffer, TRI_STRIDE, instanceRows, instanceDirToWorld } from './Common.js';
4
+ import { triangleRow, instanceRows, instanceDirToWorld } from './Common.js';
5
5
  import { sampleDisplacementMap, bucketTexelSize, getLinearBucketTextures } from './TextureSampling.js';
6
6
 
7
7
  // Ray-displacement intersection configuration
@@ -42,12 +42,12 @@ export const refineDisplacedIntersection = Fn( ( [
42
42
  // Fetch triangle vertex data
43
43
  const triIdx = hitInfo.triangleIndex;
44
44
 
45
- const pA = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, triIdx, int( 0 ), int( TRI_STRIDE ) ).xyz ).toVar();
46
- const pB = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, triIdx, int( 1 ), int( TRI_STRIDE ) ).xyz );
47
- const pC = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, triIdx, int( 2 ), int( TRI_STRIDE ) ).xyz );
45
+ const pA = uintBitsToFloat( triangleRow( triangleBuffer, triIdx, 0 ).xyz ).toVar();
46
+ const pB = uintBitsToFloat( triangleRow( triangleBuffer, triIdx, 1 ).xyz );
47
+ const pC = uintBitsToFloat( triangleRow( triangleBuffer, triIdx, 2 ).xyz );
48
48
 
49
- const uvData1 = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, triIdx, int( 3 ), int( TRI_STRIDE ) ) ).toVar();
50
- const uvData2 = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, triIdx, int( 4 ), int( TRI_STRIDE ) ).xy );
49
+ const uvData1 = uintBitsToFloat( triangleRow( triangleBuffer, triIdx, 3 ) ).toVar();
50
+ const uvData2 = uintBitsToFloat( triangleRow( triangleBuffer, triIdx, 4 ).xy );
51
51
 
52
52
  const uvA = uvData1.xy.toVar();
53
53
  const uvB = uvData1.zw;
@@ -30,7 +30,7 @@ import { struct } from './patches.js';
30
30
  import {
31
31
  MIN_PDF, getDatafromStorageBuffer, powerHeuristic, MATERIAL_SLOTS, MATERIAL_SLOT,
32
32
  computeDotProductsAniso, instanceRows, instanceNormalToWorld, instancePointToWorld,
33
- unpackTriangleNormal, TRI_STRIDE, SHADOW_END
33
+ unpackTriangleNormal, triangleRow, SHADOW_END
34
34
  } from './Common.js';
35
35
  import { TRI_MATERIAL_MASK } from '../EngineDefaults.js';
36
36
  import { getRandomSample1D, getRandomSample2D } from './Random.js';
@@ -298,11 +298,11 @@ export const TriangleData = struct( {
298
298
  export const fetchTriangleData = Fn( ( [ triangleIndex, triangleBuffer, bvhBuffer, instanceLeaf ] ) => {
299
299
 
300
300
  // Positions carry their packed normal in .w
301
- const pos0 = getDatafromStorageBuffer( triangleBuffer, triangleIndex, int( 0 ), int( TRI_STRIDE ) ).toVar();
302
- const pos1 = getDatafromStorageBuffer( triangleBuffer, triangleIndex, int( 1 ), int( TRI_STRIDE ) ).toVar();
303
- const pos2 = getDatafromStorageBuffer( triangleBuffer, triangleIndex, int( 2 ), int( TRI_STRIDE ) ).toVar();
301
+ const pos0 = triangleRow( triangleBuffer, triangleIndex, 0 ).toVar();
302
+ const pos1 = triangleRow( triangleBuffer, triangleIndex, 1 ).toVar();
303
+ const pos2 = triangleRow( triangleBuffer, triangleIndex, 2 ).toVar();
304
304
 
305
- const uvMat = getDatafromStorageBuffer( triangleBuffer, triangleIndex, int( 4 ), int( TRI_STRIDE ) );
305
+ const uvMat = triangleRow( triangleBuffer, triangleIndex, 4 );
306
306
 
307
307
  const v0 = uintBitsToFloat( pos0.xyz ).toVar(), v1 = uintBitsToFloat( pos1.xyz ).toVar(), v2 = uintBitsToFloat( pos2.xyz ).toVar();
308
308
  const n0 = unpackTriangleNormal( pos0.w ).toVar(), n1 = unpackTriangleNormal( pos1.w ).toVar(), n2 = unpackTriangleNormal( pos2.w ).toVar();
@@ -0,0 +1,176 @@
1
+ import { Fn, float, int, uint, vec2, vec4, If, Loop, Return, instanceIndex, max, sin, dot, select } from 'three/tsl';
2
+
3
+ import { REC709_LUMINANCE_COEFFICIENTS } from './Common.js';
4
+
5
+ /** Floats per row of the packed table: width + 1 texels, padded to the 256 bytes a buffer→texture copy needs. */
6
+ export const cdfRowStride = width => Math.ceil( ( width + 1 ) * 4 / 256 ) * 64;
7
+
8
+ // First index in [0, n) whose value is ≥ target, n − 1 when none is — as the CPU builder's search.
9
+ function lowerBound( n, valueAt, target ) {
10
+
11
+ const lo = int( 0 ).toVar();
12
+ const hi = int( n - 1 ).toVar();
13
+ Loop( { start: int( 0 ), end: int( Math.ceil( Math.log2( n ) ) + 1 ), type: 'int', condition: '<', name: 'step' }, () => {
14
+
15
+ If( lo.lessThan( hi ), () => {
16
+
17
+ const mid = lo.add( hi ).div( 2 ).toVar();
18
+ If( valueAt( mid ).lessThan( target ), () => {
19
+
20
+ lo.assign( mid.add( 1 ) );
21
+
22
+ } ).Else( () => {
23
+
24
+ hi.assign( mid );
25
+
26
+ } );
27
+
28
+ } );
29
+
30
+ } );
31
+ return lo;
32
+
33
+ }
34
+
35
+ // select(), never mix(): a + t·(b − a) is not b at t = 1, and the search compares a prefix sum
36
+ // against the row total it ends on.
37
+
38
+ /**
39
+ * The GPU twin of `EquirectHDRInfo.computeCDF`, into the packed (width + 1) × height table the
40
+ * sampler reads. `stats[ 1 ]` ends as ( totalSum, compensationDelta, compensated, 0 ).
41
+ * @returns {Array} kernels, dispatched in order
42
+ */
43
+ export function buildEnvironmentCDFKernels( { pixels, rows, prefix, stats, cdf, width, height } ) {
44
+
45
+ const stride = cdfRowStride( width );
46
+ const sinTheta = y => sin( float( y ).add( 0.5 ).mul( Math.PI / height ) );
47
+ const weight = ( y, x, sinT ) => dot( pixels.element( y.mul( width ).add( x ) ).xyz, REC709_LUMINANCE_COEFFICIENTS ).mul( sinT );
48
+ const firstThreadOnly = () => If( instanceIndex.greaterThan( uint( 0 ) ), () => {
49
+
50
+ Return();
51
+
52
+ } );
53
+
54
+ const rowTotals = Fn( () => {
55
+
56
+ const y = instanceIndex;
57
+ If( y.greaterThanEqual( uint( height ) ), () => {
58
+
59
+ Return();
60
+
61
+ } );
62
+
63
+ const sinT = sinTheta( y ).toVar();
64
+ const total = float( 0 ).toVar();
65
+ Loop( { start: int( 0 ), end: int( width ), type: 'int', condition: '<' }, ( { i } ) => {
66
+
67
+ total.addAssign( weight( y, uint( i ), sinT ) );
68
+
69
+ } );
70
+ rows.element( y ).assign( vec4( total, 0, 0, 0 ) );
71
+
72
+ } )().compute( height, [ 64 ] );
73
+
74
+ const mean = Fn( () => {
75
+
76
+ firstThreadOnly();
77
+ const total = float( 0 ).toVar();
78
+ Loop( { start: int( 0 ), end: int( height ), type: 'int', condition: '<' }, ( { i } ) => {
79
+
80
+ total.addAssign( rows.element( i ).x );
81
+
82
+ } );
83
+ stats.element( 0 ).assign( vec4( total, total.div( width * height ), 0, 0 ) );
84
+
85
+ } )().compute( 1, [ 1 ] );
86
+
87
+ const prefixSums = Fn( () => {
88
+
89
+ const y = instanceIndex;
90
+ If( y.greaterThanEqual( uint( height ) ), () => {
91
+
92
+ Return();
93
+
94
+ } );
95
+
96
+ const sinT = sinTheta( y ).toVar();
97
+ const delta = stats.element( 0 ).y.toVar();
98
+ const raw = float( 0 ).toVar();
99
+ const compensated = float( 0 ).toVar();
100
+ Loop( { start: int( 0 ), end: int( width ), type: 'int', condition: '<' }, ( { i } ) => {
101
+
102
+ const w = weight( y, uint( i ), sinT ).toVar();
103
+ raw.addAssign( w );
104
+ compensated.addAssign( max( w.sub( delta ), 0 ) );
105
+ prefix.element( y.mul( width ).add( uint( i ) ) ).assign( vec2( raw, compensated ) );
106
+
107
+ } );
108
+ rows.element( y ).assign( vec4( raw, compensated, 0, 0 ) );
109
+
110
+ } )().compute( height, [ 64 ] );
111
+
112
+ const marginal = Fn( () => {
113
+
114
+ firstThreadOnly();
115
+ const compensatedTotal = float( 0 ).toVar();
116
+ Loop( { start: int( 0 ), end: int( height ), type: 'int', condition: '<' }, ( { i } ) => {
117
+
118
+ compensatedTotal.addAssign( rows.element( i ).y );
119
+
120
+ } );
121
+ const useCompensated = compensatedTotal.greaterThan( 0 ).toVar();
122
+
123
+ const cumulative = float( 0 ).toVar();
124
+ Loop( { start: int( 0 ), end: int( height ), type: 'int', condition: '<' }, ( { i } ) => {
125
+
126
+ const r = rows.element( i ).toVar();
127
+ cumulative.addAssign( select( useCompensated, r.y, r.x ) );
128
+ rows.element( i ).assign( vec4( r.x, r.y, cumulative, 0 ) );
129
+
130
+ } );
131
+
132
+ const s = stats.element( 0 ).toVar();
133
+ stats.element( 1 ).assign( select( useCompensated, vec4( compensatedTotal, s.y, 1, 0 ), vec4( s.x, 0, 0, 0 ) ) );
134
+
135
+ Loop( { start: int( 0 ), end: int( height ), type: 'int', condition: '<', name: 'row' }, ( { row } ) => {
136
+
137
+ const k = select(
138
+ cumulative.greaterThan( 0 ),
139
+ float( lowerBound( height, j => rows.element( j ).z, float( row ).add( 1 ).div( height ).mul( cumulative ) ) ),
140
+ float( height - 1 ),
141
+ );
142
+ cdf.element( uint( row ).mul( stride ).add( width ) ).assign( k.add( 0.5 ).div( height ) );
143
+
144
+ } );
145
+
146
+ } )().compute( 1, [ 1 ] );
147
+
148
+ const conditional = Fn( () => {
149
+
150
+ const idx = instanceIndex;
151
+ If( idx.greaterThanEqual( uint( width * height ) ), () => {
152
+
153
+ Return();
154
+
155
+ } );
156
+
157
+ const x = idx.mod( width );
158
+ const y = idx.div( width );
159
+ const useCompensated = stats.element( 1 ).z.greaterThan( 0 ).toVar();
160
+ const r = rows.element( y ).toVar();
161
+ const rowTotal = select( useCompensated, r.y, r.x ).toVar();
162
+ const base = y.mul( width ).toVar();
163
+ const k = lowerBound( width, j => {
164
+
165
+ const p = prefix.element( base.add( uint( j ) ) );
166
+ return select( useCompensated, p.y, p.x );
167
+
168
+ }, float( x ).add( 1 ).div( width ).mul( rowTotal ) );
169
+ const column = select( rowTotal.greaterThan( 0 ), float( k ), float( width - 1 ) );
170
+ cdf.element( y.mul( stride ).add( x ) ).assign( column.add( 0.5 ).div( width ) );
171
+
172
+ } )().compute( width * height, [ 64 ] );
173
+
174
+ return [ rowTotals, mean, prefixSums, marginal, conditional ];
175
+
176
+ }
@@ -28,7 +28,7 @@ import { RAY_FLAG, COUNTER } from '../Processor/QueueManager.js';
28
28
  import {
29
29
  writeRayOriginMeta, writeRayDirFlags, writeRayThroughputPdf,
30
30
  writeRayRadiance, writeGBuffer,
31
- writeMediumStack, writeFeatureThroughput,
31
+ writeMediumStack, writeFeatureThroughput, writeRngState,
32
32
  } from '../Processor/PackedRayBuffer.js';
33
33
 
34
34
  const WG_SIZE = 16;
@@ -36,7 +36,7 @@ const WG_SIZE = 16;
36
36
  export function buildGenerateKernel( params ) {
37
37
 
38
38
  const {
39
- rayBufferRW, rngBufferRW, gBufferRW,
39
+ rayBufferRW, hitBufferRW, gBufferRW,
40
40
  resolution, frame,
41
41
  cameraWorldMatrix, cameraProjectionMatrixInverse,
42
42
  cameraProjection, panoLonRange, panoLatRange, panoLevelHorizon,
@@ -104,7 +104,7 @@ export function buildGenerateKernel( params ) {
104
104
 
105
105
  writeMediumStack( rayBufferRW, rayID, uint( 0 ), uint( transmissiveBounces ), float( 1.0 ), float( 1.0 ), float( 1.0 ) );
106
106
 
107
- rngBufferRW.element( rayID ).assign( seed );
107
+ writeRngState( hitBufferRW, rayID, seed );
108
108
 
109
109
  };
110
110
 
@@ -15,7 +15,7 @@ import {
15
15
  packHalf2x16, unpackHalf2x16,
16
16
  } from 'three/tsl';
17
17
  import {
18
- getDatafromStorageBuffer, unpackTriangleNormal, TRI_STRIDE, instanceRows, instanceFaceNormalToWorld,
18
+ unpackTriangleNormal, triangleRow, instanceRows, instanceFaceNormalToWorld,
19
19
  } from './Common.js';
20
20
  import { shadowTerminatorLift } from './ShadowTerminator.js';
21
21
 
@@ -30,9 +30,9 @@ export function hitFacet( { triangleBuffer, bvhBuffer, triIdx, instanceLeaf, hit
30
30
 
31
31
  If( didHit, () => {
32
32
 
33
- const recA = getDatafromStorageBuffer( triangleBuffer, triIdx, int( 0 ), int( TRI_STRIDE ) ).toVar();
34
- const recB = getDatafromStorageBuffer( triangleBuffer, triIdx, int( 1 ), int( TRI_STRIDE ) ).toVar();
35
- const recC = getDatafromStorageBuffer( triangleBuffer, triIdx, int( 2 ), int( TRI_STRIDE ) ).toVar();
33
+ const recA = triangleRow( triangleBuffer, triIdx, 0 ).toVar();
34
+ const recB = triangleRow( triangleBuffer, triIdx, 1 ).toVar();
35
+ const recC = triangleRow( triangleBuffer, triIdx, 2 ).toVar();
36
36
  const V0 = uintBitsToFloat( recA.xyz ).toVar();
37
37
  const V1 = uintBitsToFloat( recB.xyz ).toVar();
38
38
  const V2 = uintBitsToFloat( recC.xyz ).toVar();
@@ -27,8 +27,8 @@ import {
27
27
 
28
28
  import { Ray, ShadowMaterial, HitInfo } from './Struct.js';
29
29
  import {
30
- REC709_LUMINANCE_COEFFICIENTS, getShadowMaterial, getDatafromStorageBuffer, instanceRows,
31
- instanceFaceNormalToWorld, TRI_STRIDE, getAlphaShadowsUniform, shadowFlagsSettle, offsetRayOrigin,
30
+ REC709_LUMINANCE_COEFFICIENTS, getShadowMaterial, instanceRows,
31
+ instanceFaceNormalToWorld, triangleRow, getAlphaShadowsUniform, shadowFlagsSettle, offsetRayOrigin,
32
32
  } from './Common.js';
33
33
  import { fresnelDielectric } from './Fresnel.js';
34
34
  import { calculateBeerLawAbsorption } from './MaterialTransmission.js';
@@ -92,7 +92,7 @@ export const traceShadowRay = Fn( ( [
92
92
  // Opaque fast-path: check the per-triangle blocker bit, set at extraction time when
93
93
  // alphaMode/transparent/transmission/opacity all indicate a fully opaque surface.
94
94
  // Short-circuits the 7-slot getShadowMaterial fetch and the whole alpha decision tree.
95
- const flags = getDatafromStorageBuffer( triangleBuffer, shadowHit.triangleIndex, int( 4 ), int( TRI_STRIDE ) ).z;
95
+ const flags = triangleRow( triangleBuffer, shadowHit.triangleIndex, 4 ).z;
96
96
  If( shadowFlagsSettle( flags ), () => {
97
97
 
98
98
  transmittance.assign( 0.0 );
@@ -125,8 +125,8 @@ export const traceShadowRay = Fn( ( [
125
125
  const baryU = shadowHit.uv.x;
126
126
  const baryV = shadowHit.uv.y;
127
127
  const baryW = float( 1.0 ).sub( baryU ).sub( baryV );
128
- const uvData1 = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, shadowHit.triangleIndex, int( 3 ), int( TRI_STRIDE ) ) );
129
- const uvData2 = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, shadowHit.triangleIndex, int( 4 ), int( TRI_STRIDE ) ).xy );
128
+ const uvData1 = uintBitsToFloat( triangleRow( triangleBuffer, shadowHit.triangleIndex, 3 ) );
129
+ const uvData2 = uintBitsToFloat( triangleRow( triangleBuffer, shadowHit.triangleIndex, 4 ).xy );
130
130
  const hitUV = uvData1.xy.mul( baryW ).add( uvData1.zw.mul( baryU ) ).add( uvData2.mul( baryV ) );
131
131
  const albedoUV = getTransformedUV( { uv: hitUV, transform: shadowMaterial.albedoTransform } );
132
132
  texAlpha.assign( sampleBucket( _shadowAlbedoMaps, shadowMaterial.albedoMapIndex, albedoUV ).a );
@@ -181,9 +181,9 @@ export const traceShadowRay = Fn( ( [
181
181
  // Deferred geometric-normal compute — refetch triangle positions and
182
182
  // derive the normal here so opaque/alpha-cutout shadow hits don't pay
183
183
  // the cross+normalize cost in BVH traversal.
184
- const pA = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, shadowHit.triangleIndex, int( 0 ), int( TRI_STRIDE ) ).xyz );
185
- const pB = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, shadowHit.triangleIndex, int( 1 ), int( TRI_STRIDE ) ).xyz );
186
- const pC = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, shadowHit.triangleIndex, int( 2 ), int( TRI_STRIDE ) ).xyz );
184
+ const pA = uintBitsToFloat( triangleRow( triangleBuffer, shadowHit.triangleIndex, 0 ).xyz );
185
+ const pB = uintBitsToFloat( triangleRow( triangleBuffer, shadowHit.triangleIndex, 1 ).xyz );
186
+ const pC = uintBitsToFloat( triangleRow( triangleBuffer, shadowHit.triangleIndex, 2 ).xyz );
187
187
  // Positions are in the blocker's object space; the cross product is a normal, so
188
188
  // it rides the inverse-transpose back out rather than the forward matrix.
189
189
  const objNormal = normalize( cross( pB.sub( pA ), pC.sub( pA ) ) ).toVar();
@@ -88,6 +88,7 @@ import {
88
88
  sampleEquirectProbability,
89
89
  } from './Environment.js';
90
90
  import { shadowTerminatorOrigin } from './ShadowTerminator.js';
91
+ import { sunRadianceToward, sampleSunDisc } from './Sun.js';
91
92
 
92
93
  const TWO_PI = 2.0 * PI;
93
94
 
@@ -798,6 +799,8 @@ export const calculateDirectLightingUnified = Fn( ( [
798
799
  // The facet on the viewer's side, which shadow rays are offset off, and the shadow terminator lift
799
800
  // (ShadowTerminator.js) for the light and environment ones
800
801
  terminatorLift, facetNormal, terminatorCutoff,
802
+ // The physical sky's sun (Sun.js); its BSDF-sampled partner is the miss branch in ShadeKernel.
803
+ hasSun, sunDirection, sunRadiance, sunParams,
801
804
  ] ) => {
802
805
 
803
806
  const totalContribution = vec3( 0.0 ).toVar();
@@ -987,55 +990,71 @@ export const calculateDirectLightingUnified = Fn( ( [
987
990
 
988
991
  } );
989
992
  // =====================================================================
990
- // DETERMINISTIC ENVIRONMENT NEE
991
- // Always runs (not stochastic) — forms a two-strategy Veach MIS
992
- // estimator together with the implicit miss check in the main loop.
993
+ // DETERMINISTIC ENVIRONMENT NEE — the map, then the physical sky's sun, each MIS'd with its
994
+ // partner at the miss. One loop, so the shadow ray and BSDF evaluation are emitted once.
993
995
  // =====================================================================
994
996
 
995
997
  If( enableEnvironmentLight, () => {
996
998
 
997
- const envRandom = getRandomSample2D( pixelCoord, int( 0 ), dimBase.add( int( 1 ) ), rngState, resolution, frame ).toVar();
998
- const envColor = vec3( 0.0 ).toVar();
999
+ const passes = select( hasSun.greaterThan( int( 0 ) ), int( 2 ), int( 1 ) );
1000
+ Loop( { start: int( 0 ), end: passes, type: 'int', condition: '<', name: 'envPass' }, ( { envPass } ) => {
999
1001
 
1000
- // Sample direction + PDF + color from importance-sampled environment
1001
- const envSampleResult = sampleEquirectProbability(
1002
- envTexture, envCDFTexture,
1003
- envMatrix, environmentIntensity, envTotalSum, envCompensationDelta, envResolution, envRandom, envColor
1004
- ).toVar();
1002
+ const isSun = envPass.equal( int( 1 ) ).toVar();
1003
+ const radiance = vec3( 0.0 ).toVar();
1004
+ const direction = vec3( 0.0, 1.0, 0.0 ).toVar();
1005
+ const lightPdf = float( 0.0 ).toVar();
1005
1006
 
1006
- const envDirection = envSampleResult.xyz.toVar();
1007
- const envPdf = envSampleResult.w.toVar();
1007
+ If( isSun, () => {
1008
1008
 
1009
- If( envPdf.greaterThan( 0.0 ), () => {
1009
+ const sunXi = getRandomSample2D( pixelCoord, int( 0 ), dimBase.add( int( 9 ) ), rngState, resolution, frame );
1010
+ direction.assign( sampleSunDisc( sunDirection, sunParams, sunXi ) );
1011
+ radiance.assign( sunRadianceToward( direction, sunDirection, sunRadiance, sunParams ).mul( environmentIntensity ) );
1012
+ lightPdf.assign( select( radiance.x.add( radiance.y ).add( radiance.z ).greaterThan( 0.0 ), float( 1.0 ).div( sunParams.y ), float( 0.0 ) ) );
1010
1013
 
1011
- const NoL = max( float( 0.0 ), dot( hitNormal, envDirection ) ).toVar();
1014
+ } ).Else( () => {
1012
1015
 
1013
- If( NoL.greaterThan( 0.0 ).and( isDirectionValid( { direction: envDirection, surfaceNormal: geomNormal } ) ), () => {
1016
+ const envRandom = getRandomSample2D( pixelCoord, int( 0 ), dimBase.add( int( 1 ) ), rngState, resolution, frame ).toVar();
1017
+ const envColor = vec3( 0.0 ).toVar();
1018
+ const envSampleResult = sampleEquirectProbability(
1019
+ envTexture, envCDFTexture,
1020
+ envMatrix, environmentIntensity, envTotalSum, envCompensationDelta, envResolution, envRandom, envColor
1021
+ ).toVar();
1022
+ direction.assign( envSampleResult.xyz );
1023
+ radiance.assign( envColor );
1024
+ lightPdf.assign( envSampleResult.w );
1014
1025
 
1015
- const visibility = shadow( lightShadowOrigin( envDirection ), envDirection, float( 1e20 ) );
1026
+ } );
1016
1027
 
1017
- If( visibility.greaterThan( 0.0 ).or( wantUnoccluded ), () => {
1028
+ If( lightPdf.greaterThan( 0.0 ), () => {
1018
1029
 
1019
- // Share H + dots between env BRDF/PDF — same redundancy fix as the
1020
- // discrete-light path above.
1021
- const envDots = DotProducts.wrap( computeDotProductsAniso( hitNormal, viewDir, envDirection, material ) );
1022
- const brdfValue = evaluateMaterialResponseFromDots( material, envDots );
1023
- const bPdf = calculateMaterialPDFFromDots( material, envDots ).toVar();
1024
-
1025
- // Balance heuristic for env MIS — optimal for MIS-compensated PDFs (Karlík et al. 2019).
1026
- // The implicit path uses material combinedPdf as prevBouncePdf at the miss check.
1027
- const misW = select(
1028
- bPdf.greaterThan( 0.0 ),
1029
- balanceHeuristic( { pdf1: envPdf, pdf2: bPdf } ),
1030
- float( 1.0 )
1031
- ).toVar();
1032
-
1033
- // Base contribution WITHOUT visibility (deterministic estimator; no stochastic scaling).
1034
- const baseContribution = envColor.mul( brdfValue ).mul( NoL ).mul( misW ).div( max( envPdf, 1e-10 ) );
1035
- totalContribution.addAssign( baseContribution.mul( visibility ) );
1036
- If( wantUnoccluded, () => {
1030
+ const NoL = max( float( 0.0 ), dot( hitNormal, direction ) ).toVar();
1037
1031
 
1038
- unoccludedContribution.addAssign( baseContribution );
1032
+ If( NoL.greaterThan( 0.0 ).and( isDirectionValid( { direction, surfaceNormal: geomNormal } ) ), () => {
1033
+
1034
+ const visibility = shadow( lightShadowOrigin( direction ), direction, float( 1e20 ) );
1035
+
1036
+ If( visibility.greaterThan( 0.0 ).or( wantUnoccluded ), () => {
1037
+
1038
+ const envDots = DotProducts.wrap( computeDotProductsAniso( hitNormal, viewDir, direction, material ) );
1039
+ const brdfValue = evaluateMaterialResponseFromDots( material, envDots );
1040
+ const bPdf = calculateMaterialPDFFromDots( material, envDots ).toVar();
1041
+
1042
+ // The map pairs with the balance heuristic — optimal for its MIS-compensated pdf
1043
+ // (Karlík et al. 2019); the sun, a small bright source, with the power heuristic.
1044
+ const misW = select(
1045
+ bPdf.greaterThan( 0.0 ),
1046
+ select( isSun, powerHeuristic( { pdf1: lightPdf, pdf2: bPdf } ), balanceHeuristic( { pdf1: lightPdf, pdf2: bPdf } ) ),
1047
+ float( 1.0 )
1048
+ ).toVar();
1049
+
1050
+ // Base contribution WITHOUT visibility (deterministic estimator; no stochastic scaling).
1051
+ const baseContribution = radiance.mul( brdfValue ).mul( NoL ).mul( misW ).div( max( lightPdf, 1e-10 ) );
1052
+ totalContribution.addAssign( baseContribution.mul( visibility ) );
1053
+ If( wantUnoccluded, () => {
1054
+
1055
+ unoccludedContribution.addAssign( baseContribution );
1056
+
1057
+ } );
1039
1058
 
1040
1059
  } );
1041
1060
 
@@ -1047,7 +1066,6 @@ export const calculateDirectLightingUnified = Fn( ( [
1047
1066
 
1048
1067
  } );
1049
1068
 
1050
-
1051
1069
  // EMISSIVE TRIANGLE DIRECT LIGHTING
1052
1070
  // NOTE: Emissive triangle sampling is handled separately in pathtracer_core.fs
1053
1071
  // to bypass firefly suppression. Do not add it here to avoid double-counting.
package/src/TSL/Random.js CHANGED
@@ -132,6 +132,7 @@ export const RandomPointInCircle = ( rngState ) => {
132
132
  // +6 subsurface HG scatter dir (ShadeKernel)
133
133
  // +7 ground-catcher cosine BSDF (ShadeKernel)
134
134
  // +8 indirect-strategy direction (LightsIndirect)
135
+ // +9 physical-sky sun disc (LightsSampling)
135
136
  // 1D +0 (free — was the {lights,BRDF} stochastic strategy pick)
136
137
  // +1 emissive-triangle pick (EmissiveSampling / LightBVHSampling)
137
138
  // +2 BSDF lobe selection (ShadeKernel)