# Kerr 轨道行星系统 实现计划 (Phase 3.4) > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 让 5–8 颗行星沿 Kerr 圆轨道绕黑洞旋转,轨道面因 Lense-Thirring 进动绕自旋轴转动,位置每帧 CPU 闭式计算并上传既有 storage buffer。 **Architecture:** 路径 A — 物理/轨道全在 CPU,shader 零改动。新增 `OrbitParams`(不可变根数) + 复用 `Planet`(每帧派生 center)。闭式公式 `Ω_φ`(Bardeen 1972) 与 `Ω_θ`(垂直 epicyclic 频率) 给出精确强场节点进动 `Ω_LT = Ω_φ - Ω_θ`,χ=0 精确退化为牛顿。 **Tech Stack:** Bevy 0.19, Rust edition 2024, `rand` + `rand_chacha`(确定性 PRNG), egui 控制面板。 **Spec:** `docs/superpowers/specs/2026-07-16-kerr-orbiting-planets-design.md` --- ## 文件结构 | 文件 | 责任 | 改动 | |---|---|---| | `Cargo.toml` | 声明 `rand` + `rand_chacha` 直接依赖 | 新增 2 行 | | `src/physics.rs` | `kerr_orbital_frequency` + `kerr_nodal_precession` + 测试 | 新增 ~80 行 | | `src/scene/planets.rs` | `OrbitParams` 组件 + 轨道几何 + `orbit_system` + `spawn_planet_system` | 大改,删除 `spawn_default_planet` | | `src/params.rs` | 5 个新 `BlackHoleParams` 字段 + Default | 新增 ~15 行 | | `src/render/plugin.rs` | 系统注册 + 资源初始化 | 改 ~10 行 | | `src/ui.rs` | Planets collapsing header | 新增 ~15 行 | **不改动:** `src/render/material.rs`(`SphereData`/`BlackHoleUniforms` 不动), `assets/shaders/black_hole.wgsl`(shader 零改动)。 --- ## Task 0: 添加 rand 依赖 **Files:** - Modify: `Cargo.toml:10-12` `rand` 当前只是 bevy 的传递依赖,未在 `Cargo.toml` 直接声明;`rand_chacha` 完全缺失。需要显式声明以便 `ChaCha8Rng` 稳定可用。 - [ ] **Step 1: 添加依赖** 修改 `Cargo.toml` 的 `[dependencies]` 段: ```toml [dependencies] bevy = "0.19" bevy_egui = "0.41" rand = "0.8" rand_chacha = "0.3" ``` 注:用 `rand 0.8` + `rand_chacha 0.3`(稳定 LTS,API 与 Bevy 0.19 生态兼容)。`seed_from_u64` + `gen_range` API 在 0.8 稳定。 - [ ] **Step 2: 验证编译** Run: `cargo check` Expected: 编译通过(可能下载新 crate)。若版本冲突,用 `cargo update -p rand` 或锁到与 bevy 兼容的版本。 - [ ] **Step 3: Commit** ```bash git add Cargo.toml Cargo.lock git commit -m "deps: add rand + rand_chacha for deterministic planet seeding" ``` --- ## Task 1: Kerr 轨道频率 + 进动率 (physics.rs) — TDD 核心物理公式,先写测试。这部分是整个方案物理正确性的基石。 **Files:** - Modify: `src/physics.rs`(在文件末尾,`_phantom` 函数之前) - Test: `src/physics.rs` 内联 `#[cfg(test)]` 模块 ### 1a: `kerr_orbital_frequency` - [ ] **Step 1: 写失败测试** 在 `src/physics.rs` 的 `#[cfg(test)]` mod 里(`fn _phantom` 之后,或现有测试 mod 内)加: ```rust #[test] fn orbital_frequency_reduces_to_newton_at_zero_spin() { // χ=0: Ω = 1/r^1.5 (牛顿开普勒, Rs=1) for r in [4.0_f32, 6.0, 10.0, 20.0] { let newton = 1.0 / r.powf(1.5); let kerr = kerr_orbital_frequency(r, 0.0); assert!( (kerr - newton).abs() < 1e-6, "χ=0 at r={}: expected {} (newton), got {}", r, newton, kerr ); } } #[test] fn orbital_frequency_decreases_with_spin_at_fixed_r() { // prograde 轨道 (a>0): Ω_φ 随 χ 减小 (分母 r^1.5+a 增大) let r = 8.0; let omega_0 = kerr_orbital_frequency(r, 0.0); let omega_1 = kerr_orbital_frequency(r, 1.0); assert!(omega_1 < omega_0, "prograde Ω should decrease with spin"); } ``` - [ ] **Step 2: 运行测试,确认失败** Run: `cargo test orbital_frequency` Expected: FAIL,编译错误 `cannot find function kerr_orbital_frequency`。 - [ ] **Step 3: 实现 `kerr_orbital_frequency`** 在 `src/physics.rs` 的 `kerr_horizon` 函数之后(`kerr_bending_accel` 之前)加: ```rust /// Kerr 赤道 prograde 圆轨角速度 (Rs=1, M=0.5). Bardeen 1972 eqn 2.16. /// `Ω_φ = 1 / (r^1.5 + a)`, a = χM = 0.5χ. χ=0 退化为牛顿 1/r^1.5. pub fn kerr_orbital_frequency(r: f32, chi: f32) -> f32 { let m = 0.5; let a = chi * m; 1.0 / (r.powf(1.5) + a) } ``` - [ ] **Step 4: 运行测试,确认通过** Run: `cargo test orbital_frequency` Expected: PASS,2 个测试通过。 - [ ] **Step 5: Commit** ```bash git add src/physics.rs git commit -m "feat(physics): Kerr equatorial orbital frequency Ω_φ (Bardeen 1972)" ``` ### 1b: `kerr_nodal_precession` - [ ] **Step 1: 写失败测试** 在 `src/physics.rs` 的测试 mod 加: ```rust #[test] fn nodal_precession_vanishes_at_zero_spin() { // χ=0: 球对称 (Schwarzschild), 无节点进动 for r in [4.0_f32, 6.0, 10.0, 20.0] { let prec = kerr_nodal_precession(r, 0.0); assert!( prec.abs() < 1e-6, "χ=0 at r={} should have zero precession, got {}", r, prec ); } } #[test] fn nodal_precession_grows_with_spin() { // 固定 r, prograde 节点进动率随 χ 单调增 let r = 6.0; let p_low = kerr_nodal_precession(r, 0.3); let p_high = kerr_nodal_precession(r, 0.9); assert!(p_high > p_low, "precession should grow with spin"); assert!(p_low > 0.0, "prograde precession should be positive"); } #[test] fn nodal_precession_strong_field_exceeds_weak_field() { // r<6 强场区: 精确 Ω_LT > 弱场近似 2Ma/r³ = χ/r³ (M=0.5) let r = 4.0; let chi = 0.9; let weak = chi / r.powi(3); // 2Ma/r³ = (2·0.5·χ)/r³ = χ/r³ let strong = kerr_nodal_precession(r, chi); assert!( strong > weak, "strong-field precession at r={} should exceed weak approx {} , got {}", r, weak, strong ); } ``` - [ ] **Step 2: 运行测试,确认失败** Run: `cargo test nodal_precession` Expected: FAIL,`cannot find function kerr_nodal_precession`。 - [ ] **Step 3: 实现 `kerr_nodal_precession`** 在 `kerr_orbital_frequency` 之后加: ```rust /// Kerr 赤道圆轨节点进动率 (Lense-Thirring, 强场精确). χ=0 返回 0. /// /// `Ω_LT = Ω_φ - Ω_θ`, 其中 Ω_θ 是垂直 epicyclic 频率: /// `Ω_θ² = Ω_φ² · (1 − 4a·Ω_φ/r + 3a²/r²)` (Caltech Ph236 lec27). /// χ=0 时 a=0, 括号=1, 故 Ω_θ=Ω_φ, 进动为零 (Schwarzschild 球对称). pub fn kerr_nodal_precession(r: f32, chi: f32) -> f32 { let m = 0.5; let a = chi * m; let omega_phi = kerr_orbital_frequency(r, chi); // 垂直 epicyclic 频率比 (>=0, 极端 r/a 组合下数值精度可能略负, 钳位) let ratio = (1.0 - 4.0 * a * omega_phi / r + 3.0 * a * a / (r * r)).max(0.0); let omega_theta = omega_phi * ratio.sqrt(); omega_phi - omega_theta } ``` - [ ] **Step 4: 运行测试,确认通过** Run: `cargo test nodal_precession` Expected: PASS,3 个测试通过。 - [ ] **Step 5: 运行全部测试确认无回归** Run: `cargo test` Expected: 全部通过(原有 capture/escape 测试 + 5 个新测试)。 - [ ] **Step 6: Commit** ```bash git add src/physics.rs git commit -m "feat(physics): Kerr nodal precession Ω_LT (strong-field Lense-Thirring)" ``` --- ## Task 2: OrbitParams 组件 + 轨道几何纯函数 先把不可变根数和"根数 + 时间 → 位置"的纯函数定下来。纯函数可独立测试,不依赖 Bevy 系统。 **Files:** - Modify: `src/scene/planets.rs`(文件顶部,`Planet` struct 之后) - Test: `src/scene/planets.rs` 内联 `#[cfg(test)]` mod - [ ] **Step 1: 加 `OrbitParams` 组件 + 轨道几何函数(含测试 mod)** 在 `src/scene/planets.rs` 的 `Planet` struct 定义之后(当前 `:8-13`)加: ```rust use std::f32::consts::{PI, TAU}; /// 轨道根数 (不变量, 启动时随机生成, 运行时不变除非 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) } ``` - [ ] **Step 2: 写几何不变量测试** 在 `src/scene/planets.rs` 文件末尾加测试 mod: ```rust #[cfg(test)] mod tests { use super::*; use std::f32::consts::TAU; #[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_plane_fixed() { // χ=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_advance_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"); } } ``` - [ ] **Step 3: 运行测试** Run: `cargo test --lib scene::planets` Expected: PASS,3 个几何测试通过。 注:若 `--lib` 选择器不工作,用 `cargo test orbit_position`。 - [ ] **Step 4: Commit** ```bash git add src/scene/planets.rs git commit -m "feat(planets): OrbitParams component + orbit_position geometry" ``` --- ## Task 3: orbit_system (每帧更新 Planet.center) 把纯函数接进 Bevy 调度,每帧写 `Planet.center`。 **Files:** - Modify: `src/scene/planets.rs` - [ ] **Step 1: 加 `orbit_system`** 在 `orbit_position` 函数之后加。注:`Time` 已在 `bevy::prelude::*` 里(`planets.rs:1` 已 import prelude,参考 `plugin.rs:582` 的 `time: Res