scenes.rs(共享场景与项目装配)
//! Zero-gravity cloth simulation: a free-floating spring net wrapped by a ball.
//!
//! Iterative-only: spring forces, self-collision and ball-cloth collision have
//! no closed form; all state is carried across frames by `step`.
//!
//! The cloth is unpinned and weightless, so it floats perfectly flat (no
//! self-weight sag). A ball is driven straight down through it: the cloth
//! visibly wraps around the ball and is dragged down, then settles draped over
//! it. The cloth is rendered as a shaded `MeshItem` surface (smooth normals
//! each frame); the ball is a static sphere mesh moved by its transform.
use ranim::{
color::palettes::manim,
core::animation::eval::iterative::Iterative,
core::components::rgba::Rgba,
glam::{DVec3, dvec3},
items::mesh::{MeshItem, Sphere, Surface},
prelude::*,
};
const ROWS: usize = 16;
const COLS: usize = 16;
const SPACING: f64 = 0.22;
const CLOTH_Y: f64 = 2.2;
const DAMPING: f64 = 0.99;
const K_STRUCTURAL: f64 = 600.0;
const K_SHEAR: f64 = 420.0;
const K_BEND: f64 = 220.0;
const REPULSION_CUTOFF: f64 = 0.35 * SPACING;
const REPULSION_K: f64 = 300.0;
const BALL_RADIUS: f64 = 0.65;
/// The ball is driven straight down from `BALL_START_Y` at this speed (units/s).
const BALL_SPEED: f64 = 1.2;
const BALL_START_Y: f64 = 3.0;
/// Where the ball stops, just below the cloth, letting the cloth settle draped over it.
const BALL_STOP_Y: f64 = 1.4;
/// The cloth-plus-ball system's full state.
#[derive(Clone)]
struct ClothState {
curr: Vec<DVec3>,
prev: Vec<DVec3>,
springs: Vec<(usize, usize, f64, f64)>, // (i, j, rest_len, stiffness)
ball_y: f64,
ball_mesh: MeshItem,
cloth_indices: Vec<u32>,
}
impl ClothState {
fn new() -> Self {
let width = (COLS - 1) as f64 * SPACING;
let mut curr = Vec::with_capacity(ROWS * COLS);
for r in 0..ROWS {
for c in 0..COLS {
curr.push(dvec3(
-width / 2.0 + c as f64 * SPACING,
CLOTH_Y,
-width / 2.0 + r as f64 * SPACING,
));
}
}
let mut springs = Vec::new();
for r in 0..ROWS {
for c in 0..COLS {
let i = r * COLS + c;
if c + 1 < COLS {
springs.push((i, i + 1, SPACING, K_STRUCTURAL));
}
if r + 1 < ROWS {
springs.push((i, i + COLS, SPACING, K_STRUCTURAL));
}
if c + 1 < COLS && r + 1 < ROWS {
springs.push((i, i + COLS + 1, SPACING * 2.0f64.sqrt(), K_SHEAR));
}
if c + 2 < COLS {
springs.push((i, i + 2, 2.0 * SPACING, K_BEND));
}
if r + 2 < ROWS {
springs.push((i, i + 2 * COLS, 2.0 * SPACING, K_BEND));
}
}
}
// Grid quads -> two triangles each, wound for +Y normals on the flat cloth.
let mut cloth_indices = Vec::with_capacity(6 * (ROWS - 1) * (COLS - 1));
for r in 0..ROWS - 1 {
for c in 0..COLS - 1 {
let a = (r * COLS + c) as u32;
let b = (r * COLS + c + 1) as u32;
let d = ((r + 1) * COLS + c) as u32;
let e = ((r + 1) * COLS + c + 1) as u32;
cloth_indices.extend_from_slice(&[a, e, b, a, d, e]);
}
}
let ball_mesh = MeshItem::from(
Surface::from(
Sphere::new(BALL_RADIUS)
.with_resolution((20, 12))
.with_fill_color(manim::RED_C),
)
.with_smooth_normals(),
);
Self {
prev: curr.clone(),
curr,
springs,
ball_y: BALL_START_Y,
ball_mesh,
cloth_indices,
}
}
fn step(&mut self, dt: f64, global_secs: f64) {
let dt2 = dt * dt;
let n = self.curr.len();
// Spring forces (zero gravity: the cloth floats freely and stays flat).
let mut ax = vec![0.0f64; n];
let mut ay = vec![0.0f64; n];
let mut az = vec![0.0f64; n];
for &(i, j, rest, k) in &self.springs {
let d = self.curr[j] - self.curr[i];
let dist = d.length();
if dist < 1e-9 {
continue;
}
let f = k * (dist - rest) / dist;
ax[i] += f * d.x;
ay[i] += f * d.y;
az[i] += f * d.z;
ax[j] -= f * d.x;
ay[j] -= f * d.y;
az[j] -= f * d.z;
}
// Self-collision repulsion (keeps the wrapped cloth from folding onto itself).
for i in 0..n {
for j in (i + 1)..n {
let d = self.curr[j] - self.curr[i];
let dist = d.length();
if dist < REPULSION_CUTOFF && dist > 1e-9 {
let f = REPULSION_K * (REPULSION_CUTOFF - dist) / dist;
ax[i] -= f * d.x;
ay[i] -= f * d.y;
az[i] -= f * d.z;
ax[j] += f * d.x;
ay[j] += f * d.y;
az[j] += f * d.z;
}
}
}
// Cloth Verlet integration (no gravity).
for i in 0..n {
let vx = (self.curr[i].x - self.prev[i].x) * DAMPING;
let vy = (self.curr[i].y - self.prev[i].y) * DAMPING;
let vz = (self.curr[i].z - self.prev[i].z) * DAMPING;
self.prev[i] = self.curr[i];
self.curr[i].x += vx + ax[i] * dt2;
self.curr[i].y += vy + ay[i] * dt2;
self.curr[i].z += vz + az[i] * dt2;
}
// Ball position: driven straight down by the global (unwarped) clock,
// then held below the cloth. It is kinematic state: updated here in
// `step` and projected by `extract`.
self.ball_y = (BALL_START_Y - BALL_SPEED * global_secs).max(BALL_STOP_Y);
let ball_center = dvec3(0.0, self.ball_y, 0.0);
// Ball-cloth collision: push cloth particles out of the ball.
for curr in self.curr.iter_mut().take(n) {
let d = *curr - ball_center;
let dist = d.length();
if dist < BALL_RADIUS && dist > 1e-9 {
*curr += d / dist * (BALL_RADIUS - dist);
}
}
}
}
#[cfg(feature = "render")]
impl ranim::render::extract::Extract for ClothState {
fn extract(&self, ctx: &mut ranim::render::extract::ExtractContext<'_>) {
let points = self.curr.clone();
// Smooth normals: accumulate face normals, then normalize.
let mut normals = vec![DVec3::ZERO; points.len()];
for [a, b, c] in self.cloth_indices.as_chunks::<3>().0 {
let (a, b, c) = (*a as usize, *b as usize, *c as usize);
let face = (points[b] - points[a]).cross(points[c] - points[a]);
normals[a] += face;
normals[b] += face;
normals[c] += face;
}
for n in &mut normals {
*n = n.normalize_or_zero();
}
let vertex_colors = vec![Rgba::from(manim::WHITE.with_alpha(1.0)); points.len()];
let cloth = MeshItem {
points: points.into(),
triangle_indices: self.cloth_indices.clone(),
vertex_colors: vertex_colors.into(),
vertex_normals: normals.into(),
};
let ball = self
.ball_mesh
.clone()
.transformed(Translation(dvec3(0.0, self.ball_y, 0.0)));
cloth.extract(ctx);
ball.extract(ctx);
}
}
#[scene]
#[wasm_demo_doc]
#[output(dir = "./output/cloth_wrap")]
fn cloth_wrap(r: &mut RanimScene) {
let sim_secs = 7.0;
let mut camera = CameraFrame::default();
// Oblique downward view (~40° below horizontal, slightly asymmetric azimuth).
camera.pos = dvec3(-4.0, 5.0, 2.5);
camera.facing = (dvec3(0.0, 1.0, 0.0) - camera.pos).normalize();
camera.up = DVec3::Y;
camera.perspective_blend = 1.0;
camera.fovy = 45.0f64.to_radians();
r.play(camera.show().with_duration(sim_secs));
r.play(
Iterative::from_fn(
ClothState::new(),
move |state: &mut ClothState, alpha: f64, delta_alpha: f64| {
state.step(sim_secs * delta_alpha, sim_secs * alpha);
},
)
.with_steps((sim_secs * 60.0) as usize)
.with_duration(sim_secs),
);
r.insert_time_mark(sim_secs, 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::<ClothState>::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([cloth_wrap::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())
}