rayzee 7.0.0 → 7.2.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.
@@ -1,11 +1,13 @@
1
1
  import { Fn, vec3, vec4, float, int, uint, uvec2, uniform, normalize, mat3, storage, If,
2
- textureStore, workgroupId, localId } from 'three/tsl';
2
+ texture, textureStore, workgroupId, localId } from 'three/tsl';
3
3
  import { RenderTarget, StorageTexture } from 'three/webgpu';
4
- import { HalfFloatType, RGBAFormat, NearestFilter, Matrix4, Box2, Vector2 } from 'three';
4
+ import { HalfFloatType, RGBAFormat, NearestFilter, LinearFilter, DataArrayTexture, Matrix4, Box2, Vector2 } from 'three';
5
5
  import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
6
6
  import { MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
7
- import { Ray, HitInfo } from '../TSL/Struct.js';
7
+ import { Ray, HitInfo, RayTracingMaterial, UVCache } from '../TSL/Struct.js';
8
8
  import { traverseBVH } from '../TSL/BVHTraversal.js';
9
+ import { getMaterial } from '../TSL/Common.js';
10
+ import { computeUVCache, processNormal, processBump } from '../TSL/TextureSampling.js';
9
11
 
10
12
  /**
11
13
  * NormalDepth — primary-ray G-buffer for SVGF gates.
@@ -21,7 +23,13 @@ import { traverseBVH } from '../TSL/BVHTraversal.js';
21
23
  * aliases current — without that aliasing prev would point at older data
22
24
  * while this frame's motion vector reflects zero motion → false rejection.
23
25
  *
24
- * Publishes: pathtracer:normalDepth, pathtracer:prevNormalDepth
26
+ * Also emits a SHADING normal (geometric normal perturbed by the normal/bump
27
+ * map, recomputed from the SAME deterministic hit — no extra ray) so the
28
+ * spatial denoiser's edge-stop can see normal-map detail the flat geometric
29
+ * normal hides. Deterministic ⇒ jitter-free, so it's safe for the gates.
30
+ *
31
+ * Publishes: pathtracer:normalDepth, pathtracer:prevNormalDepth,
32
+ * pathtracer:shadingNormal
25
33
  */
26
34
  export class NormalDepth extends RenderStage {
27
35
 
@@ -52,6 +60,22 @@ export class NormalDepth extends RenderStage {
52
60
  this._outputStorageTex.minFilter = NearestFilter;
53
61
  this._outputStorageTex.magFilter = NearestFilter;
54
62
 
63
+ // Shading-normal output (geometric normal perturbed by normal/bump map).
64
+ // Single buffer — only the spatial filter (current frame) consumes it.
65
+ this._shadingStorageTex = new StorageTexture( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE );
66
+ this._shadingStorageTex.type = HalfFloatType;
67
+ this._shadingStorageTex.format = RGBAFormat;
68
+ this._shadingStorageTex.minFilter = NearestFilter;
69
+ this._shadingStorageTex.magFilter = NearestFilter;
70
+ this._shadingRT = new RenderTarget( w, h, {
71
+ type: HalfFloatType,
72
+ format: RGBAFormat,
73
+ minFilter: NearestFilter,
74
+ magFilter: NearestFilter,
75
+ depthBuffer: false,
76
+ stencilBuffer: false
77
+ } );
78
+
55
79
  this._srcRegion = new Box2( new Vector2( 0, 0 ), new Vector2( 0, 0 ) );
56
80
 
57
81
  // Ping-pong RTs share format with the StorageTexture so copyTextureToTexture works.
@@ -73,11 +97,30 @@ export class NormalDepth extends RenderStage {
73
97
 
74
98
  this._triStorageNode = null;
75
99
  this._bvhStorageNode = null;
100
+ this._matStorageNode = null;
76
101
  this._lastTriAttr = null;
77
102
  this._lastBvhAttr = null;
103
+ this._lastMatAttr = null;
78
104
  this._computeNode = null;
79
105
  this._computeBuilt = false;
80
106
 
107
+ // Normal/bump map array nodes — persistent placeholders, value swapped to
108
+ // the real DataArrayTextures on model load. processNormal/processBump
109
+ // runtime-guard on map indices, so the placeholder is never sampled.
110
+ this._normalMapsTex = texture( this._makePlaceholderArray() );
111
+ this._bumpMapsTex = texture( this._makePlaceholderArray() );
112
+
113
+ }
114
+
115
+ _makePlaceholderArray() {
116
+
117
+ const t = new DataArrayTexture( new Uint8Array( [ 128, 128, 255, 255 ] ), 1, 1, 1 );
118
+ t.minFilter = LinearFilter;
119
+ t.magFilter = LinearFilter;
120
+ t.generateMipmaps = false;
121
+ t.needsUpdate = true;
122
+ return t;
123
+
81
124
  }
82
125
 
83
126
  setupEventListeners() {
@@ -102,10 +145,12 @@ export class NormalDepth extends RenderStage {
102
145
  const pt = this.pathTracer;
103
146
  if ( ! pt ) return false;
104
147
 
148
+ const matAttr = pt.materialData?.materialStorageAttr;
105
149
  const triSwapped = pt.triangleStorageAttr && pt.triangleStorageAttr !== this._lastTriAttr;
106
150
  const bvhSwapped = pt.bvhStorageAttr && pt.bvhStorageAttr !== this._lastBvhAttr;
151
+ const matSwapped = matAttr && matAttr !== this._lastMatAttr;
107
152
 
108
- if ( triSwapped || bvhSwapped ) {
153
+ if ( triSwapped || bvhSwapped || matSwapped ) {
109
154
 
110
155
  // Buffer identity changed → compute's bind group is stale; rebuild.
111
156
  this._computeNode?.dispose?.();
@@ -113,6 +158,7 @@ export class NormalDepth extends RenderStage {
113
158
  this._computeBuilt = false;
114
159
  this._triStorageNode = null;
115
160
  this._bvhStorageNode = null;
161
+ this._matStorageNode = null;
116
162
  this._dirty = true;
117
163
 
118
164
  }
@@ -133,10 +179,22 @@ export class NormalDepth extends RenderStage {
133
179
 
134
180
  }
135
181
 
182
+ if ( matAttr && ! this._matStorageNode ) {
183
+
184
+ this._matStorageNode = storage( matAttr, 'vec4', matAttr.count ).toReadOnly();
185
+
186
+ }
187
+
188
+ // In-place map swaps (model change) — graph closes over the node, only .value changes.
189
+ const md = pt.materialData;
190
+ if ( md?.normalMaps ) this._normalMapsTex.value = md.normalMaps;
191
+ if ( md?.bumpMaps ) this._bumpMapsTex.value = md.bumpMaps;
192
+
136
193
  this._lastTriAttr = pt.triangleStorageAttr || this._lastTriAttr;
137
194
  this._lastBvhAttr = pt.bvhStorageAttr || this._lastBvhAttr;
195
+ this._lastMatAttr = matAttr || this._lastMatAttr;
138
196
 
139
- return !! ( this._triStorageNode && this._bvhStorageNode );
197
+ return !! ( this._triStorageNode && this._bvhStorageNode && this._matStorageNode );
140
198
 
141
199
  }
142
200
 
@@ -144,11 +202,15 @@ export class NormalDepth extends RenderStage {
144
202
 
145
203
  const triStorage = this._triStorageNode;
146
204
  const bvhStorage = this._bvhStorageNode;
205
+ const matStorage = this._matStorageNode;
206
+ const normalMaps = this._normalMapsTex;
207
+ const bumpMaps = this._bumpMapsTex;
147
208
  const camWorld = this.cameraWorldMatrix;
148
209
  const camProjInv = this.cameraProjectionMatrixInverse;
149
210
  const resW = this.resolutionWidth;
150
211
  const resH = this.resolutionHeight;
151
212
  const outputTex = this._outputStorageTex;
213
+ const shadingTex = this._shadingStorageTex;
152
214
 
153
215
  const WG_SIZE = 8;
154
216
 
@@ -195,6 +257,31 @@ export class NormalDepth extends RenderStage {
195
257
  result
196
258
  ).toWriteOnly();
197
259
 
260
+ // Shading normal: perturb the geometric normal by the normal/bump map
261
+ // from the SAME hit (deterministic UV → jitter-free). Miss → geo default.
262
+ const shadingNormal = hit.normal.toVar();
263
+ If( hit.didHit, () => {
264
+
265
+ const material = RayTracingMaterial.wrap(
266
+ getMaterial( hit.materialIndex, matStorage )
267
+ ).toVar();
268
+ const uvCache = UVCache.wrap( computeUVCache( hit.uv, material ) ).toVar();
269
+ const mapped = processNormal( normalMaps, hit.normal, material, uvCache ).toVar();
270
+ shadingNormal.assign( processBump( bumpMaps, mapped, material, uvCache ) );
271
+
272
+ } );
273
+
274
+ const shadingResult = hit.didHit.select(
275
+ vec4( shadingNormal.mul( 0.5 ).add( 0.5 ), depth ),
276
+ vec4( 0.0, 0.0, 0.0, float( 1e6 ) )
277
+ );
278
+
279
+ textureStore(
280
+ shadingTex,
281
+ uvec2( uint( gx ), uint( gy ) ),
282
+ shadingResult
283
+ ).toWriteOnly();
284
+
198
285
  } );
199
286
 
200
287
  } );
@@ -233,6 +320,7 @@ export class NormalDepth extends RenderStage {
233
320
  const currentRT = this._currentIdx === 0 ? this._rtA : this._rtB;
234
321
  context.setTexture( 'pathtracer:normalDepth', currentRT.texture );
235
322
  context.setTexture( 'pathtracer:prevNormalDepth', currentRT.texture );
323
+ context.setTexture( 'pathtracer:shadingNormal', this._shadingRT.texture );
236
324
  return;
237
325
 
238
326
  }
@@ -257,9 +345,10 @@ export class NormalDepth extends RenderStage {
257
345
 
258
346
  this.renderer.compute( this._computeNode );
259
347
 
260
- // Copy only the active region out of the over-allocated StorageTexture.
348
+ // Copy only the active region out of the over-allocated StorageTextures.
261
349
  this._srcRegion.max.set( writeRT.width, writeRT.height );
262
350
  this.renderer.copyTextureToTexture( this._outputStorageTex, writeRT.texture, this._srcRegion );
351
+ this.renderer.copyTextureToTexture( this._shadingStorageTex, this._shadingRT.texture, this._srcRegion );
263
352
 
264
353
  // First dispatch: seed prev from current so ASVGF doesn't see false
265
354
  // disocclusion on frame 1.
@@ -272,6 +361,7 @@ export class NormalDepth extends RenderStage {
272
361
 
273
362
  context.setTexture( 'pathtracer:normalDepth', writeRT.texture );
274
363
  context.setTexture( 'pathtracer:prevNormalDepth', prevRT.texture );
364
+ context.setTexture( 'pathtracer:shadingNormal', this._shadingRT.texture );
275
365
 
276
366
  this._dirty = false;
277
367
 
@@ -294,6 +384,8 @@ export class NormalDepth extends RenderStage {
294
384
  this._rtA.texture.needsUpdate = true;
295
385
  this._rtB.setSize( width, height );
296
386
  this._rtB.texture.needsUpdate = true;
387
+ this._shadingRT.setSize( width, height );
388
+ this._shadingRT.texture.needsUpdate = true;
297
389
  this._hasHistory = false;
298
390
  this.resolutionWidth.value = width;
299
391
  this.resolutionHeight.value = height;
@@ -314,6 +406,8 @@ export class NormalDepth extends RenderStage {
314
406
 
315
407
  this._computeNode?.dispose();
316
408
  this._outputStorageTex?.dispose();
409
+ this._shadingStorageTex?.dispose();
410
+ this._shadingRT?.dispose();
317
411
  this._rtA?.dispose();
318
412
  this._rtB?.dispose();
319
413
 
@@ -1,4 +1,4 @@
1
- import { Fn, wgslFn, float, int, uint, ivec2, uvec2, uniform, If, max,
1
+ import { Fn, wgslFn, float, int, uint, ivec2, uvec2, uniform, If, max, select,
2
2
  textureLoad, textureStore, workgroupArray, workgroupBarrier, localId, workgroupId } from 'three/tsl';
3
3
  import { RenderTarget, TextureNode, StorageTexture } from 'three/webgpu';
4
4
  import { FloatType, RGBAFormat, LinearFilter, Box2, Vector2 } from 'three';
@@ -6,6 +6,10 @@ import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
6
6
  import { luminance } from '../TSL/Common.js';
7
7
  import { MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
8
8
 
9
+ // NaN/±Inf guard: a poisoned luminance would otherwise corrupt the moment EMA forever
10
+ // (mean/meanSq → variance → BilateralFilter sigmaL). NaN (x!=x) → 0, ±Inf → [0,1e7].
11
+ const sanitizeLum = ( x ) => select( x.equal( x ), x, float( 0.0 ) ).clamp( 0.0, 1e7 );
12
+
9
13
  // ── wgslFn helpers ──────────────────────────────────────────
10
14
 
11
15
  /**
@@ -203,7 +207,7 @@ export class Variance extends RenderStage {
203
207
  const gy1 = tileOriginY.add( int( sy1 ) ).clamp( int( 0 ), int( resH ).sub( 1 ) );
204
208
 
205
209
  const sColor1 = textureLoad( colorTex, ivec2( gx1, gy1 ) ).xyz;
206
- sharedLum.element( linearIdx ).assign( luminance( sColor1 ) );
210
+ sharedLum.element( linearIdx ).assign( sanitizeLum( luminance( sColor1 ) ) );
207
211
 
208
212
  // Load #2: threads 0-35 load positions 64-99
209
213
  If( linearIdx.lessThan( uint( EXTRA_LOAD ) ), () => {
@@ -215,7 +219,7 @@ export class Variance extends RenderStage {
215
219
  const gy2 = tileOriginY.add( int( sy2 ) ).clamp( int( 0 ), int( resH ).sub( 1 ) );
216
220
 
217
221
  const sColor2 = textureLoad( colorTex, ivec2( gx2, gy2 ) ).xyz;
218
- sharedLum.element( idx2 ).assign( luminance( sColor2 ) );
222
+ sharedLum.element( idx2 ).assign( sanitizeLum( luminance( sColor2 ) ) );
219
223
 
220
224
  } );
221
225
 
@@ -19,7 +19,7 @@ import { Ray } from './Struct.js';
19
19
  import { RAY_FLAG } from '../Processor/QueueManager.js';
20
20
  import {
21
21
  writeRayOriginMeta, writeRayDirFlags, writeRayThroughputPdf,
22
- writeRayRadiance, writeGBuffer,
22
+ writeRayRadiance, writeGBuffer, writeGBufferSurfaceID,
23
23
  writeMediumStack,
24
24
  } from '../Processor/PackedRayBuffer.js';
25
25
 
@@ -91,6 +91,8 @@ export function buildGenerateKernel( params ) {
91
91
 
92
92
  // default: normal +Z, depth 1 (far), black albedo (background/miss)
93
93
  writeGBuffer( gBufferRW, uint( pixelIndex ), vec3( 0.0, 0.0, 1.0 ), float( 1.0 ), vec3( 0.0 ) );
94
+ // surface-ID lane defaults to invalid (valid=0); Shade overwrites it at the bounce-0 hit.
95
+ writeGBufferSurfaceID( gBufferRW, uint( pixelIndex ), uint( 0 ), uint( 0 ), float( 0.0 ), float( 0.0 ), uint( 0 ) );
94
96
 
95
97
  } );
96
98
 
@@ -48,9 +48,9 @@ import {
48
48
  readMediumStack, writeMediumStack, readMediumSigmaA, writeMediumSigmaA,
49
49
  readPathBounces, readSssSteps, readSSSMedium, writeSSSMedium,
50
50
  readHitDistance, readHitBarycentrics, readHitNormal,
51
- readHitMaterialIndex, readHitTriangleIndex,
51
+ readHitMaterialIndex, readHitTriangleIndex, readHitMeshIndex,
52
52
  writeRayOriginMeta, writeRayDirFlags, writeRayThroughputPdf, writeRayRadiance,
53
- writeGBuffer, readGBuffer, gbDecodeNormalDepth,
53
+ writeGBuffer, writeGBufferSurfaceID, readGBuffer, gbDecodeNormalDepth,
54
54
  readRayRadiance,
55
55
  } from '../Processor/PackedRayBuffer.js';
56
56
 
@@ -374,6 +374,9 @@ export function buildShadeKernel( params ) {
374
374
  If( sampleIndex.equal( int( 0 ) ), () => {
375
375
 
376
376
  writeGBuffer( gBufferRW, pixelIndex, vec3( 0.0, 0.0, 1.0 ), linearDepth, vec3( 0.0 ) );
377
+ // Persist the primary-hit surface ID (Tier-1 A-SVGF correlated re-projection). Hit-only
378
+ // branch (misses Return above), so this marks the pixel valid; bary from the bounce-0 hit.
379
+ writeGBufferSurfaceID( gBufferRW, pixelIndex, hitTriIdx, readHitMeshIndex( hitBufferRO, rayID ), hitUV.x, hitUV.y, uint( 1 ) );
377
380
 
378
381
  } );
379
382
 
@@ -42,6 +42,7 @@ export class CameraManager extends EventDispatcher {
42
42
  this._lastValidFocusDistance = null;
43
43
  this._smoothedFocusDistance = null;
44
44
  this._afPointDirty = false;
45
+ this._afSuspended = false;
45
46
 
46
47
  // Saved state for default camera when switching to model cameras
47
48
  this._defaultCameraState = null;
@@ -273,6 +274,24 @@ export class CameraManager extends EventDispatcher {
273
274
 
274
275
  if ( this.autoFocusMode === 'manual' ) return;
275
276
 
277
+ // Depth-of-field is the only consumer of the auto-focus distance. With DOF
278
+ // off (the default) the per-frame scene raycast is pure waste, so skip it.
279
+ // Re-snap on the frame DOF turns back on so focus is correct immediately
280
+ // rather than racking from a stale smoothed value.
281
+ if ( ! pathTracer?.enableDOF?.value ) {
282
+
283
+ this._afSuspended = true;
284
+ return;
285
+
286
+ }
287
+
288
+ if ( this._afSuspended ) {
289
+
290
+ this._afSuspended = false;
291
+ this._smoothedFocusDistance = null;
292
+
293
+ }
294
+
276
295
  // Lock focus during active tiled final rendering
277
296
  const stage = pathTracer;
278
297
  if ( stage?.isReady