simvx.graphics.renderer.ocean_fft¶
FFT ocean spectrum + inverse transform (design D11, RM-E7).
Dependency-free (numpy only, no Vulkan / no core-node imports) so BOTH the
desktop ocean pass (ocean_pass.py) and the future browser twin (Pyodide,
where vulkan is unavailable) drive the identical Tessendorf spectrum. One
canonical spectrum + inverse-FFT, producing per-cascade displacement, slope and
foam fields that the ocean shader (ocean.vert / ocean.frag, later
ocean3d.wgsl) samples.
Model (Tessendorf 2001, the film/game-standard FFT ocean):
A Phillips spectrum with directional spreading and an against-wind damp seeds a complex, Hermitian-symmetric height spectrum
h0(k)per cascade. Each cascade tiles a square patch ofpatch_lengthmetres atsizexsizeresolution, so short cascades add crisp chop and long cascades add slow swell.Per frame the spectrum is evolved to time
twith the deep-water dispersionomega = sqrt(g |k|)and inverse-FFT’d to a real height field, horizontal choppy displacement, and surface slope (for the normal).Foam comes from the Jacobian of the horizontal displacement: where the surface folds onto itself (Jacobian below a threshold) foam is deposited and then decays, so crests keep a lace of surf that dissolves in the troughs.
The wind (direction + strength) is taken from the renderer’s FrameGlobals wind
slot, exactly like the built-in Gerstner :class:WaterMaterial; the FFT ocean
replaces the analytic Gerstner displacement with a sampled displacement map and
feeds the derived normal + foam into the same water shading (design D16).
Design D11 specifies a GPU Stockham radix-2 compute FFT. On desktop Vulkan the
per-frame hot path (time-evolve the spectrum + the 2D inverse FFT + assemble
displacement / slope / Jacobian-foam) runs entirely on the GPU
(ocean_compute.py + ocean_spectrum.comp / ocean_fft.comp /
ocean_assemble.comp). This module stays the canonical spectrum builder: it
constructs the frozen h0 Phillips spectrum + the dispersion / k-vectors once
per :meth:configure (uploaded once to the GPU via :meth:gpu_static_spectrum),
and keeps a pure-numpy reference of the exact per-frame maths the shaders
run (:func:stockham_ifft2 + :meth:gpu_reference) so the GPU FFT is verifiable
headlessly against numpy.fft without an interactive GPU debug loop. The
legacy CPU :meth:evaluate is retained as the reference transform the unit
tests pin against.
Module Contents¶
Classes¶
One FFT cascade: a square |
|
Multi-cascade Tessendorf ocean spectrum + inverse transform. |
Functions¶
The first |
|
2D inverse FFT via the Stockham butterfly (rows then columns), |
Data¶
API¶
- simvx.graphics.renderer.ocean_fft.__all__¶
[‘GRAVITY’, ‘OceanCascade’, ‘OceanFFT’, ‘cascade_patch_lengths’, ‘stockham_ifft2’]
- simvx.graphics.renderer.ocean_fft.GRAVITY¶
9.81
- simvx.graphics.renderer.ocean_fft.cascade_patch_lengths(cascades: int) tuple[float, ...][source]¶
The first
cascadesdefault patch lengths (metres), longest first.
- simvx.graphics.renderer.ocean_fft.stockham_ifft2(spec: numpy.ndarray) numpy.ndarray[source]¶
2D inverse FFT via the Stockham butterfly (rows then columns),
1/N^2.Numerically matches :func:
numpy.fft.ifft2to float round-off; it is the CPU twin of the GPUocean_fft.comprow-pass + column-pass chain.
- class simvx.graphics.renderer.ocean_fft.OceanCascade[source]¶
One FFT cascade: a square
sizexsizepatch ofpatch_lengthm.h0/h0_mk_conjare the time-invariant spectrum (recomputed only when the wind/config changes); the per-cascadefoamfield accumulates acrossevaluatecalls so surf lingers on crests and fades in troughs.- size: int¶
None
- patch_length: float¶
None
- weight: float¶
None
- kx: numpy.ndarray¶
‘field(…)’
- kz: numpy.ndarray¶
‘field(…)’
- kmag: numpy.ndarray¶
‘field(…)’
- kx_unit: numpy.ndarray¶
‘field(…)’
- kz_unit: numpy.ndarray¶
‘field(…)’
- omega: numpy.ndarray¶
‘field(…)’
- h0: numpy.ndarray¶
‘field(…)’
- h0_mk_conj: numpy.ndarray¶
‘field(…)’
- gain: float¶
1.0
- foam: numpy.ndarray¶
‘field(…)’
- class simvx.graphics.renderer.ocean_fft.OceanFFT(size: int, cascades: int, *, seed: int = 1337)[source]¶
Multi-cascade Tessendorf ocean spectrum + inverse transform.
Construct once for a given
(size, cascades)quality config (design D13 dials), call :meth:configurewhenever the wind/appearance changes, then- Meth:
evaluateonce per frame to get the sampled surface fields.
Initialization
- configure(*, wind_dir: tuple[float, float], wind_speed: float, amplitude: float, direction_spread: float = 1.0) None[source]¶
(Re)build the time-invariant spectrum for the current wind/appearance.
Cheap no-op when the inputs are unchanged (so a steady scene rebuilds the spectrum once, not every frame).
wind_diris a 2D XZ direction (need not be normalised);wind_speedsets the Phillips fetch length;amplitudeis the target RMS wave height (world units) of the biggest cascade att=0.
- evaluate(time: float, *, choppiness: float = 1.0, foam_threshold: float = 0.6, foam_decay: float = 0.92) tuple[numpy.ndarray, numpy.ndarray][source]¶
Return
(disp, grad)float16 arrays for the current frame.dispis(cascades, size, size, 4)=[Dx, height, Dz, foam];gradis(cascades, size, size, 4)=[slope_x, slope_z, 0, 0]. Both are contiguous, ready to upload straight into a texture-2D-array (one layer per cascade). Foam accumulates across calls; a repeatedtimeis a no-op that returns the last result (so a double-eval in a pipelined frame does not double-advance the surf).
- gpu_static_spectrum() numpy.ndarray[source]¶
The frozen per-cascade spectrum, packed for the GPU compute FFT.
Returns a contiguous
(cascades, N, N, 8)float32 array = two vec4 per texel:[h0.re, h0.im, h0_mk_conj.re, h0_mk_conj.im]then[kx, kz, 0, 0]. The per-cascadegainis baked intoh0/h0_mk_conj(so the shader-sidehkalready carries the target RMS), exactly as :meth:evaluatemultipliesself._time_spectrum * gain.kx_unit/kz_unit/omegaare re-derived in the shader from(kx, kz)with the identical formulae (dispersion + safe normalise).
- gpu_reference(time: float, *, choppiness: float = 1.0, foam_threshold: float = 0.6, foam_decay: float = 0.92) tuple[numpy.ndarray, numpy.ndarray][source]¶
CPU twin of the GPU compute chain (spectrum -> Stockham IFFT -> assemble).
Mirrors
ocean_spectrum.comp(the 2-real-per-complex packing), theocean_fft.compStockham row/column IFFT andocean_assemble.compfield assembly, using the SAME derivedkx_unit/omegathe shader computes from the uploaded(kx, kz). Returns float32(disp, grad)with the same layout as :meth:evaluate; used by the unit tests to pin the shader maths againstnumpyheadlessly (foam accumulation excluded, it is an identical post-step in both paths).