@@ -146,7 +146,7 @@ const filterParamsType = d.struct({
146146 padHMask : d . u32 ,
147147 /** Normalized low-pass radius vs max toroidal √(hw²+hh²); keep r ≤ cutoff. */
148148 lowPassCutoff : d . f32 ,
149- /** Normalized high-pass inner radius; keep r > cutoff (DC at r = 0 is removed when cutoff = 0 ). */
149+ /** Normalized high-pass inner radius; keep ` r > cutoff`. `0` = high-pass **off** (do not attenuate DC or low r ). */
150150 highPassCutoff : d . f32 ,
151151 /** 1 when `Fft2d.skipFinalTranspose`: spectrum is `W×H` row-major with stride `padH` (`r*padH+c`), not `y*padW+x`. */
152152 swapSpectrumAxes : d . u32 ,
@@ -199,7 +199,9 @@ const filterKernel = tgpu.computeFn({
199199 const lowC = filterLayout . $ . params . lowPassCutoff * rMax ;
200200 const highC = filterLayout . $ . params . highPassCutoff * rMax ;
201201 const lowMask = std . select ( d . f32 ( 0 ) , d . f32 ( 1 ) , r <= lowC ) ;
202- const highMask = std . select ( d . f32 ( 0 ) , d . f32 ( 1 ) , r > highC ) ;
202+ const highPassOff = filterLayout . $ . params . highPassCutoff <= d . f32 ( 0 ) ;
203+ const highMaskInner = std . select ( d . f32 ( 0 ) , d . f32 ( 1 ) , r > highC ) ;
204+ const highMask = std . select ( highMaskInner , d . f32 ( 1 ) , highPassOff ) ;
203205 const mask = lowMask * highMask ;
204206 const c = filterLayout . $ . spectrum [ tid ] ;
205207 filterLayout . $ . spectrum [ tid ] = d . vec2f ( c . x * mask , c . y * mask ) ;
@@ -208,6 +210,8 @@ const filterKernel = tgpu.computeFn({
208210const magParamsType = d . struct ( {
209211 padW : d . u32 ,
210212 padH : d . u32 ,
213+ /** Same float as `Fft2d.spectrumToUnnormalizedScale` from `createFft2d` (orthonormal → unnormalized magnitude). */
214+ spectrumToUnnormalizedScale : d . f32 ,
211215 /** Exposure in stops (EV): brightness scales as `exp2(exposure)` on top of the neutral log stretch. */
212216 exposure : d . f32 ,
213217 padWLog2 : d . u32 ,
@@ -255,16 +259,20 @@ const magKernel = tgpu.computeFn({
255259 magLayout . $ . params . swapSpectrumAxes !== d . u32 ( 0 ) ,
256260 ) ;
257261 const cShift = magLayout . $ . spectrum [ srcTid ] ;
258- const len = std . sqrt ( cShift . x * cShift . x + cShift . y * cShift . y ) ;
259- /** Neutral at `exposure = 0` matches the former default `log(1+|·|) * 0.2` scale. */
260- const logv =
261- std . log ( 1.0 + len ) * 0.2 * std . exp2 ( magLayout . $ . params . exposure ) ;
262+ /** Raw magnitude from orthonormal `encodeForward` (≈ unnormalized / √(padW·padH)). */
263+ const lenRaw = std . sqrt ( cShift . x * cShift . x + cShift . y * cShift . y ) ;
264+ /**
265+ * Stretch for log display matches the pre–orthonormal spectrum brightness: neutral `log(1+|·|) * 0.2`
266+ * was tuned on unnormalized DFT magnitudes.
267+ */
268+ const lenDisp = lenRaw * std . sqrt ( d . f32 ( padW ) * d . f32 ( padH ) ) ;
269+ const logv = std . log ( 1.0 + lenDisp ) * 0.2 * std . exp2 ( magLayout . $ . params . exposure ) ;
262270 const cv = std . clamp ( logv , 0.0 , 1.0 ) ;
263271 /** `cv` from log-magnitude; L ∈ [0.04, 1]; chroma → 0 at max `cv` so peaks go neutral white. */
264272 const L = 0.04 + cv * ( 1.0 - 0.04 ) ;
265273 const chroma = cv * ( 1.0 - cv ) * 0.32 ;
266274 /** Oklab a,b = chroma × unit phase — same as chroma·(cos θ, sin θ) with θ = atan2(im, re), without trig. */
267- const invLen = 1.0 / std . max ( len , 1e-20 ) ;
275+ const invLen = 1.0 / std . max ( lenRaw , 1e-20 ) ;
268276 const lab = d . vec3f ( L , chroma * invLen * cShift . x , chroma * invLen * cShift . y ) ;
269277 const rgb = oklabToLinearRgb ( oklabGamutClip . adaptiveL05 ( lab ) ) ;
270278 std . textureStore ( magLayout . $ . outTex , d . vec2u ( xLin , yLin ) , d . vec4f ( rgb , 1 ) ) ;
@@ -275,7 +283,7 @@ const spatialParamsType = d.struct({
275283 padH : d . u32 ,
276284 padWLog2 : d . u32 ,
277285 padWMask : d . u32 ,
278- /** `1 / (padW * padH)` — unnormalized inverse scaling . */
286+ /** Orthonormal inverse already reconstructs the original signal; this is `1` (kept for legacy compat) . */
279287 invSize : d . f32 ,
280288 /** Same EV as spectrum: linear sample is multiplied by `exp2(exposure)` before clamp. */
281289 exposure : d . f32 ,
@@ -490,7 +498,7 @@ let applyInverseFft = false;
490498let exposureEv = 0 ;
491499/** Normalized low-pass radius vs max toroidal radius (1 = no attenuation). */
492500let lowPassCutoffNorm = 1 ;
493- /** Normalized high-pass inner radius (0 = no AC attenuation from this term ). */
501+ /** Normalized high-pass inner radius (`0` = high-pass off; spectrum passes through to low-pass only ). */
494502let highPassCutoffNorm = 0 ;
495503/** Separable Hann window on camera ROI before FFT (reduces periodic-boundary cross in spectrum). */
496504let applyEdgeWindow = false ;
@@ -742,6 +750,7 @@ function processVideoFrame(_: number, metadata: VideoFrameCallbackMetadata) {
742750 magParams . write ( {
743751 padW,
744752 padH,
753+ spectrumToUnnormalizedScale : activeFft . spectrumToUnnormalizedScale ,
745754 exposure : exposureEv ,
746755 padWLog2,
747756 padWMask : padW - 1 ,
@@ -757,7 +766,7 @@ function processVideoFrame(_: number, metadata: VideoFrameCallbackMetadata) {
757766 padH,
758767 padWLog2,
759768 padWMask : padW - 1 ,
760- invSize : 1 / ( padW * padH ) ,
769+ invSize : 1 ,
761770 exposure : exposureEv ,
762771 } ) ;
763772 }
@@ -868,8 +877,8 @@ export const controls = defineControls({
868877 } ,
869878 exposure : {
870879 initial : exposureEv ,
871- min : - 4 ,
872- max : 4 ,
880+ min : - 8 ,
881+ max : 8 ,
873882 step : 0.05 ,
874883 onSliderChange : ( value ) => {
875884 exposureEv = value ;
0 commit comments