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/TSL/Common.js CHANGED
@@ -336,15 +336,22 @@ export function splitStorage( parts, perPart, type ) {
336
336
 
337
337
  }
338
338
 
339
- /** Element `index` of a storage node or of a {@link splitStorage}. */
340
- export function storageElement( buffer, index ) {
341
-
342
- if ( ! buffer.isSplitStorage ) return buffer.element( index );
339
+ /**
340
+ * Elements `base + r` of a {@link splitStorage}, for each offset r in `rows`, all of one record: a part holds whole
341
+ * records, so the part is picked once and every row is read in its branch.
342
+ */
343
+ export function storageRows( buffer, base, rows ) {
343
344
 
344
345
  const { parts, perPart } = buffer;
345
- const e = int( index ).toVar();
346
- const out = ( { uvec4, uvec2 }[ buffer.type ] ?? vec4 )( 0 ).toVar();
347
- const read = ( k ) => () => out.assign( parts[ k ].element( k === 0 ? e : e.sub( int( perPart * k ) ) ) );
346
+ const e = int( base ).toVar();
347
+ const out = rows.map( () => ( { uvec4, uvec2 }[ buffer.type ] ?? vec4 )( 0 ).toVar() );
348
+ const read = ( k ) => () => {
349
+
350
+ const at = k === 0 ? e : e.sub( int( perPart * k ) );
351
+ rows.forEach( ( r, j ) => out[ j ].assign( parts[ k ].element( r === 0 ? at : at.add( int( r ) ) ) ) );
352
+
353
+ };
354
+
348
355
  let branch = If( e.lessThan( int( perPart ) ), read( 0 ) );
349
356
  for ( let k = 1; k < parts.length - 1; k ++ ) branch = branch.ElseIf( e.lessThan( int( perPart * ( k + 1 ) ) ), read( k ) );
350
357
  branch.Else( read( parts.length - 1 ) );
@@ -352,6 +359,13 @@ export function storageElement( buffer, index ) {
352
359
 
353
360
  }
354
361
 
362
+ /** Element `index` of a storage node or of a {@link splitStorage}. */
363
+ export function storageElement( buffer, index ) {
364
+
365
+ return buffer.isSplitStorage ? storageRows( buffer, index, [ 0 ] )[ 0 ] : buffer.element( index );
366
+
367
+ }
368
+
355
369
  const asNode = ( v ) => ( v?.isNode ? v : int( v ) );
356
370
 
357
371
  // Element `index · stride + offset`.
@@ -536,6 +550,7 @@ export const getMaterial = Fn( ( [ materialIndex, materialBuffer ] ) => {
536
550
  bumpScale: data12.r,
537
551
  displacementScale: data12.g,
538
552
  displacementMapIndex: int( data12.b ),
553
+ displacementBias: data12.a,
539
554
  albedoTransform: arrayToMat3( { data1: data13, data2: data14 } ),
540
555
  normalTransform: arrayToMat3( { data1: data15, data2: data16 } ),
541
556
  roughnessTransform: arrayToMat3( { data1: data17, data2: data18 } ),
@@ -586,6 +601,7 @@ export const diffuseGroundMaterial = Fn( () => {
586
601
  bumpMapIndex: int( - 1 ),
587
602
  bumpScale: float( 1.0 ),
588
603
  displacementScale: float( 0.0 ),
604
+ displacementBias: float( 0.0 ),
589
605
  metalnessMapIndex: int( - 1 ),
590
606
  roughnessMapIndex: int( - 1 ),
591
607
  displacementMapIndex: int( - 1 ),
@@ -676,9 +692,10 @@ export const shadowFlagsSettle = ( flags, alphaShadows ) => {
676
692
 
677
693
  export const instanceRows = ( bvhBuffer, leafIndex ) => {
678
694
 
679
- // Built over copy clusters, an instance is a copy record (3 vec4 from `copyRecordBase`), not a TLAS leaf.
695
+ // Over copy clusters an instance is a 3-vec4 copy record, which can straddle two parts: read per element.
680
696
  const records = bvhBuffer.value?.copyRecordBase;
681
697
  if ( records !== undefined ) return [ 0, 1, 2 ].map( ( row ) => getDatafromStorageBuffer( bvhBuffer, leafIndex, int( records + row ), int( 3 ) ) );
698
+ if ( bvhBuffer.isSplitStorage ) return storageRows( bvhBuffer, int( leafIndex ).mul( 4 ), [ 1, 2, 3 ] );
682
699
  return [
683
700
  getDatafromStorageBuffer( bvhBuffer, leafIndex, int( 1 ), int( 4 ) ),
684
701
  getDatafromStorageBuffer( bvhBuffer, leafIndex, int( 2 ), int( 4 ) ),
@@ -13,7 +13,7 @@ import {
13
13
  localId, workgroupId,
14
14
  } from 'three/tsl';
15
15
 
16
- import { generateRayFromCamera } from './CameraRay.js';
16
+ import { generateRayFromCamera, cameraClipDistance } from './CameraRay.js';
17
17
  import { Ray } from './Struct.js';
18
18
  import { TraceDebugMode } from './Debugger.js';
19
19
  import { pcgHash, getStratifiedSample } from './Random.js';
@@ -28,6 +28,7 @@ export function buildDebugKernel( params ) {
28
28
  cameraWorldMatrix, cameraProjectionMatrixInverse, cameraProjectionMatrix, cameraViewMatrix,
29
29
  cameraProjection, panoLonRange, panoLatRange, panoLevelHorizon,
30
30
  enableDOF, focalLength, aperture, focusDistance, unitsPerMetre, apertureScale, anamorphicRatio, dofMode, dofBlur,
31
+ cameraClipStart,
31
32
  bvhBuffer, triangleBuffer, materialBuffer,
32
33
  envTexture, environmentMatrix, environmentIntensity, enableEnvironmentLight,
33
34
  visMode, debugVisScale,
@@ -66,7 +67,7 @@ export function buildDebugKernel( params ) {
66
67
 
67
68
  // Modes 1-8, 10 — shared per-mode debug color (primary-ray trace + counters).
68
69
  color.assign( TraceDebugMode(
69
- ray.origin, ray.direction,
70
+ ray.origin.add( ray.direction.mul( cameraClipDistance( ray.direction, cameraClipStart, cameraWorldMatrix, cameraProjection ) ) ), ray.direction,
70
71
  bvhBuffer, triangleBuffer, materialBuffer,
71
72
  envTexture, environmentMatrix, environmentIntensity, enableEnvironmentLight,
72
73
  visMode, debugVisScale,
@@ -1,33 +1,39 @@
1
1
  import { sceneResources } from './SceneResources.js';
2
- import { Fn, float, vec2, int, If, Loop, abs, normalize, dot, max, uintBitsToFloat } from 'three/tsl';
2
+ import {
3
+ Fn, wgslFn, float, vec2, int, bool as tslBool, If, Loop, Break, abs, normalize, dot, cross, min, max, clamp, ceil, select, uintBitsToFloat,
4
+ } from 'three/tsl';
3
5
 
4
6
  import { struct } from './patches.js';
5
7
  import { triangleRow, instanceRows, instanceDirToWorld } from './Common.js';
6
8
  import { sampleDisplacementMap, bucketTexelSize } from './TextureSampling.js';
7
9
 
8
- // Ray-displacement intersection configuration
9
- const MAX_MARCH_STEPS = 32;
10
- const MIN_MARCH_STEPS = 16;
11
- const BINARY_STEPS = 5;
10
+ const MIN_MARCH_STEPS = 8;
11
+ const MAX_MARCH_STEPS = 64;
12
+ const MAX_BOUNCE_MARCH_STEPS = 16;
13
+ const STEPS_PER_TEXEL = 0.25;
14
+ const BINARY_STEPS = 6;
15
+ const SLOPE_TEXELS = 1.5;
12
16
 
13
17
  export const DisplacementResult = struct( {
14
18
  hitPoint: 'vec3',
15
19
  uv: 'vec2',
16
20
  normal: 'vec3',
17
- height: 'float',
18
21
  } );
19
22
 
23
+ // Texture-space UV per raw UV, per axis.
24
+ const uvTransformScale = /*@__PURE__*/ wgslFn( `
25
+ fn uvTransformScale( transform: mat3x3f ) -> vec2f {
26
+ return max( vec2f( length( transform[0].xy ), length( transform[1].xy ) ), vec2f( 1e-6 ) );
27
+ }
28
+ ` );
29
+
20
30
  /**
21
31
  * Tessellation-free displacement mapping via analytical UV ray marching.
22
32
  *
23
- * Instead of projecting march points onto the triangle to get barycentrics (which
24
- * causes UV distortion from clamping), this computes the ray's UV trajectory
25
- * analytically using the triangle's actual tangent vectors (dP/du, dP/dv).
26
- *
27
33
  * The ray is parameterized as: P(dt) = hitPoint + rayDir * dt
28
34
  * UV along the ray: UV(dt) = hitUV + dUV_dt * dt
29
35
  * Ray height above surface: h_ray(dt) = dt * dot(rayDir, N)
30
- * Displaced surface height: h_surf(dt) = (sample(UV(dt)) - 0.5) * scale
36
+ * Displaced surface height: h_surf(dt) = sample(UV(dt)) * displacementScale + displacementBias, as three.js
31
37
  *
32
38
  * We find dt where h_ray(dt) = h_surf(dt).
33
39
  */
@@ -38,7 +44,6 @@ export const refineDisplacedIntersection = Fn( ( [
38
44
  const resultHitPoint = hitInfo.hitPoint.toVar();
39
45
  const resultUV = hitInfo.uv.toVar();
40
46
  const resultNormal = hitInfo.normal.toVar();
41
- const resultHeight = float( 0.0 ).toVar();
42
47
 
43
48
  // Fetch triangle vertex data
44
49
  const triIdx = hitInfo.triangleIndex;
@@ -83,16 +88,13 @@ export const refineDisplacedIntersection = Fn( ( [
83
88
  const B = edge2.mul( dUV1.x ).sub( edge1.mul( dUV2.x ) ).mul( invDet ).toVar();
84
89
  const N = hitInfo.normal.toVar();
85
90
 
86
- const scale = material.displacementScale.div( float( 10.0 ) ); // Arbitrary scale factor for height
91
+ const scale = material.displacementScale;
92
+ const bias = material.displacementBias;
87
93
  const rayDir = ray.direction;
88
94
 
89
- // Compute UV velocity along the ray: dUV/dt
90
- // Project ray direction onto surface tangent plane
91
95
  const NdotD = dot( rayDir, N ).toVar();
92
96
  const rayProj = rayDir.sub( N.mul( NdotD ) ).toVar();
93
97
 
94
- // Solve: rayProj = du_dt * T + dv_dt * B
95
- // Using the Gram matrix inverse of [T, B]
96
98
  const TdotT = dot( T, T );
97
99
  const TdotB = dot( T, B );
98
100
  const BdotB = dot( B, B );
@@ -100,89 +102,60 @@ export const refineDisplacedIntersection = Fn( ( [
100
102
 
101
103
  If( abs( detJ ).greaterThan( 1e-10 ), () => {
102
104
 
105
+ // Dual basis of (T, B): dot( Td, T ) = 1, dot( Td, B ) = 0, and likewise Bd.
103
106
  const invDetJ = float( 1.0 ).div( detJ );
104
- const rayProjDotT = dot( rayProj, T );
105
- const rayProjDotB = dot( rayProj, B );
106
-
107
- const du_dt = BdotB.mul( rayProjDotT ).sub( TdotB.mul( rayProjDotB ) ).mul( invDetJ );
108
- const dv_dt = TdotT.mul( rayProjDotB ).sub( TdotB.mul( rayProjDotT ) ).mul( invDetJ );
109
- const dUV_dt = vec2( du_dt, dv_dt ).toVar();
110
-
111
- // Ray height change per unit dt: how fast the ray moves along the surface normal
112
- const dh_ray_dt = NdotD.toVar();
113
-
114
- // March range: displacement shell extends ±0.5*scale from base surface
115
- // Compute dt range to traverse the full shell
116
- const absNdotD = max( abs( dh_ray_dt ), 0.001 );
117
- const dtShell = scale.div( absNdotD ).toVar();
107
+ const Td = T.mul( BdotB ).sub( B.mul( TdotB ) ).mul( invDetJ ).toVar();
108
+ const Bd = B.mul( TdotT ).sub( T.mul( TdotB ) ).mul( invDetJ ).toVar();
109
+ const dUV_dt = vec2( dot( rayProj, Td ), dot( rayProj, Bd ) ).toVar();
118
110
 
119
- // Adaptive step count: full steps on primary ray, half on deeper bounces
120
- const marchSteps = int( bounceIndex.equal( int( 0 ) ).select(
121
- int( MAX_MARCH_STEPS ), int( MIN_MARCH_STEPS )
122
- ) ).toVar();
111
+ // +1 when the ray arrives from the side N points to.
112
+ const side = select( NdotD.lessThan( 0.0 ), float( 1.0 ), float( - 1.0 ) ).toVar();
113
+ const under = ( dt, height ) => dt.mul( NdotD ).sub( height.mul( scale ).add( bias ) ).mul( side ).lessThanEqual( 0.0 );
114
+ const heightAt = uv => sampleDisplacementMap( material.displacementMapIndex, uv, material.displacementTransform );
123
115
 
124
- // Start from above the shell, march through
125
- const dtStart = dtShell.negate().toVar();
126
- const dtEnd = dtShell;
127
- const dtStep = dtEnd.sub( dtStart ).div( float( marchSteps ) ).toVar();
116
+ // Enter the shell [bias, bias + scale] on the ray's side, never behind the ray's origin.
117
+ const top = max( bias, bias.add( scale ) );
118
+ const bottom = min( bias, bias.add( scale ) );
119
+ const slope = side.negate().mul( max( abs( NdotD ), 0.001 ) );
120
+ const dtStart = max( select( side.greaterThan( 0.0 ), top, bottom ).div( slope ), hitInfo.dst.negate() ).toVar();
121
+ const dtEnd = select( side.greaterThan( 0.0 ), bottom, top ).div( slope ).toVar();
128
122
 
129
- // Track for binary refinement
130
- const prevDt = dtStart.toVar();
131
- const currDt = dtStart.toVar();
132
- const found = int( 0 ).toVar();
123
+ // One step per few texels the ray crosses, so a grazing ray does not step over the gaps.
124
+ const texelsPerUV = uvTransformScale( { transform: material.displacementTransform } )
125
+ .div( bucketTexelSize( sceneResources( builder ).linearBuckets, material.displacementMapIndex ) ).toVar();
126
+ const texelsCrossed = dUV_dt.mul( texelsPerUV ).length().mul( dtEnd.sub( dtStart ) );
127
+ const maxSteps = select( bounceIndex.equal( int( 0 ) ), float( MAX_MARCH_STEPS ), float( MAX_BOUNCE_MARCH_STEPS ) );
128
+ const marchSteps = select( dtEnd.greaterThan( dtStart ),
129
+ int( clamp( ceil( texelsCrossed.mul( STEPS_PER_TEXEL ) ), float( MIN_MARCH_STEPS ), maxSteps ) ), int( 0 ) ).toVar();
130
+ const dtStep = dtEnd.sub( dtStart ).div( max( float( marchSteps ), 1.0 ) ).toVar();
133
131
 
134
- // Linear march through displacement shell
135
- Loop( { start: int( 0 ), end: marchSteps, type: 'int', condition: '<' }, ( { i } ) => {
132
+ const loDt = dtStart.toVar();
133
+ const hiDt = dtStart.toVar();
134
+ const found = tslBool( false ).toVar();
136
135
 
137
- If( found.equal( int( 0 ) ), () => {
136
+ Loop( { start: int( 1 ), end: marchSteps.add( 1 ), type: 'int', condition: '<' }, ( { i } ) => {
138
137
 
139
- const dt = dtStart.add( dtStep.mul( float( i ) ) ).toVar();
138
+ const dt = dtStart.add( dtStep.mul( float( i ) ) ).toVar();
140
139
 
141
- // UV at this point along the ray
142
- const marchUV = hitInfo.uv.add( dUV_dt.mul( dt ) );
140
+ If( under( dt, heightAt( hitInfo.uv.add( dUV_dt.mul( dt ) ) ) ), () => {
143
141
 
144
- // Ray height above base surface at this dt
145
- const rayHeight = dt.mul( dh_ray_dt );
146
-
147
- // Displaced surface height
148
- const heightSample = sampleDisplacementMap(
149
- material.displacementMapIndex, marchUV, material.displacementTransform,
150
- );
151
- const surfaceHeight = heightSample.sub( 0.5 ).mul( scale );
152
-
153
- // Ray crosses below displaced surface → intersection
154
- If( rayHeight.lessThanEqual( surfaceHeight ).and( i.greaterThan( int( 0 ) ) ), () => {
155
-
156
- found.assign( 1 );
157
- currDt.assign( dt );
158
-
159
- } ).Else( () => {
160
-
161
- prevDt.assign( dt );
162
-
163
- } );
142
+ found.assign( true );
143
+ hiDt.assign( dt );
144
+ Break();
164
145
 
165
146
  } );
166
147
 
167
- } );
148
+ loDt.assign( dt );
168
149
 
169
- // Binary refinement
170
- If( found.equal( int( 1 ) ), () => {
150
+ } );
171
151
 
172
- const loDt = prevDt.toVar();
173
- const hiDt = currDt.toVar();
152
+ If( found, () => {
174
153
 
175
154
  Loop( { start: int( 0 ), end: int( BINARY_STEPS ), type: 'int', condition: '<' }, () => {
176
155
 
177
156
  const midDt = loDt.add( hiDt ).mul( 0.5 ).toVar();
178
- const midUV = hitInfo.uv.add( dUV_dt.mul( midDt ) );
179
- const midRayHeight = midDt.mul( dh_ray_dt );
180
- const midSample = sampleDisplacementMap(
181
- material.displacementMapIndex, midUV, material.displacementTransform,
182
- );
183
- const midSurfHeight = midSample.sub( 0.5 ).mul( scale );
184
157
 
185
- If( midRayHeight.lessThanEqual( midSurfHeight ), () => {
158
+ If( under( midDt, heightAt( hitInfo.uv.add( dUV_dt.mul( midDt ) ) ) ), () => {
186
159
 
187
160
  hiDt.assign( midDt );
188
161
 
@@ -194,40 +167,20 @@ export const refineDisplacedIntersection = Fn( ( [
194
167
 
195
168
  } );
196
169
 
197
- // Final intersection
198
170
  const finalDt = loDt.add( hiDt ).mul( 0.5 ).toVar();
199
171
  const finalUV = hitInfo.uv.add( dUV_dt.mul( finalDt ) ).toVar();
200
- const finalPoint = hitInfo.hitPoint.add( rayDir.mul( finalDt ) );
201
-
202
- const finalHeight = sampleDisplacementMap(
203
- material.displacementMapIndex, finalUV, material.displacementTransform,
204
- );
205
- const displacedHeight = finalHeight.sub( 0.5 ).mul( scale );
206
-
207
- // Compute displaced normal from height-field gradients using UV tangent vectors.
208
- // Finite-difference step = one texel of the SELECTED bucket array's real dimensions.
209
- const texel = bucketTexelSize( sceneResources( builder ).linearBuckets, material.displacementMapIndex ).toVar();
210
- const hC = finalHeight;
211
- const hU = sampleDisplacementMap(
212
- material.displacementMapIndex, finalUV.add( vec2( texel.x, 0.0 ) ), material.displacementTransform,
213
- );
214
- const hV = sampleDisplacementMap(
215
- material.displacementMapIndex, finalUV.add( vec2( 0.0, texel.y ) ), material.displacementTransform,
216
- );
217
-
218
- // Perturb normal using actual tangent/bitangent from UV parameterization
219
- const Tn = normalize( T ).toVar();
220
- const Bn = normalize( B ).toVar();
221
- const gradU = hU.sub( hC ).mul( scale );
222
- const gradV = hV.sub( hC ).mul( scale );
223
- const displacedNormal = normalize(
224
- N.sub( Tn.mul( gradU ) ).sub( Bn.mul( gradV ) )
225
- );
226
-
227
- resultHitPoint.assign( finalPoint );
172
+
173
+ // Central differences over a few texels: an 8-bit map's steps make one-texel slopes flicker.
174
+ const step = vec2( SLOPE_TEXELS ).div( texelsPerUV ).toVar();
175
+ const dhdu = heightAt( finalUV.add( vec2( step.x, 0.0 ) ) ).sub( heightAt( finalUV.sub( vec2( step.x, 0.0 ) ) ) ).mul( scale ).div( step.x.mul( 2.0 ) );
176
+ const dhdv = heightAt( finalUV.add( vec2( 0.0, step.y ) ) ).sub( heightAt( finalUV.sub( vec2( 0.0, step.y ) ) ) ).mul( scale ).div( step.y.mul( 2.0 ) );
177
+ resultNormal.assign( normalize( N.sub( Td.mul( dhdu ) ).sub( Bd.mul( dhdv ) ) ) );
178
+
179
+ // A point behind the base triangle goes up onto its plane: rays spawned below it would hit it.
180
+ const faceN = normalize( cross( Td, Bd ) );
181
+ const moveDir = select( finalDt.greaterThan( 0.0 ), rayDir.sub( faceN.mul( dot( rayDir, faceN ) ) ), rayDir );
182
+ resultHitPoint.assign( hitInfo.hitPoint.add( moveDir.mul( finalDt ) ) );
228
183
  resultUV.assign( finalUV );
229
- resultNormal.assign( displacedNormal );
230
- resultHeight.assign( displacedHeight );
231
184
 
232
185
  } );
233
186
 
@@ -235,6 +188,6 @@ export const refineDisplacedIntersection = Fn( ( [
235
188
 
236
189
  } );
237
190
 
238
- return DisplacementResult( { hitPoint: resultHitPoint, uv: resultUV, normal: resultNormal, height: resultHeight } );
191
+ return DisplacementResult( { hitPoint: resultHitPoint, uv: resultUV, normal: resultNormal } );
239
192
 
240
193
  } );
@@ -39,7 +39,6 @@ export function buildFinalWriteKernel( params ) {
39
39
  enableAccumulation, hasPreviousAccumulated, accumulationAlpha, cameraIsMoving,
40
40
  // Aux MRT accumulates on its own epoch; hasPreviousAux=0 → write it verbatim instead of mixing.
41
41
  hasPreviousAux, auxAccumulationAlpha,
42
- transparentBackground,
43
42
  prevAccumTexture, prevAlbedoTexture, prevNormalDepthTexture,
44
43
  renderWidth,
45
44
  chunkRowBase, chunkRows, // row band offset (global first row) + row count for this chunk
@@ -85,7 +84,9 @@ export function buildFinalWriteKernel( params ) {
85
84
 
86
85
  const sampleColor = readRayRadiance( rayBufferRO, rayID );
87
86
  const finalColor = sampleColor.xyz.toVar();
88
- const outputAlpha = select( transparentBackground, sampleColor.w, float( 1.0 ) ).toVar();
87
+ // Coverage, whatever the backdrop: 0 where the camera saw past everything opaque. What shows or reads the
88
+ // image makes it opaque unless the background is transparent; auto exposure leaves a solid colour out by it.
89
+ const outputAlpha = sampleColor.w.toVar();
89
90
 
90
91
  // A pixel frozen in a prior frame was skipped by Generate, so its rayBuffer sample is stale — pass
91
92
  // the accumulated colour through unchanged (below) instead of mixing it. Inert unless adaptiveOn.
@@ -145,11 +146,7 @@ export function buildFinalWriteKernel( params ) {
145
146
 
146
147
  } );
147
148
 
148
- If( transparentBackground, () => {
149
-
150
- outputAlpha.assign( select( wasFrozen, prevAccumSample.w, mix( prevAccumSample.w, sampleColor.w, accumulationAlpha ) ) );
151
-
152
- } );
149
+ outputAlpha.assign( select( wasFrozen, prevAccumSample.w, mix( prevAccumSample.w, sampleColor.w, accumulationAlpha ) ) );
153
150
 
154
151
  } );
155
152
 
@@ -22,7 +22,7 @@ import {
22
22
  getStratifiedSample,
23
23
  } from './Random.js';
24
24
 
25
- import { generateRayFromCamera } from './CameraRay.js';
25
+ import { generateRayFromCamera, cameraClipDistance } from './CameraRay.js';
26
26
  import { Ray } from './Struct.js';
27
27
  import { RAY_FLAG, COUNTER } from '../Processor/QueueManager.js';
28
28
  import {
@@ -41,6 +41,7 @@ export function buildGenerateKernel( params ) {
41
41
  cameraWorldMatrix, cameraProjectionMatrixInverse,
42
42
  cameraProjection, panoLonRange, panoLatRange, panoLevelHorizon,
43
43
  enableDOF, focalLength, aperture, focusDistance, unitsPerMetre, apertureScale, anamorphicRatio, dofMode, dofBlur,
44
+ cameraClipStart,
44
45
  renderWidth,
45
46
  chunkRowBase, chunkRows, // row band offset (global first row) + row count for this chunk
46
47
  transmissiveBounces, // per-ray refraction budget (megakernel parity: PathTracerCore.js:606)
@@ -79,7 +80,10 @@ export function buildGenerateKernel( params ) {
79
80
  enableDOF, focalLength, aperture, focusDistance, unitsPerMetre, apertureScale, anamorphicRatio, dofMode, dofBlur,
80
81
  ) );
81
82
 
82
- writeRayOriginMeta( rayBufferRW, rayID, ray.origin, int( 0 ), int( 0 ) );
83
+ // The ray starts at the clip plane. The bidirectional weights still measure the first segment from the camera:
84
+ // the skipped length rides in the alpha-skip distance (mis_ray_t), as a cutout's would.
85
+ const clipAlong = cameraClipDistance( ray.direction, cameraClipStart, cameraWorldMatrix, cameraProjection ).toVar();
86
+ writeRayOriginMeta( rayBufferRW, rayID, ray.origin.add( ray.direction.mul( clipAlong ) ), int( 0 ), int( 0 ), uint( 0 ), bdpt ? clipAlong : null );
83
87
  // A fresh camera ray is NOT redirected — REDIRECTED stays clear so it sees the direct backdrop;
84
88
  // ShadeKernel sets REDIRECTED on the first direction-changing interaction (see RAY_FLAG.REDIRECTED).
85
89
  writeRayDirFlags( rayBufferRW, rayID, ray.direction, uint( RAY_FLAG.ACTIVE ) );
@@ -29,7 +29,7 @@ export function buildLightSplatKernel( params ) {
29
29
  hitBufferRO, splatBuffer,
30
30
  bvhBuffer, triangleBuffer, materialBuffer,
31
31
  lightPaths, slotsPerPath, lightTag, strategyView,
32
- cameraPosition, cameraForward, cameraViewMatrix, cameraProjectionMatrix, pixelArea,
32
+ cameraPosition, cameraForward, cameraClipStart, cameraViewMatrix, cameraProjectionMatrix, pixelArea,
33
33
  renderWidth, renderHeight,
34
34
  globalIlluminationIntensity, fireflyThreshold, accumFrame, frame,
35
35
  mergeVm = null,
@@ -68,7 +68,10 @@ export function buildLightSplatKernel( params ) {
68
68
  const culled = diffuseTransmission
69
69
  ? select( behind, v.extra.bitAnd( uint( 2 ) ), v.extra.bitAnd( uint( 1 ) ) ).notEqual( uint( 0 ) )
70
70
  : v.extra.notEqual( uint( 0 ) );
71
- If( ( diffuseTransmission ? cosFacet.equal( 0.0 ) : cosFacet.lessThanEqual( 0.0 ) ).or( cosCamera.lessThanEqual( 1e-6 ) ).or( culled ), () => {
71
+ // The camera sees nothing in front of its clip plane, this far back along the line to it.
72
+ const clipAlong = cameraClipStart.div( max( cosCamera, 1e-6 ) ).toVar();
73
+ If( ( diffuseTransmission ? cosFacet.equal( 0.0 ) : cosFacet.lessThanEqual( 0.0 ) ).or( cosCamera.lessThanEqual( 1e-6 ) ).or( culled )
74
+ .or( dist2.lessThanEqual( clipAlong.mul( clipAlong ) ) ), () => {
72
75
 
73
76
  Return();
74
77
 
@@ -123,9 +126,9 @@ export function buildLightSplatKernel( params ) {
123
126
  const origin = offsetRayOrigin( v.position, diffuseTransmission ? select( behind, v.facetN.negate(), v.facetN ) : v.facetN ).toVar();
124
127
  const segment = cameraPosition.sub( origin ).toVar();
125
128
  const segmentLength = length( segment ).toVar();
126
- // This segment stands in for the primary ray, which sees through the faces it culls.
129
+ // This segment stands in for the primary ray, which sees through the faces it culls and starts at the clip plane.
127
130
  const visibility = traceShadowRayRefractiveOpaque(
128
- origin, segment.div( segmentLength ), segmentLength.mul( SHADOW_END ),
131
+ origin, segment.div( segmentLength ), segmentLength.sub( clipAlong ).mul( SHADOW_END ),
129
132
  traverseBVHShadowCameraCulled, bvhBuffer, triangleBuffer, materialBuffer,
130
133
  );
131
134
 
@@ -60,6 +60,7 @@ export const PointLight = struct( {
60
60
  intensity: 'float',
61
61
  distance: 'float', // cutoff distance (0 = infinite)
62
62
  decay: 'float', // decay exponent (2 = physically correct)
63
+ radius: 'float', // 0 = a point; else a disc of this radius facing the lit point (Cycles' soft falloff)
63
64
  } );
64
65
 
65
66
  export const SpotLight = struct( {
@@ -75,6 +76,7 @@ export const SpotLight = struct( {
75
76
  goboIntensity: 'float', // mask strength multiplier (0 = no mask, 1 = full mask)
76
77
  iesIndex: 'int', // -1 = no IES, otherwise index into iesProfiles array
77
78
  iesIntensity: 'float', // blend [0,1] between flat (0) and full IES distribution (1)
79
+ radius: 'float', // as PointLight's
78
80
  } );
79
81
 
80
82
  export const LightSample = struct( {
@@ -173,7 +175,7 @@ export const getAreaLight = Fn( ( [ areaLightsBuffer, index ] ) => {
173
175
 
174
176
  export const getPointLight = Fn( ( [ pointLightsBuffer, index ] ) => {
175
177
 
176
- const baseIndex = index.mul( 9 );
178
+ const baseIndex = index.mul( 10 );
177
179
  return PointLight( {
178
180
  position: vec3(
179
181
  pointLightsBuffer.element( baseIndex ),
@@ -188,6 +190,7 @@ export const getPointLight = Fn( ( [ pointLightsBuffer, index ] ) => {
188
190
  intensity: pointLightsBuffer.element( baseIndex.add( 6 ) ),
189
191
  distance: pointLightsBuffer.element( baseIndex.add( 7 ) ),
190
192
  decay: pointLightsBuffer.element( baseIndex.add( 8 ) ),
193
+ radius: pointLightsBuffer.element( baseIndex.add( 9 ) ),
191
194
  } );
192
195
 
193
196
  } );
@@ -220,7 +223,8 @@ export const getSpotLight = Fn( ( [ spotLightsBuffer, index ] ) => {
220
223
  goboIntensity: spotLightsBuffer.element( baseIndex.add( 15 ) ),
221
224
  iesIndex: int( spotLightsBuffer.element( baseIndex.add( 16 ) ) ),
222
225
  iesIntensity: spotLightsBuffer.element( baseIndex.add( 17 ) ),
223
- // slots 18, 19 reserved (vec4 padding)
226
+ radius: spotLightsBuffer.element( baseIndex.add( 18 ) ),
227
+ // slot 19 reserved (vec4 padding)
224
228
  } );
225
229
 
226
230
  } );
@@ -2,7 +2,7 @@
2
2
  // Direct lighting calculations including shadow ray tracing
3
3
  // and contribution calculations for all light types.
4
4
 
5
- import { sceneResources } from './SceneResources.js';
5
+ import { sceneResources, usesTransmissionMaps } from './SceneResources.js';
6
6
  import {
7
7
  Fn,
8
8
  float,
@@ -30,6 +30,7 @@ import { Ray, ShadowMaterial, HitInfo } from './Struct.js';
30
30
  import {
31
31
  REC709_LUMINANCE_COEFFICIENTS, getShadowMaterial, instanceRows,
32
32
  instanceFaceNormalToWorld, triangleRow, shadowFlagsSettle, offsetRayOrigin,
33
+ getDatafromStorageBuffer, MATERIAL_SLOT, MATERIAL_SLOTS,
33
34
  } from './Common.js';
34
35
  import { fresnelDielectric } from './Fresnel.js';
35
36
  import { calculateBeerLawAbsorption } from './MaterialTransmission.js';
@@ -49,7 +50,8 @@ const makeTraceShadowRay = ( refractiveBlocks ) => Fn( ( [
49
50
  materialBuffer,
50
51
  ], builder ) => {
51
52
 
52
- const { alphaShadows, shadowAlbedoMaps } = sceneResources( builder );
53
+ const { alphaShadows, shadowAlbedoMaps, linearBuckets } = sceneResources( builder );
54
+ const transmissionMaps = ! refractiveBlocks && usesTransmissionMaps( builder );
53
55
  const transmittance = float( 1.0 ).toVar();
54
56
  const rayOrigin = origin.toVar();
55
57
  const remainingDist = float( maxDist ).toVar();
@@ -88,6 +90,19 @@ const makeTraceShadowRay = ( refractiveBlocks ) => Fn( ( [
88
90
  // Fetch material for the hit surface (thin reader: 7 slots instead of 27)
89
91
  const shadowMaterial = ShadowMaterial.wrap( getShadowMaterial( shadowHit.materialIndex, materialBuffer ) );
90
92
 
93
+ // The hit's texture coordinates through the albedo transform, which Shade reads the extension maps with too.
94
+ const hitTextureUV = () => {
95
+
96
+ const baryU = shadowHit.uv.x;
97
+ const baryV = shadowHit.uv.y;
98
+ const baryW = float( 1.0 ).sub( baryU ).sub( baryV );
99
+ const uvData1 = uintBitsToFloat( triangleRow( triangleBuffer, shadowHit.triangleIndex, 3 ) );
100
+ const uvData2 = uintBitsToFloat( triangleRow( triangleBuffer, shadowHit.triangleIndex, 4 ).xy );
101
+ const hitUV = uvData1.xy.mul( baryW ).add( uvData1.zw.mul( baryU ) ).add( uvData2.mul( baryV ) );
102
+ return getTransformedUV( { uv: hitUV, transform: shadowMaterial.albedoTransform } );
103
+
104
+ };
105
+
91
106
  // ---------------------------------------------------------------
92
107
  // Alpha-cutout handling (MASK / BLEND with albedo texture alpha)
93
108
  // Gated by runtime uniform + alphaMode check — zero overhead for opaque materials.
@@ -106,14 +121,7 @@ const makeTraceShadowRay = ( refractiveBlocks ) => Fn( ( [
106
121
 
107
122
  If( shadowMaterial.albedoMapIndex.greaterThanEqual( int( 0 ) ), () => {
108
123
 
109
- const baryU = shadowHit.uv.x;
110
- const baryV = shadowHit.uv.y;
111
- const baryW = float( 1.0 ).sub( baryU ).sub( baryV );
112
- const uvData1 = uintBitsToFloat( triangleRow( triangleBuffer, shadowHit.triangleIndex, 3 ) );
113
- const uvData2 = uintBitsToFloat( triangleRow( triangleBuffer, shadowHit.triangleIndex, 4 ).xy );
114
- const hitUV = uvData1.xy.mul( baryW ).add( uvData1.zw.mul( baryU ) ).add( uvData2.mul( baryV ) );
115
- const albedoUV = getTransformedUV( { uv: hitUV, transform: shadowMaterial.albedoTransform } );
116
- texAlpha.assign( sampleBucket( shadowAlbedoMaps, shadowMaterial.albedoMapIndex, albedoUV ).a );
124
+ texAlpha.assign( sampleBucket( shadowAlbedoMaps, shadowMaterial.albedoMapIndex, hitTextureUV() ).a );
117
125
 
118
126
  } );
119
127
 
@@ -167,6 +175,20 @@ const makeTraceShadowRay = ( refractiveBlocks ) => Fn( ( [
167
175
 
168
176
  } : () => {
169
177
 
178
+ let transmission = shadowMaterial.transmission;
179
+ if ( transmissionMaps ) {
180
+
181
+ // glTF's transmission is the factor × the map's R: where the map is black the surface is opaque.
182
+ transmission = transmission.toVar();
183
+ const mapIndex = int( getDatafromStorageBuffer( materialBuffer, shadowHit.materialIndex, int( MATERIAL_SLOT.EXT_MAP_INDICES_A ), int( MATERIAL_SLOTS ) ).r ).toVar();
184
+ If( mapIndex.greaterThanEqual( int( 0 ) ), () => {
185
+
186
+ transmission.mulAssign( sampleBucket( linearBuckets, mapIndex, hitTextureUV() ).r );
187
+
188
+ } );
189
+
190
+ }
191
+
170
192
  // Deferred geometric-normal compute — refetch triangle positions and
171
193
  // derive the normal here so opaque/alpha-cutout shadow hits don't pay
172
194
  // the cross+normalize cost in BVH traversal.
@@ -204,7 +226,7 @@ const makeTraceShadowRay = ( refractiveBlocks ) => Fn( ( [
204
226
  // Shadow rays go straight through, so both faces see the outside-in angle and eta.
205
227
  const fresnel = fresnelDielectric( abs( dot( dir, N ) ), max( shadowMaterial.ior, 1.0 ) );
206
228
 
207
- const matTransmittance = float( 1.0 ).sub( fresnel ).mul( shadowMaterial.transmission );
229
+ const matTransmittance = float( 1.0 ).sub( fresnel ).mul( transmission );
208
230
  transmittance.mulAssign( matTransmittance );
209
231
 
210
232
  // Early exit if almost no light passes through