diff --git a/docs/superpowers/plans/2026-07-16-kerr-orbiting-planets.md b/docs/superpowers/plans/2026-07-16-kerr-orbiting-planets.md new file mode 100644 index 0000000..f8d9bf4 --- /dev/null +++ b/docs/superpowers/plans/2026-07-16-kerr-orbiting-planets.md @@ -0,0 +1,839 @@ +# 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