rayzee 9.3.1 → 9.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{NeuralRetouch-CYgkQY8Z.js → NeuralRetouch-vkdXXyK2.js} +1 -1
- package/dist/{NeuralSuperRes-PkgDLQcD.js → NeuralSuperRes-FVqRJKQV.js} +1 -1
- package/dist/{ToneMapGPU-B6XIe3DU.js → ToneMapGPU-DKQM1pMd.js} +86 -78
- package/dist/rayzee.es.js +603 -462
- package/dist/rayzee.umd.js +67 -38
- package/package.json +1 -1
- package/src/Passes/AIUpscaler.js +58 -141
- package/src/Passes/OIDNDenoiser.js +92 -65
- package/src/PathTracerApp.js +3 -3
- package/src/Processor/BVHRefitter.js +64 -0
- package/src/Processor/SceneProcessor.js +34 -5
- package/src/Processor/ToneMapGPU.js +64 -15
- package/src/Processor/Workers/BVHRefitWorker.js +5 -2
- package/src/Stages/PathTracerStage.js +29 -7
- package/src/managers/AnimationManager.js +88 -6
- package/src/managers/CameraManager.js +31 -5
|
@@ -2597,6 +2597,7 @@ export class SceneProcessor {
|
|
|
2597
2597
|
async refitBVH( newPositions, newNormals ) {
|
|
2598
2598
|
|
|
2599
2599
|
this._geometryVersion = ( this._geometryVersion ?? 0 ) + 1;
|
|
2600
|
+
this._tlasLeafBoxesOf = null;
|
|
2600
2601
|
|
|
2601
2602
|
if ( ! this.bvh || ! this.triangles || ! this.instanceTable ) {
|
|
2602
2603
|
|
|
@@ -2648,13 +2649,19 @@ export class SceneProcessor {
|
|
|
2648
2649
|
// can be released as soon as it is read. Callers hand over world space; triangles are
|
|
2649
2650
|
// stored per instance, so each mesh comes back through its own inverse.
|
|
2650
2651
|
const table = this.instanceTable;
|
|
2652
|
+
const FPT = TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE;
|
|
2653
|
+
const triRanges = [];
|
|
2654
|
+
const blasRanges = [];
|
|
2655
|
+
let owners = 0;
|
|
2651
2656
|
for ( let i = 0; i < table.count; i ++ ) {
|
|
2652
2657
|
|
|
2653
2658
|
if ( ! table.isSet[ i ] || ! table.isOwner( i ) ) continue;
|
|
2659
|
+
owners ++;
|
|
2654
2660
|
|
|
2655
2661
|
const p = positionsFor( i );
|
|
2656
2662
|
if ( ! p ) continue;
|
|
2657
2663
|
this._updateMeshTrianglePositions( i, p );
|
|
2664
|
+
blasRanges.push( table.blasOffsetOf( i ), table.blasNodeCountOf( i ) );
|
|
2658
2665
|
|
|
2659
2666
|
// Smooth normals overwrite the face normals just computed, so they follow per mesh.
|
|
2660
2667
|
if ( normalsFor ) {
|
|
@@ -2664,8 +2671,18 @@ export class SceneProcessor {
|
|
|
2664
2671
|
|
|
2665
2672
|
}
|
|
2666
2673
|
|
|
2674
|
+
const offset = table.triOffsetOf( i ) * FPT, count = table.triCountOf( i ) * FPT;
|
|
2675
|
+
const last = triRanges.at( - 1 );
|
|
2676
|
+
if ( last && last.offset + last.count === offset ) last.count += count;
|
|
2677
|
+
else triRanges.push( { offset, count } );
|
|
2678
|
+
|
|
2667
2679
|
}
|
|
2668
2680
|
|
|
2681
|
+
const tlasNodeCount = table.tlasNodeCount;
|
|
2682
|
+
const partial = tlasNodeCount > 0 && blasRanges.length / 2 < owners;
|
|
2683
|
+
const bvhRanges = partial ? [ this.computeTLASDirtyRange() ] : null;
|
|
2684
|
+
for ( let r = 0; partial && r < blasRanges.length; r += 2 ) bvhRanges.push( { offset: blasRanges[ r ] * 16, count: blasRanges[ r + 1 ] * 16 } );
|
|
2685
|
+
|
|
2669
2686
|
return new Promise( ( resolve, reject ) => {
|
|
2670
2687
|
|
|
2671
2688
|
this._refitWorker.onmessage = ( e ) => {
|
|
@@ -2673,7 +2690,7 @@ export class SceneProcessor {
|
|
|
2673
2690
|
const msg = e.data;
|
|
2674
2691
|
if ( msg.type === 'refitComplete' ) {
|
|
2675
2692
|
|
|
2676
|
-
resolve( { refitTimeMs: msg.refitTimeMs } );
|
|
2693
|
+
resolve( { refitTimeMs: msg.refitTimeMs, triRanges, bvhRanges } );
|
|
2677
2694
|
|
|
2678
2695
|
} else if ( msg.type === 'error' ) {
|
|
2679
2696
|
|
|
@@ -2684,7 +2701,7 @@ export class SceneProcessor {
|
|
|
2684
2701
|
};
|
|
2685
2702
|
|
|
2686
2703
|
// Signal worker — no data transfer needed, everything is in shared memory
|
|
2687
|
-
this._refitWorker.postMessage( { type: 'refit' } );
|
|
2704
|
+
this._refitWorker.postMessage( { type: 'refit', blasRanges: partial ? blasRanges : null, tlasNodeCount } );
|
|
2688
2705
|
|
|
2689
2706
|
} );
|
|
2690
2707
|
|
|
@@ -2834,6 +2851,8 @@ export class SceneProcessor {
|
|
|
2834
2851
|
const start = performance.now();
|
|
2835
2852
|
const table = this.instanceTable;
|
|
2836
2853
|
const composed = this._transformScratch ??= new Float32Array( 16 );
|
|
2854
|
+
if ( this._movedPlacements?.length !== table.count ) this._movedPlacements = new Uint8Array( table.count );
|
|
2855
|
+
const movedMask = this._movedPlacements;
|
|
2837
2856
|
let moved = 0;
|
|
2838
2857
|
|
|
2839
2858
|
for ( const meshIndex of meshIndices ) {
|
|
@@ -2870,13 +2889,15 @@ export class SceneProcessor {
|
|
|
2870
2889
|
}
|
|
2871
2890
|
|
|
2872
2891
|
this._writeLeafMatrix( p );
|
|
2892
|
+
movedMask[ p ] = 1;
|
|
2873
2893
|
moved ++;
|
|
2874
2894
|
|
|
2875
2895
|
}
|
|
2876
2896
|
|
|
2877
2897
|
}
|
|
2878
2898
|
|
|
2879
|
-
this._refitTLAS();
|
|
2899
|
+
this._refitTLAS( movedMask );
|
|
2900
|
+
movedMask.fill( 0 );
|
|
2880
2901
|
|
|
2881
2902
|
return { refitTimeMs: performance.now() - start, placements: moved };
|
|
2882
2903
|
|
|
@@ -3357,6 +3378,8 @@ export class SceneProcessor {
|
|
|
3357
3378
|
/** Keep the chunked BVH and its u32 index view in step. @private */
|
|
3358
3379
|
_setBVHData( data ) {
|
|
3359
3380
|
|
|
3381
|
+
this._tlasLeafBoxesOf = null;
|
|
3382
|
+
|
|
3360
3383
|
const records = ! data ? null
|
|
3361
3384
|
: ( data instanceof ChunkedRecords
|
|
3362
3385
|
? data
|
|
@@ -3538,9 +3561,11 @@ export class SceneProcessor {
|
|
|
3538
3561
|
/**
|
|
3539
3562
|
* Refit TLAS AABBs in-place without rebuilding the tree structure.
|
|
3540
3563
|
* O(tlasNodeCount) bottom-up pass — much faster than full SAH rebuild.
|
|
3564
|
+
* @param {Uint8Array} [moved] - by placement: only these leaves' world boxes are recomputed, the
|
|
3565
|
+
* rest kept from the last pass. Every leaf when omitted, or when nothing valid was kept.
|
|
3541
3566
|
* @private
|
|
3542
3567
|
*/
|
|
3543
|
-
_refitTLAS() {
|
|
3568
|
+
_refitTLAS( moved = null ) {
|
|
3544
3569
|
|
|
3545
3570
|
const tlasNodeCount = this.instanceTable.tlasNodeCount;
|
|
3546
3571
|
|
|
@@ -3548,10 +3573,12 @@ export class SceneProcessor {
|
|
|
3548
3573
|
if ( ! this._tlasBounds || this._tlasBounds.length < tlasNodeCount * 6 ) {
|
|
3549
3574
|
|
|
3550
3575
|
this._tlasBounds = new Float32Array( tlasNodeCount * 6 );
|
|
3576
|
+
this._tlasLeafBoxesOf = null;
|
|
3551
3577
|
|
|
3552
3578
|
}
|
|
3553
3579
|
|
|
3554
3580
|
const table = this.instanceTable;
|
|
3581
|
+
const keep = moved && this._tlasLeafBoxesOf === table;
|
|
3555
3582
|
|
|
3556
3583
|
// Bottom-up pass: reverse iteration over TLAS nodes
|
|
3557
3584
|
for ( let i = tlasNodeCount - 1; i >= 0; i -- ) {
|
|
@@ -3567,7 +3594,7 @@ export class SceneProcessor {
|
|
|
3567
3594
|
// every placement of a shared geometry onto one box, so all but one copy sat
|
|
3568
3595
|
// outside its own bounds and rays walked straight past it.
|
|
3569
3596
|
const entryIndex = idxChunk[ o + 1 ] & TLAS_PLACEMENT_MASK;
|
|
3570
|
-
if ( entryIndex < table.count ) {
|
|
3597
|
+
if ( entryIndex < table.count && ! ( keep && ! moved[ entryIndex ] ) ) {
|
|
3571
3598
|
|
|
3572
3599
|
table.writeWorldAABB( entryIndex, this._tlasBounds, i * 6 );
|
|
3573
3600
|
|
|
@@ -3608,6 +3635,8 @@ export class SceneProcessor {
|
|
|
3608
3635
|
|
|
3609
3636
|
}
|
|
3610
3637
|
|
|
3638
|
+
this._tlasLeafBoxesOf = table;
|
|
3639
|
+
|
|
3611
3640
|
}
|
|
3612
3641
|
|
|
3613
3642
|
/**
|
|
@@ -56,7 +56,39 @@ fn rayzee_source( x: u32, y: u32 ) -> vec4<f32> {
|
|
|
56
56
|
}`,
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
// RGBA bytes for a picture, or four float planes (r, g, b, a) for a network that wants the curve
|
|
60
|
+
// unquantized. The planes take a 2.2 power, not the sRGB curve: what the AI upscaler has always fed
|
|
61
|
+
// its network, so its output does not move.
|
|
62
|
+
const OUTPUTS = {
|
|
63
|
+
rgba8: /* wgsl */ `
|
|
64
|
+
@group(0) @binding(1) var<storage, read_write> dst: array<u32>;
|
|
65
|
+
|
|
66
|
+
fn rayzee_store( i: u32, linear: vec4<f32>, mapped: vec3<f32> ) {
|
|
67
|
+
let b8 = rayzee_to_u8( rayzee_encode( mapped, params.mode ) );
|
|
68
|
+
var a8 = 255u;
|
|
69
|
+
if ( params.alpha == 1u ) { a8 = rayzee_to_u8( vec3<f32>( linear.a ) ).x; }
|
|
70
|
+
dst[ i ] = b8.x | ( b8.y << 8u ) | ( b8.z << 16u ) | ( a8 << 24u );
|
|
71
|
+
}`,
|
|
72
|
+
planar: /* wgsl */ `
|
|
73
|
+
@group(0) @binding(1) var<storage, read_write> dst: array<f32>;
|
|
74
|
+
|
|
75
|
+
fn rayzee_store( i: u32, linear: vec4<f32>, mapped: vec3<f32> ) {
|
|
76
|
+
var c = clamp( mapped, vec3<f32>( 0.0 ), vec3<f32>( 1.0 ) );
|
|
77
|
+
if ( ! rayzee_output_encoded( params.mode ) ) {
|
|
78
|
+
let m = max( mapped, vec3<f32>( 0.0 ) );
|
|
79
|
+
c = select( pow( m, vec3<f32>( 1.0 / 2.2 ) ), vec3<f32>( 0.0 ), m <= vec3<f32>( 0.0 ) );
|
|
80
|
+
}
|
|
81
|
+
let n = params.width * params.height;
|
|
82
|
+
dst[ i ] = c.r;
|
|
83
|
+
dst[ n + i ] = c.g;
|
|
84
|
+
dst[ 2u * n + i ] = c.b;
|
|
85
|
+
dst[ 3u * n + i ] = linear.a;
|
|
86
|
+
}`,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const BYTES_PER_PIXEL = { rgba8: 4, planar: 16 };
|
|
90
|
+
|
|
91
|
+
const toneMapPassWGSL = ( transformWGSL, input, output ) => /* wgsl */ `
|
|
60
92
|
struct Params {
|
|
61
93
|
width: u32,
|
|
62
94
|
height: u32,
|
|
@@ -67,11 +99,11 @@ struct Params {
|
|
|
67
99
|
alpha: u32,
|
|
68
100
|
};
|
|
69
101
|
|
|
70
|
-
@group(0) @binding(1) var<storage, read_write> dst: array<u32>;
|
|
71
102
|
@group(0) @binding(2) var<uniform> params: Params;
|
|
72
103
|
${SOURCES[ input ]}
|
|
73
104
|
|
|
74
105
|
${transformWGSL}
|
|
106
|
+
${OUTPUTS[ output ]}
|
|
75
107
|
|
|
76
108
|
@compute @workgroup_size(8, 8)
|
|
77
109
|
fn main( @builtin(global_invocation_id) gid: vec3<u32> ) {
|
|
@@ -81,11 +113,7 @@ fn main( @builtin(global_invocation_id) gid: vec3<u32> ) {
|
|
|
81
113
|
if ( params.flipY == 1u ) { srcY = params.height - 1u - gid.y; }
|
|
82
114
|
|
|
83
115
|
let linear = rayzee_source( gid.x, srcY );
|
|
84
|
-
|
|
85
|
-
let b8 = rayzee_to_u8( rayzee_encode( mapped, params.mode ) );
|
|
86
|
-
var a8 = 255u;
|
|
87
|
-
if ( params.alpha == 1u ) { a8 = rayzee_to_u8( vec3<f32>( linear.a ) ).x; }
|
|
88
|
-
dst[ gid.y * params.width + gid.x ] = b8.x | ( b8.y << 8u ) | ( b8.z << 16u ) | ( a8 << 24u );
|
|
116
|
+
rayzee_store( gid.y * params.width + gid.x, linear, rayzee_tone_map( linear.rgb, params.mode, params.exposure, params.saturation ) );
|
|
89
117
|
}
|
|
90
118
|
`;
|
|
91
119
|
|
|
@@ -94,17 +122,18 @@ const PARAMS_BYTES = 32;
|
|
|
94
122
|
|
|
95
123
|
/**
|
|
96
124
|
* Tone-maps a tightly packed rgba16float buffer (two `u32` per pixel), or with `input: 'texture'` a
|
|
97
|
-
* float texture read by pixel, into RGBA bytes.
|
|
125
|
+
* float texture read by pixel, into RGBA bytes — or with `output: 'planar'` into float planes.
|
|
98
126
|
*
|
|
99
127
|
* Bound to one device and one image size; `ensureSize()` reallocates when the size changes.
|
|
100
128
|
*/
|
|
101
129
|
export class PackedToneMapper {
|
|
102
130
|
|
|
103
|
-
constructor( device, label = 'rayzee:tonemap', { input = 'packed' } = {} ) {
|
|
131
|
+
constructor( device, label = 'rayzee:tonemap', { input = 'packed', output = 'rgba8' } = {} ) {
|
|
104
132
|
|
|
105
133
|
this.device = device;
|
|
106
134
|
this.label = label;
|
|
107
135
|
this.input = input;
|
|
136
|
+
this.output = output;
|
|
108
137
|
this.width = 0;
|
|
109
138
|
this.height = 0;
|
|
110
139
|
this._pipeline = null;
|
|
@@ -139,7 +168,7 @@ export class PackedToneMapper {
|
|
|
139
168
|
label: this.label,
|
|
140
169
|
layout: 'auto',
|
|
141
170
|
compute: {
|
|
142
|
-
module: this.device.createShaderModule( { label: this.label, code: toneMapPassWGSL( wgsl, this.input ) } ),
|
|
171
|
+
module: this.device.createShaderModule( { label: this.label, code: toneMapPassWGSL( wgsl, this.input, this.output ) } ),
|
|
143
172
|
entryPoint: 'main',
|
|
144
173
|
},
|
|
145
174
|
} );
|
|
@@ -202,7 +231,7 @@ export class PackedToneMapper {
|
|
|
202
231
|
|
|
203
232
|
this._releaseBuffers();
|
|
204
233
|
|
|
205
|
-
const bytes = width * height *
|
|
234
|
+
const bytes = width * height * BYTES_PER_PIXEL[ this.output ];
|
|
206
235
|
this._storage = this.device.createBuffer( {
|
|
207
236
|
label: `${this.label}-out`,
|
|
208
237
|
size: bytes,
|
|
@@ -235,7 +264,26 @@ export class PackedToneMapper {
|
|
|
235
264
|
* @param {boolean} [tone.preserveAlpha=false] - the source's alpha, rounded as the CPU rounds it; else 255
|
|
236
265
|
* @returns {Promise<Uint8ClampedArray>} RGBA bytes, `width * height * 4`
|
|
237
266
|
*/
|
|
238
|
-
async toRGBA8( src,
|
|
267
|
+
async toRGBA8( src, tone ) {
|
|
268
|
+
|
|
269
|
+
return new Uint8ClampedArray( await this._run( src, tone ) );
|
|
270
|
+
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* The same tone map into four float planes — r, g, b, then the source's alpha — each `width * height`.
|
|
275
|
+
* Needs `output: 'planar'`.
|
|
276
|
+
* @param {GPUBuffer|GPUTexture} src - as for toRGBA8
|
|
277
|
+
* @param {object} tone - as for toRGBA8; `preserveAlpha` does not apply
|
|
278
|
+
* @returns {Promise<Float32Array>}
|
|
279
|
+
*/
|
|
280
|
+
async toPlanar( src, tone ) {
|
|
281
|
+
|
|
282
|
+
return new Float32Array( await this._run( src, tone ) );
|
|
283
|
+
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async _run( src, { exposure = 1, toneMapping = 0, saturation = 1, flipY = false, preserveAlpha = false } = {} ) {
|
|
239
287
|
|
|
240
288
|
if ( this.disposed ) throw new Error( 'PackedToneMapper: disposed' );
|
|
241
289
|
if ( ! this._storage ) throw new Error( 'PackedToneMapper: call ensureSize() first' );
|
|
@@ -246,6 +294,7 @@ export class PackedToneMapper {
|
|
|
246
294
|
this._ensureTables();
|
|
247
295
|
|
|
248
296
|
const { width, height } = this;
|
|
297
|
+
const bytes = width * height * BYTES_PER_PIXEL[ this.output ];
|
|
249
298
|
|
|
250
299
|
this._paramU32[ 0 ] = width;
|
|
251
300
|
this._paramU32[ 1 ] = height;
|
|
@@ -275,13 +324,13 @@ export class PackedToneMapper {
|
|
|
275
324
|
pass.setBindGroup( 0, group );
|
|
276
325
|
pass.dispatchWorkgroups( Math.ceil( width / 8 ), Math.ceil( height / 8 ) );
|
|
277
326
|
pass.end();
|
|
278
|
-
encoder.copyBufferToBuffer( this._storage, 0, this._map, 0,
|
|
327
|
+
encoder.copyBufferToBuffer( this._storage, 0, this._map, 0, bytes );
|
|
279
328
|
this.device.queue.submit( [ encoder.finish() ] );
|
|
280
329
|
|
|
281
330
|
await this._map.mapAsync( GPUMapMode.READ );
|
|
282
|
-
const
|
|
331
|
+
const out = this._map.getMappedRange().slice( 0 );
|
|
283
332
|
this._map.unmap();
|
|
284
|
-
return
|
|
333
|
+
return out;
|
|
285
334
|
|
|
286
335
|
}
|
|
287
336
|
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Protocol:
|
|
5
5
|
* 'init' → receives the shared triangle and node buffers (once per scene)
|
|
6
|
-
* 'refit' → recomputes every node's AABB from the triangles already written there
|
|
6
|
+
* 'refit' → recomputes every node's AABB from the triangles already written there, or with
|
|
7
|
+
* `blasRanges` only those BLASes and the TLAS
|
|
7
8
|
*
|
|
8
9
|
* Positions never reach here: the main thread scatters each mesh into the shared triangle
|
|
9
10
|
* records as it reads it, so no buffer the size of the scene exists on either side.
|
|
@@ -45,7 +46,9 @@ self.onmessage = function ( e ) {
|
|
|
45
46
|
|
|
46
47
|
const startTime = performance.now();
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
const { blasRanges, tlasNodeCount } = e.data;
|
|
50
|
+
if ( blasRanges ) refitter.refitPartial( bvhData, triData, blasRanges, tlasNodeCount );
|
|
51
|
+
else refitter.refit( bvhData, triData, nodeCount );
|
|
49
52
|
|
|
50
53
|
self.postMessage( {
|
|
51
54
|
type: 'refitComplete',
|
|
@@ -1088,26 +1088,48 @@ export class PathTracerStage extends RenderStage {
|
|
|
1088
1088
|
|
|
1089
1089
|
}
|
|
1090
1090
|
|
|
1091
|
-
/**
|
|
1092
|
-
|
|
1091
|
+
/**
|
|
1092
|
+
* Re-uploads triangles from the CPU records, adopting `triangleData` as them when given.
|
|
1093
|
+
* @param {Object} [triangleData]
|
|
1094
|
+
* @param {Array<{offset:number,count:number}>} [ranges] - only these, in record lanes; every triangle when omitted
|
|
1095
|
+
*/
|
|
1096
|
+
updateTriangleData( triangleData, ranges ) {
|
|
1093
1097
|
|
|
1094
1098
|
if ( triangleData?.chunks?.length > 1 ) this._triangleRecords = triangleData;
|
|
1095
1099
|
else if ( triangleData ) this._triangleFlat = triangleData.chunks ? triangleData.chunks[ 0 ] : triangleData;
|
|
1096
|
-
this.
|
|
1100
|
+
if ( ranges ) this.updateBufferRanges( ranges, [] );
|
|
1101
|
+
else this._uploadTriangles( 0, this._triangleRecordCount );
|
|
1097
1102
|
|
|
1098
1103
|
}
|
|
1099
1104
|
|
|
1100
|
-
/**
|
|
1101
|
-
|
|
1105
|
+
/**
|
|
1106
|
+
* Update BVH node data in the existing GPU buffer.
|
|
1107
|
+
* @param {Object} [bvhData]
|
|
1108
|
+
* @param {Array<{offset:number,count:number}>} [ranges] - only these, in floats; every node when omitted
|
|
1109
|
+
*/
|
|
1110
|
+
updateBVHData( bvhData, ranges ) {
|
|
1102
1111
|
|
|
1103
1112
|
if ( this._bvhRecords ) {
|
|
1104
1113
|
|
|
1105
|
-
|
|
1114
|
+
if ( bvhData?.chunks?.length > 1 ) this._bvhRecords = bvhData;
|
|
1115
|
+
if ( ranges ) this.updateBufferRanges( [], ranges );
|
|
1116
|
+
else uploadStorageChunks( this.renderer, this.bvhStorageAttr, this._bvhRecords.chunks );
|
|
1117
|
+
return;
|
|
1118
|
+
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
const flat = bvhData?.chunks ? bvhData.chunks[ 0 ] : bvhData;
|
|
1122
|
+
if ( ! ranges ) {
|
|
1123
|
+
|
|
1124
|
+
this._updateStorageBuffer( this.bvhStorageAttr, flat );
|
|
1106
1125
|
return;
|
|
1107
1126
|
|
|
1108
1127
|
}
|
|
1109
1128
|
|
|
1110
|
-
|
|
1129
|
+
// The attribute keeps its own array once the refit has moved the nodes into shared memory.
|
|
1130
|
+
const own = this.bvhStorageAttr?.array;
|
|
1131
|
+
if ( flat && own && flat !== own ) for ( const r of ranges ) own.set( flat.subarray( r.offset, r.offset + r.count ), r.offset );
|
|
1132
|
+
this.updateBufferRanges( [], ranges );
|
|
1111
1133
|
|
|
1112
1134
|
}
|
|
1113
1135
|
|
|
@@ -10,6 +10,23 @@
|
|
|
10
10
|
import { AnimationMixer, EventDispatcher, Timer, Vector3, LoopRepeat, LoopOnce, PropertyBinding } from 'three';
|
|
11
11
|
import { EngineEvents } from '../EngineEvents.js';
|
|
12
12
|
|
|
13
|
+
/** The nodes of `nodes` with no ancestor in it, or null when updating each would cost more than one pass over `root`. */
|
|
14
|
+
function subtreeRoots( nodes, root ) {
|
|
15
|
+
|
|
16
|
+
const roots = [ ...nodes ].filter( node => {
|
|
17
|
+
|
|
18
|
+
for ( let o = node.parent; o; o = o.parent ) if ( nodes.has( o ) ) return false;
|
|
19
|
+
return true;
|
|
20
|
+
|
|
21
|
+
} );
|
|
22
|
+
|
|
23
|
+
let total = 0, covered = 0;
|
|
24
|
+
root.traverse?.( () => total ++ );
|
|
25
|
+
for ( const node of roots ) node.traverse?.( () => covered ++ );
|
|
26
|
+
return covered + roots.length / 2 < total ? roots : null;
|
|
27
|
+
|
|
28
|
+
}
|
|
29
|
+
|
|
13
30
|
export class AnimationManager extends EventDispatcher {
|
|
14
31
|
|
|
15
32
|
constructor() {
|
|
@@ -39,6 +56,8 @@ export class AnimationManager extends EventDispatcher {
|
|
|
39
56
|
this._slotOf = null; // meshIndex -> row in the two caches below, or -1
|
|
40
57
|
this._lastWorld = null; // world matrix last handed to applyPoseCallback, 16 per row
|
|
41
58
|
this._lastVisible = null; // 0 / 1, or 2 before the first pose
|
|
59
|
+
this._lastMeshWorld = null; // deforming clips: world matrix at each mesh's last read, 16 per mesh
|
|
60
|
+
this._lastMorphWeights = null;
|
|
42
61
|
|
|
43
62
|
/** Injected by PathTracerApp — wakes the render loop after play/resume. */
|
|
44
63
|
this.wakeCallback = null;
|
|
@@ -135,6 +154,8 @@ export class AnimationManager extends EventDispatcher {
|
|
|
135
154
|
// 9 floats × every triangle — 1,030 MB at 30M, past what a renderer can allocate.
|
|
136
155
|
this._skinnedCache = [];
|
|
137
156
|
this._meshPositions = [];
|
|
157
|
+
this._lastMeshWorld = this._rigid ? null : new Float64Array( meshes.length * 16 ).fill( NaN );
|
|
158
|
+
this._lastMorphWeights = [];
|
|
138
159
|
|
|
139
160
|
const skinnedCount = meshes.filter( m => m.isSkinnedMesh ).length;
|
|
140
161
|
console.debug( `[AnimationManager] Init: ${animations.length} clips, ${meshes.length} meshes (${skinnedCount} skinned), ${offset} triangles, ${this._rigid ? 'rigid' : 'deforming'}` );
|
|
@@ -158,11 +179,19 @@ export class AnimationManager extends EventDispatcher {
|
|
|
158
179
|
|
|
159
180
|
const animated = new Set();
|
|
160
181
|
let visibility = false;
|
|
182
|
+
let unresolved = false;
|
|
161
183
|
for ( const track of clip.tracks ) {
|
|
162
184
|
|
|
163
185
|
const { nodeName, propertyName } = PropertyBinding.parseTrackName( track.name );
|
|
164
|
-
|
|
165
|
-
|
|
186
|
+
// As PropertyBinding.findNode: no node name binds the root itself.
|
|
187
|
+
const node = nodeName === '' || nodeName === '.' ? root : byName.get( nodeName );
|
|
188
|
+
if ( ! node ) {
|
|
189
|
+
|
|
190
|
+
unresolved = true;
|
|
191
|
+
continue;
|
|
192
|
+
|
|
193
|
+
}
|
|
194
|
+
|
|
166
195
|
animated.add( node );
|
|
167
196
|
if ( propertyName === 'visible' ) visibility = true;
|
|
168
197
|
|
|
@@ -187,7 +216,7 @@ export class AnimationManager extends EventDispatcher {
|
|
|
187
216
|
const cameras = new Set();
|
|
188
217
|
for ( const node of animated ) node.traverse( o => o.isCamera && cameras.add( o ) );
|
|
189
218
|
|
|
190
|
-
return { meshes: Int32Array.from( meshes ), visibility, cameras: [ ...cameras ] };
|
|
219
|
+
return { meshes: Int32Array.from( meshes ), visibility, cameras: [ ...cameras ], nodes: unresolved ? null : animated, roots: unresolved ? null : subtreeRoots( animated, root ) };
|
|
191
220
|
|
|
192
221
|
} );
|
|
193
222
|
|
|
@@ -214,15 +243,18 @@ export class AnimationManager extends EventDispatcher {
|
|
|
214
243
|
|
|
215
244
|
const meshes = new Set(), cameras = new Set();
|
|
216
245
|
let visibility = false;
|
|
246
|
+
let nodes = new Set();
|
|
217
247
|
for ( const plan of this._plans ) {
|
|
218
248
|
|
|
219
249
|
plan.meshes.forEach( m => meshes.add( m ) );
|
|
220
250
|
plan.cameras.forEach( c => cameras.add( c ) );
|
|
221
251
|
visibility ||= plan.visibility;
|
|
252
|
+
if ( nodes && plan.nodes ) plan.nodes.forEach( n => nodes.add( n ) );
|
|
253
|
+
else nodes = null;
|
|
222
254
|
|
|
223
255
|
}
|
|
224
256
|
|
|
225
|
-
this._allPlan = { meshes: Int32Array.from( meshes ), visibility, cameras: [ ...cameras ] };
|
|
257
|
+
this._allPlan = { meshes: Int32Array.from( meshes ), visibility, cameras: [ ...cameras ], nodes, roots: nodes ? subtreeRoots( nodes, this._mixerRoot ) : null };
|
|
226
258
|
|
|
227
259
|
}
|
|
228
260
|
|
|
@@ -487,7 +519,10 @@ export class AnimationManager extends EventDispatcher {
|
|
|
487
519
|
|
|
488
520
|
// Bones live outside mesh subtrees so per-mesh updateMatrixWorld() misses them. Doing it
|
|
489
521
|
// once here, on mixerRoot rather than the scene, keeps unrelated static objects out of it.
|
|
490
|
-
|
|
522
|
+
// A rigid clip moves only what its tracks name, so only those subtrees are updated.
|
|
523
|
+
const roots = this._rigid ? this._planFor( this._activeClip )?.roots : null;
|
|
524
|
+
if ( roots ) for ( const node of roots ) node.updateMatrixWorld( true );
|
|
525
|
+
else this._mixerRoot.updateMatrixWorld( true );
|
|
491
526
|
|
|
492
527
|
if ( this._rigid ) {
|
|
493
528
|
|
|
@@ -499,7 +534,52 @@ export class AnimationManager extends EventDispatcher {
|
|
|
499
534
|
const cameras = this._planFor( this._activeClip )?.cameras;
|
|
500
535
|
if ( cameras?.length ) this.applyPoseCallback?.( { meshIndices: [], visibilityChanged: false, cameras } );
|
|
501
536
|
|
|
502
|
-
|
|
537
|
+
// Decided once per reader: an expanded instanced emitter asks for one mesh several times.
|
|
538
|
+
const moved = new Int8Array( this._meshes.length );
|
|
539
|
+
return m => {
|
|
540
|
+
|
|
541
|
+
if ( moved[ m ] === 0 ) moved[ m ] = this._meshes[ m ].isSkinnedMesh || this._poseChanged( m ) ? 1 : - 1;
|
|
542
|
+
return moved[ m ] > 0 ? this._computeMeshPositions( m ) : null;
|
|
543
|
+
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/** Whether a mesh's world matrix or morph weights differ from when its positions were last read. @private */
|
|
549
|
+
_poseChanged( m ) {
|
|
550
|
+
|
|
551
|
+
const mesh = this._meshes[ m ];
|
|
552
|
+
const last = this._lastMeshWorld;
|
|
553
|
+
const e = mesh.matrixWorld.elements;
|
|
554
|
+
const o = m * 16;
|
|
555
|
+
let changed = false;
|
|
556
|
+
|
|
557
|
+
for ( let i = 0; i < 16; i ++ ) {
|
|
558
|
+
|
|
559
|
+
if ( last[ o + i ] !== e[ i ] ) {
|
|
560
|
+
|
|
561
|
+
for ( let j = 0; j < 16; j ++ ) last[ o + j ] = e[ j ];
|
|
562
|
+
changed = true;
|
|
563
|
+
break;
|
|
564
|
+
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const weights = mesh.morphTargetInfluences;
|
|
570
|
+
if ( weights?.length ) {
|
|
571
|
+
|
|
572
|
+
const lastWeights = this._lastMorphWeights[ m ];
|
|
573
|
+
if ( ! lastWeights || lastWeights.length !== weights.length || lastWeights.some( ( w, i ) => w !== weights[ i ] ) ) {
|
|
574
|
+
|
|
575
|
+
this._lastMorphWeights[ m ] = Float64Array.from( weights );
|
|
576
|
+
changed = true;
|
|
577
|
+
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return changed;
|
|
503
583
|
|
|
504
584
|
}
|
|
505
585
|
|
|
@@ -640,6 +720,8 @@ export class AnimationManager extends EventDispatcher {
|
|
|
640
720
|
this._slotOf = null;
|
|
641
721
|
this._lastWorld = null;
|
|
642
722
|
this._lastVisible = null;
|
|
723
|
+
this._lastMeshWorld = null;
|
|
724
|
+
this._lastMorphWeights = null;
|
|
643
725
|
|
|
644
726
|
}
|
|
645
727
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EventDispatcher, MathUtils, OrthographicCamera, PerspectiveCamera, Quaternion, Vector3 } from 'three';
|
|
1
|
+
import { EventDispatcher, MathUtils, Matrix4, OrthographicCamera, PerspectiveCamera, Quaternion, Vector3 } from 'three';
|
|
2
2
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
3
3
|
import { EngineEvents, } from '../EngineEvents.js';
|
|
4
4
|
import { AF_DEFAULTS, CAMERA_PROJECTION_IDS } from '../EngineDefaults.js';
|
|
@@ -62,6 +62,7 @@ export class CameraManager extends EventDispatcher {
|
|
|
62
62
|
this._smoothedFocusDistance = null;
|
|
63
63
|
this._afPointDirty = false;
|
|
64
64
|
this._afSuspended = false;
|
|
65
|
+
this._afPick = null;
|
|
65
66
|
|
|
66
67
|
// Saved state for default camera when switching to model cameras
|
|
67
68
|
this._defaultCameraState = null;
|
|
@@ -833,13 +834,12 @@ export class CameraManager extends EventDispatcher {
|
|
|
833
834
|
&& stage.frameCount > 0
|
|
834
835
|
&& ! stage.isComplete ) return;
|
|
835
836
|
|
|
836
|
-
|
|
837
|
-
const validHit = this.interactionManager.pickSurface( this.afScreenPoint.x * 2 - 1, 1 - this.afScreenPoint.y * 2 );
|
|
837
|
+
const hitPoint = this._pickFocusPoint( stage.resetCount );
|
|
838
838
|
|
|
839
839
|
let rawDistance;
|
|
840
|
-
if (
|
|
840
|
+
if ( hitPoint ) {
|
|
841
841
|
|
|
842
|
-
rawDistance = viewDepth(
|
|
842
|
+
rawDistance = viewDepth( hitPoint, this.camera );
|
|
843
843
|
this._lastValidFocusDistance = rawDistance;
|
|
844
844
|
|
|
845
845
|
} else {
|
|
@@ -910,6 +910,32 @@ export class CameraManager extends EventDispatcher {
|
|
|
910
910
|
|
|
911
911
|
}
|
|
912
912
|
|
|
913
|
+
/** Picks again only on a new view or AF point, or a render reset, which every scene change makes. @private */
|
|
914
|
+
_pickFocusPoint( resetCount ) {
|
|
915
|
+
|
|
916
|
+
const camera = this.camera;
|
|
917
|
+
const { x, y } = this.afScreenPoint;
|
|
918
|
+
const last = this._afPick ??= { point: new Vector3(), hit: false, resetCount: NaN, x: NaN, y: NaN, view: new Matrix4(), projection: new Matrix4() };
|
|
919
|
+
|
|
920
|
+
if ( this._afPointDirty || resetCount !== last.resetCount || x !== last.x || y !== last.y
|
|
921
|
+
|| ! last.view.equals( camera.matrixWorld ) || ! last.projection.equals( camera.projectionMatrix ) ) {
|
|
922
|
+
|
|
923
|
+
// AF screen point (0 to 1, y down) to NDC.
|
|
924
|
+
const hit = this.interactionManager.pickSurface( x * 2 - 1, 1 - y * 2 );
|
|
925
|
+
last.hit = !! hit;
|
|
926
|
+
if ( hit ) last.point.copy( hit.point );
|
|
927
|
+
last.resetCount = resetCount;
|
|
928
|
+
last.x = x;
|
|
929
|
+
last.y = y;
|
|
930
|
+
last.view.copy( camera.matrixWorld );
|
|
931
|
+
last.projection.copy( camera.projectionMatrix );
|
|
932
|
+
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
return last.hit ? last.point : null;
|
|
936
|
+
|
|
937
|
+
}
|
|
938
|
+
|
|
913
939
|
/**
|
|
914
940
|
* Deferred dependency injection — InteractionManager needs the camera
|
|
915
941
|
* in its constructor, so it can't be passed during CameraManager creation.
|