Added Airy and PSF via FFT. Decided to make it a C2 feature

This commit is contained in:
connor committed 2026-10-17 18:28:52 -04:00
1 parent fbd7ce4147
commit 5e2769faa8
25 files changed
+2319 -117

No files matched your search

+88 -6
View File
@@ -727,7 +727,9 @@ take 80 bytes per pixel of GPU memory: 166 MB at 1920 x 1080 and 660 MB at
## Blur: the point spread function
Code: `camera.rs` (`Psf`); `gpu/blur.rs`, `shaders/blur.wgsl`.
Code: `camera.rs` (`Psf`); `gpu/blur.rs`, `shaders/blur.wgsl`; from camera
C2, `psf.rs`, `gpu/convolve.rs`, `gpu/fft.rs`, `shaders/convolve.wgsl`,
`shaders/fft.wgsl`.
No camera focuses a point to a point. Light from one point of the scene
lands on the detector as a small spot, and the shape of that spot is the
@@ -816,9 +818,9 @@ small-pixel averages, and the same error remains at a smaller scale:
(sigma 0.4 px, points at 16 and edges at 129 positions within a pixel; the
worst edges lie on a pixel boundary.) C1 renders 2 x 2 by default;
`RendererOptions::supersampling` sets 1 to 4. Point sources, stars in
particular, need better than that, and will be drawn with the blurred spot
computed exactly over each pixel instead.
`RendererOptions::supersampling` sets 1 to 4. Point sources need better
than that, so stars are drawn with the blurred spot computed exactly over
each pixel instead (see Stars).
A 2D Gaussian is the product of a 1D Gaussian across and one down, and
averaging an n x n block is likewise an average across and one down. So the
@@ -860,6 +862,83 @@ as an error. On the CPU through lavapipe, 2 x 2 adds 9 to 13 ms to
1920 x 1080 frames of 116 to 680 ms, and 3 x 3 at 3840 x 2160 runs out of
memory.
### Diffraction and the PSF's wings (camera C2)
A Gaussian falls to nothing within a few sigma. A real camera's spot does
not: diffraction's rings fade only as the cube of the distance, and dust and
rough lens surfaces scatter a little light much farther. Next to most of a
scene that faint halo, the PSF's wings, is invisible. Next to the Sun it is
not. In the sun-glare example scene each pixel of the Sun's disk collects
about 5e8 electrons in its 0.4 ms exposure, 26,000 full wells, over about
5,700 pixels. Diffraction at f/4 spreads a fraction of about
λN / (π^3 r^3) of a point's light into each pixel r pixels away (λN, the
wavelength times the f-number, is 0.39 px for green light in the example
camera), so 500 px from the Sun each pixel still gets about 300 electrons
from it, and 1000 px away about 40: a glow over the whole frame that raises
the background, can hide a faint target, and pulls a limb fit toward the
bright side.
Camera tier C2 models this. `Psf::Airy { obstruction }` is diffraction by
the camera's aperture: the Airy pattern, set by the f-number, with an
optional central obstruction such as a reflecting telescope's secondary
mirror (which moves light from the core into the rings). The pattern scales
with the wavelength, so a channel's spot is the pattern averaged over the
channel's band, each wavelength weighted by the photons sunlight gives it
there; across an 80 nm band the outer rings blur into a smooth halo.
`Psf::Measured { image, pixel_scale }` takes a spot measured in a lab or in
flight, the same over the whole field, one image per channel or one for
all. Measured: a 2 nm wide channel's first dark ring falls within 1 % of
1.22 λN, where the textbook puts it.
A kernel reaching hundreds of pixels is far too slow to apply pixel by
pixel. C2 instead uses the convolution theorem: transform the image into
its frequencies with a fast Fourier transform (FFT), multiply by the
kernel's frequencies, computed once at setup, and transform back. The cost
no longer depends on the kernel's size, only on the image's. The transform
runs on the GPU in compute shaders, sized to powers of two up to 4096 on a
side, with two channels sharing one complex transform, so three channels
take four transforms per frame. C2 applies every PSF this way, Gaussians
too, and draws stars into the focal plane before the transform, so they get
the same wings; each star's light is shared among its four nearest rendered
pixels, which keeps its center exact but can widen its spot by up to 0.25 px
RMS at 2 x 2.
As at C1, the transform acts on the scene rendered finer than the pixels,
before they average it. The kernel is the PSF integrated over a rendered
pixel twice, once for the pixel the light leaves and once for the one it
lands in, so a Gaussian at C2 differs slightly from C1's, which samples the
Gaussian at the rendered pixels' centers (by up to 0.34 % of the brightest
pixel, measured on the Moon). The transform treats the image as repeating,
so the renderer draws a margin around it as wide as the kernel reaches, and
the grid holds the image and that margin. The reach is therefore set by the
grid: at least `RendererOptions::psf_reach` (16 px by default), and as far
as the smallest grid that holds it allows. Light the kernel would carry
farther is added back to the spot instead, so none is lost, but the glow
stops at the reach, and with the Sun in the frame its edge shows in a
brightened preview.
| Image, rendered pixels per pixel | Grid | Reach | RTX 3060 | lavapipe |
|---|---|---|---|---|
| 1920 x 1080, 2 x 2 | 4096 x 4096 | 64 px | 30 ms | 560 ms |
| 1920 x 1080, 1 x 1 | 2048 x 2048 | 64 px | 5.8 ms | 213 ms |
| 1920 x 1080, 1 x 1, reach 1000 | 4096 x 4096 | 1088 px | 29 ms | 556 ms |
| 3840 x 2160, 1 x 1 | 4096 x 4096 | 128 px | 30 ms | 623 ms |
(The convolution pass alone, in the sun-glare scene at lighting L1. The
whole frame took 35 ms at 1920 x 1080 and 2 x 2, against 5.9 ms at C1.) A
grid of 4096 x 4096 takes 640 MB on the device. 3840 x 2160 at 2 x 2 would
need a grid 8192 across and is refused; 1 x 1 fits.
The transforms work in 32-bit floating point, which keeps about seven
significant digits, and their rounding error spreads over the whole grid in
proportion to its brightest values. With the Sun in the frame that leaves a
floor of about 10 electrons RMS, up to 165, across the frame (measured in
the sun-glare scene with a noiseless detector, where C1 reads exactly 0),
above the example detector's 5 electrons of read noise. A sunlit target at
half the full well leaves a floor near 1e-3 electrons, far below any noise.
Computing the brightest sources' wings separately, outside the transform,
would remove the Sun's floor; that is not done yet.
## Earth, Moon and Sun
Code: `bodies.rs`, `gpu/bodies.rs`, `shaders/bodies.wgsl`.
@@ -1638,7 +1717,9 @@ since all of a star's light that enters lands in its spot. A star is not
rendered and then blurred: a point of light rendered into a pixel loses
where in the pixel it fell (see the blur section). Instead its electrons are
spread over the pixels around it as the Gaussian PSF integrated over each
pixel's area, which is exact for any position within a pixel. The detector
pixel's area, which is exact for any position within a pixel. (Camera C2
draws stars into the focal plane before its convolution instead, so they
take the Airy or measured PSF too; see the PSF's wings.) The detector
adds them to the light from everything else before the full well, so a
bright star saturates like any other light. A star is hidden wherever
anything nearer is drawn, Earth, the Moon or a target: stars are at
@@ -1672,7 +1753,8 @@ is a run of the script with `--gaia 16`, at twice the size.
## Not modeled yet
- Earthshine on the Moon's night side
- The diffraction pattern's rings, and a lens's measured point spread function
- A point spread function that changes across the field, and the PSF's wings
past the reach of camera C2's grid
- Non-Lambertian materials (BRDFs), glints, and the full Moon's edge as bright
as its center
- Clouds on Earth