4.9 KiB
resolved
Renders what a spacecraft camera sees: Earth, Moon, Sun and a target spacecraft loaded from a mesh. The inputs are an epoch, the observer and target states and attitudes, and a camera model. Images are radiometric: the renderer works in radiance and converts to electrons and then DN through the camera's optics and detector, so an image should match what a real camera with the same parameters records.
It is built for closed-loop simulation, where each step renders one frame and latency is the number that matters. The goal is high-fidelity 1–2 MP RGB in about 10 ms end to end on a mid-range GPU (the reference is an RTX 3060); 100 ms is the ceiling, not the aim. Lighting fidelity and camera fidelity are selected separately, and each tier is benchmarked, so a run can trade fidelity for speed knowing what each step costs.
Rendering goes through wgpu. On a machine without a GPU, the same shaders run
on the CPU through Mesa's lavapipe (vulkan-swrast on Arch,
mesa-vulkan-drivers on Debian and Ubuntu).
Usage
use resolved::nalgebra::{UnitQuaternion, Vector3};
use resolved::radiometry::AU;
use resolved::{CameraConfig, Mesh, Pose, Renderer, Scene, Target};
# fn main() -> Result<(), resolved::Error> {
// Setup, once: device, shaders, spectra folded into the camera's channels.
let mut renderer = Renderer::new(CameraConfig::example(), Default::default())?;
// A model, uploaded once. Usually `Mesh::load(path, &options)` on an STL or glTF.
let stl = "solid panel
facet normal 0 0 0
outer loop
vertex 0 -1 -1
vertex 0 1 -1
vertex 0 0 1
endloop
endfacet
endsolid panel";
let panel = renderer.load_model(&Mesh::from_stl(&mut std::io::Cursor::new(stl), "panel", &Default::default())?)?;
// Each step: inertial positions in meters, Earth at the origin.
let target = Vector3::new(7.0e6, 0.0, 0.0);
let scene = Scene {
earth_attitude: UnitQuaternion::identity(),
sun: Vector3::new(AU, 0.0, 0.0),
moon: Vector3::new(0.0, 3.844e8, 0.0),
targets: vec![Target { model: panel, pose: Pose::new(target, UnitQuaternion::identity()) }],
};
let observer = Pose::looking_at(target + Vector3::new(20.0, 0.0, 0.0), target, Vector3::z()).unwrap();
let pending = renderer.submit(&scene, &observer)?;
// ... the simulation's own work for this step overlaps the GPU's here ...
let frame = pending.wait()?;
// 12-bit DN, channels r, g, b interleaved. The panel faces the Sun and the camera.
assert!(frame.dn_at(960, 540, 1) > 1000);
# Ok(())
# }
Features
astrokit: build aScenefrom an epoch with astrokit's ephemerides (JPL DE440s for the Sun and Moon, ITRF93 for Earth's orientation), throughephemeris::Ephemeris. astrokit is not on crates.io, so this expects a checkout at../astrokitnext to this repository, and its kernels from itsscripts/fetch-kernels.sh(orASTROKIT_KERNELSpointing at them).
Status
M1 in progress: the library renders Earth, Moon, Sun and targets to DN
through Renderer; the CLI and benchmarks are still to come. The work is
tracked as Gitea milestones, each split into one issue per change:
| Milestone | Scope |
|---|---|
| M1 | Scene API, headless wgpu, analytic Earth/Moon/Sun, glTF and STL targets, Lambert shading with eclipse, pinhole camera, radiance to DN, CLI, benchmark baseline |
| M2 | Self-shadowing, Earthshine and Moonshine, Earth and Moon albedo maps, Sun limb darkening |
| M3 | Lens distortion, PSF, detector noise, saturation and blooming, stars |
| M4 | BRDF and spectral materials, atmospheric scattering, clouds, lunar terrain |
| M5 | Portable ray tracing, path tracing, label outputs for ML |
| M6 | Validation against lunar, Earth and Mitsuba references; motion blur, rolling shutter, stray light; multi-band and 4k |
Conventions
- SI units throughout: meters, seconds, radians, watts.
- Scene state is f64 in an inertial frame (GCRF). The GPU only sees f32 coordinates relative to the camera.
Layout
crates/resolved: the library.crates/resolved-cli: theresolvedcommand, for rendering without writing a host program.docs/optics.md: the optics and radiometry the renderer models, explained for readers without an optics background.
Development
Point git at the committed hooks once per clone:
git config core.hooksPath hooks
pre-commit runs formatting, clippy and the library unit tests; pre-push
runs the full test suite, including doctests, and the documentation build.
The integration tests render on both a hardware GPU and lavapipe, so running
them needs a Vulkan driver for the GPU and lavapipe installed. Set
WGPU_BACKEND, WGPU_ADAPTER_NAME and the other standard wgpu environment
variables to steer adapter discovery.
To see what the GPU tests render, set RESOLVED_TEST_IMAGES=1. Each test then
writes a PNG per device to target/tmp/test-images/, scaled so a white
surface facing the Sun is full white:
RESOLVED_TEST_IMAGES=1 cargo test --test bodies --test mesh
License
MIT