rayzee 7.31.1 → 8.0.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 +164 -0
- package/dist/rayzee.es.js +6366 -4361
- package/dist/rayzee.umd.js +122 -73
- package/package.json +1 -1
- package/src/EngineDefaults.js +168 -13
- package/src/EngineIssues.js +5 -0
- package/src/PathTracerApp.js +120 -19
- package/src/Processor/ArchiveReader.js +538 -0
- package/src/Processor/AssetLoader.js +265 -23
- package/src/Processor/BVHBuilder.js +48 -32
- package/src/Processor/BVHRefitter.js +244 -99
- package/src/Processor/ChunkedRecords.js +302 -0
- package/src/Processor/EmissiveTriangleBuilder.js +48 -21
- package/src/Processor/GeometryExtractor.js +577 -92
- package/src/Processor/HostMemory.js +222 -0
- package/src/Processor/InstanceTable.js +548 -97
- package/src/Processor/PBRT/EqualAreaOctahedral.js +32 -8
- package/src/Processor/PBRT/PBRTCurves.js +249 -0
- package/src/Processor/PBRT/PBRTMath.js +19 -0
- package/src/Processor/PBRT/PBRTParser.js +187 -41
- package/src/Processor/PBRT/PBRTSceneBuilder.js +586 -42
- package/src/Processor/PBRT/PBRTTokenizer.js +205 -65
- package/src/Processor/PBRT/index.js +257 -50
- package/src/Processor/PackedRayBuffer.js +29 -18
- package/src/Processor/ParallelBVHBuilder.js +9 -8
- package/src/Processor/QueueManager.js +5 -3
- package/src/Processor/SceneProcessor.js +1046 -365
- package/src/Processor/TLASBuilder.js +546 -201
- package/src/Processor/TextureCreator.js +11 -9
- package/src/Processor/VRAMTracker.js +12 -3
- package/src/Processor/Workers/BVHRefitWorker.js +16 -11
- package/src/Processor/Workers/BVHWorker.js +12 -12
- package/src/Processor/Workers/TLASWorker.js +37 -0
- package/src/Stages/NormalDepth.js +6 -2
- package/src/Stages/PathTracer.js +5 -5
- package/src/Stages/PathTracerStage.js +163 -41
- package/src/TSL/BVHTraversal.js +208 -90
- package/src/TSL/Common.js +135 -2
- package/src/TSL/Displacement.js +21 -11
- package/src/TSL/EmissiveSampling.js +49 -25
- package/src/TSL/ExtendKernel.js +3 -2
- package/src/TSL/LightBVHSampling.js +7 -4
- package/src/TSL/LightsDirect.js +30 -29
- package/src/TSL/ShadeKernel.js +8 -6
- package/src/TSL/Struct.js +3 -0
- package/src/TSL/TextureSampling.js +24 -9
- package/src/TSL/patches.js +128 -3
- package/src/index.js +3 -0
- package/src/managers/AnimationManager.js +86 -76
- package/src/managers/MaterialDataManager.js +29 -23
- package/src/managers/TransformManager.js +9 -192
package/src/TSL/BVHTraversal.js
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
Fn,
|
|
3
3
|
wgslFn,
|
|
4
4
|
vec3,
|
|
5
|
+
vec4,
|
|
5
6
|
vec2,
|
|
6
7
|
float,
|
|
7
8
|
int,
|
|
@@ -17,18 +18,23 @@ import {
|
|
|
17
18
|
notEqual,
|
|
18
19
|
lessThan,
|
|
19
20
|
array,
|
|
21
|
+
uint,
|
|
22
|
+
uintBitsToFloat,
|
|
23
|
+
floatBitsToUint,
|
|
20
24
|
bool as tslBool,
|
|
21
25
|
} from 'three/tsl';
|
|
22
26
|
|
|
27
|
+
import { BVH_LEAF_MARKERS, BVH_MAX_INDEX, TRI_MATERIAL_MASK, TRI_SIDE_SHIFT, TLAS_LEAF_IDENTITY } from '../EngineDefaults.js';
|
|
23
28
|
import { HitInfo } from './Struct.js';
|
|
24
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
getDatafromStorageBuffer, instanceRows, instanceNormalToWorld, unpackTriangleNormal, TRI_STRIDE, shadowFlagsSettle
|
|
31
|
+
} from './Common.js';
|
|
25
32
|
|
|
26
33
|
const MAX_STACK_DEPTH = 32;
|
|
27
34
|
// Hang guard only — tripping it reports a MISS, which the shade kernel pays out as
|
|
28
35
|
// full-intensity environment. San Miguel needs ~1024; cost is flat from 1024 up.
|
|
29
36
|
const MAX_BVH_ITERATIONS = 4096;
|
|
30
37
|
const BVH_STRIDE = 4;
|
|
31
|
-
const TRI_STRIDE = 8;
|
|
32
38
|
const HUGE_VAL = 1e8;
|
|
33
39
|
|
|
34
40
|
// Per-mesh visibility is now packed into the TLAS BLAS-pointer leaf's slot [2]
|
|
@@ -40,13 +46,37 @@ const HUGE_VAL = 1e8;
|
|
|
40
46
|
|
|
41
47
|
const createStack = () => array( 'int', MAX_STACK_DEPTH ).toVar();
|
|
42
48
|
|
|
49
|
+
// Reciprocal ray direction with the axis-aligned cases pinned to a large signed value.
|
|
50
|
+
const buildInvDir = ( d ) => {
|
|
51
|
+
|
|
52
|
+
const dirSign = mix( vec3( 1.0 ), sign( d ), notEqual( d, vec3( 0.0 ) ) );
|
|
53
|
+
return mix( vec3( 1.0 ).div( d ), vec3( HUGE_VAL ).mul( dirSign ), lessThan( abs( d ), vec3( 1e-8 ) ) );
|
|
54
|
+
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// The ray direction is deliberately left unnormalised through the transform: t then means
|
|
58
|
+
// the same thing either side of it, so closest-hit comparisons and the world-space hit
|
|
59
|
+
// point both survive without rescaling.
|
|
60
|
+
const toObjectPoint = ( rows, p ) => vec3(
|
|
61
|
+
rows[ 0 ].xyz.dot( p ).add( rows[ 0 ].w ),
|
|
62
|
+
rows[ 1 ].xyz.dot( p ).add( rows[ 1 ].w ),
|
|
63
|
+
rows[ 2 ].xyz.dot( p ).add( rows[ 2 ].w )
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const toObjectDir = ( rows, d ) => vec3(
|
|
67
|
+
rows[ 0 ].xyz.dot( d ),
|
|
68
|
+
rows[ 1 ].xyz.dot( d ),
|
|
69
|
+
rows[ 2 ].xyz.dot( d )
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
|
|
43
73
|
// ================================================================================
|
|
44
74
|
// RAY INTERSECTION HELPERS (inlined for BVH traversal performance)
|
|
45
75
|
// ================================================================================
|
|
46
76
|
|
|
47
77
|
// Woop watertight intersection (Woop/Benthin/Wald 2013). Eliminates edge leakage
|
|
48
78
|
// at shared triangle edges that Möller-Trumbore exhibits under FP32. Per-ray shears
|
|
49
|
-
// are precomputed once via
|
|
79
|
+
// are precomputed once via computeWoopFromInvDir; per-triangle test is FMA-friendly
|
|
50
80
|
// and uses sign-aware depth comparison so it works for any det orientation.
|
|
51
81
|
const RayTriangleGeometry = wgslFn( `
|
|
52
82
|
fn RayTriangleGeometry( rayOrigin: vec3f, rayDir: vec3f, pA: vec3f, pB: vec3f, pC: vec3f, closestHitDst: f32, woopParams: vec4f ) -> vec4f {
|
|
@@ -119,8 +149,10 @@ const RayTriangleGeometry = wgslFn( `
|
|
|
119
149
|
// Compute Woop ray-space transform (Woop 2013, §3.1) — runs once per ray and
|
|
120
150
|
// amortizes across hundreds of triangle tests. Returns Sx/Sy/Sz shears plus the
|
|
121
151
|
// permuted axis indices packed via bitcast into the .w slot.
|
|
122
|
-
|
|
123
|
-
|
|
152
|
+
// The reciprocal direction is always built first, and its dominant-axis lane IS Sz; the two
|
|
153
|
+
// shears are the other components times it. Three fewer divides per call than dividing afresh.
|
|
154
|
+
const computeWoopFromInvDir = wgslFn( `
|
|
155
|
+
fn computeWoopFromInvDir( rayDir: vec3f, invDir: vec3f ) -> vec4f {
|
|
124
156
|
|
|
125
157
|
let absDir = abs( rayDir );
|
|
126
158
|
|
|
@@ -139,10 +171,9 @@ const computeWoopRayParams = wgslFn( `
|
|
|
139
171
|
ky = tmp;
|
|
140
172
|
}
|
|
141
173
|
|
|
142
|
-
let
|
|
143
|
-
let Sx = rayDir[ u32( kx ) ]
|
|
144
|
-
let Sy = rayDir[ u32( ky ) ]
|
|
145
|
-
let Sz = 1.0f / dz;
|
|
174
|
+
let Sz = invDir[ u32( kz ) ];
|
|
175
|
+
let Sx = rayDir[ u32( kx ) ] * Sz;
|
|
176
|
+
let Sy = rayDir[ u32( ky ) ] * Sz;
|
|
146
177
|
|
|
147
178
|
let packed = kx | ( ky << 2 ) | ( kz << 4 );
|
|
148
179
|
return vec4f( Sx, Sy, Sz, f32( packed ) );
|
|
@@ -196,6 +227,7 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
|
|
|
196
227
|
uv: vec2( 0.0 ),
|
|
197
228
|
materialIndex: int( - 1 ),
|
|
198
229
|
meshIndex: int( - 1 ),
|
|
230
|
+
instanceLeaf: int( - 1 ),
|
|
199
231
|
boxTests: int( 0 ),
|
|
200
232
|
triTests: int( 0 ),
|
|
201
233
|
} ).toVar();
|
|
@@ -211,19 +243,22 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
|
|
|
211
243
|
const stackPtr = int( 1 ).toVar();
|
|
212
244
|
stack.element( int( 0 ) ).assign( int( 0 ) ); // Root node
|
|
213
245
|
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
).toVar();
|
|
246
|
+
// Triangles sit in each instance's own space, so the ray is what moves: `world*` stays
|
|
247
|
+
// put, `ray*`/`invDir`/`woopParams` are rewritten on entering a BLAS and never restored.
|
|
248
|
+
const worldOrigin = vec3( ray.origin ).toVar();
|
|
249
|
+
const worldDirection = vec3( ray.direction ).toVar();
|
|
250
|
+
const worldInvDir = buildInvDir( worldDirection ).toVar();
|
|
251
|
+
const worldWoop = computeWoopFromInvDir( { rayDir: worldDirection, invDir: worldInvDir } ).toVar();
|
|
221
252
|
|
|
222
|
-
const rayOrigin =
|
|
223
|
-
const rayDirection =
|
|
253
|
+
const rayOrigin = vec3( worldOrigin ).toVar();
|
|
254
|
+
const rayDirection = vec3( worldDirection ).toVar();
|
|
255
|
+
const invDir = vec3( worldInvDir ).toVar();
|
|
256
|
+
const woopParams = vec4( worldWoop ).toVar();
|
|
224
257
|
|
|
225
|
-
//
|
|
226
|
-
const
|
|
258
|
+
// The TLAS leaf the ray is inside, or -1; dropping below `instExit` on the stack clears it.
|
|
259
|
+
const instLeaf = int( - 1 ).toVar();
|
|
260
|
+
const instExit = int( 0 ).toVar();
|
|
261
|
+
const hitInstLeaf = int( - 1 ).toVar();
|
|
227
262
|
|
|
228
263
|
const iterCount = int( 0 ).toVar();
|
|
229
264
|
|
|
@@ -232,6 +267,7 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
|
|
|
232
267
|
iterCount.addAssign( 1 );
|
|
233
268
|
stackPtr.subAssign( 1 );
|
|
234
269
|
const nodeIndex = stack.element( stackPtr ).toVar();
|
|
270
|
+
instLeaf.assign( select( stackPtr.lessThan( instExit ), int( - 1 ), instLeaf ) );
|
|
235
271
|
|
|
236
272
|
// New layout: 4 vec4 per node
|
|
237
273
|
// Leaf: vec4(0) = [triOffset, triCount, 0, -1]
|
|
@@ -240,14 +276,18 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
|
|
|
240
276
|
const nodeData0 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 0 ), int( BVH_STRIDE ) );
|
|
241
277
|
if ( trackStats ) closestHit.boxTests.addAssign( 1 );
|
|
242
278
|
|
|
243
|
-
|
|
279
|
+
// Slot [3] carries a u32 bit pattern: an inner node's left-child index, or a leaf tag
|
|
280
|
+
// above every valid index. Stored as float VALUES these rounded past 2^24 and sent the
|
|
281
|
+
// ray to a neighbouring node, which silently erased geometry from large scenes.
|
|
282
|
+
const nodeTag = floatBitsToUint( nodeData0.w ).toVar();
|
|
283
|
+
|
|
284
|
+
If( nodeTag.greaterThanEqual( uint( BVH_MAX_INDEX ) ), () => {
|
|
244
285
|
|
|
245
|
-
|
|
246
|
-
If( nodeData0.w.greaterThan( float( - 1.5 ) ), () => {
|
|
286
|
+
If( nodeTag.equal( uint( BVH_LEAF_MARKERS.TRIANGLE_LEAF ) ), () => {
|
|
247
287
|
|
|
248
|
-
// Triangle leaf
|
|
249
|
-
const triStart = int( nodeData0.x ).toVar();
|
|
250
|
-
const triCount = int( nodeData0.y ).toVar();
|
|
288
|
+
// Triangle leaf — triOffset and triCount packed in vec4(0).xy
|
|
289
|
+
const triStart = int( floatBitsToUint( nodeData0.x ) ).toVar();
|
|
290
|
+
const triCount = int( floatBitsToUint( nodeData0.y ) ).toVar();
|
|
251
291
|
|
|
252
292
|
// Process triangles in leaf
|
|
253
293
|
Loop( { start: int( 0 ), end: triCount }, ( { i } ) => {
|
|
@@ -255,10 +295,13 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
|
|
|
255
295
|
if ( trackStats ) closestHit.triTests.addAssign( 1 );
|
|
256
296
|
const triIndex = triStart.add( i ).toVar();
|
|
257
297
|
|
|
258
|
-
//
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
const
|
|
298
|
+
// Three fetches carry positions AND the packed normals in their .w lanes.
|
|
299
|
+
const recA = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 0 ), int( TRI_STRIDE ) ).toVar();
|
|
300
|
+
const recB = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 1 ), int( TRI_STRIDE ) ).toVar();
|
|
301
|
+
const recC = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 2 ), int( TRI_STRIDE ) ).toVar();
|
|
302
|
+
const pA = uintBitsToFloat( recA.xyz );
|
|
303
|
+
const pB = uintBitsToFloat( recB.xyz );
|
|
304
|
+
const pC = uintBitsToFloat( recC.xyz );
|
|
262
305
|
|
|
263
306
|
const triResult = RayTriangleGeometry( { rayOrigin, rayDir: rayDirection, pA, pB, pC, closestHitDst: closestHit.dst, woopParams } );
|
|
264
307
|
|
|
@@ -269,15 +312,13 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
|
|
|
269
312
|
const u = triResult.y;
|
|
270
313
|
const v = triResult.z;
|
|
271
314
|
|
|
272
|
-
//
|
|
273
|
-
// carries matIdx
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
const
|
|
278
|
-
const
|
|
279
|
-
const nC = normalCData.xyz;
|
|
280
|
-
const side = int( normalCData.w ).toVar();
|
|
315
|
+
// Normals came with the positions; only the side flag needs a fetch, and
|
|
316
|
+
// slot 4 also carries matIdx/meshIndex, deferred to post-traversal.
|
|
317
|
+
const nA = unpackTriangleNormal( recA.w );
|
|
318
|
+
const nB = unpackTriangleNormal( recB.w );
|
|
319
|
+
const nC = unpackTriangleNormal( recC.w );
|
|
320
|
+
const flags = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 4 ), int( TRI_STRIDE ) ).z;
|
|
321
|
+
const side = int( flags.shiftRight( uint( TRI_SIDE_SHIFT ) ).bitAnd( uint( 3 ) ) ).toVar();
|
|
281
322
|
|
|
282
323
|
// Interpolate normal for the side-culling dot product (kept local,
|
|
283
324
|
// not stored on closestHit — re-derived post-loop from closestTriIdx).
|
|
@@ -301,6 +342,7 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
|
|
|
301
342
|
closestTriIdx.assign( triIndex );
|
|
302
343
|
closestU.assign( u );
|
|
303
344
|
closestV.assign( v );
|
|
345
|
+
hitInstLeaf.assign( instLeaf );
|
|
304
346
|
|
|
305
347
|
} );
|
|
306
348
|
|
|
@@ -317,12 +359,36 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
|
|
|
317
359
|
|
|
318
360
|
} ).Else( () => {
|
|
319
361
|
|
|
320
|
-
// BLAS-pointer leaf
|
|
321
|
-
// nodeData0: [blasRootNodeIndex, meshIndex, visibility, -2]
|
|
322
|
-
// Visibility is free-fetched with the leaf
|
|
362
|
+
// BLAS-pointer leaf — enter the instance if the mesh is visible.
|
|
363
|
+
// nodeData0: [blasRootNodeIndex, meshIndex, visibility, -2]; slots 4..15 hold
|
|
364
|
+
// the world-to-object matrix. Visibility is free-fetched with the leaf.
|
|
323
365
|
If( nodeData0.z.greaterThan( 0.5 ).and( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ) ), () => {
|
|
324
366
|
|
|
325
|
-
|
|
367
|
+
// A baked placement already sits in world space; only a transformed one moves the ray.
|
|
368
|
+
If( floatBitsToUint( nodeData0.y ).bitAnd( uint( TLAS_LEAF_IDENTITY ) ).equal( uint( 0 ) ), () => {
|
|
369
|
+
|
|
370
|
+
const rows = instanceRows( bvhBuffer, nodeIndex );
|
|
371
|
+
const localDir = toObjectDir( rows, worldDirection ).toVar();
|
|
372
|
+
|
|
373
|
+
rayOrigin.assign( toObjectPoint( rows, worldOrigin ) );
|
|
374
|
+
rayDirection.assign( localDir );
|
|
375
|
+
const localInv = buildInvDir( localDir ).toVar();
|
|
376
|
+
invDir.assign( localInv );
|
|
377
|
+
woopParams.assign( computeWoopFromInvDir( { rayDir: localDir, invDir: localInv } ) );
|
|
378
|
+
instLeaf.assign( nodeIndex );
|
|
379
|
+
instExit.assign( stackPtr );
|
|
380
|
+
|
|
381
|
+
} ).Else( () => {
|
|
382
|
+
|
|
383
|
+
rayOrigin.assign( worldOrigin );
|
|
384
|
+
rayDirection.assign( worldDirection );
|
|
385
|
+
invDir.assign( worldInvDir );
|
|
386
|
+
woopParams.assign( worldWoop );
|
|
387
|
+
instLeaf.assign( int( - 1 ) );
|
|
388
|
+
|
|
389
|
+
} );
|
|
390
|
+
|
|
391
|
+
stack.element( stackPtr ).assign( int( floatBitsToUint( nodeData0.x ) ) );
|
|
326
392
|
stackPtr.addAssign( 1 );
|
|
327
393
|
|
|
328
394
|
} );
|
|
@@ -336,11 +402,14 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
|
|
|
336
402
|
const nodeData2 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 2 ), int( BVH_STRIDE ) );
|
|
337
403
|
const nodeData3 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 3 ), int( BVH_STRIDE ) );
|
|
338
404
|
|
|
339
|
-
const leftChild = int(
|
|
340
|
-
const rightChild = int( nodeData1.w ).toVar();
|
|
405
|
+
const leftChild = int( nodeTag ).toVar();
|
|
406
|
+
const rightChild = int( floatBitsToUint( nodeData1.w ) ).toVar();
|
|
341
407
|
|
|
342
|
-
const
|
|
343
|
-
const
|
|
408
|
+
const inInst = instLeaf.greaterThanEqual( int( 0 ) );
|
|
409
|
+
const boxOrigin = select( inInst, rayOrigin, worldOrigin ).toVar();
|
|
410
|
+
const boxInv = select( inInst, invDir, worldInvDir ).toVar();
|
|
411
|
+
const dstA = fastRayAABBDst( { rayOrigin: boxOrigin, invDir: boxInv, boxMin: nodeData0.xyz, boxMax: nodeData1.xyz } ).toVar();
|
|
412
|
+
const dstB = fastRayAABBDst( { rayOrigin: boxOrigin, invDir: boxInv, boxMin: nodeData2.xyz, boxMax: nodeData3.xyz } ).toVar();
|
|
344
413
|
|
|
345
414
|
// Optimized early rejection
|
|
346
415
|
const minDst = min( dstA, dstB );
|
|
@@ -380,18 +449,26 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
|
|
|
380
449
|
|
|
381
450
|
// Re-fetch the winning triangle's normals — trading 3 storage reads (once)
|
|
382
451
|
// for ~3 regs freed across every BVH iteration.
|
|
383
|
-
const nA = getDatafromStorageBuffer( triangleBuffer, closestTriIdx, int(
|
|
384
|
-
const nB = getDatafromStorageBuffer( triangleBuffer, closestTriIdx, int(
|
|
385
|
-
const nC = getDatafromStorageBuffer( triangleBuffer, closestTriIdx, int(
|
|
386
|
-
|
|
452
|
+
const nA = unpackTriangleNormal( getDatafromStorageBuffer( triangleBuffer, closestTriIdx, int( 0 ), int( TRI_STRIDE ) ).w );
|
|
453
|
+
const nB = unpackTriangleNormal( getDatafromStorageBuffer( triangleBuffer, closestTriIdx, int( 1 ), int( TRI_STRIDE ) ).w );
|
|
454
|
+
const nC = unpackTriangleNormal( getDatafromStorageBuffer( triangleBuffer, closestTriIdx, int( 2 ), int( TRI_STRIDE ) ).w );
|
|
455
|
+
const objectNormal = normalize( nA.mul( w ).add( nB.mul( closestU ) ).add( nC.mul( closestV ) ) ).toVar();
|
|
456
|
+
If( hitInstLeaf.greaterThanEqual( int( 0 ) ), () => {
|
|
457
|
+
|
|
458
|
+
objectNormal.assign( normalize( instanceNormalToWorld( instanceRows( bvhBuffer, hitInstLeaf ), objectNormal ) ) );
|
|
387
459
|
|
|
388
|
-
|
|
389
|
-
|
|
460
|
+
} );
|
|
461
|
+
|
|
462
|
+
closestHit.normal.assign( objectNormal );
|
|
463
|
+
|
|
464
|
+
const uvData1 = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, closestTriIdx, int( 3 ), int( TRI_STRIDE ) ) );
|
|
465
|
+
const uvData2 = getDatafromStorageBuffer( triangleBuffer, closestTriIdx, int( 4 ), int( TRI_STRIDE ) ).toVar();
|
|
390
466
|
closestHit.uv.assign(
|
|
391
|
-
uvData1.xy.mul( w ).add( uvData1.zw.mul( closestU ) ).add( uvData2.xy.mul( closestV ) )
|
|
467
|
+
uvData1.xy.mul( w ).add( uvData1.zw.mul( closestU ) ).add( uintBitsToFloat( uvData2.xy ).mul( closestV ) )
|
|
392
468
|
);
|
|
393
|
-
closestHit.materialIndex.assign( int( uvData2.z ) );
|
|
469
|
+
closestHit.materialIndex.assign( int( uvData2.z.bitAnd( uint( TRI_MATERIAL_MASK ) ) ) );
|
|
394
470
|
closestHit.meshIndex.assign( int( uvData2.w ) );
|
|
471
|
+
closestHit.instanceLeaf.assign( hitInstLeaf );
|
|
395
472
|
closestHit.triangleIndex.assign( closestTriIdx );
|
|
396
473
|
|
|
397
474
|
} );
|
|
@@ -423,6 +500,7 @@ export const traverseBVHShadow = Fn( ( [
|
|
|
423
500
|
uv: vec2( 0.0 ),
|
|
424
501
|
materialIndex: int( - 1 ),
|
|
425
502
|
meshIndex: int( - 1 ),
|
|
503
|
+
instanceLeaf: int( - 1 ),
|
|
426
504
|
boxTests: int( 0 ),
|
|
427
505
|
triTests: int( 0 ),
|
|
428
506
|
} ).toVar();
|
|
@@ -431,69 +509,83 @@ export const traverseBVHShadow = Fn( ( [
|
|
|
431
509
|
const stackPtr = int( 1 ).toVar();
|
|
432
510
|
stack.element( int( 0 ) ).assign( int( 0 ) );
|
|
433
511
|
|
|
434
|
-
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
512
|
+
// Same instance-space scheme as traverseBVH: the ray moves, the triangles do not.
|
|
513
|
+
const worldOrigin = vec3( ray.origin ).toVar();
|
|
514
|
+
const worldDirection = vec3( ray.direction ).toVar();
|
|
515
|
+
const worldInvDir = buildInvDir( worldDirection ).toVar();
|
|
516
|
+
const worldWoop = computeWoopFromInvDir( { rayDir: worldDirection, invDir: worldInvDir } ).toVar();
|
|
517
|
+
|
|
518
|
+
const rayOrigin = vec3( worldOrigin ).toVar();
|
|
519
|
+
const rayDirection = vec3( worldDirection ).toVar();
|
|
520
|
+
const invDir = vec3( worldInvDir ).toVar();
|
|
521
|
+
const woopParams = vec4( worldWoop ).toVar();
|
|
440
522
|
|
|
441
|
-
|
|
442
|
-
const
|
|
523
|
+
const instLeaf = int( - 1 ).toVar();
|
|
524
|
+
const instExit = int( 0 ).toVar();
|
|
525
|
+
const blocked = tslBool( false ).toVar();
|
|
443
526
|
|
|
444
527
|
const sIterCount = int( 0 ).toVar();
|
|
445
528
|
|
|
446
|
-
Loop( stackPtr.greaterThan( int( 0 ) ).and(
|
|
529
|
+
Loop( stackPtr.greaterThan( int( 0 ) ).and( blocked.not() ).and( sIterCount.lessThan( int( MAX_BVH_ITERATIONS ) ) ), () => {
|
|
447
530
|
|
|
448
531
|
sIterCount.addAssign( 1 );
|
|
449
532
|
stackPtr.subAssign( 1 );
|
|
450
533
|
const nodeIndex = stack.element( stackPtr ).toVar();
|
|
534
|
+
instLeaf.assign( select( stackPtr.lessThan( instExit ), int( - 1 ), instLeaf ) );
|
|
451
535
|
|
|
452
536
|
const nodeData0 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 0 ), int( BVH_STRIDE ) );
|
|
453
537
|
|
|
454
|
-
|
|
538
|
+
// Slot [3] carries a u32 bit pattern: an inner node's left-child index, or a leaf tag
|
|
539
|
+
// above every valid index. Stored as float VALUES these rounded past 2^24 and sent the
|
|
540
|
+
// ray to a neighbouring node, which silently erased geometry from large scenes.
|
|
541
|
+
const nodeTag = floatBitsToUint( nodeData0.w ).toVar();
|
|
455
542
|
|
|
456
|
-
|
|
457
|
-
If( nodeData0.w.greaterThan( float( - 1.5 ) ), () => {
|
|
543
|
+
If( nodeTag.greaterThanEqual( uint( BVH_MAX_INDEX ) ), () => {
|
|
458
544
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
545
|
+
If( nodeTag.equal( uint( BVH_LEAF_MARKERS.TRIANGLE_LEAF ) ), () => {
|
|
546
|
+
|
|
547
|
+
// Triangle leaf — triOffset and triCount packed in vec4(0).xy
|
|
548
|
+
const triStart = int( floatBitsToUint( nodeData0.x ) ).toVar();
|
|
549
|
+
const triCount = int( floatBitsToUint( nodeData0.y ) ).toVar();
|
|
462
550
|
|
|
463
551
|
Loop( { start: int( 0 ), end: triCount }, ( { i } ) => {
|
|
464
552
|
|
|
465
553
|
const triIndex = triStart.add( i ).toVar();
|
|
466
554
|
|
|
467
|
-
const pA = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 0 ), int( TRI_STRIDE ) ).xyz;
|
|
468
|
-
const pB = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 1 ), int( TRI_STRIDE ) ).xyz;
|
|
469
|
-
const pC = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 2 ), int( TRI_STRIDE ) ).xyz;
|
|
555
|
+
const pA = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, triIndex, int( 0 ), int( TRI_STRIDE ) ).xyz );
|
|
556
|
+
const pB = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, triIndex, int( 1 ), int( TRI_STRIDE ) ).xyz );
|
|
557
|
+
const pC = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, triIndex, int( 2 ), int( TRI_STRIDE ) ).xyz );
|
|
470
558
|
|
|
471
|
-
const triResult = RayTriangleGeometry( { rayOrigin
|
|
559
|
+
const triResult = RayTriangleGeometry( { rayOrigin, rayDir: rayDirection, pA, pB, pC, closestHitDst: closestHit.dst, woopParams } );
|
|
472
560
|
|
|
473
561
|
If( triResult.w.greaterThan( 0.5 ), () => {
|
|
474
562
|
|
|
475
|
-
// Per-mesh visibility handled at BLAS-pointer level
|
|
476
|
-
const uvData2 = getDatafromStorageBuffer( triangleBuffer, triIndex, int(
|
|
563
|
+
// Per-mesh visibility is handled at the BLAS-pointer level.
|
|
564
|
+
const uvData2 = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 4 ), int( TRI_STRIDE ) ).toVar();
|
|
477
565
|
|
|
478
566
|
closestHit.didHit.assign( true );
|
|
479
567
|
closestHit.dst.assign( triResult.x );
|
|
480
|
-
closestHit.materialIndex.assign( int( uvData2.z ) );
|
|
568
|
+
closestHit.materialIndex.assign( int( uvData2.z.bitAnd( uint( TRI_MATERIAL_MASK ) ) ) );
|
|
481
569
|
closestHit.meshIndex.assign( int( uvData2.w ) );
|
|
570
|
+
closestHit.instanceLeaf.assign( instLeaf );
|
|
482
571
|
|
|
483
572
|
// Hit point is cheap (origin + dir*t). Geometric normal is deferred
|
|
484
573
|
// to traceShadowRay — only the transmission branch needs it, so we
|
|
485
574
|
// skip the cross+normalize for the (much more common) opaque-blocker
|
|
486
575
|
// and alpha-cutout paths. Normal stays vec3(0) from struct init.
|
|
487
|
-
closestHit.hitPoint.assign(
|
|
576
|
+
closestHit.hitPoint.assign( worldOrigin.add( worldDirection.mul( triResult.x ) ) );
|
|
488
577
|
|
|
489
|
-
// Store barycentrics + triangle index for deferred UV computation.
|
|
490
|
-
// Actual UV interpolation happens in traceShadowRay only when
|
|
491
|
-
// the material needs alpha testing — zero overhead for opaque hits.
|
|
492
578
|
closestHit.uv.assign( vec2( triResult.y, triResult.z ) );
|
|
493
579
|
closestHit.triangleIndex.assign( triIndex );
|
|
494
580
|
|
|
495
|
-
//
|
|
496
|
-
|
|
581
|
+
// An opaque blocker settles the ray. A surface light passes through has to be
|
|
582
|
+
// the nearest one, or the layers behind the first find are never counted.
|
|
583
|
+
If( shadowFlagsSettle( uvData2.z ), () => {
|
|
584
|
+
|
|
585
|
+
blocked.assign( true );
|
|
586
|
+
Break();
|
|
587
|
+
|
|
588
|
+
} );
|
|
497
589
|
|
|
498
590
|
} );
|
|
499
591
|
|
|
@@ -501,11 +593,34 @@ export const traverseBVHShadow = Fn( ( [
|
|
|
501
593
|
|
|
502
594
|
} ).Else( () => {
|
|
503
595
|
|
|
504
|
-
// BLAS-pointer leaf
|
|
505
|
-
// nodeData0: [blasRootNodeIndex, meshIndex, visibility, -2]
|
|
596
|
+
// BLAS-pointer leaf — enter the instance if the mesh is visible.
|
|
506
597
|
If( nodeData0.z.greaterThan( 0.5 ).and( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ) ), () => {
|
|
507
598
|
|
|
508
|
-
|
|
599
|
+
// A baked placement already sits in world space; only a transformed one moves the ray.
|
|
600
|
+
If( floatBitsToUint( nodeData0.y ).bitAnd( uint( TLAS_LEAF_IDENTITY ) ).equal( uint( 0 ) ), () => {
|
|
601
|
+
|
|
602
|
+
const rows = instanceRows( bvhBuffer, nodeIndex );
|
|
603
|
+
const localDir = toObjectDir( rows, worldDirection ).toVar();
|
|
604
|
+
|
|
605
|
+
rayOrigin.assign( toObjectPoint( rows, worldOrigin ) );
|
|
606
|
+
rayDirection.assign( localDir );
|
|
607
|
+
const localInv = buildInvDir( localDir ).toVar();
|
|
608
|
+
invDir.assign( localInv );
|
|
609
|
+
woopParams.assign( computeWoopFromInvDir( { rayDir: localDir, invDir: localInv } ) );
|
|
610
|
+
instLeaf.assign( nodeIndex );
|
|
611
|
+
instExit.assign( stackPtr );
|
|
612
|
+
|
|
613
|
+
} ).Else( () => {
|
|
614
|
+
|
|
615
|
+
rayOrigin.assign( worldOrigin );
|
|
616
|
+
rayDirection.assign( worldDirection );
|
|
617
|
+
invDir.assign( worldInvDir );
|
|
618
|
+
woopParams.assign( worldWoop );
|
|
619
|
+
instLeaf.assign( int( - 1 ) );
|
|
620
|
+
|
|
621
|
+
} );
|
|
622
|
+
|
|
623
|
+
stack.element( stackPtr ).assign( int( floatBitsToUint( nodeData0.x ) ) );
|
|
509
624
|
stackPtr.addAssign( 1 );
|
|
510
625
|
|
|
511
626
|
} );
|
|
@@ -519,11 +634,14 @@ export const traverseBVHShadow = Fn( ( [
|
|
|
519
634
|
const nodeData2 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 2 ), int( BVH_STRIDE ) );
|
|
520
635
|
const nodeData3 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 3 ), int( BVH_STRIDE ) );
|
|
521
636
|
|
|
522
|
-
const leftChild = int(
|
|
523
|
-
const rightChild = int( nodeData1.w ).toVar();
|
|
637
|
+
const leftChild = int( nodeTag ).toVar();
|
|
638
|
+
const rightChild = int( floatBitsToUint( nodeData1.w ) ).toVar();
|
|
524
639
|
|
|
525
|
-
const
|
|
526
|
-
const
|
|
640
|
+
const inInst = instLeaf.greaterThanEqual( int( 0 ) );
|
|
641
|
+
const boxOrigin = select( inInst, rayOrigin, worldOrigin ).toVar();
|
|
642
|
+
const boxInv = select( inInst, invDir, worldInvDir ).toVar();
|
|
643
|
+
const dstA = fastRayAABBDst( { rayOrigin: boxOrigin, invDir: boxInv, boxMin: nodeData0.xyz, boxMax: nodeData1.xyz } ).toVar();
|
|
644
|
+
const dstB = fastRayAABBDst( { rayOrigin: boxOrigin, invDir: boxInv, boxMin: nodeData2.xyz, boxMax: nodeData3.xyz } ).toVar();
|
|
527
645
|
|
|
528
646
|
// Distance-ordered traversal — nearer child first for faster any-hit
|
|
529
647
|
// termination. SA build ordering improves cache locality (larger-SA
|
package/src/TSL/Common.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fn, wgslFn, float, vec2, vec3, vec4, int, mat3, If, max, dot, clamp, select } from 'three/tsl';
|
|
1
|
+
import { Fn, wgslFn, float, vec2, vec3, vec4, int, uint, mat3, If, max, dot, clamp, select, bool as tslBool } from 'three/tsl';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
AnisoFrame,
|
|
@@ -24,7 +24,7 @@ export const FP16_MAX = 65504.0;
|
|
|
24
24
|
export const sanitize1 = ( x, ceiling = 1e7 ) => select( x.equal( x ), x, float( 0.0 ) ).clamp( 0.0, ceiling );
|
|
25
25
|
export const sanitizeRGB = ( c, ceiling = 1e7 ) =>
|
|
26
26
|
vec3( sanitize1( c.x, ceiling ), sanitize1( c.y, ceiling ), sanitize1( c.z, ceiling ) );
|
|
27
|
-
import { MATERIAL_DATA_LAYOUT } from '../EngineDefaults.js';
|
|
27
|
+
import { MATERIAL_DATA_LAYOUT, TRI_BLOCKER_SHIFT, TRI_BLOCKER_ALPHA_SHIFT } from '../EngineDefaults.js';
|
|
28
28
|
|
|
29
29
|
export const MATERIAL_SLOTS = MATERIAL_DATA_LAYOUT.SLOTS_PER_MATERIAL;
|
|
30
30
|
export const MATERIAL_SLOT = MATERIAL_DATA_LAYOUT.SLOT;
|
|
@@ -314,6 +314,52 @@ export const getDatafromStorageBuffer = Fn( ( [ buffer, stride, sampleIndex, dat
|
|
|
314
314
|
|
|
315
315
|
} );
|
|
316
316
|
|
|
317
|
+
/** uvec4 lanes per triangle record — see TRIANGLE_DATA_LAYOUT. */
|
|
318
|
+
export const TRI_STRIDE = 5;
|
|
319
|
+
|
|
320
|
+
// Unit normal to an octahedral snorm16 pair; GPU twin of packNormalOct in EngineDefaults.
|
|
321
|
+
export const packNormalOct = /*@__PURE__*/ wgslFn( `
|
|
322
|
+
fn packNormalOct( n: vec3f ) -> u32 {
|
|
323
|
+
|
|
324
|
+
let s = abs( n.x ) + abs( n.y ) + abs( n.z );
|
|
325
|
+
var p = n.xy / max( s, 1e-20 );
|
|
326
|
+
|
|
327
|
+
if ( n.z < 0.0 ) {
|
|
328
|
+
|
|
329
|
+
let px = ( 1.0 - abs( p.y ) ) * select( -1.0, 1.0, p.x >= 0.0 );
|
|
330
|
+
let py = ( 1.0 - abs( p.x ) ) * select( -1.0, 1.0, p.y >= 0.0 );
|
|
331
|
+
p = vec2f( px, py );
|
|
332
|
+
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return pack2x16snorm( p );
|
|
336
|
+
|
|
337
|
+
}
|
|
338
|
+
` );
|
|
339
|
+
|
|
340
|
+
// Octahedral snorm16 pair back to a unit normal; mirrors packNormalOct on the CPU.
|
|
341
|
+
export const unpackTriangleNormal = /*@__PURE__*/ wgslFn( `
|
|
342
|
+
fn unpackTriangleNormal( packed: u32 ) -> vec3f {
|
|
343
|
+
|
|
344
|
+
let e = unpack2x16snorm( packed );
|
|
345
|
+
var nx = e.x;
|
|
346
|
+
var ny = e.y;
|
|
347
|
+
let nz = 1.0 - abs( e.x ) - abs( e.y );
|
|
348
|
+
|
|
349
|
+
if ( nz < 0.0 ) {
|
|
350
|
+
|
|
351
|
+
let tx = ( 1.0 - abs( ny ) ) * select( -1.0, 1.0, nx >= 0.0 );
|
|
352
|
+
let ty = ( 1.0 - abs( nx ) ) * select( -1.0, 1.0, ny >= 0.0 );
|
|
353
|
+
nx = tx;
|
|
354
|
+
ny = ty;
|
|
355
|
+
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return normalize( vec3f( nx, ny, nz ) );
|
|
359
|
+
|
|
360
|
+
}
|
|
361
|
+
` );
|
|
362
|
+
|
|
317
363
|
// Reconstruct mat3 from two vec4s — exact port of GLSL
|
|
318
364
|
export const arrayToMat3 = wgslFn( `
|
|
319
365
|
fn arrayToMat3( data1: vec4f, data2: vec4f ) -> mat3x3f {
|
|
@@ -531,3 +577,90 @@ export const getShadowMaterial = Fn( ( [ materialIndex, materialBuffer ] ) => {
|
|
|
531
577
|
} );
|
|
532
578
|
|
|
533
579
|
} );
|
|
580
|
+
|
|
581
|
+
// ================================================================================
|
|
582
|
+
// INSTANCE TRANSFORMS
|
|
583
|
+
// ================================================================================
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* The three rows of an instance's world-to-object matrix, read off its TLAS leaf.
|
|
587
|
+
* A BVH node is 16 floats and the BLAS pointer needs four, so the affine inverse
|
|
588
|
+
* rides along in the rest — no second binding, which matters on a backend that
|
|
589
|
+
* allows ten storage buffers per stage.
|
|
590
|
+
*/
|
|
591
|
+
// Runtime uniform that toggles alpha-cutout shadows; set by ShaderBuilder before the kernels build.
|
|
592
|
+
let _enableAlphaShadows = null;
|
|
593
|
+
|
|
594
|
+
export function setAlphaShadowsUniform( node ) {
|
|
595
|
+
|
|
596
|
+
_enableAlphaShadows = node;
|
|
597
|
+
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
export const getAlphaShadowsUniform = () => _enableAlphaShadows;
|
|
601
|
+
|
|
602
|
+
// Does a hit on a triangle with these flags settle the shadow ray without a material fetch?
|
|
603
|
+
export const shadowFlagsSettle = ( flags ) => {
|
|
604
|
+
|
|
605
|
+
const bit = ( shift ) => flags.shiftRight( uint( shift ) ).bitAnd( uint( 1 ) ).equal( uint( 1 ) );
|
|
606
|
+
const alphaOff = _enableAlphaShadows ? _enableAlphaShadows.equal( int( 0 ) ) : tslBool( true );
|
|
607
|
+
return bit( TRI_BLOCKER_SHIFT ).or( bit( TRI_BLOCKER_ALPHA_SHIFT ).and( alphaOff ) );
|
|
608
|
+
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
export const instanceRows = ( bvhBuffer, leafIndex ) => [
|
|
612
|
+
getDatafromStorageBuffer( bvhBuffer, leafIndex, int( 1 ), int( 4 ) ),
|
|
613
|
+
getDatafromStorageBuffer( bvhBuffer, leafIndex, int( 2 ), int( 4 ) ),
|
|
614
|
+
getDatafromStorageBuffer( bvhBuffer, leafIndex, int( 3 ), int( 4 ) )
|
|
615
|
+
];
|
|
616
|
+
|
|
617
|
+
/** Object-space normal to world: transpose of the world-to-object basis. */
|
|
618
|
+
export const instanceNormalToWorld = ( rows, n ) => vec3(
|
|
619
|
+
vec3( rows[ 0 ].x, rows[ 1 ].x, rows[ 2 ].x ).dot( n ),
|
|
620
|
+
vec3( rows[ 0 ].y, rows[ 1 ].y, rows[ 2 ].y ).dot( n ),
|
|
621
|
+
vec3( rows[ 0 ].z, rows[ 1 ].z, rows[ 2 ].z ).dot( n )
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* A face normal, built from an object-space cross product, put into world space. It takes the
|
|
626
|
+
* same inverse-transpose as a shading normal, but a mirroring placement reverses which way the
|
|
627
|
+
* triangle winds, so the sign of the placement's determinant has to come with it or the face
|
|
628
|
+
* points into the surface instead of out of it.
|
|
629
|
+
*
|
|
630
|
+
* Inline nodes rather than a wgslFn: a WGSL function here is a real call in the shade kernel and
|
|
631
|
+
* measured 0.6 ms per sample on a scene that never even reaches this branch.
|
|
632
|
+
*/
|
|
633
|
+
export const instanceFaceNormalToWorld = ( rows, n ) => {
|
|
634
|
+
|
|
635
|
+
const det = rows[ 0 ].xyz.dot( rows[ 1 ].xyz.cross( rows[ 2 ].xyz ) );
|
|
636
|
+
return instanceNormalToWorld( rows, n ).mul( det.sign() );
|
|
637
|
+
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Object-space direction to world. Tangents transform by the forward matrix, not the
|
|
642
|
+
* inverse-transpose that normals use, so the 3x3 is inverted here — once per shaded
|
|
643
|
+
* hit, which is far cheaper than carrying a second matrix through every BVH node.
|
|
644
|
+
*/
|
|
645
|
+
export const instanceDirToWorld = /*@__PURE__*/ wgslFn( `
|
|
646
|
+
fn instanceDirToWorld( r0: vec3f, r1: vec3f, r2: vec3f, v: vec3f ) -> vec3f {
|
|
647
|
+
|
|
648
|
+
// Rows of world-to-object; its inverse has columns cross(r1,r2), cross(r2,r0), cross(r0,r1).
|
|
649
|
+
let c0 = cross( r1, r2 );
|
|
650
|
+
let det = dot( r0, c0 );
|
|
651
|
+
// Relative: for a uniform scale s this determinant is s⁻³, so a fixed floor would call a
|
|
652
|
+
// heavily scaled-up instance singular and leave the direction untransformed.
|
|
653
|
+
let magnitude = length( r0 ) * length( r1 ) * length( r2 );
|
|
654
|
+
if ( abs( det ) <= 1e-12f * magnitude ) { return v; }
|
|
655
|
+
|
|
656
|
+
let inv = 1.0f / det;
|
|
657
|
+
return ( c0 * v.x + cross( r2, r0 ) * v.y + cross( r0, r1 ) * v.z ) * inv;
|
|
658
|
+
|
|
659
|
+
}
|
|
660
|
+
` );
|
|
661
|
+
|
|
662
|
+
/** Object-space point to world: undo the inverse translation, then the inverse basis. */
|
|
663
|
+
export const instancePointToWorld = ( rows, p ) => instanceDirToWorld( {
|
|
664
|
+
r0: rows[ 0 ].xyz, r1: rows[ 1 ].xyz, r2: rows[ 2 ].xyz,
|
|
665
|
+
v: p.sub( vec3( rows[ 0 ].w, rows[ 1 ].w, rows[ 2 ].w ) )
|
|
666
|
+
} );
|