Squashed from work; the fine-grained history is under tag archive/work-2026-08-26: - render+sim: the two hooks the model viewer already relies on - viewport: Realtime gets the engine's cascaded shadow pass, the NOAA sun, metered exposure with sky ambient, a haze knob, a time-of-day slider in the header, and no grid — the building itself still waits for its casters and direct light - render: the engine's receiver shader takes the sun with two-sided normals and the shadow term for the model batches - fab: a 3D creation shell and the viewer built on it - render: two shaders that never compiled — a let is not assignable, a var is - sim: a declared map facing becomes a body's heading through one rule - render: a HUD that already reads as a game's before anyone styles it - asset+sim: the two modules their own commits already declared
192 lines
6.8 KiB
Rust
192 lines
6.8 KiB
Rust
//! One sun: the direction the SKY paints, the direction the LIGHT shades
|
|
//! with, and the rig (colour, night ramp) must all follow the same star.
|
|
//!
|
|
//! A host that supplies an explicit `dir` (Fab's NOAA solar position) must
|
|
//! see the whole frame follow it: the engine's own `time_of_day` model is a
|
|
//! fixed-declination look, and at a real site on a real date its sun can be
|
|
//! many degrees away from the true one — far enough that the engine's rig
|
|
//! called "night" while the true sun still stood golden above the horizon.
|
|
|
|
use makepad_draw::*;
|
|
use makepad_game_sim::SunConfig;
|
|
use makepad_render::sky::{luminance, noaa_solar_position, SkyDate};
|
|
use makepad_render::sun::{resolve_sun, solar_dir};
|
|
|
|
/// Fab's default site (libs/fab api::SkyState::default): Amsterdam-ish,
|
|
/// midsummer, CEST.
|
|
const LAT: f32 = 52.37;
|
|
const LON: f32 = 4.9;
|
|
const TZ: f32 = 2.0;
|
|
const DATE: SkyDate = SkyDate {
|
|
year: 2024,
|
|
month: 6,
|
|
day: 21,
|
|
};
|
|
|
|
/// Fab's `SkyState::direction()` + `to_render`, reproduced here: NOAA
|
|
/// elevation/azimuth to the render world's y-up frame (x east, -z north).
|
|
fn noaa_render_dir(hour: f32) -> Vec3f {
|
|
let (elevation, azimuth) = noaa_solar_position(DATE, hour, TZ, LAT, LON);
|
|
let elevation = elevation.to_radians();
|
|
let azimuth = azimuth.to_radians();
|
|
let horizontal = elevation.cos();
|
|
// Fab space (z up, +y north): (sin az * h, cos az * h, sin el),
|
|
// then to_render (x, z, -y).
|
|
vec3(
|
|
horizontal * azimuth.sin(),
|
|
elevation.sin(),
|
|
-horizontal * azimuth.cos(),
|
|
)
|
|
.normalize()
|
|
}
|
|
|
|
fn fab_sun_config(hour: f32) -> SunConfig {
|
|
SunConfig {
|
|
time_of_day: Some(hour),
|
|
latitude: LAT,
|
|
dir: Some(noaa_render_dir(hour)),
|
|
color: None,
|
|
ambient: None,
|
|
daylight_balance: Some(9.0),
|
|
shadow_alpha: Some(0.85),
|
|
}
|
|
}
|
|
|
|
fn angle_deg(a: Vec3f, b: Vec3f) -> f32 {
|
|
a.normalize()
|
|
.dot(b.normalize())
|
|
.clamp(-1.0, 1.0)
|
|
.acos()
|
|
.to_degrees()
|
|
}
|
|
|
|
fn elev_deg(d: Vec3f) -> f32 {
|
|
d.y.clamp(-1.0, 1.0).asin().to_degrees()
|
|
}
|
|
|
|
/// The measurement behind the fix, printed for the record: at Fab's default
|
|
/// site the engine's fixed-declination sun and the true NOAA sun disagree by
|
|
/// double-digit degrees through the day, and at 20:00 the engine model is
|
|
/// BELOW the horizon while the true sun is still up.
|
|
#[test]
|
|
fn the_two_solar_models_disagree_at_a_real_site() {
|
|
println!(
|
|
"{:>6} {:>18} {:>18} {:>10} {:>12}",
|
|
"hour", "noaa el/az", "engine el/az", "angle", "night-ramp-el"
|
|
);
|
|
for hour in [8.0f32, 14.0, 18.5, 20.0] {
|
|
let noaa = noaa_render_dir(hour);
|
|
let simple = solar_dir(hour, LAT);
|
|
let az = |d: Vec3f| (d.x.atan2(-d.z).to_degrees()).rem_euclid(360.0);
|
|
println!(
|
|
"{:>6.1} {:>8.1}/{:>8.1} {:>8.1}/{:>8.1} {:>9.1}d {:>11.1}d",
|
|
hour,
|
|
elev_deg(noaa),
|
|
az(noaa),
|
|
elev_deg(simple),
|
|
az(simple),
|
|
angle_deg(noaa, simple),
|
|
elev_deg(simple),
|
|
);
|
|
}
|
|
// The concrete split-brain of the bug report: at 20:00 the true sun is
|
|
// still above the horizon while the engine's fixed-declination model has
|
|
// already set.
|
|
let noaa = noaa_render_dir(20.0);
|
|
assert!(elev_deg(noaa) > 2.0, "true sun at 20:00: {noaa:?}");
|
|
assert!(
|
|
elev_deg(solar_dir(20.0, LAT)) < elev_deg(noaa),
|
|
"the models should disagree at 20:00 for this site"
|
|
);
|
|
}
|
|
|
|
/// THE pinned contract: with an explicit direction, the direction the sky
|
|
/// paints (resolve_sun().dir feeds the Preetham frame, the disc, the CSM
|
|
/// projection and the shading) IS the explicit one, at every hour.
|
|
#[test]
|
|
fn sky_and_light_share_one_direction() {
|
|
for hour in [0.0f32, 4.0, 8.0, 12.0, 14.0, 18.5, 20.0, 22.0] {
|
|
let cfg = fab_sun_config(hour);
|
|
let rig = resolve_sun(&cfg);
|
|
let want = cfg.dir.unwrap();
|
|
assert!(
|
|
angle_deg(rig.dir, want) < 1.0e-3,
|
|
"{hour}h: light {:?} vs sky {:?}",
|
|
rig.dir,
|
|
want
|
|
);
|
|
}
|
|
// ...and a time-of-day-only game keeps the engine's own model.
|
|
let cfg = SunConfig {
|
|
time_of_day: Some(15.0),
|
|
latitude: LAT,
|
|
..Default::default()
|
|
};
|
|
assert!(angle_deg(resolve_sun(&cfg).dir, solar_dir(15.0, LAT)) < 1.0e-3);
|
|
}
|
|
|
|
/// The rig must FOLLOW the explicit sun: while the true sun is up, there is
|
|
/// direct light — even at an hour where the engine's own model says night.
|
|
#[test]
|
|
fn the_rig_follows_the_explicit_sun_not_the_hour() {
|
|
// 20:00 at the default site: the true sun is a few degrees up, golden.
|
|
let rig = resolve_sun(&fab_sun_config(20.0));
|
|
let true_el = elev_deg(noaa_render_dir(20.0));
|
|
assert!(true_el > 2.0, "premise: sun still up at 20:00 ({true_el})");
|
|
assert!(
|
|
luminance(rig.color) > 0.05,
|
|
"sun above the horizon but no direct light: {:?}",
|
|
rig.color
|
|
);
|
|
// The low sun is WARM: the direct term leans red over blue — the sky
|
|
// model's own transmittance at 15.8 degrees (20:00 is still two hours
|
|
// before this site's midsummer sunset; the gold deepens as it sinks).
|
|
let warmth = |c: Vec3f| c.x / c.z.max(1.0e-6);
|
|
assert!(
|
|
warmth(rig.color) > 1.45,
|
|
"20:00 direct should be golden: {:?}",
|
|
rig.color
|
|
);
|
|
// And a high sun stays effectively white, so the 20:00 warmth is a
|
|
// sunset property, not a permanent cast.
|
|
let noon = resolve_sun(&fab_sun_config(13.5));
|
|
assert!(
|
|
warmth(noon.color) < 1.2,
|
|
"noon direct should stay near-white: {:?}",
|
|
noon.color
|
|
);
|
|
assert!(warmth(rig.color) > 1.3 * warmth(noon.color));
|
|
// Deeper into the sunset the gold keeps deepening.
|
|
let later = resolve_sun(&fab_sun_config(21.3));
|
|
assert!(
|
|
warmth(later.color) > warmth(rig.color),
|
|
"21:18 {:?} vs 20:00 {:?}",
|
|
later.color,
|
|
rig.color
|
|
);
|
|
// Below the true horizon the direct term still goes out.
|
|
let night = resolve_sun(&fab_sun_config(23.0));
|
|
assert!(
|
|
luminance(night.color) < 1.0e-3,
|
|
"night direct: {:?}",
|
|
night.color
|
|
);
|
|
}
|
|
|
|
/// Same contract in the other convention: an explicit dir built in a z-up
|
|
/// host and converted with fab's `to_render` mapping lands on the same
|
|
/// render-space sun as building it in render space directly.
|
|
#[test]
|
|
fn the_conventions_agree_on_the_same_sky() {
|
|
for hour in [8.0f32, 14.0, 18.5, 20.0] {
|
|
let (elevation, azimuth) = noaa_solar_position(DATE, hour, TZ, LAT, LON);
|
|
let (el, az) = (elevation.to_radians(), azimuth.to_radians());
|
|
// Fab space: x east, y north, z up.
|
|
let fab = vec3(az.sin() * el.cos(), az.cos() * el.cos(), el.sin());
|
|
let to_render = vec3(fab.x, fab.z, -fab.y);
|
|
assert!(
|
|
angle_deg(to_render, noaa_render_dir(hour)) < 1.0e-3,
|
|
"{hour}h"
|
|
);
|
|
}
|
|
}
|