diff --git a/docs/planning/local_rendering_frame.md b/docs/planning/local_rendering_frame.md new file mode 100644 index 0000000..e6ad658 --- /dev/null +++ b/docs/planning/local_rendering_frame.md @@ -0,0 +1,596 @@ +# Local Rendering Frame Implementation Plan + +## Overview +Implement local coordinate rendering when following bodies, using SOI-based scaling for improved orbit visibility. Refactor camera update logic to remove clunky state tracking. + +## Problem Statement + +### Current Issues +1. **Float Precision Loss**: LEO orbit (6.8e6 m) scaled by 1e-9 → 0.0068 render units (5% of Earth radius) +2. **Camera State Clunkiness**: `was_following_body` and `previous_selected_body` require frame-to-frame comparison +3. **Code Duplication**: Follow logic repeated 4x, rotation logic duplicated 2x, zoom logic duplicated 2x + +### Root Cause +- Global scale factor (1e-9) optimized for solar system view +- When zoomed in on local orbits, precision is insufficient for smooth visualization + +--- + +## Design Goals + +1. **Precision**: Use local coordinates with larger scale factor when following bodies +2. **Clarity**: Maintain visual balance between bodies and their local orbits +3. **Cleanliness**: Remove redundant state tracking and code duplication +4. **Extensibility**: Enable future nested local frames if needed + +--- + +## Phase 0: Refactor `update_camera()` + +### Current Problems +1. State tracking clunkiness: `was_following_body` and `previous_selected_body` require frame-to-frame comparison +2. Code duplication: Follow logic repeated 4x, rotation logic duplicated 2x, zoom logic duplicated 2x +3. Offset updates scattered: Camera offset updated in 6 places with identical logic + +### Refactoring Strategy + +**Simplified State Management:** +- Remove `was_following_body` from `RenderState` +- Remove `previous_selected_body` from `RenderState` +- Add `last_target_index` to `RenderState` (single int tracking body or spacecraft) +- Add `camera_mode` enum: `CAMERA_FREE`, `CAMERA_FOLLOW_BODY`, `CAMERA_FOLLOW_CRAFT` + +**New Helper Functions:** +1. `detect_camera_mode()` → returns camera mode from render state +2. `get_camera_target()` → returns target position Vector3 +3. `has_target_changed()` → checks last_target_index vs current +4. `update_camera_target()` → handles all follow logic +5. `rotate_camera_orbitally()` → abstracts KEY_LEFT/RIGHT +6. `zoom_camera()` → abstracts KEY_UP/DOWN +7. `update_follow_offset()` → extracts repeated offset update logic +8. `update_last_target()` → updates tracking for next frame + +**Refactored Structure:** +```cpp +void update_camera(RenderState* render_state, SimulationState* sim) { + // 1. Update frame mode (for later phases) + render_state->camera_mode = detect_camera_mode(render_state, sim); + + // 2. Handle following (handles both global and local frames) + if (render_state->camera_follow_body) { + update_camera_target(render_state, sim); + } + + // 3. Handle rotation + if (IsKeyDown(KEY_LEFT)) { + rotate_camera_orbitally(render_state, angle_speed); + } else if (IsKeyDown(KEY_RIGHT)) { + rotate_camera_orbitally(render_state, -angle_speed); + } + + // 4. Handle zoom + if (IsKeyDown(KEY_UP)) { + zoom_camera(render_state, -2.0f); + } else if (IsKeyDown(KEY_DOWN)) { + zoom_camera(render_state, 2.0f); + } + + // 5. Update last target for next frame + update_last_target(render_state); +} +``` + +--- + +## Phase 1: Infrastructure Setup + +### 1.1 Add Rendering Mode Enum and Fields + +**src/renderer.h:** +```cpp +enum CameraMode { + CAMERA_FREE, + CAMERA_FOLLOW_BODY, + CAMERA_FOLLOW_CRAFT +}; + +enum RenderFrameMode { + RENDER_FRAME_GLOBAL, + RENDER_FRAME_LOCAL +}; + +struct RenderState { + // ... existing fields ... + CameraMode camera_mode; + RenderFrameMode frame_mode; + int last_target_index; // Tracks body or craft index (negative = spacecraft) + int local_frame_parent_index; // Body index for local frame +}; +``` + +### 1.2 Add Detection and Scale Functions + +**New functions in src/renderer.cpp:** + +```cpp +static CameraMode detect_camera_mode(RenderState* render_state, SimulationState* sim) { + if (!render_state->camera_follow_body) return CAMERA_FREE; + if (render_state->selected_body_index >= 0) return CAMERA_FOLLOW_BODY; + if (render_state->selected_craft_index >= 0) return CAMERA_FOLLOW_CRAFT; + return CAMERA_FREE; +} + +static RenderFrameMode detect_render_frame_mode(CameraMode mode, int body_index, SimulationState* sim) { + if (mode != CAMERA_FOLLOW_BODY) return RENDER_FRAME_GLOBAL; + if (body_index < 0 || body_index >= sim->body_count) return RENDER_FRAME_GLOBAL; + if (sim->bodies[body_index].parent_index < 0) return RENDER_FRAME_GLOBAL; // Root body (Sun) + return RENDER_FRAME_LOCAL; +} + +// Target: SOI occupies ~100 units in render space for visibility +static double get_local_frame_scale(SimulationState* sim, int body_index) { + CelestialBody* body = &sim->bodies[body_index]; + double soi_radius = body->soi_radius; + + // Target: 1.0 × SOI radius → 100.0 render units + return 100.0 / soi_radius; +} +``` + +--- + +## Phase 2: Local Frame Coordinate Transformation + +### 2.1 Add Local Transform Function + +**src/renderer.cpp:** +```cpp +static Vector3 sim_to_render_local(Vec3 local_pos, double local_scale) { + return (Vector3){ + (float)(local_pos.x * local_scale), + (float)(local_pos.z * local_scale), + (float)(-local_pos.y * local_scale) + }; +} +``` + +### 2.2 Modify Orbit Rendering for Local Frame + +**Add new function:** +```cpp +static void render_orbit_local(Vec3 local_position, Vec3 local_velocity, + double parent_mass, Color orbit_color, + double local_scale, RenderState* render_state) { + Vec3 r_vec = local_position; + double r = vec3_magnitude(r_vec); + double v = vec3_magnitude(local_velocity); + + if (r < 1.0) return; + + // Calculate orbit parameters (same as global version) + double mu = G * parent_mass; + double specific_energy = (v * v) / 2.0 - mu / r; + double v_squared = v * v; + double r_dot_v = vec3_dot(r_vec, local_velocity); + Vec3 e_vec = { + (v_squared - mu / r) * r_vec.x - r_dot_v * local_velocity.x, + (v_squared - mu / r) * r_vec.y - r_dot_v * local_velocity.y, + (v_squared - mu / r) * r_vec.z - r_dot_v * local_velocity.z + }; + double e = vec3_magnitude(e_vec) / mu; + + OrbitalBasis basis = calculate_orbital_basis(r_vec, local_velocity, e_vec); + + // Render with local scale and origin at (0,0,0) + if (e < 0.98) { + double a = -mu / (2.0 * specific_energy); + if (a <= 0.0) return; + render_elliptical_orbit_local(a, e, basis, local_scale, render_state, orbit_color); + } else if (e > 1.02) { + double a = mu / (2.0 * (-specific_energy)); + double p = a * (1.0 - e * e); + if (p <= 0.0) return; + render_hyperbolic_orbit_local(p, e, basis, local_scale, render_state, orbit_color); + } else { + Vec3 h_vec = vec3_cross(r_vec, local_velocity); + double h_squared = vec3_dot(h_vec, h_vec); + double p = h_squared / mu; + if (p <= 0.0) return; + render_parabolic_orbit_local(p, basis, local_scale, render_state, orbit_color); + } +} +``` + +**Add local orbit drawing functions:** +```cpp +static void draw_orbit_segment_local(double x1, double y1, double x2, double y2, + OrbitalBasis basis, double local_scale, + RenderState* render_state, Color color) { + // Convert from orbital plane to render coords (no parent offset) + Vec3 p1_local = { + basis.periapsis_dir.x * x1 + basis.q_vec.x * y1, + basis.periapsis_dir.y * x1 + basis.q_vec.y * y1, + basis.periapsis_dir.z * x1 + basis.q_vec.z * y1 + }; + Vec3 p2_local = { + basis.periapsis_dir.x * x2 + basis.q_vec.x * y2, + basis.periapsis_dir.y * x2 + basis.q_vec.y * y2, + basis.periapsis_dir.z * x2 + basis.q_vec.z * y2 + }; + + Vector3 p1 = sim_to_render_local(p1_local, local_scale); + Vector3 p2 = sim_to_render_local(p2_local, local_scale); + + DrawLine3D(p1, p2, color); +} + +static void render_elliptical_orbit_local(double a, double e, OrbitalBasis basis, + double local_scale, RenderState* render_state, Color color) { + double b = a * sqrt(1.0 - e * e); + double c = a * e; + int segments = 100; + + for (int i = 0; i < segments; i++) { + float theta1 = (float)i / segments * 2.0f * PI; + float theta2 = (float)(i + 1) / segments * 2.0f * PI; + double x1 = a * cos(theta1) - c; + double y1 = b * sin(theta1); + double x2 = a * cos(theta2) - c; + double y2 = b * sin(theta2); + draw_orbit_segment_local(x1, y1, x2, y2, basis, local_scale, render_state, color); + } +} +``` + +(Also add `render_hyperbolic_orbit_local()` and `render_parabolic_orbit_local()` - similar to existing global versions but using `draw_orbit_segment_local()`) + +--- + +## Phase 3: Local Frame Body Rendering + +### 3.1 Add Local Body Rendering Function + +**src/renderer.cpp:** +```cpp +static void render_body_local(CelestialBody* body, int local_parent_index, + double local_scale, RenderState* render_state) { + Vector3 position; + + if (body->parent_index == local_parent_index) { + // Direct child of followed body - use local position + position = sim_to_render_local(body->local_position, local_scale); + } else if (body->parent_index < 0) { + // Root body (Sun) - at origin in local frame + position = (Vector3){0.0f, 0.0f, 0.0f}; + } else { + // Other bodies - TODO: decide whether to render or skip + // For now: skip (will be very far off-screen) + return; + } + + float radius = scale_radius(body->radius, render_state->size_scale); + Color color = { + (unsigned char)(body->color[0] * 255), + (unsigned char)(body->color[1] * 255), + (unsigned char)(body->color[2] * 255), + 255 + }; + + DrawSphereWires(position, radius, 16, 16, color); +} +``` + +--- + +## Phase 4: Integration - Render Simulation Router + +### 4.1 Modify `render_simulation()` to Route by Frame Mode + +**src/renderer.cpp:** +```cpp +void render_simulation(SimulationState* sim, RenderState* render_state) { + // Update rendering mode + render_state->camera_mode = detect_camera_mode(render_state, sim); + render_state->frame_mode = detect_render_frame_mode( + render_state->camera_mode, + render_state->selected_body_index, + sim + ); + + BeginMode3D(render_state->camera); + + // Draw reference grid (in both modes) + for (int i = -50; i <= 50; i++) { + // ... existing grid code ... + } + + // Route to appropriate rendering function + if (render_state->frame_mode == RENDER_FRAME_LOCAL) { + render_simulation_local(sim, render_state); + } else { + render_simulation_global(sim, render_state); // existing implementation + } + + EndMode3D(); + + // Spacecraft and maneuvers (screen-space, shared between modes) + for (int i = 0; i < sim->craft_count; i++) { + render_spacecraft_screen_space(&sim->spacecraft[i], render_state); + } + // ... maneuver markers ... +} + +static void render_simulation_local(SimulationState* sim, RenderState* render_state) { + int parent_index = render_state->local_frame_parent_index; + double local_scale = get_local_frame_scale(sim, parent_index); + + // Render followed body at origin + CelestialBody* parent = &sim->bodies[parent_index]; + Vector3 origin = (Vector3){0.0f, 0.0f, 0.0f}; + float parent_radius = scale_radius(parent->radius, render_state->size_scale); + Color parent_color = { + (unsigned char)(parent->color[0] * 255), + (unsigned char)(parent->color[1] * 255), + (unsigned char)(parent->color[2] * 255), + 255 + }; + DrawSphereWires(origin, parent_radius, 16, 16, parent_color); + + // Render orbits of children (not followed body's orbit) + for (int i = 0; i < sim->body_count; i++) { + CelestialBody* body = &sim->bodies[i]; + if (body->parent_index == parent_index) { + render_orbit_local(body->local_position, body->local_velocity, + parent->mass, get_body_orbit_color(body), + local_scale, render_state); + } + } + + // Render spacecraft orbits + for (int i = 0; i < sim->craft_count; i++) { + Spacecraft* craft = &sim->spacecraft[i]; + if (craft->parent_index == parent_index) { + render_orbit_local(craft->local_position, craft->local_velocity, + parent->mass, (Color){0, 255, 255, 128}, + local_scale, render_state); + } + } + + // Render children bodies + for (int i = 0; i < sim->body_count; i++) { + if (i != parent_index) { + render_body_local(&sim->bodies[i], parent_index, local_scale, render_state); + } + } +} +``` + +**Extract current `render_simulation()` into `render_simulation_global()`** + +--- + +## Phase 5: Camera Integration + +### 5.1 Modify `update_camera()` for Frame Transitions + +**Add to `update_camera()` helper functions (Phase 0 refactor):** + +```cpp +static void update_camera_frame_mode(RenderState* render_state, SimulationState* sim) { + // Detect if we need to switch frame modes + int new_parent_index = -1; + + if (render_state->camera_mode == CAMERA_FOLLOW_BODY && + render_state->selected_body_index >= 0) { + // Check if following a non-root body + int body_index = render_state->selected_body_index; + if (body_index < sim->body_count && + sim->bodies[body_index].parent_index >= 0) { + new_parent_index = body_index; + } + } + + // Check for mode switch + bool mode_changed = (new_parent_index != render_state->local_frame_parent_index); + + if (mode_changed) { + render_state->local_frame_parent_index = new_parent_index; + render_state->frame_mode = detect_render_frame_mode( + render_state->camera_mode, + render_state->selected_body_index, + sim + ); + + // When switching to local frame: set camera target to origin + if (render_state->frame_mode == RENDER_FRAME_LOCAL) { + render_state->camera.target = (Vector3){0.0f, 0.0f, 0.0f}; + } + // When switching to global frame: target set by update_camera_target() + } +} +``` + +**Integrate into refactored `update_camera()`:** +```cpp +void update_camera(RenderState* render_state, SimulationState* sim) { + // 0. Update frame mode + render_state->camera_mode = detect_camera_mode(render_state, sim); + update_camera_frame_mode(render_state, sim); + + // 1. Handle following (handles both global and local frames) + if (render_state->camera_follow_body) { + update_camera_target(render_state, sim); + } + + // 2. Handle rotation + // ... existing rotation logic ... + + // 3. Handle zoom + // ... existing zoom logic ... +} +``` + +**Update `update_camera_target()` to handle local frame:** +```cpp +static void update_camera_target(RenderState* render_state, SimulationState* sim) { + Vector3 target_pos; + bool has_target = false; + + if (render_state->frame_mode == RENDER_FRAME_LOCAL) { + // Local frame: target is always origin + target_pos = (Vector3){0.0f, 0.0f, 0.0f}; + has_target = true; + } else { + // Global frame: get target from body or spacecraft + if (render_state->selected_body_index >= 0 && + render_state->selected_body_index < sim->body_count) { + CelestialBody* body = &sim->bodies[render_state->selected_body_index]; + target_pos = sim_to_render(body->global_position, render_state->distance_scale); + has_target = true; + } else if (render_state->selected_craft_index >= 0 && + render_state->selected_craft_index < sim->craft_count) { + Spacecraft* craft = &sim->spacecraft[render_state->selected_craft_index]; + target_pos = sim_to_render(craft->global_position, render_state->distance_scale); + has_target = true; + } + } + + if (has_target) { + // Check if target changed + bool target_changed = has_target_changed(render_state); + + if (target_changed) { + // Preserve camera distance by updating offset + Vector3 to_camera = Vector3Subtract(render_state->camera.position, target_pos); + render_state->camera_offset = to_camera; + } + + render_state->camera.target = target_pos; + render_state->camera.position = Vector3Add(target_pos, render_state->camera_offset); + } +} +``` + +--- + +## Phase 6: Testing (After Implementation) + +### 6.1 Manual Testing Checklist +1. Start simulation with Earth selected +2. Verify camera follows Earth in local frame +3. Zoom in to see LEO orbit clearly visible +4. Select Sun from UI → verify switch to global frame +5. Rotate/zoom controls work in both frames +6. Orbits render correctly in both frames +7. Earth's orbit around Sun omitted in local frame +8. Spacecraft billboard moves correctly with simulation + +--- + +## Design Decisions + +1. **Local Frame Scale Factor**: SOI-based scaling targeting ~100 render units + - Defined as constant at top of renderer.cpp + - Allows easy adjustment based on visual feedback + +2. **Frame Levels**: Single-level local frame only + - Following Earth shows Earth + spacecraft at LEO + - TODO: Support nested local frames (viewing Moon while following Earth) + +3. **Followed Body's Orbit**: Omitted in local frame + - Since we're now the reference frame, Earth's orbit around Sun is confusing + +4. **Other Bodies in Local Frame**: Skipped for now (TODO) + - Distant bodies will be very far off-screen + - TODO: Decide whether to render using global coordinates or skip + +5. **Transition Behavior**: Instant switch between frames + - TODO: Add smooth interpolation if jarring + +--- + +## Expected Outcomes + +### LEO Orbit Visibility (400km altitude) +- **Current**: 0.0068 render units (5% of Earth radius) +- **After Local Frame**: ~67 render units (50% of Earth radius) +- **Improvement**: ~1000x more visible + +### Float Precision +- Local frame positions: ~6.8e6 m → 67 render units (scale 1e-7) +- Precision at 67 units: ~0.0001 (1/670000) +- Orbit precision: 67 × 0.0001 = 0.0067 units +- Result: High precision, smooth orbits + +### Code Quality +- Reduced camera duplication by ~60% +- Eliminated clunky state tracking +- Clear separation of global/local rendering +- Extensible for future nested local frames + +--- + +## File Summary + +### New Files +- `docs/planning/local_rendering_frame.md` - This planning document + +### Modified Files + +#### src/renderer.h +- Add `CameraMode` enum +- Add `RenderFrameMode` enum +- Add fields to `RenderState` struct: + - Remove: `was_following_body`, `previous_selected_body` + - Add: `camera_mode`, `frame_mode`, `last_target_index`, `local_frame_parent_index` + +#### src/renderer.cpp +- **Phase 0**: Refactor update_camera() with helper functions: + - Add SOI_SCALE_TARGET define at top + - Implement helper functions: detect_camera_mode, get_camera_target, has_target_changed, + update_camera_target, rotate_camera_orbitally, zoom_camera, update_follow_offset, update_last_target + - Refactor update_camera() to use helpers + +- **Phase 1**: Add detection/scale functions: + - Add detect_render_frame_mode() + - Add get_local_frame_scale() + - Add sim_to_render_local() + +- **Phase 2**: Add local coordinate transformation functions: + - Add draw_orbit_segment_local() + - Add render_elliptical_orbit_local() + - Add render_hyperbolic_orbit_local() + - Add render_parabolic_orbit_local() + - Add render_orbit_local() + +- **Phase 3**: Add local body rendering: + - Add render_body_local() + +- **Phase 4**: Modify render_simulation() routing: + - Extract current logic to render_simulation_global() + - Add render_simulation_local() + - Modify render_simulation() to route by frame mode + +- **Phase 5**: Update camera frame mode handling: + - Add update_camera_frame_mode() + - Update update_camera_target() to handle local frame + - Integrate frame mode updates into camera update + +#### src/main.cpp +- Update initialization to remove old state tracking: + - Remove: `was_following_body` initialization + - Remove: `previous_selected_body` initialization + - Add: Initialize new RenderState fields (camera_mode, frame_mode, etc.) + +#### src/ui_renderer.cpp +- Update references to removed state fields if any +- Ensure compatibility with new camera_mode system + +--- + +## TODO Items + +1. **Nested Local Frames**: Support 2-level local frames (viewing Moon while following Earth) +2. **Distant Bodies in Local Frame**: Decide whether to render distant bodies using global coordinates or skip +3. **Smooth Frame Transitions**: Add interpolation when switching between global and local frames +4. **Test Coverage**: Add unit tests for local frame rendering after manual verification diff --git a/src/renderer.h b/src/renderer.h index 4b757b5..0cf6a95 100644 --- a/src/renderer.h +++ b/src/renderer.h @@ -6,6 +6,19 @@ #include "maneuver.h" #include "raylib.h" +// Camera tracking modes +enum TrackingMode { + TRACKING_FREE, + TRACKING_BODY, + TRACKING_CRAFT +}; + +// Rendering coordinate frame modes +enum RenderFrameMode { + RENDER_FRAME_GLOBAL, + RENDER_FRAME_LOCAL +}; + // Rendering state struct RenderState { Camera3D camera; @@ -13,10 +26,12 @@ struct RenderState { double size_scale; // Scale factor for body sizes int selected_body_index; // -1 = no selection int selected_craft_index; // -1 = no selection - int previous_selected_body; // Previous selected body index - bool camera_follow_body; // Whether camera follows selected body + bool camera_target_enabled; // Whether camera follows selected body/craft Vector3 camera_offset; // Offset from target when following body - bool was_following_body; // Previous frame follow state + TrackingMode camera_mode; + RenderFrameMode frame_mode; + int last_target_index; // Tracks body or craft index (negative = spacecraft) + int local_frame_parent_index; // Body index for local frame Texture2D spacecraft_texture; // Shared texture for all spacecraft Texture2D maneuver_textures[6]; // One per burn direction