rayzee 7.22.9 → 7.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "7.22.9",
3
+ "version": "7.23.1",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -524,6 +524,11 @@ export const MATERIAL_DATA_LAYOUT = {
524
524
 
525
525
  };
526
526
 
527
+ // glTF/three.js spell "no volume absorption" as attenuationDistance = Infinity, while the GPU
528
+ // contract is `> 0 = on` (calculateBeerLawAbsorption). Every writer into ATTENUATION_DISTANCE
529
+ // collapses both spellings to 0 so no shader divides by Inf.
530
+ export const normalizeAttenuationDistance = d => ( Number.isFinite( d ) && d > 0 ? d : 0 );
531
+
527
532
  // BVH node leaf markers
528
533
  export const BVH_LEAF_MARKERS = {
529
534
  TRIANGLE_LEAF: - 1, // Leaf containing triangle references
@@ -63,6 +63,21 @@ const _appsByCanvas = new WeakMap();
63
63
 
64
64
  const log = createLogger( 'engine' );
65
65
 
66
+ /**
67
+ * Attaches the device-lost handler at module scope so the reaction closure captures only
68
+ * `holder` — an arrow written inside a method would share that method's context and pin
69
+ * the app through `this`. See _initRenderer().
70
+ */
71
+ function attachDeviceLostHandler( device, holder ) {
72
+
73
+ device.lost.then( ( info ) => {
74
+
75
+ if ( holder.app ) holder.app._handleDeviceLost( info );
76
+
77
+ } );
78
+
79
+ }
80
+
66
81
  export class PathTracerApp extends EventDispatcher {
67
82
 
68
83
  /**
@@ -169,6 +184,8 @@ export class PathTracerApp extends EventDispatcher {
169
184
  this._trackedListeners = [];
170
185
  this._disposed = false;
171
186
  this._deviceLost = false;
187
+ this._gpuDevice = null;
188
+ this._deviceLostHolder = null;
172
189
 
173
190
  // Deterministic-render mode — see setDeterministicMode()
174
191
  this._deterministic = false;
@@ -539,6 +556,24 @@ export class PathTracerApp extends EventDispatcher {
539
556
 
540
557
  }
541
558
 
559
+ // Value of a WeakMap keyed by a canvas that outlives us: without this the whole
560
+ // disposed graph stays reachable until another app claims the same canvas.
561
+ if ( this.canvas && _appsByCanvas.get( this.canvas ) === this ) _appsByCanvas.delete( this.canvas );
562
+
563
+ if ( this._gpuDevice ) {
564
+
565
+ this._gpuDevice.onuncapturederror = null;
566
+ this._gpuDevice = null;
567
+
568
+ }
569
+
570
+ if ( this._deviceLostHolder ) {
571
+
572
+ this._deviceLostHolder.app = null;
573
+ this._deviceLostHolder = null;
574
+
575
+ }
576
+
542
577
  disposeRenderer( this.renderer );
543
578
  this.renderer = null;
544
579
 
@@ -2104,6 +2139,18 @@ export class PathTracerApp extends EventDispatcher {
2104
2139
 
2105
2140
  }
2106
2141
 
2142
+ /**
2143
+ * Read back the scalar material property the shader is actually using.
2144
+ * @param {number} materialIndex
2145
+ * @param {string} property
2146
+ * @returns {number|undefined}
2147
+ */
2148
+ getMaterialProperty( materialIndex, property ) {
2149
+
2150
+ return this.stages.pathTracer?.materialData.getMaterialProperty( materialIndex, property );
2151
+
2152
+ }
2153
+
2107
2154
  setMaterialProperty( materialIndex, property, value ) {
2108
2155
 
2109
2156
  this.stages.pathTracer?.materialData.updateMaterialProperty( materialIndex, property, value );
@@ -2307,15 +2354,21 @@ export class PathTracerApp extends EventDispatcher {
2307
2354
  // Detect GPU device loss (dGPU/iGPU switch, driver reset, TDR watchdog on heavy
2308
2355
  // compute). Without this the rAF loop keeps calling render() on a dead device,
2309
2356
  // spewing errors forever. reason 'destroyed' during dispose() is intentional teardown.
2357
+ //
2358
+ // Neither handler may outlive dispose() still holding this app. The device itself does
2359
+ // outlive it: three's RenderObjects.dispose() drops its chain maps without disposing
2360
+ // the render objects, and its Textures manager leaves a listener on every module-level
2361
+ // texture singleton, so backend and device stay reachable for the page's lifetime. A
2362
+ // handler capturing `this` therefore pinned the whole app graph — ~120 MiB of triangle,
2363
+ // BVH and CPU-side texture data per create/dispose cycle. `lost` is a pending promise
2364
+ // whose reaction cannot be unregistered, so it reads the app out of a holder that
2365
+ // dispose() empties; `onuncapturederror` is cleared there directly.
2310
2366
  const gpuDevice = this.renderer.backend?.device;
2311
2367
  if ( gpuDevice?.lost ) {
2312
2368
 
2313
- gpuDevice.lost.then( ( info ) => {
2314
-
2315
- if ( this._disposed ) return;
2316
- this._handleDeviceLost( info );
2317
-
2318
- } );
2369
+ this._gpuDevice = gpuDevice;
2370
+ this._deviceLostHolder = { app: this };
2371
+ attachDeviceLostHandler( gpuDevice, this._deviceLostHolder );
2319
2372
  gpuDevice.onuncapturederror = ( event ) => log.error( 'WebGPU uncaptured error:', event.error );
2320
2373
 
2321
2374
  }
@@ -373,20 +373,12 @@ export class GeometryExtractor {
373
373
  const materialType = this.getMaterialType( material );
374
374
  const legacyMapping = this.mapLegacyMaterialToPhysical( material, materialType );
375
375
 
376
- // Determine if material should be treated as dielectric or metallic
376
+ // A transmissive surface is a dielectric interface whose IOR drives refraction, so it wins
377
+ // over the metal Fresnel hack: glTF defaults metallicFactor to 1, which otherwise stamps
378
+ // 2.5 onto water/glass authored as MeshStandardMaterial.
379
+ const isTransmissive = ( material.transmission ?? 0.0 ) > 0.0;
377
380
  const isMetallic = ( material.metalness ?? legacyMapping.metalness ?? 0.0 ) > 0.1;
378
-
379
- // Set appropriate IOR based on material type
380
- let defaultIOR = defaults.ior;
381
- if ( isMetallic ) {
382
-
383
- defaultIOR = 2.5; // Typical metallic IOR
384
-
385
- } else if ( material.transmission > 0.0 ) {
386
-
387
- defaultIOR = 1.5; // Glass-like for transmissive materials
388
-
389
- }
381
+ const defaultIOR = ( isMetallic && ! isTransmissive ) ? 2.5 : defaults.ior;
390
382
 
391
383
  // Handle color conversion for different material types
392
384
  let baseColor = material.color || new Color( 0xffffff );
@@ -1,5 +1,5 @@
1
1
  import { DataArrayTexture, RGBAFormat, LinearFilter, UnsignedByteType, SRGBColorSpace, RepeatWrapping } from "three";
2
- import { TEXTURE_CONSTANTS, MEMORY_CONSTANTS, DEFAULT_TEXTURE_MATRIX, MATERIAL_DATA_LAYOUT } from '../EngineDefaults.js';
2
+ import { TEXTURE_CONSTANTS, MEMORY_CONSTANTS, DEFAULT_TEXTURE_MATRIX, MATERIAL_DATA_LAYOUT, normalizeAttenuationDistance } from '../EngineDefaults.js';
3
3
  import TexturesWorker from './Workers/TexturesWorker.js?worker&inline';
4
4
 
5
5
  // Canvas pooling for efficient reuse of canvas elements
@@ -948,7 +948,7 @@ export class TextureCreator {
948
948
  // Slot 0: shadow core (ior, transmission, thickness, emissiveIntensity)
949
949
  mat.ior, mat.transmission, mat.thickness, mat.emissiveIntensity,
950
950
  // Slot 1: shadow (attenuationColor, attenuationDistance)
951
- mat.attenuationColor.r, mat.attenuationColor.g, mat.attenuationColor.b, mat.attenuationDistance,
951
+ mat.attenuationColor.r, mat.attenuationColor.g, mat.attenuationColor.b, normalizeAttenuationDistance( mat.attenuationDistance ),
952
952
  // Slot 2: shadow + culling (opacity, side, transparent, alphaTest)
953
953
  mat.opacity, mat.side, mat.transparent, mat.alphaTest,
954
954
  // Slot 3: shadow (alphaMode, depthWrite, normalScale)
@@ -1,6 +1,10 @@
1
1
  import { SphereGeometry, Mesh, MeshPhysicalMaterial, Group, Object3D, Color, Vector3, ShaderMaterial, Vector2, Matrix4, GLSL3 } from 'three';
2
+ import { QuadMesh } from 'three/webgpu';
2
3
  import { EngineEvents } from '../EngineEvents.js';
3
4
 
5
+ /** Every QuadMesh in the realm shares this one geometry (module-level in three). */
6
+ const _sharedQuadGeometry = /*@__PURE__*/ new QuadMesh( null ).geometry;
7
+
4
8
  let _statusCallback = null;
5
9
 
6
10
  /**
@@ -130,6 +134,18 @@ export function disposeRenderer( renderer ) {
130
134
  renderer.dispose();
131
135
  renderer._canvasTarget = null;
132
136
 
137
+ // Three.js 0.185: `RenderObjects.dispose()` drops its chain maps without disposing the
138
+ // render objects, so each one leaves behind the 'dispose' listener it registered on its
139
+ // geometry. Full-screen passes render a QuadMesh, and every QuadMesh in the realm shares
140
+ // one module-level geometry — so that listener list grows by one entry per renderer and
141
+ // keeps RenderObject → RenderObjects → Bindings → Backend → GPUDevice reachable for the
142
+ // page's lifetime. The chain maps are WeakMaps, so this list is the only handle on them.
143
+ //
144
+ // Clearing it also drops entries belonging to live renderers, which is safe: the
145
+ // singleton geometry is never disposed, so none of these listeners can ever fire.
146
+ const quadDisposeListeners = _sharedQuadGeometry._listeners?.dispose;
147
+ if ( quadDisposeListeners ) quadDisposeListeners.length = 0;
148
+
133
149
  }
134
150
 
135
151
  export function disposeObjectFromMemory( object, exeptions = [] ) {
@@ -141,57 +141,16 @@ export const sampleWavelengthForDispersion = Fn( ( [ baseIOR, dispersionStrength
141
141
  // ABSORPTION
142
142
  // ================================================================================
143
143
 
144
- // Apply Beer's law absorption
144
+ // Beer-Lambert transmittance over pathLength. attenuationDistance <= 0 (or non-finite,
145
+ // normalized to 0 by MaterialDataManager) means "no absorption".
145
146
  export const calculateBeerLawAbsorption = /*@__PURE__*/ wgslFn( `
146
- fn calculateBeerLawAbsorption( attenuationColor: vec3f, attenuationDistance: f32, thickness: f32 ) -> vec3f {
147
+ fn calculateBeerLawAbsorption( attenuationColor: vec3f, attenuationDistance: f32, pathLength: f32 ) -> vec3f {
147
148
  if ( attenuationDistance <= 0.0f ) { return vec3f( 1.0f ); }
148
- // Convert RGB attenuation color to absorption coefficients
149
149
  let absorption = -log( max( attenuationColor, vec3f( 0.001f ) ) ) / attenuationDistance;
150
- // Apply Beer's law
151
- return exp( -absorption * thickness );
150
+ return exp( -absorption * pathLength );
152
151
  }
153
152
  ` );
154
153
 
155
- // ================================================================================
156
- // SHADOW TRANSMITTANCE
157
- // ================================================================================
158
-
159
- export const calculateShadowTransmittance = Fn( ( [ rayDir, normal, material, entering ] ) => {
160
-
161
- const n1 = select( entering, float( 1.0 ), material.ior ).toVar();
162
- const n2 = select( entering, material.ior, float( 1.0 ) ).toVar();
163
-
164
- const cosThetaI = abs( dot( normal, rayDir ) );
165
- const sinThetaT2 = n1.mul( n1 ).div( n2.mul( n2 ) ).mul( float( 1.0 ).sub( cosThetaI.mul( cosThetaI ) ) );
166
-
167
- // Handle total internal reflection
168
- const result = float( 0.0 ).toVar();
169
-
170
- If( sinThetaT2.lessThanEqual( 1.0 ), () => {
171
-
172
- // Calculate Fresnel reflectance
173
- const F0 = iorToFresnel0( n2, n1 );
174
- const Fr = fresnelSchlickFloat( cosThetaI, F0 );
175
-
176
- // Base transmission: what gets through after Fresnel reflection
177
- const baseTransmission = float( 1.0 ).sub( Fr ).mul( material.transmission ).toVar();
178
-
179
- // Apply Beer's law absorption for exiting rays
180
- If( entering.not().and( material.attenuationDistance.greaterThan( 0.0 ) ), () => {
181
-
182
- const absorption = calculateBeerLawAbsorption( { attenuationColor: material.attenuationColor, attenuationDistance: material.attenuationDistance, thickness: material.thickness } );
183
- baseTransmission.assign( baseTransmission.mul( absorption.x.add( absorption.y ).add( absorption.z ).div( 3.0 ) ) );
184
-
185
- } );
186
-
187
- result.assign( clamp( baseTransmission, 0.0, 1.0 ) );
188
-
189
- } );
190
-
191
- return result;
192
-
193
- } );
194
-
195
154
  // ================================================================================
196
155
  // MICROFACET TRANSMISSION
197
156
  // ================================================================================
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { StorageInstancedBufferAttribute } from 'three/webgpu';
11
11
  import { storage } from 'three/tsl';
12
- import { MATERIAL_DATA_LAYOUT as M, TRIANGLE_DATA_LAYOUT as T } from '../EngineDefaults.js';
12
+ import { MATERIAL_DATA_LAYOUT as M, TRIANGLE_DATA_LAYOUT as T, normalizeAttenuationDistance } from '../EngineDefaults.js';
13
13
  import { createLogger, fmt } from '../utils/Logger.js';
14
14
 
15
15
  const log = createLogger( 'material' );
@@ -23,6 +23,32 @@ const TRI_BLOCKER_OFFSET = T.NORMAL_A_OFFSET + 3; // nA.w in shader (opaque-bloc
23
23
  // Material properties that affect the shadow-ray opaque-blocker flag.
24
24
  const BLOCKER_PROPS = new Set( [ 'transmission', 'transparent', 'opacity', 'alphaMode' ] );
25
25
 
26
+ // Scalar slots readable via getMaterialProperty().
27
+ const SCALAR_PROPERTY_OFFSETS = {
28
+ ior: M.IOR,
29
+ transmission: M.TRANSMISSION,
30
+ thickness: M.THICKNESS,
31
+ emissiveIntensity: M.EMISSIVE_INTENSITY,
32
+ attenuationDistance: M.ATTENUATION_DISTANCE,
33
+ opacity: M.OPACITY,
34
+ alphaTest: M.ALPHA_TEST,
35
+ metalness: M.METALNESS,
36
+ roughness: M.ROUGHNESS,
37
+ clearcoat: M.CLEARCOAT,
38
+ clearcoatRoughness: M.CLEARCOAT_ROUGHNESS,
39
+ dispersion: M.DISPERSION,
40
+ sheen: M.SHEEN,
41
+ sheenRoughness: M.SHEEN_ROUGHNESS,
42
+ specularIntensity: M.SPECULAR_INTENSITY,
43
+ iridescence: M.IRIDESCENCE,
44
+ iridescenceIOR: M.IRIDESCENCE_IOR,
45
+ subsurface: M.SUBSURFACE,
46
+ subsurfaceRadiusScale: M.SUBSURFACE_RADIUS_SCALE,
47
+ subsurfaceAnisotropy: M.SUBSURFACE_ANISOTROPY,
48
+ anisotropy: M.ANISOTROPY,
49
+ anisotropyRotation: M.ANISOTROPY_ROTATION, // radians, unlike the degrees hosts usually show
50
+ };
51
+
26
52
  export class MaterialDataManager {
27
53
 
28
54
  /**
@@ -179,6 +205,28 @@ export class MaterialDataManager {
179
205
 
180
206
  // ===== MATERIAL PROPERTY UPDATES =====
181
207
 
208
+ /**
209
+ * Read back a scalar material property from the storage buffer — the value the shader
210
+ * actually uses. Hosts need this because the engine resolves defaults the three.js
211
+ * material never carries (e.g. IOR on a MeshStandardMaterial), so a UI that falls back
212
+ * to its own default displays a number the renderer is not using.
213
+ * @param {number} materialIndex
214
+ * @param {string} property
215
+ * @returns {number|undefined} undefined if unavailable or not a scalar slot
216
+ */
217
+ getMaterialProperty( materialIndex, property ) {
218
+
219
+ const data = this.materialStorageAttr?.array;
220
+ if ( ! data ) return undefined;
221
+
222
+ const offset = SCALAR_PROPERTY_OFFSETS[ property ];
223
+ if ( offset === undefined ) return undefined;
224
+
225
+ const index = materialIndex * M.FLOATS_PER_MATERIAL + offset;
226
+ return index < data.length ? data[ index ] : undefined;
227
+
228
+ }
229
+
182
230
  /**
183
231
  * Update a single material property in the storage buffer.
184
232
  * @param {number} materialIndex
@@ -253,7 +301,7 @@ export class MaterialDataManager {
253
301
  }
254
302
 
255
303
  break;
256
- case 'attenuationDistance': data[ stride + M.ATTENUATION_DISTANCE ] = value; break;
304
+ case 'attenuationDistance': data[ stride + M.ATTENUATION_DISTANCE ] = normalizeAttenuationDistance( value ); break;
257
305
  case 'dispersion': data[ stride + M.DISPERSION ] = value; break;
258
306
  case 'sheen': data[ stride + M.SHEEN ] = value; break;
259
307
  case 'sheenRoughness': data[ stride + M.SHEEN_ROUGHNESS ] = value; break;
@@ -446,7 +494,7 @@ export class MaterialDataManager {
446
494
 
447
495
  }
448
496
 
449
- data[ stride + M.ATTENUATION_DISTANCE ] = materialData.attenuationDistance ?? Infinity;
497
+ data[ stride + M.ATTENUATION_DISTANCE ] = normalizeAttenuationDistance( materialData.attenuationDistance );
450
498
  data[ stride + M.DISPERSION ] = materialData.dispersion ?? 0;
451
499
  data[ stride + M.VISIBLE ] = 1; // Reserved slot (per-mesh visibility handled at BLAS-pointer level)
452
500
  data[ stride + M.SHEEN ] = materialData.sheen ?? 0;