rayzee 11.7.0 → 11.8.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 (59) hide show
  1. package/README.md +13 -0
  2. package/dist/{ArchiveImporter-DYcRXeKE.js → ArchiveImporter-_sJm5uaK.js} +1923 -1243
  3. package/dist/{BidirectionalIntegrator-BvjRpbTP.js → BidirectionalIntegrator-Dzt4Z97x.js} +290 -289
  4. package/dist/{ColorManagement-oeEk7oSu.js → ColorManagement-30MfpHwQ.js} +1 -1
  5. package/dist/{EngineIssues-CIZnXHsF.js → EngineIssues-xljoXM0E.js} +18 -8
  6. package/dist/{NeuralRetouch-ClaMiKYg.js → NeuralRetouch-BKQCsmEs.js} +2 -2
  7. package/dist/{NeuralSuperRes-WNEXun9s.js → NeuralSuperRes-C4ymq68e.js} +2 -2
  8. package/dist/{PathTracer-B9wZPPVL.js → PathTracer-C_to_i40.js} +3156 -3098
  9. package/dist/{PhysicalSky-DcjItqKi.js → PhysicalSky-bMHbm_7H.js} +1 -1
  10. package/dist/{RayzeeRenderer-p-NeE6E2.js → RayzeeRenderer-B_aB6VTJ.js} +370 -371
  11. package/dist/SceneMetadata-CSmlJW4c.js +183 -0
  12. package/dist/{ToneMapGPU-sAs8wdgI.js → ToneMapGPU-4GPwoG_c.js} +13 -6
  13. package/dist/addons/archives.es.js +1 -1
  14. package/dist/addons/bidirectional.es.js +1 -1
  15. package/dist/addons/color.es.js +1 -1
  16. package/dist/addons/physical-sky.es.js +1 -1
  17. package/dist/addons/storage.es.js +1 -1
  18. package/dist/{openStorage-Fe9BoS1_.js → openStorage-B085D9FR.js} +1 -1
  19. package/dist/rayzee-core.es.js +5 -5
  20. package/dist/rayzee.es.js +815 -796
  21. package/dist/rayzee.umd.js +106 -102
  22. package/dist/{sceneCachePolicy-B9w3BnaP.js → sceneCachePolicy-D_uBSbQF.js} +2 -2
  23. package/package.json +1 -1
  24. package/src/EngineIssues.js +20 -6
  25. package/src/Passes/AIUpscaler.js +4 -0
  26. package/src/Passes/OIDNDenoiser.js +8 -6
  27. package/src/PathTracerApp.js +10 -1
  28. package/src/Processor/ArchiveImporter.js +23 -6
  29. package/src/Processor/AssetLoader.js +15 -0
  30. package/src/Processor/LightSerializer.js +6 -2
  31. package/src/Processor/PBRT/PBRTMaterials.js +2 -1
  32. package/src/Processor/PBRT/PBRTSceneBuilder.js +1 -1
  33. package/src/Processor/PlatformImageLoader.js +9 -0
  34. package/src/Processor/TextureCreator.js +44 -4
  35. package/src/Processor/ToneMapCPU.js +34 -5
  36. package/src/Processor/ToneMapGPU.js +15 -9
  37. package/src/Processor/USD/USDScene.js +981 -17
  38. package/src/RayzeeRenderer.js +81 -26
  39. package/src/Stages/AutoExposure.js +21 -8
  40. package/src/Stages/Compositor.js +39 -1
  41. package/src/Stages/LocalExposure.js +44 -18
  42. package/src/Stages/NormalDepth.js +7 -3
  43. package/src/Stages/PathTracer.js +25 -1
  44. package/src/Stages/PathTracerStage.js +9 -1
  45. package/src/TSL/CameraRay.js +12 -1
  46. package/src/TSL/DebugKernel.js +3 -2
  47. package/src/TSL/FinalWriteKernel.js +4 -7
  48. package/src/TSL/GenerateKernel.js +6 -2
  49. package/src/TSL/LightSplatKernel.js +7 -4
  50. package/src/TSL/LightsCore.js +6 -2
  51. package/src/TSL/LightsSampling.js +45 -12
  52. package/src/TSL/SceneResources.js +11 -0
  53. package/src/TSL/ShadeKernel.js +1 -2
  54. package/src/integrators/BidirectionalIntegrator.js +1 -0
  55. package/src/managers/CameraManager.js +9 -0
  56. package/src/managers/DenoisingManager.js +1 -0
  57. package/src/managers/InteractionManager.js +6 -0
  58. package/src/managers/UniformManager.js +3 -1
  59. package/dist/SceneMetadata-D2Sh-0e-.js +0 -110
@@ -1437,13 +1437,18 @@ export class PathTracerStage extends RenderStage {
1437
1437
  */
1438
1438
  _updateCameraUniforms() {
1439
1439
 
1440
+ // Only an imported camera's clip plane: `near` is the viewer's own for its default camera.
1441
+ const clipStart = Math.max( this.camera.userData?.clipStart ?? 0, 0 );
1442
+
1440
1443
  if ( ! this._matricesApproxEqual( this.lastCameraMatrix, this.camera.matrixWorld ) ||
1441
- ! this._matricesApproxEqual( this.lastProjectionMatrix, this.camera.projectionMatrixInverse ) ) {
1444
+ ! this._matricesApproxEqual( this.lastProjectionMatrix, this.camera.projectionMatrixInverse ) ||
1445
+ clipStart !== this.cameraClipStart.value ) {
1442
1446
 
1443
1447
  this.cameraWorldMatrix.value.copy( this.camera.matrixWorld );
1444
1448
  this.cameraViewMatrix.value.copy( this.camera.matrixWorldInverse );
1445
1449
  this.cameraProjectionMatrix.value.copy( this.camera.projectionMatrix );
1446
1450
  this.cameraProjectionMatrixInverse.value.copy( this.camera.projectionMatrixInverse );
1451
+ this.cameraClipStart.value = clipStart;
1447
1452
 
1448
1453
  this.lastCameraMatrix.copy( this.camera.matrixWorld );
1449
1454
  this.lastProjectionMatrix.copy( this.camera.projectionMatrixInverse );
@@ -1503,6 +1508,9 @@ export class PathTracerStage extends RenderStage {
1503
1508
  // freezes when the frame retires or the camera moves, which is what a denoiser needs.
1504
1509
  context.setState( 'pathtracer:samples', this.frameCount );
1505
1510
  context.setState( 'renderMode', this.renderMode.value );
1511
+ // What a camera ray that sees nothing shows; the colour texture's alpha is 0 there in every case.
1512
+ context.setState( 'pathtracer:backdrop', this.uniforms.get( 'transparentBackground' ).value ? 'transparent'
1513
+ : this.uniforms.get( 'showBackground' ).value ? 'environment' : 'color' );
1506
1514
 
1507
1515
  }
1508
1516
 
@@ -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
 
@@ -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,
@@ -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
  } );
@@ -86,7 +86,9 @@ import {
86
86
  offsetRayOrigin,
87
87
  SHADOW_END,
88
88
  REC709_LUMINANCE_COEFFICIENTS,
89
+ constructTBN,
89
90
  } from './Common.js';
91
+ import { usesLightRadius } from './SceneResources.js';
90
92
  import { sampleEnvironment, sampleEnvironmentExact } from './Environment.js';
91
93
  import { shadowTerminatorOrigin } from './ShadowTerminator.js';
92
94
  import { sunRadianceToward, sampleSunDisc } from './Sun.js';
@@ -233,8 +235,30 @@ export const sampleRectAreaLight = Fn( ( [ light, rayOrigin, ruv, lightSelection
233
235
 
234
236
  } );
235
237
 
236
- // Enhanced spot light sampling with radius support
237
- export const sampleSpotLightWithRadius = Fn( ( [ light, rayOrigin, lightSelectionPdf ] ) => {
238
+ /**
239
+ * A point or spot light with a radius is a disc of it facing the lit point, as Cycles' with Soft Falloff: uniform on the
240
+ * disc, so on its axis the light gives intensity / ( d² + r² ) where a point gives intensity / d². Moves `toLight` and
241
+ * `distSq` to the sampled point and returns the disc's tilt toward it. No BSDF ray hits these lights, so no MIS.
242
+ */
243
+ const aimAtLightDisc = ( light, toLight, distSq, ruv ) => {
244
+
245
+ const facing = float( 1.0 ).toVar();
246
+ If( light.radius.greaterThan( 0.0 ), () => {
247
+
248
+ const axis = toLight.div( sqrt( distSq ) ).toVar();
249
+ const r = light.radius.mul( sqrt( ruv.x ) );
250
+ const phi = ruv.y.mul( 2 * Math.PI );
251
+ toLight.addAssign( constructTBN( { N: axis } ).mul( vec3( r.mul( cos( phi ) ), r.mul( sin( phi ) ), 0.0 ) ) );
252
+ distSq.assign( dot( toLight, toLight ) );
253
+ facing.assign( max( dot( toLight, axis ).div( sqrt( distSq ) ), 0.0 ) );
254
+
255
+ } );
256
+ return facing;
257
+
258
+ };
259
+
260
+ // Spot light sampling; a radius makes it a disc (aimAtLightDisc), its cone still read from the centre.
261
+ export const sampleSpotLightWithRadius = Fn( ( [ light, rayOrigin, lightSelectionPdf, ruv ], builder ) => {
238
262
 
239
263
  const ls_valid = tslBool( false ).toVar();
240
264
  const ls_direction = vec3( 0.0, 1.0, 0.0 ).toVar();
@@ -250,11 +274,16 @@ export const sampleSpotLightWithRadius = Fn( ( [ light, rayOrigin, lightSelectio
250
274
  // Guard against zero distance
251
275
  If( lightDistSq.greaterThanEqual( 1e-20 ), () => {
252
276
 
277
+ // Compiled in only while some light has a radius (SceneResources lightRadius); otherwise the exact point code.
278
+ const sized = usesLightRadius( builder );
279
+ const centerDir = sized ? toLight.div( sqrt( lightDistSq ) ).toVar() : null;
280
+ const facing = sized ? aimAtLightDisc( light, toLight, lightDistSq, ruv ) : null;
253
281
  const lightDist = sqrt( lightDistSq ).toVar();
254
282
  const lightDir = toLight.div( lightDist ).toVar();
283
+ const coneDir = sized ? centerDir : lightDir;
255
284
 
256
285
  // Check cone attenuation
257
- const spotCosAngle = dot( lightDir.negate(), light.direction ).toVar();
286
+ const spotCosAngle = dot( coneDir.negate(), light.direction ).toVar();
258
287
  const coneCosAngle = cos( light.angle ).toVar();
259
288
 
260
289
  ls_direction.assign( lightDir );
@@ -268,10 +297,11 @@ export const sampleSpotLightWithRadius = Fn( ( [ light, rayOrigin, lightSelectio
268
297
  const distanceAttenuation = getDistanceAttenuation( { lightDistance: lightDist, cutoffDistance: light.distance, decayExponent: light.decay } );
269
298
 
270
299
  // Gobo projection mask + IES photometric profile — both 1.0 when not assigned.
271
- const goboMask = sampleSpotGoboMask( light, lightDir );
272
- const iesProfile = sampleIESProfile( light, lightDir );
300
+ const goboMask = sampleSpotGoboMask( light, coneDir );
301
+ const iesProfile = sampleIESProfile( light, coneDir );
273
302
 
274
- ls_emission.assign( light.color.mul( light.intensity ).mul( distanceAttenuation ).mul( coneAttenuation ).mul( goboMask ).mul( iesProfile ) );
303
+ const emission = light.color.mul( light.intensity ).mul( distanceAttenuation ).mul( coneAttenuation ).mul( goboMask ).mul( iesProfile );
304
+ ls_emission.assign( sized ? emission.mul( facing ) : emission );
275
305
 
276
306
  } );
277
307
 
@@ -290,7 +320,7 @@ export const sampleSpotLightWithRadius = Fn( ( [ light, rayOrigin, lightSelectio
290
320
  } );
291
321
 
292
322
  // Enhanced point light sampling with distance attenuation
293
- export const samplePointLightWithAttenuation = Fn( ( [ light, rayOrigin, lightSelectionPdf ] ) => {
323
+ export const samplePointLightWithAttenuation = Fn( ( [ light, rayOrigin, lightSelectionPdf, ruv ], builder ) => {
294
324
 
295
325
  const ls_valid = tslBool( false ).toVar();
296
326
  const ls_direction = vec3( 0.0, 1.0, 0.0 ).toVar();
@@ -306,6 +336,8 @@ export const samplePointLightWithAttenuation = Fn( ( [ light, rayOrigin, lightSe
306
336
  // Guard against zero distance
307
337
  If( lightDistSq.greaterThanEqual( 1e-20 ), () => {
308
338
 
339
+ const sized = usesLightRadius( builder );
340
+ const facing = sized ? aimAtLightDisc( light, toLight, lightDistSq, ruv ) : null;
309
341
  const lightDist = sqrt( lightDistSq ).toVar();
310
342
  const lightDir = toLight.div( lightDist );
311
343
 
@@ -315,7 +347,8 @@ export const samplePointLightWithAttenuation = Fn( ( [ light, rayOrigin, lightSe
315
347
  ls_lightType.assign( int( LIGHT_TYPE_POINT ) );
316
348
  ls_direction.assign( lightDir );
317
349
  ls_distance.assign( lightDist );
318
- ls_emission.assign( light.color.mul( light.intensity ).mul( distanceAttenuation ) );
350
+ const emission = light.color.mul( light.intensity ).mul( distanceAttenuation );
351
+ ls_emission.assign( sized ? emission.mul( facing ) : emission );
319
352
  ls_pdf.assign( lightSelectionPdf );
320
353
  ls_valid.assign( tslBool( true ) );
321
354
 
@@ -614,7 +647,7 @@ export const sampleLightWithImportance = Fn( ( [
614
647
 
615
648
  If( light.intensity.greaterThan( 0.0 ), () => {
616
649
 
617
- const ptSample = LightSample.wrap( samplePointLightWithAttenuation( light, rayOrigin, lightSelectionPdf ) );
650
+ const ptSample = LightSample.wrap( samplePointLightWithAttenuation( light, rayOrigin, lightSelectionPdf, vec2( randomSeed.y, getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 15 ) ), rngState, resolution, frame ) ) ) );
618
651
  r_valid.assign( ptSample.valid );
619
652
  r_direction.assign( ptSample.direction );
620
653
  r_emission.assign( ptSample.emission );
@@ -639,7 +672,7 @@ export const sampleLightWithImportance = Fn( ( [
639
672
 
640
673
  If( light.intensity.greaterThan( 0.0 ), () => {
641
674
 
642
- const spotSample = LightSample.wrap( sampleSpotLightWithRadius( light, rayOrigin, lightSelectionPdf ) );
675
+ const spotSample = LightSample.wrap( sampleSpotLightWithRadius( light, rayOrigin, lightSelectionPdf, vec2( randomSeed.y, getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 15 ) ), rngState, resolution, frame ) ) ) );
643
676
  r_valid.assign( spotSample.valid );
644
677
  r_direction.assign( spotSample.direction );
645
678
  r_emission.assign( spotSample.emission );
@@ -702,7 +735,7 @@ export const sampleLightWithImportance = Fn( ( [
702
735
  If( selectedType.equal( int( 2 ) ).and( selectedIdx.greaterThanEqual( int( 0 ) ) ), () => {
703
736
 
704
737
  const light = PointLight.wrap( getPointLight( pointLightsBuffer, selectedIdx ) );
705
- const ptSample = LightSample.wrap( samplePointLightWithAttenuation( light, rayOrigin, pdf ) );
738
+ const ptSample = LightSample.wrap( samplePointLightWithAttenuation( light, rayOrigin, pdf, vec2( randomSeed.y, getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 15 ) ), rngState, resolution, frame ) ) ) );
706
739
  r_valid.assign( ptSample.valid );
707
740
  r_direction.assign( ptSample.direction );
708
741
  r_emission.assign( ptSample.emission );
@@ -716,7 +749,7 @@ export const sampleLightWithImportance = Fn( ( [
716
749
  If( selectedType.equal( int( 3 ) ).and( selectedIdx.greaterThanEqual( int( 0 ) ) ), () => {
717
750
 
718
751
  const light = SpotLight.wrap( getSpotLight( spotLightsBuffer, selectedIdx ) );
719
- const spotSample = LightSample.wrap( sampleSpotLightWithRadius( light, rayOrigin, pdf ) );
752
+ const spotSample = LightSample.wrap( sampleSpotLightWithRadius( light, rayOrigin, pdf, vec2( randomSeed.y, getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 15 ) ), rngState, resolution, frame ) ) ) );
720
753
  r_valid.assign( spotSample.valid );
721
754
  r_direction.assign( spotSample.direction );
722
755
  r_emission.assign( spotSample.emission );
@@ -12,6 +12,7 @@
12
12
  * @property {?Node} iesProfiles - IES profile texture array, or null
13
13
  * @property {?Node} alphaShadows - int uniform, 1 = shadow rays test texture alpha; null = never
14
14
  * @property {?Object<string, boolean>} materialLayers - the MATERIAL_LAYERS some material uses; null = all of them
15
+ * @property {boolean} [lightRadius] - some point or spot light has a radius (path tracing only); false = all are points
15
16
  */
16
17
 
17
18
  import { context } from 'three/tsl';
@@ -49,3 +50,13 @@ export function materialLayers( builder ) {
49
50
  return builder.context[ KEY ]?.materialLayers ?? ALL_MATERIAL_LAYERS;
50
51
 
51
52
  }
53
+
54
+ /**
55
+ * Whether the kernel being built samples light radii. Off unless a point or spot light has a radius, and off for the
56
+ * bidirectional integrator, whose light paths leave a light's centre. Outside a scene kernel (a test) it is on.
57
+ */
58
+ export function usesLightRadius( builder ) {
59
+
60
+ return builder.context[ KEY ]?.lightRadius ?? true;
61
+
62
+ }
@@ -653,8 +653,7 @@ export function buildShadeKernel( params ) {
653
653
  // surface that escapes to env keeps alpha 1 (HAS_HIT_OPAQUE set), so glass-in-front-of-an-
654
654
  // object stays opaque while glass-in-front-of-sky exports see-through.
655
655
  // Ungated (was transparentBackground-only) so the alpha lane always carries coverage: it is the
656
- // only hit/miss signal valid at FinalWrite. The image is unchanged — FinalWrite forces alpha 1
657
- // when transparent-bg is off.
656
+ // only hit/miss signal valid at FinalWrite, which accumulates it as is.
658
657
  If( flags.bitAnd( uint( RAY_FLAG.HAS_HIT_OPAQUE ) ).equal( uint( 0 ) ), () => {
659
658
 
660
659
  currentRadiance.w.assign( 0.0 );
@@ -336,6 +336,7 @@ export class BidirectionalIntegrator {
336
336
  strategyView: bd.strategyView,
337
337
  cameraPosition: bd.cameraPosition,
338
338
  cameraForward: bd.cameraForward,
339
+ cameraClipStart: this.pt.cameraClipStart,
339
340
  cameraViewMatrix: this.pt.cameraViewMatrix,
340
341
  cameraProjectionMatrix: this.pt.cameraProjectionMatrix,
341
342
  pixelArea: bd.pixelArea,
@@ -93,6 +93,7 @@ export class CameraManager extends EventDispatcher {
93
93
 
94
94
  this.cameras = cameras;
95
95
  this._userCameraCounter = 0;
96
+ this.camera.userData.clipStart = 0;
96
97
  this.resetAutoFocus();
97
98
 
98
99
  }
@@ -120,6 +121,7 @@ export class CameraManager extends EventDispatcher {
120
121
  cam.userData.__rayzeeUserCamera = true;
121
122
  cam.userData.__rayzeeEffects = this._captureEffects();
122
123
  if ( this.controls ) cam.userData.__rayzeeOrbitTarget = this.controls.target.clone();
124
+ if ( src.userData.clipStart > 0 ) cam.userData.clipStart = src.userData.clipStart;
123
125
 
124
126
  this._userCameraCounter += 1;
125
127
  cam.name = name || `View Camera ${this._userCameraCounter}`;
@@ -310,6 +312,7 @@ export class CameraManager extends EventDispatcher {
310
312
  this.camera.fov = s.fov;
311
313
  this.camera.near = s.near;
312
314
  this.camera.far = s.far;
315
+ this.camera.userData.clipStart = 0;
313
316
  this.camera.updateProjectionMatrix();
314
317
  this.camera.updateMatrixWorld( true );
315
318
 
@@ -380,6 +383,8 @@ export class CameraManager extends EventDispatcher {
380
383
  if ( source.isPerspectiveCamera ) this.camera.fov = source.fov;
381
384
  this.camera.near = source.near;
382
385
  this.camera.far = source.far;
386
+ // Only an imported camera's clip start clips the render: see PathTracerStage._updateCameraUniforms.
387
+ this.camera.userData.clipStart = source.userData?.clipStart ?? 0;
383
388
  this.camera.updateProjectionMatrix();
384
389
  this.camera.updateMatrixWorld( true );
385
390
 
@@ -611,6 +616,7 @@ export class CameraManager extends EventDispatcher {
611
616
  fov: camera.fov,
612
617
  near: camera.near,
613
618
  far: camera.far,
619
+ clipStart: camera.userData.clipStart ?? 0,
614
620
  zoom: camera.zoom,
615
621
  orthographic: !! camera.orthographic,
616
622
  orthoHalfHeight: camera.orthoHalfHeight,
@@ -641,6 +647,7 @@ export class CameraManager extends EventDispatcher {
641
647
  aspect: cam.aspect ?? null,
642
648
  near: cam.near,
643
649
  far: cam.far,
650
+ clipStart: cam.userData.clipStart ?? 0,
644
651
  zoom: cam.zoom,
645
652
  position: cam.position.toArray(),
646
653
  quaternion: cam.quaternion.toArray(),
@@ -684,6 +691,7 @@ export class CameraManager extends EventDispatcher {
684
691
  cam.updateMatrixWorld( true );
685
692
  cam.userData.__rayzeeUserCamera = true;
686
693
  cam.userData.__rayzeeEffects = fromPortable( entry.effects );
694
+ if ( entry.clipStart > 0 ) cam.userData.clipStart = entry.clipStart;
687
695
  if ( entry.orbitTarget ) cam.userData.__rayzeeOrbitTarget = new Vector3().fromArray( entry.orbitTarget );
688
696
  indexMap.set( entry.index, this.cameras.length );
689
697
  this.cameras.push( cam );
@@ -730,6 +738,7 @@ export class CameraManager extends EventDispatcher {
730
738
  camera.fov = v.fov;
731
739
  camera.near = v.near;
732
740
  camera.far = v.far;
741
+ camera.userData.clipStart = v.clipStart ?? 0;
733
742
  this._setView( v.orthographic, v.orthoHalfHeight );
734
743
  camera.zoom = v.zoom ?? 1;
735
744
  camera.updateProjectionMatrix();
@@ -426,6 +426,7 @@ export class DenoisingManager extends EventDispatcher {
426
426
  getToneMapping: () => this._getToneMapping(),
427
427
  getSaturation: () => this._getSaturation(),
428
428
  getDisplayGain: () => this._getDisplayGain?.() ?? null,
429
+ getTransparentBackground: () => !! this._settings?.get( 'transparentBackground' ),
429
430
  } );
430
431
 
431
432
  this.upscaler.enabled = DENOISER_DEFAULTS.enableUpscaler || false;
@@ -704,6 +704,12 @@ export class InteractionManager extends EventDispatcher {
704
704
  */
705
705
  _intersectScene() {
706
706
 
707
+ // Nothing in front of an imported camera's clip plane is rendered, so nothing there is picked. An orthographic
708
+ // ray already starts on the near plane, which a switch to such a camera set to its clip start.
709
+ const clipStart = this.camera.userData?.clipStart ?? 0;
710
+ this.raycaster.near = clipStart > 0 && ! this.camera.isOrthographicCamera
711
+ ? clipStart / Math.max( this.raycaster.ray.direction.dot( this.camera.getWorldDirection( _viewAxis ) ), 1e-4 )
712
+ : 0;
707
713
  return this.isGeometryOnDisk?.() ? [] : this.raycaster.intersectObjects( this.scene.children, true );
708
714
 
709
715
  }
@@ -23,7 +23,7 @@ const SHADER_NAMES = {
23
23
  export const LIGHT_FLOATS = Object.freeze( {
24
24
  directional: 12, // 8 light fields + gobo { index, signed intensity, scale, pad }
25
25
  area: 16, // 13 base + normalize, spread, shape
26
- point: 9,
26
+ point: 10, // 9 light fields + radius
27
27
  spot: 20, // 14 light fields + gobo { index, signed intensity } + IES { index, intensity } + 2 reserved
28
28
  } );
29
29
 
@@ -301,6 +301,8 @@ export class UniformManager {
301
301
  u( 'cameraProjectionMatrixInverse', new Matrix4(), 'mat4' );
302
302
  u( 'cameraViewMatrix', new Matrix4(), 'mat4' );
303
303
  u( 'cameraProjectionMatrix', new Matrix4(), 'mat4' );
304
+ // An imported camera's near clipping plane: camera rays start this far in front of it (0 = at the camera).
305
+ u( 'cameraClipStart', 0, 'float' );
304
306
 
305
307
  // Projection: CAMERA_PROJECTION_IDS. Ranges are radians (UI carries degrees).
306
308
  const radRange = ( [ min, max ] ) => new Vector2( min, max ).multiplyScalar( MathUtils.DEG2RAD );
@@ -1,110 +0,0 @@
1
- //#region src/Processor/archiveFormats.js
2
- var e = Object.freeze({
3
- zip: {
4
- type: "archive",
5
- name: "ZIP Archive"
6
- },
7
- gz: {
8
- type: "archive",
9
- name: "Gzipped TAR Archive"
10
- },
11
- tgz: {
12
- type: "archive",
13
- name: "Gzipped TAR Archive"
14
- },
15
- tar: {
16
- type: "archive",
17
- name: "TAR Archive"
18
- }
19
- }), t = (e) => typeof Blob < "u" && e instanceof Blob, n = (e) => !!e && typeof e == "object" && !t(e) && e.files != null, r = (e) => e.startsWith(".") || e === "__MACOSX";
20
- function i({ name: e = null, files: n }, { isModel: i = () => !1 } = {}) {
21
- let a = /* @__PURE__ */ new Map();
22
- for (let e of n) {
23
- let n = t(e) ? e : e.file, i = t(e) ? e.webkitRelativePath || e.name : e.path, o = String(i ?? "").replace(/\\/g, "/").split("/").filter((e) => e && e !== ".").join("/");
24
- !o || !n || o.split("/").some(r) || a.set(o, {
25
- path: o,
26
- file: n
27
- });
28
- }
29
- let o = [...a.values()].sort((e, t) => e.path < t.path ? -1 : +(e.path > t.path)), s = o[0]?.path.split("/")[0], c = o.length > 0 && o.every((e) => e.path.startsWith(s + "/")) ? s : null;
30
- return {
31
- name: e ?? c ?? (o.find((e) => i(e.path)) ?? o[0])?.path.split("/").pop() ?? "Folder",
32
- files: o,
33
- flat: c === null && o.every((e) => !e.path.includes("/"))
34
- };
35
- }
36
- //#endregion
37
- //#region src/Processor/SceneMetadata.js
38
- var a = [
39
- "sourceFile",
40
- "url",
41
- "file"
42
- ];
43
- function o(e) {
44
- if (typeof e == "string") try {
45
- let t = JSON.parse(e);
46
- return t && typeof t == "object" ? t : null;
47
- } catch {
48
- return null;
49
- }
50
- return e && typeof e == "object" ? e : null;
51
- }
52
- function s(e) {
53
- let t = typeof e == "string" ? parseFloat(e) : e;
54
- return Number.isFinite(t) ? t : void 0;
55
- }
56
- function c(e) {
57
- let t = o(e);
58
- if (!t) return null;
59
- let n = (e) => e && (e.environment !== void 0 || e.render !== void 0), r = o(t.rayzee);
60
- return n(r) ? r : n(t) ? t : null;
61
- }
62
- function l(e) {
63
- let t = o(e);
64
- if (!t) return null;
65
- let n = a.map((e) => t[e]).find((e) => typeof e == "string" && e.trim());
66
- if (!n) return null;
67
- let r = s(t.rotation), i = s(t.intensity), c = { sourceFile: n.trim() };
68
- return r !== void 0 && (c.rotation = r), i !== void 0 && (c.intensity = Math.max(0, i)), c;
69
- }
70
- var u = [
71
- "maxBounces",
72
- "samples",
73
- "width",
74
- "height"
75
- ];
76
- function d(e) {
77
- let t = o(e);
78
- if (!t) return null;
79
- let n = {};
80
- for (let e of u) {
81
- let r = s(t[e]);
82
- r !== void 0 && r > 0 && (n[e] = Math.round(r));
83
- }
84
- return Object.keys(n).length ? n : null;
85
- }
86
- function f(e) {
87
- let t = c(e);
88
- if (!t) return null;
89
- let n = l(t.environment), r = d(t.render);
90
- return !n && !r ? null : {
91
- ...n ? { environment: n } : {},
92
- ...r ? { render: r } : {}
93
- };
94
- }
95
- function p(e) {
96
- if (!e) return null;
97
- let t = [
98
- e.userData,
99
- e.asset?.extras,
100
- e.scene?.userData
101
- ];
102
- Array.isArray(e.scenes) && t.push(...e.scenes.map((e) => e?.userData));
103
- for (let e of t) {
104
- let t = f(e);
105
- if (t) return t;
106
- }
107
- return null;
108
- }
109
- //#endregion
110
- export { i as a, n as i, f as n, e as r, p as t };