@@ -9,8 +9,8 @@ import tgpu, { common, d, std } from 'typegpu';
99import { defineControls } from '../../common/defineControls.ts' ;
1010
1111/**
12- * Pipeline: camera → luminance (optional separable Hann window) → `encodeForward` → radial low-pass on the
13- * spectrum buffer → optional `encodeInverse` (spatial) or log-magnitude spectrum colored in **Oklab**
12+ * Pipeline: camera → luminance (optional separable Hann window) → `encodeForward` → radial low- or high- pass on the
13+ * spectrum buffer (independent radial low- and high-pass masks, multiplied) → optional `encodeInverse` (spatial) or log-magnitude spectrum colored in **Oklab**
1414 * (lightness from magnitude, hue from complex phase via `a,b`).
1515 * Line FFT: **radix-4 (default)** (faster Stockham-style radix-4 + optional radix-2 tail) or **radix-2**
1616 * (pure Stockham radix-2). One compute pass chains fill, FFT, filter, inverse FFT, and spatial/mag; then a
@@ -144,8 +144,10 @@ const filterParamsType = d.struct({
144144 padWMask : d . u32 ,
145145 padHLog2 : d . u32 ,
146146 padHMask : d . u32 ,
147- /** 1 = pass all bins; 0 = DC only. Scales max toroidal radius √(hw²+hh²), hw = padW/2, hh = padH/2. */
148- cutoffRadius : d . f32 ,
147+ /** Normalized low-pass radius vs max toroidal √(hw²+hh²); keep r ≤ cutoff. */
148+ lowPassCutoff : d . f32 ,
149+ /** Normalized high-pass inner radius; keep r > cutoff (DC at r = 0 is removed when cutoff = 0). */
150+ highPassCutoff : d . f32 ,
149151 /** 1 when `Fft2d.skipFinalTranspose`: spectrum is `W×H` row-major with stride `padH` (`r*padH+c`), not `y*padW+x`. */
150152 swapSpectrumAxes : d . u32 ,
151153} ) ;
@@ -193,17 +195,21 @@ const filterKernel = tgpu.computeFn({
193195 const hw = d . f32 ( halfW ) ;
194196 const hh = d . f32 ( halfH ) ;
195197 const rMax = std . sqrt ( hw * hw + hh * hh ) ;
196- const cutoff = filterLayout . $ . params . cutoffRadius * rMax ;
197198 const r = std . sqrt ( r2 ) ;
198- const mask = std . select ( d . f32 ( 0 ) , d . f32 ( 1 ) , r <= cutoff ) ;
199+ const lowC = filterLayout . $ . params . lowPassCutoff * rMax ;
200+ const highC = filterLayout . $ . params . highPassCutoff * rMax ;
201+ 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 ) ;
203+ const mask = lowMask * highMask ;
199204 const c = filterLayout . $ . spectrum [ tid ] ;
200205 filterLayout . $ . spectrum [ tid ] = d . vec2f ( c . x * mask , c . y * mask ) ;
201206} ) ;
202207
203208const magParamsType = d . struct ( {
204209 padW : d . u32 ,
205210 padH : d . u32 ,
206- gain : d . f32 ,
211+ /** Exposure in stops (EV): brightness scales as `exp2(exposure)` on top of the neutral log stretch. */
212+ exposure : d . f32 ,
207213 padWLog2 : d . u32 ,
208214 padWMask : d . u32 ,
209215 swapSpectrumAxes : d . u32 ,
@@ -250,7 +256,9 @@ const magKernel = tgpu.computeFn({
250256 ) ;
251257 const cShift = magLayout . $ . spectrum [ srcTid ] ;
252258 const len = std . sqrt ( cShift . x * cShift . x + cShift . y * cShift . y ) ;
253- const logv = std . log ( 1.0 + len ) * magLayout . $ . params . gain ;
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 ) ;
254262 const cv = std . clamp ( logv , 0.0 , 1.0 ) ;
255263 /** `cv` from log-magnitude; L ∈ [0.04, 1]; chroma → 0 at max `cv` so peaks go neutral white. */
256264 const L = 0.04 + cv * ( 1.0 - 0.04 ) ;
@@ -269,6 +277,8 @@ const spatialParamsType = d.struct({
269277 padWMask : d . u32 ,
270278 /** `1 / (padW * padH)` — unnormalized inverse scaling. */
271279 invSize : d . f32 ,
280+ /** Same EV as spectrum: linear sample is multiplied by `exp2(exposure)` before clamp. */
281+ exposure : d . f32 ,
272282} ) ;
273283
274284const spatialLayout = tgpu . bindGroupLayout ( {
@@ -297,7 +307,7 @@ const spatialKernel = tgpu.computeFn({
297307
298308 const c = spatialLayout . $ . spectrum [ tid ] ;
299309 const inv = spatialLayout . $ . params . invSize ;
300- const g = std . clamp ( c . x * inv , 0.0 , 1.0 ) ;
310+ const g = std . clamp ( c . x * inv * std . exp2 ( spatialLayout . $ . params . exposure ) , 0.0 , 1.0 ) ;
301311 const padWLog2 = spatialLayout . $ . params . padWLog2 ;
302312 const padWMask = spatialLayout . $ . params . padWMask ;
303313 const x = tid & padWMask ;
@@ -476,9 +486,12 @@ const renderPipeline = root.createRenderPipeline({
476486
477487/** When true: after forward FFT, run inverse and show grayscale spatial reconstruction. When false: show log-magnitude spectrum (forward only). */
478488let applyInverseFft = false ;
479- let gainValue = 0.2 ;
480- /** Normalized low-pass cutoff vs max toroidal radius (1 = no filtering). */
481- let cutoffRadiusNorm = 1 ;
489+ /** Exposure in stops (EV), −4…+4; 0 is neutral vs the legacy spectrum scale. */
490+ let exposureEv = 0 ;
491+ /** Normalized low-pass radius vs max toroidal radius (1 = no attenuation). */
492+ let lowPassCutoffNorm = 1 ;
493+ /** Normalized high-pass inner radius (0 = no AC attenuation from this term). */
494+ let highPassCutoffNorm = 0 ;
482495/** Separable Hann window on camera ROI before FFT (reduces periodic-boundary cross in spectrum). */
483496let applyEdgeWindow = false ;
484497let lastFillUniformKey = '' ;
@@ -723,20 +736,20 @@ function processVideoFrame(_: number, metadata: VideoFrameCallbackMetadata) {
723736 } ) ;
724737 }
725738
726- const magUniformKey = `${ padW } x${ padH } x${ gainValue } x${ activeFft . skipFinalTranspose } ` ;
739+ const magUniformKey = `${ padW } x${ padH } x${ exposureEv } x${ activeFft . skipFinalTranspose } ` ;
727740 if ( magUniformKey !== lastMagUniformKey ) {
728741 lastMagUniformKey = magUniformKey ;
729742 magParams . write ( {
730743 padW,
731744 padH,
732- gain : gainValue ,
745+ exposure : exposureEv ,
733746 padWLog2,
734747 padWMask : padW - 1 ,
735748 swapSpectrumAxes : activeFft . skipFinalTranspose ? 1 : 0 ,
736749 } ) ;
737750 }
738751
739- const spatialUniformKey = `${ padW } x${ padH } x${ gainValue } ` ;
752+ const spatialUniformKey = `${ padW } x${ padH } x${ exposureEv } ` ;
740753 if ( spatialUniformKey !== lastSpatialUniformKey ) {
741754 lastSpatialUniformKey = spatialUniformKey ;
742755 spatialParams . write ( {
@@ -745,10 +758,11 @@ function processVideoFrame(_: number, metadata: VideoFrameCallbackMetadata) {
745758 padWLog2,
746759 padWMask : padW - 1 ,
747760 invSize : 1 / ( padW * padH ) ,
761+ exposure : exposureEv ,
748762 } ) ;
749763 }
750764
751- const filterUniformKey = `${ padW } x${ padH } x${ cutoffRadiusNorm } x${ activeFft . skipFinalTranspose } ` ;
765+ const filterUniformKey = `${ padW } x${ padH } x${ lowPassCutoffNorm } x ${ highPassCutoffNorm } x${ activeFft . skipFinalTranspose } ` ;
752766 if ( filterUniformKey !== lastFilterUniformKey ) {
753767 lastFilterUniformKey = filterUniformKey ;
754768 filterParams . write ( {
@@ -758,7 +772,8 @@ function processVideoFrame(_: number, metadata: VideoFrameCallbackMetadata) {
758772 padWMask : padW - 1 ,
759773 padHLog2 : log2Int ( padH ) ,
760774 padHMask : padH - 1 ,
761- cutoffRadius : cutoffRadiusNorm ,
775+ lowPassCutoff : lowPassCutoffNorm ,
776+ highPassCutoff : highPassCutoffNorm ,
762777 swapSpectrumAxes : activeFft . skipFinalTranspose ? 1 : 0 ,
763778 } ) ;
764779 }
@@ -851,24 +866,34 @@ export const controls = defineControls({
851866 lineFftMode = value as LineFftMode ;
852867 } ,
853868 } ,
854- gain : {
855- initial : gainValue ,
856- min : 0.01 ,
857- max : 0.45 ,
858- step : 0.005 ,
869+ exposure : {
870+ initial : exposureEv ,
871+ min : - 4 ,
872+ max : 4 ,
873+ step : 0.05 ,
859874 onSliderChange : ( value ) => {
860- gainValue = value ;
875+ exposureEv = value ;
861876 lastMagUniformKey = '' ;
862877 lastSpatialUniformKey = '' ;
863878 } ,
864879 } ,
865- cutoffRadius : {
866- initial : 1 ,
880+ lowPassCutoff : {
881+ initial : lowPassCutoffNorm ,
867882 min : 0 ,
868883 max : 1 ,
869884 step : 0.01 ,
870885 onSliderChange : ( value ) => {
871- cutoffRadiusNorm = value ;
886+ lowPassCutoffNorm = value ;
887+ lastFilterUniformKey = '' ;
888+ } ,
889+ } ,
890+ highPassCutoff : {
891+ initial : highPassCutoffNorm ,
892+ min : 0 ,
893+ max : 1 ,
894+ step : 0.01 ,
895+ onSliderChange : ( value ) => {
896+ highPassCutoffNorm = value ;
872897 lastFilterUniformKey = '' ;
873898 } ,
874899 } ,
0 commit comments