vibe coding an orbital mechanics simulation to try out claude code
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

20 KiB

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:

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:

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:

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:

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:

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:

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:

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:

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):

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():

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:

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