rayzee 5.0.2 → 5.1.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": "5.0.2",
3
+ "version": "5.1.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",
@@ -34,7 +34,7 @@
34
34
  "prepublishOnly": "npm run build"
35
35
  },
36
36
  "peerDependencies": {
37
- "three": ">=0.170.0"
37
+ "three": ">=0.183.2"
38
38
  },
39
39
  "dependencies": {
40
40
  "stats-gl": "^4.0.2"
@@ -533,6 +533,11 @@ export class PathTracerApp extends EventDispatcher {
533
533
 
534
534
  if ( ! this._sdf.uploadToPathTracer( this.stages.pathTracer, this.lightManager, this.meshScene, environmentTexture ) ) return false;
535
535
 
536
+ // Build per-mesh visibility buffer (must happen before setupMaterial so the
537
+ // shader graph captures the storage node during compilation)
538
+ this.stages.pathTracer._meshRefs = this.stages.pathTracer._collectMeshRefs( this.meshScene );
539
+ this.stages.pathTracer.setMeshVisibilityData( this.stages.pathTracer._meshRefs );
540
+
536
541
  timer.end( 'GPU data transfer' );
537
542
 
538
543
  // Compile shaders
@@ -877,7 +882,7 @@ export class PathTracerApp extends EventDispatcher {
877
882
 
878
883
  this.stages.pathTracer?.materialData.updateMaterialProperty( materialIndex, property, value );
879
884
 
880
- const emissiveAffectingProps = [ 'emissive', 'emissiveIntensity', 'visible' ];
885
+ const emissiveAffectingProps = [ 'emissive', 'emissiveIntensity' ];
881
886
  if ( emissiveAffectingProps.includes( property )
882
887
  && this.stages.pathTracer?.enableEmissiveTriangleSampling?.value ) {
883
888
 
@@ -896,6 +901,30 @@ export class PathTracerApp extends EventDispatcher {
896
901
 
897
902
  }
898
903
 
904
+ /**
905
+ * Update per-mesh visibility without rebuilding the scene.
906
+ * Walks the parent chain to resolve world-space visibility.
907
+ * @param {number} meshIndex
908
+ * @param {boolean} visible
909
+ */
910
+ setMeshVisibility( meshIndex, visible ) {
911
+
912
+ this.stages.pathTracer?.updateMeshVisibility( meshIndex, visible );
913
+ this.reset();
914
+
915
+ }
916
+
917
+ /**
918
+ * Recompute world-visibility for all meshes.
919
+ * Call after changing visibility on groups or parent objects.
920
+ */
921
+ updateAllMeshVisibility() {
922
+
923
+ this.stages.pathTracer?.updateAllMeshVisibility();
924
+ this.reset();
925
+
926
+ }
927
+
899
928
  /**
900
929
  * Updates a material's texture transform (offset, repeat, rotation).
901
930
  * @param {number} materialIndex
@@ -142,7 +142,6 @@ export class RenderStage {
142
142
 
143
143
  const renderMode = context.getState( 'renderMode' ) || 0;
144
144
  const tileRenderingComplete = context.getState( 'tileRenderingComplete' );
145
- const frame = context.getState( 'frame' ) || 0;
146
145
 
147
146
  switch ( this.executionMode ) {
148
147
 
@@ -1,4 +1,4 @@
1
- import { Box3, Vector3, RectAreaLight, Color, FloatType, LinearFilter, EquirectangularReflectionMapping, LinearMipmapLinearFilter,
1
+ import { Box3, Vector3, RectAreaLight, Color, FloatType, LinearFilter, EquirectangularReflectionMapping,
2
2
  TextureLoader, Mesh, MeshStandardMaterial, MeshPhysicalMaterial, CircleGeometry, Points, PointsMaterial, LoadingManager, EventDispatcher
3
3
  } from 'three';
4
4
  import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
@@ -171,8 +171,6 @@ export class AssetLoader extends EventDispatcher {
171
171
  }
172
172
 
173
173
  texture.generateMipmaps = true;
174
- // texture.minFilter = LinearMipmapLinearFilter;
175
- // texture.magFilter = LinearFilter;
176
174
 
177
175
  this.applyEnvironmentToScene( texture );
178
176
  this.dispatchEvent( { type: 'load', texture } );
@@ -353,7 +351,7 @@ export class AssetLoader extends EventDispatcher {
353
351
 
354
352
  }
355
353
 
356
- async findAndLoadModelFromZip( zip, filename ) {
354
+ async findAndLoadModelFromZip( zip ) {
357
355
 
358
356
  const mainModelFiles = [
359
357
  'scene.gltf', 'scene.glb', 'model.gltf', 'model.glb',
@@ -459,7 +457,6 @@ export class AssetLoader extends EventDispatcher {
459
457
  if ( extension === 'gltf' ) {
460
458
 
461
459
  const gltfContent = strFromU8( fileContent );
462
- const gltfJson = JSON.parse( gltfContent );
463
460
  const manager = new LoadingManager();
464
461
  const gltfDir = filePath.split( '/' ).slice( 0, - 1 ).join( '/' );
465
462
 
@@ -50,12 +50,11 @@ export class EmissiveTriangleBuilder {
50
50
  const material = materials[ materialIndex ];
51
51
  if ( ! material ) continue;
52
52
 
53
- // Check if emissive and visible
53
+ // Check if emissive
54
54
  const emissive = material.emissive || { r: 0, g: 0, b: 0 };
55
55
  const emissiveIntensity = material.emissiveIntensity || 0;
56
- const isVisible = material.visible === undefined || material.visible !== 0;
57
56
 
58
- const isEmissive = isVisible && emissiveIntensity > 0 && (
57
+ const isEmissive = emissiveIntensity > 0 && (
59
58
  emissive.r > 0 || emissive.g > 0 || emissive.b > 0
60
59
  );
61
60
 
@@ -410,8 +409,7 @@ export class EmissiveTriangleBuilder {
410
409
 
411
410
  const emissive = material.emissive || { r: 0, g: 0, b: 0 };
412
411
  const emissiveIntensity = material.emissiveIntensity || 0;
413
- const isVisible = material.visible === undefined || material.visible !== 0;
414
- const isNowEmissive = isVisible && emissiveIntensity > 0 && ( emissive.r > 0 || emissive.g > 0 || emissive.b > 0 );
412
+ const isNowEmissive = emissiveIntensity > 0 && ( emissive.r > 0 || emissive.g > 0 || emissive.b > 0 );
415
413
 
416
414
  // Check if this material had any emissive triangles before
417
415
  const hadEmissive = this.emissiveTriangles.some( t => t.materialIndex === materialIndex );
@@ -1,4 +1,4 @@
1
- import { DataUtils, HalfFloatType, FloatType } from 'three';
1
+ import { DataUtils, HalfFloatType, FloatType, SRGBColorSpace } from 'three';
2
2
 
3
3
  /**
4
4
  * Binary search to find the closest index
@@ -38,18 +38,35 @@ function colorToLuminance( r, g, b ) {
38
38
 
39
39
  }
40
40
 
41
+ /**
42
+ * sRGB to linear conversion (IEC 61966-2-1 transfer function)
43
+ */
44
+ function sRGBToLinear( c ) {
45
+
46
+ return c <= 0.04045 ? c / 12.92 : ( ( c + 0.055 ) / 1.055 ) ** 2.4;
47
+
48
+ }
49
+
41
50
  /**
42
51
  * Extract Float32 RGBA pixel data from an environment map.
43
- * Handles HalfFloat/integer type conversion and Y-flip.
52
+ * Handles HalfFloat/integer type conversion, canvas extraction for
53
+ * non-DataTexture images (JPG/PNG), sRGB-to-linear conversion, and Y-flip.
44
54
  * @returns {{ floatData: Float32Array, width: number, height: number }}
45
55
  */
46
56
  export function extractFloatData( envMap ) {
47
57
 
48
- const { width, height, data } = envMap.image;
58
+ const { width, height } = envMap.image;
59
+ let data = envMap.image.data;
60
+ let needsSRGBToLinear = false;
49
61
 
62
+ // No CPU-accessible data — extract from HTMLImageElement / ImageBitmap via canvas
50
63
  if ( ! data ) {
51
64
 
52
- throw new Error( 'EquirectHDRInfo: Environment map must have CPU-accessible image data. Render target textures are not supported.' );
65
+ const canvas = new OffscreenCanvas( width, height );
66
+ const ctx = canvas.getContext( '2d' );
67
+ ctx.drawImage( envMap.image, 0, 0, width, height );
68
+ data = ctx.getImageData( 0, 0, width, height ).data;
69
+ needsSRGBToLinear = true;
53
70
 
54
71
  }
55
72
 
@@ -72,7 +89,7 @@ export function extractFloatData( envMap ) {
72
89
 
73
90
  } else {
74
91
 
75
- // Integer types (Uint8, Int16, etc.)
92
+ // Integer types (Uint8, Uint8Clamped, Int16, etc.)
76
93
  let maxIntValue;
77
94
  if ( data instanceof Int8Array || data instanceof Int16Array || data instanceof Int32Array ) {
78
95
 
@@ -93,6 +110,26 @@ export function extractFloatData( envMap ) {
93
110
 
94
111
  }
95
112
 
113
+ // Also flag sRGB conversion for DataTextures explicitly marked as sRGB
114
+ if ( ! needsSRGBToLinear && envMap.colorSpace === SRGBColorSpace ) {
115
+
116
+ needsSRGBToLinear = true;
117
+
118
+ }
119
+
120
+ // Convert sRGB to linear so CDF luminance matches GPU-sampled linear values
121
+ if ( needsSRGBToLinear ) {
122
+
123
+ for ( let i = 0, l = floatData.length; i < l; i += 4 ) {
124
+
125
+ floatData[ i ] = sRGBToLinear( floatData[ i ] );
126
+ floatData[ i + 1 ] = sRGBToLinear( floatData[ i + 1 ] );
127
+ floatData[ i + 2 ] = sRGBToLinear( floatData[ i + 2 ] );
128
+
129
+ }
130
+
131
+ }
132
+
96
133
  // Remove Y-flip for CDF computation
97
134
  if ( envMap.flipY ) {
98
135
 
@@ -114,7 +114,7 @@ export class GeometryExtractor {
114
114
  }
115
115
 
116
116
  // Process material and get its index
117
- const materialIndex = this.processMaterial( mesh.material, mesh );
117
+ const materialIndex = this.processMaterial( mesh.material );
118
118
  mesh.userData.materialIndex = materialIndex;
119
119
 
120
120
  // Assign mesh index and store mesh reference
@@ -133,7 +133,7 @@ export class GeometryExtractor {
133
133
 
134
134
  }
135
135
 
136
- processMaterial( material, mesh = null ) {
136
+ processMaterial( material ) {
137
137
 
138
138
  // Check if material already exists in our array (O(1) Map lookup)
139
139
  let materialIndex = this._materialUuidMap.get( material.uuid ) ?? - 1;
@@ -148,7 +148,7 @@ export class GeometryExtractor {
148
148
  }
149
149
 
150
150
  // Create a new material object and add it to the array
151
- const newMaterial = this.createMaterialObject( material, mesh );
151
+ const newMaterial = this.createMaterialObject( material );
152
152
  this.materials.push( newMaterial );
153
153
  materialIndex = this.materials.length - 1;
154
154
  this._materialUuidMap.set( material.uuid, materialIndex );
@@ -327,7 +327,7 @@ export class GeometryExtractor {
327
327
 
328
328
  }
329
329
 
330
- createMaterialObject( material, mesh = null ) {
330
+ createMaterialObject( material ) {
331
331
 
332
332
  const defaults = this.getPhysicalDefaults();
333
333
  const materialType = this.getMaterialType( material );
@@ -407,8 +407,6 @@ export class GeometryExtractor {
407
407
  // Rendering properties
408
408
  side: this.getMaterialSide( material ),
409
409
  depthWrite: material.depthWrite ?? true ? 1 : 0,
410
- // Use mesh visibility if available, otherwise fall back to material or default to true
411
- visible: mesh ? ( mesh.visible ? 1 : 0 ) : ( material.visible ?? true ? 1 : 0 ),
412
410
 
413
411
  // Texture processing
414
412
  map: this.processTexture( material.map, this.maps ),
@@ -757,7 +755,7 @@ export class GeometryExtractor {
757
755
 
758
756
  if ( object.isMesh && object.geometry && object.material ) {
759
757
 
760
- const materialIndex = this.processMaterial( object.material, object );
758
+ const materialIndex = this.processMaterial( object.material );
761
759
  object.userData.materialIndex = materialIndex;
762
760
 
763
761
  const meshIndex = this.meshes.length;
@@ -820,8 +820,8 @@ export class SceneProcessor {
820
820
 
821
821
  // Factory method for creating any additional scene elements
822
822
  // Currently returns an empty array by default
823
- const white = new Color( 0xffffff );
824
- const black = new Color( 0x000000 );
823
+ // const white = new Color( 0xffffff );
824
+ // const black = new Color( 0x000000 );
825
825
  return [
826
826
  // { position: new Vector3( - 4, 2, 0 ), radius: 0.8, material: { color: white, emissive: black, emissiveIntensity: 0, roughness: 1.0 } },
827
827
  // { position: new Vector3( - 1.5, 2, 0 ), radius: 0.8, material: { color: white, emissive: black, emissiveIntensity: 0, roughness: 1.0 } },
@@ -1413,7 +1413,7 @@ export class SceneProcessor {
1413
1413
  * Returns null if no change, or the updated emissive data for GPU upload.
1414
1414
  *
1415
1415
  * @param {number} materialIndex
1416
- * @param {string} property - 'emissive' | 'emissiveIntensity' | 'visible'
1416
+ * @param {string} property - 'emissive' | 'emissiveIntensity'
1417
1417
  * @param {*} value
1418
1418
  * @returns {{ rawData: Float32Array, emissiveCount: number, totalPower: number }|null}
1419
1419
  */
@@ -1426,7 +1426,6 @@ export class SceneProcessor {
1426
1426
 
1427
1427
  if ( property === 'emissive' ) mat.emissive = value;
1428
1428
  else if ( property === 'emissiveIntensity' ) mat.emissiveIntensity = value;
1429
- else if ( property === 'visible' ) mat.visible = value;
1430
1429
 
1431
1430
  const changed = this.emissiveTriangleBuilder.updateMaterialEmissive(
1432
1431
  materialIndex, mat,
@@ -16,6 +16,7 @@ import { Fn, texture, vec2, float, int, uniform, If,
16
16
  import { TextureNode } from 'three/webgpu';
17
17
  import { LinearFilter, DataArrayTexture } from 'three';
18
18
  import { pathTracerMain } from '../TSL/PathTracer.js';
19
+ import { setMeshVisibilityBuffer } from '../TSL/BVHTraversal.js';
19
20
  import { BuildTimer } from './BuildTimer.js';
20
21
 
21
22
  const WG_SIZE = 8;
@@ -188,6 +189,9 @@ export class ShaderBuilder {
188
189
  const emissiveTriStorage = stage.emissiveTriangleStorageNode;
189
190
  const lightBVHStorage = stage.lightBVHStorageNode;
190
191
 
192
+ // Set per-mesh visibility buffer (module-level in BVHTraversal.js, read during graph construction)
193
+ setMeshVisibilityBuffer( stage.meshVisibilityStorageNode );
194
+
191
195
  const envTex = texture( stage.environment.environmentTexture );
192
196
 
193
197
  // Adaptive sampling texture
@@ -268,7 +268,7 @@ export class TLASBuilder {
268
268
  // Leaf node — BLAS pointer
269
269
  const entry = entries[ n.entryIndex ];
270
270
  data[ o ] = entry.blasOffset; // Absolute node index of BLAS root in combined buffer
271
- // data[o+1] = 0
271
+ data[ o + 1 ] = n.entryIndex; // meshIndex for per-mesh visibility check
272
272
  // data[o+2] = 0
273
273
  data[ o + 3 ] = BVH_LEAF_MARKERS.BLAS_POINTER_LEAF; // -2 marker
274
274
 
@@ -188,6 +188,10 @@ export class PathTracer extends RenderStage {
188
188
  this.lightBVHStorageAttr = new StorageInstancedBufferAttribute( new Float32Array( 16 ), 4 );
189
189
  this.lightBVHStorageNode = storage( this.lightBVHStorageAttr, 'vec4', 1 ).toReadOnly();
190
190
 
191
+ // Per-mesh visibility (storage buffer for TLAS BLAS-pointer skip)
192
+ this.meshVisibilityStorageAttr = new StorageInstancedBufferAttribute( new Float32Array( [ 1, 0, 0, 0 ] ), 4 );
193
+ this.meshVisibilityStorageNode = storage( this.meshVisibilityStorageAttr, 'vec4', 1 ).toReadOnly();
194
+
191
195
  // Adaptive sampling
192
196
  this.adaptiveSamplingTexture = null;
193
197
 
@@ -480,6 +484,10 @@ export class PathTracer extends RenderStage {
480
484
 
481
485
  }
482
486
 
487
+ // Per-mesh visibility — collect meshes from scene ordered by meshIndex
488
+ this._meshRefs = this._collectMeshRefs( this.scene );
489
+ this.setMeshVisibilityData( this._meshRefs );
490
+
483
491
  // Spheres
484
492
  this.spheres = this.sdfs.spheres || [];
485
493
 
@@ -759,6 +767,109 @@ export class PathTracer extends RenderStage {
759
767
 
760
768
  }
761
769
 
770
+ /**
771
+ * Build per-mesh visibility storage buffer from mesh world-visibility.
772
+ * Each mesh gets one float (1.0 = visible, 0.0 = hidden).
773
+ * Padded to vec4 alignment for GPU storage buffer compatibility.
774
+ * @param {Array} meshes - Array of Three.js mesh objects
775
+ */
776
+ setMeshVisibilityData( meshes ) {
777
+
778
+ if ( ! meshes || meshes.length === 0 ) return;
779
+
780
+ const meshCount = meshes.length;
781
+ // One vec4 per mesh — visibility stored in .x (simple indexing on GPU)
782
+ const data = new Float32Array( meshCount * 4 );
783
+
784
+ for ( let i = 0; i < meshCount; i ++ ) {
785
+
786
+ data[ i * 4 ] = this._isWorldVisible( meshes[ i ] ) ? 1.0 : 0.0;
787
+
788
+ }
789
+
790
+ this.meshVisibilityStorageAttr = new StorageInstancedBufferAttribute( data, 4 );
791
+ this.meshVisibilityStorageNode.value = this.meshVisibilityStorageAttr;
792
+ this.meshVisibilityStorageNode.bufferCount = meshCount;
793
+
794
+ }
795
+
796
+ /**
797
+ * Update visibility for a single mesh in the GPU buffer (no rebuild).
798
+ * @param {number} meshIndex
799
+ * @param {boolean} visible
800
+ */
801
+ updateMeshVisibility( meshIndex, visible ) {
802
+
803
+ if ( ! this.meshVisibilityStorageAttr ) return;
804
+
805
+ this.meshVisibilityStorageAttr.array[ meshIndex * 4 ] = visible ? 1.0 : 0.0;
806
+ this.meshVisibilityStorageAttr.needsUpdate = true;
807
+
808
+ }
809
+
810
+ /**
811
+ * Recompute world-visibility for all meshes and update the GPU buffer.
812
+ * Call this when group visibility changes at runtime.
813
+ */
814
+ updateAllMeshVisibility() {
815
+
816
+ if ( ! this._meshRefs || ! this.meshVisibilityStorageAttr ) return;
817
+
818
+ const data = this.meshVisibilityStorageAttr.array;
819
+ for ( let i = 0; i < this._meshRefs.length; i ++ ) {
820
+
821
+ data[ i * 4 ] = this._isWorldVisible( this._meshRefs[ i ] ) ? 1.0 : 0.0;
822
+
823
+ }
824
+
825
+ this.meshVisibilityStorageAttr.needsUpdate = true;
826
+
827
+ }
828
+
829
+ /**
830
+ * Collect mesh references from scene, ordered by meshIndex (assigned during extraction).
831
+ * @param {Object3D} scene
832
+ * @returns {Array}
833
+ * @private
834
+ */
835
+ _collectMeshRefs( scene ) {
836
+
837
+ if ( ! scene ) return [];
838
+
839
+ const meshes = [];
840
+ scene.traverse( obj => {
841
+
842
+ if ( obj.isMesh && obj.userData.meshIndex !== undefined ) {
843
+
844
+ meshes[ obj.userData.meshIndex ] = obj;
845
+
846
+ }
847
+
848
+ } );
849
+
850
+ return meshes;
851
+
852
+ }
853
+
854
+ /**
855
+ * Walk the parent chain to determine world-space visibility.
856
+ * @param {Object3D} object
857
+ * @returns {boolean}
858
+ * @private
859
+ */
860
+ _isWorldVisible( object ) {
861
+
862
+ while ( object ) {
863
+
864
+ if ( ! object.visible ) return false;
865
+ object = object.parent;
866
+
867
+ }
868
+
869
+ return true;
870
+
871
+ }
872
+
762
873
  // ===== FAST BUFFER UPDATES (BVH Refit / Animation) =====
763
874
 
764
875
  /**