1- import { createFft2d , createStockhamRadix4LineStrategy , type Fft2d } from '@typegpu/fft' ;
1+ import { oklabGamutClipSlot , oklabToLinearRgb } from '@typegpu/color' ;
2+ import {
3+ createFft2d ,
4+ createStockhamRadix2LineStrategy ,
5+ createStockhamRadix4LineStrategy ,
6+ type Fft2d ,
7+ } from '@typegpu/fft' ;
28import tgpu , { common , d , std } from 'typegpu' ;
39import { defineControls } from '../../common/defineControls.ts' ;
410
511/**
612 * Pipeline: camera → luminance (optional separable Hann window) → `encodeForward` → radial low-pass on the
7- * spectrum buffer → optional `encodeInverse` (spatial) or log-magnitude spectrum.
8- * Radix-2 Stockham by default; optional radix-4 line strategy. One compute pass chains fill, FFT, filter,
9- * inverse FFT, and spatial/mag; then a render pass presents.
13+ * spectrum buffer → optional `encodeInverse` (spatial) or log-magnitude spectrum colored in **Oklab**
14+ * (lightness from magnitude, hue from complex phase via `a,b`).
15+ * Line FFT: **radix-4 (default)** (faster Stockham-style radix-4 + optional radix-2 tail) or **radix-2**
16+ * (pure Stockham radix-2). One compute pass chains fill, FFT, filter, inverse FFT, and spatial/mag; then a
17+ * render pass presents.
1018 */
1119
1220const WORKGROUP = 256 ;
@@ -48,9 +56,10 @@ function decomposeWorkgroups(total: number): [number, number, number] {
4856/** Max longer side of the camera ROI before downscale; FFT pad is `nextPowerOf2(effW)×nextPowerOf2(effH)`. */
4957let fftMaxSide = 1024 ;
5058
51- type LineFftMode = 'default' | 'radix4' ;
59+ /** UI select values — must match `lineFft` control `options`. */
60+ type LineFftMode = 'radix-4 (default)' | 'radix-2' ;
5261
53- let lineFftMode : LineFftMode = 'default' ;
62+ let lineFftMode : LineFftMode = 'radix-4 ( default) ' ;
5463/** Tracks which line mode the current `fft` was built with (invalidate on change). */
5564let fftLineFftMode : LineFftMode | undefined ;
5665
@@ -213,6 +222,7 @@ const magKernel = tgpu.computeFn({
213222 numWorkgroups : d . builtin . numWorkgroups ,
214223 } ,
215224} ) ( ( input ) => {
225+ 'use gpu' ;
216226 const wg = d . u32 ( WORKGROUP ) ;
217227 const spanX = input . numWorkgroups . x * wg ;
218228 const spanY = input . numWorkgroups . y * spanX ;
@@ -242,7 +252,14 @@ const magKernel = tgpu.computeFn({
242252 const len = std . sqrt ( cShift . x * cShift . x + cShift . y * cShift . y ) ;
243253 const logv = std . log ( 1.0 + len ) * magLayout . $ . params . gain ;
244254 const cv = std . clamp ( logv , 0.0 , 1.0 ) ;
245- std . textureStore ( magLayout . $ . outTex , d . vec2u ( xLin , yLin ) , d . vec4f ( cv , cv , cv , 1 ) ) ;
255+ /** Perceptual lightness from log-magnitude; chroma scales with magnitude; phase → hue in the `a,b` plane. */
256+ const eps = 1e-8 ;
257+ const hue = std . atan2 ( cShift . y , cShift . x ) ;
258+ const chroma = std . select ( cv * 0.16 , d . f32 ( 0 ) , len < eps ) ;
259+ const L = 0.04 + cv * 0.88 ;
260+ const lab = d . vec3f ( L , chroma * std . cos ( hue ) , chroma * std . sin ( hue ) ) ;
261+ const rgb = oklabToLinearRgb ( oklabGamutClipSlot . $ ( lab ) ) ;
262+ std . textureStore ( magLayout . $ . outTex , d . vec2u ( xLin , yLin ) , d . vec4f ( rgb , 1 ) ) ;
246263} ) ;
247264
248265const spatialParamsType = d . struct ( {
@@ -368,8 +385,8 @@ if (navigator.mediaDevices.getUserMedia) {
368385 video . srcObject = await navigator . mediaDevices . getUserMedia ( {
369386 video : {
370387 facingMode : 'user' ,
371- width : { ideal : 1280 } ,
372- height : { ideal : 720 } ,
388+ width : { ideal : 1920 } ,
389+ height : { ideal : 1080 } ,
373390 frameRate : { ideal : 60 } ,
374391 } ,
375392 } ) ;
@@ -458,8 +475,8 @@ const renderPipeline = root.createRenderPipeline({
458475} ) ;
459476
460477/** When true: after forward FFT, run inverse and show grayscale spatial reconstruction. When false: show log-magnitude spectrum (forward only). */
461- let applyInverseFft = true ;
462- let gainValue = 0.12 ;
478+ let applyInverseFft = false ;
479+ let gainValue = 0.2 ;
463480/** Normalized low-pass cutoff vs max toroidal radius (1 = no filtering). */
464481let cutoffRadiusNorm = 1 ;
465482/** Separable Hann window on camera ROI before FFT (reduces periodic-boundary cross in spectrum). */
@@ -535,12 +552,13 @@ function ensureResources(frameW: number, frameH: number) {
535552 destroyFftBlock ( ) ;
536553 padW = nextPadW ;
537554 padH = nextPadH ;
538- const lineFactory = lineFftMode === 'radix4' ? createStockhamRadix4LineStrategy : undefined ;
555+ const lineFftStrategyFactory =
556+ lineFftMode === 'radix-2' ? createStockhamRadix2LineStrategy : createStockhamRadix4LineStrategy ;
539557 fft = createFft2d ( root , {
540558 width : padW ,
541559 height : padH ,
542560 skipFinalTranspose : SKIP_FINAL_FFT_TRANSPOSE ,
543- ... ( lineFactory !== undefined ? { lineFftStrategyFactory : lineFactory } : { } ) ,
561+ lineFftStrategyFactory,
544562 } ) ;
545563 fftLineFftMode = lineFftMode ;
546564 lastMagUniformKey = '' ;
@@ -785,7 +803,7 @@ videoFrameCallbackId = video.requestVideoFrameCallback(processVideoFrame);
785803
786804export const controls = defineControls ( {
787805 inverseFft : {
788- initial : true ,
806+ initial : false ,
789807 onToggleChange : ( value ) => {
790808 applyInverseFft = value ;
791809 } ,
@@ -805,7 +823,7 @@ export const controls = defineControls({
805823 } ,
806824 lineFft : {
807825 initial : lineFftMode ,
808- options : [ 'radix4 ' , 'default ' ] ,
826+ options : [ 'radix-4 (default) ' , 'radix-2 ' ] ,
809827 onSelectChange : ( value ) => {
810828 lineFftMode = value as LineFftMode ;
811829 } ,
0 commit comments