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.
- package/README.md +1 -1
- package/dist/rayzee.es.js +1676 -1636
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +20 -20
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/EngineDefaults.js +16 -5
- package/src/PathTracerApp.js +10 -3
- package/src/Processor/PackedRayBuffer.js +33 -7
- package/src/Stages/ASVGF.js +122 -79
- package/src/Stages/BilateralFilter.js +23 -4
- package/src/Stages/NormalDepth.js +101 -7
- package/src/Stages/Variance.js +7 -3
- package/src/TSL/GenerateKernel.js +3 -1
- package/src/TSL/ShadeKernel.js +5 -2
- package/src/managers/CameraManager.js +19 -0
|
@@ -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
|
-
*
|
|
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
|
|
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
|
|
package/src/Stages/Variance.js
CHANGED
|
@@ -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
|
|
package/src/TSL/ShadeKernel.js
CHANGED
|
@@ -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
|