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.
352 lines
12 KiB
352 lines
12 KiB
#include "renderer.h" |
|
#include "raymath.h" |
|
#include <cmath> |
|
#include <cstdio> |
|
|
|
// Initialize raylib window |
|
void init_renderer(int width, int height, const char* title) { |
|
InitWindow(width, height, title); |
|
SetTargetFPS(60); |
|
} |
|
|
|
// Close raylib |
|
void close_renderer() { |
|
CloseWindow(); |
|
} |
|
|
|
// Setup the 3D camera |
|
void setup_camera(RenderState* render_state) { |
|
render_state->camera.position = (Vector3){ 0.0f, 50.0f, 100.0f }; |
|
render_state->camera.target = (Vector3){ 0.0f, 0.0f, 0.0f }; |
|
render_state->camera.up = (Vector3){ 0.0f, 1.0f, 0.0f }; |
|
render_state->camera.fovy = 45.0f; |
|
render_state->camera.projection = CAMERA_PERSPECTIVE; |
|
|
|
// Set scaling factors (same scale for distances and sizes) |
|
render_state->distance_scale = 1e-9; // Meters to scaled units (1 unit = 1 billion meters) |
|
render_state->size_scale = 1e-9; // Same scale for body sizes (minimum size still applies) |
|
render_state->show_info = true; |
|
} |
|
|
|
// Update camera with keyboard/mouse controls |
|
void update_camera(RenderState* render_state) { |
|
// Orbital camera rotation with arrow keys |
|
float camera_distance = Vector3Distance(render_state->camera.position, render_state->camera.target); |
|
float angle_speed = 0.02f; |
|
|
|
// Rotate around target |
|
if (IsKeyDown(KEY_LEFT)) { |
|
Vector3 pos = render_state->camera.position; |
|
float angle = angle_speed; |
|
float x = pos.x * cosf(angle) - pos.z * sinf(angle); |
|
float z = pos.x * sinf(angle) + pos.z * cosf(angle); |
|
render_state->camera.position.x = x; |
|
render_state->camera.position.z = z; |
|
} |
|
if (IsKeyDown(KEY_RIGHT)) { |
|
Vector3 pos = render_state->camera.position; |
|
float angle = -angle_speed; |
|
float x = pos.x * cosf(angle) - pos.z * sinf(angle); |
|
float z = pos.x * sinf(angle) + pos.z * cosf(angle); |
|
render_state->camera.position.x = x; |
|
render_state->camera.position.z = z; |
|
} |
|
|
|
// Zoom in/out with up/down keys |
|
if (IsKeyDown(KEY_UP) && camera_distance > 10.0f) { |
|
Vector3 direction = Vector3Subtract(render_state->camera.target, render_state->camera.position); |
|
direction = Vector3Normalize(direction); |
|
render_state->camera.position = Vector3Add(render_state->camera.position, Vector3Scale(direction, 2.0f)); |
|
} |
|
if (IsKeyDown(KEY_DOWN)) { |
|
Vector3 direction = Vector3Subtract(render_state->camera.position, render_state->camera.target); |
|
direction = Vector3Normalize(direction); |
|
render_state->camera.position = Vector3Add(render_state->camera.position, Vector3Scale(direction, 2.0f)); |
|
} |
|
|
|
// Toggle info display with I key |
|
if (IsKeyPressed(KEY_I)) { |
|
render_state->show_info = !render_state->show_info; |
|
} |
|
} |
|
|
|
// Transform from simulation coordinates (XY plane) to render coordinates (XZ plane) |
|
// Rotation matrix: 90 degrees around X-axis maps Y -> Z |
|
Vector3 sim_to_render(Vec3 pos, double scale) { |
|
return (Vector3){ |
|
(float)(pos.x * scale), |
|
(float)(-pos.z * scale), |
|
(float)(pos.y * scale) |
|
}; |
|
} |
|
|
|
void focus_camera(RenderState* render_state, CelestialBody* body, Vector3& offset) { |
|
Vector3 p = sim_to_render(body->position, render_state->distance_scale); |
|
render_state->camera.position = (Vector3){ p.x, p.y + offset.y, p.z + offset.z}; |
|
render_state->camera.target = p; |
|
} |
|
|
|
// Scale a radius for rendering (with minimum visible size) |
|
float scale_radius(double radius, double scale) { |
|
float scaled = (float)(radius * scale); |
|
float min_radius = 0.5f; // Minimum visible radius |
|
return (scaled > min_radius) ? scaled : min_radius; |
|
} |
|
|
|
// Render a single celestial body |
|
void render_body(CelestialBody* body, RenderState* render_state) { |
|
Vector3 position = sim_to_render(body->position, render_state->distance_scale); |
|
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); |
|
} |
|
|
|
struct OrbitalBasis { |
|
Vec3 periapsis_dir; |
|
Vec3 normal; |
|
Vec3 q_vec; |
|
}; |
|
|
|
static OrbitalBasis calculate_orbital_basis(Vec3 r_vec, Vec3 velocity, Vec3 e_vec) { |
|
OrbitalBasis basis; |
|
basis.periapsis_dir = vec3_normalize(e_vec); |
|
|
|
Vec3 h_vec = { |
|
r_vec.y * velocity.z - r_vec.z * velocity.y, |
|
r_vec.z * velocity.x - r_vec.x * velocity.z, |
|
r_vec.x * velocity.y - r_vec.y * velocity.x |
|
}; |
|
basis.normal = vec3_normalize(h_vec); |
|
|
|
basis.q_vec = { |
|
basis.normal.y * basis.periapsis_dir.z - basis.normal.z * basis.periapsis_dir.y, |
|
basis.normal.z * basis.periapsis_dir.x - basis.normal.x * basis.periapsis_dir.z, |
|
basis.normal.x * basis.periapsis_dir.y - basis.normal.y * basis.periapsis_dir.x |
|
}; |
|
|
|
return basis; |
|
} |
|
|
|
static Vec3 orbital_to_cartesian(double x, double y, OrbitalBasis basis, Vec3 parent_pos) { |
|
return { |
|
basis.periapsis_dir.x * x + basis.q_vec.x * y + parent_pos.x, |
|
basis.periapsis_dir.y * x + basis.q_vec.y * y + parent_pos.y, |
|
basis.periapsis_dir.z * x + basis.q_vec.z * y + parent_pos.z |
|
}; |
|
} |
|
|
|
static void draw_orbit_segment(double x1, double y1, double x2, double y2, |
|
OrbitalBasis basis, Vec3 parent_pos, |
|
RenderState* render_state, Color color) { |
|
Vec3 p1_sim = orbital_to_cartesian(x1, y1, basis, parent_pos); |
|
Vec3 p2_sim = orbital_to_cartesian(x2, y2, basis, parent_pos); |
|
|
|
Vector3 p1 = sim_to_render(p1_sim, render_state->distance_scale); |
|
Vector3 p2 = sim_to_render(p2_sim, render_state->distance_scale); |
|
|
|
DrawLine3D(p1, p2, color); |
|
} |
|
|
|
static void render_elliptical_orbit(double a, double e, OrbitalBasis basis, |
|
Vec3 parent_pos, 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(x1, y1, x2, y2, basis, parent_pos, render_state, color); |
|
} |
|
} |
|
|
|
static void render_hyperbolic_orbit(double p, double e, OrbitalBasis basis, |
|
Vec3 parent_pos, RenderState* render_state, Color color) { |
|
double max_true_anomaly = (e > 1.01) ? acos(-1.0 / e) * 0.95 : PI * 0.48; |
|
|
|
int segments = 60; |
|
for (int i = 0; i < segments; i++) { |
|
float theta1 = -max_true_anomaly + (float)i / segments * 2.0f * max_true_anomaly; |
|
float theta2 = -max_true_anomaly + (float)(i + 1) / segments * 2.0f * max_true_anomaly; |
|
|
|
double r1 = p / (1.0 + e * cos(theta1)); |
|
double r2 = p / (1.0 + e * cos(theta2)); |
|
|
|
double x1 = r1 * cos(theta1); |
|
double y1 = r1 * sin(theta1); |
|
double x2 = r2 * cos(theta2); |
|
double y2 = r2 * sin(theta2); |
|
|
|
draw_orbit_segment(x1, y1, x2, y2, basis, parent_pos, render_state, color); |
|
} |
|
} |
|
|
|
static void render_parabolic_orbit(double p, OrbitalBasis basis, |
|
Vec3 parent_pos, RenderState* render_state, Color color) { |
|
double max_true_anomaly = PI * 0.95; |
|
|
|
int segments = 80; |
|
for (int i = 0; i < segments; i++) { |
|
float theta1 = -max_true_anomaly + (float)i / segments * 2.0f * max_true_anomaly; |
|
float theta2 = -max_true_anomaly + (float)(i + 1) / segments * 2.0f * max_true_anomaly; |
|
|
|
double r1 = p / (1.0 + cos(theta1)); |
|
double r2 = p / (1.0 + cos(theta2)); |
|
|
|
double x1 = r1 * cos(theta1); |
|
double y1 = r1 * sin(theta1); |
|
double x2 = r2 * cos(theta2); |
|
double y2 = r2 * sin(theta2); |
|
|
|
draw_orbit_segment(x1, y1, x2, y2, basis, parent_pos, render_state, color); |
|
} |
|
} |
|
|
|
// Render orbit path for a body |
|
void render_orbit(CelestialBody* body, CelestialBody* parent, RenderState* render_state) { |
|
if (body->parent_index == -1 || parent == NULL) { |
|
return; |
|
} |
|
|
|
Vec3 r_vec = vec3_sub(body->position, parent->position); |
|
double r = vec3_magnitude(r_vec); |
|
double v = vec3_magnitude(body->velocity); |
|
|
|
if (r < 1.0) return; |
|
|
|
double mu = G * parent->mass; |
|
double specific_energy = (v * v) / 2.0 - mu / r; |
|
|
|
double v_squared = v * v; |
|
double r_dot_v = r_vec.x * body->velocity.x + r_vec.y * body->velocity.y + r_vec.z * body->velocity.z; |
|
Vec3 e_vec = { |
|
(v_squared - mu / r) * r_vec.x - r_dot_v * body->velocity.x, |
|
(v_squared - mu / r) * r_vec.y - r_dot_v * body->velocity.y, |
|
(v_squared - mu / r) * r_vec.z - r_dot_v * body->velocity.z |
|
}; |
|
double e = vec3_magnitude(e_vec) / mu; |
|
|
|
Color orbit_color = { |
|
(unsigned char)(body->color[0] * 128), |
|
(unsigned char)(body->color[1] * 128), |
|
(unsigned char)(body->color[2] * 128), |
|
128 |
|
}; |
|
|
|
OrbitalBasis basis = calculate_orbital_basis(r_vec, body->velocity, e_vec); |
|
|
|
if (e < 0.98) { |
|
double a = -mu / (2.0 * specific_energy); |
|
if (a <= 0.0) return; |
|
|
|
render_elliptical_orbit(a, e, basis, parent->position, 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(p, e, basis, parent->position, render_state, orbit_color); |
|
} else { |
|
Vec3 h_vec = { |
|
r_vec.y * body->velocity.z - r_vec.z * body->velocity.y, |
|
r_vec.z * body->velocity.x - r_vec.x * body->velocity.z, |
|
r_vec.x * body->velocity.y - r_vec.y * body->velocity.x |
|
}; |
|
double h_squared = h_vec.x * h_vec.x + h_vec.y * h_vec.y + h_vec.z * h_vec.z; |
|
double p = h_squared / mu; |
|
|
|
if (p <= 0.0) return; |
|
|
|
render_parabolic_orbit(p, basis, parent->position, render_state, orbit_color); |
|
} |
|
} |
|
|
|
// Render the entire simulation |
|
void render_simulation(SimulationState* sim, RenderState* render_state) { |
|
BeginDrawing(); |
|
ClearBackground(BLACK); |
|
|
|
BeginMode3D(render_state->camera); |
|
|
|
// Draw a subtle reference grid |
|
for (int i = -50; i <= 50; i++) { |
|
if (i == 0) continue; |
|
DrawLine3D((Vector3){(float)i * 10.0f, 0, -500.0f}, |
|
(Vector3){(float)i * 10.0f, 0, 500.0f}, |
|
(Color){20, 20, 20, 255}); |
|
DrawLine3D((Vector3){-500.0f, 0, (float)i * 10.0f}, |
|
(Vector3){500.0f, 0, (float)i * 10.0f}, |
|
(Color){20, 20, 20, 255}); |
|
} |
|
DrawLine3D((Vector3){0, 0, -500.0f}, (Vector3){0, 0, 500.0f}, (Color){40, 40, 40, 255}); |
|
DrawLine3D((Vector3){-500.0f, 0, 0}, (Vector3){500.0f, 0, 0}, (Color){40, 40, 40, 255}); |
|
|
|
// Render orbit paths first |
|
for (int i = 0; i < sim->body_count; i++) { |
|
CelestialBody* body = &sim->bodies[i]; |
|
if (body->parent_index >= 0 && body->parent_index < sim->body_count) { |
|
CelestialBody* parent = &sim->bodies[body->parent_index]; |
|
render_orbit(body, parent, render_state); |
|
} |
|
} |
|
|
|
// Render all bodies |
|
for (int i = 0; i < sim->body_count; i++) { |
|
render_body(&sim->bodies[i], render_state); |
|
} |
|
|
|
EndMode3D(); |
|
|
|
// Render 2D info overlay |
|
if (render_state->show_info) { |
|
render_info(sim, "solar_system.txt"); |
|
} |
|
|
|
EndDrawing(); |
|
} |
|
|
|
// Render simulation information overlay |
|
void render_info(SimulationState* sim, const char* config_name) { |
|
DrawText("Orbital Mechanics Simulation", 10, 10, 20, WHITE); |
|
|
|
char buffer[256]; |
|
|
|
// Simulation time (in days) |
|
double days = sim->time / 86400.0; // seconds to days |
|
snprintf(buffer, sizeof(buffer), "Time: %.2f days", days); |
|
DrawText(buffer, 10, 40, 16, LIGHTGRAY); |
|
|
|
// Body count |
|
snprintf(buffer, sizeof(buffer), "Bodies: %d", sim->body_count); |
|
DrawText(buffer, 10, 60, 16, LIGHTGRAY); |
|
|
|
// Config name |
|
snprintf(buffer, sizeof(buffer), "Config: %s", config_name); |
|
DrawText(buffer, 10, 80, 16, LIGHTGRAY); |
|
|
|
// FPS |
|
snprintf(buffer, sizeof(buffer), "FPS: %d", GetFPS()); |
|
DrawText(buffer, 10, 100, 16, LIGHTGRAY); |
|
|
|
// Controls |
|
DrawText("Controls:", 10, 130, 16, YELLOW); |
|
DrawText(" Arrows: Rotate/Zoom camera", 10, 150, 14, LIGHTGRAY); |
|
DrawText(" Space: Pause/Resume", 10, 170, 14, LIGHTGRAY); |
|
DrawText(" +/-: Speed up/slow down", 10, 190, 14, LIGHTGRAY); |
|
DrawText(" I: Toggle info", 10, 210, 14, LIGHTGRAY); |
|
DrawText(" ESC: Quit", 10, 230, 14, LIGHTGRAY); |
|
}
|
|
|