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.
@@ -14,17 +14,14 @@ import {
14
14
  min,
15
15
  normalize,
16
16
  mix,
17
- vec4,
18
17
  notEqual,
19
18
  lessThan,
20
- mat3,
21
19
  array,
22
20
  bool as tslBool,
23
21
  } from 'three/tsl';
24
22
 
25
- import { Ray, HitInfo } from './Struct.js';
23
+ import { HitInfo } from './Struct.js';
26
24
  import { getDatafromStorageBuffer } from './Common.js';
27
- import { RandomPointInCircle } from './Random.js';
28
25
 
29
26
  const MAX_STACK_DEPTH = 32;
30
27
  const MAX_BVH_ITERATIONS = 512;
@@ -559,66 +556,3 @@ export const traverseBVHShadow = Fn( ( [
559
556
  return closestHit;
560
557
 
561
558
  } );
562
-
563
- // ================================================================================
564
- // CAMERA RAY GENERATION
565
- // ================================================================================
566
-
567
- export const generateRayFromCamera = Fn( ( [
568
- screenPosition, rngState,
569
- cameraWorldMatrix, cameraProjectionMatrixInverse,
570
- enableDOF, focalLength, aperture, focusDistance, sceneScale, apertureScale, anamorphicRatio
571
- ] ) => {
572
-
573
- // Convert screen position to NDC
574
- const ndcPos = vec3( screenPosition.xy, 1.0 );
575
-
576
- // Convert NDC to camera space
577
- const rayDirCS = cameraProjectionMatrixInverse.mul( vec4( ndcPos, 1.0 ) );
578
-
579
- // Convert to world space
580
- const rayDirectionWorld = normalize( mat3(
581
- cameraWorldMatrix[ 0 ].xyz,
582
- cameraWorldMatrix[ 1 ].xyz,
583
- cameraWorldMatrix[ 2 ].xyz
584
- ).mul( rayDirCS.xyz.div( rayDirCS.w ) ) ).toVar();
585
-
586
- const rayOriginWorld = vec3( cameraWorldMatrix[ 3 ] ).toVar();
587
-
588
- const resultOrigin = rayOriginWorld.toVar();
589
- const resultDirection = rayDirectionWorld.toVar();
590
-
591
- // Check if DOF is disabled or conditions make it ineffective
592
- If( enableDOF.and( focalLength.greaterThan( 0.0 ) ).and( aperture.lessThan( 64.0 ) ).and( focusDistance.greaterThan( 0.001 ) ), () => {
593
-
594
- // Physical aperture calculation
595
- const effectiveAperture = focalLength.div( aperture );
596
- // Apply scene scale to maintain correct physical aperture size
597
- const apertureRadius = effectiveAperture.mul( 0.001 ).mul( sceneScale ).mul( apertureScale );
598
-
599
- // Generate random point on aperture disk
600
- const randomPoint = RandomPointInCircle( rngState );
601
-
602
- // Apply anamorphic squeeze — stretch horizontally for oval bokeh
603
- const lensX = randomPoint.x.mul( anamorphicRatio.max( 0.01 ) );
604
- const lensY = randomPoint.y;
605
-
606
- // Extract camera coordinate system directly from camera matrix
607
- const cameraRight = normalize( vec3( cameraWorldMatrix[ 0 ] ) );
608
- const cameraUp = normalize( vec3( cameraWorldMatrix[ 1 ] ) );
609
-
610
- // Apply aperture offset using camera's actual coordinate system
611
- const offset = cameraRight.mul( lensX ).add( cameraUp.mul( lensY ) ).mul( apertureRadius );
612
-
613
- // Calculate new ray from offset origin to focal point
614
- resultOrigin.assign( rayOriginWorld.add( offset ) );
615
- resultDirection.assign( normalize( rayOriginWorld.add( rayDirectionWorld.mul( focusDistance ) ).sub( resultOrigin ) ) );
616
-
617
- } );
618
-
619
- return Ray( {
620
- origin: resultOrigin,
621
- direction: resultDirection,
622
- } );
623
-
624
- } );
@@ -0,0 +1,141 @@
1
+ /**
2
+ * CameraRay.js — primary camera ray generation.
3
+ *
4
+ * Two projections behind one `cameraProjection` uniform: 0 = pinhole (NDC through
5
+ * cameraProjectionMatrixInverse), 1 = equirectangular 360 panorama. Both branches live in the same
6
+ * kernel, so switching modes writes a uniform and resets accumulation — it never recompiles WGSL.
7
+ *
8
+ * Equirect mapping, for uv ∈ [0,1] with v = 0 at the top row:
9
+ * lon = mix( lonMin, lonMax, u ), lat = mix( latMax, latMin, v )
10
+ * dirCam = ( sin(lon)·cos(lat), sin(lat), -cos(lon)·cos(lat) )
11
+ * so lon = lat = 0 is camera-forward (image centre), u = 0.75 is camera-right and v = 0 is the zenith.
12
+ */
13
+
14
+ import {
15
+ Fn, vec3, vec4, float, int,
16
+ If, normalize, mat3, mix, sin, cos, cross, dot,
17
+ } from 'three/tsl';
18
+
19
+ import { Ray } from './Struct.js';
20
+ import { constructTBN } from './Common.js';
21
+ import { RandomPointInCircle } from './Random.js';
22
+
23
+ /** World-space primary ray direction for pixel uv. Origin is always cameraWorldMatrix[3]. */
24
+ export const cameraRayDirection = Fn( ( [
25
+ uv01, cameraWorldMatrix, cameraProjectionMatrixInverse,
26
+ cameraProjection, panoLonRange, panoLatRange, panoLevelHorizon
27
+ ] ) => {
28
+
29
+ const direction = vec3( 0.0 ).toVar();
30
+
31
+ If( cameraProjection.equal( int( 1 ) ), () => {
32
+
33
+ const lon = mix( panoLonRange.x, panoLonRange.y, uv01.x ).toVar();
34
+ const lat = mix( panoLatRange.y, panoLatRange.x, uv01.y ).toVar();
35
+ const cosLat = cos( lat ).toVar();
36
+
37
+ const right = vec3( 0.0 ).toVar();
38
+ const up = vec3( 0.0 ).toVar();
39
+ const back = vec3( 0.0 ).toVar();
40
+
41
+ If( panoLevelHorizon.equal( int( 1 ) ), () => {
42
+
43
+ // Yaw-only frame — orbit pitch/roll must not tilt the panorama. The flattened column
44
+ // is normalized below, so it needs no pre-normalize.
45
+ const flat = vec3( cameraWorldMatrix[ 2 ].x, 0.0, cameraWorldMatrix[ 2 ].z ).toVar();
46
+ // Straight up/down: the camera's own up vector is the horizontal one.
47
+ If( dot( flat, flat ).lessThan( float( 1e-8 ) ), () => {
48
+
49
+ flat.assign( vec3( cameraWorldMatrix[ 1 ].x, 0.0, cameraWorldMatrix[ 1 ].z ) );
50
+
51
+ } );
52
+
53
+ back.assign( normalize( flat ) );
54
+ up.assign( vec3( 0.0, 1.0, 0.0 ) );
55
+ right.assign( cross( up, back ) );
56
+
57
+ } ).Else( () => {
58
+
59
+ right.assign( normalize( vec3( cameraWorldMatrix[ 0 ] ) ) );
60
+ up.assign( normalize( vec3( cameraWorldMatrix[ 1 ] ) ) );
61
+ back.assign( normalize( vec3( cameraWorldMatrix[ 2 ] ) ) );
62
+
63
+ } );
64
+
65
+ direction.assign( normalize(
66
+ right.mul( sin( lon ).mul( cosLat ) )
67
+ .add( up.mul( sin( lat ) ) )
68
+ .sub( back.mul( cos( lon ).mul( cosLat ) ) )
69
+ ) );
70
+
71
+ } ).Else( () => {
72
+
73
+ const ndcPos = vec3( uv01.x.mul( 2.0 ).sub( 1.0 ), float( 1.0 ).sub( uv01.y.mul( 2.0 ) ), 1.0 );
74
+ const rayDirCS = cameraProjectionMatrixInverse.mul( vec4( ndcPos, 1.0 ) );
75
+
76
+ direction.assign( normalize( mat3(
77
+ cameraWorldMatrix[ 0 ].xyz,
78
+ cameraWorldMatrix[ 1 ].xyz,
79
+ cameraWorldMatrix[ 2 ].xyz
80
+ ).mul( rayDirCS.xyz.div( rayDirCS.w ) ) ) );
81
+
82
+ } );
83
+
84
+ return direction;
85
+
86
+ } );
87
+
88
+ export const generateRayFromCamera = Fn( ( [
89
+ uv01, rngState,
90
+ cameraWorldMatrix, cameraProjectionMatrixInverse,
91
+ cameraProjection, panoLonRange, panoLatRange, panoLevelHorizon,
92
+ enableDOF, focalLength, aperture, focusDistance, sceneScale, apertureScale, anamorphicRatio
93
+ ] ) => {
94
+
95
+ const rayOriginWorld = vec3( cameraWorldMatrix[ 3 ] ).toVar();
96
+ // .toVar() so the two reads below don't inline the whole projection graph twice.
97
+ const rayDirectionWorld = cameraRayDirection(
98
+ uv01, cameraWorldMatrix, cameraProjectionMatrixInverse,
99
+ cameraProjection, panoLonRange, panoLatRange, panoLevelHorizon
100
+ ).toVar();
101
+
102
+ const resultOrigin = rayOriginWorld.toVar();
103
+ const resultDirection = rayDirectionWorld.toVar();
104
+
105
+ If( enableDOF.and( focalLength.greaterThan( 0.0 ) ).and( aperture.lessThan( 64.0 ) ).and( focusDistance.greaterThan( 0.001 ) ), () => {
106
+
107
+ const effectiveAperture = focalLength.div( aperture );
108
+ const apertureRadius = effectiveAperture.mul( 0.001 ).mul( sceneScale ).mul( apertureScale );
109
+
110
+ const randomPoint = RandomPointInCircle( rngState );
111
+ // Anamorphic squeeze — stretch horizontally for oval bokeh
112
+ const lensX = randomPoint.x.mul( anamorphicRatio.max( 0.01 ) );
113
+ const lensY = randomPoint.y;
114
+
115
+ const lensOffset = vec3( 0.0 ).toVar();
116
+
117
+ If( cameraProjection.equal( int( 1 ) ), () => {
118
+
119
+ // Every pixel points a different way, so the lens plane is built around the ray. The
120
+ // camera's right/up would skew bokeh into a slit away from the image centre.
121
+ lensOffset.assign( constructTBN( { N: rayDirectionWorld } ).mul( vec3( lensX, lensY, 0.0 ) ) );
122
+
123
+ } ).Else( () => {
124
+
125
+ const camRight = normalize( vec3( cameraWorldMatrix[ 0 ] ) );
126
+ const camUp = normalize( vec3( cameraWorldMatrix[ 1 ] ) );
127
+ lensOffset.assign( camRight.mul( lensX ).add( camUp.mul( lensY ) ) );
128
+
129
+ } );
130
+
131
+ resultOrigin.assign( rayOriginWorld.add( lensOffset.mul( apertureRadius ) ) );
132
+ resultDirection.assign( normalize( rayOriginWorld.add( rayDirectionWorld.mul( focusDistance ) ).sub( resultOrigin ) ) );
133
+
134
+ } );
135
+
136
+ return Ray( {
137
+ origin: resultOrigin,
138
+ direction: resultDirection,
139
+ } );
140
+
141
+ } );
@@ -13,7 +13,7 @@ import {
13
13
  localId, workgroupId,
14
14
  } from 'three/tsl';
15
15
 
16
- import { generateRayFromCamera } from './BVHTraversal.js';
16
+ import { generateRayFromCamera } from './CameraRay.js';
17
17
  import { Ray } from './Struct.js';
18
18
  import { TraceDebugMode } from './Debugger.js';
19
19
  import { pcgHash, getStratifiedSample } from './Random.js';
@@ -26,6 +26,7 @@ export function buildDebugKernel( params ) {
26
26
  writeColorTex, writeNDTex, writeAlbedoTex,
27
27
  resolution, renderWidth, renderHeight,
28
28
  cameraWorldMatrix, cameraProjectionMatrixInverse, cameraProjectionMatrix, cameraViewMatrix,
29
+ cameraProjection, panoLonRange, panoLatRange, panoLevelHorizon,
29
30
  enableDOF, focalLength, aperture, focusDistance, sceneScale, apertureScale, anamorphicRatio,
30
31
  bvhBuffer, triangleBuffer, materialBuffer,
31
32
  envTexture, environmentMatrix, environmentIntensity, enableEnvironmentLight,
@@ -45,12 +46,10 @@ export function buildDebugKernel( params ) {
45
46
  const seed = pcgHash( { state: uint( pixelIndex ).add( uint( 1 ) ) } ).toVar();
46
47
 
47
48
  // Center-pixel primary ray (no AA jitter — debug viz wants a stable, sharp image).
48
- const screenPosition = pixelCoord.div( resolution ).mul( 2.0 ).sub( 1.0 ).toVar();
49
- screenPosition.y.assign( screenPosition.y.negate() );
50
-
51
49
  const ray = Ray.wrap( generateRayFromCamera(
52
- screenPosition, seed,
50
+ pixelCoord.div( resolution ), seed,
53
51
  cameraWorldMatrix, cameraProjectionMatrixInverse,
52
+ cameraProjection, panoLonRange, panoLatRange, panoLevelHorizon,
54
53
  enableDOF, focalLength, aperture, focusDistance, sceneScale, apertureScale, anamorphicRatio,
55
54
  ) );
56
55
 
@@ -13,17 +13,17 @@ import {
13
13
  import { traverseBVH } from './BVHTraversal.js';
14
14
  import { Ray, HitInfo } from './Struct.js';
15
15
  import {
16
- readRayOrigin, readRayDirection, readMediumStack,
16
+ readRayOrigin, readRayDirection, readRayBounceFlags, readMediumStack,
17
17
  writeHitPacked,
18
18
  } from '../Processor/PackedRayBuffer.js';
19
- import { COUNTER } from '../Processor/QueueManager.js';
19
+ import { COUNTER, RAY_FLAG } from '../Processor/QueueManager.js';
20
20
 
21
21
  const WG_SIZE = 256;
22
22
 
23
23
  export function buildExtendKernel( params ) {
24
24
 
25
25
  const {
26
- bvhBuffer, triangleBuffer, materialBuffer,
26
+ bvhBuffer, triangleBuffer,
27
27
  rayBufferRO,
28
28
  hitBufferRW,
29
29
  activeIndicesRO,
@@ -45,6 +45,14 @@ export function buildExtendKernel( params ) {
45
45
 
46
46
  const rayID = activeIndicesRO.element( threadIdx );
47
47
 
48
+ // Parity with Shade's guard. Free — the flags share DIR_FLAGS.w with the direction read below.
49
+ // Only culls work on the pinned-dispatch path, whose identity list still holds dead rays.
50
+ If( readRayBounceFlags( rayBufferRO, rayID ).bitAnd( uint( RAY_FLAG.ACTIVE ) ).equal( uint( 0 ) ), () => {
51
+
52
+ Return();
53
+
54
+ } );
55
+
48
56
  const origin = readRayOrigin( rayBufferRO, rayID ).toVar();
49
57
  const direction = readRayDirection( rayBufferRO, rayID ).toVar();
50
58
 
@@ -54,7 +54,7 @@ export function buildFinalWriteKernel( params ) {
54
54
 
55
55
  // Tier-2 per-pixel freeze: streakBufferRW = per-pixel freeze-candidate streak (stamped here);
56
56
  // frozenMaskRO = dilated frozen mask from buildActivePixels (pass-through gates on it, not streak).
57
- usePixelFreeze, pixelFreezeThreshold, pixelFreezeStability, streakBufferRW, frozenMaskRO,
57
+ usePixelFreeze, pixelFreezeThreshold, streakBufferRW, frozenMaskRO,
58
58
  } = params;
59
59
 
60
60
  const auxOn = auxGBufferEnabled.greaterThan( uint( 0 ) );
@@ -184,25 +184,12 @@ export function buildFinalWriteKernel( params ) {
184
184
 
185
185
  } );
186
186
 
187
- If( wasFrozen, () => {
188
-
189
- // Already frozen (passed through above) — count it.
190
- atomicAdd( counters.element( uint( COUNTER.FROZEN_COUNT ) ), uint( 1 ) );
191
-
192
- } );
193
-
194
187
  If( wasFrozen.not().and( frame.greaterThan( uint( 0 ) ) ), () => {
195
188
 
196
189
  const freezeCandidate = frame.greaterThanEqual( uint( adaptiveMinSamples ) )
197
190
  .and( relErr.lessThan( pixelFreezeThreshold ) );
198
191
  const newStreak = select( freezeCandidate, streakBufferRW.element( pixelId ).add( uint( 1 ) ), uint( 0 ) ).toVar();
199
192
  streakBufferRW.element( pixelId ).assign( newStreak );
200
- // Just reached K → frozen next frame; count it now so the frozen population is current.
201
- If( newStreak.greaterThanEqual( uint( pixelFreezeStability ) ), () => {
202
-
203
- atomicAdd( counters.element( uint( COUNTER.FROZEN_COUNT ) ), uint( 1 ) );
204
-
205
- } );
206
193
 
207
194
  } );
208
195
 
@@ -22,7 +22,7 @@ import {
22
22
  getStratifiedSample,
23
23
  } from './Random.js';
24
24
 
25
- import { generateRayFromCamera } from './BVHTraversal.js';
25
+ import { generateRayFromCamera } from './CameraRay.js';
26
26
  import { Ray } from './Struct.js';
27
27
  import { RAY_FLAG, COUNTER } from '../Processor/QueueManager.js';
28
28
  import {
@@ -39,6 +39,7 @@ export function buildGenerateKernel( params ) {
39
39
  rayBufferRW, rngBufferRW, gBufferRW,
40
40
  resolution, frame,
41
41
  cameraWorldMatrix, cameraProjectionMatrixInverse,
42
+ cameraProjection, panoLonRange, panoLatRange, panoLevelHorizon,
42
43
  enableDOF, focalLength, aperture, focusDistance, sceneScale, apertureScale, anamorphicRatio,
43
44
  renderWidth,
44
45
  chunkRowBase, chunkRows, // row band offset (global first row) + row count for this chunk
@@ -56,9 +57,6 @@ export function buildGenerateKernel( params ) {
56
57
 
57
58
  const pixelCoord = vec2( float( gx ).add( 0.5 ), float( gy ).add( 0.5 ) );
58
59
 
59
- const screenPosition = pixelCoord.div( resolution ).mul( 2.0 ).sub( 1.0 ).toVar();
60
- screenPosition.y.assign( screenPosition.y.negate() );
61
-
62
60
  const baseSeed = getDecorrelatedSeed( { pixelCoord, rayIndex: int( 0 ), frame } ).toVar();
63
61
  const seed = pcgHash( { state: baseSeed } ).toVar();
64
62
 
@@ -68,13 +66,18 @@ export function buildGenerateKernel( params ) {
68
66
  // position from the first scatter direction (they were reading the identical cell).
69
67
  const stratifiedJitter = getStratifiedSample( pixelCoord, int( 1 ), int( 1 ), seed, resolution, frame ).toVar();
70
68
 
71
- const jitterScale = vec2( 2.0 ).div( resolution );
72
- const jitter = stratifiedJitter.sub( 0.5 ).mul( jitterScale );
73
- const jitteredScreenPosition = screenPosition.add( jitter );
69
+ // Y is subtracted because uv.y grows downward while the NDC this used to build grew upward —
70
+ // keeps the sub-pixel sample sequence bit-identical to the pre-panorama ray gen.
71
+ const jitterOffset = stratifiedJitter.sub( 0.5 );
72
+ const jitteredUV = vec2(
73
+ pixelCoord.x.add( jitterOffset.x ),
74
+ pixelCoord.y.sub( jitterOffset.y )
75
+ ).div( resolution );
74
76
 
75
77
  const ray = Ray.wrap( generateRayFromCamera(
76
- jitteredScreenPosition, seed,
78
+ jitteredUV, seed,
77
79
  cameraWorldMatrix, cameraProjectionMatrixInverse,
80
+ cameraProjection, panoLonRange, panoLatRange, panoLevelHorizon,
78
81
  enableDOF, focalLength, aperture, focusDistance, sceneScale, apertureScale, anamorphicRatio,
79
82
  ) );
80
83
 
@@ -111,20 +111,65 @@ export function buildGlobalPrefixKernel( { sortGlobalHistogram, bins = SORT_GLOB
111
111
 
112
112
  }
113
113
 
114
- // Pass 4 — scatter: atomicAdd on the prefix-summed histogram gives each ray its global slot
115
- // within its material's contiguous region.
114
+ // Pass 4 — scatter: each ray lands in a unique slot inside its material's contiguous region.
115
+ //
116
+ // Global atomics are reduced from O(rays) to O(workgroups × bins touched) by reserving one block
117
+ // per bin per workgroup, the same aggregation Pass 2 already applies to the histogram. `local`
118
+ // changes meaning across the barriers: first a per-workgroup count, then the workgroup's global
119
+ // base cursor for that bin, which the lanes then bump to claim their own slot.
120
+ //
121
+ // Slot order WITHIN a bin changes versus the per-ray version, but it was never deterministic —
122
+ // the old path raced on the same global atomicAdd. The output is still an exact permutation of the
123
+ // entering set, and Shade reads each ray independently.
116
124
  export function buildGlobalScatterKernel( { hitBufferRO, activeIndicesReadRO, sortedIndicesRW, sortGlobalHistogram, counters, bins = SORT_GLOBAL_MAX_BINS } ) {
117
125
 
118
126
  return Fn( () => {
119
127
 
120
128
  const tid = instanceIndex;
129
+ const lid = localId.x;
130
+ const local = workgroupAtomicArray( 'uint', bins );
131
+
132
+ If( lid.lessThan( uint( bins ) ), () => {
133
+
134
+ atomicStore( local.element( lid ), uint( 0 ) );
135
+
136
+ } );
137
+ workgroupBarrier();
138
+
139
+ // Read once and reuse: recomputing the bin after the barriers would re-fetch the hit buffer.
121
140
  const entering = atomicLoad( counters.element( uint( COUNTER.ENTERING_COUNT ) ) );
122
- If( tid.lessThan( entering ), () => {
141
+ const inRange = tid.lessThan( entering );
142
+ const rayID = uint( 0 ).toVar();
143
+ const bin = uint( 0 ).toVar();
123
144
 
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 );
145
+ If( inRange, () => {
146
+
147
+ rayID.assign( activeIndicesReadRO.element( tid ) );
148
+ bin.assign( uint( readHitMaterialIndex( hitBufferRO, rayID ) ).clamp( uint( 0 ), uint( bins - 1 ) ) );
149
+ atomicAdd( local.element( bin ), uint( 1 ) );
150
+
151
+ } );
152
+ workgroupBarrier();
153
+
154
+ // One global atomic per bin per workgroup reserves that block; the returned base replaces the
155
+ // count in place. Bins this workgroup never touched are skipped — no lane will read them.
156
+ If( lid.lessThan( uint( bins ) ), () => {
157
+
158
+ const slot = local.element( lid );
159
+ const c = atomicLoad( slot );
160
+ If( c.greaterThan( uint( 0 ) ), () => {
161
+
162
+ atomicStore( slot, atomicAdd( sortGlobalHistogram.element( lid ), c ) );
163
+
164
+ } );
165
+
166
+ } );
167
+ workgroupBarrier();
168
+
169
+ If( inRange, () => {
170
+
171
+ // Workgroup-local atomic: gives this lane its rank inside the block reserved above.
172
+ sortedIndicesRW.element( atomicAdd( local.element( bin ), uint( 1 ) ) ).assign( rayID );
128
173
 
129
174
  } );
130
175
 
@@ -225,6 +225,19 @@ export class DenoisingManager extends EventDispatcher {
225
225
  * @param {string} strategy - 'none' | 'asvgf' | 'edgeaware'
226
226
  * @param {string} [asvgfPreset] - ASVGF quality preset when strategy is 'asvgf'
227
227
  */
228
+ /**
229
+ * Active real-time denoiser, derived from stage state so it can't drift from
230
+ * setDenoiserStrategy / setASVGFEnabled.
231
+ * @returns {'asvgf'|'edgeaware'|'none'}
232
+ */
233
+ get denoiserStrategy() {
234
+
235
+ if ( this._stages.asvgf?.enabled ) return 'asvgf';
236
+ if ( this._stages.edgeFilter?.enabled ) return 'edgeaware';
237
+ return 'none';
238
+
239
+ }
240
+
228
241
  setDenoiserStrategy( strategy, asvgfPreset ) {
229
242
 
230
243
  const s = this._stages;
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import {
11
- RGBAFormat, RedFormat, FloatType, Vector2, Vector3, Color, Matrix4, DataTexture,
11
+ RGBAFormat, RedFormat, FloatType, LinearFilter, Vector2, Vector3, Color, Matrix4, DataTexture,
12
12
  } from 'three';
13
13
  import { EquirectHDRInfo } from '../Processor/EquirectHDRInfo.js';
14
14
  import { ProceduralSky } from '../Processor/ProceduralSky.js';
@@ -36,9 +36,12 @@ export class EnvironmentManager {
36
36
  this.proceduralSkyRenderer = null;
37
37
  this.simpleSkyRenderer = null;
38
38
 
39
- // Environment texture — 1×1 black placeholder for shader compilation
39
+ // Environment texture — 1×1 black placeholder for shader compilation. Filters must be
40
+ // Linear: DataTexture defaults to Nearest, which is unfilterable, and WGSLNodeBuilder then
41
+ // omits the `_sampler` binding that sampleEnvironment() takes as an explicit wgslFn arg.
40
42
  this._envPlaceholder = new DataTexture(
41
- new Float32Array( [ 0, 0, 0, 1 ] ), 1, 1, RGBAFormat, FloatType
43
+ new Float32Array( [ 0, 0, 0, 1 ] ), 1, 1, RGBAFormat, FloatType,
44
+ undefined, undefined, undefined, LinearFilter, LinearFilter
42
45
  );
43
46
  this._envPlaceholder.needsUpdate = true;
44
47
  this.environmentTexture = this._envPlaceholder;
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { uniform, uniformArray } from 'three/tsl';
9
- import { Vector2, Matrix4, Vector3, Color } from 'three';
9
+ import { Vector2, Matrix4, Vector3, Color, MathUtils } from 'three';
10
10
  import { samplingTechniqueUniform } from '../TSL/Random.js';
11
11
  import { ENGINE_DEFAULTS as DEFAULT_STATE } from '../EngineDefaults.js';
12
12
 
@@ -241,6 +241,13 @@ export class UniformManager {
241
241
  u( 'cameraViewMatrix', new Matrix4(), 'mat4' );
242
242
  u( 'cameraProjectionMatrix', new Matrix4(), 'mat4' );
243
243
 
244
+ // Projection: 0 = pinhole, 1 = equirectangular panorama. Ranges are radians (UI carries degrees).
245
+ const radRange = ( [ min, max ] ) => new Vector2( min, max ).multiplyScalar( MathUtils.DEG2RAD );
246
+ u( 'cameraProjection', DEFAULT_STATE.cameraProjection === 'equirectangular' ? 1 : 0, 'int' );
247
+ u( 'panoLonRange', radRange( DEFAULT_STATE.panoramaLonRange ), 'vec2' );
248
+ u( 'panoLatRange', radRange( DEFAULT_STATE.panoramaLatRange ), 'vec2' );
249
+ ub( 'panoLevelHorizon', DEFAULT_STATE.panoramaLevelHorizon );
250
+
244
251
  // DOF
245
252
  ub( 'enableDOF', DEFAULT_STATE.enableDOF );
246
253
  u( 'focusDistance', DEFAULT_STATE.focusDistance, 'float' );