rayzee 4.8.4 → 4.8.7

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 (35) hide show
  1. package/README.md +141 -82
  2. package/dist/assets/AIUpscalerWorker-D58dcMrY.js +2 -0
  3. package/dist/assets/AIUpscalerWorker-D58dcMrY.js.map +1 -0
  4. package/dist/assets/BVHRefitWorker-GkmNJYvb.js +2 -0
  5. package/dist/assets/BVHRefitWorker-GkmNJYvb.js.map +1 -0
  6. package/dist/assets/BVHSubtreeWorker-C02ZWVeG.js +2 -0
  7. package/dist/assets/BVHSubtreeWorker-C02ZWVeG.js.map +1 -0
  8. package/dist/assets/BVHWorker-DobVXMda.js +2 -0
  9. package/dist/assets/BVHWorker-DobVXMda.js.map +1 -0
  10. package/dist/assets/CDFWorker-2MoynL4F.js +2 -0
  11. package/dist/assets/CDFWorker-2MoynL4F.js.map +1 -0
  12. package/dist/assets/TexturesWorker-DBqGmVdR.js +2 -0
  13. package/dist/assets/TexturesWorker-DBqGmVdR.js.map +1 -0
  14. package/dist/rayzee.es.js +922 -871
  15. package/dist/rayzee.es.js.map +1 -1
  16. package/dist/rayzee.umd.js +43 -43
  17. package/dist/rayzee.umd.js.map +1 -1
  18. package/package.json +1 -1
  19. package/src/Passes/AIUpscaler.js +1 -2
  20. package/src/PathTracerApp.js +36 -3
  21. package/src/Processor/AssetLoader.js +2 -2
  22. package/src/Processor/BVHBuilder.js +1 -2
  23. package/src/Processor/EquirectHDRInfo.js +1 -2
  24. package/src/Processor/LightSerializer.js +26 -7
  25. package/src/Processor/ParallelBVHBuilder.js +8 -9
  26. package/src/Processor/SceneProcessor.js +3 -4
  27. package/src/Processor/TextureCreator.js +1 -2
  28. package/src/Processor/Workers/AIUpscalerWorker.js +21 -7
  29. package/src/README.md +1 -2
  30. package/src/Stages/PathTracer.js +7 -6
  31. package/src/TSL/LightsCore.js +12 -2
  32. package/src/TSL/LightsSampling.js +8 -6
  33. package/src/managers/LightManager.js +1 -1
  34. package/src/managers/UniformManager.js +2 -2
  35. package/src/Processor/createWorker.js +0 -38
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "4.8.4",
3
+ "version": "4.8.7",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -1,5 +1,4 @@
1
1
  import { EventDispatcher, ACESFilmicToneMapping } from 'three';
2
- import { createWorker } from '../Processor/createWorker.js';
3
2
  import { TONE_MAP_FNS, SRGB_GAMMA, applySaturation } from '../Processor/ToneMapCPU.js';
4
3
 
5
4
 
@@ -138,7 +137,7 @@ export class AIUpscaler extends EventDispatcher {
138
137
  // Create worker on first use
139
138
  if ( ! this._worker ) {
140
139
 
141
- this._worker = createWorker(
140
+ this._worker = new Worker(
142
141
  new URL( '../Processor/Workers/AIUpscalerWorker.js', import.meta.url ),
143
142
  { type: 'module' }
144
143
  );
@@ -54,17 +54,16 @@ export class PathTracerApp extends EventDispatcher {
54
54
 
55
55
  /**
56
56
  * @param {HTMLCanvasElement} canvas - Canvas element for rendering
57
- * @param {HTMLCanvasElement} [denoiserCanvas] - Optional canvas for OIDN denoiser output
58
57
  * @param {Object} [options] - Engine options
59
58
  * @param {boolean} [options.autoResize=true] - Automatically listen for window resize events
60
59
  * @param {HTMLElement} [options.statsContainer] - DOM element to append the stats panel to (defaults to document.body)
61
60
  */
62
- constructor( canvas, denoiserCanvas = null, options = {} ) {
61
+ constructor( canvas, options = {} ) {
63
62
 
64
63
  super();
65
64
 
66
65
  this.canvas = canvas;
67
- this.denoiserCanvas = denoiserCanvas;
66
+ this.denoiserCanvas = null;
68
67
  this._autoResize = options.autoResize !== false;
69
68
  this._statsContainer = options.statsContainer || null;
70
69
 
@@ -295,6 +294,7 @@ export class PathTracerApp extends EventDispatcher {
295
294
  // ── Managers ──
296
295
  this.cameraManager = new CameraManager( this._camera, this._controls, this._interactionManager );
297
296
  this.lightManager = new LightManager( this.scene, this._sceneHelpers, this.stages.pathTracer );
297
+ this._createDenoiserCanvas();
298
298
  this._setupDenoisingManager();
299
299
  this._setupOverlayManager();
300
300
 
@@ -640,6 +640,14 @@ export class PathTracerApp extends EventDispatcher {
640
640
  this.overlayManager?.dispose();
641
641
  this._sceneHelpers?.clear();
642
642
  this.denoisingManager?.dispose();
643
+
644
+ if ( this.denoiserCanvas?.parentNode ) {
645
+
646
+ this.denoiserCanvas.parentNode.removeChild( this.denoiserCanvas );
647
+ this.denoiserCanvas = null;
648
+
649
+ }
650
+
643
651
  this.pipeline?.dispose();
644
652
  this._interactionManager?.dispose();
645
653
  this._controls?.dispose();
@@ -1102,6 +1110,13 @@ export class PathTracerApp extends EventDispatcher {
1102
1110
  this._camera.aspect = width / height;
1103
1111
  this._camera.updateProjectionMatrix();
1104
1112
 
1113
+ if ( this.denoiserCanvas ) {
1114
+
1115
+ this.denoiserCanvas.style.width = `${width}px`;
1116
+ this.denoiserCanvas.style.height = `${height}px`;
1117
+
1118
+ }
1119
+
1105
1120
  // Overlay helpers always render at display resolution
1106
1121
  const dpr = window.devicePixelRatio || 1;
1107
1122
  this.overlayManager?.setSize(
@@ -1955,6 +1970,24 @@ export class PathTracerApp extends EventDispatcher {
1955
1970
 
1956
1971
  }
1957
1972
 
1973
+ _createDenoiserCanvas() {
1974
+
1975
+ if ( this.denoiserCanvas ) return; // guard against double init
1976
+
1977
+ const parent = this.canvas.parentNode;
1978
+ if ( ! parent ) return; // headless / detached canvas — skip
1979
+
1980
+ const dc = document.createElement( 'canvas' );
1981
+ dc.width = this.canvas.width;
1982
+ dc.height = this.canvas.height;
1983
+ dc.style.width = `${this.canvas.clientWidth}px`;
1984
+ dc.style.height = `${this.canvas.clientHeight}px`;
1985
+
1986
+ parent.insertBefore( dc, this.canvas );
1987
+ this.denoiserCanvas = dc;
1988
+
1989
+ }
1990
+
1958
1991
  _setupDenoisingManager() {
1959
1992
 
1960
1993
  this.denoisingManager = new DenoisingManager( {
@@ -1200,7 +1200,7 @@ export class AssetLoader extends EventDispatcher {
1200
1200
 
1201
1201
  if ( object.isRectAreaLight && ! visitedAreaLights.includes( object.uuid ) ) {
1202
1202
 
1203
- object.intensity *= 100 / 4; // Compensate for Three.js's dimming of RectAreaLights
1203
+ visitedAreaLights.push( object.uuid );
1204
1204
 
1205
1205
  }
1206
1206
 
@@ -1214,7 +1214,7 @@ export class AssetLoader extends EventDispatcher {
1214
1214
 
1215
1215
  const light = new RectAreaLight(
1216
1216
  new Color( ...userData.color ),
1217
- userData.intensity * 0.1 / 4,
1217
+ userData.intensity,
1218
1218
  userData.width,
1219
1219
  userData.height
1220
1220
  );
@@ -1,6 +1,5 @@
1
1
  import { TreeletOptimizer } from './TreeletOptimizer.js';
2
2
  import { ReinsertionOptimizer } from './ReinsertionOptimizer.js';
3
- import { createWorker } from './createWorker.js';
4
3
 
5
4
  // Inline copy of TRIANGLE_DATA_LAYOUT (mirrors Constants.js).
6
5
  // Cannot import Constants.js because BVHBuilder runs inside BVHWorker
@@ -400,7 +399,7 @@ export class BVHBuilder {
400
399
 
401
400
  try {
402
401
 
403
- const worker = createWorker(
402
+ const worker = new Worker(
404
403
  new URL( './Workers/BVHWorker.js', import.meta.url ),
405
404
  { type: 'module' }
406
405
  );
@@ -1,5 +1,4 @@
1
1
  import { DataUtils, HalfFloatType, FloatType } from 'three';
2
- import { createWorker } from './createWorker.js';
3
2
 
4
3
  /**
5
4
  * Binary search to find the closest index
@@ -185,7 +184,7 @@ export class EquirectHDRInfo {
185
184
  // Reuse worker across calls; create on first use
186
185
  if ( ! this._worker ) {
187
186
 
188
- this._worker = createWorker(
187
+ this._worker = new Worker(
189
188
  new URL( './Workers/CDFWorker.js', import.meta.url ),
190
189
  { type: 'module' }
191
190
  );
@@ -65,21 +65,35 @@ export class LightSerializer {
65
65
 
66
66
  if ( light.intensity <= 0.0 ) return; // Skip zero intensity lights
67
67
 
68
- // Convert world position to direction
69
68
  light.updateMatrixWorld();
70
69
  const position = light.getWorldPosition( new Vector3() );
71
70
 
71
+ // Compute direction toward the light source.
72
+ // Three.js convention: light shines from position toward target.
73
+ // For shadow rays we need the reverse: direction from target toward position.
74
+ let direction;
75
+ if ( light.target ) {
76
+
77
+ light.target.updateMatrixWorld();
78
+ const targetPos = light.target.getWorldPosition( new Vector3() );
79
+ direction = position.sub( targetPos ).normalize();
80
+
81
+ } else {
82
+
83
+ direction = position.normalize();
84
+
85
+ }
86
+
72
87
  // Calculate importance for sorting
73
88
  const importance = this.calculateLightImportance( light, 'directional' );
74
89
 
75
90
  // Get angle parameter from light (default to 0 for sharp shadows)
76
- // You can add this as a custom property to your DirectionalLight
77
91
  const angle = light.userData.angle || light.angle || 0.0; // In radians
78
92
 
79
93
  // Store in cache with importance
80
94
  this.directionalLightCache.push( {
81
95
  data: [
82
- position.x, position.y, position.z, // position (3)
96
+ direction.x, direction.y, direction.z, // direction toward light (3)
83
97
  light.color.r, light.color.g, light.color.b, // color (3)
84
98
  light.intensity, // intensity (1)
85
99
  angle // angular diameter in radians (1)
@@ -139,7 +153,9 @@ export class LightSerializer {
139
153
  data: [
140
154
  position.x, position.y, position.z, // position (3)
141
155
  light.color.r, light.color.g, light.color.b, // color (3)
142
- light.intensity // intensity (1)
156
+ light.intensity, // intensity (1)
157
+ light.distance || 0.0, // cutoff distance (0 = infinite) (1)
158
+ light.decay !== undefined ? light.decay : 2.0 // decay exponent (1)
143
159
  ],
144
160
  importance: importance,
145
161
  light: light
@@ -167,7 +183,10 @@ export class LightSerializer {
167
183
  direction.x, direction.y, direction.z, // direction (3)
168
184
  light.color.r, light.color.g, light.color.b, // color (3)
169
185
  light.intensity, // intensity (1)
170
- light.angle || Math.PI / 4 // cone half-angle in radians (1)
186
+ light.angle || Math.PI / 4, // cone half-angle in radians (1)
187
+ light.penumbra || 0.0, // penumbra [0,1] (1)
188
+ light.distance || 0.0, // cutoff distance (0 = infinite) (1)
189
+ light.decay !== undefined ? light.decay : 2.0 // decay exponent (1)
171
190
  ],
172
191
  importance: importance,
173
192
  light: light
@@ -244,8 +263,8 @@ export class LightSerializer {
244
263
  // Divide flat array lengths by per-light stride to get actual light counts
245
264
  const directionalCount = Math.floor( this.lightData.directional.length / 8 );
246
265
  const areaCount = Math.floor( this.lightData.rectArea.length / 13 );
247
- const pointCount = Math.floor( this.lightData.point.length / 7 );
248
- const spotCount = Math.floor( this.lightData.spot.length / 11 );
266
+ const pointCount = Math.floor( this.lightData.point.length / 9 );
267
+ const spotCount = Math.floor( this.lightData.spot.length / 14 );
249
268
 
250
269
  // Update light counts in shader defines
251
270
  material.defines.MAX_DIRECTIONAL_LIGHTS = directionalCount;
@@ -7,8 +7,6 @@
7
7
  * a cycle that Vite cannot resolve.
8
8
  */
9
9
 
10
- import { createWorker } from './createWorker.js';
11
-
12
10
  const FPT = 32; // FLOATS_PER_TRIANGLE
13
11
  const PARALLEL_THRESHOLD = 50000;
14
12
  const MAX_PARALLEL_WORKERS = 8;
@@ -36,11 +34,6 @@ export function buildBVHParallel( triangles, depth, progressCallback, config ) {
36
34
 
37
35
  return new Promise( ( resolve, reject ) => {
38
36
 
39
- const coordinatorWorker = createWorker(
40
- new URL( './Workers/BVHWorker.js', import.meta.url ),
41
- { type: 'module' }
42
- );
43
-
44
37
  try {
45
38
 
46
39
  // Allocate SharedArrayBuffers
@@ -54,6 +47,12 @@ export function buildBVHParallel( triangles, depth, progressCallback, config ) {
54
47
  const sharedMortonCodes = new SharedArrayBuffer( triangleCount * 4 );
55
48
  const sharedReorderBuffer = new SharedArrayBuffer( triangleCount * FPT * 4 );
56
49
 
50
+ // Phase 1: Coordinator worker
51
+ const coordinatorWorker = new Worker(
52
+ new URL( './Workers/BVHWorker.js', import.meta.url ),
53
+ { type: 'module' }
54
+ );
55
+
57
56
  let phase1Stats = null;
58
57
  const allWorkers = [ coordinatorWorker ];
59
58
 
@@ -300,7 +299,7 @@ function handlePhase2(
300
299
  const bucket = workerTaskBuckets[ w ];
301
300
  if ( bucket.length === 0 ) continue;
302
301
 
303
- const subtreeWorker = createWorker(
302
+ const subtreeWorker = new Worker(
304
303
  new URL( './Workers/BVHSubtreeWorker.js', import.meta.url ),
305
304
  { type: 'module' }
306
305
  );
@@ -408,7 +407,7 @@ function buildSingleWorker( triangles, depth, progressCallback, config ) {
408
407
 
409
408
  try {
410
409
 
411
- const worker = createWorker(
410
+ const worker = new Worker(
412
411
  new URL( './Workers/BVHWorker.js', import.meta.url ),
413
412
  { type: 'module' }
414
413
  );
@@ -11,7 +11,6 @@ import { EmissiveTriangleBuilder } from './EmissiveTriangleBuilder.js';
11
11
  import { updateLoading } from '../Processor/utils.js';
12
12
  import { BuildTimer } from './BuildTimer.js';
13
13
  import { TRIANGLE_DATA_LAYOUT } from '../EngineDefaults.js';
14
- import { createWorker } from './createWorker.js';
15
14
 
16
15
  /**
17
16
  * SceneProcessor - Processes scene geometry into GPU-ready data:
@@ -666,7 +665,7 @@ export class SceneProcessor {
666
665
  // Spin up the pool
667
666
  for ( let i = 0; i < poolSize; i ++ ) {
668
667
 
669
- const worker = createWorker(
668
+ const worker = new Worker(
670
669
  new URL( './Workers/BVHWorker.js', import.meta.url ),
671
670
  { type: 'module' }
672
671
  );
@@ -1137,7 +1136,7 @@ export class SceneProcessor {
1137
1136
  // Lazy-create worker
1138
1137
  if ( ! this._refitWorker ) {
1139
1138
 
1140
- this._refitWorker = createWorker(
1139
+ this._refitWorker = new Worker(
1141
1140
  new URL( './Workers/BVHRefitWorker.js', import.meta.url ),
1142
1141
  { type: 'module' }
1143
1142
  );
@@ -1526,7 +1525,7 @@ export class SceneProcessor {
1526
1525
  ( entry.triOffset + entry.triCount ) * FPT
1527
1526
  );
1528
1527
 
1529
- const worker = createWorker(
1528
+ const worker = new Worker(
1530
1529
  new URL( './Workers/BVHWorker.js', import.meta.url ),
1531
1530
  { type: 'module' }
1532
1531
  );
@@ -1,6 +1,5 @@
1
1
  import { DataArrayTexture, RGBAFormat, LinearFilter, UnsignedByteType, SRGBColorSpace } from "three";
2
2
  import { TEXTURE_CONSTANTS, MEMORY_CONSTANTS, DEFAULT_TEXTURE_MATRIX } from '../EngineDefaults.js';
3
- import { createWorker } from './createWorker.js';
4
3
 
5
4
  // Canvas pooling for efficient reuse of canvas elements
6
5
  class CanvasPool {
@@ -596,7 +595,7 @@ export class TextureCreator {
596
595
 
597
596
  try {
598
597
 
599
- const worker = createWorker(
598
+ const worker = new Worker(
600
599
  new URL( './Workers/TexturesWorker.js', import.meta.url ),
601
600
  { type: 'module' }
602
601
  );
@@ -14,11 +14,24 @@
14
14
  * { type: 'error', message, id? }
15
15
  */
16
16
 
17
- import * as ort from 'onnxruntime-web/webgpu';
17
+ // Loaded lazily via CDN to avoid bundling the 69 MB onnxruntime-web package
18
+ const ORT_CDN_URL = 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.24.3/dist/ort.webgpu.bundle.min.mjs';
18
19
 
19
- // WASM paths for CDN delivery — WebGPU EP still uses WASM for lightweight shape ops
20
- ort.env.wasm.wasmPaths = 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.24.3/dist/';
21
- ort.env.logLevel = 'error';
20
+ let ort = null;
21
+
22
+ async function getOrt() {
23
+
24
+ if ( ort ) return ort;
25
+
26
+ ort = await import( /* @vite-ignore */ ORT_CDN_URL );
27
+
28
+ // WASM paths for CDN delivery — WebGPU EP still uses WASM for lightweight shape ops
29
+ ort.env.wasm.wasmPaths = 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.24.3/dist/';
30
+ ort.env.logLevel = 'error';
31
+
32
+ return ort;
33
+
34
+ }
22
35
 
23
36
  const IDB_NAME = 'ai-upscaler-models';
24
37
  const IDB_STORE = 'models';
@@ -127,9 +140,9 @@ async function loadModel( url, sessionOptions ) {
127
140
 
128
141
  }
129
142
 
130
- const modelBuffer = await fetchModel( url );
143
+ const [ modelBuffer, ortLib ] = await Promise.all( [ fetchModel( url ), getOrt() ] );
131
144
 
132
- session = await ort.InferenceSession.create( modelBuffer, sessionOptions );
145
+ session = await ortLib.InferenceSession.create( modelBuffer, sessionOptions );
133
146
  currentModelUrl = url;
134
147
 
135
148
  // Detect GPU and recommend tile size based on device type
@@ -170,9 +183,10 @@ async function loadModel( url, sessionOptions ) {
170
183
 
171
184
  async function inferTile( tileData, width, height, id ) {
172
185
 
186
+ const ortLib = await getOrt();
173
187
  const inputName = session.inputNames[ 0 ];
174
188
  const outputName = session.outputNames[ 0 ];
175
- const inputTensor = new ort.Tensor( 'float32', tileData, [ 1, 3, height, width ] );
189
+ const inputTensor = new ortLib.Tensor( 'float32', tileData, [ 1, 3, height, width ] );
176
190
 
177
191
  const results = await session.run( { [ inputName ]: inputTensor } );
178
192
  const outputData = results[ outputName ].data;
package/src/README.md CHANGED
@@ -20,13 +20,12 @@ engine.animate();
20
20
  ## Constructor
21
21
 
22
22
  ```js
23
- new PathTracerApp(canvas, denoiserCanvas?, options?)
23
+ new PathTracerApp(canvas, options?)
24
24
  ```
25
25
 
26
26
  | Param | Type | Description |
27
27
  |-------|------|-------------|
28
28
  | `canvas` | `HTMLCanvasElement` | Render target |
29
- | `denoiserCanvas` | `HTMLCanvasElement \| null` | Optional canvas for OIDN output |
30
29
  | `options.autoResize` | `boolean` | Auto-listen for window resize (default `true`) |
31
30
 
32
31
  ## API Reference
@@ -26,8 +26,8 @@ import { LightSerializer } from '../Processor/LightSerializer';
26
26
  // Constants
27
27
  import { ENGINE_DEFAULTS as DEFAULT_STATE } from '../EngineDefaults.js';
28
28
 
29
- // Blue noise
30
- import blueNoiseImage from '../../assets/noise/simple_bluenoise.png';
29
+ // Blue noise (loaded at runtime from CDN — not inlined to keep bundle small)
30
+ const blueNoiseImage = 'https://assets.rayzee.atulmourya.com/noise/simple_bluenoise.png';
31
31
 
32
32
  /**
33
33
  * Data layout constants
@@ -361,6 +361,7 @@ export class PathTracer extends RenderStage {
361
361
  setupBlueNoise() {
362
362
 
363
363
  const loader = new TextureLoader();
364
+ loader.setCrossOrigin( 'anonymous' );
364
365
  loader.load( blueNoiseImage, ( texture ) => {
365
366
 
366
367
  texture.minFilter = NearestFilter;
@@ -572,11 +573,11 @@ export class PathTracer extends RenderStage {
572
573
 
573
574
  }
574
575
 
575
- // Point lights (7 floats per light)
576
+ // Point lights (9 floats per light)
576
577
  if ( this.pointLightsData && this.pointLightsData.length > 0 ) {
577
578
 
578
579
  this.pointLightsBufferNode.array = Array.from( this.pointLightsData );
579
- this.numPointLights.value = Math.floor( this.pointLightsData.length / 7 );
580
+ this.numPointLights.value = Math.floor( this.pointLightsData.length / 9 );
580
581
 
581
582
  } else {
582
583
 
@@ -584,11 +585,11 @@ export class PathTracer extends RenderStage {
584
585
 
585
586
  }
586
587
 
587
- // Spot lights (11 floats per light)
588
+ // Spot lights (14 floats per light)
588
589
  if ( this.spotLightsData && this.spotLightsData.length > 0 ) {
589
590
 
590
591
  this.spotLightsBufferNode.array = Array.from( this.spotLightsData );
591
- this.numSpotLights.value = Math.floor( this.spotLightsData.length / 11 );
592
+ this.numSpotLights.value = Math.floor( this.spotLightsData.length / 14 );
592
593
 
593
594
  } else {
594
595
 
@@ -40,6 +40,8 @@ export const PointLight = struct( {
40
40
  position: 'vec3',
41
41
  color: 'vec3',
42
42
  intensity: 'float',
43
+ distance: 'float', // cutoff distance (0 = infinite)
44
+ decay: 'float', // decay exponent (2 = physically correct)
43
45
  } );
44
46
 
45
47
  export const SpotLight = struct( {
@@ -48,6 +50,9 @@ export const SpotLight = struct( {
48
50
  color: 'vec3',
49
51
  intensity: 'float',
50
52
  angle: 'float', // cone half-angle in radians
53
+ penumbra: 'float', // penumbra factor [0,1]
54
+ distance: 'float', // cutoff distance (0 = infinite)
55
+ decay: 'float', // decay exponent (2 = physically correct)
51
56
  } );
52
57
 
53
58
  export const LightSample = struct( {
@@ -135,7 +140,7 @@ export const getAreaLight = Fn( ( [ areaLightsBuffer, index ] ) => {
135
140
 
136
141
  export const getPointLight = Fn( ( [ pointLightsBuffer, index ] ) => {
137
142
 
138
- const baseIndex = index.mul( 7 );
143
+ const baseIndex = index.mul( 9 );
139
144
  return PointLight( {
140
145
  position: vec3(
141
146
  pointLightsBuffer.element( baseIndex ),
@@ -148,13 +153,15 @@ export const getPointLight = Fn( ( [ pointLightsBuffer, index ] ) => {
148
153
  pointLightsBuffer.element( baseIndex.add( 5 ) ),
149
154
  ),
150
155
  intensity: pointLightsBuffer.element( baseIndex.add( 6 ) ),
156
+ distance: pointLightsBuffer.element( baseIndex.add( 7 ) ),
157
+ decay: pointLightsBuffer.element( baseIndex.add( 8 ) ),
151
158
  } );
152
159
 
153
160
  } );
154
161
 
155
162
  export const getSpotLight = Fn( ( [ spotLightsBuffer, index ] ) => {
156
163
 
157
- const baseIndex = index.mul( 11 );
164
+ const baseIndex = index.mul( 14 );
158
165
  return SpotLight( {
159
166
  position: vec3(
160
167
  spotLightsBuffer.element( baseIndex ),
@@ -173,6 +180,9 @@ export const getSpotLight = Fn( ( [ spotLightsBuffer, index ] ) => {
173
180
  ),
174
181
  intensity: spotLightsBuffer.element( baseIndex.add( 9 ) ),
175
182
  angle: spotLightsBuffer.element( baseIndex.add( 10 ) ),
183
+ penumbra: spotLightsBuffer.element( baseIndex.add( 11 ) ),
184
+ distance: spotLightsBuffer.element( baseIndex.add( 12 ) ),
185
+ decay: spotLightsBuffer.element( baseIndex.add( 13 ) ),
176
186
  } );
177
187
 
178
188
  } );
@@ -145,7 +145,7 @@ export const sampleRectAreaLight = Fn( ( [ light, rayOrigin, ruv, lightSelection
145
145
  const cosAngle = dot( direction.negate(), lightNormal ).toVar();
146
146
 
147
147
  ls_lightType.assign( int( LIGHT_TYPE_AREA ) );
148
- ls_emission.assign( light.color.mul( light.intensity ) );
148
+ ls_emission.assign( light.color.mul( light.intensity ).div( PI ) );
149
149
  ls_distance.assign( dist );
150
150
  ls_direction.assign( direction );
151
151
  // Guard division: ensure denominator is never zero
@@ -205,7 +205,7 @@ export const sampleCircAreaLight = Fn( ( [ light, rayOrigin, ruv, lightSelection
205
205
  const cosAngle = dot( direction.negate(), lightNormal ).toVar();
206
206
 
207
207
  ls_lightType.assign( int( LIGHT_TYPE_AREA ) );
208
- ls_emission.assign( light.color.mul( light.intensity ) );
208
+ ls_emission.assign( light.color.mul( light.intensity ).div( PI ) );
209
209
  ls_distance.assign( dist );
210
210
  ls_direction.assign( direction );
211
211
  // Guard division
@@ -258,9 +258,11 @@ export const sampleSpotLightWithRadius = Fn( ( [ light, rayOrigin, ruv, lightSel
258
258
 
259
259
  If( ls_valid, () => {
260
260
 
261
- const penumbraCosAngle = cos( light.angle.mul( 0.9 ) ).toVar(); // 10% penumbra
261
+ // Penumbra: inner cone angle = outerAngle * (1 - penumbra)
262
+ // Clamp penumbraCosAngle > coneCosAngle to avoid smoothstep UB when penumbra = 0
263
+ const penumbraCosAngle = cos( light.angle.mul( float( 1.0 ).sub( light.penumbra ) ) ).max( coneCosAngle.add( 1e-5 ) ).toVar();
262
264
  const coneAttenuation = getSpotAttenuation( { coneCosine: coneCosAngle, penumbraCosine: penumbraCosAngle, angleCosine: spotCosAngle } );
263
- const distanceAttenuation = getDistanceAttenuation( { lightDistance: lightDist, cutoffDistance: float( 0.0 ), decayExponent: float( 2.0 ) } );
265
+ const distanceAttenuation = getDistanceAttenuation( { lightDistance: lightDist, cutoffDistance: light.distance, decayExponent: light.decay } );
264
266
 
265
267
  ls_emission.assign( light.color.mul( light.intensity ).mul( distanceAttenuation ).mul( coneAttenuation ) );
266
268
 
@@ -297,8 +299,8 @@ export const samplePointLightWithAttenuation = Fn( ( [ light, rayOrigin, lightSe
297
299
 
298
300
  const lightDir = toLight.div( lightDist ).toVar();
299
301
 
300
- // Calculate distance attenuation
301
- const distanceAttenuation = getDistanceAttenuation( { lightDistance: lightDist, cutoffDistance: float( 0.0 ), decayExponent: float( 2.0 ) } );
302
+ // Calculate distance attenuation using the light's actual distance and decay properties
303
+ const distanceAttenuation = getDistanceAttenuation( { lightDistance: lightDist, cutoffDistance: light.distance, decayExponent: light.decay } );
302
304
 
303
305
  ls_lightType.assign( int( LIGHT_TYPE_POINT ) );
304
306
  ls_direction.assign( lightDir );
@@ -166,7 +166,7 @@ export class LightManager extends EventDispatcher {
166
166
 
167
167
  }
168
168
 
169
- if ( light.isSpotLight && light.target ) {
169
+ if ( ( light.isSpotLight || light.isDirectionalLight ) && light.target ) {
170
170
 
171
171
  const clonedTarget = new Object3D();
172
172
  light.target.updateWorldMatrix( true, false );
@@ -203,8 +203,8 @@ export class UniformManager {
203
203
  this._lightBuffers = {
204
204
  directional: uniformArray( new Float32Array( 8 * 16 ), 'float' ),
205
205
  area: uniformArray( new Float32Array( 13 * 16 ), 'float' ),
206
- point: uniformArray( new Float32Array( 7 * 16 ), 'float' ),
207
- spot: uniformArray( new Float32Array( 11 * 16 ), 'float' ),
206
+ point: uniformArray( new Float32Array( 9 * 16 ), 'float' ),
207
+ spot: uniformArray( new Float32Array( 14 * 16 ), 'float' ),
208
208
  };
209
209
 
210
210
  // Camera matrices
@@ -1,38 +0,0 @@
1
- /**
2
- * Cross-origin Worker utility.
3
- *
4
- * Browsers block `new Worker(url)` when the script is cross-origin (e.g. CDN).
5
- * The workaround: create a same-origin blob that `import()`s the real script.
6
- * This preserves the original URL as the base for relative imports inside the worker.
7
- */
8
-
9
- function crossOriginWorker( url, options ) {
10
-
11
- const blob = new Blob(
12
- [ `import ${JSON.stringify( url.toString() )};` ],
13
- { type: 'application/javascript' }
14
- );
15
- return new Worker( URL.createObjectURL( blob ), { ...options, type: 'module' } );
16
-
17
- }
18
-
19
- /**
20
- * Creates a Worker, falling back to a blob-based proxy for cross-origin scripts.
21
- * @param {URL} url - Worker script URL
22
- * @param {WorkerOptions} [options] - Worker options (e.g. { type: 'module' })
23
- * @returns {Worker}
24
- */
25
- export function createWorker( url, options = {} ) {
26
-
27
- try {
28
-
29
- return new Worker( url, options );
30
-
31
- } catch ( e ) {
32
-
33
- if ( e.name !== 'SecurityError' ) throw e;
34
- return crossOriginWorker( url, options );
35
-
36
- }
37
-
38
- }