rayzee 7.5.0 → 7.6.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 +1 -1
- package/dist/assets/{TexturesWorker-DBqGmVdR.js → TexturesWorker-6mlBy4Dm.js} +2 -2
- package/dist/assets/TexturesWorker-6mlBy4Dm.js.map +1 -0
- package/dist/rayzee.es.js +1826 -1660
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +56 -55
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/EngineDefaults.js +14 -1
- package/src/PathTracerApp.js +44 -0
- package/src/Processor/KernelManager.js +1 -2
- package/src/Processor/QueueManager.js +37 -1
- package/src/Processor/SceneProcessor.js +14 -2
- package/src/Processor/TextureCreator.js +27 -2
- package/src/Processor/Workers/TexturesWorker.js +3 -3
- package/src/Stages/PathTracer.js +75 -2
- package/src/TSL/Displacement.js +6 -6
- package/src/TSL/SortGlobalKernels.js +135 -0
- package/src/TSL/TextureSampling.js +3 -3
- package/src/TSL/patches.js +90 -0
- package/dist/assets/TexturesWorker-DBqGmVdR.js.map +0 -1
package/package.json
CHANGED
package/src/EngineDefaults.js
CHANGED
|
@@ -10,6 +10,11 @@ export const ENGINE_DEFAULTS = {
|
|
|
10
10
|
canvasWidth: 512,
|
|
11
11
|
canvasHeight: 512,
|
|
12
12
|
|
|
13
|
+
// Max material-texture dimension (longest edge) used when processing a scene's
|
|
14
|
+
// textures into GPU arrays. Larger = sharper textures but ~quadratic VRAM. Clamped
|
|
15
|
+
// to TEXTURE_CONSTANTS.MAX_TEXTURE_SIZE (hardware ceiling). Applied at scene load.
|
|
16
|
+
maxTextureSize: 4096,
|
|
17
|
+
|
|
13
18
|
toneMapping: 4,
|
|
14
19
|
exposure: 1,
|
|
15
20
|
saturation: 1.2,
|
|
@@ -88,6 +93,10 @@ export const ENGINE_DEFAULTS = {
|
|
|
88
93
|
performanceModeAdaptive: 'medium',
|
|
89
94
|
|
|
90
95
|
fireflyThreshold: 3.0,
|
|
96
|
+
// Wavefront material-coherence sort: global counting-sort of entering rays by material before
|
|
97
|
+
// Shade (material-pure workgroups), under dynamic dispatch. Measured −8% at 1024²/8b. Gated on
|
|
98
|
+
// material count > 8; the histogram bin count is sized per-scene to the material count.
|
|
99
|
+
wavefrontSortMaterials: true,
|
|
91
100
|
renderLimitMode: 'frames',
|
|
92
101
|
renderTimeLimit: 30,
|
|
93
102
|
renderMode: 0,
|
|
@@ -479,7 +488,11 @@ export const TEXTURE_CONSTANTS = {
|
|
|
479
488
|
BUFFER_POOL_SIZE: 20,
|
|
480
489
|
CANVAS_POOL_SIZE: 12,
|
|
481
490
|
CACHE_SIZE_LIMIT: 50,
|
|
482
|
-
|
|
491
|
+
// Hardware ceiling for a single texture-array dimension (WebGPU maxTextureDimension2D
|
|
492
|
+
// guaranteed minimum). The configurable maxTextureSize setting is clamped to this.
|
|
493
|
+
MAX_TEXTURE_SIZE: 8192,
|
|
494
|
+
// Default cap applied when no maxTextureSize is supplied (engine standalone use).
|
|
495
|
+
DEFAULT_MAX_TEXTURE_SIZE: 4096
|
|
483
496
|
};
|
|
484
497
|
|
|
485
498
|
// Default texture matrix for materials
|
package/src/PathTracerApp.js
CHANGED
|
@@ -103,6 +103,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
103
103
|
this.assetLoader = null;
|
|
104
104
|
this._sdf = null;
|
|
105
105
|
this._animRefitInFlight = false;
|
|
106
|
+
// Max material-texture dimension (longest edge); applied on each scene build.
|
|
107
|
+
this._maxTextureSize = DEFAULT_STATE.maxTextureSize;
|
|
106
108
|
|
|
107
109
|
// ── Pipeline & stages ──
|
|
108
110
|
this.pipeline = null;
|
|
@@ -667,6 +669,47 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
667
669
|
|
|
668
670
|
}
|
|
669
671
|
|
|
672
|
+
/**
|
|
673
|
+
* Set the max material-texture dimension (longest edge) used when processing a
|
|
674
|
+
* scene's textures into GPU arrays. Clamped to the hardware ceiling. Larger =
|
|
675
|
+
* sharper textures, ~quadratic VRAM. By default reprocesses the current scene so
|
|
676
|
+
* the change is visible without a manual reload.
|
|
677
|
+
* @param {number} size
|
|
678
|
+
* @param {Object} [opts]
|
|
679
|
+
* @param {boolean} [opts.reprocess=true] - Rebuild the current scene now.
|
|
680
|
+
* @returns {Promise<void>}
|
|
681
|
+
*/
|
|
682
|
+
async setMaxTextureSize( size, { reprocess = true } = {} ) {
|
|
683
|
+
|
|
684
|
+
const prev = this._maxTextureSize;
|
|
685
|
+
const clamped = this._sdf?.setMaxTextureSize( size );
|
|
686
|
+
this._maxTextureSize = clamped ?? size;
|
|
687
|
+
if ( typeof this.stages?.pathTracer?.sdfs?.setMaxTextureSize === 'function' ) {
|
|
688
|
+
|
|
689
|
+
this.stages.pathTracer.sdfs.setMaxTextureSize( this._maxTextureSize );
|
|
690
|
+
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Reprocess the loaded scene so the new cap takes effect immediately.
|
|
694
|
+
if ( reprocess && this._maxTextureSize !== prev && this._sdf?.triangleData && ! this._loadingInProgress ) {
|
|
695
|
+
|
|
696
|
+
this._loadingInProgress = true;
|
|
697
|
+
try {
|
|
698
|
+
|
|
699
|
+
await this.loadSceneData();
|
|
700
|
+
this.reset();
|
|
701
|
+
this.dispatchEvent( { type: 'TexturesReprocessed', maxTextureSize: this._maxTextureSize } );
|
|
702
|
+
|
|
703
|
+
} finally {
|
|
704
|
+
|
|
705
|
+
this._loadingInProgress = false;
|
|
706
|
+
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
}
|
|
712
|
+
|
|
670
713
|
/** Shared pipeline: load asset → sync controls → build BVH → reset → dispatch events */
|
|
671
714
|
async _loadWithSceneRebuild( loadFn, eventPayload ) {
|
|
672
715
|
|
|
@@ -730,6 +773,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
730
773
|
|
|
731
774
|
// Build BVH
|
|
732
775
|
timer.start( 'BVH build (SceneProcessor)' );
|
|
776
|
+
this._sdf.setMaxTextureSize( this._maxTextureSize );
|
|
733
777
|
await this._sdf.buildBVH( this.meshScene );
|
|
734
778
|
timer.end( 'BVH build (SceneProcessor)' );
|
|
735
779
|
|
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
const WORKGROUP_SIZES = {
|
|
13
13
|
generate: [ 16, 16, 1 ], // 2D screen-space
|
|
14
14
|
extend: [ 256, 1, 1 ], // 1D ray-parallel
|
|
15
|
-
|
|
16
|
-
shade: [ 256, 1, 1 ], // 1D ray-parallel (sorted)
|
|
15
|
+
shade: [ 256, 1, 1 ], // 1D ray-parallel (material-sorted when sort enabled)
|
|
17
16
|
connect: [ 256, 1, 1 ], // 1D shadow-ray-parallel
|
|
18
17
|
accumulate: [ 256, 1, 1 ], // 1D shadow-ray-parallel
|
|
19
18
|
compact: [ 256, 1, 1 ], // 1D ray-parallel
|
|
@@ -40,6 +40,9 @@ export class QueueManager {
|
|
|
40
40
|
// A/B alternate: one read by current bounce, other written by compaction
|
|
41
41
|
this.activeIndices = null;
|
|
42
42
|
this.activeIndicesRO = null;
|
|
43
|
+
this.sortedIndices = null;
|
|
44
|
+
this.sortedIndicesRO = null;
|
|
45
|
+
this.sortGlobalHistogram = null;
|
|
43
46
|
this.pingPong = 0; // 0 = read A / write B, 1 = read B / write A
|
|
44
47
|
|
|
45
48
|
if ( maxRays > 0 ) {
|
|
@@ -83,11 +86,23 @@ export class QueueManager {
|
|
|
83
86
|
b: storage( attrB, 'uint' ).toReadOnly(),
|
|
84
87
|
};
|
|
85
88
|
|
|
89
|
+
// Material-sort output: a material-reordered permutation of the active index
|
|
90
|
+
// list, written by the global material sort and read by Shade in place of activeIndices.
|
|
91
|
+
const sortAttr = new StorageInstancedBufferAttribute( new Uint32Array( capacity ), 1 );
|
|
92
|
+
this._sortAttr = sortAttr;
|
|
93
|
+
this.sortedIndices = storage( sortAttr, 'uint' );
|
|
94
|
+
this.sortedIndicesRO = storage( sortAttr, 'uint' ).toReadOnly();
|
|
95
|
+
|
|
96
|
+
// Global material-sort histogram, sized to the bin cap (SORT_GLOBAL_MAX_BINS=256); kernels use
|
|
97
|
+
// only the first `bins` entries (= per-scene material count). 256 × 4B = 1KB.
|
|
98
|
+
this._sortGlobalHistAttr = new StorageInstancedBufferAttribute( new Uint32Array( 256 ), 1 );
|
|
99
|
+
this.sortGlobalHistogram = storage( this._sortGlobalHistAttr, 'uint' ).toAtomic();
|
|
100
|
+
|
|
86
101
|
this.pingPong = 0;
|
|
87
102
|
|
|
88
103
|
const totalBytes = (
|
|
89
104
|
COUNTER.COUNT * 4 +
|
|
90
|
-
capacity * 4 *
|
|
105
|
+
capacity * 4 * 3
|
|
91
106
|
);
|
|
92
107
|
|
|
93
108
|
console.log(
|
|
@@ -131,6 +146,24 @@ export class QueueManager {
|
|
|
131
146
|
|
|
132
147
|
}
|
|
133
148
|
|
|
149
|
+
getSortedRW() {
|
|
150
|
+
|
|
151
|
+
return this.sortedIndices;
|
|
152
|
+
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
getSortedRO() {
|
|
156
|
+
|
|
157
|
+
return this.sortedIndicesRO;
|
|
158
|
+
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
getSortGlobalHistogram() {
|
|
162
|
+
|
|
163
|
+
return this.sortGlobalHistogram;
|
|
164
|
+
|
|
165
|
+
}
|
|
166
|
+
|
|
134
167
|
// raw attribute for `renderer.getArrayBufferAsync(...)` readback
|
|
135
168
|
getCountersAttribute() {
|
|
136
169
|
|
|
@@ -167,6 +200,9 @@ export class QueueManager {
|
|
|
167
200
|
this.counters = null;
|
|
168
201
|
this.activeIndices = null;
|
|
169
202
|
this.activeIndicesRO = null;
|
|
203
|
+
this.sortedIndices = null;
|
|
204
|
+
this.sortedIndicesRO = null;
|
|
205
|
+
this.sortGlobalHistogram = null;
|
|
170
206
|
this.capacity = 0;
|
|
171
207
|
|
|
172
208
|
}
|
|
@@ -9,7 +9,7 @@ import { GeometryExtractor } from './GeometryExtractor.js';
|
|
|
9
9
|
import { EmissiveTriangleBuilder } from './EmissiveTriangleBuilder.js';
|
|
10
10
|
import { updateLoading } from '../Processor/utils.js';
|
|
11
11
|
import { BuildTimer } from './BuildTimer.js';
|
|
12
|
-
import { TRIANGLE_DATA_LAYOUT } from '../EngineDefaults.js';
|
|
12
|
+
import { TRIANGLE_DATA_LAYOUT, TEXTURE_CONSTANTS } from '../EngineDefaults.js';
|
|
13
13
|
import { fetchAsWorker } from './Workers/fetchAsWorker.js';
|
|
14
14
|
import BVH_WORKER_URL from './Workers/BVHWorker.js?worker&url';
|
|
15
15
|
import BVH_REFIT_WORKER_URL from './Workers/BVHRefitWorker.js?worker&url';
|
|
@@ -41,6 +41,7 @@ export class SceneProcessor {
|
|
|
41
41
|
verbose: false,
|
|
42
42
|
useFloat32Array: true,
|
|
43
43
|
textureQuality: 'adaptive', // 'low', 'medium', 'high', 'adaptive'
|
|
44
|
+
maxTextureSize: TEXTURE_CONSTANTS.DEFAULT_MAX_TEXTURE_SIZE, // longest-edge cap for material textures
|
|
44
45
|
enableTextureCache: true,
|
|
45
46
|
maxConcurrentTextureTasks: Math.min( navigator.hardwareConcurrency || 4, 6 ),
|
|
46
47
|
// Treelet optimization configuration
|
|
@@ -109,6 +110,17 @@ export class SceneProcessor {
|
|
|
109
110
|
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Set the max material-texture dimension applied on the next scene build.
|
|
115
|
+
* @param {number} size - Longest-edge cap (clamped to the hardware ceiling).
|
|
116
|
+
*/
|
|
117
|
+
setMaxTextureSize( size ) {
|
|
118
|
+
|
|
119
|
+
this.config.maxTextureSize = size;
|
|
120
|
+
return this.textureCreator?.setMaxTextureSize( size );
|
|
121
|
+
|
|
122
|
+
}
|
|
123
|
+
|
|
112
124
|
/**
|
|
113
125
|
* Initialize processing components with configuration
|
|
114
126
|
* @private
|
|
@@ -131,7 +143,7 @@ export class SceneProcessor {
|
|
|
131
143
|
} );
|
|
132
144
|
|
|
133
145
|
// Create and configure texture creator
|
|
134
|
-
this.textureCreator = new TextureCreator();
|
|
146
|
+
this.textureCreator = new TextureCreator( { maxTextureSize: this.config.maxTextureSize } );
|
|
135
147
|
// The optimized TextureCreator will auto-detect capabilities and select optimal methods
|
|
136
148
|
|
|
137
149
|
// Create emissive triangle builder for direct lighting
|
|
@@ -446,6 +446,11 @@ export class TextureCreator {
|
|
|
446
446
|
this.maxConcurrentWorkers = TEXTURE_CONSTANTS.MAX_CONCURRENT_WORKERS;
|
|
447
447
|
this.activeWorkers = 0;
|
|
448
448
|
|
|
449
|
+
// Longest-edge cap for material-texture arrays. Clamped to the hardware ceiling.
|
|
450
|
+
this.maxTextureSize = this._clampTextureSize(
|
|
451
|
+
options.maxTextureSize ?? TEXTURE_CONSTANTS.DEFAULT_MAX_TEXTURE_SIZE
|
|
452
|
+
);
|
|
453
|
+
|
|
449
454
|
// Initialize high-performance components
|
|
450
455
|
this.canvasPool = new CanvasPool();
|
|
451
456
|
this.bufferPool = new SmartBufferPool( {
|
|
@@ -460,6 +465,25 @@ export class TextureCreator {
|
|
|
460
465
|
|
|
461
466
|
}
|
|
462
467
|
|
|
468
|
+
_clampTextureSize( size ) {
|
|
469
|
+
|
|
470
|
+
const n = Math.floor( Number( size ) || TEXTURE_CONSTANTS.DEFAULT_MAX_TEXTURE_SIZE );
|
|
471
|
+
return Math.max( TEXTURE_CONSTANTS.MIN_TEXTURE_WIDTH, Math.min( n, TEXTURE_CONSTANTS.MAX_TEXTURE_SIZE ) );
|
|
472
|
+
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
setMaxTextureSize( size ) {
|
|
476
|
+
|
|
477
|
+
const clamped = this._clampTextureSize( size );
|
|
478
|
+
if ( clamped === this.maxTextureSize ) return clamped; // unchanged — keep cache
|
|
479
|
+
this.maxTextureSize = clamped;
|
|
480
|
+
// Cache keys don't encode size — drop cached arrays so a new cap takes effect.
|
|
481
|
+
this.textureCache?.dispose();
|
|
482
|
+
this.textureCache = new TextureCache();
|
|
483
|
+
return this.maxTextureSize;
|
|
484
|
+
|
|
485
|
+
}
|
|
486
|
+
|
|
463
487
|
detectCapabilities() {
|
|
464
488
|
|
|
465
489
|
return {
|
|
@@ -656,7 +680,7 @@ export class TextureCreator {
|
|
|
656
680
|
|
|
657
681
|
worker.postMessage( {
|
|
658
682
|
textures: texturesData,
|
|
659
|
-
maxTextureSize:
|
|
683
|
+
maxTextureSize: this.maxTextureSize,
|
|
660
684
|
method: 'direct-transfer'
|
|
661
685
|
}, transferables );
|
|
662
686
|
|
|
@@ -1256,7 +1280,8 @@ export class TextureCreator {
|
|
|
1256
1280
|
maxWidth = Math.pow( 2, Math.ceil( Math.log2( maxWidth ) ) );
|
|
1257
1281
|
maxHeight = Math.pow( 2, Math.ceil( Math.log2( maxHeight ) ) );
|
|
1258
1282
|
|
|
1259
|
-
while
|
|
1283
|
+
// Halve only while a dimension exceeds the cap (preserves native res up to the cap).
|
|
1284
|
+
while ( maxWidth > this.maxTextureSize || maxHeight > this.maxTextureSize ) {
|
|
1260
1285
|
|
|
1261
1286
|
maxWidth = Math.max( 1, Math.floor( maxWidth / 2 ) );
|
|
1262
1287
|
maxHeight = Math.max( 1, Math.floor( maxHeight / 2 ) );
|
|
@@ -3,7 +3,7 @@ let canvas, ctx;
|
|
|
3
3
|
// Memory limits and chunking configuration
|
|
4
4
|
const MEMORY_LIMITS = {
|
|
5
5
|
MAX_BYTES_PER_TEXTURE: 256 * 1024 * 1024, // 256MB per texture array
|
|
6
|
-
MAX_TEXTURE_DIMENSION:
|
|
6
|
+
MAX_TEXTURE_DIMENSION: 8192, // Hardware ceiling (WebGPU maxTextureDimension2D guaranteed min); the per-scene maxTextureSize setting is the actual knob
|
|
7
7
|
CHUNK_SIZE: 8, // Optimized: Process textures in chunks of 8 for better memory locality
|
|
8
8
|
ADAPTIVE_CHUNK_SIZE: true, // Enable adaptive chunk sizing based on texture dimensions
|
|
9
9
|
MEMORY_SAFETY_FACTOR: 0.8 // Use only 80% of estimated available memory
|
|
@@ -627,8 +627,8 @@ function calculateOptimalDimensions( textures, maxTextureSize ) {
|
|
|
627
627
|
maxWidth = Math.min( maxWidth, maxTextureSize, MEMORY_LIMITS.MAX_TEXTURE_DIMENSION );
|
|
628
628
|
maxHeight = Math.min( maxHeight, maxTextureSize, MEMORY_LIMITS.MAX_TEXTURE_DIMENSION );
|
|
629
629
|
|
|
630
|
-
//
|
|
631
|
-
while ( maxWidth
|
|
630
|
+
// Halve only while a dimension exceeds the cap (preserves native res up to the cap).
|
|
631
|
+
while ( maxWidth > maxTextureSize || maxHeight > maxTextureSize ) {
|
|
632
632
|
|
|
633
633
|
maxWidth = Math.max( 1, Math.floor( maxWidth / 2 ) );
|
|
634
634
|
maxHeight = Math.max( 1, Math.floor( maxHeight / 2 ) );
|
package/src/Stages/PathTracer.js
CHANGED
|
@@ -17,6 +17,11 @@ import { buildShadeKernel, SHADE_WG_SIZE } from '../TSL/ShadeKernel.js';
|
|
|
17
17
|
import { buildCompactKernel, buildCompactSubgroupKernel, COMPACT_WG_SIZE } from '../TSL/CompactKernel.js';
|
|
18
18
|
import { buildFinalWriteKernel, FINALWRITE_WG_SIZE } from '../TSL/FinalWriteKernel.js';
|
|
19
19
|
import { buildDebugKernel, DEBUG_WG_SIZE } from '../TSL/DebugKernel.js';
|
|
20
|
+
import {
|
|
21
|
+
buildResetGlobalHistKernel, buildGlobalHistKernel, buildGlobalPrefixKernel, buildGlobalScatterKernel,
|
|
22
|
+
SORT_GLOBAL_WG_SIZE, SORT_GLOBAL_MAX_BINS,
|
|
23
|
+
} from '../TSL/SortGlobalKernels.js';
|
|
24
|
+
import { ENGINE_DEFAULTS } from '../EngineDefaults.js';
|
|
20
25
|
import {
|
|
21
26
|
Fn, uint, atomicStore, atomicLoad, instanceIndex, If, Return,
|
|
22
27
|
} from 'three/tsl';
|
|
@@ -43,6 +48,11 @@ export class PathTracer extends PathTracerStage {
|
|
|
43
48
|
// CPU sizes per-bounce kernels from last frame's survivor curve; kernels bound on ENTERING_COUNT so over-sizing is safe. (indirect dispatch not viable — three.js doesn't sync compute-written indirect buffers across submissions)
|
|
44
49
|
this._useDynamicDispatch = true;
|
|
45
50
|
|
|
51
|
+
// Global material-coherence sort: set per-build from ENGINE_DEFAULTS + material count (>8).
|
|
52
|
+
// Reorders entering rays into material-pure workgroups before Shade; runs under dynamic dispatch
|
|
53
|
+
// (compact reads the unsorted active list, so the survivor set is unchanged). Measured −8% at 1024²/8b.
|
|
54
|
+
this._sortMaterials = false;
|
|
55
|
+
|
|
46
56
|
// Flag-gated off: perf-neutral vs atomic-append and adds a 'subgroups' feature dependency.
|
|
47
57
|
this._useSubgroupCompact = false;
|
|
48
58
|
|
|
@@ -90,7 +100,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
90
100
|
if ( ! qm ) return null;
|
|
91
101
|
return [
|
|
92
102
|
qm._countersAttr, qm._bounceCountsAttr,
|
|
93
|
-
qm._attrA, qm._attrB,
|
|
103
|
+
qm._attrA, qm._attrB, qm._sortAttr,
|
|
94
104
|
];
|
|
95
105
|
|
|
96
106
|
} );
|
|
@@ -250,6 +260,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
250
260
|
this._wfCurrentBounce.value = bounce;
|
|
251
261
|
|
|
252
262
|
// Functional-compaction path (dynamic dispatch): copyback keeps the read buffer dense, kernels sized to live survivors. Dynamic-off uses the full path (ENTERING=maxRays, identity buffer).
|
|
263
|
+
// Material sort is compatible: shade reads sortedIndices while compact still reads the UNSORTED active list (getActiveReadRO), so the survivor set is unchanged.
|
|
253
264
|
const useFunctionalCompaction = this._useDynamicDispatch;
|
|
254
265
|
if ( useFunctionalCompaction ) {
|
|
255
266
|
|
|
@@ -281,6 +292,8 @@ export class PathTracer extends PathTracerStage {
|
|
|
281
292
|
const wg = [ Math.ceil( sized / 256 ), 1, 1 ];
|
|
282
293
|
km.setDispatchCount( 'extend', wg );
|
|
283
294
|
km.setDispatchCount( 'shade', wg );
|
|
295
|
+
km.setDispatchCount( 'globalHist', wg );
|
|
296
|
+
km.setDispatchCount( 'globalScatter', wg );
|
|
284
297
|
km.setDispatchCount( 'compact', wg );
|
|
285
298
|
km.setDispatchCount( 'compactCopyback', wg );
|
|
286
299
|
|
|
@@ -291,11 +304,23 @@ export class PathTracer extends PathTracerStage {
|
|
|
291
304
|
km.setDispatchCount( 'extend', full );
|
|
292
305
|
km.setDispatchCount( 'shade', full );
|
|
293
306
|
km.setDispatchCount( 'compact', full );
|
|
307
|
+
km.setDispatchCount( 'globalHist', full );
|
|
308
|
+
km.setDispatchCount( 'globalScatter', full );
|
|
294
309
|
|
|
295
310
|
}
|
|
296
311
|
|
|
297
312
|
// Extend/Shade kept separate (not fused): a fused kernel's register pressure drops occupancy more than fusion saves.
|
|
298
313
|
km.dispatch( 'extend' );
|
|
314
|
+
if ( this._sortMaterials ) {
|
|
315
|
+
|
|
316
|
+
// Global material counting sort (material-pure workgroups): reset, histogram, prefix-sum, scatter.
|
|
317
|
+
km.dispatch( 'resetGlobalHist' );
|
|
318
|
+
km.dispatch( 'globalHist' );
|
|
319
|
+
km.dispatch( 'globalPrefix' );
|
|
320
|
+
km.dispatch( 'globalScatter' );
|
|
321
|
+
|
|
322
|
+
}
|
|
323
|
+
|
|
299
324
|
km.dispatch( 'shade' );
|
|
300
325
|
|
|
301
326
|
km.dispatch( 'resetActiveCounter' );
|
|
@@ -660,6 +685,10 @@ export class PathTracer extends PathTracerStage {
|
|
|
660
685
|
displacementMaps: freshDisplacementMaps,
|
|
661
686
|
};
|
|
662
687
|
|
|
688
|
+
// Material-coherence sort gate (experiment): only worthwhile above a few materials.
|
|
689
|
+
this._sortMaterials = ( ENGINE_DEFAULTS.wavefrontSortMaterials ?? false )
|
|
690
|
+
&& ( this.materialData?.materialCount ?? 0 ) > 8;
|
|
691
|
+
|
|
663
692
|
const extFn = buildExtendKernel( {
|
|
664
693
|
bvhBuffer: freshBvh,
|
|
665
694
|
triangleBuffer: freshTri,
|
|
@@ -677,6 +706,50 @@ export class PathTracer extends PathTracerStage {
|
|
|
677
706
|
)
|
|
678
707
|
);
|
|
679
708
|
|
|
709
|
+
// Material-coherence sort: reorder the entering-ray indices by material between
|
|
710
|
+
// Extend and Shade. Histogram is workgroup-shared (patches.js §4); Shade reads the output.
|
|
711
|
+
if ( this._sortMaterials ) {
|
|
712
|
+
|
|
713
|
+
const sgHist = qm.getSortGlobalHistogram();
|
|
714
|
+
const sortBins = Math.min( SORT_GLOBAL_MAX_BINS, this.materialData?.materialCount ?? SORT_GLOBAL_MAX_BINS );
|
|
715
|
+
this._kernelManager.register( 'resetGlobalHist',
|
|
716
|
+
buildResetGlobalHistKernel( { sortGlobalHistogram: sgHist, bins: sortBins } )().compute(
|
|
717
|
+
[ 1, 1, 1 ], [ SORT_GLOBAL_WG_SIZE, 1, 1 ]
|
|
718
|
+
)
|
|
719
|
+
);
|
|
720
|
+
this._kernelManager.register( 'globalHist',
|
|
721
|
+
buildGlobalHistKernel( {
|
|
722
|
+
hitBufferRO: pb.hitBuffer.ro,
|
|
723
|
+
activeIndicesReadRO: qm.getActiveReadRO(),
|
|
724
|
+
sortGlobalHistogram: sgHist,
|
|
725
|
+
counters,
|
|
726
|
+
bins: sortBins,
|
|
727
|
+
} )().compute(
|
|
728
|
+
[ Math.ceil( maxRays / SORT_GLOBAL_WG_SIZE ), 1, 1 ],
|
|
729
|
+
[ SORT_GLOBAL_WG_SIZE, 1, 1 ]
|
|
730
|
+
)
|
|
731
|
+
);
|
|
732
|
+
this._kernelManager.register( 'globalPrefix',
|
|
733
|
+
buildGlobalPrefixKernel( { sortGlobalHistogram: sgHist, bins: sortBins } )().compute(
|
|
734
|
+
[ 1, 1, 1 ], [ 1, 1, 1 ]
|
|
735
|
+
)
|
|
736
|
+
);
|
|
737
|
+
this._kernelManager.register( 'globalScatter',
|
|
738
|
+
buildGlobalScatterKernel( {
|
|
739
|
+
hitBufferRO: pb.hitBuffer.ro,
|
|
740
|
+
activeIndicesReadRO: qm.getActiveReadRO(),
|
|
741
|
+
sortedIndicesRW: qm.getSortedRW(),
|
|
742
|
+
sortGlobalHistogram: sgHist,
|
|
743
|
+
counters,
|
|
744
|
+
bins: sortBins,
|
|
745
|
+
} )().compute(
|
|
746
|
+
[ Math.ceil( maxRays / SORT_GLOBAL_WG_SIZE ), 1, 1 ],
|
|
747
|
+
[ SORT_GLOBAL_WG_SIZE, 1, 1 ]
|
|
748
|
+
)
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
}
|
|
752
|
+
|
|
680
753
|
const shadeFn = buildShadeKernel( {
|
|
681
754
|
gBufferRW,
|
|
682
755
|
envCompensationDelta: this.envCompensationDelta,
|
|
@@ -689,7 +762,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
689
762
|
rngBufferRW: pb.rngBuffer.rw,
|
|
690
763
|
hitBufferRO: pb.hitBuffer.ro,
|
|
691
764
|
counters,
|
|
692
|
-
activeIndicesRO: qm.getActiveReadRO(),
|
|
765
|
+
activeIndicesRO: this._sortMaterials ? qm.getSortedRO() : qm.getActiveReadRO(),
|
|
693
766
|
albedoMaps: freshAlbedoMaps,
|
|
694
767
|
normalMaps: freshNormalMaps,
|
|
695
768
|
bumpMaps: freshBumpMaps,
|
package/src/TSL/Displacement.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fn, float, vec2, int, If, Loop, abs, normalize, dot, max } from 'three/tsl';
|
|
1
|
+
import { Fn, float, vec2, int, If, Loop, abs, normalize, dot, max, textureSize } from 'three/tsl';
|
|
2
2
|
|
|
3
3
|
import { struct } from './patches.js';
|
|
4
4
|
import { getDatafromStorageBuffer } from './Common.js';
|
|
@@ -8,7 +8,6 @@ import { sampleDisplacementMap } from './TextureSampling.js';
|
|
|
8
8
|
const MAX_MARCH_STEPS = 32;
|
|
9
9
|
const MIN_MARCH_STEPS = 16;
|
|
10
10
|
const BINARY_STEPS = 5;
|
|
11
|
-
const NORMAL_TEXEL_SIZE = 1.0 / 1024.0;
|
|
12
11
|
const TRI_STRIDE = 8;
|
|
13
12
|
|
|
14
13
|
export const DisplacementResult = struct( {
|
|
@@ -194,14 +193,15 @@ export const refineDisplacedIntersection = Fn( ( [
|
|
|
194
193
|
);
|
|
195
194
|
const displacedHeight = finalHeight.sub( 0.5 ).mul( scale );
|
|
196
195
|
|
|
197
|
-
// Compute displaced normal from height-field gradients using UV tangent vectors
|
|
198
|
-
|
|
196
|
+
// Compute displaced normal from height-field gradients using UV tangent vectors.
|
|
197
|
+
// Finite-difference step = one texel of the actual displacement-array dimensions.
|
|
198
|
+
const texel = vec2( 1.0 ).div( vec2( textureSize( displacementMaps ) ) ).toVar();
|
|
199
199
|
const hC = finalHeight;
|
|
200
200
|
const hU = sampleDisplacementMap(
|
|
201
|
-
displacementMaps, material.displacementMapIndex, finalUV.add( vec2(
|
|
201
|
+
displacementMaps, material.displacementMapIndex, finalUV.add( vec2( texel.x, 0.0 ) ), material.displacementTransform,
|
|
202
202
|
);
|
|
203
203
|
const hV = sampleDisplacementMap(
|
|
204
|
-
displacementMaps, material.displacementMapIndex, finalUV.add( vec2( 0.0,
|
|
204
|
+
displacementMaps, material.displacementMapIndex, finalUV.add( vec2( 0.0, texel.y ) ), material.displacementTransform,
|
|
205
205
|
);
|
|
206
206
|
|
|
207
207
|
// Perturb normal using actual tangent/bitangent from UV parameterization
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SortGlobalKernels.js — GLOBAL material counting sort of the entering rays.
|
|
3
|
+
*
|
|
4
|
+
* Unlike a per-workgroup sort (locally coherent only), this orders rays by material
|
|
5
|
+
* ACROSS ALL workgroups, so each material's rays occupy one contiguous region of sortedIndices.
|
|
6
|
+
* For materials with many rays that means whole material-PURE workgroups/subgroups → real shading
|
|
7
|
+
* coherence. MEASURED −8% vs no-sort at 1024²/8b (per-WG sort was neutral).
|
|
8
|
+
*
|
|
9
|
+
* Four passes (counting sort): reset → histogram → exclusive prefix sum → scatter.
|
|
10
|
+
* - histogram uses a per-workgroup local histogram (workgroup-shared atomics, patches.js §4)
|
|
11
|
+
* flushed to the global histogram once per bin per workgroup, cutting global-atomic contention
|
|
12
|
+
* from O(rays) to O(workgroups × bins).
|
|
13
|
+
* - the prefix-summed histogram doubles as the per-bin running cursor in scatter.
|
|
14
|
+
*
|
|
15
|
+
* `bins` is chosen per-scene by the caller (= material count, capped). Materials ≥ bins clamp into
|
|
16
|
+
* the last bin (graceful coherence loss). Capped at SORT_GLOBAL_MAX_BINS so a single 256-thread
|
|
17
|
+
* workgroup can zero/flush exactly one bin per thread (no per-thread loop needed).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
Fn, uint,
|
|
22
|
+
If, Loop,
|
|
23
|
+
instanceIndex, localId,
|
|
24
|
+
workgroupBarrier,
|
|
25
|
+
atomicAdd, atomicLoad, atomicStore,
|
|
26
|
+
} from 'three/tsl';
|
|
27
|
+
|
|
28
|
+
import { workgroupAtomicArray } from './patches.js';
|
|
29
|
+
import { readHitMaterialIndex } from '../Processor/PackedRayBuffer.js';
|
|
30
|
+
import { COUNTER } from '../Processor/QueueManager.js';
|
|
31
|
+
|
|
32
|
+
const WG_SIZE = 256;
|
|
33
|
+
export const SORT_GLOBAL_MAX_BINS = 256;
|
|
34
|
+
|
|
35
|
+
// Pass 1 — zero the global histogram.
|
|
36
|
+
export function buildResetGlobalHistKernel( { sortGlobalHistogram, bins = SORT_GLOBAL_MAX_BINS } ) {
|
|
37
|
+
|
|
38
|
+
return Fn( () => {
|
|
39
|
+
|
|
40
|
+
If( instanceIndex.lessThan( uint( bins ) ), () => {
|
|
41
|
+
|
|
42
|
+
atomicStore( sortGlobalHistogram.element( instanceIndex ), uint( 0 ) );
|
|
43
|
+
|
|
44
|
+
} );
|
|
45
|
+
|
|
46
|
+
} );
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Pass 2 — histogram: per-workgroup local count (on-chip), flushed to global once per bin/WG.
|
|
51
|
+
export function buildGlobalHistKernel( { hitBufferRO, activeIndicesReadRO, sortGlobalHistogram, counters, bins = SORT_GLOBAL_MAX_BINS } ) {
|
|
52
|
+
|
|
53
|
+
return Fn( () => {
|
|
54
|
+
|
|
55
|
+
const tid = instanceIndex;
|
|
56
|
+
const lid = localId.x;
|
|
57
|
+
const local = workgroupAtomicArray( 'uint', bins );
|
|
58
|
+
|
|
59
|
+
If( lid.lessThan( uint( bins ) ), () => {
|
|
60
|
+
|
|
61
|
+
atomicStore( local.element( lid ), uint( 0 ) );
|
|
62
|
+
|
|
63
|
+
} );
|
|
64
|
+
workgroupBarrier();
|
|
65
|
+
|
|
66
|
+
const entering = atomicLoad( counters.element( uint( COUNTER.ENTERING_COUNT ) ) );
|
|
67
|
+
If( tid.lessThan( entering ), () => {
|
|
68
|
+
|
|
69
|
+
const rayID = activeIndicesReadRO.element( tid );
|
|
70
|
+
const bin = uint( readHitMaterialIndex( hitBufferRO, rayID ) ).clamp( uint( 0 ), uint( bins - 1 ) );
|
|
71
|
+
atomicAdd( local.element( bin ), uint( 1 ) );
|
|
72
|
+
|
|
73
|
+
} );
|
|
74
|
+
workgroupBarrier();
|
|
75
|
+
|
|
76
|
+
If( lid.lessThan( uint( bins ) ), () => {
|
|
77
|
+
|
|
78
|
+
const c = atomicLoad( local.element( lid ) );
|
|
79
|
+
If( c.greaterThan( uint( 0 ) ), () => {
|
|
80
|
+
|
|
81
|
+
atomicAdd( sortGlobalHistogram.element( lid ), c );
|
|
82
|
+
|
|
83
|
+
} );
|
|
84
|
+
|
|
85
|
+
} );
|
|
86
|
+
|
|
87
|
+
} );
|
|
88
|
+
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Pass 3 — exclusive prefix sum over global bins (single thread; dispatch [1,1,1]).
|
|
92
|
+
export function buildGlobalPrefixKernel( { sortGlobalHistogram, bins = SORT_GLOBAL_MAX_BINS } ) {
|
|
93
|
+
|
|
94
|
+
return Fn( () => {
|
|
95
|
+
|
|
96
|
+
If( instanceIndex.equal( uint( 0 ) ), () => {
|
|
97
|
+
|
|
98
|
+
const running = uint( 0 ).toVar();
|
|
99
|
+
Loop( { start: uint( 0 ), end: uint( bins ), type: 'uint' }, ( { i } ) => {
|
|
100
|
+
|
|
101
|
+
const slot = sortGlobalHistogram.element( i );
|
|
102
|
+
const c = atomicLoad( slot );
|
|
103
|
+
atomicStore( slot, running );
|
|
104
|
+
running.addAssign( c );
|
|
105
|
+
|
|
106
|
+
} );
|
|
107
|
+
|
|
108
|
+
} );
|
|
109
|
+
|
|
110
|
+
} );
|
|
111
|
+
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Pass 4 — scatter: atomicAdd on the prefix-summed histogram gives each ray its global slot
|
|
115
|
+
// within its material's contiguous region.
|
|
116
|
+
export function buildGlobalScatterKernel( { hitBufferRO, activeIndicesReadRO, sortedIndicesRW, sortGlobalHistogram, counters, bins = SORT_GLOBAL_MAX_BINS } ) {
|
|
117
|
+
|
|
118
|
+
return Fn( () => {
|
|
119
|
+
|
|
120
|
+
const tid = instanceIndex;
|
|
121
|
+
const entering = atomicLoad( counters.element( uint( COUNTER.ENTERING_COUNT ) ) );
|
|
122
|
+
If( tid.lessThan( entering ), () => {
|
|
123
|
+
|
|
124
|
+
const rayID = activeIndicesReadRO.element( tid );
|
|
125
|
+
const bin = uint( readHitMaterialIndex( hitBufferRO, rayID ) ).clamp( uint( 0 ), uint( bins - 1 ) );
|
|
126
|
+
const pos = atomicAdd( sortGlobalHistogram.element( bin ), uint( 1 ) );
|
|
127
|
+
sortedIndicesRW.element( pos ).assign( rayID );
|
|
128
|
+
|
|
129
|
+
} );
|
|
130
|
+
|
|
131
|
+
} );
|
|
132
|
+
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export { WG_SIZE as SORT_GLOBAL_WG_SIZE };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fn, wgslFn, float, vec2, vec3, vec4, int, If, normalize, cross, abs, mix, clamp, texture } from 'three/tsl';
|
|
1
|
+
import { Fn, wgslFn, float, vec2, vec3, vec4, int, If, normalize, cross, abs, mix, clamp, texture, textureSize } from 'three/tsl';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
UVCache,
|
|
@@ -283,8 +283,8 @@ export const processBump = Fn( ( [ bumpMaps, currentNormal, material, uvCache ]
|
|
|
283
283
|
|
|
284
284
|
If( material.bumpMapIndex.greaterThanEqual( int( 0 ) ).and( material.bumpScale.greaterThan( 0.0 ) ), () => {
|
|
285
285
|
|
|
286
|
-
//
|
|
287
|
-
const texelSize = vec2( 1.0
|
|
286
|
+
// Texel size from the actual bump-array dimensions (finite-difference step).
|
|
287
|
+
const texelSize = vec2( 1.0 ).div( vec2( textureSize( bumpMaps ) ) ).toVar();
|
|
288
288
|
|
|
289
289
|
const h_c = texture( bumpMaps, uvCache.bumpUV ).depth( int( material.bumpMapIndex ) ).r;
|
|
290
290
|
const h_u = texture( bumpMaps, vec2( uvCache.bumpUV.x.add( texelSize.x ), uvCache.bumpUV.y ) ).depth( int( material.bumpMapIndex ) ).r;
|