Moon surface map

This commit is contained in:
connor committed 2026-10-15 10:04:13 -04:00
1 parent 0e8134aa59
commit f8b515e198
36 files changed
+1710 -222

No files matched your search

+39 -13
View File
@@ -10,7 +10,7 @@ use std::time::Instant;
use clap::{Args, Parser, Subcommand};
use resolved::gpu::AdapterChoice;
use resolved::{
CameraTier, EarthTexture, Fidelity, Frame, LightingTier, Renderer, RendererOptions,
CameraTier, EarthTexture, Fidelity, Frame, LightingTier, MoonTexture, Renderer, RendererOptions,
};
use crate::scene_file::SceneFile;
@@ -60,8 +60,8 @@ struct RenderArgs {
#[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/.
/// scripts/fetch-earth.sh, moon/wac-hapke-643.png from scripts/fetch-moon.py).
/// 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:
@@ -108,15 +108,28 @@ fn data_directories(explicit: Option<&Path>) -> Vec<PathBuf> {
/// 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");
data_file(
data,
&format!("earth/bmng-2004-{month:02}.jpg"),
"fetch-earth.sh",
)
}
/// `name` from the first data directory that has it; the error names `script`, which
/// makes it.
fn data_file(data: Option<&Path>, name: &str, script: &str) -> Result<PathBuf, String> {
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(", ")
)
})
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/{script}, or pass --data DIR); looked in {}",
tried.join(", ")
)
})
}
fn render(args: &RenderArgs) -> Result<(), String> {
@@ -141,6 +154,18 @@ fn render(args: &RenderArgs) -> Result<(), String> {
} else {
None
};
let moon_texture = if args.lighting >= LightingTier::L1 {
let image = data_file(
args.data.as_deref(),
"moon/wac-hapke-643.png",
"fetch-moon.py",
)?;
Some(Arc::new(
MoonTexture::load(&image).map_err(|e| e.to_string())?,
))
} else {
None
};
let texturing = start.elapsed();
let options = RendererOptions {
@@ -155,6 +180,7 @@ fn render(args: &RenderArgs) -> Result<(), String> {
camera: args.camera,
},
earth_texture,
moon_texture,
appearance: file.appearance(),
read_radiance: false,
profile: timings.is_some(),
@@ -189,11 +215,11 @@ fn render(args: &RenderArgs) -> Result<(), String> {
println!("adapter {}", renderer.adapter_info().name);
println!("tiers {} {}", args.lighting, args.camera);
println!(
"setup {:.1} ms (includes the Earth texture's upload)",
"setup {:.1} ms (includes the Earth and Moon maps' upload)",
setup.as_secs_f64() * 1e3
);
println!(
"earth {:.1} ms (texture decode)",
"maps {:.1} ms (Earth and Moon image decode)",
texturing.as_secs_f64() * 1e3
);
println!(
+14 -2
View File
@@ -33,6 +33,9 @@ pub struct SceneFile {
pub moon: Option<[f64; 3]>,
/// Earth-fixed to inertial rotation, if not given by `epoch`; identity by default.
pub earth_attitude: Option<[f64; 4]>,
/// Moon-fixed to inertial rotation, if not given by `epoch`; by default the Moon
/// faces Earth (`Scene::moon_facing_earth`, within about 10 degrees).
pub moon_attitude: Option<[f64; 4]>,
/// Earth's and the Moon's albedo.
#[serde(default)]
pub appearance: AppearanceFile,
@@ -242,8 +245,12 @@ impl SceneFile {
#[cfg(feature = "astrokit")]
fn bodies(&self, targets: Vec<Target>, kernels: Option<&Path>) -> Result<Scene> {
if let Some(epoch) = &self.epoch {
if self.sun.is_some() || self.moon.is_some() || self.earth_attitude.is_some() {
return Err("give either epoch or sun, moon and earth_attitude, not both".into());
if self.sun.is_some()
|| self.moon.is_some()
|| self.earth_attitude.is_some()
|| self.moon_attitude.is_some()
{
return Err("give either epoch or sun, moon and the attitudes, not both".into());
}
let epoch: hifitime::Epoch =
epoch.parse().map_err(|e| format!("epoch {epoch:?}: {e}"))?;
@@ -274,6 +281,11 @@ impl SceneFile {
.unwrap_or_else(UnitQuaternion::identity),
sun: Vector3::from(sun),
moon: Vector3::from(moon),
moon_attitude: self
.moon_attitude
.map(quaternion)
.transpose()?
.unwrap_or_else(|| Scene::moon_facing_earth(Vector3::from(moon))),
targets,
})
}
+36 -2
View File
@@ -30,8 +30,8 @@ fn render(scene: &str, adapter: &str) -> image::ImageBuffer<image::Rgb<u16>, Vec
render_at(scene, adapter, "L0")
}
/// A data directory whose Earth images are small and uniformly gray, for tests at
/// lighting L1 that do not need Blue Marble: loading the real 16384 x 8192 images takes
/// A data directory whose Earth images and Moon map are small and uniform, for tests at
/// lighting L1 that do not need the real ones: loading those (16384 x 8192 each) takes
/// seconds in a debug build, and they are only there once fetched.
fn gray_earth() -> PathBuf {
static DIR: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
@@ -44,6 +44,11 @@ fn gray_earth() -> PathBuf {
.save(dir.join(format!("earth/bmng-2004-{month:02}.jpg")))
.unwrap();
}
// A uniform Moon: I/F 0.06 (16-bit values of I/F x 100,000).
std::fs::create_dir_all(dir.join("moon")).unwrap();
image::ImageBuffer::<image::Luma<u16>, _>::from_pixel(64, 32, image::Luma([6000]))
.save(dir.join("moon/wac-hapke-643.png"))
.unwrap();
dir
})
.clone()
@@ -164,6 +169,35 @@ fn solar_eclipse_puts_the_umbra_at_greatest_eclipse() {
assert!(corner.iter().all(|&v| v > 70), "{corner:?}");
}
#[test]
fn lighting_l1_names_a_missing_moon_map() {
// Earth's images but no Moon map: the error names the file and the script.
let dir = out_dir().join("earth-only");
std::fs::create_dir_all(dir.join("earth")).unwrap();
std::fs::copy(
gray_earth().join("earth/bmng-2004-07.jpg"),
dir.join("earth/bmng-2004-07.jpg"),
)
.unwrap();
let result = resolved(&[
"render",
"--scene",
"examples/scenes/landsat8.toml",
"--lighting",
"L1",
"--data",
dir.to_str().unwrap(),
"--preview",
"x.png",
]);
assert!(!result.status.success());
let stderr = String::from_utf8_lossy(&result.stderr);
assert!(
stderr.contains("moon/wac-hapke-643.png") && stderr.contains("fetch-moon.py"),
"{stderr}"
);
}
#[test]
fn errors_are_reported() {
let missing = resolved(&["render", "--scene", "no/such.toml", "--preview", "x.png"]);