use kiss3d::prelude::*;
const NUM_BODIES: usize = 128;
const TRAIL_LENGTH: usize = 256;
const G: f32 = 1.0; const EPSILON: f32 = 0.5; const DT: f32 = 0.01; const SUBSTEPS: usize = 2;
struct Body {
position: Vec3,
velocity: Vec3,
mass: f32,
}
struct Trail {
positions: Vec<Vec3>,
head: usize,
len: usize,
}
impl Trail {
fn new() -> Self {
Self {
positions: vec![Vec3::ZERO; TRAIL_LENGTH],
head: 0,
len: 0,
}
}
fn push(&mut self, pos: Vec3) {
self.positions[self.head] = pos;
self.head = (self.head + 1) % TRAIL_LENGTH;
if self.len < TRAIL_LENGTH {
self.len += 1;
}
}
fn copy_to(&self, dest: &mut Vec<Vec3>) {
dest.clear();
if self.len == 0 {
return;
}
let start = if self.len < TRAIL_LENGTH {
0
} else {
self.head
};
for i in 0..self.len {
let idx = (start + i) % TRAIL_LENGTH;
dest.push(self.positions[idx]);
}
}
}
fn hsl_to_rgb(h: f32, s: f32, l: f32) -> Vec3 {
let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
let h_prime = h / 60.0;
let x = c * (1.0 - ((h_prime % 2.0) - 1.0).abs());
let m = l - c / 2.0;
let (r, g, b) = if h_prime < 1.0 {
(c, x, 0.0)
} else if h_prime < 2.0 {
(x, c, 0.0)
} else if h_prime < 3.0 {
(0.0, c, x)
} else if h_prime < 4.0 {
(0.0, x, c)
} else if h_prime < 5.0 {
(x, 0.0, c)
} else {
(c, 0.0, x)
};
Vec3::new(r + m, g + m, b + m)
}
struct Rng {
state: u32,
}
impl Rng {
fn new(seed: u32) -> Self {
Self { state: seed }
}
fn next_f32(&mut self) -> f32 {
self.state ^= self.state << 13;
self.state ^= self.state >> 17;
self.state ^= self.state << 5;
(self.state as f32) / (u32::MAX as f32)
}
}
fn init_bodies() -> Vec<Body> {
use std::f32::consts::TAU;
let mut rng = Rng::new(42);
let mut bodies = Vec::with_capacity(NUM_BODIES);
bodies.push(Body {
position: Vec3::ZERO,
velocity: Vec3::ZERO,
mass: 100.0,
});
for _ in 1..NUM_BODIES {
let angle = rng.next_f32() * TAU;
let radius = 1.0 + rng.next_f32() * 8.0;
let height = (rng.next_f32() - 0.5) * 1.0;
let position = Vec3::new(radius * angle.cos(), height, radius * angle.sin());
let orbital_speed = (G * 100.0 / radius).sqrt() * (0.9 + rng.next_f32() * 0.2);
let tangent = Vec3::new(-angle.sin(), 0.0, angle.cos());
let velocity = tangent * orbital_speed;
let mass = 0.01 + rng.next_f32() * 0.1;
bodies.push(Body {
position,
velocity,
mass,
});
}
bodies
}
fn init_polylines() -> Vec<Polyline3d> {
let mut rng = Rng::new(42);
let mut polylines = Vec::with_capacity(NUM_BODIES);
polylines.push(
Polyline3d::new(Vec::with_capacity(TRAIL_LENGTH))
.with_color(Color::new(1.0, 1.0, 0.5, 1.0))
.with_width(3.0),
);
for i in 1..NUM_BODIES {
let _ = rng.next_f32(); let _ = rng.next_f32(); let _ = rng.next_f32(); let _ = rng.next_f32(); let _ = rng.next_f32();
let hue = (i as f32 / NUM_BODIES as f32) * 360.0;
let color = hsl_to_rgb(hue, 1.0, 0.6);
let line_width = 0.5 + rng.next_f32() * 9.5;
polylines.push(
Polyline3d::new(Vec::with_capacity(TRAIL_LENGTH))
.with_color(Color::new(color.x, color.y, color.z, 1.0))
.with_width(line_width),
);
}
polylines
}
fn compute_acceleration(bodies: &[Body], i: usize) -> Vec3 {
let mut acceleration = Vec3::ZERO;
let pos_i = bodies[i].position;
for (j, body_j) in bodies.iter().enumerate() {
if i == j {
continue;
}
let offset = body_j.position - pos_i;
let dist_sq = offset.length_squared() + EPSILON * EPSILON;
let dist = dist_sq.sqrt();
let force_mag = G * body_j.mass / dist_sq;
acceleration += offset / dist * force_mag;
}
acceleration
}
fn physics_step(bodies: &mut [Body], dt: f32) {
let n = bodies.len();
let accelerations: Vec<Vec3> = (0..n).map(|i| compute_acceleration(bodies, i)).collect();
for (i, body) in bodies.iter_mut().enumerate() {
body.position += body.velocity * dt + accelerations[i] * (0.5 * dt * dt);
}
let new_accelerations: Vec<Vec3> = (0..n).map(|i| compute_acceleration(bodies, i)).collect();
for (i, body) in bodies.iter_mut().enumerate() {
body.velocity += (accelerations[i] + new_accelerations[i]) * (0.5 * dt);
}
}
#[kiss3d::main]
async fn main() {
let mut window = Window::new("Kiss3d: N-Body Polyline Simulation").await;
let mut scene = SceneNode3d::empty();
scene
.add_light(Light::point(100.0))
.set_position(Vec3::new(0.0, 10.0, 10.0));
window.set_background_color(Color::new(0.02, 0.02, 0.05, 1.0));
let eye = Vec3::new(0.0, 15.0, 25.0);
let at = Vec3::ZERO;
let mut camera = OrbitCamera3d::new(eye, at);
let mut bodies = init_bodies();
let mut trails: Vec<Trail> = (0..NUM_BODIES).map(|_| Trail::new()).collect();
let mut polylines = init_polylines();
#[allow(unused_mut)] let mut perspective_mode = false;
while window.render_3d(&mut scene, &mut camera).await {
#[cfg(feature = "egui")]
window.draw_ui(|ctx| {
egui::Window::new("Settings")
.default_pos([10.0, 10.0])
.show(ctx, |ui| {
ui.checkbox(&mut perspective_mode, "Perspective mode");
ui.label("When enabled, lines get thinner with distance");
});
});
let sub_dt = DT / SUBSTEPS as f32;
for _ in 0..SUBSTEPS {
physics_step(&mut bodies, sub_dt);
}
for (i, body) in bodies.iter().enumerate() {
trails[i].push(body.position);
}
for (i, trail) in trails.iter().enumerate() {
trail.copy_to(&mut polylines[i].vertices);
polylines[i].perspective = perspective_mode;
if polylines[i].vertices.len() >= 2 {
window.draw_polyline(&polylines[i]);
}
}
}
}