feat(planets): OrbitParams component + orbit_position geometry

This commit is contained in:
xfy 2026-07-16 17:55:42 +08:00
parent 6ff5a05e74
commit 474dfbe723

View File

@ -1,3 +1,5 @@
use std::f32::consts::{PI, TAU};
use bevy::prelude::*;
use bevy::render::storage::ShaderBuffer;
@ -12,6 +14,59 @@ pub struct Planet {
pub emissive: bool,
}
/// 轨道根数 (不变量, 启动时随机生成, 运行时不变除非 UI 改种子重生).
#[derive(Component, Clone, Copy)]
pub struct OrbitParams {
/// k, 乘到 kerr_isco(χ) 上得实际轨道半径.
pub radius_factor: f32,
/// 轨道面法向与 Y 轴(自旋轴)的夹角 (rad).
pub inclination: f32,
/// 升交点经度 (rad), 决定轨道面在方位上的初始取向.
pub longitude_of_node: f32,
/// 轨道内初始相位 (rad).
pub phase: f32,
}
/// 由轨道根数 + 当前 (模拟)时间 + 自旋, 计算行星世界空间位置.
/// 纯函数: 无 Bevy 依赖, 可独立测试.
///
/// 物理:
/// - r = k · kerr_isco(χ)
/// - Ω_φ = kerr_orbital_frequency(r, χ) (轨道角速度)
/// - Ω_LT = kerr_nodal_precession(r, χ) (轨道面绕 Y 轴的进动率)
/// 轨道面基 (u, v) 由 inclination + longitude_of_node 构造, 然后绕 Y 轴
/// 整体旋转 Ω_LT·t (Lense-Thirring 进动).
pub fn orbit_position(orbit: &OrbitParams, t: f32, chi: f32) -> Vec3 {
let r = orbit.radius_factor * crate::physics::kerr_isco(chi);
let omega_phi = crate::physics::kerr_orbital_frequency(r, chi);
let omega_lt = crate::physics::kerr_nodal_precession(r, chi);
// 1. 轨道面法向 (Y 轴为极轴的球坐标)
let inc = orbit.inclination;
let lon = orbit.longitude_of_node;
let sin_inc = inc.sin();
let n = Vec3::new(
sin_inc * lon.cos(),
inc.cos(),
sin_inc * lon.sin(),
);
// 2. 轨道面内正交基: u 沿升节点方向, v = n × u
// u 在 XZ 平面 (垂直于 Y 轴), 指向升节点
let u = Vec3::new(-lon.sin(), 0.0, lon.cos());
let v = n.cross(u);
// 3. 进动: (u, v) 绕 Y 轴整体旋转 Ω_LT·t
let pa = omega_lt * t;
let cp = pa.cos();
let sp = pa.sin();
let u_p = Vec3::new(u.x * cp + u.z * sp, u.y, -u.x * sp + u.z * cp);
let v_p = Vec3::new(v.x * cp + v.z * sp, v.y, -v.x * sp + v.z * cp);
// 4. 行星在进动后的轨道面内的位置
let theta = orbit.phase + omega_phi * t;
r * (theta.cos() * u_p + theta.sin() * v_p)
}
/// Collects all Planet components, writes them into the shared MAX_PLANETS-sized
/// `ShaderBuffer` that the material already binds, and updates `planet_count`.
///
@ -71,3 +126,59 @@ pub fn spawn_default_planet(mut commands: Commands) {
emissive: false,
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn orbit_position_radius_is_preserved() {
// 不管时间/相位, 行星到原点距离应恒等于 r = k·isco(χ)
let orbit = OrbitParams {
radius_factor: 2.5,
inclination: 0.7,
longitude_of_node: 1.3,
phase: 0.5,
};
let chi = 0.8;
let expected_r = 2.5 * crate::physics::kerr_isco(chi);
for t in [0.0_f32, 1.0, 5.5, 100.0] {
let pos = orbit_position(&orbit, t, chi);
let dist = pos.length();
assert!(
(dist - expected_r).abs() < 1e-4,
"t={}: dist {} != r {}",
t, dist, expected_r
);
}
}
#[test]
fn orbit_position_zero_spin_keeps_equatorial_plane() {
// χ=0: 无进动, 倾角 0 (赤道面) 的行星应严格在 y=0 平面
let orbit = OrbitParams {
radius_factor: 3.0,
inclination: 0.0,
longitude_of_node: 0.0,
phase: 0.0,
};
for t in [0.0_f32, 1.0, 10.0] {
let pos = orbit_position(&orbit, t, 0.0);
assert!(pos.y.abs() < 1e-5, "χ=0 equatorial orbit should stay in y=0 plane at t={}", t);
}
}
#[test]
fn orbit_position_advances_with_time() {
// 不同时间应给不同位置 (除非极端巧合)
let orbit = OrbitParams {
radius_factor: 3.0,
inclination: 0.5,
longitude_of_node: 0.0,
phase: 0.0,
};
let p0 = orbit_position(&orbit, 0.0, 0.5);
let p1 = orbit_position(&orbit, 1.0, 0.5);
assert!((p0 - p1).length() > 0.01, "planet should move over time");
}
}