scenes.rs(共享场景与项目装配)
//! N-body gravitational simulation — a canonical "iterative only" animation:
//! no closed-form solution, chaotic, and inherently stateful across frames.
//!
//! `NBodyState::new(n)` places `n` equal masses on a regular n-gon with
//! tangential velocities (exact circular speed for the relative equilibrium,
//! plus a tiny asymmetry). The logical simulation duration is the local
//! `sim_secs` value captured by the step closure (not a global constant), and
//! the same value is passed to `with_duration`. Each body leaves a fading
//! trail of recent positions; the `Extract` impl projects bodies and trails
//! once per frame. Circles stay semantic and their positions live in
//! `Translation` wrappers applied at extraction time.
//!
//! Behavior by `n`: n = 3 stays inside the frame and wobbles chaotically for
//! the whole scene; n >= 4 destabilizes and ejects a body (a dramatic finale).
use ranim::{
color::{AlphaColor, Srgb, palettes::manim},
core::animation::eval::iterative::Iterative,
glam::DVec3,
items::vitem::geometry::Circle,
prelude::*,
};
use std::f64::consts::PI;
const G: f64 = 8.0;
const TRAIL_SAMPLE_EVERY: usize = 4; // sample a trail point every 4th step (~30 Hz)
const TRAIL_LEN: usize = 90; // ~3 s of trail per body
const PALETTE: [AlphaColor<Srgb>; 6] = [
manim::BLUE_C,
manim::RED_C,
manim::YELLOW_C,
manim::GREEN_C,
manim::PURPLE_C,
manim::ORANGE,
];
#[derive(Clone, Copy)]
struct Body {
pos: DVec3,
vel: DVec3,
mass: f64,
}
/// The N-body system's full state.
#[derive(Clone)]
struct NBodyState {
bodies: Vec<Body>,
trails: Vec<Vec<DVec3>>,
step_count: usize,
colors: Vec<AlphaColor<Srgb>>,
}
impl NBodyState {
/// Place `n` equal masses on a regular n-gon.
///
/// The circular speed is exact for the regular n-gon relative equilibrium:
/// `v² = G·Σ csc(πj/n) / (4R)`. A 4% overspeed plus a tiny asymmetry keeps
/// the system lively (and chaotic — the n-gon equilibrium is unstable).
fn new(n: usize) -> Self {
assert!(n >= 2, "nbody needs at least 2 bodies");
let radius = 2.0;
let csc_sum: f64 = (1..n).map(|j| 1.0 / (PI * j as f64 / n as f64).sin()).sum();
let v_circ = (G * csc_sum / (4.0 * radius)).sqrt();
let v0 = 1.04 * v_circ;
let mut bodies = Vec::with_capacity(n);
for i in 0..n {
let angle = PI * 2.0 * i as f64 / n as f64;
let pos = DVec3::new(radius * angle.cos(), radius * angle.sin(), 0.0);
let velocity = v0 * (1.0 + 0.001 * i as f64);
let vel = DVec3::new(-velocity * angle.sin(), velocity * angle.cos(), 0.0);
bodies.push(Body {
pos,
vel,
mass: 1.0,
});
}
let colors = (0..n).map(|i| PALETTE[i % PALETTE.len()]).collect();
Self {
bodies,
trails: vec![Vec::new(); n],
step_count: 0,
colors,
}
}
fn accelerations(&self) -> Vec<DVec3> {
let mut accs = vec![DVec3::ZERO; self.bodies.len()];
for (i, acc) in accs.iter_mut().enumerate() {
for j in 0..self.bodies.len() {
if i == j {
continue;
}
let r = self.bodies[j].pos - self.bodies[i].pos;
let d2 = r.length_squared();
let d = d2.sqrt();
*acc += r * (G * self.bodies[j].mass / (d2 * d));
}
}
accs
}
fn step(&mut self, dt: f64) {
// Velocity Verlet (symplectic): conserves energy far better than
// semi-implicit Euler for gravitational orbits.
let a0 = self.accelerations();
for (i, body) in self.bodies.iter_mut().enumerate() {
body.pos += body.vel * dt + a0[i] * (0.5 * dt * dt);
}
let a1 = self.accelerations();
for (i, body) in self.bodies.iter_mut().enumerate() {
body.vel += (a0[i] + a1[i]) * (0.5 * dt);
}
self.step_count += 1;
if self.step_count.is_multiple_of(TRAIL_SAMPLE_EVERY) {
for (i, trail) in self.trails.iter_mut().enumerate() {
trail.push(self.bodies[i].pos);
if trail.len() > TRAIL_LEN {
trail.remove(0);
}
}
}
}
}
#[cfg(feature = "render")]
impl ranim::render::extract::Extract for NBodyState {
fn extract(&self, ctx: &mut ranim::render::extract::ExtractContext<'_>) {
// Trails: dim dots at recent positions.
for (i, trail) in self.trails.iter().enumerate() {
for &p in trail {
let mut dot = Circle::new(0.02).transformed(Translation(p));
dot.set_fill_color(self.colors[i]);
dot.set_fill_opacity(0.15);
dot.set_stroke_opacity(0.0);
dot.extract(ctx);
}
}
// Bodies.
for (i, body) in self.bodies.iter().enumerate() {
let mut ball = Circle::new(0.32).transformed(Translation(body.pos));
ball.set_fill_color(self.colors[i]);
ball.set_stroke_opacity(0.0);
ball.extract(ctx);
}
}
}
#[scene]
#[wasm_demo_doc]
#[output(dir = "./output/nbody")]
fn nbody(r: &mut RanimScene) {
let sim_secs = 32.0;
r.play(CameraFrame::default().show().with_duration(sim_secs));
// n = 3: chaotic wobble that stays in frame; n >= 4: ejection finale.
r.play(
Iterative::from_fn(
NBodyState::new(3),
move |state: &mut NBodyState, _alpha: f64, delta_alpha: f64| {
state.step(sim_secs * delta_alpha);
},
)
.with_steps((sim_secs * 60.0) as usize)
.with_duration(sim_secs),
);
r.insert_time_mark(sim_secs / 2.0, TimeMark::Capture("preview.png".to_string()));
}
#[cfg(feature = "render")]
pub(crate) fn configure_render(app: &mut bevy_app::App) {
use ranim::render::extract::ExtractPlugin;
app.add_plugins(ExtractPlugin::<NBodyState>::default());
}
/// Register the scenes provided by this example.
pub struct ScenesPlugin;
impl ranim::Plugin for ScenesPlugin {
fn build(&self, app: &mut bevy_app::App) {
app.add_scenes([nbody::scene()]);
#[cfg(feature = "render")]
{
use ranim::{items::RanimItemsPlugin, render::RenderSetup};
app.insert_resource(RenderSetup::new(|render_app| {
render_app.add_plugins(RanimItemsPlugin);
configure_render(render_app);
}));
}
}
}
main.rs(平台入口)
mod scenes;
#[cfg(not(target_arch = "wasm32"))]
fn main() {
#[cfg(feature = "cmd-render")]
{
use ranim::prelude::*;
let mut app = App::new();
app.add_plugins(scenes::ScenesPlugin);
run_cli(app.build());
}
#[cfg(not(feature = "cmd-render"))]
compile_error!("native examples require the `cmd-render` feature (enabled by default)");
}
// The WASM module is driven by JavaScript through app().
#[cfg(target_arch = "wasm32")]
fn main() {}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn app() -> ranim::wasm::WasmApp {
use ranim::prelude::*;
let mut app = App::new();
app.add_plugins(scenes::ScenesPlugin);
ranim::wasm::WasmApp::new(app.build())
}