feat(cosmobiologia): esfera 3D — la piel real del cielo: estrellas + Vía Láctea
La «piel» de una esfera celeste no son continentes —esos van en la Tierra— sino las estrellas y la Vía Láctea. Y a diferencia del brillo especular (fijo a la pantalla), esta piel gira CON la esfera, así que delata la rotación de un vistazo. - Campo de estrellas isótropo, decorativo (no un catálogo real), generado con un hash determinista — no titila entre frames. - Vía Láctea: una sobredensidad de estrellas tenues a lo largo del plano galáctico, ubicado con el polo galáctico real (J2000, AR 192.859° / Dec +27.128°). - Estrellas con brillo y tinte variables (azuladas / cálidas), atenuadas por profundidad. Van detrás de la rejilla, delante del sombreado — un fondo de planetario. Solo en tema oscuro. 36 tests verdes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -86,6 +86,9 @@ pub struct SphereOpts {
|
|||||||
/// El horizonte local, el cénit del observador y el meridiano.
|
/// El horizonte local, el cénit del observador y el meridiano.
|
||||||
/// Necesita `RenderModel::geo_latitude_deg`.
|
/// Necesita `RenderModel::geo_latitude_deg`.
|
||||||
pub show_horizon: bool,
|
pub show_horizon: bool,
|
||||||
|
/// El cielo de fondo: campo de estrellas + Vía Láctea. Solo se
|
||||||
|
/// dibuja en tema oscuro (en papel rompería la metáfora de imprenta).
|
||||||
|
pub show_sky: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SphereOpts {
|
impl Default for SphereOpts {
|
||||||
@@ -99,6 +102,7 @@ impl Default for SphereOpts {
|
|||||||
show_bodies: true,
|
show_bodies: true,
|
||||||
show_signs: true,
|
show_signs: true,
|
||||||
show_horizon: true,
|
show_horizon: true,
|
||||||
|
show_sky: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -447,6 +451,109 @@ fn add_point_marker(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Cielo de fondo: estrellas decorativas + Vía Láctea --------------
|
||||||
|
|
||||||
|
/// Polo norte galáctico (J2000): AR 192.859°, Dec +27.128° — constante
|
||||||
|
/// estándar IAU que fija el plano de la Vía Láctea.
|
||||||
|
const GAL_POLE_RA: f32 = 192.859;
|
||||||
|
const GAL_POLE_DEC: f32 = 27.128;
|
||||||
|
|
||||||
|
/// Hash entero → f32 en [0,1). Determinista (variante de splitmix32):
|
||||||
|
/// la misma entrada da siempre el mismo valor, así el campo de
|
||||||
|
/// estrellas no titila ni salta entre frames.
|
||||||
|
fn hash01(n: u32) -> f32 {
|
||||||
|
let mut x = n.wrapping_mul(0x9E37_79B9);
|
||||||
|
x ^= x >> 16;
|
||||||
|
x = x.wrapping_mul(0x85EB_CA6B);
|
||||||
|
x ^= x >> 13;
|
||||||
|
x = x.wrapping_mul(0xC2B2_AE35);
|
||||||
|
x ^= x >> 16;
|
||||||
|
(x as f32) / (u32::MAX as f32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Punto uniforme sobre la esfera unidad a partir de dos uniformes.
|
||||||
|
fn sphere_point(u1: f32, u2: f32) -> Vec3 {
|
||||||
|
let z = 2.0 * u1 - 1.0;
|
||||||
|
let rho = (1.0 - z * z).max(0.0).sqrt();
|
||||||
|
let theta = std::f32::consts::TAU * u2;
|
||||||
|
Vec3::new(rho * theta.cos(), rho * theta.sin(), z)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vector unitario de una dirección ecuatorial (AR, Dec en grados).
|
||||||
|
fn equatorial_dir(ra_deg: f32, dec_deg: f32) -> Vec3 {
|
||||||
|
let (sr, cr) = ra_deg.to_radians().sin_cos();
|
||||||
|
let (sd, cd) = dec_deg.to_radians().sin_cos();
|
||||||
|
Vec3::new(cd * cr, cd * sr, sd)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Empuja una estrella: un disco diminuto con brillo y un leve tinte
|
||||||
|
/// (azulado o cálido). Va detrás de la rejilla pero delante del
|
||||||
|
/// sombreado — un fondo de planetario.
|
||||||
|
fn push_star(
|
||||||
|
items: &mut Vec<(f32, DrawCommand)>,
|
||||||
|
proj: &Projector,
|
||||||
|
size: f32,
|
||||||
|
pos: Vec3,
|
||||||
|
brightness: f32,
|
||||||
|
tint: f32,
|
||||||
|
) {
|
||||||
|
let p = proj.project(pos);
|
||||||
|
let bright = brightness * brightness; // sesga hacia las tenues
|
||||||
|
let r = size * (0.0011 + 0.0026 * bright);
|
||||||
|
let alpha = (0.20 + 0.62 * bright) * depth_alpha(p.depth);
|
||||||
|
let col = if tint < 0.22 {
|
||||||
|
Rgba { r: 0.74, g: 0.81, b: 1.0, a: alpha }
|
||||||
|
} else if tint > 0.86 {
|
||||||
|
Rgba { r: 1.0, g: 0.86, b: 0.72, a: alpha }
|
||||||
|
} else {
|
||||||
|
Rgba { r: 0.95, g: 0.96, b: 1.0, a: alpha }
|
||||||
|
};
|
||||||
|
items.push((
|
||||||
|
p.depth - 3.0,
|
||||||
|
DrawCommand::Circle {
|
||||||
|
cx: p.x,
|
||||||
|
cy: p.y,
|
||||||
|
r,
|
||||||
|
stroke: None,
|
||||||
|
fill: Some(col),
|
||||||
|
stroke_w: 0.0,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// El cielo de fondo: un campo de estrellas isótropo —decorativo, no un
|
||||||
|
/// catálogo real— más una sobredensidad de estrellas tenues a lo largo
|
||||||
|
/// del plano galáctico, que dibuja la Vía Láctea. Ambos giran con la
|
||||||
|
/// esfera, así que delatan su rotación de un vistazo.
|
||||||
|
fn add_starfield(items: &mut Vec<(f32, DrawCommand)>, proj: &Projector, size: f32, eps: f32) {
|
||||||
|
const FONDO: u32 = 210;
|
||||||
|
for i in 0..FONDO {
|
||||||
|
let pos = sphere_point(hash01(i * 3), hash01(i * 3 + 1));
|
||||||
|
push_star(items, proj, size, pos, hash01(i * 3 + 2), hash01(i * 7 + 1));
|
||||||
|
}
|
||||||
|
// Vía Láctea — el plano galáctico ubicado con el polo galáctico real.
|
||||||
|
let gpole = rot_x(equatorial_dir(GAL_POLE_RA, GAL_POLE_DEC), eps);
|
||||||
|
let geq = great_circle_perp(gpole, 256);
|
||||||
|
const VIA: u32 = 240;
|
||||||
|
for i in 0..VIA {
|
||||||
|
let s = 9001 + i;
|
||||||
|
let idx = (hash01(s * 5) * geq.len() as f32) as usize % geq.len();
|
||||||
|
let on_eq = geq[idx];
|
||||||
|
// Latitud galáctica pequeña, concentrada cerca de 0 — producto
|
||||||
|
// de dos uniformes centrados → densa en el plano.
|
||||||
|
let u = hash01(s * 5 + 1) - 0.5;
|
||||||
|
let v = hash01(s * 5 + 2) - 0.5;
|
||||||
|
let b = (u * v * 4.0 * 13.0).to_radians();
|
||||||
|
let (sb, cb) = b.sin_cos();
|
||||||
|
let pos = Vec3::new(
|
||||||
|
on_eq.x * cb + gpole.x * sb,
|
||||||
|
on_eq.y * cb + gpole.y * sb,
|
||||||
|
on_eq.z * cb + gpole.z * sb,
|
||||||
|
);
|
||||||
|
push_star(items, proj, size, pos, hash01(s * 5 + 3) * 0.55, hash01(s * 5 + 4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
// Composición
|
// Composición
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
@@ -480,6 +587,11 @@ pub fn compose_sphere(
|
|||||||
// --- Cuerpo de la esfera: sombreado con volumen ---
|
// --- Cuerpo de la esfera: sombreado con volumen ---
|
||||||
add_sphere_shading(&mut items, pal, center, rad);
|
add_sphere_shading(&mut items, pal, center, rad);
|
||||||
|
|
||||||
|
// --- Cielo de fondo: estrellas + Vía Láctea (solo tema oscuro) ---
|
||||||
|
if opts.show_sky && pal.is_dark {
|
||||||
|
add_starfield(&mut items, &proj, size, eps);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Rejilla: meridianos + paralelos de la eclíptica ---
|
// --- Rejilla: meridianos + paralelos de la eclíptica ---
|
||||||
if opts.show_grid {
|
if opts.show_grid {
|
||||||
let grid = pal.fg_muted.with_alpha(0.16);
|
let grid = pal.fg_muted.with_alpha(0.16);
|
||||||
@@ -858,6 +970,30 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn el_cielo_dibuja_un_campo_de_estrellas() {
|
||||||
|
let modelo = modelo_demo();
|
||||||
|
let con = compose_sphere(
|
||||||
|
&modelo,
|
||||||
|
&SphereView::default(),
|
||||||
|
&SphereOpts { show_sky: true, ..Default::default() },
|
||||||
|
);
|
||||||
|
let sin = compose_sphere(
|
||||||
|
&modelo,
|
||||||
|
&SphereView::default(),
|
||||||
|
&SphereOpts { show_sky: false, ..Default::default() },
|
||||||
|
);
|
||||||
|
let discos = |c: &[DrawCommand]| {
|
||||||
|
c.iter().filter(|d| matches!(d, DrawCommand::Circle { .. })).count()
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
discos(&con) > discos(&sin) + 300,
|
||||||
|
"el cielo agrega cientos de estrellas: {} vs {}",
|
||||||
|
discos(&con),
|
||||||
|
discos(&sin),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn el_meridiano_contiene_cenit_polo_y_medio_cielo() {
|
fn el_meridiano_contiene_cenit_polo_y_medio_cielo() {
|
||||||
let eps = OBLICUIDAD_DEG.to_radians();
|
let eps = OBLICUIDAD_DEG.to_radians();
|
||||||
|
|||||||
Reference in New Issue
Block a user