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/dist/rayzee.es.js +1585 -1561
- package/dist/rayzee.umd.js +59 -61
- package/package.json +1 -1
- package/src/EngineDefaults.js +5 -0
- package/src/PathTracerApp.js +59 -6
- package/src/Processor/GeometryExtractor.js +5 -13
- package/src/Processor/TextureCreator.js +2 -2
- package/src/Processor/utils.js +16 -0
- package/src/TSL/MaterialTransmission.js +4 -45
- package/src/managers/MaterialDataManager.js +51 -3
package/package.json
CHANGED
package/src/EngineDefaults.js
CHANGED
|
@@ -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
|
package/src/PathTracerApp.js
CHANGED
|
@@ -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
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
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
|
-
//
|
|
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)
|
package/src/Processor/utils.js
CHANGED
|
@@ -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
|
-
//
|
|
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,
|
|
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
|
-
|
|
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
|
|
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;
|