rayzee 11.2.0 → 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 +62 -33
- 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-DFGbnmLb.js → NeuralRetouch-DiBH63kz.js} +1 -1
- package/dist/{NeuralSuperRes-BtXvyJQl.js → NeuralSuperRes-CZNQP13W.js} +1 -1
- package/dist/{PathTracer-BHXGJtPN.js → PathTracer-BXCCVrPK.js} +4371 -4612
- package/dist/{PhysicalSky-DN6u8UX4.js → PhysicalSky-DqmaS0lb.js} +2 -2
- package/dist/{RayzeeRenderer-Dw1x8mVL.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/EngineDefaults.js +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 +168 -27
- 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-eNlu0_6F.js +0 -5198
- package/dist/BidirectionalIntegrator-D1-6UySB.js +0 -946
- package/dist/SolarLimb-CPaeZVwz.js +0 -721
- package/dist/sceneCachePolicy-DA7iGoL4.js +0 -485
|
@@ -4,6 +4,16 @@ const PACK_BYTES = 32 * 1024 * 1024;
|
|
|
4
4
|
const OWN_WRITE_BYTES = PACK_BYTES / 4;
|
|
5
5
|
const READ_WINDOW = 64 * 1024 * 1024;
|
|
6
6
|
const EMPTY = new Map();
|
|
7
|
+
// Buffers a restore made: nothing else holds them, so once on disk again they are let go at once rather than at the
|
|
8
|
+
// next major collection, which an idle page may not reach for minutes.
|
|
9
|
+
const RESTORED = new WeakSet();
|
|
10
|
+
|
|
11
|
+
function release( buffer ) {
|
|
12
|
+
|
|
13
|
+
RESTORED.delete( buffer );
|
|
14
|
+
buffer.transfer?.( 0 );
|
|
15
|
+
|
|
16
|
+
}
|
|
7
17
|
|
|
8
18
|
function empty( Type ) {
|
|
9
19
|
|
|
@@ -61,13 +71,25 @@ export class GeometrySpill {
|
|
|
61
71
|
|
|
62
72
|
}
|
|
63
73
|
|
|
64
|
-
/** Moves a geometry's arrays to disk, leaving empty arrays of the same type in their place. */
|
|
74
|
+
/** Moves a geometry's arrays to disk, leaving empty arrays of the same type in their place; its bounds stay. */
|
|
65
75
|
add( geometry ) {
|
|
66
76
|
|
|
67
|
-
|
|
77
|
+
if ( ! geometry.boundingBox ) geometry.computeBoundingBox();
|
|
78
|
+
if ( ! geometry.boundingSphere ) geometry.computeBoundingSphere();
|
|
79
|
+
|
|
68
80
|
const attributes = Object.values( geometry.attributes );
|
|
69
81
|
if ( geometry.index ) attributes.push( geometry.index );
|
|
82
|
+
this.addAttributes( attributes );
|
|
83
|
+
|
|
84
|
+
// What the bounds were with the arrays in place: anything computed from the empty ones must not outlive the restore.
|
|
85
|
+
this._geometries.push( { geometry, box: geometry.boundingBox.clone(), sphere: geometry.boundingSphere.clone() } );
|
|
86
|
+
|
|
87
|
+
}
|
|
70
88
|
|
|
89
|
+
/** Moves attributes' arrays to disk, each array once however many attributes share it. */
|
|
90
|
+
addAttributes( attributes ) {
|
|
91
|
+
|
|
92
|
+
const byArray = new Map();
|
|
71
93
|
for ( const attribute of attributes ) {
|
|
72
94
|
|
|
73
95
|
const array = attribute.array;
|
|
@@ -79,9 +101,6 @@ export class GeometrySpill {
|
|
|
79
101
|
|
|
80
102
|
}
|
|
81
103
|
|
|
82
|
-
// Bounds computed from an empty array would outlive the restore.
|
|
83
|
-
this._geometries.push( { geometry, box: geometry.boundingBox !== null, sphere: geometry.boundingSphere !== null } );
|
|
84
|
-
|
|
85
104
|
}
|
|
86
105
|
|
|
87
106
|
/** @private */
|
|
@@ -116,8 +135,9 @@ export class GeometrySpill {
|
|
|
116
135
|
record.at = this._packAt + this._packUsed;
|
|
117
136
|
record.pack = this._pack;
|
|
118
137
|
record.offset = this._packUsed;
|
|
119
|
-
this._packUsed +=
|
|
138
|
+
this._packUsed += record.byteLength;
|
|
120
139
|
this._packRecords.push( record );
|
|
140
|
+
if ( RESTORED.has( array.buffer ) ) release( array.buffer );
|
|
121
141
|
return record;
|
|
122
142
|
|
|
123
143
|
}
|
|
@@ -126,26 +146,35 @@ export class GeometrySpill {
|
|
|
126
146
|
_flushPack() {
|
|
127
147
|
|
|
128
148
|
if ( ! this._pack ) return;
|
|
129
|
-
this._queue( this._packAt, this._pack.subarray( 0, this._packUsed ), this._packRecords );
|
|
149
|
+
this._queue( this._packAt, this._pack.subarray( 0, this._packUsed ), this._packRecords, true );
|
|
130
150
|
this._pack = null;
|
|
131
151
|
this._packRecords = [];
|
|
132
152
|
|
|
133
153
|
}
|
|
134
154
|
|
|
135
|
-
/** @private */
|
|
136
|
-
_queue( at, bytes, records ) {
|
|
155
|
+
/** `own`: the buffer is this spill's (a pack), let go once written. @private */
|
|
156
|
+
_queue( at, bytes, records, own = false ) {
|
|
137
157
|
|
|
138
|
-
|
|
158
|
+
const length = bytes.byteLength;
|
|
159
|
+
this._queued += length;
|
|
139
160
|
this._chain = this._chain
|
|
140
161
|
.then( async () => {
|
|
141
162
|
|
|
142
163
|
if ( this.error ) return;
|
|
143
164
|
await this._store.writeAt( at, bytes );
|
|
144
|
-
for ( const record of records )
|
|
165
|
+
for ( const record of records ) {
|
|
166
|
+
|
|
167
|
+
const buffer = record.array?.buffer;
|
|
168
|
+
record.array = record.pack = null;
|
|
169
|
+
if ( RESTORED.has( buffer ) ) release( buffer );
|
|
170
|
+
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if ( own ) bytes.buffer.transfer?.( 0 );
|
|
145
174
|
|
|
146
175
|
} )
|
|
147
176
|
.catch( error => void ( this.error ??= error ) )
|
|
148
|
-
.finally( () => void ( this._queued -=
|
|
177
|
+
.finally( () => void ( this._queued -= length ) );
|
|
149
178
|
|
|
150
179
|
}
|
|
151
180
|
|
|
@@ -176,6 +205,7 @@ export class GeometrySpill {
|
|
|
176
205
|
|
|
177
206
|
if ( ! window || record.at + record.byteLength > windowAt + window.byteLength ) {
|
|
178
207
|
|
|
208
|
+
if ( window ) release( window );
|
|
179
209
|
windowAt = record.at;
|
|
180
210
|
window = await this._store.readAt( record.at, READ_WINDOW );
|
|
181
211
|
|
|
@@ -186,14 +216,16 @@ export class GeometrySpill {
|
|
|
186
216
|
}
|
|
187
217
|
|
|
188
218
|
if ( array.byteLength !== record.byteLength ) throw new Error( 'geometry spill: an array came back short' );
|
|
219
|
+
if ( array !== record.array ) RESTORED.add( array.buffer );
|
|
189
220
|
for ( const attribute of record.attributes ) attribute.array = array;
|
|
190
221
|
|
|
191
222
|
}
|
|
192
223
|
|
|
224
|
+
if ( window ) release( window );
|
|
193
225
|
for ( const { geometry, box, sphere } of this._geometries ) {
|
|
194
226
|
|
|
195
|
-
|
|
196
|
-
|
|
227
|
+
geometry.boundingBox = box;
|
|
228
|
+
geometry.boundingSphere = sphere;
|
|
197
229
|
|
|
198
230
|
}
|
|
199
231
|
|
|
@@ -192,6 +192,8 @@ export function encodeSceneGraph( root, { environment = null, animations = [], s
|
|
|
192
192
|
|
|
193
193
|
const sections = new SectionWriter();
|
|
194
194
|
const nodes = [];
|
|
195
|
+
// Instanced meshes sharing one matrix attribute are placed as one object; stored once, they come back shared.
|
|
196
|
+
const instanceSections = new Map();
|
|
195
197
|
const geometries = new Map();
|
|
196
198
|
const materials = new Map();
|
|
197
199
|
const textures = new Map();
|
|
@@ -228,7 +230,9 @@ export function encodeSceneGraph( root, { environment = null, animations = [], s
|
|
|
228
230
|
if ( object.isInstancedMesh ) {
|
|
229
231
|
|
|
230
232
|
node.count = object.count;
|
|
231
|
-
|
|
233
|
+
let matrices = instanceSections.get( object.instanceMatrix );
|
|
234
|
+
if ( ! matrices ) instanceSections.set( object.instanceMatrix, matrices = sections.add( object.instanceMatrix.array ) );
|
|
235
|
+
node.instanceMatrix = matrices;
|
|
232
236
|
node.instanceColor = object.instanceColor ? sections.add( object.instanceColor.array ) : null;
|
|
233
237
|
|
|
234
238
|
}
|
|
@@ -355,9 +359,16 @@ async function readSections( file, sections ) {
|
|
|
355
359
|
function sectionsOf( manifest ) {
|
|
356
360
|
|
|
357
361
|
const all = [];
|
|
362
|
+
const seen = new Set();
|
|
358
363
|
for ( const node of manifest.nodes ) {
|
|
359
364
|
|
|
360
|
-
if ( node.instanceMatrix
|
|
365
|
+
if ( node.instanceMatrix && ! seen.has( node.instanceMatrix.offset ) ) {
|
|
366
|
+
|
|
367
|
+
seen.add( node.instanceMatrix.offset );
|
|
368
|
+
all.push( node.instanceMatrix );
|
|
369
|
+
|
|
370
|
+
}
|
|
371
|
+
|
|
361
372
|
if ( node.instanceColor ) all.push( node.instanceColor );
|
|
362
373
|
|
|
363
374
|
}
|
|
@@ -408,6 +419,7 @@ export async function decodeSceneGraph( manifest, data, { loadTexture } ) {
|
|
|
408
419
|
|
|
409
420
|
const arrays = await readSections( data, sectionsOf( manifest ) );
|
|
410
421
|
const section = ( record ) => arrays.get( record.offset );
|
|
422
|
+
const instanceMatrices = new Map();
|
|
411
423
|
|
|
412
424
|
const textures = {};
|
|
413
425
|
for ( const record of manifest.textures ) {
|
|
@@ -476,7 +488,9 @@ export async function decodeSceneGraph( manifest, data, { loadTexture } ) {
|
|
|
476
488
|
|
|
477
489
|
// Built for one instance, then given the stored matrices: no second full-size array.
|
|
478
490
|
object = new InstancedMesh( geometries[ node.geometry ], material, 1 );
|
|
479
|
-
|
|
491
|
+
let matrices = instanceMatrices.get( node.instanceMatrix.offset );
|
|
492
|
+
if ( ! matrices ) instanceMatrices.set( node.instanceMatrix.offset, matrices = new InstancedBufferAttribute( section( node.instanceMatrix ), 16 ) );
|
|
493
|
+
object.instanceMatrix = matrices;
|
|
480
494
|
object.count = node.count;
|
|
481
495
|
if ( node.instanceColor ) object.instanceColor = new InstancedBufferAttribute( section( node.instanceColor ), 3 );
|
|
482
496
|
break;
|
|
@@ -99,6 +99,15 @@ export class SpillStore {
|
|
|
99
99
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/** Reads chunk `k` into `target` (bytes) and lets the read buffer go at once, not at the next major collection. */
|
|
103
|
+
async readInto( k, target ) {
|
|
104
|
+
|
|
105
|
+
const buffer = await this.read( k, target.byteLength );
|
|
106
|
+
target.set( new Uint8Array( buffer ) );
|
|
107
|
+
buffer.transfer?.( 0 );
|
|
108
|
+
|
|
109
|
+
}
|
|
110
|
+
|
|
102
111
|
async dispose() {
|
|
103
112
|
|
|
104
113
|
if ( this._writer ) await this._writer.abort();
|
package/src/TSL/BVHTraversal.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
If,
|
|
11
11
|
Loop,
|
|
12
12
|
Break,
|
|
13
|
+
Continue,
|
|
13
14
|
select,
|
|
14
15
|
abs,
|
|
15
16
|
sign,
|
|
@@ -27,7 +28,7 @@ import {
|
|
|
27
28
|
|
|
28
29
|
import {
|
|
29
30
|
TRI_MATERIAL_MASK, TRI_SIDE_SHIFT, BVH_LEAF_MARKERS, BVH_MAX_INDEX, BVH_FOLDED_LEAF_MAX,
|
|
30
|
-
TLAS_LEAF_IDENTITY,
|
|
31
|
+
TLAS_LEAF_IDENTITY, COPY_ENTRY, CLUSTER_FIRST_MASK, CLUSTER_COUNT_SHIFT, CLUSTER_SIZE, CLUSTER_ROOT_MASK, CLUSTER_COPY_HIDDEN,
|
|
31
32
|
} from '../Processor/BufferLayout.js';
|
|
32
33
|
import { HitInfo } from './Struct.js';
|
|
33
34
|
import {
|
|
@@ -83,6 +84,61 @@ const toObjectDir = ( rows, d ) => vec3(
|
|
|
83
84
|
rows[ 2 ].xyz.dot( d )
|
|
84
85
|
);
|
|
85
86
|
|
|
87
|
+
// Copy clusters (BufferLayout CLUSTER_LEAF): a leaf pushes a COPY_ENTRY for each copy whose byte box the ray reaches,
|
|
88
|
+
// farthest first so the nearest is entered first, as an inner node orders its children; popping one moves the ray into
|
|
89
|
+
// that copy and pushes its object's root.
|
|
90
|
+
|
|
91
|
+
// Compare-swaps sorting up to four ascending.
|
|
92
|
+
const SORT_NETWORKS = [[], [], [[ 0, 1 ]], [[ 0, 1 ], [ 1, 2 ], [ 0, 1 ]], [[ 0, 1 ], [ 2, 3 ], [ 0, 2 ], [ 1, 3 ], [ 1, 2 ]]];
|
|
93
|
+
|
|
94
|
+
const pushHitCopies = ( bvhBuffer, nodeIndex, nodeData0, origin, invDir, stack, stackPtr, limit ) => {
|
|
95
|
+
|
|
96
|
+
const d1 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 1 ), int( BVH_STRIDE ) );
|
|
97
|
+
const d2 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 2 ), int( BVH_STRIDE ) );
|
|
98
|
+
const base = vec3( nodeData0.z, d1.x, d1.y ).toVar();
|
|
99
|
+
const exps = floatBitsToUint( nodeData0.y ).toVar();
|
|
100
|
+
const stepOf = ( a ) => uintBitsToFloat( exps.shiftRight( uint( a * 8 ) ).bitAnd( uint( 255 ) ).shiftLeft( uint( 23 ) ) );
|
|
101
|
+
const step = vec3( stepOf( 0 ), stepOf( 1 ), stepOf( 2 ) ).toVar();
|
|
102
|
+
const lo = [ floatBitsToUint( d1.z ).toVar(), floatBitsToUint( d1.w ).toVar(), floatBitsToUint( d2.x ).toVar() ];
|
|
103
|
+
const hi = [ floatBitsToUint( d2.y ).toVar(), floatBitsToUint( d2.z ).toVar(), floatBitsToUint( d2.w ).toVar() ];
|
|
104
|
+
const count = int( floatBitsToUint( nodeData0.x ).shiftRight( uint( CLUSTER_COUNT_SHIFT ) ).bitAnd( uint( 3 ) ) ).add( 1 ).toVar();
|
|
105
|
+
const byte = ( word, k ) => float( word.shiftRight( uint( k * 8 ) ).bitAnd( uint( 255 ) ) );
|
|
106
|
+
|
|
107
|
+
const dist = [], slot = [];
|
|
108
|
+
for ( let k = 0; k < CLUSTER_SIZE; k ++ ) {
|
|
109
|
+
|
|
110
|
+
const boxMin = base.add( vec3( byte( lo[ 0 ], k ), byte( lo[ 1 ], k ), byte( lo[ 2 ], k ) ).mul( step ) );
|
|
111
|
+
const boxMax = base.add( vec3( byte( hi[ 0 ], k ), byte( hi[ 1 ], k ), byte( hi[ 2 ], k ) ).mul( step ) );
|
|
112
|
+
const d = fastRayAABBDst( { rayOrigin: origin, invDir, boxMin, boxMax } );
|
|
113
|
+
dist.push( select( int( k ).lessThan( count ).and( d.lessThan( limit ) ), d, float( 1e30 ) ).toVar() );
|
|
114
|
+
slot.push( int( k ).toVar() );
|
|
115
|
+
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for ( const [ a, b ] of SORT_NETWORKS[ CLUSTER_SIZE ] ) {
|
|
119
|
+
|
|
120
|
+
If( dist[ b ].lessThan( dist[ a ] ), () => {
|
|
121
|
+
|
|
122
|
+
const td = float( dist[ a ] ).toVar(), ts = int( slot[ a ] ).toVar();
|
|
123
|
+
dist[ a ].assign( dist[ b ] ); slot[ a ].assign( slot[ b ] );
|
|
124
|
+
dist[ b ].assign( td ); slot[ b ].assign( ts );
|
|
125
|
+
|
|
126
|
+
} );
|
|
127
|
+
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for ( let k = CLUSTER_SIZE - 1; k >= 0; k -- ) {
|
|
131
|
+
|
|
132
|
+
If( dist[ k ].lessThan( limit ).and( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ) ), () => {
|
|
133
|
+
|
|
134
|
+
stack.element( stackPtr ).assign( int( uint( COPY_ENTRY ).bitOr( uint( nodeIndex ).shiftLeft( uint( 2 ) ) ).bitOr( uint( slot[ k ] ) ) ) );
|
|
135
|
+
stackPtr.addAssign( 1 );
|
|
136
|
+
|
|
137
|
+
} );
|
|
138
|
+
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
};
|
|
86
142
|
|
|
87
143
|
// ================================================================================
|
|
88
144
|
// RAY INTERSECTION HELPERS (inlined for BVH traversal performance)
|
|
@@ -301,6 +357,49 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
|
|
|
301
357
|
const instExit = int( 0 ).toVar();
|
|
302
358
|
const hitInstLeaf = int( - 1 ).toVar();
|
|
303
359
|
|
|
360
|
+
// Built over copy clusters: the copies' transforms are records from vec4 `copyRecordBase` (BufferLayout).
|
|
361
|
+
const clustered = bvhBuffer.value?.copyRecordBase !== undefined;
|
|
362
|
+
const enterCopy = ( entry ) => {
|
|
363
|
+
|
|
364
|
+
const leaf = int( uint( entry ).bitAnd( uint( ~ COPY_ENTRY >>> 0 ) ).shiftRight( uint( 2 ) ) );
|
|
365
|
+
const k = uint( entry ).bitAnd( uint( 3 ) ).toVar();
|
|
366
|
+
const head = getDatafromStorageBuffer( bvhBuffer, leaf, int( 0 ), int( BVH_STRIDE ) );
|
|
367
|
+
const words = floatBitsToUint( getDatafromStorageBuffer( bvhBuffer, leaf, int( 3 ), int( BVH_STRIDE ) ) ).toVar();
|
|
368
|
+
const word = select( k.equal( uint( 0 ) ), words.x, select( k.equal( uint( 1 ) ), words.y, select( k.equal( uint( 2 ) ), words.z, words.w ) ) ).toVar();
|
|
369
|
+
|
|
370
|
+
If( word.bitAnd( uint( CLUSTER_COPY_HIDDEN ) ).equal( uint( 0 ) ), () => {
|
|
371
|
+
|
|
372
|
+
If( word.bitAnd( uint( TLAS_LEAF_IDENTITY ) ).equal( uint( 0 ) ), () => {
|
|
373
|
+
|
|
374
|
+
const record = int( floatBitsToUint( head.x ).bitAnd( uint( CLUSTER_FIRST_MASK ) ).add( k ) ).toVar();
|
|
375
|
+
const rows = instanceRows( bvhBuffer, record );
|
|
376
|
+
const localDir = toObjectDir( rows, worldDirection ).toVar();
|
|
377
|
+
|
|
378
|
+
rayOrigin.assign( toObjectPoint( rows, worldOrigin ) );
|
|
379
|
+
rayDirection.assign( localDir );
|
|
380
|
+
const localInv = buildInvDir( localDir ).toVar();
|
|
381
|
+
invDir.assign( localInv );
|
|
382
|
+
woopParams.assign( computeWoopFromInvDir( { rayDir: localDir, invDir: localInv } ) );
|
|
383
|
+
instLeaf.assign( record );
|
|
384
|
+
instExit.assign( stackPtr );
|
|
385
|
+
|
|
386
|
+
} ).Else( () => {
|
|
387
|
+
|
|
388
|
+
rayOrigin.assign( worldOrigin );
|
|
389
|
+
rayDirection.assign( worldDirection );
|
|
390
|
+
invDir.assign( worldInvDir );
|
|
391
|
+
woopParams.assign( worldWoop );
|
|
392
|
+
instLeaf.assign( int( - 1 ) );
|
|
393
|
+
|
|
394
|
+
} );
|
|
395
|
+
|
|
396
|
+
stack.element( stackPtr ).assign( int( word.bitAnd( uint( CLUSTER_ROOT_MASK ) ) ) );
|
|
397
|
+
stackPtr.addAssign( 1 );
|
|
398
|
+
|
|
399
|
+
} );
|
|
400
|
+
|
|
401
|
+
};
|
|
402
|
+
|
|
304
403
|
// Emitted once per leaf kind, in its own branch: hoisted below both, the range stayed live
|
|
305
404
|
// through the inner-node path and cost 9–13 % of traversal time.
|
|
306
405
|
const testLeaf = ( first, count ) => {
|
|
@@ -403,7 +502,11 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
|
|
|
403
502
|
|
|
404
503
|
testLeaf( floatBitsToUint( nodeData0.x ), floatBitsToUint( nodeData0.y ) );
|
|
405
504
|
|
|
406
|
-
} ).Else( () => {
|
|
505
|
+
} ).Else( clustered ? () => {
|
|
506
|
+
|
|
507
|
+
pushHitCopies( bvhBuffer, nodeIndex, nodeData0, worldOrigin, worldInvDir, stack, stackPtr, closestHit.dst );
|
|
508
|
+
|
|
509
|
+
} : () => {
|
|
407
510
|
|
|
408
511
|
// BLAS-pointer leaf — enter the instance if the mesh is visible.
|
|
409
512
|
// nodeData0: [blasRootNodeIndex, meshIndex, visibility, -2]; slots 4..15 hold
|
|
@@ -487,9 +590,22 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
|
|
|
487
590
|
|
|
488
591
|
};
|
|
489
592
|
|
|
593
|
+
// A copy continues the loop: the visit nested in an Else overflowed the JS stack building the island's kernels.
|
|
594
|
+
const dispatch = clustered ? () => {
|
|
595
|
+
|
|
596
|
+
If( entry.greaterThanEqual( int( COPY_ENTRY ) ), () => {
|
|
597
|
+
|
|
598
|
+
enterCopy( entry );
|
|
599
|
+
Continue();
|
|
600
|
+
|
|
601
|
+
} );
|
|
602
|
+
visitNode();
|
|
603
|
+
|
|
604
|
+
} : visitNode;
|
|
605
|
+
|
|
490
606
|
// Only a folded BVH has negative entries: a leaf folded into its parent, tested with no fetch.
|
|
491
|
-
if ( folded ) If( entry.lessThan( int( 0 ) ), () => testLeaf( foldedFirst( entry ), foldedCount( entry ) ) ).Else(
|
|
492
|
-
else
|
|
607
|
+
if ( folded ) If( entry.lessThan( int( 0 ) ), () => testLeaf( foldedFirst( entry ), foldedCount( entry ) ) ).Else( dispatch );
|
|
608
|
+
else dispatch();
|
|
493
609
|
|
|
494
610
|
} );
|
|
495
611
|
|
|
@@ -583,6 +699,49 @@ const makeTraverseBVHShadow = ( cameraCulled ) => Fn( ( [
|
|
|
583
699
|
const instExit = int( 0 ).toVar();
|
|
584
700
|
const blocked = tslBool( false ).toVar();
|
|
585
701
|
|
|
702
|
+
// Built over copy clusters: the copies' transforms are records from vec4 `copyRecordBase` (BufferLayout).
|
|
703
|
+
const clustered = bvhBuffer.value?.copyRecordBase !== undefined;
|
|
704
|
+
const enterCopy = ( entry ) => {
|
|
705
|
+
|
|
706
|
+
const leaf = int( uint( entry ).bitAnd( uint( ~ COPY_ENTRY >>> 0 ) ).shiftRight( uint( 2 ) ) );
|
|
707
|
+
const k = uint( entry ).bitAnd( uint( 3 ) ).toVar();
|
|
708
|
+
const head = getDatafromStorageBuffer( bvhBuffer, leaf, int( 0 ), int( BVH_STRIDE ) );
|
|
709
|
+
const words = floatBitsToUint( getDatafromStorageBuffer( bvhBuffer, leaf, int( 3 ), int( BVH_STRIDE ) ) ).toVar();
|
|
710
|
+
const word = select( k.equal( uint( 0 ) ), words.x, select( k.equal( uint( 1 ) ), words.y, select( k.equal( uint( 2 ) ), words.z, words.w ) ) ).toVar();
|
|
711
|
+
|
|
712
|
+
If( word.bitAnd( uint( CLUSTER_COPY_HIDDEN ) ).equal( uint( 0 ) ), () => {
|
|
713
|
+
|
|
714
|
+
If( word.bitAnd( uint( TLAS_LEAF_IDENTITY ) ).equal( uint( 0 ) ), () => {
|
|
715
|
+
|
|
716
|
+
const record = int( floatBitsToUint( head.x ).bitAnd( uint( CLUSTER_FIRST_MASK ) ).add( k ) ).toVar();
|
|
717
|
+
const rows = instanceRows( bvhBuffer, record );
|
|
718
|
+
const localDir = toObjectDir( rows, worldDirection ).toVar();
|
|
719
|
+
|
|
720
|
+
rayOrigin.assign( toObjectPoint( rows, worldOrigin ) );
|
|
721
|
+
rayDirection.assign( localDir );
|
|
722
|
+
const localInv = buildInvDir( localDir ).toVar();
|
|
723
|
+
invDir.assign( localInv );
|
|
724
|
+
woopParams.assign( computeWoopFromInvDir( { rayDir: localDir, invDir: localInv } ) );
|
|
725
|
+
instLeaf.assign( record );
|
|
726
|
+
instExit.assign( stackPtr );
|
|
727
|
+
|
|
728
|
+
} ).Else( () => {
|
|
729
|
+
|
|
730
|
+
rayOrigin.assign( worldOrigin );
|
|
731
|
+
rayDirection.assign( worldDirection );
|
|
732
|
+
invDir.assign( worldInvDir );
|
|
733
|
+
woopParams.assign( worldWoop );
|
|
734
|
+
instLeaf.assign( int( - 1 ) );
|
|
735
|
+
|
|
736
|
+
} );
|
|
737
|
+
|
|
738
|
+
stack.element( stackPtr ).assign( int( word.bitAnd( uint( CLUSTER_ROOT_MASK ) ) ) );
|
|
739
|
+
stackPtr.addAssign( 1 );
|
|
740
|
+
|
|
741
|
+
} );
|
|
742
|
+
|
|
743
|
+
};
|
|
744
|
+
|
|
586
745
|
const testLeaf = ( first, count ) => {
|
|
587
746
|
|
|
588
747
|
const triStart = int( first ).toVar();
|
|
@@ -672,7 +831,11 @@ const makeTraverseBVHShadow = ( cameraCulled ) => Fn( ( [
|
|
|
672
831
|
|
|
673
832
|
testLeaf( floatBitsToUint( nodeData0.x ), floatBitsToUint( nodeData0.y ) );
|
|
674
833
|
|
|
675
|
-
} ).Else( () => {
|
|
834
|
+
} ).Else( clustered ? () => {
|
|
835
|
+
|
|
836
|
+
pushHitCopies( bvhBuffer, nodeIndex, nodeData0, worldOrigin, worldInvDir, stack, stackPtr, closestHit.dst );
|
|
837
|
+
|
|
838
|
+
} : () => {
|
|
676
839
|
|
|
677
840
|
// BLAS-pointer leaf — enter the instance if the mesh is visible.
|
|
678
841
|
If( nodeData0.z.greaterThan( 0.5 ).and( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ) ), () => {
|
|
@@ -755,9 +918,22 @@ const makeTraverseBVHShadow = ( cameraCulled ) => Fn( ( [
|
|
|
755
918
|
|
|
756
919
|
};
|
|
757
920
|
|
|
921
|
+
// A copy continues the loop: the visit nested in an Else overflowed the JS stack building the island's kernels.
|
|
922
|
+
const dispatch = clustered ? () => {
|
|
923
|
+
|
|
924
|
+
If( entry.greaterThanEqual( int( COPY_ENTRY ) ), () => {
|
|
925
|
+
|
|
926
|
+
enterCopy( entry );
|
|
927
|
+
Continue();
|
|
928
|
+
|
|
929
|
+
} );
|
|
930
|
+
visitNode();
|
|
931
|
+
|
|
932
|
+
} : visitNode;
|
|
933
|
+
|
|
758
934
|
// Only a folded BVH has negative entries: a leaf folded into its parent, tested with no fetch.
|
|
759
|
-
if ( folded ) If( entry.lessThan( int( 0 ) ), () => testLeaf( foldedFirst( entry ), foldedCount( entry ) ) ).Else(
|
|
760
|
-
else
|
|
935
|
+
if ( folded ) If( entry.lessThan( int( 0 ) ), () => testLeaf( foldedFirst( entry ), foldedCount( entry ) ) ).Else( dispatch );
|
|
936
|
+
else dispatch();
|
|
761
937
|
|
|
762
938
|
} );
|
|
763
939
|
|
package/src/TSL/Common.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fn, wgslFn, float, vec2, vec3, vec4, int, uint, mat3, If, max, dot, clamp, select, bool as tslBool } from 'three/tsl';
|
|
1
|
+
import { Fn, wgslFn, float, vec2, vec3, vec4, uvec2, uvec4, int, uint, mat3, If, max, dot, clamp, select, bool as tslBool } from 'three/tsl';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
AnisoFrame,
|
|
@@ -322,12 +322,41 @@ export const classifyMaterial = Fn( ( [ metalness, roughness, transmission, clea
|
|
|
322
322
|
|
|
323
323
|
// Storage buffer access — flat 1D indexing (WebGPU native)
|
|
324
324
|
// No 2D coordinate math needed: directly indexes into the buffer
|
|
325
|
-
|
|
325
|
+
/**
|
|
326
|
+
* One storage array split over several buffers, for a tree or triangle store past maxStorageBufferBindingSize: element
|
|
327
|
+
* e lives in part ⌊e / perPart⌋. A single node needs no split and is returned as it is, so a scene that fits one
|
|
328
|
+
* buffer compiles exactly the code it always did.
|
|
329
|
+
* @param {Array} parts - storage nodes, all but the last `perPart` vec4 elements long
|
|
330
|
+
* @param {number} perPart
|
|
331
|
+
* @param {'vec4'|'uvec4'} type
|
|
332
|
+
*/
|
|
333
|
+
export function splitStorage( parts, perPart, type ) {
|
|
326
334
|
|
|
327
|
-
|
|
328
|
-
return buffer.element( elementIndex );
|
|
335
|
+
return parts.length === 1 ? parts[ 0 ] : { isSplitStorage: true, parts, perPart, type, value: parts[ 0 ].value };
|
|
329
336
|
|
|
330
|
-
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Element `index` of a storage node or of a {@link splitStorage}. */
|
|
340
|
+
export function storageElement( buffer, index ) {
|
|
341
|
+
|
|
342
|
+
if ( ! buffer.isSplitStorage ) return buffer.element( index );
|
|
343
|
+
|
|
344
|
+
const { parts, perPart } = buffer;
|
|
345
|
+
const e = int( index ).toVar();
|
|
346
|
+
const out = ( { uvec4, uvec2 }[ buffer.type ] ?? vec4 )( 0 ).toVar();
|
|
347
|
+
const read = ( k ) => () => out.assign( parts[ k ].element( k === 0 ? e : e.sub( int( perPart * k ) ) ) );
|
|
348
|
+
let branch = If( e.lessThan( int( perPart ) ), read( 0 ) );
|
|
349
|
+
for ( let k = 1; k < parts.length - 1; k ++ ) branch = branch.ElseIf( e.lessThan( int( perPart * ( k + 1 ) ) ), read( k ) );
|
|
350
|
+
branch.Else( read( parts.length - 1 ) );
|
|
351
|
+
return out;
|
|
352
|
+
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const asNode = ( v ) => ( v?.isNode ? v : int( v ) );
|
|
356
|
+
|
|
357
|
+
// Element `index · stride + offset`.
|
|
358
|
+
export const getDatafromStorageBuffer = ( buffer, index, offset, stride ) =>
|
|
359
|
+
storageElement( buffer, asNode( stride ).mul( asNode( index ) ).add( asNode( offset ) ) );
|
|
331
360
|
|
|
332
361
|
/**
|
|
333
362
|
* The five uvec4 rows of a triangle record (TRIANGLE_DATA_LAYOUT) live in two GPU buffers: rows
|
|
@@ -337,9 +366,17 @@ export const getDatafromStorageBuffer = Fn( ( [ buffer, stride, sampleIndex, dat
|
|
|
337
366
|
export const TRI_GEO_ROWS = 3;
|
|
338
367
|
export const TRI_SHADE_ROWS = 2;
|
|
339
368
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
369
|
+
// Without texture coordinates (no material samples a texture) the shade buffer holds only flags and mesh index, one
|
|
370
|
+
// uvec2 a triangle: rows 3–4 read as zero UVs around them, 24 B a triangle saved.
|
|
371
|
+
export const triangleRow = ( tris, triIndex, row ) => {
|
|
372
|
+
|
|
373
|
+
if ( row < TRI_GEO_ROWS ) return getDatafromStorageBuffer( tris.geo, triIndex, int( row ), int( TRI_GEO_ROWS ) );
|
|
374
|
+
if ( ! tris.withoutUV ) return getDatafromStorageBuffer( tris.shade, triIndex, int( row - TRI_GEO_ROWS ), int( TRI_SHADE_ROWS ) );
|
|
375
|
+
if ( row === TRI_GEO_ROWS ) return uvec4( 0 );
|
|
376
|
+
const flagsMesh = storageElement( tris.shade, asNode( triIndex ) );
|
|
377
|
+
return uvec4( 0, 0, flagsMesh.x, flagsMesh.y );
|
|
378
|
+
|
|
379
|
+
};
|
|
343
380
|
|
|
344
381
|
// Unit normal to an octahedral snorm16 pair; GPU twin of packNormalOct in EngineDefaults.
|
|
345
382
|
export const packNormalOct = /*@__PURE__*/ wgslFn( `
|
|
@@ -637,11 +674,18 @@ export const shadowFlagsSettle = ( flags, alphaShadows ) => {
|
|
|
637
674
|
|
|
638
675
|
};
|
|
639
676
|
|
|
640
|
-
export const instanceRows = ( bvhBuffer, leafIndex ) =>
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
];
|
|
677
|
+
export const instanceRows = ( bvhBuffer, leafIndex ) => {
|
|
678
|
+
|
|
679
|
+
// Built over copy clusters, an instance is a copy record (3 vec4 from `copyRecordBase`), not a TLAS leaf.
|
|
680
|
+
const records = bvhBuffer.value?.copyRecordBase;
|
|
681
|
+
if ( records !== undefined ) return [ 0, 1, 2 ].map( ( row ) => getDatafromStorageBuffer( bvhBuffer, leafIndex, int( records + row ), int( 3 ) ) );
|
|
682
|
+
return [
|
|
683
|
+
getDatafromStorageBuffer( bvhBuffer, leafIndex, int( 1 ), int( 4 ) ),
|
|
684
|
+
getDatafromStorageBuffer( bvhBuffer, leafIndex, int( 2 ), int( 4 ) ),
|
|
685
|
+
getDatafromStorageBuffer( bvhBuffer, leafIndex, int( 3 ), int( 4 ) )
|
|
686
|
+
];
|
|
687
|
+
|
|
688
|
+
};
|
|
645
689
|
|
|
646
690
|
/** Object-space normal to world: transpose of the world-to-object basis. */
|
|
647
691
|
export const instanceNormalToWorld = ( rows, n ) => vec3(
|
package/src/TSL/ShadeKernel.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ShadeKernel.js — wavefront material eval + bounce generation. 256×1 workgroup, 1D dispatch.
|
|
3
|
-
*
|
|
4
|
-
* counters, activeIndices
|
|
3
|
+
* 8 storage-buffer bindings: bvh, triGeo, triShade, scene data (materials, then lights), ray, hit (+ RNG state and the
|
|
4
|
+
* G-buffer), counters, activeIndices; envCDF is a texture. The device allows 10: the other two are for the BVH and
|
|
5
|
+
* triangle stores' parts past one buffer's limit (PathTracerStage._assertBindings).
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import {
|
|
@@ -363,11 +363,11 @@ export const processMetalnessRoughness = Fn( ( [ material, uvCache ], builder )
|
|
|
363
363
|
|
|
364
364
|
} );
|
|
365
365
|
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
const
|
|
369
|
-
fn
|
|
370
|
-
return
|
|
366
|
+
// The inverse of the transform's 2x2 linear part times |det|: .xy takes (dP/du, dP/dv) to dP/du', .zw to dP/dv'.
|
|
367
|
+
// Times det instead, a mirrored transform turned both round.
|
|
368
|
+
const uvTransformInverse = /*@__PURE__*/ wgslFn( `
|
|
369
|
+
fn uvTransformInverse( t: mat3x3f ) -> vec4f {
|
|
370
|
+
return vec4f( t[1][1], - t[0][1], - t[1][0], t[0][0] ) * sign( t[0][0] * t[1][1] - t[1][0] * t[0][1] );
|
|
371
371
|
}
|
|
372
372
|
` );
|
|
373
373
|
|
|
@@ -411,13 +411,16 @@ export const triangleUVTangent = Fn( ( [ triangleBuffer, triIndex, geometryNorma
|
|
|
411
411
|
const T = e1.mul( d2.y ).sub( e2.mul( d1.y ) ).mul( r ).toVar();
|
|
412
412
|
const B = e2.mul( d1.x ).sub( e1.mul( d2.x ) ).mul( r ).toVar();
|
|
413
413
|
|
|
414
|
-
// Re-express in the map's transformed UV space
|
|
415
|
-
|
|
416
|
-
const
|
|
414
|
+
// Re-express both in the map's transformed UV space: the handedness test below needs B there too, or a
|
|
415
|
+
// mirrored map, or one turned past 90°, gets its bitangent reversed.
|
|
416
|
+
const inv = uvTransformInverse( { t: transform } ).toVar();
|
|
417
|
+
const Tt = T.mul( inv.x ).add( B.mul( inv.y ) ).toVar();
|
|
418
|
+
const Bt = T.mul( inv.z ).add( B.mul( inv.w ) ).toVar();
|
|
417
419
|
|
|
418
420
|
If( length( Tt ).greaterThan( 1e-12 ), () => {
|
|
419
421
|
|
|
420
422
|
T.assign( Tt );
|
|
423
|
+
B.assign( Bt );
|
|
421
424
|
|
|
422
425
|
} );
|
|
423
426
|
|