Files
GR-raytracing/usage.md
T
wyj e4d093a9fe Doc: Record 4K fast-mode accuracy limits and error distribution
Add the production-density 4K Galactic-center comparison to the 2026-09-25 benchmark record, including the per-image error decomposition, and refine the fast-mode positioning in usage.md, the design document, and README.

- Document that nearest/bilinear deposit accuracy is set by the deposit mode and N rather than output resolution, and that the still-frame error is dominated by a small number of resolved bright cores while the flux-dominant faint texture is much better than the global relative L2.
- Add the temporal-coherence caveat: nearest deposition jumps by 1/N output pixel per axis across supersampled-cell boundaries, so fast mode is not qualified as a temporally coherent movie path; bilinear removes the centroid jump but broadens the profile.
- Add scripts/fast_mode_error_decomposition.py, which ranks the largest |fast-base| RGB samples and partitions the error by a disjoint base-value range.
2026-09-26 01:02:41 -04:00

379 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Rendering guide
See [README.md](README.md) for the project overview and a first 2MASS render,
and [build.md](build.md) for compilation. Commands below run from the repository
root and use the default Release binaries. Run a binary with `--help` for its
complete option list and defaults.
## Catalog input
Use `--all-sky-catalog assets/2mass/processed/all_sky` for the tiled 2MASS
catalog, or `--catalog PATH` for a single processed CSV. Acquisition and
processing are documented in [assets/2mass/README.md](assets/2mass/README.md).
2MASS blackbody amplitudes need a much larger display exposure than the
synthetic test catalog; `--exposure 1e15` is a starting point, to be adjusted
for the field and camera.
## Single-frame camera
Both backends accept the same instantaneous camera parameters. Position and
velocity use the backend's coordinates; velocity means `dx/dt, dy/dt, dz/dt`,
not a local physical speed. The analytic single-frame event is at `t=0`.
| Option | Meaning / default |
| --- | --- |
| `--observer-position X Y Z` | Coordinate position; if look is omitted, point toward the origin |
| `--look-ra-deg RA`, `--look-dec-deg DEC` | Coordinate look direction; missing angle defaults to RA=90°, Dec=-90° |
| `--observer-radius R` | Positive radius used only to infer position, default 30; conflicts with explicit position |
| `--observer-velocity VX VY VZ` | Coordinate velocity, default `0 0 0`; must be future-timelike in the local metric |
| `--camera-roll-deg ANGLE` | Rotate the camera up axis toward its right axis, default 0° |
When position and look are both supplied, they are independent. Look alone
implies position `-R * look_direction`, in either backend. Radius alone uses
the default look. With no position/look/radius options, Minkowski keeps the
origin camera and Schwarzschild keeps its radius-30 camera on +Z; both look
along -Z. Velocity and roll alone do not alter these defaults.
An explicit origin position requires a look angle. Position-derived directions
on the Z axis use RA=0° to fix the pole orientation deterministically.
The axes are right-handed ICRS: X=RA 0°/Dec 0°, Y=RA 90°/Dec 0°,
Z=Dec +90°. The coordinate look vector `(cos(dec) cos(ra), cos(dec) sin(ra),
sin(dec))` is projected into the moving camera's rest space. The coordinate
north and west seeds are then orthogonalized there to form up and right.
Consequently the look angles specify a projected coordinate orientation;
they do not lock a lensed sky object to the image center. Positive roll uses
`up' = cos(roll) up + sin(roll) right`,
`right' = -sin(roll) up + cos(roll) right`.
No static observer is needed. The metric determines whether `(1,VX,VY,VZ)`
is timelike; there is no Euclidean `|V| < 1` check in curved coordinates.
Invalid velocities are rejected before loading catalogs or building the PSF
cache, with the velocity and its metric norm in the diagnostic.
`--observer-inward-speed` has been removed.
Single-frame camera options cannot be combined with `--observer-track`,
`--frames-dir`, or `--lens-map-input`.
Schwarzschild uses Cartesian ingoing Kerr–Schild coordinates with `M=1`.
Cameras at and inside the horizon `r=2` are allowed with a valid timelike
coordinate velocity. The current backend excludes camera positions at or
inside its capture cutoff `r=1.5`; its finite escape radius is `256`.
These remain analytic demonstration settings, not criteria for future NR data.
Zero coordinate velocity at or inside the horizon is not timelike and is rejected.
The following complete examples use the bundled synthetic catalog:
```sh
make -j PSF_BACKEND=cpu SPACETIME=minkowski backend
mkdir -p output/imgs
./build/Release/minkowski_sky --catalog assets/sky_grid_5deg.csv \
--observer-position 3 -4 5 --observer-velocity 0.2 -0.1 0.3 \
--look-ra-deg 37 --look-dec-deg -23 --camera-roll-deg 19 \
--width 64 --height 48 --fov-deg 80 --exposure 0.1 \
--coarse-cell-pixels 8 --refine-max-level 0 --psf-direct \
--output output/imgs/minkowski_moving.png
make -j PSF_BACKEND=cpu SPACETIME=schwarzschild backend
./build/Release/schwarzschild_sky --catalog assets/sky_grid_5deg.csv \
--observer-position 3 -4 5 --observer-velocity 0.2 -0.1 0.3 \
--look-ra-deg 37 --look-dec-deg -23 --camera-roll-deg 19 \
--width 64 --height 48 --fov-deg 80 --exposure 0.1 \
--coarse-cell-pixels 8 --refine-max-level 0 --psf-direct \
--output output/imgs/schwarzschild_off_axis.png
./build/Release/schwarzschild_sky --catalog assets/sky_grid_5deg.csv \
--observer-position 1.75 0 0 --observer-velocity -0.5 0 0 \
--look-ra-deg 0 --look-dec-deg 0 \
--width 64 --height 48 --fov-deg 80 --exposure 0.1 \
--coarse-cell-pixels 8 --refine-max-level 0 --psf-direct \
--output output/imgs/schwarzschild_inside_horizon.png
```
The last example points outward from a camera moving inward inside the horizon.
It needs no CSV trajectory or movie wrapper. These small images are camera
checks; increase resolution and refinement for production renders.
## Movie image sequences
Movie mode reads an observer-track CSV containing coordinate time, proper
time, Cartesian position, and a full four-by-four tetrad (21 columns). The
track must cover the requested frame times. The following generates a sample
accelerating Minkowski trajectory and renders it against the 2MASS sky:
```sh
mkdir -p output/imgs
./build/Release/minkowski_sky --write-minkowski-accel-track output/minkowski_accel_2s.csv \
--duration 2 --fps 30 --proper-acceleration 1.52
./build/Release/minkowski_sky --observer-track output/minkowski_accel_2s.csv \
--all-sky-catalog assets/2mass/processed/all_sky \
--frames-dir output/imgs --frames-prefix minkowski_accel \
--start-time 0 --duration 2 --fps 30 --exposure 1e15
```
The inclusive time interval produces frames `minkowski_accel_000000.png`
through `minkowski_accel_000060.png`. Exposure may need adjustment as Doppler
shifts change the brightness. The trajectory generator is a flat-spacetime
example; other camera motions are supplied through observer-track CSVs.
Output is an image sequence; video encoding is a separate step.
Movie rays from all frames share a newest-to-oldest coordinate-time sweep.
`--slab-duration` sets its time-slab width (default `64`). The current analytic
backends use logical slabs without metric I/O; [Nmesh](https://github.com/nmeshsource/nmesh) metric loading remains
future work.
## Reuse a completed lens map
`--lens-map-output FILE` saves the finalized inverse-lens mesh after tracing
and refinement, while also rendering the image normally. It stores the
geometry, frequency shifts, terminal statuses, triangle topology, and frame
metadata in a versioned `.grlens` file. This allows later catalog, PSF, and
exposure changes without repeating ray tracing.
```sh
mkdir -p output/imgs output/maps
./build/Release/schwarzschild_sky \
--all-sky-catalog assets/2mass/processed/all_sky \
--width 640 --height 360 --coarse-cell-pixels 8 --fov-deg 60 \
--refine-max-level 2 --exposure 1e15 \
--lens-map-output output/maps/schwarzschild.grlens \
--output output/imgs/schwarzschild_trace.png
./build/Release/schwarzschild_sky \
--lens-map-input output/maps/schwarzschild.grlens \
--all-sky-catalog assets/2mass/processed/all_sky \
--psf-fwhm-pixels 6 --psf-moffat-beta 3 --exposure 1e15 \
--output output/imgs/schwarzschild_restyled.png
```
Import and export are mutually exclusive. Imported maps retain their original
pixel dimensions, horizontal FOV, and mesh resolution; they cannot be refined
on import. Explicit conflicting dimensions or FOV are rejected. The reader
checks the format version, finite values, unit directions, triangle indices,
and per-frame CRCs.
A movie export stores all final frame meshes in one file. To render it again,
pass `--lens-map-input FILE`, the catalog, `--frames-dir DIR`, and
`--frames-prefix NAME`; no observer track is needed on import.
## Adaptive image mesh refinement
Adaptive refinement is disabled by default (`--refine-max-level 0`), so the
existing coarse-mesh renders remain unchanged. When enabled, its defaults are
an absolute direction error of `1e-3` degrees, relative error `0.1`, minimum
long edge `0.5` pixels, minimum area `0.25` pixel-squared, and a provisional
minimum discrete-Jacobian magnitude of `1e-3`. Each value can be overridden
independently:
```text
--refine-max-level N
--refine-angle-abs-deg D
--refine-angle-rel R
--refine-jacobian-min J
--refine-min-edge-pixels P
--refine-min-area-pixels2 A
```
`N` caps the triangle refinement level. Let `e` be the angle between the
traced longest-edge midpoint direction and the normalized endpoint
interpolation, and let `s` be the angle between those two endpoint **camera
directions**. Both are evaluated internally in radians; the absolute CLI
threshold `D` is specified in degrees and converted before comparison. `s`
is the angular geometric size of the image triangle's test edge, not a
source-sky/lens-map length. A locally escaped triangle is split only when
**both** `e > D_rad` (the converted `--refine-angle-abs-deg D`) and
`e / max(s, 1e-15) > --refine-angle-rel`. `P` and `A`
prevent selecting a leaf already at or below the requested image-plane
long-edge and area scales.
Triangles whose three vertices disagree between capture and escape are split
independently of the direction-error thresholds, allowing the mesh to follow a
shadow boundary.
Independently of the midpoint geometry test, an all-escaped triangle also
computes the discrete lens Jacobian
`J = Omega_source / Omega_image`. Both signed solid angles use
`2 atan2(dot(a, cross(b,c)), 1 + dot(a,b) + dot(b,c) + dot(c,a))`, with the
ordered camera directions for `Omega_image` and their traced infinity
directions for `Omega_source`. A J-driven split requires **both** a shared
image edge whose incident triangles have opposite nonzero signs of `J` and
`min(abs(J_left), abs(J_right)) < --refine-jacobian-min`. It then requests
that shared edge on both leaves. Thus `|J|` bounds the fold selection instead
of widening it as a standalone critical-curve band. A negative sign is
physical parity and is retained. The `1e-3` default is deliberately
provisional and should be tuned with the small Schwarzschild refinement
diagnostic before being treated as a production threshold.
For a small mesh diagnostic using the synthetic catalog:
```sh
./build/Release/schwarzschild_sky --catalog assets/sky_grid_5deg.csv \
--width 48 --height 48 --coarse-cell-pixels 24 --fov-deg 40 \
--refine-max-level 1 --refine-angle-abs-deg 0.001 \
--refine-angle-rel 0.001 --refine-jacobian-min 0.001 \
--refine-min-edge-pixels 1 \
--refine-min-area-pixels2 1 --draw-mesh \
--output output/imgs/schwarzschild_refinement.png
```
For movies, each refinement generation completes the full newest-to-oldest
time-slab sweep before any probe becomes a mesh vertex. Newly added vertices
are therefore traced only by the next generation; the renderer never returns
to a slab that has already been released. At the start of every generation,
newly inserted vertices and geometry-only longest-edge probes for its new
leaves are collected together, so both ray sets use the same parallel
`RayPool` pass.
## Camera directions and optics
Catalog directions and `--look-ra-deg`/`--look-dec-deg` use standard
right-handed ICRS Cartesian axes: `+X` is RA 0 degrees/Dec 0 degrees, `+Y` is
RA 90 degrees/Dec 0 degrees, and `+Z` is the north celestial pole. The local
camera axes are forward, celestial north, and celestial west, so an image with
north up has decreasing RA to the right. The defaults preserve the original
`-Z` view. `--exposure` converts a catalog's physical flux
normalization to the prototype HDR scale. The current synthetic catalog is
calibrated for default exposure `1e-3`; a 2MASS blackbody normalization in
steradians requires a much larger display exposure such as the example above.
The optics path
integrates each fitted Planck spectrum through CIE 1931 color-matching functions
and converts the resulting radiance to linear sRGB; it does not use an empirical
color-temperature RGB approximation.
Point sources use a flux-normalized circular Moffat PSF by default
(`--psf-fwhm-pixels 2.7 --psf-moffat-beta 4.5`). The FWHM matches the former
1.15-pixel Gaussian core while the Moffat wings remain continuous; both values
are display/optics calibration parameters.
The default renderer builds one immutable, process-wide 64-by-64 sub-pixel
Moffat lookup kernel. Its weights are pixel-area integrals and are bilinearly
interpolated between phase tables. The renderer reports its build time and
cached/direct-fallback image counts. Pass `--psf-direct` to use the slower
8-point quadrature reference evaluator for regression comparisons; an image
whose required HDR-tail support exceeds the cache radius selects that reference
path automatically.
`--psf-relative-tail R` controls the maximum omitted PSF tail fraction per
image; it defaults to `1e-8`. Larger values intentionally shorten the Moffat
support and rebuild the immutable cache at the corresponding radius, which is
useful when a faster, lower-fidelity render is acceptable.
`--psf-min-y Y` defaults to `0` (disabled). A positive value is a linear-HDR
luminance cutoff: a PSF stops where its continuous Moffat Y profile falls below
`Y`, and an image whose central value is already below `Y` is omitted. The
final PSF report counts such omitted events and emits a warning when any occur.
For bounded preview renders, `--max-cache-psf-flux F` (default `1`) allows
images with `1 < flux <= F` to use the existing cache instead of the direct
evaluator. This does not rebuild or enlarge the cache: the image retains its
true core flux, color, and sub-pixel position, while its Moffat wing is clipped
at the existing cache radius. Images above `F` retain the direct fallback.
`--max-magnification M` (default unlimited) caps the per-triangle rendering
magnification before flux is formed; it is an explicit preview approximation.
### Fast preview mode
`--fast-mode` replaces the per-event PSF splat with a two-stage approximation:
each point-source image is deposited as a delta into an `N×N` supersampled HDR
buffer (`--fast-supersample N`, default 2), then the whole frame is convolved
once with a single global Moffat kernel and averaged down by the `N×N` block.
The kernel is the pixel-area integral of the requested Moffat at the
supersampled scale, so the requested FWHM and beta are preserved by the
downsample itself.
`--fast-deposit nearest` (default) deposits into the single nearest
supersampled pixel. It keeps the PSF shape exactly on that grid. The grid
spacing is `1/N` output pixel per axis, while the instantaneous rounding error
is bounded by `1/(2N)` per axis. `--fast-deposit bilinear` deposits into 4
adjacent pixels; it preserves the continuous centroid but broadens the profile
(measured +4.5%/+1.9%/+1.0% FWHM at N=2/3/4). The deposition experiment and
its raw output are in
[benchmarks/fast_mode_deposit_2026-09-18.md](benchmarks/fast_mode_deposit_2026-09-18.md).
Fast mode is available only in the CPU PSF backend. It ignores
`--max-cache-psf-flux` and uses one global kernel radius for every event, so a
bright event whose requested support exceeds that radius is wing-clipped and
counted in the report. `--psf-min-y` is still applied per event. Fast mode is a
preview approximation, not the physically exact per-event PSF path.
Its accuracy is governed by the deposit mode and `N`, not by the output
resolution. A nearest deposit shifts each image by at most `1/(2N)` output
pixels per axis. The separate two-dimensional centroid-distance experiment
measured worst cases of ~`0.30 px` at `N=2` and ~`0.12 px` at `N=4` for a FWHM
`2.7`, beta `4.5` PSF. These offsets remain fixed fractions of one output
pixel: with `--psf-fwhm-pixels` held fixed, a larger image does not reduce
them, and only a larger `N` (or the centroid-preserving bilinear deposit, at
the cost of a broadened profile) does.
That per-star number is not how one dense still image behaves. In the measured
production-density 4K Galactic-center frame, fast-versus-standard HDR relative
L2 is `10.44%` at `N=2` and `4.37%` at `N=4`, with a `+0.018%` total
linear-RGB-sum offset. The difference is concentrated in a small set of scalar
RGB channel samples: the top `0.01%` ranked by `|fast-base|` account for
`95.7%` (`N=2`) or `94.5%` (`N=4`) of squared-error energy, while containing
about `9%` of the summed baseline linear-RGB values. The disjoint base-value
partition puts the flux-bearing faint ranges (`[1e-3,0.06)`, `[0.06,0.1)`,
`[0.1,0.23)`, together ~`47%` of the linear-RGB sum) at ~`2-5%` local relative
RMS with signed bias at or below `0.19%`. This pattern is consistent with
partial cancellation among many faint PSFs, while resolved bright cores retain
bounded sub-pixel displacement. It does not make fast mode pixelwise or
visually lossless.
These measurements characterize independent still frames only. With nearest
deposition, a smoothly moving image jumps by one complete grid spacing when it
crosses a cell boundary: `1/N` output pixel per axis, or `0.5 px` at `N=2` and
`0.25 px` at `N=4`. Such discontinuities can appear as visible flicker even at
4K. Neither nearest mode is therefore qualified as a temporally coherent movie
output path. Bilinear deposition removes this particular centroid jump but has
the profile-broadening tradeoff above and has not been qualified as a final
movie path either. For single-frame dense-star previews, `N=2` remains the
throughput default and `N=4` the higher-fidelity point; the exact per-event
reference PSF path remains the quantitative and movie reference. The
single-frame error decomposition is reproducible with
[`scripts/fast_mode_error_decomposition.py`](scripts/fast_mode_error_decomposition.py).
In CPU builds the global convolution is a zero-padded FFTW linear convolution
(`fftw3`/`fftw3_omp`, double precision). The immutable kernel spectrum, FFTW
plans, and scratch buffers are built once when the accumulator is initialized
and reused for every frame; the disposable spatial reference remains only for
tests and benchmarks. Startup prints one `Fast FFTW:` line with the minimum
linear-convolution extent, the selected smooth FFT dimensions, worker count,
plan mode, one-time plan and kernel-transform time, and scratch bytes.
`--verbose` additionally prints per-frame stage timings (zero/pack, forward FFT,
frequency multiply, inverse FFT, crop/downsample, total). The default FFTW
planning mode is `FFTW_ESTIMATE`; `FFTW_MEASURE` can cut steady-state execution
substantially but costs a much longer one-time plan for large frames, as
recorded in
[benchmarks/fast_mode_fftw_2026-09-25.md](benchmarks/fast_mode_fftw_2026-09-25.md).
## Progress and diagnostics
The PSF-cache completion line is printed before tracing and catalog splatting
begin. For long renders, pass `--verbose` to print catalog-prefetch state,
splat-worker local heartbeats (8, 16, 32, ... completed triangles per worker),
and image-write boundaries. The worker heartbeats use neither global progress
accounting nor cross-worker synchronization.
Verbose ray-trace output reports the initial mesh trace and refinement stages
for single frames; movie mode additionally reports each generation's sample
count and each time slab's activation and terminal-ray summary.
Movie renders always print one summary per time slab; `--verbose` also prints
the ray counts before each slab is loaded.
Pass `--draw-mesh` to alpha-composite image-plane triangle edges as
one-pixel-wide 0.5 linear-gray diagnostic lines at 0.5 opacity. The line
rasterizer uses coverage-based antialiasing.
## HDR output
To preserve a single-frame render for later exposure and tone-mapping work,
build with `ENABLE_HDR=1` as described in [build.md](build.md). This produces `build/Release/schwarzschild_sky`,
which accepts `--hdr-output`. This switch writes the HDR file next to the
ordinary output, replacing its extension with `_HDR.fits`; for example,
`--output output/imgs/ring.png --hdr-output` writes
`output/imgs/ring_HDR.fits`. It writes the pre-tone-mapping RGB
framebuffer as a three-plane, 32-bit float FITS image. Values remain linear HDR
at the renderer's arbitrary scale; no tone mapping or per-frame normalization
is applied. The ordinary
binaries do not contain this option or writer.
The FITS header describes a synthetic 8640-by-5760, 36-by-24 mm full-frame
sensor with 4.1667 um pixels. Each render records its active centered crop and
derives `FOCALLEN` from that render's width and horizontal `--fov-deg`; these
camera fields support plate-solving workflows but do not calibrate flux.