rayzee 9.2.0 → 9.3.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 +6 -5
- package/dist/{NeuralRetouch-DgnfldWO.js → NeuralRetouch-CYgkQY8Z.js} +1 -1
- package/dist/{NeuralSuperRes-Baa2w8VU.js → NeuralSuperRes-PkgDLQcD.js} +1 -1
- package/dist/{ToneMapGPU-CNgexVto.js → ToneMapGPU-B6XIe3DU.js} +31 -25
- package/dist/rayzee.es.js +5033 -4982
- package/dist/rayzee.umd.js +83 -72
- package/package.json +1 -1
- package/src/PathTracerApp.js +92 -30
- package/src/Processor/QueueManager.js +1 -0
- package/src/Processor/TextureReadback.js +26 -10
- package/src/Processor/ToneMapGPU.js +45 -15
- package/src/Storage/DownloadCache.js +30 -7
- package/src/TSL/BVHTraversal.js +13 -12
- package/src/TSL/Debugger.js +3 -0
- package/src/TSL/ExtendKernel.js +6 -2
- package/src/TSL/MaterialEvaluation.js +11 -6
- package/src/TSL/PathTracerCore.js +25 -4
- package/src/TSL/ShadeKernel.js +34 -8
package/package.json
CHANGED
package/src/PathTracerApp.js
CHANGED
|
@@ -32,6 +32,7 @@ import { nameFromUrl } from './Storage/DownloadCache.js';
|
|
|
32
32
|
import { fileIdentity, identityKey } from './Storage/identity.js';
|
|
33
33
|
import { SETTING_SOURCE } from './RenderSettings.js';
|
|
34
34
|
import { toneMapToRGBA8 } from './Processor/ToneMapCPU.js';
|
|
35
|
+
import { PackedToneMapper } from './Processor/ToneMapGPU.js';
|
|
35
36
|
import { ColorManagement, setActiveColorManagement } from './Color/ColorManagement.js';
|
|
36
37
|
import { getViewTransform } from './Color/ViewTransforms.js';
|
|
37
38
|
import { AssetLoader } from './Processor/AssetLoader.js';
|
|
@@ -786,6 +787,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
786
787
|
this.color = null;
|
|
787
788
|
this._textureReadback?.dispose();
|
|
788
789
|
this._textureReadback = null;
|
|
790
|
+
this._readbackToneMapper?.dispose();
|
|
791
|
+
this._readbackToneMapper = null;
|
|
789
792
|
|
|
790
793
|
this.interactionManager?.deselect?.();
|
|
791
794
|
this.transformManager?.detach?.();
|
|
@@ -3016,6 +3019,24 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3016
3019
|
// The pool over-allocates to the reserve, so the texture is larger than the frame.
|
|
3017
3020
|
const { width, height } = stage;
|
|
3018
3021
|
|
|
3022
|
+
if ( colorSpace === 'srgb' ) {
|
|
3023
|
+
|
|
3024
|
+
const shown = source === 'display' ? this._displaySource( target ) : null;
|
|
3025
|
+
const tone = {
|
|
3026
|
+
exposure: this.renderer.toneMappingExposure,
|
|
3027
|
+
toneMapping: this.renderer.toneMapping,
|
|
3028
|
+
saturation: this.settings.get( 'saturation' ) ?? 1,
|
|
3029
|
+
preserveAlpha,
|
|
3030
|
+
};
|
|
3031
|
+
|
|
3032
|
+
const data = await this._toneMapOnGPU( shown?.texture ?? null, target, width, height, tone )
|
|
3033
|
+
?? toneMapToRGBA8( shown
|
|
3034
|
+
? await this._readTexture( shown.texture, width, height )
|
|
3035
|
+
: await this.renderer.readRenderTargetPixelsAsync( target, 0, 0, width, height, 0 ), tone );
|
|
3036
|
+
return { data, width, height, colorSpace, source: shown?.source ?? 'accumulation' };
|
|
3037
|
+
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3019
3040
|
const { data: linear, source: read } = source === 'display'
|
|
3020
3041
|
? await this._readDisplaySource( target, width, height )
|
|
3021
3042
|
: { data: await this.renderer.readRenderTargetPixelsAsync( target, 0, 0, width, height, 0 ), source: 'accumulation' };
|
|
@@ -3031,25 +3052,42 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3031
3052
|
// A named space is a delivery buffer, not a picture: scene-referred float in whatever the
|
|
3032
3053
|
// config calls that space. Saving an EXR for a compositor and grading through an sRGB view
|
|
3033
3054
|
// on screen are different questions, and this is the one that answers the first.
|
|
3034
|
-
|
|
3055
|
+
const { rgba, colorSpace: got } = this.color.exportPixels( linear, colorSpace );
|
|
3056
|
+
return { data: rgba, width, height, colorSpace: got, source: read };
|
|
3035
3057
|
|
|
3036
|
-
|
|
3037
|
-
return { data: rgba, width, height, colorSpace: got, source: read };
|
|
3058
|
+
}
|
|
3038
3059
|
|
|
3039
|
-
|
|
3060
|
+
/**
|
|
3061
|
+
* Display bytes tone-mapped on the card, or null to do it on the CPU. On the CPU it took 1.4 s at
|
|
3062
|
+
* 4096×2160 on an M-series Mac and 10 s on a cloud L4's host; the GPU pass matches it within one
|
|
3063
|
+
* level (`tests/gpu/toneMapParity.test.js`).
|
|
3064
|
+
*/
|
|
3065
|
+
async _toneMapOnGPU( shown, accumulation, width, height, tone ) {
|
|
3040
3066
|
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3067
|
+
const backend = this.renderer?.backend;
|
|
3068
|
+
if ( ! backend?.device ) return null;
|
|
3069
|
+
|
|
3070
|
+
try {
|
|
3071
|
+
|
|
3072
|
+
const texture = shown ? this._readbackPass().draw( shown, width, height ).texture : accumulation.textures[ 0 ];
|
|
3073
|
+
const gpuTexture = backend.get( texture ).texture;
|
|
3074
|
+
if ( ! gpuTexture ) return null;
|
|
3075
|
+
|
|
3076
|
+
this._readbackToneMapper ??= new PackedToneMapper( backend.device, 'rayzee:readback-tonemap', { input: 'texture' } );
|
|
3077
|
+
this._readbackToneMapper.ensureSize( width, height );
|
|
3078
|
+
return await this._readbackToneMapper.toRGBA8( gpuTexture, tone );
|
|
3079
|
+
|
|
3080
|
+
} catch ( error ) {
|
|
3081
|
+
|
|
3082
|
+
log.warn( `renderToBuffer: tone mapping on the GPU failed, doing it on the CPU: ${error?.message ?? error}` );
|
|
3083
|
+
return null;
|
|
3084
|
+
|
|
3085
|
+
} finally {
|
|
3086
|
+
|
|
3087
|
+
this._readbackToneMapper?.release();
|
|
3088
|
+
if ( shown ) this._textureReadback?.release();
|
|
3089
|
+
|
|
3090
|
+
}
|
|
3053
3091
|
|
|
3054
3092
|
}
|
|
3055
3093
|
|
|
@@ -3655,32 +3693,56 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3655
3693
|
}
|
|
3656
3694
|
|
|
3657
3695
|
/**
|
|
3658
|
-
* What
|
|
3659
|
-
*
|
|
3696
|
+
* What a `'display'` read shows: the texture a denoiser published, or null for the raw
|
|
3697
|
+
* accumulation — recorded as a fallback when a denoiser is in use.
|
|
3660
3698
|
* @private
|
|
3661
3699
|
*/
|
|
3662
|
-
|
|
3700
|
+
_displaySource( accumulation ) {
|
|
3663
3701
|
|
|
3664
3702
|
const context = this.pipeline?.context;
|
|
3665
3703
|
const found = context && this.stages.compositor?.resolveLightSource( context );
|
|
3666
|
-
if (
|
|
3704
|
+
if ( found && found.key !== 'pathtracer:color' && found.texture !== accumulation.texture ) {
|
|
3667
3705
|
|
|
3668
|
-
|
|
3706
|
+
return { texture: found.texture, source: found.key.split( ':' )[ 0 ] };
|
|
3669
3707
|
|
|
3670
|
-
|
|
3671
|
-
ISSUE_CODES.OUTPUT_SOURCE_FALLBACK,
|
|
3672
|
-
'renderToBuffer: \'display\' was asked for, but no denoiser has published a picture — returned the raw accumulation',
|
|
3673
|
-
{ requested: 'display', read: 'accumulation' }
|
|
3674
|
-
);
|
|
3708
|
+
}
|
|
3675
3709
|
|
|
3676
|
-
|
|
3710
|
+
if ( this._denoiserInUse() ) {
|
|
3677
3711
|
|
|
3678
|
-
|
|
3712
|
+
this._issues.record(
|
|
3713
|
+
ISSUE_CODES.OUTPUT_SOURCE_FALLBACK,
|
|
3714
|
+
'renderToBuffer: \'display\' was asked for, but no denoiser has published a picture — returned the raw accumulation',
|
|
3715
|
+
{ requested: 'display', read: 'accumulation' }
|
|
3716
|
+
);
|
|
3679
3717
|
|
|
3680
3718
|
}
|
|
3681
3719
|
|
|
3682
|
-
|
|
3683
|
-
|
|
3720
|
+
return null;
|
|
3721
|
+
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3724
|
+
/**
|
|
3725
|
+
* What the Compositor is drawing from, as linear float. Falls back to the accumulation when no
|
|
3726
|
+
* denoiser has published anything, which is also what the viewport shows then.
|
|
3727
|
+
* @private
|
|
3728
|
+
*/
|
|
3729
|
+
async _readDisplaySource( accumulation, width, height ) {
|
|
3730
|
+
|
|
3731
|
+
const shown = this._displaySource( accumulation );
|
|
3732
|
+
if ( ! shown ) return { data: await this.renderer.readRenderTargetPixelsAsync( accumulation, 0, 0, width, height, 0 ), source: 'accumulation' };
|
|
3733
|
+
return { data: await this._readTexture( shown.texture, width, height ), source: shown.source };
|
|
3734
|
+
|
|
3735
|
+
}
|
|
3736
|
+
|
|
3737
|
+
_readbackPass() {
|
|
3738
|
+
|
|
3739
|
+
return ( this._textureReadback ??= new TextureReadback( this.renderer ) );
|
|
3740
|
+
|
|
3741
|
+
}
|
|
3742
|
+
|
|
3743
|
+
_readTexture( texture, width, height ) {
|
|
3744
|
+
|
|
3745
|
+
return this._readbackPass().read( texture, width, height );
|
|
3684
3746
|
|
|
3685
3747
|
}
|
|
3686
3748
|
|
|
@@ -51,6 +51,7 @@ export const RAY_FLAG = {
|
|
|
51
51
|
AUX_LOCKED: 1 << 17, // bit 17: OIDN aux (normal/albedo) locked onto first non-specular hit (megakernel auxLocked)
|
|
52
52
|
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.
|
|
53
53
|
SUN_NEE: 1 << 19, // bit 19: the last opaque scatter's sun NEE could have drawn this direction (MIS partner for a sun hit on miss)
|
|
54
|
+
UNDER_SURFACE: 1 << 20, // bit 20: the last opaque scatter left below its own facet (shading normal allowed it), so the ray culls back faces to leave that shell
|
|
54
55
|
};
|
|
55
56
|
|
|
56
57
|
export class QueueManager {
|
|
@@ -46,6 +46,25 @@ export class TextureReadback {
|
|
|
46
46
|
*/
|
|
47
47
|
async read( source, width, height ) {
|
|
48
48
|
|
|
49
|
+
try {
|
|
50
|
+
|
|
51
|
+
return await this.renderer.readRenderTargetPixelsAsync( this.draw( source, width, height ), 0, 0, width, height );
|
|
52
|
+
|
|
53
|
+
} finally {
|
|
54
|
+
|
|
55
|
+
this.release();
|
|
56
|
+
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Draws `source` into this pass's float target, for a caller that reads it on the GPU. Pair with
|
|
63
|
+
* `release()`.
|
|
64
|
+
* @returns {import('three').RenderTarget}
|
|
65
|
+
*/
|
|
66
|
+
draw( source, width, height ) {
|
|
67
|
+
|
|
49
68
|
this._ensureTarget( width, height );
|
|
50
69
|
|
|
51
70
|
if ( this._source !== source ) {
|
|
@@ -63,19 +82,16 @@ export class TextureReadback {
|
|
|
63
82
|
this.renderer.setRenderTarget( this.target );
|
|
64
83
|
this.quad.render( this.renderer );
|
|
65
84
|
this.renderer.setRenderTarget( previous );
|
|
85
|
+
return this.target;
|
|
66
86
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return await this.renderer.readRenderTargetPixelsAsync( this.target, 0, 0, width, height );
|
|
70
|
-
|
|
71
|
-
} finally {
|
|
87
|
+
}
|
|
72
88
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
this.target = null;
|
|
89
|
+
// A float target the size of the frame — 132 MB at 4K — for something done once per saved file.
|
|
90
|
+
// Not worth keeping between saves.
|
|
91
|
+
release() {
|
|
77
92
|
|
|
78
|
-
|
|
93
|
+
this.target?.dispose();
|
|
94
|
+
this.target = null;
|
|
79
95
|
|
|
80
96
|
}
|
|
81
97
|
|
|
@@ -39,7 +39,24 @@ function currentShader() {
|
|
|
39
39
|
*/
|
|
40
40
|
export const TONE_MAP_WGSL = currentShader().wgsl;
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
// Where the linear colour comes from: a packed rgba16float buffer, or a float texture read by pixel.
|
|
43
|
+
const SOURCES = {
|
|
44
|
+
packed: /* wgsl */ `
|
|
45
|
+
@group(0) @binding(0) var<storage, read> src: array<u32>;
|
|
46
|
+
|
|
47
|
+
fn rayzee_source( x: u32, y: u32 ) -> vec4<f32> {
|
|
48
|
+
let a = ( y * params.width + x ) * 2u;
|
|
49
|
+
return vec4<f32>( unpack2x16float( src[ a ] ), unpack2x16float( src[ a + 1u ] ) );
|
|
50
|
+
}`,
|
|
51
|
+
texture: /* wgsl */ `
|
|
52
|
+
@group(0) @binding(0) var src: texture_2d<f32>;
|
|
53
|
+
|
|
54
|
+
fn rayzee_source( x: u32, y: u32 ) -> vec4<f32> {
|
|
55
|
+
return textureLoad( src, vec2<u32>( x, y ), 0 );
|
|
56
|
+
}`,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const toneMapPassWGSL = ( transformWGSL, input ) => /* wgsl */ `
|
|
43
60
|
struct Params {
|
|
44
61
|
width: u32,
|
|
45
62
|
height: u32,
|
|
@@ -47,11 +64,12 @@ struct Params {
|
|
|
47
64
|
flipY: u32,
|
|
48
65
|
exposure: f32,
|
|
49
66
|
saturation: f32,
|
|
67
|
+
alpha: u32,
|
|
50
68
|
};
|
|
51
69
|
|
|
52
|
-
@group(0) @binding(0) var<storage, read> src: array<u32>;
|
|
53
70
|
@group(0) @binding(1) var<storage, read_write> dst: array<u32>;
|
|
54
71
|
@group(0) @binding(2) var<uniform> params: Params;
|
|
72
|
+
${SOURCES[ input ]}
|
|
55
73
|
|
|
56
74
|
${transformWGSL}
|
|
57
75
|
|
|
@@ -62,30 +80,31 @@ fn main( @builtin(global_invocation_id) gid: vec3<u32> ) {
|
|
|
62
80
|
var srcY = gid.y;
|
|
63
81
|
if ( params.flipY == 1u ) { srcY = params.height - 1u - gid.y; }
|
|
64
82
|
|
|
65
|
-
let
|
|
66
|
-
let
|
|
67
|
-
let ba = unpack2x16float( src[ a + 1u ] );
|
|
68
|
-
|
|
69
|
-
let mapped = rayzee_tone_map( vec3<f32>( rg.x, rg.y, ba.x ), params.mode, params.exposure, params.saturation );
|
|
83
|
+
let linear = rayzee_source( gid.x, srcY );
|
|
84
|
+
let mapped = rayzee_tone_map( linear.rgb, params.mode, params.exposure, params.saturation );
|
|
70
85
|
let b8 = rayzee_to_u8( rayzee_encode( mapped, params.mode ) );
|
|
71
|
-
|
|
86
|
+
var a8 = 255u;
|
|
87
|
+
if ( params.alpha == 1u ) { a8 = rayzee_to_u8( vec3<f32>( linear.a ) ).x; }
|
|
88
|
+
dst[ gid.y * params.width + gid.x ] = b8.x | ( b8.y << 8u ) | ( b8.z << 16u ) | ( a8 << 24u );
|
|
72
89
|
}
|
|
73
90
|
`;
|
|
74
91
|
|
|
75
|
-
/** Uniform blocks round up to 16 bytes; the struct above is
|
|
92
|
+
/** Uniform blocks round up to 16 bytes; the struct above is 28. */
|
|
76
93
|
const PARAMS_BYTES = 32;
|
|
77
94
|
|
|
78
95
|
/**
|
|
79
|
-
* Tone-maps a tightly packed rgba16float buffer (two `u32` per pixel)
|
|
96
|
+
* Tone-maps a tightly packed rgba16float buffer (two `u32` per pixel), or with `input: 'texture'` a
|
|
97
|
+
* float texture read by pixel, into RGBA bytes.
|
|
80
98
|
*
|
|
81
99
|
* Bound to one device and one image size; `ensureSize()` reallocates when the size changes.
|
|
82
100
|
*/
|
|
83
101
|
export class PackedToneMapper {
|
|
84
102
|
|
|
85
|
-
constructor( device, label = 'rayzee:tonemap' ) {
|
|
103
|
+
constructor( device, label = 'rayzee:tonemap', { input = 'packed' } = {} ) {
|
|
86
104
|
|
|
87
105
|
this.device = device;
|
|
88
106
|
this.label = label;
|
|
107
|
+
this.input = input;
|
|
89
108
|
this.width = 0;
|
|
90
109
|
this.height = 0;
|
|
91
110
|
this._pipeline = null;
|
|
@@ -120,7 +139,7 @@ export class PackedToneMapper {
|
|
|
120
139
|
label: this.label,
|
|
121
140
|
layout: 'auto',
|
|
122
141
|
compute: {
|
|
123
|
-
module: this.device.createShaderModule( { label: this.label, code:
|
|
142
|
+
module: this.device.createShaderModule( { label: this.label, code: toneMapPassWGSL( wgsl, this.input ) } ),
|
|
124
143
|
entryPoint: 'main',
|
|
125
144
|
},
|
|
126
145
|
} );
|
|
@@ -206,15 +225,17 @@ export class PackedToneMapper {
|
|
|
206
225
|
}
|
|
207
226
|
|
|
208
227
|
/**
|
|
209
|
-
* @param {GPUBuffer} src packed rgba16float, `width * height * 8` bytes, STORAGE-capable
|
|
228
|
+
* @param {GPUBuffer|GPUTexture} src packed rgba16float, `width * height * 8` bytes, STORAGE-capable;
|
|
229
|
+
* or with `input: 'texture'` a float texture at least `width × height`
|
|
210
230
|
* @param {object} tone
|
|
211
231
|
* @param {number} tone.exposure raw `renderer.toneMappingExposure`
|
|
212
232
|
* @param {number} tone.toneMapping Three.js ToneMapping constant
|
|
213
233
|
* @param {number} [tone.saturation=1]
|
|
214
234
|
* @param {boolean} [tone.flipY=false]
|
|
235
|
+
* @param {boolean} [tone.preserveAlpha=false] - the source's alpha, rounded as the CPU rounds it; else 255
|
|
215
236
|
* @returns {Promise<Uint8ClampedArray>} RGBA bytes, `width * height * 4`
|
|
216
237
|
*/
|
|
217
|
-
async toRGBA8( src, { exposure = 1, toneMapping = 0, saturation = 1, flipY = false } = {} ) {
|
|
238
|
+
async toRGBA8( src, { exposure = 1, toneMapping = 0, saturation = 1, flipY = false, preserveAlpha = false } = {} ) {
|
|
218
239
|
|
|
219
240
|
if ( this.disposed ) throw new Error( 'PackedToneMapper: disposed' );
|
|
220
241
|
if ( ! this._storage ) throw new Error( 'PackedToneMapper: call ensureSize() first' );
|
|
@@ -232,6 +253,7 @@ export class PackedToneMapper {
|
|
|
232
253
|
this._paramU32[ 3 ] = flipY ? 1 : 0;
|
|
233
254
|
this._paramF32[ 4 ] = exposure;
|
|
234
255
|
this._paramF32[ 5 ] = saturation;
|
|
256
|
+
this._paramU32[ 6 ] = preserveAlpha ? 1 : 0;
|
|
235
257
|
this.device.queue.writeBuffer( this._params, 0, this._paramData );
|
|
236
258
|
|
|
237
259
|
const tableEntries = [ ...this._tables.entries() ]
|
|
@@ -241,7 +263,7 @@ export class PackedToneMapper {
|
|
|
241
263
|
layout: this._pipeline.getBindGroupLayout( 0 ),
|
|
242
264
|
entries: [
|
|
243
265
|
...tableEntries,
|
|
244
|
-
{ binding: 0, resource: { buffer: src, size: width * height * 8 } },
|
|
266
|
+
{ binding: 0, resource: this.input === 'texture' ? src.createView() : { buffer: src, size: width * height * 8 } },
|
|
245
267
|
{ binding: 1, resource: { buffer: this._storage } },
|
|
246
268
|
{ binding: 2, resource: { buffer: this._params } },
|
|
247
269
|
],
|
|
@@ -263,6 +285,14 @@ export class PackedToneMapper {
|
|
|
263
285
|
|
|
264
286
|
}
|
|
265
287
|
|
|
288
|
+
/** Frees the size-dependent buffers and keeps the pipeline, for a mapper used now and then. */
|
|
289
|
+
release() {
|
|
290
|
+
|
|
291
|
+
this._releaseBuffers();
|
|
292
|
+
this.width = this.height = 0;
|
|
293
|
+
|
|
294
|
+
}
|
|
295
|
+
|
|
266
296
|
_releaseBuffers() {
|
|
267
297
|
|
|
268
298
|
this._storage?.destroy();
|
|
@@ -191,15 +191,37 @@ export class DownloadCache {
|
|
|
191
191
|
if ( this._revalidating.has( key ) ) return;
|
|
192
192
|
this._revalidating.add( key );
|
|
193
193
|
|
|
194
|
+
let changed = false;
|
|
195
|
+
|
|
194
196
|
try {
|
|
195
197
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
+
// One byte, not HEAD: a CORS rule that allows GET — the engine's own asset host — refuses
|
|
199
|
+
// HEAD, and a single-range Range header needs no preflight.
|
|
200
|
+
const headers = new Headers( fetchOptions?.headers );
|
|
201
|
+
headers.set( 'Range', 'bytes=0-0' );
|
|
202
|
+
const probe = await fetch( url, { ...fetchOptions, headers } );
|
|
203
|
+
probe.body?.cancel().catch( () => {} );
|
|
204
|
+
|
|
205
|
+
if ( probe.ok ) {
|
|
206
|
+
|
|
207
|
+
const lastModified = header( probe, 'last-modified' );
|
|
208
|
+
// A 206 carries the size in Content-Range, unreadable cross-origin unless exposed; a server
|
|
209
|
+
// that ignores Range answers 200 with the whole length.
|
|
210
|
+
const length = probe.status === 206
|
|
211
|
+
? Number( header( probe, 'content-range' )?.match( /\/(\d+)$/ )?.[ 1 ] ) || null
|
|
212
|
+
: Number( header( probe, 'content-length' ) ) || null;
|
|
213
|
+
changed = !! ( ( lastModified && extra.lastModified && lastModified !== extra.lastModified )
|
|
214
|
+
|| ( length && extra.contentLength && length !== extra.contentLength ) );
|
|
198
215
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
} catch {
|
|
219
|
+
|
|
220
|
+
// Offline, or the server refuses the probe: keep serving the stored copy.
|
|
221
|
+
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
203
225
|
|
|
204
226
|
if ( changed ) {
|
|
205
227
|
|
|
@@ -208,13 +230,14 @@ export class DownloadCache {
|
|
|
208
230
|
|
|
209
231
|
} else {
|
|
210
232
|
|
|
233
|
+
// Stamped when the check failed too, or a refusing server is asked again on every load.
|
|
211
234
|
await this._area.patchMeta( key, { extra: { ...extra, checkedAt: Date.now() } } );
|
|
212
235
|
|
|
213
236
|
}
|
|
214
237
|
|
|
215
238
|
} catch {
|
|
216
239
|
|
|
217
|
-
//
|
|
240
|
+
// The stored copy stays.
|
|
218
241
|
|
|
219
242
|
} finally {
|
|
220
243
|
|
package/src/TSL/BVHTraversal.js
CHANGED
|
@@ -212,8 +212,14 @@ const fastRayAABBDst = wgslFn( `
|
|
|
212
212
|
// ================================================================================
|
|
213
213
|
// MAIN BVH TRAVERSAL
|
|
214
214
|
// ================================================================================
|
|
215
|
-
|
|
216
|
-
//
|
|
215
|
+
|
|
216
|
+
// Side from a triangle's flags word (row 4 .z): 0 front, 1 back, 2 double.
|
|
217
|
+
export const triangleSide = ( flags ) => int( flags.shiftRight( uint( TRI_SIDE_SHIFT ) ).bitAnd( uint( 3 ) ) );
|
|
218
|
+
|
|
219
|
+
// Whether a face of this side is seen by a ray; rayDotNormal < 0 is a front hit.
|
|
220
|
+
export const sideAccepts = ( side, rayDotNormal ) => side.equal( int( 2 ) )
|
|
221
|
+
.or( side.equal( int( 0 ) ).and( rayDotNormal.lessThan( - 0.0001 ) ) )
|
|
222
|
+
.or( side.equal( int( 1 ) ).and( rayDotNormal.greaterThan( 0.0001 ) ) );
|
|
217
223
|
|
|
218
224
|
// Factory: the boxTests/triTests debug counters are compiled OUT of the hot closest-hit
|
|
219
225
|
// path (extend/normalDepth use traverseBVH = trackStats false) — only the debug viz reads
|
|
@@ -223,11 +229,11 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
|
|
|
223
229
|
bvhBuffer,
|
|
224
230
|
triangleBuffer,
|
|
225
231
|
insideMedium, // optional: when true (ray inside a medium), bypass front/back culling
|
|
232
|
+
cullBackFaces, // optional, default true: false hits either side, as shadow rays do
|
|
226
233
|
] ) => {
|
|
227
234
|
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
const inMedium = insideMedium ?? tslBool( false );
|
|
235
|
+
// Culling is for what the camera sees through; a bounce culling it leaks light through hollow models.
|
|
236
|
+
const eitherSide = ( insideMedium ?? tslBool( false ) ).or( ( cullBackFaces ?? tslBool( true ) ).not() ).toVar();
|
|
231
237
|
|
|
232
238
|
// Folded leaves cost up to 4 % of traversal, so only a BVH built with them gets that code.
|
|
233
239
|
const folded = bvhBuffer.value?.foldedLeaves === true;
|
|
@@ -308,8 +314,7 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
|
|
|
308
314
|
const nA = unpackTriangleNormal( recA.w );
|
|
309
315
|
const nB = unpackTriangleNormal( recB.w );
|
|
310
316
|
const nC = unpackTriangleNormal( recC.w );
|
|
311
|
-
const
|
|
312
|
-
const side = int( flags.shiftRight( uint( TRI_SIDE_SHIFT ) ).bitAnd( uint( 3 ) ) ).toVar();
|
|
317
|
+
const side = triangleSide( triangleRow( triangleBuffer, triIndex, 4 ).z ).toVar();
|
|
313
318
|
|
|
314
319
|
// Interpolate normal for the side-culling dot product (kept local,
|
|
315
320
|
// not stored on closestHit — re-derived post-loop from closestTriIdx).
|
|
@@ -319,11 +324,7 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
|
|
|
319
324
|
);
|
|
320
325
|
|
|
321
326
|
// Side culling (inline; per-mesh visibility is at the BLAS-pointer level).
|
|
322
|
-
|
|
323
|
-
const sidePass = inMedium.or( side.equal( int( 2 ) ) )
|
|
324
|
-
.or( side.equal( int( 0 ) ).and( rayDotNormal.lessThan( - 0.0001 ) ) )
|
|
325
|
-
.or( side.equal( int( 1 ) ).and( rayDotNormal.greaterThan( 0.0001 ) ) );
|
|
326
|
-
If( sidePass, () => {
|
|
327
|
+
If( eitherSide.or( sideAccepts( side, rayDotNormal ) ), () => {
|
|
327
328
|
|
|
328
329
|
closestHit.didHit.assign( true );
|
|
329
330
|
closestHit.dst.assign( t );
|
package/src/TSL/Debugger.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
If,
|
|
19
19
|
select,
|
|
20
20
|
sampler,
|
|
21
|
+
bool as tslBool,
|
|
21
22
|
} from 'three/tsl';
|
|
22
23
|
|
|
23
24
|
import { Ray, HitInfo, RayTracingMaterial, MaterialSamples } from './Struct.js';
|
|
@@ -343,6 +344,8 @@ export const TraceDebugMode = Fn( ( [
|
|
|
343
344
|
bounceRay,
|
|
344
345
|
bvhBuffer,
|
|
345
346
|
triangleBuffer,
|
|
347
|
+
tslBool( false ),
|
|
348
|
+
tslBool( false ),
|
|
346
349
|
).toVar() );
|
|
347
350
|
|
|
348
351
|
const incoming = vec3( 0.0 ).toVar();
|
package/src/TSL/ExtendKernel.js
CHANGED
|
@@ -49,7 +49,8 @@ export function buildExtendKernel( params ) {
|
|
|
49
49
|
|
|
50
50
|
// Parity with Shade's guard. Free — the flags share DIR_FLAGS.w with the direction read below.
|
|
51
51
|
// Only culls work on the pinned-dispatch path, whose identity list still holds dead rays.
|
|
52
|
-
|
|
52
|
+
const flags = readRayBounceFlags( rayBufferRO, rayID ).toVar();
|
|
53
|
+
If( flags.bitAnd( uint( RAY_FLAG.ACTIVE ) ).equal( uint( 0 ) ), () => {
|
|
53
54
|
|
|
54
55
|
Return();
|
|
55
56
|
|
|
@@ -62,8 +63,11 @@ export function buildExtendKernel( params ) {
|
|
|
62
63
|
|
|
63
64
|
// insideMedium bypasses front/back culling so the ray can hit a glass/SSS back-facing boundary.
|
|
64
65
|
const insideMedium = readMediumStack( rayBufferRO, rayID ).stackDepth.greaterThan( uint( 0 ) );
|
|
66
|
+
// Only the camera's own view culls back faces, and a bounce that dipped under its surface.
|
|
67
|
+
const cull = flags.bitAnd( uint( RAY_FLAG.REDIRECTED ) ).equal( uint( 0 ) )
|
|
68
|
+
.or( flags.bitAnd( uint( RAY_FLAG.UNDER_SURFACE ) ).notEqual( uint( 0 ) ) );
|
|
65
69
|
const hitInfo = HitInfo.wrap( traverseBVH(
|
|
66
|
-
ray, bvhBuffer, triangleBuffer, insideMedium,
|
|
70
|
+
ray, bvhBuffer, triangleBuffer, insideMedium, cull,
|
|
67
71
|
) ).toVar();
|
|
68
72
|
|
|
69
73
|
const facet = hitFacet( {
|
|
@@ -24,7 +24,9 @@ import { evalIridescence } from './MaterialProperties.js';
|
|
|
24
24
|
// Body of evaluateMaterialResponse taking precomputed dot products. Callers
|
|
25
25
|
// that also need calculateMaterialPDF for the same (V, L, N) should share dots
|
|
26
26
|
// to save one computeDotProducts call.
|
|
27
|
-
|
|
27
|
+
// Roughness 0 marks an exact mirror (ShadeKernel): its delta lobe is left out of this BSDF, and
|
|
28
|
+
// deltaOnly returns that lobe's reflectance instead, under the same sheen and coat attenuation.
|
|
29
|
+
const makeEvaluateMaterialResponse = ( deltaOnly ) => Fn( ( [ material, dots ] ) => {
|
|
28
30
|
|
|
29
31
|
const result = vec3( 0.0 ).toVar();
|
|
30
32
|
|
|
@@ -92,7 +94,7 @@ export const evaluateMaterialResponseFromDots = Fn( ( [ material, dots ] ) => {
|
|
|
92
94
|
const Va = VisibilityGGXAniso( a.x, a.y, dots.ToV, dots.BoV, dots.ToL, dots.BoL, dots.NoV, dots.NoL );
|
|
93
95
|
specularSS.assign( F.mul( Da.mul( Va ) ) );
|
|
94
96
|
|
|
95
|
-
} ).
|
|
97
|
+
} ).ElseIf( material.roughness.greaterThan( 0.0 ), () => {
|
|
96
98
|
|
|
97
99
|
const D = DistributionGGX( dots.NoH, material.roughness );
|
|
98
100
|
const Vis = VisibilityGGXSmithCorrelated( dots.NoV, dots.NoL, material.roughness );
|
|
@@ -105,7 +107,7 @@ export const evaluateMaterialResponseFromDots = Fn( ( [ material, dots ] ) => {
|
|
|
105
107
|
bf.f0, bf.f90, bf.eta, bf.F0m, material.metalness, iridF, material.iridescence, F0,
|
|
106
108
|
dots.NoV, material.roughness,
|
|
107
109
|
) );
|
|
108
|
-
const specular = specularSS.mul( dfg.compensation );
|
|
110
|
+
const specular = ( deltaOnly ? F : specularSS ).mul( dfg.compensation );
|
|
109
111
|
|
|
110
112
|
// Diffuse energy budget from hemisphere-integrated specular albedo (includes multiscatter)
|
|
111
113
|
// Transmission removes energy from diffuse just as metalness does — KHR_materials_transmission
|
|
@@ -113,7 +115,7 @@ export const evaluateMaterialResponseFromDots = Fn( ( [ material, dots ] ) => {
|
|
|
113
115
|
const kD = vec3( 1.0 ).sub( dfg.E_total )
|
|
114
116
|
.mul( float( 1.0 ).sub( material.metalness ) )
|
|
115
117
|
.mul( float( 1.0 ).sub( material.transmission ) );
|
|
116
|
-
const diffuse = kD.mul( materialColor ).mul( PI_INV );
|
|
118
|
+
const diffuse = deltaOnly ? vec3( 0.0 ) : kD.mul( materialColor ).mul( PI_INV );
|
|
117
119
|
|
|
118
120
|
const baseLayer = diffuse.add( specular ).toVar();
|
|
119
121
|
|
|
@@ -131,7 +133,7 @@ export const evaluateMaterialResponseFromDots = Fn( ( [ material, dots ] ) => {
|
|
|
131
133
|
const sheenReflectance = clamp( material.sheenColor.mul( material.sheen ).mul( sheenE ), vec3( 0.0 ), vec3( 1.0 ) );
|
|
132
134
|
const sheenAttenuation = vec3( 1.0 ).sub( sheenReflectance );
|
|
133
135
|
|
|
134
|
-
result.assign( baseLayer.mul( sheenAttenuation ).add( sheenTerm ) );
|
|
136
|
+
result.assign( deltaOnly ? baseLayer.mul( sheenAttenuation ) : baseLayer.mul( sheenAttenuation ).add( sheenTerm ) );
|
|
135
137
|
|
|
136
138
|
} ).Else( () => {
|
|
137
139
|
|
|
@@ -161,7 +163,7 @@ export const evaluateMaterialResponseFromDots = Fn( ( [ material, dots ] ) => {
|
|
|
161
163
|
|
|
162
164
|
const ccAttenuation = vec3( 1.0 ).sub( ccDfg.E_total.mul( material.clearcoat ) );
|
|
163
165
|
|
|
164
|
-
result.assign( result.mul( ccAttenuation ).add( ccLobe.mul( material.clearcoat ) ) );
|
|
166
|
+
result.assign( deltaOnly ? result.mul( ccAttenuation ) : result.mul( ccAttenuation ).add( ccLobe.mul( material.clearcoat ) ) );
|
|
165
167
|
|
|
166
168
|
} );
|
|
167
169
|
|
|
@@ -171,6 +173,9 @@ export const evaluateMaterialResponseFromDots = Fn( ( [ material, dots ] ) => {
|
|
|
171
173
|
|
|
172
174
|
} );
|
|
173
175
|
|
|
176
|
+
export const evaluateMaterialResponseFromDots = /*@__PURE__*/ makeEvaluateMaterialResponse( false );
|
|
177
|
+
export const evaluateSpecularDeltaFromDots = /*@__PURE__*/ makeEvaluateMaterialResponse( true );
|
|
178
|
+
|
|
174
179
|
// Wrapper that computes dot products internally. Use this when you don't already
|
|
175
180
|
// have dots; otherwise prefer evaluateMaterialResponseFromDots to share the work.
|
|
176
181
|
export const evaluateMaterialResponse = Fn( ( [ V, L, N, material ] ) => {
|