rayzee 7.21.0 → 7.22.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "7.21.0",
3
+ "version": "7.22.0",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -62,6 +62,12 @@ export const ENGINE_DEFAULTS = {
62
62
  skyMieAnisotropy: 0.76,
63
63
  skyPreset: 'clearMorning',
64
64
 
65
+ // Camera projection — 'perspective' | 'equirectangular'. Panorama ranges are UI-facing degrees.
66
+ cameraProjection: 'perspective',
67
+ panoramaLonRange: [ - 180, 180 ],
68
+ panoramaLatRange: [ - 90, 90 ],
69
+ panoramaLevelHorizon: true,
70
+
65
71
  enableDOF: false,
66
72
  fov: 55,
67
73
  focusDistance: 0.8,
@@ -1722,6 +1722,80 @@ export class PathTracerApp extends EventDispatcher {
1722
1722
 
1723
1723
  }
1724
1724
 
1725
+ /**
1726
+ * Per-kernel GPU milliseconds for the last resolved frame.
1727
+ *
1728
+ * {@link PathTracerApp#getGPUTimings} only reports the frame aggregate, which buries a change to
1729
+ * one kernel under everything else. The backend's timestamp query pool already retains one
1730
+ * duration per compute pass, keyed `c:<frameCalls>:<nodeId>:f<frame>` (three.js
1731
+ * `WebGPUTimestampQueryPool.resolveQueriesAsync` → `timestamps`); this attributes those back to
1732
+ * kernel names through `KernelManager`'s registry.
1733
+ *
1734
+ * Durations are SUMMED per kernel across the frame, so `extend` reports its whole per-frame cost
1735
+ * over every bounce iteration rather than one bounce. `unattributed` collects passes belonging to
1736
+ * no registered kernel (other stages, denoisers) so `sum(kernels) + unattributed` reconciles with
1737
+ * `total` — a gap there means a pass was missed, not that a kernel is free.
1738
+ *
1739
+ * The pool's map accumulates across frames and is never pruned, hence the newest-frame filter.
1740
+ *
1741
+ * Requires {@link PathTracerApp#enableGPUTiming}. Returns null when timestamps are unavailable.
1742
+ *
1743
+ * @returns {Promise<{kernels: Object<string, number>, total: number, unattributed: number, frame: number}|null>}
1744
+ */
1745
+ async getKernelGPUTimings() {
1746
+
1747
+ const renderer = this.renderer;
1748
+ if ( ! renderer?.backend?.trackTimestamp ) return null;
1749
+
1750
+ await renderer.resolveTimestampsAsync( 'compute' );
1751
+
1752
+ const timestamps = renderer.backend.timestampQueryPool?.compute?.timestamps;
1753
+ if ( ! timestamps || timestamps.size === 0 ) return null;
1754
+
1755
+ const nameByNodeId = new Map();
1756
+ const kernelMap = this.stages?.pathTracer?._kernelManager?.kernels;
1757
+ if ( kernelMap ) {
1758
+
1759
+ for ( const [ name, node ] of kernelMap ) nameByNodeId.set( node.id, name );
1760
+
1761
+ }
1762
+
1763
+ const parsed = [];
1764
+ let frame = - 1;
1765
+
1766
+ for ( const [ uid, ms ] of timestamps ) {
1767
+
1768
+ // 'c:<frameCalls>:<nodeId>:f<frame>'
1769
+ const parts = uid.split( ':' );
1770
+ if ( parts.length !== 4 ) continue;
1771
+
1772
+ const f = Number( parts[ 3 ].slice( 1 ) );
1773
+ if ( ! Number.isFinite( f ) ) continue;
1774
+
1775
+ parsed.push( { nodeId: Number( parts[ 2 ] ), f, ms } );
1776
+ if ( f > frame ) frame = f;
1777
+
1778
+ }
1779
+
1780
+ const kernels = {};
1781
+ let total = 0;
1782
+ let unattributed = 0;
1783
+
1784
+ for ( const entry of parsed ) {
1785
+
1786
+ if ( entry.f !== frame ) continue;
1787
+
1788
+ total += entry.ms;
1789
+ const name = nameByNodeId.get( entry.nodeId );
1790
+ if ( name === undefined ) unattributed += entry.ms;
1791
+ else kernels[ name ] = ( kernels[ name ] ?? 0 ) + entry.ms;
1792
+
1793
+ }
1794
+
1795
+ return { kernels, total, unattributed, frame };
1796
+
1797
+ }
1798
+
1725
1799
  // ═══════════════════════════════════════════════════════════════
1726
1800
  // Output (absorbed from OutputAPI)
1727
1801
  // ═══════════════════════════════════════════════════════════════
@@ -2298,6 +2372,8 @@ export class PathTracerApp extends EventDispatcher {
2298
2372
  renderer: this.renderer,
2299
2373
  resetCallback: () => this.reset(),
2300
2374
  reconcileCompletion: () => this._reconcileCompletion(),
2375
+ denoisingManager: this.denoisingManager,
2376
+ cameraManager: this.cameraManager,
2301
2377
  } );
2302
2378
 
2303
2379
  this.renderer.toneMappingExposure = this.settings.get( 'exposure' ) ?? 1.0;
@@ -101,8 +101,6 @@ export class BVHBuilder {
101
101
  this.treeletSize = 5;
102
102
  this.treeletOptimizationPasses = 1;
103
103
  this.treeletMinImprovement = 0.02;
104
- this.maxTreeletDepth = 3;
105
- this.maxTreeletsPerScene = 20;
106
104
  this.treeletComplexityThreshold = 50000;
107
105
 
108
106
  // Reinsertion optimization configuration
@@ -191,6 +189,7 @@ export class BVHBuilder {
191
189
  if ( config.size !== undefined ) this.treeletSize = Math.max( 3, Math.min( 12, config.size ) );
192
190
  if ( config.passes !== undefined ) this.treeletOptimizationPasses = Math.max( 1, Math.min( 3, config.passes ) );
193
191
  if ( config.minImprovement !== undefined ) this.treeletMinImprovement = Math.max( 0.001, config.minImprovement );
192
+ if ( config.complexityThreshold !== undefined ) this.treeletComplexityThreshold = config.complexityThreshold;
194
193
 
195
194
  }
196
195
 
@@ -595,12 +594,10 @@ export class BVHBuilder {
595
594
 
596
595
  const isLargeScene = this.totalTriangles > this.treeletComplexityThreshold;
597
596
  const adaptiveTreeletSize = isLargeScene ? 3 : this.treeletSize;
598
- const adaptiveMaxTreelets = isLargeScene ? 10 : this.maxTreeletsPerScene;
599
597
 
600
598
  const optimizer = new TreeletOptimizer( this.traversalCost, this.intersectionCost );
601
599
  optimizer.setTreeletSize( adaptiveTreeletSize );
602
600
  optimizer.setMinImprovement( this.treeletMinImprovement );
603
- optimizer.setMaxTreelets( adaptiveMaxTreelets );
604
601
 
605
602
  const optimizationStartTime = performance.now();
606
603
 
@@ -4,25 +4,14 @@
4
4
  * Builds, caches, and dispatches individual compute nodes for the wavefront
5
5
  * path tracing pipeline. Each kernel is a separate `Fn().compute()` node.
6
6
  *
7
- * Manages workgroup sizes, dispatch dimensions, and provides a unified
8
- * dispatch interface that wraps `renderer.compute(node)`.
7
+ * Dispatch grids are derived from each node's own `workgroupSize`, so the divisor can never
8
+ * drift from the size the kernel was compiled with — see `setDispatchForCount`.
9
9
  */
10
10
 
11
11
  import { createLogger, fmt } from '../utils/Logger.js';
12
12
 
13
13
  const log = createLogger( 'wavefront' );
14
14
 
15
- /** Default workgroup sizes per kernel type */
16
- const WORKGROUP_SIZES = {
17
- generate: [ 16, 16, 1 ], // 2D screen-space
18
- extend: [ 256, 1, 1 ], // 1D ray-parallel
19
- shade: [ 256, 1, 1 ], // 1D ray-parallel (material-sorted when sort enabled)
20
- connect: [ 256, 1, 1 ], // 1D shadow-ray-parallel
21
- accumulate: [ 256, 1, 1 ], // 1D shadow-ray-parallel
22
- compact: [ 256, 1, 1 ], // 1D ray-parallel
23
- finalWrite: [ 16, 16, 1 ], // 2D screen-space
24
- };
25
-
26
15
  export class KernelManager {
27
16
 
28
17
  /**
@@ -41,12 +30,6 @@ export class KernelManager {
41
30
  */
42
31
  this.kernels = new Map();
43
32
 
44
- /**
45
- * Map of kernel name → workgroup size [x, y, z].
46
- * @type {Map<string, number[]>}
47
- */
48
- this.workgroupSizes = new Map();
49
-
50
33
  /**
51
34
  * Timing data for performance profiling.
52
35
  * @type {Map<string, {compiledOnce: boolean, lastDispatchMs: number}>}
@@ -74,13 +57,6 @@ export class KernelManager {
74
57
  this._firstDispatches = [];
75
58
  this._firstDispatchFlush = null;
76
59
 
77
- // Initialize workgroup sizes from defaults
78
- for ( const [ name, wgSize ] of Object.entries( WORKGROUP_SIZES ) ) {
79
-
80
- this.workgroupSizes.set( name, wgSize );
81
-
82
- }
83
-
84
60
  }
85
61
 
86
62
  /**
@@ -177,61 +153,32 @@ export class KernelManager {
177
153
  }
178
154
 
179
155
  /**
180
- * Update dispatch dimensions for a kernel.
156
+ * Size a 1D kernel's grid to cover `count` items, deriving the divisor from the workgroup size the
157
+ * kernel was actually registered with. Extend/Shade bound on ENTERING_COUNT rather than on the grid,
158
+ * so an under-sized grid silently drops the tail of the active list — never hardcode the divisor.
181
159
  * @param {string} name - Kernel name
182
- * @param {number[]} count - Dispatch dimensions [x, y, z]
160
+ * @param {number} count - Number of items to cover
183
161
  */
184
- setDispatchCount( name, count ) {
162
+ setDispatchForCount( name, count ) {
185
163
 
186
164
  const node = this.kernels.get( name );
187
165
  if ( ! node ) return;
188
- node.dispatchSize = count;
189
-
190
- }
191
-
192
- /**
193
- * Calculate 2D dispatch dimensions for a screen-space kernel.
194
- * @param {number} width - Render width in pixels
195
- * @param {number} height - Render height in pixels
196
- * @param {string} kernelName - Kernel name for WG size lookup
197
- * @returns {number[]} [dispatchX, dispatchY, 1]
198
- */
199
- calcScreenDispatch( width, height, kernelName ) {
200
-
201
- const wg = this.workgroupSizes.get( kernelName ) || [ 16, 16, 1 ];
202
- return [
203
- Math.ceil( width / wg[ 0 ] ),
204
- Math.ceil( height / wg[ 1 ] ),
205
- 1
206
- ];
207
-
208
- }
209
-
210
- /**
211
- * Calculate 1D dispatch dimensions for a ray-parallel kernel.
212
- * @param {number} rayCount - Number of rays to process
213
- * @param {string} kernelName - Kernel name for WG size lookup
214
- * @returns {number[]} [dispatchX, 1, 1]
215
- */
216
- calcRayDispatch( rayCount, kernelName ) {
217
-
218
- const wg = this.workgroupSizes.get( kernelName ) || [ 256, 1, 1 ];
219
- return [
220
- Math.ceil( rayCount / wg[ 0 ] ),
221
- 1,
222
- 1
223
- ];
166
+ node.dispatchSize = [ Math.ceil( count / node.workgroupSize[ 0 ] ), 1, 1 ];
224
167
 
225
168
  }
226
169
 
227
170
  /**
228
- * Get the workgroup size for a kernel.
229
- * @param {string} name
230
- * @returns {number[]}
171
+ * Size a 2D screen-space kernel's grid, deriving both divisors from the registered workgroup size.
172
+ * @param {string} name - Kernel name
173
+ * @param {number} width - Width in pixels
174
+ * @param {number} height - Height in pixels
231
175
  */
232
- getWorkgroupSize( name ) {
176
+ setDispatchForGrid( name, width, height ) {
233
177
 
234
- return this.workgroupSizes.get( name ) || [ 256, 1, 1 ];
178
+ const node = this.kernels.get( name );
179
+ if ( ! node ) return;
180
+ const wg = node.workgroupSize;
181
+ node.dispatchSize = [ Math.ceil( width / wg[ 0 ] ), Math.ceil( height / wg[ 1 ] ), 1 ];
235
182
 
236
183
  }
237
184
 
@@ -17,9 +17,10 @@ export const COUNTER = {
17
17
  // per-frame count of pixels whose Tier-1 relative-error dropped below threshold; zeroed at frame start by
18
18
  // initActiveIndices, incremented in FinalWrite, read back async to drive the whole-frame convergence early-stop.
19
19
  CONVERGED_COUNT: 2,
20
- // Tier-2 per-pixel freeze: FROZEN_COUNT = pixels skipped this frame; ACTIVE_PIXEL_COUNT = bounce-0 active
21
- // count (maxRays − frozen), read back to size next frame's grid.
22
- FROZEN_COUNT: 3,
20
+ // Reserved hole — was FROZEN_COUNT, a write-only counter no reader ever consumed. Kept as a hole so
21
+ // slot indices stay stable across the JS enum and every kernel that stores by index.
22
+ RESERVED_3: 3,
23
+ // Tier-2 per-pixel freeze: bounce-0 active count (maxRays − frozen), read back to size next frame's grid.
23
24
  ACTIVE_PIXEL_COUNT: 4,
24
25
  COUNT: 5,
25
26
  };
@@ -51,6 +51,8 @@ export class SceneProcessor {
51
51
  treeletSize: 7, // 7 nodes gives 315 topologies for optimal enumeration
52
52
  treeletOptimizationPasses: 1,
53
53
  treeletMinImprovement: 0.01, // Minimum SAH improvement threshold
54
+ // Above this triangle count the builder drops treelets to size 3.
55
+ treeletComplexityThreshold: 50000,
54
56
  ...options
55
57
  };
56
58
 
@@ -151,7 +153,8 @@ export class SceneProcessor {
151
153
  enabled: this.config.enableTreeletOptimization,
152
154
  size: this.config.treeletSize,
153
155
  passes: this.config.treeletOptimizationPasses,
154
- minImprovement: this.config.treeletMinImprovement
156
+ minImprovement: this.config.treeletMinImprovement,
157
+ complexityThreshold: this.config.treeletComplexityThreshold
155
158
  } );
156
159
 
157
160
  // Create and configure texture creator
@@ -436,7 +439,8 @@ export class SceneProcessor {
436
439
  enabled: originalTreeletEnabled !== false,
437
440
  size: this.config.treeletSize,
438
441
  passes: this.config.treeletOptimizationPasses,
439
- minImprovement: this.config.treeletMinImprovement
442
+ minImprovement: this.config.treeletMinImprovement,
443
+ complexityThreshold: this.config.treeletComplexityThreshold
440
444
  },
441
445
  reinsertionOptimization: {
442
446
  enabled: this.bvhBuilder.enableReinsertionOptimization,
@@ -1267,7 +1271,8 @@ export class SceneProcessor {
1267
1271
  enabled: this.config.enableTreeletOptimization,
1268
1272
  size: this.config.treeletSize,
1269
1273
  passes: this.config.treeletOptimizationPasses,
1270
- minImprovement: this.config.treeletMinImprovement
1274
+ minImprovement: this.config.treeletMinImprovement,
1275
+ complexityThreshold: this.config.treeletComplexityThreshold
1271
1276
  } );
1272
1277
 
1273
1278
  }
@@ -1953,7 +1958,8 @@ export class SceneProcessor {
1953
1958
  enabled: treeletEnabled,
1954
1959
  size: this.config.treeletSize,
1955
1960
  passes: this.config.treeletOptimizationPasses,
1956
- minImprovement: this.config.treeletMinImprovement
1961
+ minImprovement: this.config.treeletMinImprovement,
1962
+ complexityThreshold: this.config.treeletComplexityThreshold
1957
1963
  },
1958
1964
  reinsertionOptimization: {
1959
1965
  enabled: this.bvhBuilder.enableReinsertionOptimization,
@@ -545,13 +545,6 @@ export class TreeletOptimizer {
545
545
 
546
546
  }
547
547
 
548
- setMaxTreelets() {
549
-
550
- // No-op — we process all valid treelets now.
551
- // Kept for API compatibility with BVHBuilder.
552
-
553
- }
554
-
555
548
  getStatistics() {
556
549
 
557
550
  return { ...this.stats };
@@ -1,4 +1,4 @@
1
- import { EventDispatcher, Color } from 'three';
1
+ import { EventDispatcher, Color, Vector2, MathUtils } from 'three';
2
2
  import { ENGINE_DEFAULTS } from './EngineDefaults.js';
3
3
  import { EngineEvents } from './EngineEvents.js';
4
4
 
@@ -32,6 +32,7 @@ const SETTING_ROUTES = {
32
32
  enableGroundCatcher: { uniform: 'enableGroundCatcher', reset: true },
33
33
  groundCatcherHeight: { uniform: 'groundCatcherHeight', reset: true },
34
34
  globalIlluminationIntensity: { uniform: 'globalIlluminationIntensity', reset: true },
35
+ panoramaLevelHorizon: { uniform: 'panoLevelHorizon', reset: true },
35
36
  enableDOF: { uniform: 'enableDOF', reset: true },
36
37
  focusDistance: { uniform: 'focusDistance', reset: false },
37
38
  focalLength: { uniform: 'focalLength', reset: true },
@@ -56,6 +57,9 @@ const SETTING_ROUTES = {
56
57
 
57
58
  // ── Multi-stage / special handling ────────────────────────────
58
59
 
60
+ cameraProjection: { handler: 'handleCameraProjection', reset: true },
61
+ panoramaLonRange: { handler: 'handlePanoramaLonRange', reset: true },
62
+ panoramaLatRange: { handler: 'handlePanoramaLatRange', reset: true },
59
63
  interactionModeEnabled: { handler: 'handleInteractionModeEnabled', reset: false },
60
64
  maxSamples: { handler: 'handleMaxSamples', reset: false },
61
65
  transparentBackground: { handler: 'handleTransparentBackground' },
@@ -122,13 +126,15 @@ export class RenderSettings extends EventDispatcher {
122
126
  * @param {Object} params.stages - Pipeline stages { pathTracer, compositor, autoExposure, ... }
123
127
  * @param {Function} params.resetCallback - Called to reset accumulation
124
128
  * @param {Function} [params.reconcileCompletion] - Called when completion limits change
129
+ * @param {Object} [params.denoisingManager] - Needed to force ASVGF off under panorama
130
+ * @param {Object} [params.cameraManager] - Needed to force auto-focus manual under panorama
125
131
  */
126
- bind( { stages, renderer, resetCallback, reconcileCompletion } ) {
132
+ bind( params ) {
127
133
 
128
- this._pathTracer = stages.pathTracer;
129
- this._resetCallback = resetCallback;
134
+ this._pathTracer = params.stages.pathTracer;
135
+ this._resetCallback = params.resetCallback;
130
136
  this._delegates = {};
131
- this._handlers = this._buildHandlers( stages, renderer, reconcileCompletion );
137
+ this._handlers = this._buildHandlers( params );
132
138
 
133
139
  }
134
140
 
@@ -136,10 +142,39 @@ export class RenderSettings extends EventDispatcher {
136
142
  * Builds handler functions for multi-stage settings that can't
137
143
  * be routed with a simple uniform forward.
138
144
  */
139
- _buildHandlers( stages, renderer, reconcileCompletion ) {
145
+ _buildHandlers( { stages, renderer, reconcileCompletion, denoisingManager, cameraManager } ) {
146
+
147
+ // UniformManager copies into the existing node, so one scratch vector serves every write.
148
+ const panoScratch = new Vector2();
149
+ const setPanoRange = ( uniform, value ) => {
150
+
151
+ panoScratch.set( value?.[ 0 ] ?? 0, value?.[ 1 ] ?? 0 ).multiplyScalar( MathUtils.DEG2RAD );
152
+ stages.pathTracer?.setUniform( uniform, panoScratch );
153
+
154
+ };
140
155
 
141
156
  return {
142
157
 
158
+ handleCameraProjection: ( value ) => {
159
+
160
+ const isPanorama = value === 'equirectangular';
161
+ stages.pathTracer?.setUniform( 'cameraProjection', isPanorama ? 1 : 0 );
162
+
163
+ if ( ! isPanorama ) return;
164
+
165
+ // ASVGF is driven entirely by MotionVector, which unprojects through
166
+ // projectionMatrixInverse — meaningless once every pixel is its own direction.
167
+ // Fall back to the spatial-only denoiser rather than leaving no strategy.
168
+ if ( denoisingManager?.denoiserStrategy === 'asvgf' ) denoisingManager.setDenoiserStrategy( 'edgeaware' );
169
+ // Auto-focus raycasts via Raycaster.setFromCamera, which only knows the frustum.
170
+ cameraManager?.setAutoFocusMode( 'manual' );
171
+
172
+ },
173
+
174
+ handlePanoramaLonRange: ( value ) => setPanoRange( 'panoLonRange', value ),
175
+
176
+ handlePanoramaLatRange: ( value ) => setPanoRange( 'panoLatRange', value ),
177
+
143
178
  handleTransparentBackground: ( value ) => {
144
179
 
145
180
  stages.pathTracer?.setUniform( 'transparentBackground', value );
@@ -1,4 +1,4 @@
1
- import { Fn, vec3, vec4, float, int, uint, uvec2, uniform, normalize, mat3, min, storage, If,
1
+ import { Fn, vec2, vec3, vec4, float, int, uint, uvec2, uniform, min, storage, If,
2
2
  textureStore, workgroupId, localId } from 'three/tsl';
3
3
  import { RenderTarget, StorageTexture } from 'three/webgpu';
4
4
  import { HalfFloatType, RGBAFormat, NearestFilter, Matrix4, Box2, Vector2 } from 'three';
@@ -6,6 +6,7 @@ import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
6
6
  import { MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
7
7
  import { Ray, HitInfo, RayTracingMaterial, UVCache } from '../TSL/Struct.js';
8
8
  import { traverseBVH } from '../TSL/BVHTraversal.js';
9
+ import { cameraRayDirection } from '../TSL/CameraRay.js';
9
10
  import { getMaterial } from '../TSL/Common.js';
10
11
  import { computeUVCache, processNormal, processBump, buildBucketTextureNodes, refreshBucketTextureNodes, setMaterialBucketTextures } from '../TSL/TextureSampling.js';
11
12
 
@@ -48,6 +49,12 @@ export class NormalDepth extends RenderStage {
48
49
 
49
50
  this.cameraWorldMatrix = uniform( new Matrix4(), 'mat4' );
50
51
  this.cameraProjectionMatrixInverse = uniform( new Matrix4(), 'mat4' );
52
+ // Mirrored from the path tracer each frame — these rays must use the same camera model
53
+ // as the colour buffer or the denoiser's normal/depth edge-stops fight the image.
54
+ this.cameraProjection = uniform( 0, 'int' );
55
+ this.panoLonRange = uniform( new Vector2(), 'vec2' );
56
+ this.panoLatRange = uniform( new Vector2(), 'vec2' );
57
+ this.panoLevelHorizon = uniform( 1, 'int' );
51
58
  this.resolutionWidth = uniform( options.width || 1 );
52
59
  this.resolutionHeight = uniform( options.height || 1 );
53
60
 
@@ -218,20 +225,12 @@ export class NormalDepth extends RenderStage {
218
225
 
219
226
  If( gx.lessThan( int( resW ) ).and( gy.lessThan( int( resH ) ) ), () => {
220
227
 
221
- // Pixel center → NDC, Y negated for Three.js WebGPU.
222
- const ndcX = float( gx ).add( 0.5 ).div( resW ).mul( 2.0 ).sub( 1.0 );
223
- const ndcY = float( gy ).add( 0.5 ).div( resH ).mul( 2.0 ).sub( 1.0 ).negate();
224
- const ndcPos = vec3( ndcX, ndcY, 1.0 );
225
-
226
228
  // No jitter — deterministic per-pixel ray so the temporal gate
227
229
  // sees stable per-pixel normals across frames.
228
- const rayDirCS = camProjInvMat.mul( vec4( ndcPos, 1.0 ) );
229
- const rayDirWorld = normalize(
230
- mat3(
231
- camWorldMat[ 0 ].xyz,
232
- camWorldMat[ 1 ].xyz,
233
- camWorldMat[ 2 ].xyz
234
- ).mul( rayDirCS.xyz.div( rayDirCS.w ) )
230
+ const uv = vec2( float( gx ).add( 0.5 ).div( resW ), float( gy ).add( 0.5 ).div( resH ) );
231
+ const rayDirWorld = cameraRayDirection(
232
+ uv, camWorldMat, camProjInvMat,
233
+ this.cameraProjection, this.panoLonRange, this.panoLatRange, this.panoLevelHorizon
235
234
  );
236
235
  const rayOrigin = vec3( camWorldMat[ 3 ] );
237
236
 
@@ -307,6 +306,10 @@ export class NormalDepth extends RenderStage {
307
306
 
308
307
  this.cameraWorldMatrix.value.copy( pt.uniforms.get( 'cameraWorldMatrix' ).value );
309
308
  this.cameraProjectionMatrixInverse.value.copy( pt.uniforms.get( 'cameraProjectionMatrixInverse' ).value );
309
+ this.cameraProjection.value = pt.uniforms.get( 'cameraProjection' ).value;
310
+ this.panoLonRange.value.copy( pt.uniforms.get( 'panoLonRange' ).value );
311
+ this.panoLatRange.value.copy( pt.uniforms.get( 'panoLatRange' ).value );
312
+ this.panoLevelHorizon.value = pt.uniforms.get( 'panoLevelHorizon' ).value;
310
313
 
311
314
  }
312
315
 
@@ -31,6 +31,13 @@ import {
31
31
 
32
32
  const log = createLogger( 'wavefront' );
33
33
 
34
+ // Shared by the 1D index-list kernels built inline below.
35
+ const LIST_WG_SIZE = 256;
36
+
37
+ // Resized per bounce iteration, each from its own registered workgroup size. Unregistered entries
38
+ // (the sort passes when _sortMaterials is off) are skipped by setDispatchForCount.
39
+ const BOUNCE_KERNELS = [ 'extend', 'shade', 'globalHist', 'globalScatter', 'compact', 'compactCopyback' ];
40
+
34
41
  export class PathTracer extends PathTracerStage {
35
42
 
36
43
  constructor( renderer, scene, camera, options = {} ) {
@@ -350,7 +357,7 @@ export class PathTracer extends PathTracerStage {
350
357
 
351
358
  // reset counters → compact non-frozen pixel IDs → publish count → list-driven 1D generate.
352
359
  const genSized = Math.min( maxRays, Math.ceil( activeBounce0 * 1.5 ) + 1024 );
353
- km.setDispatchCount( 'generateList', [ Math.ceil( genSized / 256 ), 1, 1 ] );
360
+ km.setDispatchForCount( 'generateList', genSized );
354
361
  km.dispatch( 'resetFrameCounters' );
355
362
  km.dispatch( 'buildActivePixels' );
356
363
  km.dispatch( 'seedEnter' );
@@ -402,23 +409,12 @@ export class PathTracer extends PathTracerStage {
402
409
  }
403
410
 
404
411
  const sized = Math.min( maxRays, Math.ceil( entering * 1.5 ) + 1024 );
405
- const wg = [ Math.ceil( sized / 256 ), 1, 1 ];
406
- km.setDispatchCount( 'extend', wg );
407
- km.setDispatchCount( 'shade', wg );
408
- km.setDispatchCount( 'globalHist', wg );
409
- km.setDispatchCount( 'globalScatter', wg );
410
- km.setDispatchCount( 'compact', wg );
411
- km.setDispatchCount( 'compactCopyback', wg );
412
+ for ( const k of BOUNCE_KERNELS ) km.setDispatchForCount( k, sized );
412
413
 
413
414
  } else {
414
415
 
415
416
  km.dispatch( 'enterFull' );
416
- const full = [ Math.ceil( maxRays / 256 ), 1, 1 ];
417
- km.setDispatchCount( 'extend', full );
418
- km.setDispatchCount( 'shade', full );
419
- km.setDispatchCount( 'compact', full );
420
- km.setDispatchCount( 'globalHist', full );
421
- km.setDispatchCount( 'globalScatter', full );
417
+ for ( const k of BOUNCE_KERNELS ) km.setDispatchForCount( k, maxRays );
422
418
 
423
419
  }
424
420
 
@@ -935,7 +931,7 @@ export class PathTracer extends PathTracerStage {
935
931
 
936
932
  } );
937
933
  this._kernelManager.register( 'initActiveIndices',
938
- initFn().compute( [ Math.ceil( ( this._chunkRows * w ) / 256 ), 1, 1 ], [ 256, 1, 1 ] )
934
+ initFn().compute( [ Math.ceil( ( this._chunkRows * w ) / LIST_WG_SIZE ), 1, 1 ], [ LIST_WG_SIZE, 1, 1 ] )
939
935
  );
940
936
 
941
937
  const genParams = {
@@ -948,6 +944,10 @@ export class PathTracer extends PathTracerStage {
948
944
  frame: this.seedFrame,
949
945
  cameraWorldMatrix: this.cameraWorldMatrix,
950
946
  cameraProjectionMatrixInverse: this.cameraProjectionMatrixInverse,
947
+ cameraProjection: this.cameraProjection,
948
+ panoLonRange: this.panoLonRange,
949
+ panoLatRange: this.panoLatRange,
950
+ panoLevelHorizon: this.panoLevelHorizon,
951
951
  enableDOF: this.enableDOF,
952
952
  focalLength: this.focalLength,
953
953
  aperture: this.aperture,
@@ -977,13 +977,12 @@ export class PathTracer extends PathTracerStage {
977
977
  const freezeK = this.pixelFreezeStability;
978
978
  const resetFrameFn = Fn( () => {
979
979
 
980
- // ACTIVE is per-chunk (zeroed before every chunk's scatter). CONVERGED/FROZEN are per-FRAME counters
981
- // summed across chunks in FinalWrite → zero them only on the first chunk of the frame.
980
+ // ACTIVE is per-chunk (zeroed before every chunk's scatter). CONVERGED is a per-FRAME counter
981
+ // summed across chunks in FinalWrite → zero it only on the first chunk of the frame.
982
982
  atomicStore( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ), uint( 0 ) );
983
983
  If( this._wfIsFirstChunk.greaterThan( uint( 0 ) ), () => {
984
984
 
985
985
  atomicStore( counters.element( uint( COUNTER.CONVERGED_COUNT ) ), uint( 0 ) );
986
- atomicStore( counters.element( uint( COUNTER.FROZEN_COUNT ) ), uint( 0 ) );
987
986
 
988
987
  } );
989
988
 
@@ -1050,7 +1049,7 @@ export class PathTracer extends PathTracerStage {
1050
1049
 
1051
1050
  } );
1052
1051
  this._kernelManager.register( 'buildActivePixels',
1053
- buildActiveFn().compute( [ Math.ceil( ( this._chunkRows * w ) / 256 ), 1, 1 ], [ 256, 1, 1 ] )
1052
+ buildActiveFn().compute( [ Math.ceil( ( this._chunkRows * w ) / LIST_WG_SIZE ), 1, 1 ], [ LIST_WG_SIZE, 1, 1 ] )
1054
1053
  );
1055
1054
 
1056
1055
  const seedEnterFn = Fn( () => {
@@ -1073,7 +1072,7 @@ export class PathTracer extends PathTracerStage {
1073
1072
  counters,
1074
1073
  } );
1075
1074
  this._kernelManager.register( 'generateList',
1076
- genListFn().compute( [ Math.ceil( maxRays / 256 ), 1, 1 ], [ 256, 1, 1 ] )
1075
+ genListFn().compute( [ Math.ceil( maxRays / LIST_WG_SIZE ), 1, 1 ], [ LIST_WG_SIZE, 1, 1 ] )
1077
1076
  );
1078
1077
 
1079
1078
  const freshBvh = this.bvhStorageNode;
@@ -1108,7 +1107,6 @@ export class PathTracer extends PathTracerStage {
1108
1107
  const extFn = buildExtendKernel( {
1109
1108
  bvhBuffer: freshBvh,
1110
1109
  triangleBuffer: freshTri,
1111
- materialBuffer: freshMat,
1112
1110
  rayBufferRO: pb.rayBuffer.ro,
1113
1111
  hitBufferRW: pb.hitBuffer.rw,
1114
1112
  activeIndicesRO: qm.getActiveReadRO(),
@@ -1292,7 +1290,7 @@ export class PathTracer extends PathTracerStage {
1292
1290
 
1293
1291
  } );
1294
1292
  this._kernelManager.register( 'compactCopyback',
1295
- copyFn().compute( [ Math.ceil( maxRays / 256 ), 1, 1 ], [ 256, 1, 1 ] )
1293
+ copyFn().compute( [ Math.ceil( maxRays / LIST_WG_SIZE ), 1, 1 ], [ LIST_WG_SIZE, 1, 1 ] )
1296
1294
  );
1297
1295
 
1298
1296
  const fwFn = buildFinalWriteKernel( {
@@ -1324,7 +1322,6 @@ export class PathTracer extends PathTracerStage {
1324
1322
  // Tier-2 freeze (stamp + pass-through)
1325
1323
  usePixelFreeze: this.usePixelFreeze,
1326
1324
  pixelFreezeThreshold: this.pixelFreezeThreshold,
1327
- pixelFreezeStability: this.pixelFreezeStability,
1328
1325
  streakBufferRW: streakRW,
1329
1326
  frozenMaskRO, // dilated frozen mask (read-only; matches the active-list decision)
1330
1327
  chunkRowBase: this._wfChunkRowBase,
@@ -1351,6 +1348,10 @@ export class PathTracer extends PathTracerStage {
1351
1348
  cameraProjectionMatrixInverse: this.cameraProjectionMatrixInverse,
1352
1349
  cameraProjectionMatrix: this.cameraProjectionMatrix,
1353
1350
  cameraViewMatrix: this.cameraViewMatrix,
1351
+ cameraProjection: this.cameraProjection,
1352
+ panoLonRange: this.panoLonRange,
1353
+ panoLatRange: this.panoLatRange,
1354
+ panoLevelHorizon: this.panoLevelHorizon,
1354
1355
  enableDOF: this.enableDOF,
1355
1356
  focalLength: this.focalLength,
1356
1357
  aperture: this.aperture,
@@ -1396,10 +1397,7 @@ export class PathTracer extends PathTracerStage {
1396
1397
  const w = this._wfRenderWidth.value;
1397
1398
  const h = this._wfRenderHeight.value;
1398
1399
 
1399
- this._kernelManager.setDispatchCount( 'debug', [
1400
- Math.ceil( w / DEBUG_WG_SIZE ),
1401
- Math.ceil( h / DEBUG_WG_SIZE ), 1
1402
- ] );
1400
+ this._kernelManager.setDispatchForGrid( 'debug', w, h );
1403
1401
 
1404
1402
  }
1405
1403
 
@@ -1416,10 +1414,10 @@ export class PathTracer extends PathTracerStage {
1416
1414
  this._wfMaxRayCount.value = chunkPixels;
1417
1415
 
1418
1416
  const km = this._kernelManager;
1419
- km.setDispatchCount( 'generate', [ Math.ceil( w / GENERATE_WG_SIZE ), Math.ceil( rows / GENERATE_WG_SIZE ), 1 ] );
1420
- km.setDispatchCount( 'finalWrite', [ Math.ceil( w / FINALWRITE_WG_SIZE ), Math.ceil( rows / FINALWRITE_WG_SIZE ), 1 ] );
1421
- km.setDispatchCount( 'initActiveIndices', [ Math.ceil( chunkPixels / 256 ), 1, 1 ] );
1422
- km.setDispatchCount( 'buildActivePixels', [ Math.ceil( chunkPixels / 256 ), 1, 1 ] );
1417
+ km.setDispatchForGrid( 'generate', w, rows );
1418
+ km.setDispatchForGrid( 'finalWrite', w, rows );
1419
+ km.setDispatchForCount( 'initActiveIndices', chunkPixels );
1420
+ km.setDispatchForCount( 'buildActivePixels', chunkPixels );
1423
1421
 
1424
1422
  return chunkPixels;
1425
1423