rayzee 7.22.7 → 7.22.9
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/dist/rayzee.es.js +1116 -1038
- package/dist/rayzee.umd.js +42 -26
- package/package.json +1 -1
- package/src/EngineDefaults.js +2 -1
- package/src/Passes/OIDNDenoiser.js +163 -26
- package/src/Stages/NormalDepth.js +11 -2
- package/src/TSL/Debugger.js +5 -5
- package/src/TSL/ShadeKernel.js +11 -2
- package/src/TSL/TextureSampling.js +97 -8
package/package.json
CHANGED
package/src/EngineDefaults.js
CHANGED
|
@@ -604,7 +604,8 @@ export const PRODUCTION_RENDER_CONFIG = {
|
|
|
604
604
|
// so easy scenes finish well under it while hard GI scenes use the full budget.
|
|
605
605
|
maxSamples: 150, bounces: 20, transmissiveBounces: 8, maxSubsurfaceSteps: 64,
|
|
606
606
|
renderMode: 1, enableAlphaShadows: true,
|
|
607
|
-
|
|
607
|
+
// 'high' is the only tier that reaches OIDN's _large weights (calb_cnrm); ~2x denoise cost.
|
|
608
|
+
enableOIDN: true, oidnQuality: 'high',
|
|
608
609
|
interactionModeEnabled: false,
|
|
609
610
|
// 0.94 against the eroded count ≈ the old raw-count 0.98; erosion holds the fraction a few points lower.
|
|
610
611
|
useAdaptiveSampling: true,
|
|
@@ -52,20 +52,65 @@ import { getAssetConfig } from '../AssetConfig.js';
|
|
|
52
52
|
const _tmOut = new Float32Array( 3 );
|
|
53
53
|
|
|
54
54
|
const MODEL_CONFIG = {
|
|
55
|
-
//
|
|
55
|
+
// No cleanAux flag in oidn-web, so the blob is it — and must match what setCleanAuxNormal
|
|
56
|
+
// feeds: `fast` point-samples the normal (noisy aux), balance/high accumulate it. calb_cnrm
|
|
57
|
+
// is also the only model with a `_large` blob.
|
|
56
58
|
QUALITY_MODELS: {
|
|
57
59
|
fast: 'rt_hdr_alb_nrm_small',
|
|
58
|
-
balance: '
|
|
60
|
+
balance: 'rt_hdr_calb_cnrm',
|
|
59
61
|
high: 'rt_hdr_calb_cnrm_large'
|
|
60
62
|
},
|
|
61
63
|
DEFAULT_OPTIONS: {
|
|
62
64
|
enableOIDN: true,
|
|
63
65
|
oidnQuality: 'fast',
|
|
64
66
|
debugGbufferMaps: true,
|
|
65
|
-
tileSize
|
|
67
|
+
// Tiles run at tileSize + 2*overlap (96px, 112 for _large), so small tiles pay that padding
|
|
68
|
+
// every tile: 256 puts 3.06x the pixels through the UNet at 512². Measured 3.64x faster.
|
|
69
|
+
tileSize: 512
|
|
66
70
|
}
|
|
67
71
|
};
|
|
68
72
|
|
|
73
|
+
// Stands in for OIDN's inputScale, which oidn-web only applies on its CPU path — the GPUBuffer
|
|
74
|
+
// path leaves the uniform at 1.0. Equivalent because oidn-web does PUForward(col*inputScale) in
|
|
75
|
+
// and PUInverse(..)/inputScale out, so pre-multiplying here and dividing it back out of the
|
|
76
|
+
// output exposure round-trips. Alpha is preserved for _cacheInputAlpha.
|
|
77
|
+
const COLOR_SCALE_WGSL = /* wgsl */`
|
|
78
|
+
@group(0) @binding(0) var<storage, read_write> col: array<vec4<f32>>;
|
|
79
|
+
@group(0) @binding(1) var<uniform> scale: f32;
|
|
80
|
+
|
|
81
|
+
@compute @workgroup_size(64)
|
|
82
|
+
fn main( @builtin(global_invocation_id) gid: vec3<u32>,
|
|
83
|
+
@builtin(num_workgroups) nwg: vec3<u32> ) {
|
|
84
|
+
let i = gid.y * nwg.x * 64u + gid.x;
|
|
85
|
+
if ( i >= arrayLength( &col ) ) { return; }
|
|
86
|
+
let c = col[ i ];
|
|
87
|
+
col[ i ] = vec4<f32>( c.xyz * scale, c.w );
|
|
88
|
+
}
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
const SCALE_WG_SIZE = 64;
|
|
92
|
+
// 1D would exceed maxComputeWorkgroupsPerDimension (65535) past ~2048², so the dispatch is 2D.
|
|
93
|
+
const SCALE_MAX_WG_X = 32768;
|
|
94
|
+
|
|
95
|
+
// OIDN's autoexposure, mirroring oidn-web's avgLogLum. Whole-frame, so it cannot seam per tile.
|
|
96
|
+
const OIDN_AUTOEXPOSURE_KEY = 0.18;
|
|
97
|
+
|
|
98
|
+
function oidnInputScale( f32, pixelCount ) {
|
|
99
|
+
|
|
100
|
+
let logSum = 0;
|
|
101
|
+
for ( let i = 0; i < pixelCount; i ++ ) {
|
|
102
|
+
|
|
103
|
+
const o = i * 4;
|
|
104
|
+
const lum = 0.212671 * f32[ o ] + 0.71516 * f32[ o + 1 ] + 0.072169 * f32[ o + 2 ];
|
|
105
|
+
logSum += Math.log2( Math.max( lum, 0 ) + 0.0001 );
|
|
106
|
+
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const scale = OIDN_AUTOEXPOSURE_KEY / Math.pow( 2, logSum / pixelCount );
|
|
110
|
+
return Number.isFinite( scale ) && scale > 0 ? scale : 1.0;
|
|
111
|
+
|
|
112
|
+
}
|
|
113
|
+
|
|
69
114
|
export class OIDNDenoiser extends EventDispatcher {
|
|
70
115
|
|
|
71
116
|
constructor( output, renderer, scene, camera, options = {} ) {
|
|
@@ -110,6 +155,11 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
110
155
|
// order guarantees the overwrites are serialized).
|
|
111
156
|
this._gpuInputPadBuffer = null;
|
|
112
157
|
this._gpuInputPaddedRowBytes = 0;
|
|
158
|
+
// 1.0 until the first readback.
|
|
159
|
+
this._colorScalePipeline = null;
|
|
160
|
+
this._colorScaleBindGroup = null;
|
|
161
|
+
this._colorScaleUniform = null;
|
|
162
|
+
this._oidnInputScale = 1.0;
|
|
113
163
|
// Pooled MAP_READ staging buffer for _cacheInputAlpha. Only allocated
|
|
114
164
|
// when transparent-background readback is used, destroyed on resolution
|
|
115
165
|
// change or dispose. Same spirit as r184's ReadbackBuffer — we can't use
|
|
@@ -130,6 +180,8 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
130
180
|
this.quality = this.config.oidnQuality;
|
|
131
181
|
this.debugGbufferMaps = this.config.debugGbufferMaps;
|
|
132
182
|
this.tileSize = this.config.tileSize;
|
|
183
|
+
// tileSize actually baked into the live UNet, so a runtime change forces a rebuild.
|
|
184
|
+
this._activeTileSize = 0;
|
|
133
185
|
|
|
134
186
|
// State management
|
|
135
187
|
this.state = {
|
|
@@ -212,8 +264,8 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
212
264
|
this.state.isLoading = true;
|
|
213
265
|
const tzaUrl = this._generateTzaUrl();
|
|
214
266
|
|
|
215
|
-
//
|
|
216
|
-
if ( this.currentTZAUrl === tzaUrl && this.unet ) {
|
|
267
|
+
// maxTileSize is a constructor arg, so comparing the URL alone left tileSize changes inert.
|
|
268
|
+
if ( this.currentTZAUrl === tzaUrl && this._activeTileSize === this.tileSize && this.unet ) {
|
|
217
269
|
|
|
218
270
|
this.state.isLoading = false;
|
|
219
271
|
return;
|
|
@@ -251,6 +303,7 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
251
303
|
} );
|
|
252
304
|
|
|
253
305
|
this.currentTZAUrl = tzaUrl;
|
|
306
|
+
this._activeTileSize = this.tileSize;
|
|
254
307
|
this.dispatchEvent( { type: 'loaded' } );
|
|
255
308
|
log.debug( 'UNet weights loaded:', tzaUrl );
|
|
256
309
|
|
|
@@ -477,21 +530,18 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
477
530
|
|
|
478
531
|
copyTex( textures.color, this._gpuInputBuffers.color );
|
|
479
532
|
copyTex( textures.albedo, this._gpuInputBuffers.albedo );
|
|
533
|
+
// The normal stays [0,1]-encoded. This contradicts OIDN's API ("must be in the [-1,1]
|
|
534
|
+
// range") but is right for this port: OIDN's own getNormal (cpu_input_process.isph) does
|
|
535
|
+
// `value*0.5+0.5` before the network, and oidn-web omits that remap. Decoding to [-1,1]
|
|
536
|
+
// measured 0.878 -> 2.439 denoise ratio at 64 spp. Do not "fix" it.
|
|
480
537
|
copyTex( textures.normal, this._gpuInputBuffers.normal );
|
|
481
538
|
|
|
482
539
|
device.queue.submit( [ encoder.finish() ] );
|
|
483
540
|
|
|
484
|
-
//
|
|
485
|
-
|
|
486
|
-
if (
|
|
487
|
-
|
|
488
|
-
await this._cacheInputAlpha( device, width, height );
|
|
489
|
-
|
|
490
|
-
} else {
|
|
491
|
-
|
|
492
|
-
this._cachedAlpha = null;
|
|
493
|
-
|
|
494
|
-
}
|
|
541
|
+
// One readback serves the autoexposure scale and the alpha OIDN discards. Must precede the scale.
|
|
542
|
+
await this._readbackColor( device, width, height );
|
|
543
|
+
if ( globalThis.__OIDN_NO_INPUT_SCALE ) this._oidnInputScale = 1.0;
|
|
544
|
+
this._applyColorScale( device, width * height );
|
|
495
545
|
|
|
496
546
|
// Draw the current noisy frame as the base — denoised tiles paint on top progressively
|
|
497
547
|
this.ctx.drawImage( this.input, 0, 0, width, height );
|
|
@@ -571,6 +621,10 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
571
621
|
|
|
572
622
|
this._alphaReadbackBuffer?.destroy();
|
|
573
623
|
this._alphaReadbackMapped = false;
|
|
624
|
+
// Bind group holds the destroyed buffer; the pipeline is device-scoped and survives.
|
|
625
|
+
this._colorScaleBindGroup = null;
|
|
626
|
+
this._colorScaleUniform?.destroy();
|
|
627
|
+
this._colorScaleUniform = null;
|
|
574
628
|
this._gpuInputBuffers = { color: null, albedo: null, normal: null };
|
|
575
629
|
this._gpuInputPadBuffer = null;
|
|
576
630
|
this._gpuInputPaddedRowBytes = 0;
|
|
@@ -580,10 +634,10 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
580
634
|
}
|
|
581
635
|
|
|
582
636
|
/**
|
|
583
|
-
* Reads the
|
|
584
|
-
*
|
|
637
|
+
* Reads back the colour buffer for the autoexposure scale, plus alpha when transparent
|
|
638
|
+
* background is on (OIDN discards alpha).
|
|
585
639
|
*/
|
|
586
|
-
async
|
|
640
|
+
async _readbackColor( device, width, height ) {
|
|
587
641
|
|
|
588
642
|
const byteSize = width * height * 16; // rgba32float, tightly packed
|
|
589
643
|
|
|
@@ -620,13 +674,20 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
620
674
|
}
|
|
621
675
|
|
|
622
676
|
const f32 = new Float32Array( staging.getMappedRange() );
|
|
623
|
-
|
|
624
|
-
// Extract alpha channel as uint8 (pre-multiplied is not needed — alpha is 0 or 1)
|
|
625
677
|
const pixelCount = width * height;
|
|
626
|
-
const alpha = new Uint8Array( pixelCount );
|
|
627
|
-
for ( let i = 0; i < pixelCount; i ++ ) {
|
|
628
678
|
|
|
629
|
-
|
|
679
|
+
this._oidnInputScale = oidnInputScale( f32, pixelCount );
|
|
680
|
+
|
|
681
|
+
let alpha = null;
|
|
682
|
+
if ( this.getTransparentBackground() ) {
|
|
683
|
+
|
|
684
|
+
// Alpha as uint8; pre-multiplication is not needed — alpha is 0 or 1
|
|
685
|
+
alpha = new Uint8Array( pixelCount );
|
|
686
|
+
for ( let i = 0; i < pixelCount; i ++ ) {
|
|
687
|
+
|
|
688
|
+
alpha[ i ] = Math.min( Math.max( f32[ i * 4 + 3 ] * 255, 0 ), 255 ) | 0;
|
|
689
|
+
|
|
690
|
+
}
|
|
630
691
|
|
|
631
692
|
}
|
|
632
693
|
|
|
@@ -638,6 +699,81 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
638
699
|
|
|
639
700
|
}
|
|
640
701
|
|
|
702
|
+
/** Display exposure with the input scale divided back out — output arrives pre-multiplied. */
|
|
703
|
+
_outputExposure() {
|
|
704
|
+
|
|
705
|
+
return this.getExposure() / this._oidnInputScale;
|
|
706
|
+
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** Pre-multiplies the colour buffer by the autoexposure scale. */
|
|
710
|
+
_applyColorScale( device, pixelCount ) {
|
|
711
|
+
|
|
712
|
+
const scale = this._oidnInputScale;
|
|
713
|
+
if ( ! this._ensureColorScalePipeline( device ) ) return;
|
|
714
|
+
|
|
715
|
+
device.queue.writeBuffer( this._colorScaleUniform, 0, new Float32Array( [ scale ] ) );
|
|
716
|
+
|
|
717
|
+
const wgTotal = Math.ceil( pixelCount / SCALE_WG_SIZE );
|
|
718
|
+
const wgX = Math.min( wgTotal, SCALE_MAX_WG_X );
|
|
719
|
+
const wgY = Math.ceil( wgTotal / wgX );
|
|
720
|
+
|
|
721
|
+
const encoder = device.createCommandEncoder( { label: 'oidn-color-scale' } );
|
|
722
|
+
const pass = encoder.beginComputePass( { label: 'oidn-color-scale' } );
|
|
723
|
+
pass.setPipeline( this._colorScalePipeline );
|
|
724
|
+
pass.setBindGroup( 0, this._colorScaleBindGroup );
|
|
725
|
+
pass.dispatchWorkgroups( wgX, wgY, 1 );
|
|
726
|
+
pass.end();
|
|
727
|
+
device.queue.submit( [ encoder.finish() ] );
|
|
728
|
+
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/** @returns {GPUComputePipeline|null} */
|
|
732
|
+
_ensureColorScalePipeline( device ) {
|
|
733
|
+
|
|
734
|
+
const buffer = this._gpuInputBuffers.color;
|
|
735
|
+
if ( ! device || ! buffer ) return null;
|
|
736
|
+
|
|
737
|
+
if ( ! this._colorScalePipeline ) {
|
|
738
|
+
|
|
739
|
+
this._colorScalePipeline = device.createComputePipeline( {
|
|
740
|
+
label: 'oidn-color-scale',
|
|
741
|
+
layout: 'auto',
|
|
742
|
+
compute: {
|
|
743
|
+
module: device.createShaderModule( { label: 'oidn-color-scale', code: COLOR_SCALE_WGSL } ),
|
|
744
|
+
entryPoint: 'main'
|
|
745
|
+
}
|
|
746
|
+
} );
|
|
747
|
+
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if ( ! this._colorScaleUniform ) {
|
|
751
|
+
|
|
752
|
+
this._colorScaleUniform = device.createBuffer( {
|
|
753
|
+
label: 'oidn-color-scale-uniform',
|
|
754
|
+
size: 16, // min uniform binding size
|
|
755
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
756
|
+
} );
|
|
757
|
+
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if ( ! this._colorScaleBindGroup ) {
|
|
761
|
+
|
|
762
|
+
this._colorScaleBindGroup = device.createBindGroup( {
|
|
763
|
+
label: 'oidn-color-scale',
|
|
764
|
+
layout: this._colorScalePipeline.getBindGroupLayout( 0 ),
|
|
765
|
+
entries: [
|
|
766
|
+
{ binding: 0, resource: { buffer } },
|
|
767
|
+
{ binding: 1, resource: { buffer: this._colorScaleUniform } }
|
|
768
|
+
]
|
|
769
|
+
} );
|
|
770
|
+
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return this._colorScalePipeline;
|
|
774
|
+
|
|
775
|
+
}
|
|
776
|
+
|
|
641
777
|
/**
|
|
642
778
|
* Promise wrapper around tileExecute for the GPU path.
|
|
643
779
|
* Outputs a GPUBuffer — copied to a staging buffer then converted to ImageData for the 2D canvas.
|
|
@@ -754,7 +890,7 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
754
890
|
|
|
755
891
|
const f32 = new Float32Array( staging.getMappedRange() );
|
|
756
892
|
const tileImageData = new ImageData( clampedW, clampedH );
|
|
757
|
-
const exposure = this.
|
|
893
|
+
const exposure = this._outputExposure();
|
|
758
894
|
const saturation = this.getSaturation();
|
|
759
895
|
const tmFn = TONE_MAP_FNS.get( this.getToneMapping() ) || TONE_MAP_FNS.get( ACESFilmicToneMapping );
|
|
760
896
|
const alpha = this._cachedAlpha;
|
|
@@ -856,7 +992,7 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
856
992
|
const float32 = new Float32Array( stagingBuffer.getMappedRange() );
|
|
857
993
|
|
|
858
994
|
const imageData = new ImageData( width, height );
|
|
859
|
-
const exposure = this.
|
|
995
|
+
const exposure = this._outputExposure();
|
|
860
996
|
const saturation = this.getSaturation();
|
|
861
997
|
const tmFn = TONE_MAP_FNS.get( this.getToneMapping() ) || TONE_MAP_FNS.get( ACESFilmicToneMapping );
|
|
862
998
|
const alpha = this._cachedAlpha;
|
|
@@ -962,6 +1098,7 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
962
1098
|
// TFJS tears down the cached backend's buffers/textures.
|
|
963
1099
|
removeOidnTfjsBackend();
|
|
964
1100
|
this._destroyGPUInputBuffers();
|
|
1101
|
+
this._colorScalePipeline = null;
|
|
965
1102
|
|
|
966
1103
|
// Clean up DOM
|
|
967
1104
|
if ( this.output?.parentNode ) {
|
|
@@ -8,7 +8,7 @@ import { Ray, HitInfo, RayTracingMaterial, UVCache } from '../TSL/Struct.js';
|
|
|
8
8
|
import { traverseBVH } from '../TSL/BVHTraversal.js';
|
|
9
9
|
import { cameraRayDirection } from '../TSL/CameraRay.js';
|
|
10
10
|
import { getMaterial } from '../TSL/Common.js';
|
|
11
|
-
import { computeUVCache, processNormal, processBump, buildBucketTextureNodes, refreshBucketTextureNodes, setMaterialBucketTextures } from '../TSL/TextureSampling.js';
|
|
11
|
+
import { computeUVCache, processNormal, processBump, triangleUVTangent, buildBucketTextureNodes, refreshBucketTextureNodes, setMaterialBucketTextures } from '../TSL/TextureSampling.js';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* NormalDepth — primary-ray G-buffer for SVGF gates.
|
|
@@ -263,7 +263,16 @@ export class NormalDepth extends RenderStage {
|
|
|
263
263
|
getMaterial( hit.materialIndex, matStorage )
|
|
264
264
|
).toVar();
|
|
265
265
|
const uvCache = UVCache.wrap( computeUVCache( hit.uv, material ) ).toVar();
|
|
266
|
-
|
|
266
|
+
// Same UV tangent frame the Shade kernel uses, or the aux normal disagrees
|
|
267
|
+
// with the shading normal it is supposed to guide the denoiser with.
|
|
268
|
+
const uvTangent = vec4( 0.0 ).toVar();
|
|
269
|
+
If( material.normalMapIndex.greaterThanEqual( int( 0 ) ), () => {
|
|
270
|
+
|
|
271
|
+
uvTangent.assign( triangleUVTangent( triStorage, hit.triangleIndex, hit.normal, material.normalTransform ) );
|
|
272
|
+
|
|
273
|
+
} );
|
|
274
|
+
|
|
275
|
+
const mapped = processNormal( hit.normal, material, uvCache, uvTangent ).toVar();
|
|
267
276
|
shadingNormal.assign( processBump( mapped, material, uvCache ) );
|
|
268
277
|
|
|
269
278
|
} );
|
package/src/TSL/Debugger.js
CHANGED
|
@@ -202,7 +202,7 @@ export const TraceDebugMode = Fn( ( [
|
|
|
202
202
|
|
|
203
203
|
// Sample all textures to get emissive
|
|
204
204
|
const matSamples = MaterialSamples.wrap( sampleAllMaterialTextures(
|
|
205
|
-
material, hitInfo.uv, hitInfo.normal,
|
|
205
|
+
material, hitInfo.uv, hitInfo.normal, vec4( 0.0 ),
|
|
206
206
|
) ).toVar();
|
|
207
207
|
|
|
208
208
|
const emissiveColor = matSamples.emissive.toVar();
|
|
@@ -246,7 +246,7 @@ export const TraceDebugMode = Fn( ( [
|
|
|
246
246
|
|
|
247
247
|
// Get material-mapped normal (same as what's used in main shader)
|
|
248
248
|
const matSamples = MaterialSamples.wrap( sampleAllMaterialTextures(
|
|
249
|
-
material, hitInfo.uv, hitInfo.normal,
|
|
249
|
+
material, hitInfo.uv, hitInfo.normal, vec4( 0.0 ),
|
|
250
250
|
) ).toVar();
|
|
251
251
|
|
|
252
252
|
const worldNormal = normalize( matSamples.normal );
|
|
@@ -292,7 +292,7 @@ export const TraceDebugMode = Fn( ( [
|
|
|
292
292
|
const material = RayTracingMaterial.wrap( getMaterial( hitInfo.materialIndex, materialBuffer ) ).toVar();
|
|
293
293
|
|
|
294
294
|
const matSamples = MaterialSamples.wrap( sampleAllMaterialTextures(
|
|
295
|
-
material, hitInfo.uv, hitInfo.normal,
|
|
295
|
+
material, hitInfo.uv, hitInfo.normal, vec4( 0.0 ),
|
|
296
296
|
) ).toVar();
|
|
297
297
|
|
|
298
298
|
const objectColor = matSamples.albedo.rgb;
|
|
@@ -318,7 +318,7 @@ export const TraceDebugMode = Fn( ( [
|
|
|
318
318
|
// Get primary surface material
|
|
319
319
|
const material = RayTracingMaterial.wrap( getMaterial( hitInfo.materialIndex, materialBuffer ) ).toVar();
|
|
320
320
|
const matSamples = MaterialSamples.wrap( sampleAllMaterialTextures(
|
|
321
|
-
material, hitInfo.uv, hitInfo.normal,
|
|
321
|
+
material, hitInfo.uv, hitInfo.normal, vec4( 0.0 ),
|
|
322
322
|
) ).toVar();
|
|
323
323
|
|
|
324
324
|
const albedoA = matSamples.albedo.rgb;
|
|
@@ -364,7 +364,7 @@ export const TraceDebugMode = Fn( ( [
|
|
|
364
364
|
// Bounce hit surface B — evaluate its contribution
|
|
365
365
|
const materialB = RayTracingMaterial.wrap( getMaterial( bounceHit.materialIndex, materialBuffer ) ).toVar();
|
|
366
366
|
const matSamplesB = MaterialSamples.wrap( sampleAllMaterialTextures(
|
|
367
|
-
materialB, bounceHit.uv, bounceHit.normal,
|
|
367
|
+
materialB, bounceHit.uv, bounceHit.normal, vec4( 0.0 ),
|
|
368
368
|
) ).toVar();
|
|
369
369
|
|
|
370
370
|
// Emissive contribution from surface B
|
package/src/TSL/ShadeKernel.js
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
import { sampleEnvironment, sampleEquirectProbability, sampleEquirect, groundProjectedEnvDir } from './Environment.js';
|
|
18
18
|
import { getMaterial, powerHeuristic, balanceHeuristic, classifyMaterial, REC709_LUMINANCE_COEFFICIENTS, PI_INV, EPSILON, diffuseGroundMaterial } from './Common.js';
|
|
19
19
|
import { cosineWeightedSample } from './MaterialSampling.js';
|
|
20
|
-
import { sampleAllMaterialTextures, processAnisotropyMap, applyExtensionMaps, getTransformedUV } from './TextureSampling.js';
|
|
20
|
+
import { sampleAllMaterialTextures, processAnisotropyMap, applyExtensionMaps, getTransformedUV, triangleUVTangent } from './TextureSampling.js';
|
|
21
21
|
import { evaluateMaterialResponse } from './MaterialEvaluation.js';
|
|
22
22
|
import { calculateDirectLightingUnified, calculateMaterialPDF } from './LightsSampling.js';
|
|
23
23
|
import { traceShadowRay, calculateRayOffset } from './LightsDirect.js';
|
|
@@ -616,8 +616,17 @@ export function buildShadeKernel( params ) {
|
|
|
616
616
|
}
|
|
617
617
|
);
|
|
618
618
|
|
|
619
|
+
// Normal-map tangent frame. Only paid for when there is a normal map: the arbitrary
|
|
620
|
+
// cross(up, N) fallback inside processNormal is what runs otherwise.
|
|
621
|
+
const uvTangent = vec4( 0.0 ).toVar();
|
|
622
|
+
If( material.normalMapIndex.greaterThanEqual( int( 0 ) ), () => {
|
|
623
|
+
|
|
624
|
+
uvTangent.assign( triangleUVTangent( triangleBuffer, int( hitTriIdx ), N, material.normalTransform ) );
|
|
625
|
+
|
|
626
|
+
} );
|
|
627
|
+
|
|
619
628
|
const matSamples = MaterialSamples.wrap( sampleAllMaterialTextures(
|
|
620
|
-
material, samplingUV, N,
|
|
629
|
+
material, samplingUV, N, uvTangent,
|
|
621
630
|
) ).toVar();
|
|
622
631
|
|
|
623
632
|
// DDFA: snapshot the UNCLAMPED roughness (before the min-0.05 clamp below) for the specular↔diffuse
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fn, wgslFn, float, vec2, vec3, vec4, int, If, normalize, cross, abs, atan, mix, clamp, texture, textureSize } from 'three/tsl';
|
|
1
|
+
import { Fn, wgslFn, float, vec2, vec3, vec4, int, If, normalize, cross, dot, length, sign, abs, atan, mix, clamp, texture, textureSize } from 'three/tsl';
|
|
2
2
|
import { DataArrayTexture, LinearFilter } from 'three';
|
|
3
3
|
|
|
4
4
|
import {
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
MaterialSamples,
|
|
7
7
|
ExtMapResult,
|
|
8
8
|
} from './Struct.js';
|
|
9
|
+
import { getDatafromStorageBuffer } from './Common.js';
|
|
9
10
|
import { TEXTURE_CONSTANTS } from '../EngineDefaults.js';
|
|
10
11
|
|
|
11
12
|
// ================================================================================
|
|
@@ -382,7 +383,80 @@ export const processMetalnessRoughness = Fn( ( [ material, uvCache ] ) => {
|
|
|
382
383
|
|
|
383
384
|
} );
|
|
384
385
|
|
|
385
|
-
|
|
386
|
+
// (t[1][1], t[0][1]) — the row of the inverse 2x2 linear part that maps dP/du onto dP/du',
|
|
387
|
+
// up to the determinant, which drops out under the normalize that follows.
|
|
388
|
+
const uvTransformJacobian = /*@__PURE__*/ wgslFn( `
|
|
389
|
+
fn uvTransformJacobian( t: mat3x3f ) -> vec2f {
|
|
390
|
+
return vec2f( t[1][1], t[0][1] );
|
|
391
|
+
}
|
|
392
|
+
` );
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Build the UV tangent for one triangle, as vec4( T.xyz, handedness ).
|
|
396
|
+
*
|
|
397
|
+
* Returned in the *transformed* UV space of `transform`, since that is the space the normal
|
|
398
|
+
* map is sampled in — the inverse of the transform's 2x2 linear part maps dP/du back onto
|
|
399
|
+
* dP/du'. A pure repeat/scale leaves the direction alone; a rotated transform does not.
|
|
400
|
+
* w == 0 signals a degenerate parameterisation, which processNormal treats as "no frame".
|
|
401
|
+
*
|
|
402
|
+
* @param {Node} triangleBuffer packed triangle storage (8 vec4 stride)
|
|
403
|
+
* @param {Node} triIndex absolute triangle index
|
|
404
|
+
* @param {Node} geometryNormal shading-space normal, for the handedness test
|
|
405
|
+
* @param {Node} transform the normal map's mat3 UV transform
|
|
406
|
+
*/
|
|
407
|
+
export const triangleUVTangent = Fn( ( [ triangleBuffer, triIndex, geometryNormal, transform ] ) => {
|
|
408
|
+
|
|
409
|
+
const S = int( 8 );
|
|
410
|
+
const pA = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 0 ), S ).xyz;
|
|
411
|
+
const e1 = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 1 ), S ).xyz.sub( pA );
|
|
412
|
+
const e2 = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 2 ), S ).xyz.sub( pA );
|
|
413
|
+
|
|
414
|
+
const uvAB = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 6 ), S );
|
|
415
|
+
const uvC = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 7 ), S ).xy;
|
|
416
|
+
const d1 = uvAB.zw.sub( uvAB.xy );
|
|
417
|
+
const d2 = uvC.sub( uvAB.xy );
|
|
418
|
+
|
|
419
|
+
const det = d1.x.mul( d2.y ).sub( d1.y.mul( d2.x ) );
|
|
420
|
+
const result = vec4( 0.0 ).toVar();
|
|
421
|
+
|
|
422
|
+
If( abs( det ).greaterThan( 1e-12 ), () => {
|
|
423
|
+
|
|
424
|
+
const r = float( 1.0 ).div( det );
|
|
425
|
+
const T = e1.mul( d2.y ).sub( e2.mul( d1.y ) ).mul( r ).toVar();
|
|
426
|
+
const B = e2.mul( d1.x ).sub( e1.mul( d2.x ) ).mul( r ).toVar();
|
|
427
|
+
|
|
428
|
+
// Re-express in the map's transformed UV space (inverse of the 2x2 linear part).
|
|
429
|
+
const uvJ = uvTransformJacobian( { t: transform } );
|
|
430
|
+
const Tt = T.mul( uvJ.x ).sub( B.mul( uvJ.y ) ).toVar();
|
|
431
|
+
|
|
432
|
+
If( length( Tt ).greaterThan( 1e-12 ), () => {
|
|
433
|
+
|
|
434
|
+
T.assign( Tt );
|
|
435
|
+
|
|
436
|
+
} );
|
|
437
|
+
|
|
438
|
+
If( length( T ).greaterThan( 1e-12 ), () => {
|
|
439
|
+
|
|
440
|
+
result.assign( vec4( normalize( T ), sign( dot( cross( geometryNormal, T ), B ) ) ) );
|
|
441
|
+
|
|
442
|
+
} );
|
|
443
|
+
|
|
444
|
+
} );
|
|
445
|
+
|
|
446
|
+
return result;
|
|
447
|
+
|
|
448
|
+
} );
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Per-triangle tangent frame from the UV parameterisation.
|
|
452
|
+
*
|
|
453
|
+
* `uvTangent` is vec4( T.xyz, handedness ); w == 0 means "no usable frame" (degenerate or
|
|
454
|
+
* missing UVs) and falls back to the arbitrary cross(up, N) frame. A frame unrelated to the
|
|
455
|
+
* texture's U axis rotates the whole perturbation: on a vertical fluted-glass panel the
|
|
456
|
+
* arbitrary frame comes out 90 deg off, so the rib normals tilt up/down instead of across
|
|
457
|
+
* the ribs.
|
|
458
|
+
*/
|
|
459
|
+
export const processNormal = Fn( ( [ geometryNormal, material, uvCache, uvTangent ] ) => {
|
|
386
460
|
|
|
387
461
|
const result = geometryNormal.toVar();
|
|
388
462
|
|
|
@@ -393,10 +467,25 @@ export const processNormal = Fn( ( [ geometryNormal, material, uvCache ] ) => {
|
|
|
393
467
|
normalMap.x.mulAssign( material.normalScale.x );
|
|
394
468
|
normalMap.y.assign( normalMap.y.negate().mul( material.normalScale.x ) );
|
|
395
469
|
|
|
396
|
-
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
470
|
+
const tangent = vec3( 0.0 ).toVar();
|
|
471
|
+
const handedness = float( 1.0 ).toVar();
|
|
472
|
+
|
|
473
|
+
// Gram-Schmidt against the interpolated normal, which is not exactly triangle-planar.
|
|
474
|
+
const perp = uvTangent.xyz.sub( geometryNormal.mul( dot( geometryNormal, uvTangent.xyz ) ) ).toVar();
|
|
475
|
+
|
|
476
|
+
If( abs( uvTangent.w ).greaterThan( 0.5 ).and( length( perp ).greaterThan( 1e-6 ) ), () => {
|
|
477
|
+
|
|
478
|
+
tangent.assign( normalize( perp ) );
|
|
479
|
+
handedness.assign( uvTangent.w );
|
|
480
|
+
|
|
481
|
+
} ).Else( () => {
|
|
482
|
+
|
|
483
|
+
const up = abs( geometryNormal.z ).lessThan( 0.999 ).select( vec3( 0.0, 0.0, 1.0 ), vec3( 1.0, 0.0, 0.0 ) );
|
|
484
|
+
tangent.assign( normalize( cross( up, geometryNormal ) ) );
|
|
485
|
+
|
|
486
|
+
} );
|
|
487
|
+
|
|
488
|
+
const bitangent = cross( geometryNormal, tangent ).mul( handedness );
|
|
400
489
|
|
|
401
490
|
result.assign( normalize(
|
|
402
491
|
tangent.mul( normalMap.x ).add( bitangent.mul( normalMap.y ) ).add( geometryNormal.mul( normalMap.z ) )
|
|
@@ -563,7 +652,7 @@ export const processEmissive = Fn( ( [ material, uvCache ] ) => {
|
|
|
563
652
|
// MAIN BATCHED SAMPLING FUNCTION
|
|
564
653
|
// ================================================================================
|
|
565
654
|
|
|
566
|
-
export const sampleAllMaterialTextures = Fn( ( [ material, uv, geometryNormal ] ) => {
|
|
655
|
+
export const sampleAllMaterialTextures = Fn( ( [ material, uv, geometryNormal, uvTangent ] ) => {
|
|
567
656
|
|
|
568
657
|
const albedo = vec4( 0.0 ).toVar();
|
|
569
658
|
const emissive = vec3( 0.0 ).toVar();
|
|
@@ -591,7 +680,7 @@ export const sampleAllMaterialTextures = Fn( ( [ material, uv, geometryNormal ]
|
|
|
591
680
|
metalness.assign( metalRough.x );
|
|
592
681
|
roughness.assign( metalRough.y );
|
|
593
682
|
|
|
594
|
-
const currentNormal = processNormal( geometryNormal, material, uvCache ).toVar();
|
|
683
|
+
const currentNormal = processNormal( geometryNormal, material, uvCache, uvTangent ).toVar();
|
|
595
684
|
normal.assign( processBump( currentNormal, material, uvCache ) );
|
|
596
685
|
|
|
597
686
|
emissive.assign( processEmissive( material, uvCache ) );
|