rayzee 7.29.0 → 7.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/rayzee.es.js +3724 -3009
- package/dist/rayzee.umd.js +56 -56
- package/package.json +1 -1
- package/src/EngineDefaults.js +272 -19
- package/src/EngineIssues.js +1 -0
- package/src/PathTracerApp.js +10 -1
- package/src/Processor/GeometryExtractor.js +21 -3
- package/src/Processor/PackedRayBuffer.js +26 -5
- package/src/Processor/SceneProcessor.js +71 -24
- package/src/Processor/StorageTexturePool.js +8 -4
- package/src/Processor/TextureCreator.js +8 -9
- package/src/Processor/Workers/TexturesWorker.js +13 -14
- package/src/Processor/utils.js +0 -6
- package/src/RenderSettings.js +4 -4
- package/src/Stages/Compositor.js +1 -0
- package/src/Stages/NRD.js +1395 -0
- package/src/Stages/NormalDepth.js +8 -4
- package/src/Stages/PathTracerStage.js +4 -0
- package/src/TSL/Common.js +8 -1
- package/src/TSL/FinalWriteKernel.js +9 -4
- package/src/TSL/GenerateKernel.js +1 -1
- package/src/TSL/ShadeKernel.js +14 -2
- package/src/TSL/TextureSampling.js +3 -2
- package/src/index.js +5 -0
- package/src/managers/DenoisingManager.js +80 -21
package/package.json
CHANGED
package/src/EngineDefaults.js
CHANGED
|
@@ -224,6 +224,10 @@ export const ENGINE_DEFAULTS = {
|
|
|
224
224
|
asvgfQualityPreset: 'medium',
|
|
225
225
|
showAsvgfHeatmap: false,
|
|
226
226
|
|
|
227
|
+
// NRD (ReBLUR port) real-time denoiser — see NRD_DEFAULTS / NRD_QUALITY_PRESETS.
|
|
228
|
+
nrdQualityPreset: 'medium',
|
|
229
|
+
nrdDebugMode: 0,
|
|
230
|
+
|
|
227
231
|
// Auto-exposure settings
|
|
228
232
|
autoExposure: false,
|
|
229
233
|
autoExposureKeyValue: 0.18,
|
|
@@ -233,6 +237,16 @@ export const ENGINE_DEFAULTS = {
|
|
|
233
237
|
autoExposureAdaptSpeedDark: 0.5,
|
|
234
238
|
};
|
|
235
239
|
|
|
240
|
+
// Ray distance NormalDepth writes on a miss. Max finite half-float, so miss−miss diffs stay 0
|
|
241
|
+
// rather than Inf−Inf=NaN.
|
|
242
|
+
export const GBUFFER_MISS_DEPTH = 65504.0;
|
|
243
|
+
export const GBUFFER_MISS_THRESHOLD = 6e4;
|
|
244
|
+
|
|
245
|
+
// normHitDist = hitDist / (A + B·viewZ). A constant, not a uniform: the Shade write and the NRD
|
|
246
|
+
// decode must agree, and the normalization is a pure round trip.
|
|
247
|
+
export const NRD_HIT_DIST_A = 3.0;
|
|
248
|
+
export const NRD_HIT_DIST_B = 0.1;
|
|
249
|
+
|
|
236
250
|
// Albedo demodulation safety floor. ASVGF and BilateralFilter MUST use the
|
|
237
251
|
// same value — demod (`color / safeAlbedo`) and remod (`lighting * safeAlbedo`)
|
|
238
252
|
// only round-trip exactly when both sides agree.
|
|
@@ -307,6 +321,67 @@ export const ASVGF_QUALITY_PRESETS = {
|
|
|
307
321
|
}
|
|
308
322
|
};
|
|
309
323
|
|
|
324
|
+
// NRD ReBLUR port (Stages/NRD.js). Names follow nrd::ReblurSettings so NVIDIA's tuning notes apply.
|
|
325
|
+
export const NRD_DEFAULTS = {
|
|
326
|
+
maxAccumulatedFrameNum: 30,
|
|
327
|
+
maxFastAccumulatedFrameNum: 6,
|
|
328
|
+
maxStabilizedFrameNum: 63,
|
|
329
|
+
historyFixFrameNum: 3,
|
|
330
|
+
historyFixBasePixelStride: 14,
|
|
331
|
+
prepassBlurRadius: 30,
|
|
332
|
+
minBlurRadius: 1,
|
|
333
|
+
maxBlurRadius: 30,
|
|
334
|
+
lobeAngleFraction: 0.15,
|
|
335
|
+
roughnessFraction: 0.15,
|
|
336
|
+
planeDistanceSensitivity: 0.02,
|
|
337
|
+
minHitDistanceWeight: 0.1,
|
|
338
|
+
fastHistoryClampingSigmaScale: 2.0,
|
|
339
|
+
fireflySuppressorMinRelativeScale: 2.0,
|
|
340
|
+
enableAntiFirefly: true,
|
|
341
|
+
antilagLuminanceSigmaScale: 2.0,
|
|
342
|
+
antilagLuminanceSensitivity: 3.0,
|
|
343
|
+
disocclusionThreshold: 0.01,
|
|
344
|
+
convergenceS: 1.0,
|
|
345
|
+
convergenceB: 0.2,
|
|
346
|
+
convergenceP: 0.8,
|
|
347
|
+
// Share of the lobe the normal weight accepts before any history exists. NRD's own constant is
|
|
348
|
+
// 0.75, which its source flags as probably too much; at that width it smears curved surfaces.
|
|
349
|
+
lobeVolumePercent: 0.1,
|
|
350
|
+
// Progressive handover: input sample count at which the denoiser passes the render through untouched.
|
|
351
|
+
// 0 = 2 · maxAccumulatedFrameNum.
|
|
352
|
+
handoverFrames: 0,
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
// Keys a preset may override. Applying one resets every key to its default first, so a preset only
|
|
356
|
+
// states its deltas and `medium` can be empty.
|
|
357
|
+
export const NRD_PRESET_KEYS = [
|
|
358
|
+
'maxAccumulatedFrameNum', 'maxFastAccumulatedFrameNum', 'maxStabilizedFrameNum',
|
|
359
|
+
'historyFixFrameNum', 'prepassBlurRadius', 'maxBlurRadius', 'enableAntiFirefly',
|
|
360
|
+
];
|
|
361
|
+
|
|
362
|
+
export const NRD_QUALITY_PRESETS = {
|
|
363
|
+
// Short history + no pre-pass: most responsive, noisiest.
|
|
364
|
+
low: {
|
|
365
|
+
maxAccumulatedFrameNum: 16,
|
|
366
|
+
maxFastAccumulatedFrameNum: 4,
|
|
367
|
+
maxStabilizedFrameNum: 16,
|
|
368
|
+
historyFixFrameNum: 2,
|
|
369
|
+
prepassBlurRadius: 0,
|
|
370
|
+
maxBlurRadius: 20,
|
|
371
|
+
enableAntiFirefly: false,
|
|
372
|
+
},
|
|
373
|
+
// nrd::ReblurSettings defaults.
|
|
374
|
+
medium: {},
|
|
375
|
+
// Longer history and wider kernels: smoother, more lag on lighting change.
|
|
376
|
+
high: {
|
|
377
|
+
maxAccumulatedFrameNum: 45,
|
|
378
|
+
maxFastAccumulatedFrameNum: 8,
|
|
379
|
+
historyFixFrameNum: 4,
|
|
380
|
+
prepassBlurRadius: 40,
|
|
381
|
+
maxBlurRadius: 40,
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
|
|
310
385
|
export const CAMERA_RANGES = {
|
|
311
386
|
fov: {
|
|
312
387
|
min: 10,
|
|
@@ -591,11 +666,11 @@ export const TEXTURE_CONSTANTS = {
|
|
|
591
666
|
MAX_TEXTURE_SIZE: 8192,
|
|
592
667
|
// Default cap applied when no maxTextureSize is supplied (engine standalone use).
|
|
593
668
|
DEFAULT_MAX_TEXTURE_SIZE: 4096,
|
|
594
|
-
//
|
|
595
|
-
MAX_TEXTURES_LIMIT:
|
|
596
|
-
//
|
|
597
|
-
//
|
|
598
|
-
// footprint. 4 → ~8 bound material arrays (4 sRGB + 4 linear).
|
|
669
|
+
// Per-map-type ceiling before bucketing; the real cap is per (pool, bucket) in _bucketTextures.
|
|
670
|
+
MAX_TEXTURES_LIMIT: 4 * 256,
|
|
671
|
+
// Array shapes per colorSpace pool. Material maps are grouped into this many
|
|
672
|
+
// (width, height) classes so a small or oddly-shaped map no longer pays a large
|
|
673
|
+
// neighbour's footprint. 4 → ~8 bound material arrays (4 sRGB + 4 linear).
|
|
599
674
|
MATERIAL_BUCKET_COUNT: 4,
|
|
600
675
|
// Packing stride: a map's stored index encodes bucketId * BUCKET_LAYER_STRIDE + layer.
|
|
601
676
|
// Also the per-bucket layer cap. Kept at the WebGPU portable maxTextureArrayLayers floor (256)
|
|
@@ -603,30 +678,208 @@ export const TEXTURE_CONSTANTS = {
|
|
|
603
678
|
BUCKET_LAYER_STRIDE: 256,
|
|
604
679
|
};
|
|
605
680
|
|
|
606
|
-
//
|
|
607
|
-
|
|
608
|
-
|
|
681
|
+
// An RGBA8 row upload must be a multiple of 256 bytes, so bucket widths land on 64 texels.
|
|
682
|
+
const BUCKET_WIDTH_ALIGN = 64;
|
|
683
|
+
|
|
684
|
+
export function alignBucketWidth( width, maxTextureSize ) {
|
|
685
|
+
|
|
686
|
+
const aligned = Math.ceil( Math.max( 1, width ) / BUCKET_WIDTH_ALIGN ) * BUCKET_WIDTH_ALIGN;
|
|
687
|
+
return Math.min( maxTextureSize, Math.max( TEXTURE_CONSTANTS.MIN_TEXTURE_WIDTH, aligned ) );
|
|
688
|
+
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function bucketOf( units ) {
|
|
692
|
+
|
|
693
|
+
let width = 0, height = 0, count = 0;
|
|
694
|
+
for ( const unit of units ) {
|
|
695
|
+
|
|
696
|
+
width = Math.max( width, unit.width );
|
|
697
|
+
height = Math.max( height, unit.height );
|
|
698
|
+
count += unit.count;
|
|
699
|
+
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return { units, width, height, count };
|
|
703
|
+
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Footprint times layer count.
|
|
707
|
+
function bucketCost( bucket, maxTextureSize ) {
|
|
708
|
+
|
|
709
|
+
return alignBucketWidth( bucket.width, maxTextureSize ) * bucket.height * bucket.count * 4;
|
|
710
|
+
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function totalCost( buckets, maxTextureSize ) {
|
|
714
|
+
|
|
715
|
+
return buckets.reduce( ( sum, b ) => sum + bucketCost( b, maxTextureSize ), 0 );
|
|
716
|
+
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Repeatedly fuse the pair of buckets whose union wastes least, until `count` remain.
|
|
720
|
+
function mergeDown( buckets, count, maxTextureSize ) {
|
|
721
|
+
|
|
722
|
+
let current = buckets.slice();
|
|
723
|
+
|
|
724
|
+
while ( current.length > count ) {
|
|
725
|
+
|
|
726
|
+
let best = null;
|
|
727
|
+
|
|
728
|
+
for ( let i = 0; i < current.length; i ++ ) {
|
|
729
|
+
|
|
730
|
+
for ( let j = i + 1; j < current.length; j ++ ) {
|
|
731
|
+
|
|
732
|
+
const fused = bucketOf( current[ i ].units.concat( current[ j ].units ) );
|
|
733
|
+
const delta = bucketCost( fused, maxTextureSize )
|
|
734
|
+
- bucketCost( current[ i ], maxTextureSize ) - bucketCost( current[ j ], maxTextureSize );
|
|
735
|
+
if ( ! best || delta < best.delta ) best = { i, j, delta, fused };
|
|
736
|
+
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
current = current.filter( ( _, k ) => k !== best.i && k !== best.j ).concat( [ best.fused ] );
|
|
742
|
+
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return current;
|
|
746
|
+
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// The pre-shape-aware layout, kept as a rival opening: square scenes still do well on it.
|
|
750
|
+
function ladderBuckets( units, count, maxTextureSize ) {
|
|
609
751
|
|
|
610
|
-
const
|
|
752
|
+
const ladder = [];
|
|
611
753
|
for ( let i = count - 1; i >= 0; i -- ) {
|
|
612
754
|
|
|
613
|
-
|
|
755
|
+
ladder.push( Math.max( TEXTURE_CONSTANTS.MIN_TEXTURE_WIDTH, Math.round( maxTextureSize / Math.pow( 2, i ) ) ) );
|
|
614
756
|
|
|
615
757
|
}
|
|
616
758
|
|
|
617
|
-
|
|
759
|
+
const bins = ladder.map( () => [] );
|
|
760
|
+
|
|
761
|
+
for ( const unit of units ) {
|
|
762
|
+
|
|
763
|
+
const longest = Math.pow( 2, Math.ceil( Math.log2( Math.max( unit.width, unit.height ) ) ) );
|
|
764
|
+
let slot = ladder.findIndex( size => longest <= size );
|
|
765
|
+
if ( slot < 0 ) slot = ladder.length - 1;
|
|
766
|
+
bins[ slot ].push( unit );
|
|
767
|
+
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return bins.filter( bin => bin.length ).map( bucketOf );
|
|
618
771
|
|
|
619
772
|
}
|
|
620
773
|
|
|
621
|
-
//
|
|
622
|
-
//
|
|
623
|
-
|
|
774
|
+
// Both openings above are greedy and stop short on mixed aspects; moving one shape at a time
|
|
775
|
+
// to whichever bucket makes the pool cheapest recovers the difference.
|
|
776
|
+
function refine( buckets, maxTextureSize ) {
|
|
777
|
+
|
|
778
|
+
let current = buckets;
|
|
779
|
+
|
|
780
|
+
for ( let pass = 0; pass < 8; pass ++ ) {
|
|
781
|
+
|
|
782
|
+
let improved = false;
|
|
783
|
+
|
|
784
|
+
for ( const unit of current.flatMap( b => b.units ) ) {
|
|
785
|
+
|
|
786
|
+
const from = current.findIndex( b => b.units.includes( unit ) );
|
|
787
|
+
if ( from < 0 ) continue;
|
|
788
|
+
|
|
789
|
+
for ( let to = 0; to < current.length; to ++ ) {
|
|
790
|
+
|
|
791
|
+
if ( to === from ) continue;
|
|
792
|
+
|
|
793
|
+
const moved = [];
|
|
794
|
+
for ( let i = 0; i < current.length; i ++ ) {
|
|
795
|
+
|
|
796
|
+
if ( i === from ) {
|
|
797
|
+
|
|
798
|
+
const rest = current[ i ].units.filter( u => u !== unit );
|
|
799
|
+
if ( rest.length ) moved.push( bucketOf( rest ) );
|
|
800
|
+
|
|
801
|
+
} else if ( i === to ) moved.push( bucketOf( current[ i ].units.concat( [ unit ] ) ) );
|
|
802
|
+
else moved.push( current[ i ] );
|
|
803
|
+
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if ( totalCost( moved, maxTextureSize ) < totalCost( current, maxTextureSize ) ) {
|
|
807
|
+
|
|
808
|
+
current = moved;
|
|
809
|
+
improved = true;
|
|
810
|
+
break;
|
|
811
|
+
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if ( ! improved ) break;
|
|
819
|
+
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
return current;
|
|
823
|
+
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Choose up to `count` array shapes for one colorSpace pool. Grouping by longest edge alone
|
|
828
|
+
* forces a 2000x453 banner into a 2048x2048 array; grouping on both axes does not.
|
|
829
|
+
*
|
|
830
|
+
* @param {Array<{width: number, height: number}>} sizes - every texture in the pool
|
|
831
|
+
* @param {number} maxTextureSize
|
|
832
|
+
* @param {number} [count]
|
|
833
|
+
* @returns {Array<{width: number, height: number}>} ascending by footprint
|
|
834
|
+
*/
|
|
835
|
+
export function planTextureBuckets( sizes, maxTextureSize, count = TEXTURE_CONSTANTS.MATERIAL_BUCKET_COUNT ) {
|
|
836
|
+
|
|
837
|
+
const distinct = new Map();
|
|
838
|
+
|
|
839
|
+
for ( const { width, height } of sizes ) {
|
|
840
|
+
|
|
841
|
+
const w = Math.min( maxTextureSize, Math.max( 1, width || 1 ) );
|
|
842
|
+
const h = Math.min( maxTextureSize, Math.max( 1, height || 1 ) );
|
|
843
|
+
const key = `${w}x${h}`;
|
|
844
|
+
const seen = distinct.get( key );
|
|
845
|
+
if ( seen ) seen.count ++;
|
|
846
|
+
else distinct.set( key, { width: w, height: h, count: 1 } );
|
|
847
|
+
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const units = [ ...distinct.values() ];
|
|
851
|
+
let buckets = units.map( unit => bucketOf( [ unit ] ) );
|
|
852
|
+
|
|
853
|
+
if ( buckets.length > count ) {
|
|
854
|
+
|
|
855
|
+
const ladder = ladderBuckets( units, count, maxTextureSize );
|
|
856
|
+
const merged = mergeDown( buckets, count, maxTextureSize );
|
|
857
|
+
buckets = totalCost( ladder, maxTextureSize ) < totalCost( merged, maxTextureSize ) ? ladder : merged;
|
|
858
|
+
buckets = refine( buckets, maxTextureSize );
|
|
859
|
+
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return buckets
|
|
863
|
+
.map( b => ( { width: alignBucketWidth( b.width, maxTextureSize ), height: b.height } ) )
|
|
864
|
+
.sort( ( a, b ) => a.width * a.height - b.width * b.height );
|
|
865
|
+
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Index of the cheapest planned bucket that can hold a texture at its native size.
|
|
870
|
+
* A texture larger than every bucket lands in the largest and is downscaled there.
|
|
871
|
+
*
|
|
872
|
+
* @param {number} width
|
|
873
|
+
* @param {number} height
|
|
874
|
+
* @param {Array<{width: number, height: number}>} shapes - from {@link planTextureBuckets}
|
|
875
|
+
* @returns {number}
|
|
876
|
+
*/
|
|
877
|
+
export function getTextureBucketId( width, height, shapes ) {
|
|
624
878
|
|
|
625
|
-
const
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
return sizes.length - 1;
|
|
879
|
+
const w = Math.max( 1, width || 1 );
|
|
880
|
+
const h = Math.max( 1, height || 1 );
|
|
881
|
+
for ( let i = 0; i < shapes.length; i ++ ) if ( shapes[ i ].width >= w && shapes[ i ].height >= h ) return i;
|
|
882
|
+
return Math.max( 0, shapes.length - 1 );
|
|
630
883
|
|
|
631
884
|
}
|
|
632
885
|
|
package/src/EngineIssues.js
CHANGED
|
@@ -9,6 +9,7 @@ export const ISSUE_CODES = Object.freeze( {
|
|
|
9
9
|
ASSET_UNREACHABLE: 'asset.unreachable',
|
|
10
10
|
TEXTURE_BUILD_FAILED: 'texture.build_failed',
|
|
11
11
|
TEXTURE_PROCESSING_FALLBACK: 'texture.processing_fallback',
|
|
12
|
+
TEXTURE_LIMIT_EXCEEDED: 'texture.limit_exceeded',
|
|
12
13
|
ENVIRONMENT_LOAD_FAILED: 'environment.load_failed',
|
|
13
14
|
SETTING_UNKNOWN_KEY: 'setting.unknown_key',
|
|
14
15
|
RENDER_SIZE_DECLINED: 'render.size_declined',
|
package/src/PathTracerApp.js
CHANGED
|
@@ -9,6 +9,7 @@ import { PathTracer } from './Stages/PathTracer.js';
|
|
|
9
9
|
import { NormalDepth } from './Stages/NormalDepth.js';
|
|
10
10
|
import { MotionVector } from './Stages/MotionVector.js';
|
|
11
11
|
import { ASVGF } from './Stages/ASVGF.js';
|
|
12
|
+
import { NRD } from './Stages/NRD.js';
|
|
12
13
|
import { Variance } from './Stages/Variance.js';
|
|
13
14
|
import { BilateralFilter } from './Stages/BilateralFilter.js';
|
|
14
15
|
import { EdgeFilter } from './Stages/EdgeFilter.js';
|
|
@@ -524,7 +525,12 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
524
525
|
if ( this.pipeline ) {
|
|
525
526
|
|
|
526
527
|
this.pipeline.reset();
|
|
527
|
-
if ( ! soft )
|
|
528
|
+
if ( ! soft ) {
|
|
529
|
+
|
|
530
|
+
this.pipeline.eventBus.emit( 'asvgf:reset' );
|
|
531
|
+
this.pipeline.eventBus.emit( 'denoiser:reset' );
|
|
532
|
+
|
|
533
|
+
}
|
|
528
534
|
|
|
529
535
|
}
|
|
530
536
|
|
|
@@ -2857,6 +2863,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2857
2863
|
this.pipeline.addStage( this.stages.pathTracer );
|
|
2858
2864
|
this.pipeline.addStage( this.stages.normalDepth );
|
|
2859
2865
|
this.pipeline.addStage( this.stages.motionVector );
|
|
2866
|
+
this.pipeline.addStage( this.stages.nrd );
|
|
2860
2867
|
this.pipeline.addStage( this.stages.asvgf );
|
|
2861
2868
|
this.pipeline.addStage( this.stages.variance );
|
|
2862
2869
|
this.pipeline.addStage( this.stages.bilateralFilter );
|
|
@@ -3090,6 +3097,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3090
3097
|
pathTracer: this.stages.pathTracer
|
|
3091
3098
|
} );
|
|
3092
3099
|
this.stages.asvgf = new ASVGF( this.renderer, { enabled: false } );
|
|
3100
|
+
this.stages.nrd = new NRD( this.renderer, { enabled: false, pathTracer: this.stages.pathTracer } );
|
|
3093
3101
|
this.stages.variance = new Variance( this.renderer, { enabled: false } );
|
|
3094
3102
|
this.stages.bilateralFilter = new BilateralFilter( this.renderer, { enabled: false } );
|
|
3095
3103
|
this.stages.edgeFilter = new EdgeFilter( this.renderer, { enabled: false } );
|
|
@@ -3114,6 +3122,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3114
3122
|
normalDepth: this.stages.normalDepth,
|
|
3115
3123
|
motionVector: this.stages.motionVector,
|
|
3116
3124
|
asvgf: this.stages.asvgf,
|
|
3125
|
+
nrd: this.stages.nrd,
|
|
3117
3126
|
variance: this.stages.variance,
|
|
3118
3127
|
bilateralFilter: this.stages.bilateralFilter,
|
|
3119
3128
|
edgeFilter: this.stages.edgeFilter,
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Vector3, Vector2, Color, Matrix3, Matrix4, FrontSide, BackSide, DoubleSide, RGBAFormat } from "three";
|
|
2
|
-
import { TRIANGLE_DATA_LAYOUT } from '../EngineDefaults.js';
|
|
2
|
+
import { TEXTURE_CONSTANTS, TRIANGLE_DATA_LAYOUT } from '../EngineDefaults.js';
|
|
3
|
+
import { ISSUE_CODES } from '../EngineIssues.js';
|
|
3
4
|
import { createLogger, fmt, warnOnce } from '../utils/Logger.js';
|
|
4
5
|
|
|
5
6
|
const log = createLogger( 'geometry' );
|
|
6
7
|
|
|
7
|
-
const MAX_TEXTURES_LIMIT =
|
|
8
|
+
const MAX_TEXTURES_LIMIT = TEXTURE_CONSTANTS.MAX_TEXTURES_LIMIT;
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* glTF 2.0 alphaMode for a three.js material: 0 OPAQUE, 1 MASK, 2 BLEND.
|
|
@@ -55,7 +56,11 @@ export function deriveAlphaMode( material ) {
|
|
|
55
56
|
|
|
56
57
|
export class GeometryExtractor {
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
/** @param {{issues?: import('../EngineIssues.js').IssueLog}} [options] */
|
|
60
|
+
constructor( { issues = null } = {} ) {
|
|
61
|
+
|
|
62
|
+
this._issues = issues;
|
|
63
|
+
this._droppedTextures = 0;
|
|
59
64
|
|
|
60
65
|
// Object pools for reusing objects
|
|
61
66
|
this._vectorPool = {
|
|
@@ -528,6 +533,17 @@ export class GeometryExtractor {
|
|
|
528
533
|
|
|
529
534
|
}
|
|
530
535
|
|
|
536
|
+
this._droppedTextures ++;
|
|
537
|
+
if ( this._droppedTextures === 1 ) {
|
|
538
|
+
|
|
539
|
+
this._issues?.record(
|
|
540
|
+
ISSUE_CODES.TEXTURE_LIMIT_EXCEEDED,
|
|
541
|
+
`scene needs more than ${MAX_TEXTURES_LIMIT} distinct textures for one map type; the rest render untextured`,
|
|
542
|
+
{ limit: MAX_TEXTURES_LIMIT, firstDropped: texture.name || texture.source?.uuid }
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
}
|
|
546
|
+
|
|
531
547
|
return - 1;
|
|
532
548
|
|
|
533
549
|
}
|
|
@@ -800,6 +816,8 @@ export class GeometryExtractor {
|
|
|
800
816
|
|
|
801
817
|
resetArrays() {
|
|
802
818
|
|
|
819
|
+
this._droppedTextures = 0;
|
|
820
|
+
|
|
803
821
|
// Reset triangle data
|
|
804
822
|
this.triangleData = null;
|
|
805
823
|
this.triangleCount = 0;
|
|
@@ -16,9 +16,10 @@ export const RAY_STRIDE = 7;
|
|
|
16
16
|
export const HIT_STRIDE = 2;
|
|
17
17
|
// Per-pixel G-buffer (first-hit MRT staging): 1 uvec4/pixel — half-packed normal/depth/albedo
|
|
18
18
|
// (pack2x16, no f32 bitcast); read by FinalWrite:
|
|
19
|
-
// .x=packSnorm2x16(normal.xy) .y=packSnorm2x16(normal.z, depth) .z=packUnorm2x16(albedo.rg)
|
|
19
|
+
// .x=packSnorm2x16(normal.xy) .y=packSnorm2x16(normal.z, depth) .z=packUnorm2x16(albedo.rg)
|
|
20
|
+
// .w=packUnorm2x16(albedo.b, normHitDist)
|
|
21
|
+
// normHitDist is written by Shade; writeGBuffer preserves it so the DDFA albedo commits can't zero it.
|
|
20
22
|
// Separate buffer from RAY (per-pixel, not per-ray×S) — written by Generate/Shade bounce-0.
|
|
21
|
-
// (A second lane reserved for an A-SVGF Tier-1 surface ID was removed — write-only, never read.)
|
|
22
23
|
export const GBUFFER_STRIDE = 1;
|
|
23
24
|
|
|
24
25
|
export const RAY = {
|
|
@@ -162,15 +163,35 @@ export const readRayRadiance = ( buf, id ) =>
|
|
|
162
163
|
// One uvec4 per pixel (stride 1); AoS base = pixelIndex * GBUFFER_STRIDE.
|
|
163
164
|
const gbLane = ( pixelIndex ) => uint( pixelIndex ).mul( GBUFFER_STRIDE );
|
|
164
165
|
|
|
165
|
-
|
|
166
|
-
|
|
166
|
+
// hitDist = null keeps the lane's current normHitDist.
|
|
167
|
+
export const writeGBuffer = ( buf, pixelIndex, normal, depth, albedo, hitDist = null ) => {
|
|
168
|
+
|
|
169
|
+
const lane = gbLane( pixelIndex );
|
|
170
|
+
const h = hitDist === null ? unpackUnorm2x16( buf.element( lane ).w ).y : float( hitDist );
|
|
171
|
+
return buf.element( lane ).assign( uvec4(
|
|
167
172
|
packSnorm2x16( vec2( normal.x, normal.y ) ),
|
|
168
173
|
packSnorm2x16( vec2( normal.z, depth ) ),
|
|
169
174
|
packUnorm2x16( vec2( albedo.x, albedo.y ) ),
|
|
170
|
-
packUnorm2x16( vec2( albedo.z,
|
|
175
|
+
packUnorm2x16( vec2( albedo.z, h ) ),
|
|
176
|
+
) );
|
|
177
|
+
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export const writeGBufferHitDist = ( buf, pixelIndex, normHitDist ) => {
|
|
181
|
+
|
|
182
|
+
const lane = gbLane( pixelIndex );
|
|
183
|
+
const cur = buf.element( lane ).toVar();
|
|
184
|
+
return buf.element( lane ).assign( uvec4(
|
|
185
|
+
cur.x, cur.y, cur.z,
|
|
186
|
+
packUnorm2x16( vec2( unpackUnorm2x16( cur.w ).x, normHitDist ) ),
|
|
171
187
|
) );
|
|
188
|
+
|
|
189
|
+
};
|
|
190
|
+
|
|
172
191
|
export const readGBuffer = ( buf, pixelIndex ) => buf.element( gbLane( pixelIndex ) );
|
|
173
192
|
|
|
193
|
+
export const gbDecodeHitDist = ( packed ) => unpackUnorm2x16( packed.w ).y;
|
|
194
|
+
|
|
174
195
|
// Decode for FinalWrite. normalDepth.xyz matches the prior path (normal*0.5+0.5), .w = raw depth.
|
|
175
196
|
export const gbDecodeNormalDepth = ( packed ) => {
|
|
176
197
|
|
|
@@ -11,7 +11,8 @@ import { updateLoading } from '../Processor/utils.js';
|
|
|
11
11
|
import { BuildTimer } from './BuildTimer.js';
|
|
12
12
|
import { createLogger, fmt, workerLogLevel } from '../utils/Logger.js';
|
|
13
13
|
import { SRGBColorSpace } from 'three';
|
|
14
|
-
import { TRIANGLE_DATA_LAYOUT, TEXTURE_CONSTANTS, getTextureBucketId, packTextureIndex } from '../EngineDefaults.js';
|
|
14
|
+
import { TRIANGLE_DATA_LAYOUT, TEXTURE_CONSTANTS, getTextureBucketId, packTextureIndex, planTextureBuckets } from '../EngineDefaults.js';
|
|
15
|
+
import { ISSUE_CODES } from '../EngineIssues.js';
|
|
15
16
|
import BVHWorker from './Workers/BVHWorker.js?worker&inline';
|
|
16
17
|
import BVHRefitWorker from './Workers/BVHRefitWorker.js?worker&inline';
|
|
17
18
|
|
|
@@ -145,7 +146,7 @@ export class SceneProcessor {
|
|
|
145
146
|
_initProcessors() {
|
|
146
147
|
|
|
147
148
|
// Create and configure geometry extractor
|
|
148
|
-
this.geometryExtractor = new GeometryExtractor();
|
|
149
|
+
this.geometryExtractor = new GeometryExtractor( { issues: this.config.issues } );
|
|
149
150
|
|
|
150
151
|
// Create and configure BVH builder
|
|
151
152
|
this.bvhBuilder = new BVHBuilder();
|
|
@@ -869,8 +870,9 @@ export class SceneProcessor {
|
|
|
869
870
|
/**
|
|
870
871
|
* Group the extractor's seven per-type texture arrays into two consolidated colorSpace
|
|
871
872
|
* pools (sRGB: albedo+emissive; linear: normal/bump/roughness/metalness/displacement),
|
|
872
|
-
* each split into
|
|
873
|
-
* across types within a (pool, bucket)
|
|
873
|
+
* each split into at most MATERIAL_BUCKET_COUNT buckets whose shapes are planned from the
|
|
874
|
+
* pool's own texture dimensions. Textures are deduped across types within a (pool, bucket)
|
|
875
|
+
* so a shared image (e.g. ORM) costs one layer.
|
|
874
876
|
* @returns {{ srgbLists: Array<Array>, linearLists: Array<Array>, remap: Object }}
|
|
875
877
|
* bucket lists + per-type remap arrays (old per-type layer → packed bucket index).
|
|
876
878
|
* @private
|
|
@@ -881,6 +883,43 @@ export class SceneProcessor {
|
|
|
881
883
|
const K = TEXTURE_CONSTANTS.MATERIAL_BUCKET_COUNT;
|
|
882
884
|
const STRIDE = TEXTURE_CONSTANTS.BUCKET_LAYER_STRIDE;
|
|
883
885
|
|
|
886
|
+
const poolSizes = ( types ) => {
|
|
887
|
+
|
|
888
|
+
const seen = new Set();
|
|
889
|
+
const sizes = [];
|
|
890
|
+
|
|
891
|
+
for ( const arr of types ) {
|
|
892
|
+
|
|
893
|
+
for ( const tex of arr || [] ) {
|
|
894
|
+
|
|
895
|
+
if ( ! tex?.image ) continue;
|
|
896
|
+
const uuid = tex.source?.uuid ?? tex.uuid;
|
|
897
|
+
if ( seen.has( uuid ) ) continue;
|
|
898
|
+
seen.add( uuid );
|
|
899
|
+
sizes.push( { width: tex.image.width, height: tex.image.height } );
|
|
900
|
+
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
return sizes;
|
|
906
|
+
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
const srgbTypes = [ this.maps, this.emissiveMaps, this.sheenColorMaps, this.specularColorMaps ];
|
|
910
|
+
const linearTypes = [
|
|
911
|
+
this.normalMaps, this.bumpMaps, this.roughnessMaps, this.metalnessMaps, this.displacementMaps,
|
|
912
|
+
this.anisotropyMaps, this.transmissionMaps, this.clearcoatMaps, this.clearcoatRoughnessMaps,
|
|
913
|
+
this.sheenRoughnessMaps, this.iridescenceMaps, this.iridescenceThicknessMaps, this.specularIntensityMaps,
|
|
914
|
+
];
|
|
915
|
+
|
|
916
|
+
const srgbShapes = planTextureBuckets( poolSizes( srgbTypes ), cap, K );
|
|
917
|
+
const linearShapes = planTextureBuckets( poolSizes( linearTypes ), cap, K );
|
|
918
|
+
this._srgbBucketShapes = srgbShapes;
|
|
919
|
+
this._linearBucketShapes = linearShapes;
|
|
920
|
+
|
|
921
|
+
// Always K-length even when the plan needs fewer shapes: downstream nodes are built
|
|
922
|
+
// per MATERIAL_BUCKET_COUNT and a short array would leave stale bindings behind.
|
|
884
923
|
const srgbLists = Array.from( { length: K }, () => [] );
|
|
885
924
|
const linearLists = Array.from( { length: K }, () => [] );
|
|
886
925
|
const srgbDedup = Array.from( { length: K }, () => new Map() );
|
|
@@ -891,17 +930,25 @@ export class SceneProcessor {
|
|
|
891
930
|
this._srgbTexPacked = new Map();
|
|
892
931
|
this._linearTexPacked = new Map();
|
|
893
932
|
|
|
933
|
+
let bucketOverflowReported = false;
|
|
934
|
+
|
|
894
935
|
// Assign one texture to its (bucket, layer) within a pool; dedup by source uuid.
|
|
895
|
-
const assign = ( tex, lists, dedup, flat ) => {
|
|
936
|
+
const assign = ( tex, lists, dedup, flat, shapes ) => {
|
|
896
937
|
|
|
897
938
|
if ( ! tex || ! tex.image ) return - 1;
|
|
898
|
-
const bucket = getTextureBucketId( tex.image.width, tex.image.height,
|
|
939
|
+
const bucket = getTextureBucketId( tex.image.width, tex.image.height, shapes );
|
|
899
940
|
const uuid = tex.source?.uuid ?? tex.uuid;
|
|
900
941
|
const seen = dedup[ bucket ].get( uuid );
|
|
901
942
|
if ( seen !== undefined ) return packTextureIndex( bucket, seen );
|
|
902
943
|
if ( lists[ bucket ].length >= STRIDE ) {
|
|
903
944
|
|
|
904
945
|
log.warn( `texture bucket ${bucket} full (${STRIDE}); dropping a map` );
|
|
946
|
+
if ( ! bucketOverflowReported ) this.config.issues?.record(
|
|
947
|
+
ISSUE_CODES.TEXTURE_LIMIT_EXCEEDED,
|
|
948
|
+
`texture bucket ${bucket} full (${STRIDE} layers); the rest render untextured`,
|
|
949
|
+
{ bucket, limit: STRIDE }
|
|
950
|
+
);
|
|
951
|
+
bucketOverflowReported = true;
|
|
905
952
|
return - 1;
|
|
906
953
|
|
|
907
954
|
}
|
|
@@ -917,27 +964,27 @@ export class SceneProcessor {
|
|
|
917
964
|
|
|
918
965
|
// Per-type arrays hold unique textures indexed by the layer the extractor assigned
|
|
919
966
|
// (= array position), so remap[type][oldLayer] = packed index.
|
|
920
|
-
const remapType = ( arr, lists, dedup, flat ) => ( arr || [] ).map( tex => assign( tex, lists, dedup, flat ) );
|
|
967
|
+
const remapType = ( arr, lists, dedup, flat, shapes ) => ( arr || [] ).map( tex => assign( tex, lists, dedup, flat, shapes ) );
|
|
921
968
|
|
|
922
969
|
const remap = {
|
|
923
|
-
albedo: remapType( this.maps, srgbLists, srgbDedup, this._srgbTexPacked ),
|
|
924
|
-
emissive: remapType( this.emissiveMaps, srgbLists, srgbDedup, this._srgbTexPacked ),
|
|
925
|
-
normal: remapType( this.normalMaps, linearLists, linearDedup, this._linearTexPacked ),
|
|
926
|
-
bump: remapType( this.bumpMaps, linearLists, linearDedup, this._linearTexPacked ),
|
|
927
|
-
roughness: remapType( this.roughnessMaps, linearLists, linearDedup, this._linearTexPacked ),
|
|
928
|
-
metalness: remapType( this.metalnessMaps, linearLists, linearDedup, this._linearTexPacked ),
|
|
929
|
-
displacement: remapType( this.displacementMaps, linearLists, linearDedup, this._linearTexPacked ),
|
|
930
|
-
anisotropy: remapType( this.anisotropyMaps, linearLists, linearDedup, this._linearTexPacked ),
|
|
970
|
+
albedo: remapType( this.maps, srgbLists, srgbDedup, this._srgbTexPacked, srgbShapes ),
|
|
971
|
+
emissive: remapType( this.emissiveMaps, srgbLists, srgbDedup, this._srgbTexPacked, srgbShapes ),
|
|
972
|
+
normal: remapType( this.normalMaps, linearLists, linearDedup, this._linearTexPacked, linearShapes ),
|
|
973
|
+
bump: remapType( this.bumpMaps, linearLists, linearDedup, this._linearTexPacked, linearShapes ),
|
|
974
|
+
roughness: remapType( this.roughnessMaps, linearLists, linearDedup, this._linearTexPacked, linearShapes ),
|
|
975
|
+
metalness: remapType( this.metalnessMaps, linearLists, linearDedup, this._linearTexPacked, linearShapes ),
|
|
976
|
+
displacement: remapType( this.displacementMaps, linearLists, linearDedup, this._linearTexPacked, linearShapes ),
|
|
977
|
+
anisotropy: remapType( this.anisotropyMaps, linearLists, linearDedup, this._linearTexPacked, linearShapes ),
|
|
931
978
|
// Extension maps — data maps → linear pool; color maps (sheenColor, specularColor) → sRGB pool.
|
|
932
|
-
transmission: remapType( this.transmissionMaps, linearLists, linearDedup, this._linearTexPacked ),
|
|
933
|
-
clearcoat: remapType( this.clearcoatMaps, linearLists, linearDedup, this._linearTexPacked ),
|
|
934
|
-
clearcoatRoughness: remapType( this.clearcoatRoughnessMaps, linearLists, linearDedup, this._linearTexPacked ),
|
|
935
|
-
sheenColor: remapType( this.sheenColorMaps, srgbLists, srgbDedup, this._srgbTexPacked ),
|
|
936
|
-
sheenRoughness: remapType( this.sheenRoughnessMaps, linearLists, linearDedup, this._linearTexPacked ),
|
|
937
|
-
iridescence: remapType( this.iridescenceMaps, linearLists, linearDedup, this._linearTexPacked ),
|
|
938
|
-
iridescenceThickness: remapType( this.iridescenceThicknessMaps, linearLists, linearDedup, this._linearTexPacked ),
|
|
939
|
-
specularIntensity: remapType( this.specularIntensityMaps, linearLists, linearDedup, this._linearTexPacked ),
|
|
940
|
-
specularColor: remapType( this.specularColorMaps, srgbLists, srgbDedup, this._srgbTexPacked ),
|
|
979
|
+
transmission: remapType( this.transmissionMaps, linearLists, linearDedup, this._linearTexPacked, linearShapes ),
|
|
980
|
+
clearcoat: remapType( this.clearcoatMaps, linearLists, linearDedup, this._linearTexPacked, linearShapes ),
|
|
981
|
+
clearcoatRoughness: remapType( this.clearcoatRoughnessMaps, linearLists, linearDedup, this._linearTexPacked, linearShapes ),
|
|
982
|
+
sheenColor: remapType( this.sheenColorMaps, srgbLists, srgbDedup, this._srgbTexPacked, srgbShapes ),
|
|
983
|
+
sheenRoughness: remapType( this.sheenRoughnessMaps, linearLists, linearDedup, this._linearTexPacked, linearShapes ),
|
|
984
|
+
iridescence: remapType( this.iridescenceMaps, linearLists, linearDedup, this._linearTexPacked, linearShapes ),
|
|
985
|
+
iridescenceThickness: remapType( this.iridescenceThicknessMaps, linearLists, linearDedup, this._linearTexPacked, linearShapes ),
|
|
986
|
+
specularIntensity: remapType( this.specularIntensityMaps, linearLists, linearDedup, this._linearTexPacked, linearShapes ),
|
|
987
|
+
specularColor: remapType( this.specularColorMaps, srgbLists, srgbDedup, this._srgbTexPacked, srgbShapes ),
|
|
941
988
|
};
|
|
942
989
|
|
|
943
990
|
return { srgbLists, linearLists, remap };
|