Skip to content

Commit 642c171

Browse files
committed
Fix precision issues, use operators
1 parent d10c30a commit 642c171

14 files changed

Lines changed: 694 additions & 203 deletions

File tree

apps/typegpu-docs/src/examples/image-processing/camera-fft/index.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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({
208210
const 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;
490498
let exposureEv = 0;
491499
/** Normalized low-pass radius vs max toroidal radius (1 = no attenuation). */
492500
let 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). */
494502
let highPassCutoffNorm = 0;
495503
/** Separable Hann window on camera ROI before FFT (reduces periodic-boundary cross in spectrum). */
496504
let 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

Comments
 (0)