Added earth surface texture
This commit is contained in:
1 parent
5b802b4455
commit
e77d68d0a6
27 files changed
+1284
-93
No files matched your search
+104
-51
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
Reference in new issue
Block a user