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
- Float Precision Loss: LEO orbit (6.8e6 m) scaled by 1e-9 → 0.0068 render units (5% of Earth radius)
- Camera State Clunkiness:
was_following_bodyandprevious_selected_bodyrequire frame-to-frame comparison - 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
- Precision: Use local coordinates with larger scale factor when following bodies
- Clarity: Maintain visual balance between bodies and their local orbits
- Cleanliness: Remove redundant state tracking and code duplication
- Extensibility: Enable future nested local frames if needed
Phase 0: Refactor update_camera()
Current Problems
- State tracking clunkiness:
was_following_bodyandprevious_selected_bodyrequire frame-to-frame comparison - Code duplication: Follow logic repeated 4x, rotation logic duplicated 2x, zoom logic duplicated 2x
- Offset updates scattered: Camera offset updated in 6 places with identical logic
Refactoring Strategy
Simplified State Management:
- Remove
was_following_bodyfromRenderState - Remove
previous_selected_bodyfromRenderState - Add
last_target_indextoRenderState(single int tracking body or spacecraft) - Add
camera_modeenum:CAMERA_FREE,CAMERA_FOLLOW_BODY,CAMERA_FOLLOW_CRAFT
New Helper Functions:
detect_camera_mode()→ returns camera mode from render stateget_camera_target()→ returns target position Vector3has_target_changed()→ checks last_target_index vs currentupdate_camera_target()→ handles all follow logicrotate_camera_orbitally()→ abstracts KEY_LEFT/RIGHTzoom_camera()→ abstracts KEY_UP/DOWNupdate_follow_offset()→ extracts repeated offset update logicupdate_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
- Start simulation with Earth selected
- Verify camera follows Earth in local frame
- Zoom in to see LEO orbit clearly visible
- Select Sun from UI → verify switch to global frame
- Rotate/zoom controls work in both frames
- Orbits render correctly in both frames
- Earth's orbit around Sun omitted in local frame
- Spacecraft billboard moves correctly with simulation
Design Decisions
-
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
-
Frame Levels: Single-level local frame only
- Following Earth shows Earth + spacecraft at LEO
- TODO: Support nested local frames (viewing Moon while following Earth)
-
Followed Body's Orbit: Omitted in local frame
- Since we're now the reference frame, Earth's orbit around Sun is confusing
-
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
-
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
CameraModeenum - Add
RenderFrameModeenum - Add fields to
RenderStatestruct:- Remove:
was_following_body,previous_selected_body - Add:
camera_mode,frame_mode,last_target_index,local_frame_parent_index
- Remove:
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_bodyinitialization - Remove:
previous_selected_bodyinitialization - Add: Initialize new RenderState fields (camera_mode, frame_mode, etc.)
- Remove:
src/ui_renderer.cpp
- Update references to removed state fields if any
- Ensure compatibility with new camera_mode system
TODO Items
- Nested Local Frames: Support 2-level local frames (viewing Moon while following Earth)
- Distant Bodies in Local Frame: Decide whether to render distant bodies using global coordinates or skip
- Smooth Frame Transitions: Add interpolation when switching between global and local frames
- Test Coverage: Add unit tests for local frame rendering after manual verification