rayzee 7.20.3 → 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.
Files changed (42) hide show
  1. package/README.md +75 -4
  2. package/dist/rayzee.es.js +2821 -2854
  3. package/dist/rayzee.umd.js +60 -58
  4. package/package.json +1 -1
  5. package/src/EngineDefaults.js +9 -2
  6. package/src/PathTracerApp.js +83 -0
  7. package/src/Pipeline/BindingAudit.js +178 -0
  8. package/src/Pipeline/RenderPipeline.js +5 -2
  9. package/src/Processor/BVHBuilder.js +1 -4
  10. package/src/Processor/CameraOptimizer.js +0 -5
  11. package/src/Processor/KernelManager.js +17 -70
  12. package/src/Processor/PackedRayBuffer.js +33 -8
  13. package/src/Processor/QueueManager.js +4 -3
  14. package/src/Processor/SceneProcessor.js +10 -4
  15. package/src/Processor/TreeletOptimizer.js +0 -7
  16. package/src/RenderSettings.js +41 -6
  17. package/src/Stages/ASVGF.js +10 -10
  18. package/src/Stages/BilateralFilter.js +70 -11
  19. package/src/Stages/EdgeFilter.js +44 -6
  20. package/src/Stages/NormalDepth.js +26 -15
  21. package/src/Stages/PathTracer.js +36 -34
  22. package/src/Stages/PathTracerStage.js +15 -46
  23. package/src/TSL/BVHTraversal.js +1 -67
  24. package/src/TSL/CameraRay.js +141 -0
  25. package/src/TSL/Common.js +1 -47
  26. package/src/TSL/DebugKernel.js +4 -5
  27. package/src/TSL/EmissiveSampling.js +40 -44
  28. package/src/TSL/ExtendKernel.js +11 -3
  29. package/src/TSL/FinalWriteKernel.js +1 -14
  30. package/src/TSL/GenerateKernel.js +11 -8
  31. package/src/TSL/LightsCore.js +3 -0
  32. package/src/TSL/LightsDirect.js +4 -4
  33. package/src/TSL/LightsSampling.js +138 -327
  34. package/src/TSL/PathTracerCore.js +2 -2
  35. package/src/TSL/Random.js +2 -2
  36. package/src/TSL/ShadeKernel.js +39 -17
  37. package/src/TSL/SortGlobalKernels.js +52 -7
  38. package/src/TSL/Struct.js +0 -10
  39. package/src/index.js +5 -0
  40. package/src/managers/DenoisingManager.js +13 -0
  41. package/src/managers/EnvironmentManager.js +6 -8
  42. package/src/managers/UniformManager.js +12 -2
package/src/TSL/Random.js CHANGED
@@ -132,7 +132,7 @@ export const RandomPointInCircle = ( rngState ) => {
132
132
  // +6 subsurface HG scatter dir (ShadeKernel)
133
133
  // +7 ground-catcher cosine BSDF (ShadeKernel)
134
134
  // +8 indirect-strategy direction (LightsIndirect)
135
- // 1D +0 {lights,BRDF} strategy (LightsSampling)
135
+ // 1D +0 (free — was the {lights,BRDF} stochastic strategy pick)
136
136
  // +1 emissive-triangle pick (EmissiveSampling / LightBVHSampling)
137
137
  // +2 BSDF lobe selection (ShadeKernel)
138
138
  // +3 Russian roulette (PathTracerCore)
@@ -155,7 +155,7 @@ export const RandomPointInCircle = ( rngState ) => {
155
155
  // Draws whose COUNT varies per pixel cannot sit in a per-bounce block without overflowing into
156
156
  // the next bounce's dimensions, so they live above AUX_BASE:
157
157
  // AUX_BASE + 0..63 env-backdrop blur taps (ShadeKernel)
158
- // AUX_BASE + 64..127 light reservoir, 4 types×16 (LightsSampling)
158
+ // AUX_BASE + 64.. light reservoir, one dimension per scene light (LightsSampling)
159
159
  export const SAMPLER_DIMS_PER_BOUNCE = 32;
160
160
  export const SAMPLER_DIM_AUX_BASE = 4096;
161
161
 
@@ -53,6 +53,7 @@ import {
53
53
  readMediumStack, writeMediumStack, readMediumSigmaA, writeMediumSigmaA,
54
54
  readPathBounces, readSssSteps, readSSSMedium, writeSSSMedium,
55
55
  readTransparentCount,
56
+ readMisRayT,
56
57
  readHitDistance, readHitBarycentrics, readHitNormal,
57
58
  readHitMaterialIndex, readHitTriangleIndex,
58
59
  writeRayOriginMeta, writeRayDirFlags, writeRayThroughputPdf, writeRayRadiance,
@@ -79,7 +80,7 @@ export function buildShadeKernel( params ) {
79
80
  counters,
80
81
  activeIndicesRO,
81
82
  envTexture, environmentIntensity, envMatrix,
82
- enableEnvironmentLight, useEnvMapIS,
83
+ enableEnvironmentLight,
83
84
  groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight, groundProjectionLevel,
84
85
  enableGroundCatcher, groundCatcherHeight,
85
86
  envTotalSum, envCompensationDelta, envResolution,
@@ -94,6 +95,10 @@ export function buildShadeKernel( params ) {
94
95
  globalIlluminationIntensity,
95
96
  cameraProjectionMatrix, cameraViewMatrix,
96
97
  fireflyThreshold, frame, resolution,
98
+ // Accumulation index, NOT the free-running seed counter `frame` rides. The firefly clamp
99
+ // relaxes as sqrt(frame+1) so its bias decays as a render converges; keying that off the
100
+ // seed axis let it grow for the whole session and disabled suppression outright.
101
+ accumFrame,
97
102
  chunkRowBase, // chunked path pool: global pixel = chunkRowBase·W + localSlot (rayID). See spec.
98
103
  emissiveTriangleCount, emissiveVec4Offset, emissiveTotalPower,
99
104
  emissiveBoost, totalTriangleCount, enableEmissiveTriangleSampling,
@@ -405,7 +410,7 @@ export function buildShadeKernel( params ) {
405
410
 
406
411
  // MIS weight for implicit env hit — prevents double-counting with NEE
407
412
  const envMisWeight = float( 1.0 ).toVar();
408
- If( isBackdropView.not().and( useEnvMapIS ), () => {
413
+ If( isBackdropView.not(), () => {
409
414
 
410
415
  const prevBouncePdf = readRayPdf( rayBufferRW, rayID );
411
416
  If( prevBouncePdf.greaterThan( 0.0 ), () => {
@@ -440,7 +445,7 @@ export function buildShadeKernel( params ) {
440
445
  currentRadiance.xyz.add(
441
446
  regularizePathContribution(
442
447
  throughput.mul( envColor ).mul( envScale ),
443
- fireflyPathLen, fireflyThreshold, int( frame ),
448
+ fireflyPathLen, fireflyThreshold, int( accumFrame ),
444
449
  ),
445
450
  ),
446
451
  currentRadiance.w
@@ -927,7 +932,10 @@ export function buildShadeKernel( params ) {
927
932
  } );
928
933
 
929
934
  } );
930
- writeRayOriginMeta( rayBufferRW, rayID, newOrigin, cameraDepth, sssSteps, transparentCount );
935
+ // Alpha skip keeps the direction, so the MIS origin is recoverable: accumulate the segment.
936
+ // Any redirection (refraction/reflection/SSS) breaks the line — reset.
937
+ const misRayT = select( interaction.isAlphaSkip, readMisRayT( rayBufferRW, rayID ).add( hitDist ), float( 0.0 ) );
938
+ writeRayOriginMeta( rayBufferRW, rayID, newOrigin, cameraDepth, sssSteps, transparentCount, misRayT );
931
939
  writeRayDirFlags( rayBufferRW, rayID, interaction.direction, flags );
932
940
  // Free bounce: preserve prevBouncePdf (megakernel keeps the last opaque-scatter pdf across
933
941
  // transmission/alpha-skip/SSS-boundary). Writing 1.0 corrupts the next bounce's env/emissive MIS,
@@ -980,18 +988,24 @@ export function buildShadeKernel( params ) {
980
988
  // MIS partner pdf MUST match the actual NEE sampler: re-walk the Light BVH descent
981
989
  // when it is active, else use the flat-CDF pdf. Mismatching them breaks MIS
982
990
  // partition-of-unity → a real bias (see calculateLightBVHPdf).
991
+ // Evaluate it at the scatter point that produced prevBouncePdf — alpha skips moved
992
+ // the ray origin, and the skip-point pdf under-counts dist² so full-Le BSDF hits
993
+ // pass nearly unweighted (fireflies on emitters inside alpha-blend shades).
994
+ const misT = readMisRayT( rayBufferRW, rayID ).toVar();
995
+ const misOrigin = origin.sub( direction.mul( misT ) ).toVar();
996
+ const misDist = hitDist.add( misT ).toVar();
983
997
  const lightPdf = float( 0.0 ).toVar();
984
998
  If( lightBVHNodeCount.greaterThan( int( 0 ) ), () => {
985
999
 
986
1000
  lightPdf.assign( calculateLightBVHPdf(
987
- int( hitTriIdx ), hitDist, direction, origin,
1001
+ int( hitTriIdx ), misDist, direction, misOrigin,
988
1002
  lightBuffer, emissiveVec4Offset, reverseMapVec4Offset, triangleBuffer,
989
1003
  ) );
990
1004
 
991
1005
  } ).Else( () => {
992
1006
 
993
1007
  lightPdf.assign( calculateEmissiveLightPdf(
994
- int( hitTriIdx ), hitDist, direction, origin,
1008
+ int( hitTriIdx ), misDist, direction, misOrigin,
995
1009
  triangleBuffer, materialBuffer, emissiveTotalPower,
996
1010
  ) );
997
1011
 
@@ -1008,7 +1022,7 @@ export function buildShadeKernel( params ) {
1008
1022
  currentRadiance.xyz.add(
1009
1023
  regularizePathContribution(
1010
1024
  emissive.mul( throughput ).mul( emissiveGiScale ).mul( emissiveMISWeight ),
1011
- float( bounceIndex ), fireflyThreshold, int( frame ),
1025
+ float( bounceIndex ), fireflyThreshold, int( accumFrame ),
1012
1026
  ),
1013
1027
  ),
1014
1028
  currentRadiance.w
@@ -1137,7 +1151,7 @@ export function buildShadeKernel( params ) {
1137
1151
  currentRadiance.xyz.add(
1138
1152
  regularizePathContribution(
1139
1153
  throughput.mul( directLight ).mul( giScale ),
1140
- float( bounceIndex ), fireflyThreshold, int( frame ),
1154
+ float( bounceIndex ), fireflyThreshold, int( accumFrame ),
1141
1155
  ),
1142
1156
  ),
1143
1157
  currentRadiance.w
@@ -1173,12 +1187,9 @@ export function buildShadeKernel( params ) {
1173
1187
  triangleBuffer,
1174
1188
  ) );
1175
1189
 
1176
- // skip rough diffuse surfaces on secondary bounces
1177
- const skip = bounceIndex.greaterThan( int( 1 ) )
1178
- .and( material.roughness.greaterThan( 0.9 ) )
1179
- .and( material.metalness.lessThan( 0.1 ) );
1180
-
1181
- If( skip.not().and( emissiveSample.valid ).and( emissiveSample.pdf.greaterThan( 0.0 ) ), () => {
1190
+ // No rough-diffuse secondary-bounce skip here: dropping NEE while the emissive-hit
1191
+ // MIS still down-weights BSDF hits deletes emitter energy from deep GI.
1192
+ If( emissiveSample.valid.and( emissiveSample.pdf.greaterThan( 0.0 ) ), () => {
1182
1193
 
1183
1194
  const NoL = max( float( 0.0 ), dot( N, emissiveSample.direction ) );
1184
1195
 
@@ -1210,7 +1221,7 @@ export function buildShadeKernel( params ) {
1210
1221
  currentRadiance.xyz.add(
1211
1222
  regularizePathContribution(
1212
1223
  emissiveLight.mul( throughput ).mul( giScale ),
1213
- float( bounceIndex ), fireflyThreshold, int( frame ),
1224
+ float( bounceIndex ), fireflyThreshold, int( accumFrame ),
1214
1225
  ),
1215
1226
  ),
1216
1227
  currentRadiance.w,
@@ -1239,7 +1250,7 @@ export function buildShadeKernel( params ) {
1239
1250
  currentRadiance.xyz.add(
1240
1251
  regularizePathContribution(
1241
1252
  emissiveLight.mul( throughput ).mul( giScale ),
1242
- float( bounceIndex ), fireflyThreshold, int( frame ),
1253
+ float( bounceIndex ), fireflyThreshold, int( accumFrame ),
1243
1254
  ),
1244
1255
  ),
1245
1256
  currentRadiance.w,
@@ -1265,6 +1276,17 @@ export function buildShadeKernel( params ) {
1265
1276
  ) ).toVar();
1266
1277
 
1267
1278
  const bounceDir = indirectResult.direction.toVar();
1279
+
1280
+ // Shading-normal leak guard: a normal-mapped lobe can sample below the geometric surface;
1281
+ // tracing that ray tunnels through single-sided shells onto whatever sits behind them.
1282
+ const NgeoFF = select( dot( hitNormal, V ).lessThan( 0.0 ), hitNormal.negate(), hitNormal );
1283
+ If( brdfIsTransmission.not().and( dot( bounceDir, NgeoFF ).lessThanEqual( 0.0 ) ), () => {
1284
+
1285
+ commitDeferredAux( N );
1286
+ terminatePath();
1287
+ Return();
1288
+
1289
+ } );
1268
1290
  // combinedPdf is stored as next bounce's prevBouncePdf for NEE↔implicit-env MIS
1269
1291
  const bouncePdf = max( indirectResult.combinedPdf, 0.001 ).toVar();
1270
1292
  throughput.mulAssign( indirectResult.throughput );
@@ -1276,7 +1298,7 @@ export function buildShadeKernel( params ) {
1276
1298
  const rrSurvival = handleRussianRoulette(
1277
1299
  bounceIndex, throughput, mc, bounceDir, rngState,
1278
1300
  _pixelCoord, resolution, frame, dimBase,
1279
- enableEnvironmentLight, useEnvMapIS,
1301
+ enableEnvironmentLight,
1280
1302
  ).toVar();
1281
1303
  If( rrSurvival.lessThanEqual( 0.0 ), () => {
1282
1304
 
@@ -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
 
package/src/TSL/Struct.js CHANGED
@@ -242,16 +242,6 @@ export const MaterialCache = struct( {
242
242
  } );
243
243
 
244
244
 
245
- // IMPROVEMENT: Dynamic MIS strategy based on material properties
246
- export const MISStrategy = struct( {
247
- brdfWeight: 'float',
248
- lightWeight: 'float',
249
- envWeight: 'float',
250
- useBRDFSampling: 'bool',
251
- useLightSampling: 'bool',
252
- useEnvSampling: 'bool',
253
- } );
254
-
255
245
  // General rendering state (used across all rendering paths)
256
246
  export const RenderState = struct( {
257
247
  traversals: 'int', // Remaining general bounces
package/src/index.js CHANGED
@@ -54,6 +54,11 @@ export { VRAMTracker, bufferBytes, textureBytes } from './Processor/VRAMTracker.
54
54
  export { RenderPipeline } from './Pipeline/RenderPipeline.js';
55
55
  export { RenderStage, StageExecutionMode } from './Pipeline/RenderStage.js';
56
56
  export { PipelineContext } from './Pipeline/PipelineContext.js';
57
+ export {
58
+ setBindingAudit,
59
+ getBindingAuditFindings,
60
+ clearBindingAuditFindings,
61
+ } from './Pipeline/BindingAudit.js';
57
62
 
58
63
  // Animation (for advanced consumers)
59
64
  export { AnimationManager } from './managers/AnimationManager.js';
@@ -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;
@@ -274,7 +277,6 @@ export class EnvironmentManager {
274
277
  this._updateCDFTexture();
275
278
  this.uniforms.set( 'envTotalSum', 0.0 );
276
279
  this.uniforms.set( 'envCompensationDelta', 0.0 );
277
- this.uniforms.set( 'useEnvMapIS', 0 );
278
280
  return;
279
281
 
280
282
  }
@@ -289,7 +291,6 @@ export class EnvironmentManager {
289
291
  this._updateCDFTexture();
290
292
  this.uniforms.set( 'envTotalSum', 0.0 );
291
293
  this.uniforms.set( 'envCompensationDelta', 0.0 );
292
- this.uniforms.set( 'useEnvMapIS', 0 );
293
294
  return;
294
295
 
295
296
  }
@@ -309,7 +310,6 @@ export class EnvironmentManager {
309
310
  this._updateCDFTexture();
310
311
  this.uniforms.set( 'envTotalSum', this.equirectHdrInfo.totalSum );
311
312
  this.uniforms.set( 'envCompensationDelta', this.equirectHdrInfo.compensationDelta );
312
- this.uniforms.set( 'useEnvMapIS', 1 );
313
313
 
314
314
  const { width, height } = this.equirectHdrInfo;
315
315
  if ( width && height ) {
@@ -326,7 +326,6 @@ export class EnvironmentManager {
326
326
  } catch ( error ) {
327
327
 
328
328
  log.error( 'CDF build failed:', error );
329
- this.uniforms.set( 'useEnvMapIS', 0 );
330
329
  this.uniforms.set( 'envTotalSum', 0.0 );
331
330
  this.uniforms.set( 'envCompensationDelta', 0.0 );
332
331
 
@@ -387,7 +386,6 @@ export class EnvironmentManager {
387
386
  this._updateCDFTexture();
388
387
  this.uniforms.set( 'envTotalSum', 0.0 );
389
388
  this.uniforms.set( 'envCompensationDelta', 0.0 );
390
- this.uniforms.set( 'useEnvMapIS', 0 );
391
389
 
392
390
  }
393
391
 
@@ -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
 
@@ -160,6 +160,10 @@ export class UniformManager {
160
160
 
161
161
  // Frame and sampling
162
162
  u( 'frame', 0, 'uint' );
163
+ // Sampler seed axis, separate from the accumulation index: `frame` is zeroed by every
164
+ // reset, and a camera move resets each frame of a drag, so the RNG redrew the identical
165
+ // sequence and temporal denoising had nothing to average.
166
+ u( 'seedFrame', 0, 'uint' );
163
167
  u( 'maxBounces', DEFAULT_STATE.bounces, 'int' );
164
168
  u( 'maxSamples', DEFAULT_STATE.maxSamples, 'int' );
165
169
  u( 'transmissiveBounces', DEFAULT_STATE.transmissiveBounces, 'int' );
@@ -198,7 +202,6 @@ export class UniformManager {
198
202
  ub( 'transparentBackground', DEFAULT_STATE.transparentBackground );
199
203
  ub( 'enableEnvironment', DEFAULT_STATE.enableEnvironment );
200
204
  u( 'environmentMatrix', new Matrix4(), 'mat4' );
201
- ub( 'useEnvMapIS', DEFAULT_STATE.useImportanceSampledEnvironment );
202
205
  u( 'envTotalSum', 0.0, 'float' );
203
206
  u( 'envCompensationDelta', 0.0, 'float' );
204
207
  u( 'envResolution', new Vector2( 1, 1 ), 'vec2' );
@@ -238,6 +241,13 @@ export class UniformManager {
238
241
  u( 'cameraViewMatrix', new Matrix4(), 'mat4' );
239
242
  u( 'cameraProjectionMatrix', new Matrix4(), 'mat4' );
240
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
+
241
251
  // DOF
242
252
  ub( 'enableDOF', DEFAULT_STATE.enableDOF );
243
253
  u( 'focusDistance', DEFAULT_STATE.focusDistance, 'float' );