rayzee 11.2.1 → 11.3.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.
- package/README.md +58 -29
- package/dist/ArchiveImporter-D735xX-4.js +7860 -0
- package/dist/BidirectionalIntegrator-M89c0AlP.js +954 -0
- package/dist/{BufferLayout-D_IBo2ci.js → BufferLayout-xPRH9jYD.js} +41 -5
- package/dist/{NeuralRetouch-Cr-otSW8.js → NeuralRetouch-DiBH63kz.js} +1 -1
- package/dist/{NeuralSuperRes-B7E3_M0t.js → NeuralSuperRes-CZNQP13W.js} +1 -1
- package/dist/{PathTracer-D6nbknj5.js → PathTracer-BXCCVrPK.js} +4371 -4612
- package/dist/{PhysicalSky-Dq5VSiCQ.js → PhysicalSky-DqmaS0lb.js} +2 -2
- package/dist/{RayzeeRenderer-DbjqO6v-.js → RayzeeRenderer-DUSMFhmc.js} +426 -363
- package/dist/SolarLimb-bZE6y63S.js +762 -0
- package/dist/addons/archives.es.js +1 -1
- package/dist/addons/bidirectional.es.js +1 -1
- package/dist/addons/physical-sky.es.js +1 -1
- package/dist/rayzee-core.es.js +7 -7
- package/dist/rayzee.es.js +472 -468
- package/dist/rayzee.umd.js +106 -105
- package/dist/sceneCachePolicy-BAib5fCp.js +1602 -0
- package/package.json +1 -1
- package/src/PathTracerApp.js +4 -3
- package/src/Processor/ArchiveImporter.js +173 -94
- package/src/Processor/ArchiveReader.js +20 -23
- package/src/Processor/AssetLoader.js +48 -1
- package/src/Processor/BVHRefitter.js +109 -13
- package/src/Processor/BufferLayout.js +115 -0
- package/src/Processor/ChunkedRecords.js +3 -2
- package/src/Processor/EmissiveTriangleBuilder.js +6 -6
- package/src/Processor/GeometryExtractor.js +189 -79
- package/src/Processor/HostMemory.js +39 -9
- package/src/Processor/InstanceTable.js +714 -42
- package/src/Processor/PBRT/PBRTCurves.js +224 -6
- package/src/Processor/PBRT/PBRTMaterials.js +3 -0
- package/src/Processor/PBRT/PBRTSceneBuilder.js +82 -25
- package/src/Processor/PackedRayBuffer.js +5 -2
- package/src/Processor/SceneProcessor.js +747 -141
- package/src/Processor/TLASBuilder.js +32 -17
- package/src/Processor/USD/USDLayer.js +1149 -0
- package/src/Processor/USD/USDScene.js +1655 -0
- package/src/Processor/USD/USDStage.js +682 -0
- package/src/Processor/USD/USDText.js +674 -0
- package/src/Processor/USD/index.js +138 -0
- package/src/Processor/Workers/BVHRefitWorker.js +3 -3
- package/src/RayzeeRenderer.js +167 -26
- package/src/Stages/NormalDepth.js +7 -12
- package/src/Stages/PathTracer.js +12 -17
- package/src/Stages/PathTracerStage.js +273 -96
- package/src/Storage/GeometrySpill.js +46 -14
- package/src/Storage/SceneGraphCodec.js +17 -3
- package/src/Storage/SpillStore.js +9 -0
- package/src/TSL/BVHTraversal.js +183 -7
- package/src/TSL/Common.js +57 -13
- package/src/TSL/ShadeKernel.js +3 -2
- package/src/TSL/TextureSampling.js +11 -8
- package/src/TSL/patches.js +84 -0
- package/src/integrators/BidirectionalIntegrator.js +20 -2
- package/src/managers/DenoisingManager.js +4 -4
- package/src/managers/InteractionManager.js +19 -6
- package/src/managers/MaterialDataManager.js +22 -5
- package/src/managers/UniformManager.js +3 -0
- package/dist/ArchiveImporter-D_x863yu.js +0 -5198
- package/dist/BidirectionalIntegrator-DmkxxEOR.js +0 -946
- package/dist/SolarLimb-CPaeZVwz.js +0 -721
- package/dist/sceneCachePolicy-C4-4qwt6.js +0 -485
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { storage } from 'three/tsl';
|
|
2
|
-
import {
|
|
2
|
+
import { StorageParts } from '../TSL/patches.js';
|
|
3
3
|
import { StorageInstancedBufferAttribute } from 'three/webgpu';
|
|
4
4
|
import { Vector2, Matrix4 } from 'three';
|
|
5
5
|
|
|
@@ -21,8 +21,8 @@ import { LightSerializer } from '../Processor/LightSerializer';
|
|
|
21
21
|
|
|
22
22
|
// Constants
|
|
23
23
|
import { ENGINE_DEFAULTS as DEFAULT_STATE } from '../EngineDefaults.js';
|
|
24
|
-
import { TRIANGLE_DATA_LAYOUT } from '../Processor/BufferLayout.js';
|
|
25
|
-
import { TRI_GEO_ROWS, TRI_SHADE_ROWS } from '../TSL/Common.js';
|
|
24
|
+
import { TRIANGLE_DATA_LAYOUT, CLUSTER_COPY_HIDDEN, bvhIndexView } from '../Processor/BufferLayout.js';
|
|
25
|
+
import { TRI_GEO_ROWS, TRI_SHADE_ROWS, splitStorage } from '../TSL/Common.js';
|
|
26
26
|
import { createLogger, fmt } from '../utils/Logger.js';
|
|
27
27
|
|
|
28
28
|
// Triangles converted per staging pass: 256K is 12 + 8 MB of staging, kept for the stage's life.
|
|
@@ -30,6 +30,25 @@ const TRIANGLE_UPLOAD_SLICE = 1 << 18;
|
|
|
30
30
|
|
|
31
31
|
const log = createLogger( 'pathtracer' );
|
|
32
32
|
|
|
33
|
+
|
|
34
|
+
// Storage buffers the shading kernel binds besides the BVH and triangle parts: rays, hits (with the RNG state and the
|
|
35
|
+
// G-buffer), counters, active indices, and the scene data (materials, then lights). Measured on the compiled WGSL.
|
|
36
|
+
const SHADE_OTHER_BINDINGS = 5;
|
|
37
|
+
|
|
38
|
+
/** Read-only storage nodes over `parts`, one node when it is a single buffer. */
|
|
39
|
+
function readParts( parts, type ) {
|
|
40
|
+
|
|
41
|
+
return splitStorage( parts.attrs.map( ( attr ) => storage( attr, type, attr.count ).toReadOnly() ), parts.elementsPerPart, type );
|
|
42
|
+
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The `{ geo, shade }` pair kernels read triangles through (TSL/Common.js `triangleRow`). */
|
|
46
|
+
function triangleNodes( geo, shade ) {
|
|
47
|
+
|
|
48
|
+
return { geo: readParts( geo, 'uvec4' ), shade: readParts( shade, shade.withoutUV ? 'uvec2' : 'uvec4' ), withoutUV: shade.withoutUV === true };
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
|
|
33
52
|
// How long after the last move the view still counts as moving. Matches CameraOptimizer's own
|
|
34
53
|
// settle delay, so the answer does not change when interaction mode is switched off.
|
|
35
54
|
const VIEW_SETTLE_MS = 100;
|
|
@@ -113,6 +132,7 @@ export class PathTracerStage extends RenderStage {
|
|
|
113
132
|
// Initialize material data manager
|
|
114
133
|
this.materialData = new MaterialDataManager( this.sdfs );
|
|
115
134
|
this.materialData.callbacks.onReset = () => this.reset();
|
|
135
|
+
this.materialData.callbacks.adoptMaterials = ( data ) => this._adoptMaterials( data );
|
|
116
136
|
// A material edit can switch the diffuse transmission lobe on or off, which the shade kernel compiles in.
|
|
117
137
|
this.materialData.callbacks.onMaterialFeaturesChanged = () => {
|
|
118
138
|
|
|
@@ -625,7 +645,7 @@ export class PathTracerStage extends RenderStage {
|
|
|
625
645
|
* @param {Uint32Array|import('../Processor/ChunkedRecords.js').ChunkedRecords} triangleData
|
|
626
646
|
* @param {number} triangleCount - Number of triangles
|
|
627
647
|
*/
|
|
628
|
-
setTriangleData( triangleData, triangleCount ) {
|
|
648
|
+
setTriangleData( triangleData, triangleCount, { textureCoordinates = true } = {} ) {
|
|
629
649
|
|
|
630
650
|
if ( ! triangleData ) return;
|
|
631
651
|
|
|
@@ -633,45 +653,57 @@ export class PathTracerStage extends RenderStage {
|
|
|
633
653
|
const flat = chunked ? null : ( triangleData.chunks ? triangleData.chunks[ 0 ] : triangleData );
|
|
634
654
|
const records = chunked ? chunked.recordCount : Math.ceil( flat.length / TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE );
|
|
635
655
|
const pre = chunked?._gpuUpload?.triangles ? chunked._gpuUpload : null;
|
|
636
|
-
const { geo, shade } = pre ?? this._allocateTriangleBuffers( records );
|
|
656
|
+
const { geo, shade } = pre ?? this._allocateTriangleBuffers( records, { textureCoordinates } );
|
|
657
|
+
const withoutUV = shade.withoutUV === true;
|
|
658
|
+
const node = this.triangleStorageNode;
|
|
637
659
|
|
|
638
|
-
if (
|
|
660
|
+
if ( node && geo.attrs.length === 1 && shade.attrs.length === 1 && ! node.geo.isSplitStorage && ! node.shade.isSplitStorage && !! node.withoutUV === withoutUV ) {
|
|
639
661
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
662
|
+
node.geo.value = geo.attrs[ 0 ];
|
|
663
|
+
node.geo.bufferCount = geo.attrs[ 0 ].count;
|
|
664
|
+
node.shade.value = shade.attrs[ 0 ];
|
|
665
|
+
node.shade.bufferCount = shade.attrs[ 0 ].count;
|
|
644
666
|
|
|
645
667
|
} else {
|
|
646
668
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
shade: storage( shade, 'uvec4', shade.count ).toReadOnly(),
|
|
650
|
-
};
|
|
669
|
+
// Kernels rebuild with the scene (setupMaterial), so a new layout takes new nodes.
|
|
670
|
+
this.triangleStorageNode = triangleNodes( geo, shade );
|
|
651
671
|
|
|
652
672
|
}
|
|
653
673
|
|
|
654
|
-
this.
|
|
655
|
-
this.
|
|
674
|
+
this._geoParts = geo;
|
|
675
|
+
this._shadeParts = shade;
|
|
676
|
+
this.triangleGeoAttr = geo.attrs[ 0 ];
|
|
677
|
+
this.triangleShadeAttr = shade.attrs[ 0 ];
|
|
656
678
|
this._triangleRecords = chunked;
|
|
657
679
|
this._triangleFlat = flat;
|
|
658
680
|
this._triangleRecordCount = records;
|
|
659
681
|
this.triangleCount = triangleCount;
|
|
660
682
|
this._uploadTriangles( 0, records, { skipChunks: pre?.uploaded } );
|
|
683
|
+
// Adopted once: a later call with these records uploads them again, in the layout it asks for.
|
|
684
|
+
if ( pre ) chunked._gpuUpload = null;
|
|
661
685
|
|
|
662
686
|
log.debug( `${fmt.n( this.triangleCount )} triangles (storage buffer)` );
|
|
663
687
|
|
|
664
688
|
}
|
|
665
689
|
|
|
666
|
-
/**
|
|
667
|
-
|
|
690
|
+
/**
|
|
691
|
+
* GPU-only geo and shade buffers for `records` triangles, in as many parts as one binding's limit needs. Without
|
|
692
|
+
* texture coordinates the shade buffer keeps only flags and mesh index, a uvec2 a triangle (TSL/Common.js
|
|
693
|
+
* `triangleRow`): 24 B a triangle less, 2.65 GB on the whole Moana island, which samples no texture. @private
|
|
694
|
+
*/
|
|
695
|
+
_allocateTriangleBuffers( records, { textureCoordinates = true } = {} ) {
|
|
668
696
|
|
|
669
697
|
const n = Math.max( 1, records );
|
|
670
|
-
this.
|
|
671
|
-
const geo =
|
|
672
|
-
const shade =
|
|
673
|
-
|
|
674
|
-
|
|
698
|
+
const max = this._maxPartBytes();
|
|
699
|
+
const geo = new StorageParts( n * TRI_GEO_ROWS * 4, TRI_GEO_ROWS * 4, max, Uint32Array );
|
|
700
|
+
const shade = textureCoordinates
|
|
701
|
+
? new StorageParts( n * TRI_SHADE_ROWS * 4, TRI_SHADE_ROWS * 4, max, Uint32Array )
|
|
702
|
+
: new StorageParts( n * 2, 2, max, Uint32Array, 2 );
|
|
703
|
+
shade.withoutUV = ! textureCoordinates;
|
|
704
|
+
this._assertBindings( { geo: geo.attrs.length, shade: shade.attrs.length, bvh: 1 } );
|
|
705
|
+
geo.create( this.renderer.backend );
|
|
706
|
+
shade.create( this.renderer.backend );
|
|
675
707
|
return { geo, shade };
|
|
676
708
|
|
|
677
709
|
}
|
|
@@ -685,21 +717,22 @@ export class PathTracerStage extends RenderStage {
|
|
|
685
717
|
* `source` writes those records instead of reading the stage's own (a chunk being handed over).
|
|
686
718
|
* @private
|
|
687
719
|
*/
|
|
688
|
-
_uploadTriangles( start, count, { geo = true, shade = true, skipChunks = null, source = null, sourceStart = 0,
|
|
720
|
+
_uploadTriangles( start, count, { geo = true, shade = true, skipChunks = null, source = null, sourceStart = 0, geoParts = this._geoParts, shadeParts = this._shadeParts } = {} ) {
|
|
689
721
|
|
|
690
|
-
if ( count <= 0 || !
|
|
722
|
+
if ( count <= 0 || ! geoParts ) return;
|
|
691
723
|
|
|
692
724
|
const backend = this.renderer.backend;
|
|
693
|
-
const geoBuffer = backend.get( geoAttr ).buffer;
|
|
694
|
-
const shadeBuffer = backend.get( shadeAttr ).buffer;
|
|
695
725
|
const LANES = TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE;
|
|
696
|
-
const GEO = TRI_GEO_ROWS * 4
|
|
726
|
+
const GEO = TRI_GEO_ROWS * 4;
|
|
727
|
+
// Without texture coordinates only the flags and mesh index lanes go up.
|
|
728
|
+
const SHADE = shadeParts.withoutUV ? 2 : TRI_SHADE_ROWS * 4;
|
|
729
|
+
const SHADE_FROM = LANES - SHADE;
|
|
697
730
|
const records = this._triangleRecords;
|
|
698
731
|
const perChunk = source ? count : records ? records.recordsPerChunk : this._triangleRecordCount;
|
|
699
732
|
// One staging pair for every upload: a fresh one per chunk was churn that failed to
|
|
700
733
|
// allocate on a scene already at the edge of the address space.
|
|
701
734
|
const slice = TRIANGLE_UPLOAD_SLICE;
|
|
702
|
-
this._triangleStaging ??= { geo: new Uint32Array( slice * GEO ), shade: new Uint32Array( slice *
|
|
735
|
+
this._triangleStaging ??= { geo: new Uint32Array( slice * GEO ), shade: new Uint32Array( slice * TRI_SHADE_ROWS * 4 ) };
|
|
703
736
|
const geoStage = this._triangleStaging.geo;
|
|
704
737
|
const shadeStage = this._triangleStaging.shade;
|
|
705
738
|
|
|
@@ -723,12 +756,12 @@ export class PathTracerStage extends RenderStage {
|
|
|
723
756
|
for ( let i = 0, g = 0, h = 0; i < n; i ++, s += LANES ) {
|
|
724
757
|
|
|
725
758
|
if ( geo ) for ( let l = 0; l < GEO; l ++ ) geoStage[ g ++ ] = src[ s + l ];
|
|
726
|
-
if ( shade ) for ( let l =
|
|
759
|
+
if ( shade ) for ( let l = SHADE_FROM; l < LANES; l ++ ) shadeStage[ h ++ ] = src[ s + l ];
|
|
727
760
|
|
|
728
761
|
}
|
|
729
762
|
|
|
730
|
-
if ( geo )
|
|
731
|
-
if ( shade )
|
|
763
|
+
if ( geo ) geoParts.write( backend, at * GEO, geoStage, 0, n * GEO );
|
|
764
|
+
if ( shade ) shadeParts.write( backend, at * SHADE, shadeStage, 0, n * SHADE );
|
|
732
765
|
at += n;
|
|
733
766
|
|
|
734
767
|
}
|
|
@@ -740,38 +773,48 @@ export class PathTracerStage extends RenderStage {
|
|
|
740
773
|
* chunk into it, so a build can hand chunks over as it finishes them and let them go (the
|
|
741
774
|
* memory spill). setTriangleData / setBVHData then adopt that buffer rather than upload again.
|
|
742
775
|
* @param {import('../Processor/ChunkedRecords.js').ChunkedRecords} records
|
|
743
|
-
* @returns {function(number): void} uploads chunk k from `records.chunks[ k ]
|
|
776
|
+
* @returns {function(number): Promise<void>} uploads chunk k from `records.chunks[ k ]`; resolves with {@link drainUploads}
|
|
744
777
|
*/
|
|
745
778
|
createChunkUploader( records ) {
|
|
746
779
|
|
|
747
|
-
this.
|
|
748
|
-
const attr = gpuOnlyStorageAttribute( records.recordCount * records.lanesPerRecord / 4, 4, records.LaneType );
|
|
780
|
+
const parts = this._allocateBVHParts( records.recordCount * records.lanesPerRecord );
|
|
749
781
|
const backend = this.renderer.backend;
|
|
750
|
-
|
|
751
|
-
const buffer = backend.get( attr ).buffer;
|
|
752
|
-
const stride = records.recordsPerChunk * records.lanesPerRecord * records.LaneType.BYTES_PER_ELEMENT;
|
|
782
|
+
const lanesPerChunk = records.recordsPerChunk * records.lanesPerRecord;
|
|
753
783
|
const uploaded = new Set();
|
|
754
|
-
records._gpuUpload = {
|
|
784
|
+
records._gpuUpload = { parts, uploaded };
|
|
755
785
|
|
|
756
786
|
return k => {
|
|
757
787
|
|
|
758
788
|
const chunk = records.chunks[ k ];
|
|
759
|
-
|
|
789
|
+
parts.write( backend, k * lanesPerChunk, chunk, 0, chunk.length );
|
|
760
790
|
uploaded.add( k );
|
|
791
|
+
return this.drainUploads();
|
|
761
792
|
|
|
762
793
|
};
|
|
763
794
|
|
|
764
795
|
}
|
|
765
796
|
|
|
797
|
+
/**
|
|
798
|
+
* Resolves once the GPU has taken every upload so far. Chrome passes each upload through a shared memory pool that
|
|
799
|
+
* grows to the most ever sent before the GPU caught up and is kept until the page closes (2.2 GB on the Moana
|
|
800
|
+
* island); awaiting this after each chunk keeps it to one. Measured: 1 GB in 64 MB writes left 1 GB behind, paced
|
|
801
|
+
* 64 MB, in the same time.
|
|
802
|
+
*/
|
|
803
|
+
drainUploads() {
|
|
804
|
+
|
|
805
|
+
return this.renderer.backend.device.queue.onSubmittedWorkDone();
|
|
806
|
+
|
|
807
|
+
}
|
|
808
|
+
|
|
766
809
|
/**
|
|
767
810
|
* createChunkUploader for triangle records: both GPU buffers are allocated now and each
|
|
768
811
|
* chunk is split into them as it is handed over.
|
|
769
812
|
* @param {import('../Processor/ChunkedRecords.js').ChunkedRecords} records
|
|
770
|
-
* @returns {function(number): void}
|
|
813
|
+
* @returns {function(number): Promise<void>}
|
|
771
814
|
*/
|
|
772
|
-
createTriangleChunkUploader( records ) {
|
|
815
|
+
createTriangleChunkUploader( records, { textureCoordinates = true } = {} ) {
|
|
773
816
|
|
|
774
|
-
const { geo, shade } = this._allocateTriangleBuffers( records.recordCount );
|
|
817
|
+
const { geo, shade } = this._allocateTriangleBuffers( records.recordCount, { textureCoordinates } );
|
|
775
818
|
const uploaded = new Set();
|
|
776
819
|
records._gpuUpload = { triangles: true, geo, shade, uploaded };
|
|
777
820
|
const perChunk = records.recordsPerChunk;
|
|
@@ -780,46 +823,79 @@ export class PathTracerStage extends RenderStage {
|
|
|
780
823
|
|
|
781
824
|
const first = k * perChunk;
|
|
782
825
|
const count = Math.min( perChunk, records.recordCount - first );
|
|
783
|
-
this._uploadTriangles( first, count, { source: records.chunks[ k ],
|
|
826
|
+
this._uploadTriangles( first, count, { source: records.chunks[ k ], geoParts: geo, shadeParts: shade } );
|
|
784
827
|
uploaded.add( k );
|
|
828
|
+
return this.drainUploads();
|
|
785
829
|
|
|
786
830
|
};
|
|
787
831
|
|
|
788
832
|
}
|
|
789
833
|
|
|
834
|
+
/** Fresh read-only nodes over the scene's BVH and triangles, for a stage that builds a kernel of its own. */
|
|
835
|
+
sceneStorageNodes() {
|
|
836
|
+
|
|
837
|
+
if ( ! this.bvhStorageAttr || ! this._geoParts ) return null;
|
|
838
|
+
const bvh = this._bvhParts ? readParts( this._bvhParts, 'vec4' ) : storage( this.bvhStorageAttr, 'vec4', this.bvhStorageAttr.count ).toReadOnly();
|
|
839
|
+
return { bvh, triangles: triangleNodes( this._geoParts, this._shadeParts ) };
|
|
840
|
+
|
|
841
|
+
}
|
|
842
|
+
|
|
790
843
|
/**
|
|
791
|
-
*
|
|
792
|
-
*
|
|
844
|
+
* Most bytes one storage buffer may hold: past it WebGPU hands back an invalid buffer and every write to it fails
|
|
845
|
+
* quietly, so a larger store is split into parts. `bufferPartBytes` lowers it, for tests to force several parts.
|
|
793
846
|
* @private
|
|
794
847
|
*/
|
|
795
|
-
|
|
848
|
+
_maxPartBytes() {
|
|
796
849
|
|
|
797
850
|
const limits = this.renderer?.backend?.device?.limits;
|
|
798
851
|
const max = Math.min( limits?.maxBufferSize ?? Infinity, limits?.maxStorageBufferBindingSize ?? Infinity );
|
|
799
|
-
|
|
852
|
+
return Math.min( max, this.bufferPartBytes ?? Infinity );
|
|
800
853
|
|
|
801
854
|
}
|
|
802
855
|
|
|
803
|
-
/**
|
|
804
|
-
|
|
856
|
+
/**
|
|
857
|
+
* Every part of the tree and triangle stores is a binding of its own, and the shading kernel binds them all beside
|
|
858
|
+
* SHADE_OTHER_BINDINGS others, so past the device's per-stage limit no kernel could be built: refuse before upload.
|
|
859
|
+
* @private
|
|
860
|
+
*/
|
|
861
|
+
_assertBindings( counts ) {
|
|
862
|
+
|
|
863
|
+
// The latest allocation of each: a build allocates its triangles before its BVH, and the previous scene's parts
|
|
864
|
+
// must not count against the next one.
|
|
865
|
+
this._partCounts = { ...( this._partCounts ?? { bvh: 1, geo: 1, shade: 1 } ), ...counts };
|
|
866
|
+
const { bvh, geo, shade } = this._partCounts;
|
|
867
|
+
const limit = this.renderer?.backend?.device?.limits?.maxStorageBuffersPerShaderStage ?? 10;
|
|
868
|
+
if ( SHADE_OTHER_BINDINGS + bvh + geo + shade <= limit ) return;
|
|
869
|
+
throw new Error(
|
|
870
|
+
`the scene's BVH and triangles need ${bvh + geo + shade} GPU buffers (BVH ${bvh}, triangles ${geo} + ${shade}), ` +
|
|
871
|
+
`more than the ${limit - SHADE_OTHER_BINDINGS} one GPU program can bind beside the rest — load fewer triangles or copies`
|
|
872
|
+
);
|
|
873
|
+
|
|
874
|
+
}
|
|
805
875
|
|
|
806
|
-
|
|
807
|
-
|
|
876
|
+
/** GPU-only BVH buffers for `lanes` floats, in as many parts as one binding's limit needs. @private */
|
|
877
|
+
_allocateBVHParts( lanes ) {
|
|
808
878
|
|
|
809
|
-
|
|
810
|
-
|
|
879
|
+
const parts = new StorageParts( lanes, 16, this._maxPartBytes(), Float32Array );
|
|
880
|
+
this._assertBindings( { bvh: parts.attrs.length } );
|
|
881
|
+
parts.create( this.renderer.backend );
|
|
882
|
+
return parts;
|
|
811
883
|
|
|
812
|
-
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/** Uploads chunked records into `parts`, or — for parts a build already filled — whatever it has not. @private */
|
|
887
|
+
_uploadChunked( parts, records ) {
|
|
813
888
|
|
|
814
|
-
const
|
|
815
|
-
const
|
|
889
|
+
const pre = records._gpuUpload?.parts === parts ? records._gpuUpload : null;
|
|
890
|
+
const lanesPerChunk = records.recordsPerChunk * records.lanesPerRecord;
|
|
891
|
+
const backend = this.renderer.backend;
|
|
816
892
|
for ( let k = 0; k < records.chunks.length; k ++ ) {
|
|
817
893
|
|
|
818
|
-
if ( pre
|
|
894
|
+
if ( pre?.uploaded.has( k ) ) continue;
|
|
819
895
|
const chunk = records.chunks[ k ];
|
|
820
896
|
if ( ! chunk ) throw new Error( `chunk ${k} is on disk and was never uploaded` );
|
|
821
|
-
|
|
822
|
-
pre
|
|
897
|
+
parts.write( backend, k * lanesPerChunk, chunk, 0, chunk.length );
|
|
898
|
+
pre?.uploaded.add( k );
|
|
823
899
|
|
|
824
900
|
}
|
|
825
901
|
|
|
@@ -839,28 +915,31 @@ export class PathTracerStage extends RenderStage {
|
|
|
839
915
|
const flat = chunked ? null : ( bvhImageData.chunks ? bvhImageData.chunks[ 0 ] : bvhImageData );
|
|
840
916
|
const lanes = chunked ? chunked.recordCount * chunked.lanesPerRecord : flat.length;
|
|
841
917
|
const vec4Count = lanes / 4;
|
|
842
|
-
this._assertFitsGPU( lanes * 4 );
|
|
843
918
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
919
|
+
// A flat array keeps a CPU-backed attribute (patches go up through its update ranges); chunked records go to
|
|
920
|
+
// GPU-only parts, as many as one binding's limit needs.
|
|
921
|
+
const parts = chunked ? chunked._gpuUpload?.parts ?? this._allocateBVHParts( lanes ) : null;
|
|
922
|
+
const attrs = parts ? parts.attrs : [ new StorageInstancedBufferAttribute( flat, 4 ) ];
|
|
923
|
+
if ( ! parts ) this._assertBindings( { bvh: 1 } );
|
|
847
924
|
|
|
848
|
-
if ( this.bvhStorageNode ) {
|
|
925
|
+
if ( this.bvhStorageNode && attrs.length === 1 && ! this.bvhStorageNode.isSplitStorage ) {
|
|
849
926
|
|
|
850
|
-
this.
|
|
851
|
-
this.bvhStorageNode.value = this.bvhStorageAttr;
|
|
927
|
+
this.bvhStorageNode.value = attrs[ 0 ];
|
|
852
928
|
this.bvhStorageNode.bufferCount = vec4Count;
|
|
853
929
|
|
|
854
930
|
} else {
|
|
855
931
|
|
|
856
|
-
this.
|
|
857
|
-
this.bvhStorageNode = storage( this.bvhStorageAttr, 'vec4', vec4Count ).toReadOnly();
|
|
932
|
+
this.bvhStorageNode = parts ? readParts( parts, 'vec4' ) : storage( attrs[ 0 ], 'vec4', vec4Count ).toReadOnly();
|
|
858
933
|
|
|
859
934
|
}
|
|
860
935
|
|
|
936
|
+
this.bvhStorageAttr = attrs[ 0 ];
|
|
861
937
|
this.bvhStorageAttr.foldedLeaves = bvhImageData.foldedLeaves === true;
|
|
938
|
+
this.bvhStorageAttr.copyRecordBase = bvhImageData.copyRecordBase;
|
|
939
|
+
this._bvhParts = parts;
|
|
862
940
|
this._bvhRecords = chunked;
|
|
863
|
-
if ( chunked ) this._uploadChunked(
|
|
941
|
+
if ( chunked ) this._uploadChunked( parts, chunked );
|
|
942
|
+
if ( chunked ) chunked._gpuUpload = null;
|
|
864
943
|
|
|
865
944
|
this.bvhNodeCount = Math.floor( vec4Count / BVH_VEC4_PER_NODE );
|
|
866
945
|
log.debug( `${fmt.n( this.bvhNodeCount )} BVH nodes (storage buffer)` );
|
|
@@ -913,6 +992,29 @@ export class PathTracerStage extends RenderStage {
|
|
|
913
992
|
|
|
914
993
|
}
|
|
915
994
|
|
|
995
|
+
this._patchGroupNodes();
|
|
996
|
+
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Rewrites the group trees: a hidden member's child points at the empty leaf. Every copy of a group shares its
|
|
1001
|
+
* tree, as every instance of an InstancedMesh shares its visibility.
|
|
1002
|
+
* @private
|
|
1003
|
+
*/
|
|
1004
|
+
_patchGroupNodes() {
|
|
1005
|
+
|
|
1006
|
+
const table = this._instanceTable;
|
|
1007
|
+
if ( ! table?.groupNodeCount || ! this.bvhStorageAttr ) return;
|
|
1008
|
+
|
|
1009
|
+
table.computeGroupAABBs();
|
|
1010
|
+
const block = table.groupNodeBlock();
|
|
1011
|
+
const start = table.groupNodeStart;
|
|
1012
|
+
if ( this._bvhRecords ) this._bvhRecords.setRecords( start, block );
|
|
1013
|
+
else this.bvhStorageAttr.array.set( block, start * 16 );
|
|
1014
|
+
|
|
1015
|
+
this._dirtyBVHLeaves ??= new Set();
|
|
1016
|
+
for ( let n = 0; n < table.groupNodeCount; n ++ ) this._dirtyBVHLeaves.add( start + n );
|
|
1017
|
+
|
|
916
1018
|
}
|
|
917
1019
|
|
|
918
1020
|
/**
|
|
@@ -922,8 +1024,13 @@ export class PathTracerStage extends RenderStage {
|
|
|
922
1024
|
*/
|
|
923
1025
|
updateMeshVisibility( meshIndex, visible ) {
|
|
924
1026
|
|
|
925
|
-
|
|
926
|
-
|
|
1027
|
+
this._whenTLASResident( () => {
|
|
1028
|
+
|
|
1029
|
+
if ( ! this._patchTLASLeafVisibility( meshIndex, visible ) ) return;
|
|
1030
|
+
this._patchGroupNodes();
|
|
1031
|
+
this._flushBVHEdits();
|
|
1032
|
+
|
|
1033
|
+
} );
|
|
927
1034
|
|
|
928
1035
|
}
|
|
929
1036
|
|
|
@@ -935,8 +1042,30 @@ export class PathTracerStage extends RenderStage {
|
|
|
935
1042
|
|
|
936
1043
|
if ( ! this._meshRefs || ! this._instanceTable ) return;
|
|
937
1044
|
|
|
938
|
-
this.
|
|
939
|
-
|
|
1045
|
+
this._whenTLASResident( () => {
|
|
1046
|
+
|
|
1047
|
+
this._patchVisibilityFromMeshes( this._meshRefs );
|
|
1048
|
+
this._flushBVHEdits();
|
|
1049
|
+
|
|
1050
|
+
} );
|
|
1051
|
+
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* Runs `edit` once the TLAS is in memory: a spilling build of clustered copies leaves it on disk (SceneProcessor
|
|
1056
|
+
* `_spillTLAS`), and the first edit reads it back, where it then stays.
|
|
1057
|
+
* @private
|
|
1058
|
+
*/
|
|
1059
|
+
_whenTLASResident( edit ) {
|
|
1060
|
+
|
|
1061
|
+
const records = this._bvhRecords, table = this._instanceTable;
|
|
1062
|
+
if ( ! records || ! table || records.isResident( 0, table.blasBase ) ) return edit();
|
|
1063
|
+
this._tlasReading ??= records.ensureResident( 0, table.blasBase ).finally( () => {
|
|
1064
|
+
|
|
1065
|
+
this._tlasReading = null;
|
|
1066
|
+
|
|
1067
|
+
} );
|
|
1068
|
+
this._tlasReading.then( edit );
|
|
940
1069
|
|
|
941
1070
|
}
|
|
942
1071
|
|
|
@@ -950,14 +1079,25 @@ export class PathTracerStage extends RenderStage {
|
|
|
950
1079
|
const table = this._instanceTable;
|
|
951
1080
|
if ( ! table || ! table.isSet?.[ meshIndex ] || ! this.bvhStorageAttr ) return false;
|
|
952
1081
|
|
|
953
|
-
const leaf = table.
|
|
1082
|
+
const leaf = table.leafOfPlacement( meshIndex );
|
|
954
1083
|
if ( leaf < 0 ) return false;
|
|
955
1084
|
|
|
956
1085
|
table.visible[ meshIndex ] = visible ? 1 : 0;
|
|
1086
|
+
// A group copy's leaf is shared by its members: it stays in while any of them is visible.
|
|
1087
|
+
const shown = table.entryVisible( table.entryOf( meshIndex ) );
|
|
957
1088
|
|
|
958
1089
|
// A chunked attribute owns no CPU array; write into the chunk that holds this leaf.
|
|
959
|
-
|
|
960
|
-
|
|
1090
|
+
const f = this._bvhRecords ? this._bvhRecords.chunkFor( leaf ) : this.bvhStorageAttr.array;
|
|
1091
|
+
const o = this._bvhRecords ? this._bvhRecords.baseOf( leaf ) : leaf * 16;
|
|
1092
|
+
if ( table.clusterCount ) {
|
|
1093
|
+
|
|
1094
|
+
const views = this._indexViews ??= new WeakMap();
|
|
1095
|
+
let idx = views.get( f );
|
|
1096
|
+
if ( ! idx ) views.set( f, idx = bvhIndexView( f ) );
|
|
1097
|
+
const w = o + table.copyWordSlot( table.tlasLeafIndex[ meshIndex ] );
|
|
1098
|
+
idx[ w ] = shown ? idx[ w ] & ~ CLUSTER_COPY_HIDDEN : ( idx[ w ] | CLUSTER_COPY_HIDDEN ) >>> 0;
|
|
1099
|
+
|
|
1100
|
+
} else f[ o + 2 ] = shown ? 1.0 : 0.0;
|
|
961
1101
|
|
|
962
1102
|
this._dirtyBVHLeaves ??= new Set();
|
|
963
1103
|
this._dirtyBVHLeaves.add( leaf );
|
|
@@ -999,7 +1139,7 @@ export class PathTracerStage extends RenderStage {
|
|
|
999
1139
|
|
|
1000
1140
|
for ( const leaf of this._dirtyBVHLeaves ?? [] ) {
|
|
1001
1141
|
|
|
1002
|
-
|
|
1142
|
+
this._bvhParts.writeRecords( this.renderer.backend, this._bvhRecords, leaf * 16, 16 );
|
|
1003
1143
|
|
|
1004
1144
|
}
|
|
1005
1145
|
|
|
@@ -1094,7 +1234,7 @@ export class PathTracerStage extends RenderStage {
|
|
|
1094
1234
|
|
|
1095
1235
|
if ( bvhData?.chunks?.length > 1 ) this._bvhRecords = bvhData;
|
|
1096
1236
|
if ( ranges ) this.updateBufferRanges( [], ranges );
|
|
1097
|
-
else
|
|
1237
|
+
else this._bvhParts.writeRecords( this.renderer.backend, this._bvhRecords, 0, this._bvhRecords.recordCount * this._bvhRecords.lanesPerRecord );
|
|
1098
1238
|
return;
|
|
1099
1239
|
|
|
1100
1240
|
}
|
|
@@ -1142,7 +1282,7 @@ export class PathTracerStage extends RenderStage {
|
|
|
1142
1282
|
|
|
1143
1283
|
for ( const r of bvhRanges ) {
|
|
1144
1284
|
|
|
1145
|
-
|
|
1285
|
+
this._bvhParts.writeRecords( this.renderer.backend, this._bvhRecords, r.offset, r.count );
|
|
1146
1286
|
|
|
1147
1287
|
}
|
|
1148
1288
|
|
|
@@ -1468,15 +1608,28 @@ export class PathTracerStage extends RenderStage {
|
|
|
1468
1608
|
setBlueNoiseTexture() {}
|
|
1469
1609
|
|
|
1470
1610
|
/**
|
|
1471
|
-
*
|
|
1472
|
-
*
|
|
1473
|
-
|
|
1611
|
+
* Takes the material block into the scene data buffer, in front of the light data, and hands the buffer back as
|
|
1612
|
+
* the material storage. @private
|
|
1613
|
+
*/
|
|
1614
|
+
_adoptMaterials( data ) {
|
|
1615
|
+
|
|
1616
|
+
this._materialFloats = data.length;
|
|
1617
|
+
this._pendingMaterials = data;
|
|
1618
|
+
this._rebuildLightBuffer();
|
|
1619
|
+
return { attr: this.lightStorageAttr, node: this.lightStorageNode };
|
|
1620
|
+
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
/**
|
|
1624
|
+
* Rebuild the scene data buffer: [ materials | lightBVH (LBVH_STRIDE vec4s per node) | emissive (EMISSIVE_STRIDE
|
|
1625
|
+
* vec4s per entry) | bit-trail map ]. One binding for materials and lights: Shade's ten were all spoken for. The
|
|
1626
|
+
* light offsets are within the light data, which starts at `lightVec4Offset`.
|
|
1474
1627
|
*
|
|
1475
1628
|
* The compiled wavefront kernels bind the attribute that existed at kernel-build
|
|
1476
1629
|
* time and NEVER pick up a `lightStorageNode.value` reassignment — so runtime
|
|
1477
|
-
* updates MUST write in-place into the bound attribute
|
|
1478
|
-
*
|
|
1479
|
-
*
|
|
1630
|
+
* updates MUST write in-place into the bound attribute. Only grow-reallocate when the data outgrows capacity, and
|
|
1631
|
+
* flag it so the kernels get rebuilt against the new attribute. Materials are edited in place (MaterialDataManager)
|
|
1632
|
+
* and carried over as they stand; only a new material block replaces them.
|
|
1480
1633
|
* @private
|
|
1481
1634
|
*/
|
|
1482
1635
|
_rebuildLightBuffer() {
|
|
@@ -1484,9 +1637,12 @@ export class PathTracerStage extends RenderStage {
|
|
|
1484
1637
|
const LBVH_STRIDE = 4; // vec4s per LBVH node — must match LightBVHSampling.js
|
|
1485
1638
|
const lbvh = this._lbvhDataCache;
|
|
1486
1639
|
const emis = this._emissiveDataCache;
|
|
1640
|
+
const held = this.lightStorageAttr?.array;
|
|
1641
|
+
const matLen = this._materialFloats ?? 0;
|
|
1642
|
+
const materials = this._pendingMaterials ?? ( held && matLen ? held.subarray( 0, matLen ) : null );
|
|
1643
|
+
this._pendingMaterials = null;
|
|
1487
1644
|
// The bit-trail map (4 B a triangle) lives only in the light buffer: without a new one, the
|
|
1488
1645
|
// current one is carried over from there rather than kept as a second copy.
|
|
1489
|
-
const held = this.lightStorageAttr?.array;
|
|
1490
1646
|
const trail = this._bitTrailMapCache
|
|
1491
1647
|
?? ( held && this._bitTrailLength ? held.slice( this._bitTrailOffset, this._bitTrailOffset + this._bitTrailLength ) : null );
|
|
1492
1648
|
this._bitTrailMapCache = null;
|
|
@@ -1496,28 +1652,36 @@ export class PathTracerStage extends RenderStage {
|
|
|
1496
1652
|
const trailPadded = trail ? Math.ceil( trail.length / 4 ) * 4 : 0;
|
|
1497
1653
|
|
|
1498
1654
|
// Ensure at least a minimal non-empty buffer so GPU allocation remains valid.
|
|
1499
|
-
const totalLen = Math.max( lbvhLen + emisLen + trailPadded, 4 );
|
|
1655
|
+
const totalLen = Math.max( matLen + lbvhLen + emisLen + trailPadded, 4 );
|
|
1656
|
+
const reuse = this.lightStorageAttr && totalLen <= this.lightStorageAttr.array.length && this._builtMaterialFloats === matLen;
|
|
1500
1657
|
|
|
1501
|
-
if (
|
|
1658
|
+
if ( reuse ) {
|
|
1502
1659
|
|
|
1503
1660
|
// In-place update of the bound attribute. Stale floats past the new data
|
|
1504
1661
|
// are unreachable — all reads are gated by the count/offset uniforms.
|
|
1505
1662
|
const arr = this.lightStorageAttr.array;
|
|
1506
|
-
|
|
1507
|
-
if (
|
|
1508
|
-
if (
|
|
1663
|
+
const newMaterials = materials && materials.buffer !== arr.buffer;
|
|
1664
|
+
if ( newMaterials ) arr.set( materials, 0 );
|
|
1665
|
+
if ( lbvh ) arr.set( lbvh, matLen );
|
|
1666
|
+
if ( emis ) arr.set( emis, matLen + lbvhLen );
|
|
1667
|
+
if ( trail ) arr.set( trail, matLen + lbvhLen + emisLen );
|
|
1668
|
+
// A range, not the whole buffer: a material edit's own range may be pending beside it (pitfall 13).
|
|
1669
|
+
const from = newMaterials ? 0 : matLen;
|
|
1670
|
+
this.lightStorageAttr.addUpdateRange( from, totalLen - from );
|
|
1509
1671
|
this.lightStorageAttr.needsUpdate = true;
|
|
1510
1672
|
|
|
1511
1673
|
} else {
|
|
1512
1674
|
|
|
1513
1675
|
const combined = new Float32Array( totalLen );
|
|
1514
|
-
if (
|
|
1515
|
-
if (
|
|
1516
|
-
if (
|
|
1676
|
+
if ( materials ) combined.set( materials, 0 );
|
|
1677
|
+
if ( lbvh ) combined.set( lbvh, matLen );
|
|
1678
|
+
if ( emis ) combined.set( emis, matLen + lbvhLen );
|
|
1679
|
+
if ( trail ) combined.set( trail, matLen + lbvhLen + emisLen );
|
|
1517
1680
|
|
|
1518
1681
|
this.lightStorageAttr = new StorageInstancedBufferAttribute( combined, 4 );
|
|
1519
1682
|
this.lightStorageNode.value = this.lightStorageAttr;
|
|
1520
1683
|
this.lightStorageNode.bufferCount = combined.length / 4;
|
|
1684
|
+
if ( this.materialData && this.materialData.materialStorageNode === this.lightStorageNode ) this.materialData.materialStorageAttr = this.lightStorageAttr;
|
|
1521
1685
|
|
|
1522
1686
|
// Already-compiled kernels still bind the old attribute — request a rebuild.
|
|
1523
1687
|
// (During scene load this is a no-op: setupMaterial rebuilds kernels anyway.)
|
|
@@ -1525,16 +1689,29 @@ export class PathTracerStage extends RenderStage {
|
|
|
1525
1689
|
|
|
1526
1690
|
}
|
|
1527
1691
|
|
|
1692
|
+
this._builtMaterialFloats = matLen;
|
|
1693
|
+
this.lightVec4Offset.value = matLen / 4;
|
|
1528
1694
|
// Offset (in vec4 elements) where emissive data starts.
|
|
1529
1695
|
this.emissiveVec4Offset.value = ( this.lightBVHNodeCount.value || 0 ) * LBVH_STRIDE;
|
|
1530
1696
|
// Offset (in vec4 elements) where the bit-trail map starts (lbvhLen + emisLen are float
|
|
1531
1697
|
// counts, both multiples of 4, so this divides cleanly).
|
|
1532
1698
|
this.reverseMapVec4Offset.value = ( lbvhLen + emisLen ) / 4;
|
|
1533
|
-
this._bitTrailOffset = lbvhLen + emisLen;
|
|
1699
|
+
this._bitTrailOffset = matLen + lbvhLen + emisLen;
|
|
1534
1700
|
this._bitTrailLength = trail ? trail.length : 0;
|
|
1535
1701
|
|
|
1536
1702
|
}
|
|
1537
1703
|
|
|
1704
|
+
/**
|
|
1705
|
+
* The light data as kernels read it: the scene data buffer behind the materials. One binding with the materials,
|
|
1706
|
+
* whichever of the two a kernel reads.
|
|
1707
|
+
*/
|
|
1708
|
+
get lightDataNode() {
|
|
1709
|
+
|
|
1710
|
+
const node = this.lightStorageNode, base = this.lightVec4Offset;
|
|
1711
|
+
return { isLightData: true, node, element: ( index ) => node.element( base.add( index ) ) };
|
|
1712
|
+
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1538
1715
|
setEmissiveTriangleData( emissiveData, count, totalPower = 0, bitTrailMap = null ) {
|
|
1539
1716
|
|
|
1540
1717
|
if ( ! emissiveData ) return;
|