rayzee 7.2.1 → 7.4.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 +2 -3
- package/dist/rayzee.es.js +1517 -1370
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +52 -38
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +2 -2
- package/src/EngineDefaults.js +15 -7
- package/src/PathTracerApp.js +24 -2
- package/src/Processor/CameraOptimizer.js +0 -5
- package/src/Processor/EmissiveTriangleBuilder.js +4 -3
- package/src/Processor/PackedRayBuffer.js +3 -4
- package/src/Processor/QueueManager.js +1 -0
- package/src/RenderSettings.js +16 -2
- package/src/Stages/PathTracer.js +34 -52
- package/src/Stages/PathTracerStage.js +0 -13
- package/src/TSL/Common.js +62 -0
- package/src/TSL/DebugKernel.js +2 -3
- package/src/TSL/Environment.js +22 -1
- package/src/TSL/FinalWriteKernel.js +28 -29
- package/src/TSL/GenerateKernel.js +11 -16
- package/src/TSL/LightsSampling.js +35 -14
- package/src/TSL/ShadeKernel.js +276 -51
- package/src/TSL/Struct.js +8 -0
- package/src/managers/DenoisingManager.js +7 -0
- package/src/managers/UniformManager.js +7 -2
- package/src/managers/VideoRenderManager.js +0 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rayzee",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.4.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",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"prepublishOnly": "npm run build"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"three": ">=0.
|
|
37
|
+
"three": ">=0.185.0"
|
|
38
38
|
},
|
|
39
39
|
"optionalDependencies": {
|
|
40
40
|
"oidn-web": ">=0.3.0",
|
package/src/EngineDefaults.js
CHANGED
|
@@ -19,10 +19,23 @@ export const ENGINE_DEFAULTS = {
|
|
|
19
19
|
useImportanceSampledEnvironment: true,
|
|
20
20
|
environmentIntensity: 1,
|
|
21
21
|
backgroundIntensity: 1,
|
|
22
|
+
// Solid backdrop color shown on camera-ray misses in 'color' background mode
|
|
23
|
+
// (showBackground=false, transparentBackground=false). Black = legacy hidden-backdrop look.
|
|
24
|
+
backgroundColor: '#000000',
|
|
25
|
+
// Backdrop blur (env background only). 0 = sharp/off (no cost). Cone-jitter blur of the
|
|
26
|
+
// primary-ray env lookup; lighting/reflections stay sharp. Samples = taps/frame (noise vs cost).
|
|
27
|
+
backgroundBlurriness: 0,
|
|
28
|
+
backgroundBlurSamples: 8,
|
|
22
29
|
environmentRotation: 270.0,
|
|
23
30
|
groundProjectionEnabled: false,
|
|
24
31
|
groundProjectionRadius: 100,
|
|
25
32
|
groundProjectionHeight: 15,
|
|
33
|
+
// World Y of the projected ground plane; auto-seeded to the scene floor (min-Y) on model
|
|
34
|
+
// load so models that aren't authored at y=0 sit ON the ground instead of sinking into it.
|
|
35
|
+
groundProjectionLevel: 0,
|
|
36
|
+
// Analytic ground-plane shadow catcher (primary-ray holdout; no geometry)
|
|
37
|
+
enableGroundCatcher: false,
|
|
38
|
+
groundCatcherHeight: 0,
|
|
26
39
|
globalIlluminationIntensity: 1,
|
|
27
40
|
|
|
28
41
|
// Environment Mode System
|
|
@@ -58,16 +71,11 @@ export const ENGINE_DEFAULTS = {
|
|
|
58
71
|
afScreenPoint: { x: 0.5, y: 0.5 },
|
|
59
72
|
afSmoothingFactor: 0.15,
|
|
60
73
|
|
|
61
|
-
// Multi-sample pool: S=samplesPerPixel rays/pixel/frame, FinalWrite averages them; interactive-only (renderMode 0, ≤ cap), else S=1.
|
|
62
|
-
// Pixel cap (768²) bounds pool memory; covers the 512² default, excludes ≥768².
|
|
63
|
-
wavefrontMultiSampleMaxPixels: 589824,
|
|
64
|
-
|
|
65
74
|
enablePathTracer: true,
|
|
66
75
|
enableAccumulation: true,
|
|
67
76
|
pauseRendering: false,
|
|
68
77
|
maxSamples: 60,
|
|
69
78
|
bounces: 3,
|
|
70
|
-
samplesPerPixel: 1,
|
|
71
79
|
transmissiveBounces: 5,
|
|
72
80
|
maxSubsurfaceSteps: 8, // interactive default: low cap (bounded random-walk SSS)
|
|
73
81
|
samplingTechnique: 3,
|
|
@@ -481,7 +489,7 @@ export const DEFAULT_TEXTURE_MATRIX = [ 0, 0, 1, 1, 0, 0, 0, 1 ];
|
|
|
481
489
|
// 'interactive' — low-sample, bounded bounces, no offline denoising, controls enabled.
|
|
482
490
|
// 'production' — high-sample, deep bounces, OIDN enabled, controls disabled.
|
|
483
491
|
export const PRODUCTION_RENDER_CONFIG = {
|
|
484
|
-
maxSamples: 30, bounces: 20, transmissiveBounces: 8, maxSubsurfaceSteps: 64,
|
|
492
|
+
maxSamples: 30, bounces: 20, transmissiveBounces: 8, maxSubsurfaceSteps: 64,
|
|
485
493
|
renderMode: 1, enableAlphaShadows: true,
|
|
486
494
|
enableOIDN: true, oidnQuality: 'balance',
|
|
487
495
|
interactionModeEnabled: false,
|
|
@@ -489,7 +497,7 @@ export const PRODUCTION_RENDER_CONFIG = {
|
|
|
489
497
|
|
|
490
498
|
export const INTERACTIVE_RENDER_CONFIG = {
|
|
491
499
|
maxSamples: ENGINE_DEFAULTS.maxSamples, bounces: ENGINE_DEFAULTS.bounces,
|
|
492
|
-
|
|
500
|
+
renderMode: ENGINE_DEFAULTS.renderMode, enableAlphaShadows: ENGINE_DEFAULTS.enableAlphaShadows,
|
|
493
501
|
transmissiveBounces: ENGINE_DEFAULTS.transmissiveBounces,
|
|
494
502
|
maxSubsurfaceSteps: ENGINE_DEFAULTS.maxSubsurfaceSteps,
|
|
495
503
|
enableOIDN: false, oidnQuality: 'fast',
|
package/src/PathTracerApp.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { WebGPURenderer, RectAreaLightNode, SRGBColorSpace } from 'three/webgpu';
|
|
2
2
|
import { texture as _tslTexture, cubeTexture as _tslCubeTexture } from 'three/tsl';
|
|
3
3
|
import {
|
|
4
|
-
ACESFilmicToneMapping, Scene, EventDispatcher
|
|
4
|
+
ACESFilmicToneMapping, Scene, EventDispatcher, Box3
|
|
5
5
|
} from 'three';
|
|
6
6
|
import { RectAreaLightTexturesLib } from 'three/addons/lights/RectAreaLightTexturesLib.js';
|
|
7
7
|
import { SceneHelpers } from './SceneHelpers.js';
|
|
@@ -777,6 +777,12 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
777
777
|
|
|
778
778
|
}
|
|
779
779
|
|
|
780
|
+
// Seed the ground-projection plane AND the shadow-catcher plane to the scene floor so models
|
|
781
|
+
// that aren't authored at y=0 sit on the ground (not sunk) — auto-updates on every model change.
|
|
782
|
+
const sceneMinY = this.getSceneMinY();
|
|
783
|
+
this.settings.set( 'groundProjectionLevel', sceneMinY, { reset: false } );
|
|
784
|
+
this.settings.set( 'groundCatcherHeight', sceneMinY, { reset: false } );
|
|
785
|
+
|
|
780
786
|
// Apply all settings to stages in one shot
|
|
781
787
|
timer.start( 'Apply settings' );
|
|
782
788
|
this.settings.applyAll();
|
|
@@ -953,7 +959,6 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
953
959
|
this.settings.setMany( {
|
|
954
960
|
maxSamples: config.maxSamples,
|
|
955
961
|
maxBounces: config.bounces,
|
|
956
|
-
samplesPerPixel: config.samplesPerPixel,
|
|
957
962
|
transmissiveBounces: config.transmissiveBounces,
|
|
958
963
|
maxSubsurfaceSteps: config.maxSubsurfaceSteps,
|
|
959
964
|
}, { silent: true } );
|
|
@@ -972,6 +977,10 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
972
977
|
|
|
973
978
|
}
|
|
974
979
|
|
|
980
|
+
// OIDN toggled directly above (bypassing setOIDNEnabled) — re-sync so the wavefront produces the
|
|
981
|
+
// aux MRT when OIDN is on and skips it otherwise. Runs before the reset below so kernels rebuild once.
|
|
982
|
+
this.denoisingManager?._syncGBufferStages?.();
|
|
983
|
+
|
|
975
984
|
this.denoisingManager?.upscaler?.abort();
|
|
976
985
|
|
|
977
986
|
if ( options.canvasWidth && options.canvasHeight ) {
|
|
@@ -1178,6 +1187,19 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1178
1187
|
* @param {string} property
|
|
1179
1188
|
* @param {*} value
|
|
1180
1189
|
*/
|
|
1190
|
+
/**
|
|
1191
|
+
* World-space minimum Y of the loaded scene (the floor). Used to seed the
|
|
1192
|
+
* analytic ground-plane shadow catcher height. Returns 0 if no scene is loaded.
|
|
1193
|
+
* @returns {number}
|
|
1194
|
+
*/
|
|
1195
|
+
getSceneMinY() {
|
|
1196
|
+
|
|
1197
|
+
if ( ! this.meshScene ) return 0;
|
|
1198
|
+
const box = new Box3().setFromObject( this.meshScene );
|
|
1199
|
+
return Number.isFinite( box.min.y ) ? box.min.y : 0;
|
|
1200
|
+
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1181
1203
|
setMaterialProperty( materialIndex, property, value ) {
|
|
1182
1204
|
|
|
1183
1205
|
this.stages.pathTracer?.materialData.updateMaterialProperty( materialIndex, property, value );
|
|
@@ -24,7 +24,6 @@ export class CameraOptimizer {
|
|
|
24
24
|
// Enhanced interaction mode settings for reduced quality during interaction
|
|
25
25
|
this.interactionQualitySettings = {
|
|
26
26
|
maxBounceCount: 1,
|
|
27
|
-
numRaysPerPixel: 1,
|
|
28
27
|
useEnvMapIS: false,
|
|
29
28
|
// pixelRatio: 0.25,
|
|
30
29
|
enableAccumulation: false,
|
|
@@ -304,28 +303,24 @@ export class CameraOptimizer {
|
|
|
304
303
|
const presets = {
|
|
305
304
|
'ultra-low': {
|
|
306
305
|
maxBounceCount: 1,
|
|
307
|
-
numRaysPerPixel: 1,
|
|
308
306
|
useEnvMapIS: false,
|
|
309
307
|
pixelRatio: 0.125,
|
|
310
308
|
enableAccumulation: false
|
|
311
309
|
},
|
|
312
310
|
'low': {
|
|
313
311
|
maxBounceCount: 1,
|
|
314
|
-
numRaysPerPixel: 1,
|
|
315
312
|
useEnvMapIS: false,
|
|
316
313
|
pixelRatio: 0.25,
|
|
317
314
|
enableAccumulation: false
|
|
318
315
|
},
|
|
319
316
|
'medium': {
|
|
320
317
|
maxBounceCount: 2,
|
|
321
|
-
numRaysPerPixel: 1,
|
|
322
318
|
useEnvMapIS: true,
|
|
323
319
|
pixelRatio: 0.5,
|
|
324
320
|
enableAccumulation: false
|
|
325
321
|
},
|
|
326
322
|
'high': {
|
|
327
323
|
maxBounceCount: 3,
|
|
328
|
-
numRaysPerPixel: 1,
|
|
329
324
|
useEnvMapIS: true,
|
|
330
325
|
pixelRatio: 0.75,
|
|
331
326
|
enableAccumulation: true
|
|
@@ -443,8 +443,9 @@ export class EmissiveTriangleBuilder {
|
|
|
443
443
|
// If not emissive before and not now, nothing to do
|
|
444
444
|
if ( ! isNowEmissive ) return false;
|
|
445
445
|
|
|
446
|
-
// Fast path: just update power + CDF for affected entries
|
|
447
|
-
|
|
446
|
+
// Fast path: just update power + CDF for affected entries.
|
|
447
|
+
// Rec.709 luma — must match the build path + shader MIS weighting.
|
|
448
|
+
const luma = 0.2126 * emissive.r + 0.7152 * emissive.g + 0.0722 * emissive.b;
|
|
448
449
|
|
|
449
450
|
this.totalEmissivePower = 0;
|
|
450
451
|
for ( let i = 0; i < this.emissiveCount; i ++ ) {
|
|
@@ -452,7 +453,7 @@ export class EmissiveTriangleBuilder {
|
|
|
452
453
|
const tri = this.emissiveTriangles[ i ];
|
|
453
454
|
if ( tri.materialIndex === materialIndex ) {
|
|
454
455
|
|
|
455
|
-
tri.power =
|
|
456
|
+
tri.power = luma * emissiveIntensity * tri.area;
|
|
456
457
|
tri.emissive = { r: emissive.r, g: emissive.g, b: emissive.b };
|
|
457
458
|
tri.emissiveIntensity = emissiveIntensity;
|
|
458
459
|
this.emissivePowerArray[ i ] = tri.power;
|
|
@@ -20,7 +20,7 @@ export const HIT_STRIDE = 2;
|
|
|
20
20
|
export const GBUFFER_STRIDE = 2;
|
|
21
21
|
|
|
22
22
|
export const RAY = {
|
|
23
|
-
ORIGIN_META: 0, // vec4(origin.xyz, uintBitsToFloat(perRayBounces | sssSteps<<8)); pixelIndex
|
|
23
|
+
ORIGIN_META: 0, // vec4(origin.xyz, uintBitsToFloat(perRayBounces | sssSteps<<8)); pixelIndex == rayID
|
|
24
24
|
DIR_FLAGS: 1, // vec4(direction.xyz, uintBitsToFloat(bounceFlags))
|
|
25
25
|
THROUGHPUT_PDF: 2, // vec4(throughput.xyz, pdf)
|
|
26
26
|
RADIANCE_ALPHA: 3, // vec4(radiance.xyz, alpha)
|
|
@@ -195,8 +195,8 @@ export const gbDecodeNormalDepth = ( packed ) => {
|
|
|
195
195
|
export const gbDecodeAlbedo = ( packed ) =>
|
|
196
196
|
vec3( unpackUnorm2x16( packed.z ), unpackUnorm2x16( packed.w ).x );
|
|
197
197
|
|
|
198
|
-
// .w packs per-ray bounce state: perRayBounces (bits 0-7) | sssSteps (bits 8-15). pixelIndex
|
|
199
|
-
//
|
|
198
|
+
// .w packs per-ray bounce state: perRayBounces (bits 0-7) | sssSteps (bits 8-15). pixelIndex is
|
|
199
|
+
// NOT stored — it equals rayID (one ray per pixel).
|
|
200
200
|
export const writeRayOriginMeta = ( buf, id, origin, bounces, sssSteps ) =>
|
|
201
201
|
buf.element( soa( id, RAY.ORIGIN_META ) )
|
|
202
202
|
.assign( vec4( origin, uintBitsToFloat(
|
|
@@ -273,7 +273,6 @@ export const writeMediumSigmaA = ( buf, id, sigmaA ) =>
|
|
|
273
273
|
// Per-ray bounce state packed into ORIGIN_META.w (written by writeRayOriginMeta alongside the origin):
|
|
274
274
|
// perRayBounces = bits 0-7 (camera-bounce depth; the loop index can't track it once free bounces decouple it)
|
|
275
275
|
// sssSteps = bits 8-15 (SSS random-walk step counter)
|
|
276
|
-
// sampleIndex (the multi-sample sub-sample 0..S-1) is derived in-kernel from rayID, not stored.
|
|
277
276
|
export const readPathBounces = ( buf, id ) =>
|
|
278
277
|
int( floatBitsToUint( buf.element( soa( id, RAY.ORIGIN_META ) ).w ).bitAnd( 0xFF ) );
|
|
279
278
|
export const readSssSteps = ( buf, id ) =>
|
|
@@ -25,6 +25,7 @@ export const RAY_FLAG = {
|
|
|
25
25
|
// bits 16-31: spare per-ray state carried across bounces
|
|
26
26
|
HAS_HIT_OPAQUE: 1 << 16, // bit 16: ray chain has hit non-transmissive geometry (transparent-bg alpha; megakernel hasHitOpaqueSurface)
|
|
27
27
|
AUX_LOCKED: 1 << 17, // bit 17: OIDN aux (normal/albedo) locked onto first non-specular hit (megakernel auxLocked)
|
|
28
|
+
REDIRECTED: 1 << 18, // bit 18: ray has been redirected (refraction/reflection/SSS/opaque scatter) since the camera, so env it reaches is transported light (sharp), NOT the direct backdrop. NOT set by pure alpha/transparent passthrough (direction unchanged) → env through cutout holes is still the backdrop (blur/intensity/show/color/ground-projection). Set via bitOr only (positive mask) so it never disturbs ACTIVE/bounce bits.
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
export class QueueManager {
|
package/src/RenderSettings.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EventDispatcher } from 'three';
|
|
1
|
+
import { EventDispatcher, Color } from 'three';
|
|
2
2
|
import { ENGINE_DEFAULTS } from './EngineDefaults.js';
|
|
3
3
|
import { EngineEvents } from './EngineEvents.js';
|
|
4
4
|
|
|
@@ -16,16 +16,20 @@ const SETTING_ROUTES = {
|
|
|
16
16
|
// ── Simple PathTracer uniforms ──────────────────────────
|
|
17
17
|
|
|
18
18
|
maxBounces: { uniform: 'maxBounces', reset: true },
|
|
19
|
-
samplesPerPixel: { uniform: 'samplesPerPixel', reset: true },
|
|
20
19
|
transmissiveBounces: { uniform: 'transmissiveBounces', reset: true },
|
|
21
20
|
maxSubsurfaceSteps: { uniform: 'maxSubsurfaceSteps', reset: true },
|
|
22
21
|
environmentIntensity: { uniform: 'environmentIntensity', reset: true },
|
|
23
22
|
backgroundIntensity: { uniform: 'backgroundIntensity', reset: true },
|
|
23
|
+
backgroundBlurriness: { uniform: 'backgroundBlurriness', reset: true },
|
|
24
|
+
backgroundBlurSamples: { uniform: 'backgroundBlurSamples', reset: true },
|
|
24
25
|
showBackground: { uniform: 'showBackground', reset: true },
|
|
25
26
|
enableEnvironment: { uniform: 'enableEnvironment', reset: true },
|
|
26
27
|
groundProjectionEnabled: { uniform: 'groundProjectionEnabled', reset: true },
|
|
27
28
|
groundProjectionRadius: { uniform: 'groundProjectionRadius', reset: true },
|
|
28
29
|
groundProjectionHeight: { uniform: 'groundProjectionHeight', reset: true },
|
|
30
|
+
groundProjectionLevel: { uniform: 'groundProjectionLevel', reset: true },
|
|
31
|
+
enableGroundCatcher: { uniform: 'enableGroundCatcher', reset: true },
|
|
32
|
+
groundCatcherHeight: { uniform: 'groundCatcherHeight', reset: true },
|
|
29
33
|
globalIlluminationIntensity: { uniform: 'globalIlluminationIntensity', reset: true },
|
|
30
34
|
enableDOF: { uniform: 'enableDOF', reset: true },
|
|
31
35
|
focusDistance: { uniform: 'focusDistance', reset: false },
|
|
@@ -46,6 +50,7 @@ const SETTING_ROUTES = {
|
|
|
46
50
|
interactionModeEnabled: { handler: 'handleInteractionModeEnabled', reset: false },
|
|
47
51
|
maxSamples: { handler: 'handleMaxSamples', reset: false },
|
|
48
52
|
transparentBackground: { handler: 'handleTransparentBackground' },
|
|
53
|
+
backgroundColor: { handler: 'handleBackgroundColor', reset: true },
|
|
49
54
|
exposure: { handler: 'handleExposure' },
|
|
50
55
|
saturation: { handler: 'handleSaturation' },
|
|
51
56
|
renderLimitMode: { handler: 'handleRenderLimitMode' },
|
|
@@ -133,6 +138,15 @@ export class RenderSettings extends EventDispatcher {
|
|
|
133
138
|
|
|
134
139
|
},
|
|
135
140
|
|
|
141
|
+
handleBackgroundColor: ( value ) => {
|
|
142
|
+
|
|
143
|
+
// Accept a hex string ('#rrggbb') or a Color; THREE.Color converts sRGB → linear working
|
|
144
|
+
// space, which is what the shader adds to radiance.
|
|
145
|
+
const c = value?.isColor ? value : new Color( value );
|
|
146
|
+
stages.pathTracer?.setUniform( 'backgroundColor', c );
|
|
147
|
+
|
|
148
|
+
},
|
|
149
|
+
|
|
136
150
|
handleExposure: ( value ) => {
|
|
137
151
|
|
|
138
152
|
// Three.js applies toneMappingExposure inside the tone-mapping branch,
|
package/src/Stages/PathTracer.js
CHANGED
|
@@ -17,7 +17,6 @@ import { buildShadeKernel, SHADE_WG_SIZE } from '../TSL/ShadeKernel.js';
|
|
|
17
17
|
import { buildCompactKernel, buildCompactSubgroupKernel, COMPACT_WG_SIZE } from '../TSL/CompactKernel.js';
|
|
18
18
|
import { buildFinalWriteKernel, FINALWRITE_WG_SIZE } from '../TSL/FinalWriteKernel.js';
|
|
19
19
|
import { buildDebugKernel, DEBUG_WG_SIZE } from '../TSL/DebugKernel.js';
|
|
20
|
-
import { ENGINE_DEFAULTS } from '../EngineDefaults.js';
|
|
21
20
|
import {
|
|
22
21
|
Fn, uint, atomicStore, atomicLoad, instanceIndex, If, Return,
|
|
23
22
|
} from 'three/tsl';
|
|
@@ -35,16 +34,18 @@ export class PathTracer extends PathTracerStage {
|
|
|
35
34
|
this._gBufferAttr = null; // per-pixel first-hit MRT (ND + albedo); see _buildWavefrontKernels
|
|
36
35
|
this._wavefrontReady = false;
|
|
37
36
|
|
|
37
|
+
// Aux MRT (normalDepth + albedo) feeds only the denoiser/OIDN. When no denoiser is active the
|
|
38
|
+
// wavefront skips those writes (Generate/Shade G-buffer + FinalWrite stores). Gated by a live
|
|
39
|
+
// uniform — NOT baked — so DenoisingManager can toggle it without a (UI-freezing) kernel rebuild.
|
|
40
|
+
this._auxGBufferEnabled = false;
|
|
41
|
+
this._auxGBufferUniform = uniform( 0, 'uint' );
|
|
42
|
+
|
|
38
43
|
// CPU sizes per-bounce kernels from last frame's survivor curve; kernels bound on ENTERING_COUNT so over-sizing is safe. (indirect dispatch not viable — three.js doesn't sync compute-written indirect buffers across submissions)
|
|
39
44
|
this._useDynamicDispatch = true;
|
|
40
45
|
|
|
41
46
|
// Flag-gated off: perf-neutral vs atomic-append and adds a 'subgroups' feature dependency.
|
|
42
47
|
this._useSubgroupCompact = false;
|
|
43
48
|
|
|
44
|
-
// Multi-sample pool: S=samplesPerPixel primary rays/pixel/frame (interactive-only, ≤ the pixel cap; else S=1). FinalWrite averages the S slots. Baked into kernels; _ensureSamplesPerPass() rebuilds on change.
|
|
45
|
-
this._multiSampleMaxPixels = ENGINE_DEFAULTS.wavefrontMultiSampleMaxPixels ?? 589824; // 768²
|
|
46
|
-
this._samplesPerPass = 1;
|
|
47
|
-
|
|
48
49
|
this._lastBounceCounts = null;
|
|
49
50
|
// maxBounces the curve was measured at; the curve is ignored once this no longer matches (-1 = none).
|
|
50
51
|
this._lastBounceCountsBudget = - 1;
|
|
@@ -164,19 +165,15 @@ export class PathTracer extends PathTracerStage {
|
|
|
164
165
|
const renderMode = this.renderMode.value;
|
|
165
166
|
|
|
166
167
|
let originalMaxBounces = null;
|
|
167
|
-
let originalSamplesPerPixel = null;
|
|
168
168
|
|
|
169
169
|
if ( renderMode === 1 && frameValue === 0 ) {
|
|
170
170
|
|
|
171
171
|
originalMaxBounces = this.maxBounces.value;
|
|
172
|
-
originalSamplesPerPixel = this.samplesPerPixel.value;
|
|
173
172
|
this.maxBounces.value = 1;
|
|
174
|
-
this.samplesPerPixel.value = 1;
|
|
175
173
|
|
|
176
174
|
}
|
|
177
175
|
|
|
178
176
|
this._handleResize();
|
|
179
|
-
this._ensureSamplesPerPass();
|
|
180
177
|
this.manageASVGFForRenderMode( renderMode );
|
|
181
178
|
|
|
182
179
|
// Full-frame render is always a complete cycle (PER_CYCLE stages gate on this).
|
|
@@ -218,7 +215,6 @@ export class PathTracer extends PathTracerStage {
|
|
|
218
215
|
if ( ! this.cameraOptimizer?.isInInteractionMode() ) this.frameCount ++;
|
|
219
216
|
|
|
220
217
|
if ( originalMaxBounces !== null ) this.maxBounces.value = originalMaxBounces;
|
|
221
|
-
if ( originalSamplesPerPixel !== null ) this.samplesPerPixel.value = originalSamplesPerPixel;
|
|
222
218
|
|
|
223
219
|
this.performanceMonitor?.end();
|
|
224
220
|
return;
|
|
@@ -336,7 +332,6 @@ export class PathTracer extends PathTracerStage {
|
|
|
336
332
|
if ( ! this.cameraOptimizer?.isInInteractionMode() ) this.frameCount ++;
|
|
337
333
|
|
|
338
334
|
if ( originalMaxBounces !== null ) this.maxBounces.value = originalMaxBounces;
|
|
339
|
-
if ( originalSamplesPerPixel !== null ) this.samplesPerPixel.value = originalSamplesPerPixel;
|
|
340
335
|
|
|
341
336
|
this.performanceMonitor?.end();
|
|
342
337
|
|
|
@@ -354,27 +349,16 @@ export class PathTracer extends PathTracerStage {
|
|
|
354
349
|
|
|
355
350
|
}
|
|
356
351
|
|
|
357
|
-
//
|
|
358
|
-
|
|
352
|
+
// Aux MRT (normalDepth/albedo) is needed only by the denoiser/OIDN; DenoisingManager calls this to
|
|
353
|
+
// turn the wavefront's aux writes on/off. It's a live uniform, so toggling is just a value flip +
|
|
354
|
+
// accumulation reset — no kernel rebuild, no UI freeze.
|
|
355
|
+
setAuxGBufferEnabled( enabled ) {
|
|
359
356
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
// S is baked at build but samplesPerPixel/mode can change without a resize; rebuild when the implied S differs.
|
|
367
|
-
_ensureSamplesPerPass() {
|
|
368
|
-
|
|
369
|
-
if ( ! this._wavefrontReady ) return;
|
|
370
|
-
const w = this.storageTextures.renderWidth;
|
|
371
|
-
const h = this.storageTextures.renderHeight;
|
|
372
|
-
if ( this._resolveSamplesPerPass( w, h ) !== this._samplesPerPass ) {
|
|
373
|
-
|
|
374
|
-
this._wavefrontReady = false;
|
|
375
|
-
this._buildWavefrontKernels();
|
|
376
|
-
|
|
377
|
-
}
|
|
357
|
+
enabled = !! enabled;
|
|
358
|
+
if ( this._auxGBufferEnabled === enabled ) return;
|
|
359
|
+
this._auxGBufferEnabled = enabled;
|
|
360
|
+
this._auxGBufferUniform.value = enabled ? 1 : 0;
|
|
361
|
+
this.reset();
|
|
378
362
|
|
|
379
363
|
}
|
|
380
364
|
|
|
@@ -464,12 +448,10 @@ export class PathTracer extends PathTracerStage {
|
|
|
464
448
|
this._readbackFrameCounter = 0;
|
|
465
449
|
this._readbackGeneration ++;
|
|
466
450
|
|
|
467
|
-
// Recompile only when buffers reallocate (capacity grows)
|
|
468
|
-
const
|
|
469
|
-
const neededCap = PackedRayBuffer.requiredCapacity( newW * newH * newS );
|
|
451
|
+
// Recompile only when buffers reallocate (capacity grows); otherwise resize uniforms in place.
|
|
452
|
+
const neededCap = PackedRayBuffer.requiredCapacity( newW * newH );
|
|
470
453
|
const mustRebuild = ! this._packedBuffers
|
|
471
|
-
|| neededCap > this._packedBuffers.capacity
|
|
472
|
-
|| newS !== this._samplesPerPass;
|
|
454
|
+
|| neededCap > this._packedBuffers.capacity;
|
|
473
455
|
|
|
474
456
|
if ( mustRebuild ) {
|
|
475
457
|
|
|
@@ -485,10 +467,10 @@ export class PathTracer extends PathTracerStage {
|
|
|
485
467
|
|
|
486
468
|
}
|
|
487
469
|
|
|
488
|
-
// Same-capacity
|
|
470
|
+
// Same-capacity resize: update render-size uniforms + early-exit threshold, no recompile.
|
|
489
471
|
_resizeWavefrontInPlace( w, h ) {
|
|
490
472
|
|
|
491
|
-
const maxRays = w * h
|
|
473
|
+
const maxRays = w * h;
|
|
492
474
|
this._wfRenderWidth.value = w;
|
|
493
475
|
this._wfRenderHeight.value = h;
|
|
494
476
|
this._wfMaxRayCount.value = maxRays;
|
|
@@ -509,11 +491,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
509
491
|
|
|
510
492
|
const w = this.storageTextures.renderWidth;
|
|
511
493
|
const h = this.storageTextures.renderHeight;
|
|
512
|
-
|
|
513
|
-
this._samplesPerPass = this._resolveSamplesPerPass( w, h );
|
|
514
|
-
const S = this._samplesPerPass | 0;
|
|
515
|
-
const maxRaysPerSample = w * h;
|
|
516
|
-
const maxRays = maxRaysPerSample * S;
|
|
494
|
+
const maxRays = w * h;
|
|
517
495
|
|
|
518
496
|
if ( this._bounceEarlyExitThreshold !== - 1 ) {
|
|
519
497
|
|
|
@@ -534,9 +512,9 @@ export class PathTracer extends PathTracerStage {
|
|
|
534
512
|
// Per-pixel G-buffer (first-hit MRT: ND + albedo), 1 uvec4/pixel — half-precision packed (pack2x16).
|
|
535
513
|
// uint (not f32) buffer: packed lanes can hit the NaN exponent range (e.g. snorm 1.0 → 0x7FFF), which a
|
|
536
514
|
// GPU may canonicalize through f32 storage; u32 stores the bits verbatim. Separate from RAY — it's
|
|
537
|
-
// per-pixel
|
|
515
|
+
// per-pixel, written by Generate/Shade bounce-0 and read only by FinalWrite.
|
|
538
516
|
// 1.25× margin (same as the per-ray buffers) so it survives the in-place-resize range.
|
|
539
|
-
const gBufferVec4s = PackedRayBuffer.requiredCapacity(
|
|
517
|
+
const gBufferVec4s = PackedRayBuffer.requiredCapacity( maxRays ) * GBUFFER_STRIDE;
|
|
540
518
|
this._gBufferAttr = new StorageInstancedBufferAttribute( new Uint32Array( gBufferVec4s * 4 ), 4 );
|
|
541
519
|
const gBufferRW = storage( this._gBufferAttr, 'uvec4' );
|
|
542
520
|
const gBufferRO = storage( this._gBufferAttr, 'uvec4' ).toReadOnly();
|
|
@@ -639,14 +617,13 @@ export class PathTracer extends PathTracerStage {
|
|
|
639
617
|
anamorphicRatio: this.anamorphicRatio,
|
|
640
618
|
renderWidth: this._wfRenderWidth,
|
|
641
619
|
renderHeight: this._wfRenderHeight,
|
|
642
|
-
samplesPerPass: S,
|
|
643
620
|
transmissiveBounces: this.transmissiveBounces,
|
|
644
621
|
transparentBackground: this.transparentBackground,
|
|
622
|
+
auxGBufferEnabled: this._auxGBufferUniform,
|
|
645
623
|
} );
|
|
646
624
|
this._kernelManager.register( 'generate',
|
|
647
625
|
genFn().compute(
|
|
648
|
-
|
|
649
|
-
[ Math.ceil( w / GENERATE_WG_SIZE ), Math.ceil( ( h * S ) / GENERATE_WG_SIZE ), 1 ],
|
|
626
|
+
[ Math.ceil( w / GENERATE_WG_SIZE ), Math.ceil( h / GENERATE_WG_SIZE ), 1 ],
|
|
650
627
|
[ GENERATE_WG_SIZE, GENERATE_WG_SIZE, 1 ]
|
|
651
628
|
)
|
|
652
629
|
);
|
|
@@ -726,6 +703,9 @@ export class PathTracer extends PathTracerStage {
|
|
|
726
703
|
groundProjectionEnabled: this.groundProjectionEnabled,
|
|
727
704
|
groundProjectionRadius: this.groundProjectionRadius,
|
|
728
705
|
groundProjectionHeight: this.groundProjectionHeight,
|
|
706
|
+
groundProjectionLevel: this.groundProjectionLevel,
|
|
707
|
+
enableGroundCatcher: this.enableGroundCatcher,
|
|
708
|
+
groundCatcherHeight: this.groundCatcherHeight,
|
|
729
709
|
envTotalSum: this.envTotalSum,
|
|
730
710
|
envResolution: this.envResolution,
|
|
731
711
|
directionalLightsBuffer: this.directionalLightsBufferNode,
|
|
@@ -740,6 +720,9 @@ export class PathTracer extends PathTracerStage {
|
|
|
740
720
|
maxSubsurfaceSteps: this.maxSubsurfaceSteps,
|
|
741
721
|
transparentBackground: this.transparentBackground,
|
|
742
722
|
backgroundIntensity: this.backgroundIntensity,
|
|
723
|
+
backgroundColor: this.backgroundColor,
|
|
724
|
+
backgroundBlurriness: this.backgroundBlurriness,
|
|
725
|
+
backgroundBlurSamples: this.backgroundBlurSamples,
|
|
743
726
|
showBackground: this.showBackground,
|
|
744
727
|
globalIlluminationIntensity: this.globalIlluminationIntensity,
|
|
745
728
|
cameraProjectionMatrix: this.cameraProjectionMatrix,
|
|
@@ -757,6 +740,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
757
740
|
reverseMapVec4Offset: this.reverseMapVec4Offset,
|
|
758
741
|
currentBounce: this._wfCurrentBounce,
|
|
759
742
|
maxRayCount: this._wfMaxRayCount,
|
|
743
|
+
auxGBufferEnabled: this._auxGBufferUniform,
|
|
760
744
|
} );
|
|
761
745
|
this._kernelManager.register( 'shade',
|
|
762
746
|
shadeFn().compute(
|
|
@@ -830,8 +814,8 @@ export class PathTracer extends PathTracerStage {
|
|
|
830
814
|
prevAlbedoTexture: prevAlbedo,
|
|
831
815
|
renderWidth: this._wfRenderWidth,
|
|
832
816
|
renderHeight: this._wfRenderHeight,
|
|
833
|
-
samplesPerPass: S,
|
|
834
817
|
visMode: this.visMode,
|
|
818
|
+
auxGBufferEnabled: this._auxGBufferUniform,
|
|
835
819
|
} );
|
|
836
820
|
this._kernelManager.register( 'finalWrite',
|
|
837
821
|
// Per-pixel (w×h) — kernel averages the S sample-slots internally.
|
|
@@ -870,7 +854,6 @@ export class PathTracer extends PathTracerStage {
|
|
|
870
854
|
enableEnvironmentLight: this.enableEnvironment,
|
|
871
855
|
visMode: this.visMode,
|
|
872
856
|
debugVisScale: this.debugVisScale,
|
|
873
|
-
samplesPerPass: this._samplesPerPass,
|
|
874
857
|
albedoMaps: freshAlbedoMaps,
|
|
875
858
|
normalMaps: freshNormalMaps,
|
|
876
859
|
bumpMaps: freshBumpMaps,
|
|
@@ -895,11 +878,10 @@ export class PathTracer extends PathTracerStage {
|
|
|
895
878
|
|
|
896
879
|
const w = this._wfRenderWidth.value;
|
|
897
880
|
const h = this._wfRenderHeight.value;
|
|
898
|
-
const S = this._samplesPerPass | 0;
|
|
899
881
|
|
|
900
882
|
this._kernelManager.setDispatchCount( 'generate', [
|
|
901
883
|
Math.ceil( w / GENERATE_WG_SIZE ),
|
|
902
|
-
Math.ceil(
|
|
884
|
+
Math.ceil( h / GENERATE_WG_SIZE ), 1
|
|
903
885
|
] );
|
|
904
886
|
this._kernelManager.setDispatchCount( 'finalWrite', [
|
|
905
887
|
Math.ceil( w / FINALWRITE_WG_SIZE ),
|
|
@@ -279,18 +279,6 @@ export class PathTracerStage extends RenderStage {
|
|
|
279
279
|
|
|
280
280
|
}
|
|
281
281
|
},
|
|
282
|
-
numRaysPerPixel: {
|
|
283
|
-
get value() {
|
|
284
|
-
|
|
285
|
-
return self.samplesPerPixel.value;
|
|
286
|
-
|
|
287
|
-
},
|
|
288
|
-
set value( v ) {
|
|
289
|
-
|
|
290
|
-
self.samplesPerPixel.value = v;
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
},
|
|
294
282
|
useEnvMapIS: {
|
|
295
283
|
get value() {
|
|
296
284
|
|
|
@@ -346,7 +334,6 @@ export class PathTracerStage extends RenderStage {
|
|
|
346
334
|
enabled: DEFAULT_STATE.interactionModeEnabled,
|
|
347
335
|
qualitySettings: {
|
|
348
336
|
maxBounceCount: 1,
|
|
349
|
-
numRaysPerPixel: 1,
|
|
350
337
|
useEnvMapIS: false,
|
|
351
338
|
enableAccumulation: false,
|
|
352
339
|
enableEmissiveTriangleSampling: false,
|
package/src/TSL/Common.js
CHANGED
|
@@ -400,6 +400,68 @@ export const getMaterial = Fn( ( [ materialIndex, materialBuffer ] ) => {
|
|
|
400
400
|
|
|
401
401
|
} );
|
|
402
402
|
|
|
403
|
+
// Synthetic diffuse-white material for the analytic ground-plane shadow catcher.
|
|
404
|
+
// No geometry/material buffer is involved — the plane is shaded as a matte Lambertian
|
|
405
|
+
// so the direct-lighting estimator yields an irradiance-weighted shadow ratio (the
|
|
406
|
+
// diffuse BRDF is constant and cancels in shadowed/unoccluded). All fields are set to
|
|
407
|
+
// inert defaults; only color/roughness/metalness/transmission affect the lighting path.
|
|
408
|
+
export const diffuseGroundMaterial = Fn( () => {
|
|
409
|
+
|
|
410
|
+
const idn = mat3( 1, 0, 0, 0, 1, 0, 0, 0, 1 );
|
|
411
|
+
return RayTracingMaterial( {
|
|
412
|
+
color: vec4( 1.0, 1.0, 1.0, 1.0 ),
|
|
413
|
+
emissive: vec3( 0.0 ),
|
|
414
|
+
emissiveIntensity: float( 0.0 ),
|
|
415
|
+
roughness: float( 1.0 ),
|
|
416
|
+
metalness: float( 0.0 ),
|
|
417
|
+
ior: float( 1.5 ),
|
|
418
|
+
transmission: float( 0.0 ),
|
|
419
|
+
thickness: float( 0.0 ),
|
|
420
|
+
clearcoat: float( 0.0 ),
|
|
421
|
+
clearcoatRoughness: float( 0.0 ),
|
|
422
|
+
opacity: float( 1.0 ),
|
|
423
|
+
transparent: float( 0.0 ),
|
|
424
|
+
attenuationColor: vec3( 1.0 ),
|
|
425
|
+
attenuationDistance: float( 0.0 ),
|
|
426
|
+
dispersion: float( 0.0 ),
|
|
427
|
+
sheen: float( 0.0 ),
|
|
428
|
+
sheenRoughness: float( 1.0 ),
|
|
429
|
+
sheenColor: vec3( 0.0 ),
|
|
430
|
+
specularIntensity: float( 1.0 ),
|
|
431
|
+
specularColor: vec3( 1.0 ),
|
|
432
|
+
alphaTest: float( 0.0 ),
|
|
433
|
+
alphaMode: int( 0 ),
|
|
434
|
+
side: int( 0 ),
|
|
435
|
+
depthWrite: int( 1 ),
|
|
436
|
+
albedoMapIndex: int( - 1 ),
|
|
437
|
+
emissiveMapIndex: int( - 1 ),
|
|
438
|
+
normalMapIndex: int( - 1 ),
|
|
439
|
+
bumpMapIndex: int( - 1 ),
|
|
440
|
+
bumpScale: float( 1.0 ),
|
|
441
|
+
displacementScale: float( 0.0 ),
|
|
442
|
+
metalnessMapIndex: int( - 1 ),
|
|
443
|
+
roughnessMapIndex: int( - 1 ),
|
|
444
|
+
displacementMapIndex: int( - 1 ),
|
|
445
|
+
normalScale: vec2( 1.0, 1.0 ),
|
|
446
|
+
albedoTransform: idn,
|
|
447
|
+
emissiveTransform: idn,
|
|
448
|
+
normalTransform: idn,
|
|
449
|
+
bumpTransform: idn,
|
|
450
|
+
metalnessTransform: idn,
|
|
451
|
+
roughnessTransform: idn,
|
|
452
|
+
displacementTransform: idn,
|
|
453
|
+
iridescence: float( 0.0 ),
|
|
454
|
+
iridescenceIOR: float( 1.3 ),
|
|
455
|
+
iridescenceThicknessRange: vec2( 100.0, 400.0 ),
|
|
456
|
+
subsurface: float( 0.0 ),
|
|
457
|
+
subsurfaceColor: vec3( 1.0 ),
|
|
458
|
+
subsurfaceRadius: vec3( 1.0, 0.2, 0.1 ),
|
|
459
|
+
subsurfaceRadiusScale: float( 1.0 ),
|
|
460
|
+
subsurfaceAnisotropy: float( 0.0 ),
|
|
461
|
+
} );
|
|
462
|
+
|
|
463
|
+
} );
|
|
464
|
+
|
|
403
465
|
// ── Shadow material thin reader (7 slot reads instead of 27) ─────────────
|
|
404
466
|
// Only fetches fields needed by traceShadowRay: alpha, transmission, attenuation, albedo transform.
|
|
405
467
|
|
package/src/TSL/DebugKernel.js
CHANGED
|
@@ -32,7 +32,6 @@ export function buildDebugKernel( params ) {
|
|
|
32
32
|
visMode, debugVisScale,
|
|
33
33
|
albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
|
|
34
34
|
frame,
|
|
35
|
-
samplesPerPass = 1,
|
|
36
35
|
} = params;
|
|
37
36
|
|
|
38
37
|
const computeFn = Fn( () => {
|
|
@@ -61,8 +60,8 @@ export function buildDebugKernel( params ) {
|
|
|
61
60
|
// Mode 9: visualize the stratified AA-jitter pattern (R,G = jitter).
|
|
62
61
|
If( visMode.equal( int( 9 ) ), () => {
|
|
63
62
|
|
|
64
|
-
//
|
|
65
|
-
const jitter = getStratifiedSample( pixelCoord, int( 0 ), int(
|
|
63
|
+
// One ray per pixel — plain per-frame jitter (totalRays = 1 → random, no stratified lattice).
|
|
64
|
+
const jitter = getStratifiedSample( pixelCoord, int( 0 ), int( 1 ), seed, resolution, frame );
|
|
66
65
|
color.assign( vec4( jitter, 1.0, 1.0 ) );
|
|
67
66
|
|
|
68
67
|
} ).Else( () => {
|
package/src/TSL/Environment.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fn, wgslFn, vec2, vec4, ivec2, float, int, If, texture, dot, sin, sqrt, floor, fract, min, max, mix, clamp } from 'three/tsl';
|
|
1
|
+
import { Fn, wgslFn, vec2, vec3, vec4, ivec2, float, int, If, texture, dot, sin, sqrt, floor, fract, min, max, mix, clamp } from 'three/tsl';
|
|
2
2
|
|
|
3
3
|
import { REC709_LUMINANCE_COEFFICIENTS } from './Common.js';
|
|
4
4
|
|
|
@@ -211,3 +211,24 @@ export const getGroundProjectedDirection = Fn( ( [ rayOrigin, rayDirection, radi
|
|
|
211
211
|
return projected;
|
|
212
212
|
|
|
213
213
|
} );
|
|
214
|
+
|
|
215
|
+
// Primary-ray environment lookup direction: bends onto the ground-projection sphere/disk when
|
|
216
|
+
// ground projection is enabled, else returns the ray direction unchanged. Shared by the background
|
|
217
|
+
// miss branch AND the shadow catcher so they can never disagree on the projection (the source of a
|
|
218
|
+
// past horizon-seam bug when only one path bent the direction).
|
|
219
|
+
export const groundProjectedEnvDir = Fn( ( [ rayOrigin, rayDirection, enabled, radius, height, level ] ) => {
|
|
220
|
+
|
|
221
|
+
const dir = rayDirection.toVar();
|
|
222
|
+
If( enabled, () => {
|
|
223
|
+
|
|
224
|
+
// Relocate the projected ground plane from y=0 to world y=level (the scene floor) so a model
|
|
225
|
+
// authored off the origin still sits ON the ground instead of sinking. Shifting the projection
|
|
226
|
+
// origin down by `level` puts the disk at y=level and the sphere at y=level+height; the internal
|
|
227
|
+
// horizontal radius test stays correct because the shifted disk point lands at y'=0.
|
|
228
|
+
const shiftedOrigin = vec3( rayOrigin.x, rayOrigin.y.sub( level ), rayOrigin.z );
|
|
229
|
+
dir.assign( getGroundProjectedDirection( shiftedOrigin, rayDirection, radius, height ) );
|
|
230
|
+
|
|
231
|
+
} );
|
|
232
|
+
return dir;
|
|
233
|
+
|
|
234
|
+
} );
|