Added earth surface texture

This commit is contained in:
connor committed 2026-10-14 20:12:49 -04:00
1 parent 5b802b4455
commit e77d68d0a6
27 files changed
+1284 -93

No files matched your search

+104 -51
View File
@@ -4,11 +4,14 @@ mod scene_file;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::Arc;
use std::time::Instant;
use clap::{Parser, Subcommand};
use clap::{Args, Parser, Subcommand};
use resolved::gpu::AdapterChoice;
use resolved::{Frame, Renderer, RendererOptions};
use resolved::{
CameraTier, EarthTexture, Fidelity, Frame, LightingTier, Renderer, RendererOptions,
};
use crate::scene_file::SceneFile;
@@ -25,32 +28,46 @@ struct Cli {
#[derive(Subcommand)]
enum Command {
/// Render a scene file.
Render {
/// The scene, as TOML (see examples/scenes/).
#[arg(long)]
scene: PathBuf,
/// Raw DN as a 16-bit PNG (RGB for three channels, gray for one), shifted up to
/// fill the 16-bit range: DN = value >> (16 - ADC bits), so 12-bit DN are x 16.
#[arg(long)]
out: Option<PathBuf>,
/// An 8-bit sRGB PNG for viewing: bias subtracted, saturation as white.
#[arg(long)]
preview: Option<PathBuf>,
/// Device: auto, gpu, cpu, or part of an adapter's name.
#[arg(long, default_value = "auto")]
adapter: String,
/// Render this many frames and report setup and per-frame times.
#[arg(long, value_name = "FRAMES")]
timings: Option<usize>,
/// Directory of astrokit's kernels, for scenes given by epoch. Without it:
/// ASTROKIT_KERNELS, then ./data/kernels, then the sibling astrokit checkout.
#[arg(long, value_name = "DIR")]
kernels: Option<PathBuf>,
},
Render(RenderArgs),
/// List the adapters this machine offers.
Adapters,
}
#[derive(Args)]
struct RenderArgs {
/// The scene, as TOML (see examples/scenes/).
#[arg(long)]
scene: PathBuf,
/// Raw DN as a 16-bit PNG (RGB for three channels, gray for one), shifted up to
/// fill the 16-bit range: DN = value >> (16 - ADC bits), so 12-bit DN are x 16.
#[arg(long)]
out: Option<PathBuf>,
/// An 8-bit sRGB PNG for viewing: bias subtracted, saturation as white.
#[arg(long)]
preview: Option<PathBuf>,
/// Lighting fidelity tier: L0, or L1 (adds Earth's surface; needs Blue Marble data).
#[arg(long, default_value = "L0")]
lighting: LightingTier,
/// Camera fidelity tier: C0.
#[arg(long, default_value = "C0")]
camera: CameraTier,
/// Device: auto, gpu, cpu, or part of an adapter's name.
#[arg(long, default_value = "auto")]
adapter: String,
/// Render this many frames and report setup and per-frame times.
#[arg(long, value_name = "FRAMES")]
timings: Option<usize>,
/// Directory of data the tiers need (earth/bmng-2004-MM.jpg from
/// scripts/fetch-earth.sh). Without it: RESOLVED_DATA, then ./data, then the
/// repository's data/.
#[arg(long, value_name = "DIR")]
data: Option<PathBuf>,
/// Directory of astrokit's kernels, for scenes given by epoch. Without it:
/// ASTROKIT_KERNELS, then ./data/kernels, then the sibling astrokit checkout.
#[arg(long, value_name = "DIR")]
kernels: Option<PathBuf>,
}
fn main() -> ExitCode {
let result = match Cli::parse().command {
Command::Adapters => {
@@ -62,21 +79,7 @@ fn main() -> ExitCode {
}
Ok(())
}
Command::Render {
scene,
out,
preview,
adapter,
timings,
kernels,
} => render(
&scene,
out.as_deref(),
preview.as_deref(),
&adapter,
timings,
kernels.as_deref(),
),
Command::Render(args) => render(&args),
};
match result {
Ok(()) => ExitCode::SUCCESS,
@@ -87,37 +90,79 @@ fn main() -> ExitCode {
}
}
fn render(
path: &Path,
out: Option<&Path>,
preview: Option<&Path>,
adapter: &str,
timings: Option<usize>,
kernels: Option<&Path>,
) -> Result<(), String> {
/// Where to look for data: `explicit` alone if given, else `RESOLVED_DATA`, `./data`
/// and the repository's `data/`.
fn data_directories(explicit: Option<&Path>) -> Vec<PathBuf> {
if let Some(dir) = explicit {
return vec![dir.to_owned()];
}
let repository = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../data");
std::env::var_os("RESOLVED_DATA")
.map(PathBuf::from)
.into_iter()
.chain([PathBuf::from("data"), repository])
.collect()
}
/// The Earth image for `month` (1-12), from the first data directory that has it.
fn earth_image(data: Option<&Path>, month: u32) -> Result<PathBuf, String> {
let name = format!("earth/bmng-2004-{month:02}.jpg");
let tried = data_directories(data);
tried.iter().map(|dir| dir.join(&name)).find(|p| p.is_file()).ok_or_else(|| {
let tried: Vec<_> = tried.iter().map(|d| d.display().to_string()).collect();
format!(
"lighting L1 needs {name} (run scripts/fetch-earth.sh, or pass --data DIR); looked in {}",
tried.join(", ")
)
})
}
fn render(args: &RenderArgs) -> Result<(), String> {
let (out, preview, timings) = (args.out.as_deref(), args.preview.as_deref(), args.timings);
if out.is_none() && preview.is_none() && timings.is_none() {
return Err("nothing to do: give --out, --preview or --timings".into());
}
let path = args.scene.as_path();
let file = SceneFile::read(path)?;
let camera = file.camera();
// What the tiers need, gathered before the renderer exists so it can check them.
let start = Instant::now();
let earth_texture = if args.lighting >= LightingTier::L1 {
let month = file.earth_month()?.ok_or(
"lighting L1 needs the month for Earth's surface: give the scene an epoch, or [appearance] earth_month",
)?;
let image = earth_image(args.data.as_deref(), month)?;
Some(Arc::new(
EarthTexture::load(&image).map_err(|e| e.to_string())?,
))
} else {
None
};
let texturing = start.elapsed();
let options = RendererOptions {
adapter: match adapter {
adapter: match args.adapter.as_str() {
"auto" => AdapterChoice::Auto,
"gpu" => AdapterChoice::Gpu,
"cpu" => AdapterChoice::Cpu,
name => AdapterChoice::Named(name.to_owned()),
},
fidelity: Fidelity {
lighting: args.lighting,
camera: args.camera,
},
earth_texture,
appearance: file.appearance(),
read_radiance: false,
profile: timings.is_some(),
};
let start = Instant::now();
let mut renderer = Renderer::new(camera, options).map_err(|e| e.to_string())?;
let setup = start.elapsed();
let start = Instant::now();
let dir = path.parent().unwrap_or(Path::new("."));
let scene = file.scene(&mut renderer, dir, kernels)?;
let scene = file.scene(&mut renderer, dir, args.kernels.as_deref())?;
let observer = file.observer()?;
let loading = start.elapsed();
@@ -138,7 +183,15 @@ fn render(
if timings.is_some() {
println!("adapter {}", renderer.adapter_info().name);
println!("setup {:.1} ms", setup.as_secs_f64() * 1e3);
println!("tiers {} {}", args.lighting, args.camera);
println!(
"setup {:.1} ms (includes the Earth texture's upload)",
setup.as_secs_f64() * 1e3
);
println!(
"earth {:.1} ms (texture decode)",
texturing.as_secs_f64() * 1e3
);
println!(
"scene {:.1} ms (models and ephemeris)",
loading.as_secs_f64() * 1e3
+51
View File
@@ -75,6 +75,9 @@ pub struct ObserverFile {
pub struct AppearanceFile {
pub earth_albedo: Option<f64>,
pub moon_albedo: Option<f64>,
/// Which month (1-12) Earth's surface shows at lighting L1, for scenes without an
/// epoch; with one, the epoch's month is the default.
pub earth_month: Option<u32>,
}
/// One spacecraft.
@@ -154,6 +157,32 @@ impl SceneFile {
a
}
/// The month (1-12) for Earth's surface: `earth_month`, else the epoch's month.
pub fn earth_month(&self) -> Result<Option<u32>> {
let month = match self.appearance.earth_month {
Some(m) => Some(m),
None => self.epoch_month()?,
};
match month {
Some(m) if !(1..=12).contains(&m) => Err(format!("earth_month {m} is not 1-12")),
m => Ok(m),
}
}
#[cfg(feature = "astrokit")]
fn epoch_month(&self) -> Result<Option<u32>> {
let Some(epoch) = &self.epoch else {
return Ok(None);
};
let epoch: hifitime::Epoch = epoch.parse().map_err(|e| format!("epoch {epoch:?}: {e}"))?;
Ok(Some(epoch.to_gregorian_utc().1 as u32))
}
#[cfg(not(feature = "astrokit"))]
fn epoch_month(&self) -> Result<Option<u32>> {
Ok(None)
}
/// The observer's pose.
pub fn observer(&self) -> Result<Pose> {
let o = &self.observer;
@@ -329,6 +358,28 @@ mod tests {
assert_eq!(camera.detector.adc_bits, 10);
}
#[test]
fn earth_month_comes_from_the_setting() {
let with = |extra: &str| -> SceneFile {
toml::from_str(&format!("{MINIMAL}\n[appearance]\n{extra}\n")).unwrap()
};
assert_eq!(with("earth_month = 3").earth_month().unwrap(), Some(3));
assert_eq!(
with("").earth_month().unwrap(),
None,
"no month and no epoch"
);
assert!(with("earth_month = 13").earth_month().is_err());
}
#[cfg(feature = "astrokit")]
#[test]
fn earth_month_defaults_to_the_epoch() {
let file: SceneFile =
toml::from_str(&format!("epoch = \"2026-10-14T12:00:00 UTC\"\n{MINIMAL}")).unwrap();
assert_eq!(file.earth_month().unwrap(), Some(10));
}
#[test]
fn mistakes_are_reported() {
// A misspelled field.
+54
View File
@@ -123,6 +123,60 @@ fn missing_kernels_say_where_they_looked() {
);
}
#[test]
fn lighting_l1_says_what_it_is_missing() {
// boxwing.toml has no epoch and no earth_month: L1 cannot pick a month.
let no_month = resolved(&[
"render",
"--scene",
"examples/scenes/boxwing.toml",
"--lighting",
"L1",
"--preview",
"x.png",
]);
assert!(!no_month.status.success());
let stderr = String::from_utf8_lossy(&no_month.stderr);
assert!(stderr.contains("earth_month"), "{stderr}");
// With a month but no data: the error names the file and the fetch script.
let scene = out_dir().join("month.toml");
let models = repo().join("examples/models").canonicalize().unwrap();
let text = std::fs::read_to_string(repo().join("examples/scenes/boxwing.toml"))
.unwrap()
.replace("../models", models.to_str().unwrap())
+ "\n[appearance]\nearth_month = 7\n";
std::fs::write(&scene, text).unwrap();
let no_data = resolved(&[
"render",
"--scene",
scene.to_str().unwrap(),
"--lighting",
"L1",
"--data",
"no/such/dir",
"--preview",
"x.png",
]);
assert!(!no_data.status.success());
let stderr = String::from_utf8_lossy(&no_data.stderr);
assert!(
stderr.contains("bmng-2004-07.jpg") && stderr.contains("fetch-earth.sh"),
"{stderr}"
);
let unknown = resolved(&[
"render",
"--scene",
"examples/scenes/boxwing.toml",
"--lighting",
"L7",
"--preview",
"x.png",
]);
assert!(!unknown.status.success());
}
#[test]
fn adapters_lists_something() {
let result = resolved(&["adapters"]);