311 SetConfigFlags(FLAG_MSAA_4X_HINT | FLAG_VSYNC_HINT);
312 InitWindow(SW, SH,
"N-Body Gravitational Simulation");
319 std::vector<Trail> trails(sim.
n());
322 bool trail_fade =
true;
324 float scale = scene_scale(current);
325 float cx = SIM_W * 0.5f;
326 float cy = SH * 0.5f;
328 std::deque<float> energy_hist;
329 constexpr int ENERGY_HIST = 400;
331 while (!WindowShouldClose()) {
336 trails.assign(sim.
n(),
Trail{});
337 scale = scene_scale(s);
345 if (IsKeyPressed(KEY_R)) switch_scenario(current);
346 if (IsKeyPressed(KEY_SPACE)) paused = !paused;
347 if (IsKeyPressed(KEY_V)) {
350 trails.assign(sim.
n(),
Trail{});
353 if (IsKeyPressed(KEY_F)) trail_fade = !trail_fade;
354 if (IsKeyPressed(KEY_EQUAL) || IsKeyPressed(KEY_KP_ADD))
355 speed_mult = std::min(speed_mult + 1, 16);
356 if (IsKeyPressed(KEY_MINUS) || IsKeyPressed(KEY_KP_SUBTRACT))
357 speed_mult = std::max(speed_mult - 1, 1);
361 double dt =
static_cast<double>(BASE_DT) / speed_mult;
362 int inner = std::max(1, speed_mult);
364 for (
int s = 0; s < inner; ++s) {
370 for (
auto& [j, last_] : ops) {
371 if (j != last_) std::swap(trails[j], trails[last_]);
378 for (
int i = 0; i < sim.
n(); ++i) {
380 if (sc.x > 0 && sc.x < SIM_W && sc.y > 0 && sc.y < SH)
381 trails[i].push(sc.x, sc.y);
384 float drift =
static_cast<float>(sim.
energy_drift() * 100.0);
385 energy_hist.push_front(drift);
386 if ((
int)energy_hist.size() > ENERGY_HIST) energy_hist.pop_back();
391 ClearBackground({13, 17, 23, 255});
393 BeginScissorMode(0, 0, SIM_W, SH);
396 for (
int i = 0; i < sim.
n(); ++i) {
397 const auto& tr = trails[i];
398 Color col = unpack_color(sim.
bodies[i].color);
399 int len =
static_cast<int>(tr.pts.size());
400 for (
int k = 0; k + 1 < len; ++k) {
402 float alpha = 1.0f -
static_cast<float>(k) / MAX_TRAIL;
403 col.a =
static_cast<uint8_t
>(alpha * alpha * 200);
405 DrawLineV(tr.pts[k], tr.pts[k+1], col);
410 for (
int i = 0; i < sim.
n(); ++i) {
412 Color col = unpack_color(sim.
bodies[i].color);
413 float r = sim.
bodies[i].display_radius;
414 DrawCircleV(sc, r * 2.2f, {col.r, col.g, col.b, 40});
415 DrawCircleV(sc, r * 1.4f, {col.r, col.g, col.b, 100});
416 DrawCircleV(sc, r, col);
421 DrawRectangle(SIM_W, 0, 1, SH, {48, 54, 61, 255});
424 const int PX = SIM_W + 14;
428 auto label = [&](
const char* txt, Color c = {139,148,158,255}) {
429 DrawText(txt, PX, py, 13, c);
432 auto value = [&](
const char* key,
const char* val) {
433 DrawText(key, PX, py, 13, {139,148,158,255});
434 DrawText(val, PX + 90, py, 13, {201,209,217,255});
438 DrawText(scenario_name(current), PX, py, 14, {88,166,255,255});
440 DrawLine(PX, py, SW-8, py, {48,54,61,255}); py += 10;
442 snprintf(buf,
sizeof(buf),
"%.2f", sim.
t);
444 snprintf(buf,
sizeof(buf),
"%dx", speed_mult);
446 value(
"integrator", sim.
use_verlet ?
"Verlet (symp)" :
"RK4");
447 snprintf(buf,
sizeof(buf),
"%d", sim.
n());
448 value(
"bodies", buf);
452 DrawLine(PX, py, SW-8, py, {48,54,61,255}); py += 10;
454 label(
"Energy drift (%)");
455 snprintf(buf,
sizeof(buf),
"%+.2e", sim.
energy_drift() * 100.0);
457 ? Color{63,185,80,255}
459 ? Color{255,166,64,255}
460 : Color{248,81,73,255};
461 DrawText(buf, PX, py, 14, drift_col);
464 if (!energy_hist.empty()) {
465 float plot_h = 70.0f;
466 float plot_w =
static_cast<float>(PANEL_W - 28);
467 float plot_x =
static_cast<float>(PX);
468 float plot_y =
static_cast<float>(py);
469 DrawRectangle((
int)plot_x, (
int)plot_y, (
int)plot_w, (
int)plot_h, {22,27,34,255});
470 DrawRectangleLines((
int)plot_x, (
int)plot_y, (
int)plot_w, (
int)plot_h, {48,54,61,255});
472 float max_abs = 0.01f;
473 for (
float vv : energy_hist) max_abs = std::max(max_abs, std::abs(vv));
476 int sz =
static_cast<int>(energy_hist.size());
477 for (
int k = 0; k + 1 < sz && k + 1 < (int)plot_w; ++k) {
478 float x0 = plot_x + plot_w - k - 1;
479 float x1 = plot_x + plot_w - k - 2;
480 float y0 = plot_y + plot_h*0.5f - energy_hist[k] / max_abs * (plot_h*0.45f);
481 float y1 = plot_y + plot_h*0.5f - energy_hist[k+1] / max_abs * (plot_h*0.45f);
482 DrawLineV({x0,y0}, {x1,y1}, drift_col);
484 DrawLine((
int)plot_x, (
int)(plot_y+plot_h*0.5f),
485 (
int)(plot_x+plot_w), (
int)(plot_y+plot_h*0.5f), {48,54,61,255});
486 py +=
static_cast<int>(plot_h) + 10;
490 double total_m = 0.0;
491 for (
int i = 0; i < sim.
n(); ++i) total_m += sim.
bodies[i].mass;
492 label(
"Merging simulation");
493 snprintf(buf,
sizeof(buf),
"%.1f", total_m);
494 value(
"total mass", buf);
496 label(
"** Fully collapsed! **", {255, 166, 64, 255});
502 DrawLine(PX, py, SW-8, py, {48,54,61,255}); py += 10;
503 label(
"Controls", {88,166,255,255});
505 label(
"2 Solar system");
507 label(
"4 Galaxy collapse");
508 label(
"V Verlet/RK4");
509 label(
"SPACE pause");
512 label(
"F trail fade");
515 DrawRectangle(SIM_W/2 - 50, SH/2 - 16, 100, 32, {0,0,0,160});
516 DrawText(
"PAUSED", SIM_W/2 - 38, SH/2 - 10, 20, {201,209,217,255});
@ Galaxy
Random N bodies — gravity only, bodies merge on contact.
@ Figure8
Chenciner-Montgomery figure-8 choreography (3 equal masses)
Gravitational N-body simulation using num::ode_verlet / num::ode_rk4.
void make_accel(const num::Vector &pos, num::Vector &acc) const
Fill acc from current positions (gravitational acceleration on each body).
num::Vector v
Velocities: [vx0,vy0, vx1,vy1, ...].
double energy_drift() const
Relative energy drift since reset: (E - E0) / |E0|.