use bevy::prelude::*;
use bevy_carnage::{CutSettings, FragmentGeometry, audit_proxy, fracture_mesh, hash_f32};
mod common;
use common::body;
use common::recorder::Recorder;
use common::{arg, light_and_floor, material};
const WIDTH: u32 = 720;
const HEIGHT: u32 = 540;
const INTACT_FRAMES: u32 = 14;
const BROKEN_FRAMES: u32 = 86;
const DT: f32 = 1.0 / 30.0 * 0.4;
const GRAVITY: f32 = 18.0;
const RESTITUTION: f32 = 0.35;
const GROUND_DRAG: f32 = 4.0;
const TARGET: usize = 18;
const MIN_FRACTION: f32 = 0.12;
const MAX_DEPTH: u16 = 64;
fn cut(seed: u32) -> CutSettings {
let mut c = CutSettings { max_depth: MAX_DEPTH, ..CutSettings::new(TARGET, MIN_FRACTION, seed) };
if let Some(s) = arg("--soften").and_then(|v| v.parse::<f32>().ok()) {
c.soften = s;
}
c
}
const SEED: u32 = 0x00C0_FFEE;
#[derive(Component)]
struct Chunk {
velocity: Vec3,
spin: Vec3,
drop_to_rest: f32,
}
#[derive(Component)]
struct Intact;
#[derive(Clone, Copy, PartialEq, Eq)]
enum Verdict {
Solid,
ClosedNonManifold,
Open,
}
impl Verdict {
fn of(frag: &FragmentGeometry) -> Self {
match audit_proxy(frag) {
Ok(a) if a.is_closed() && a.is_manifold() => Verdict::Solid,
Ok(a) if a.is_closed() => Verdict::ClosedNonManifold,
_ => Verdict::Open,
}
}
fn color(self) -> Color {
match self {
Verdict::Solid => Color::srgb(0.24, 0.62, 0.36),
Verdict::ClosedNonManifold => Color::srgb(0.90, 0.65, 0.12),
Verdict::Open => Color::srgb(0.85, 0.18, 0.72),
}
}
fn label(self) -> &'static str {
match self {
Verdict::Solid => "watertight + manifold",
Verdict::ClosedNonManifold => "watertight, non-manifold",
Verdict::Open => "open cut edges",
}
}
}
fn main() {
let out = arg("--out").unwrap_or_else(|| "frames".to_string());
let tint = match arg("--tint").as_deref() {
Some("demo") => Tint::Demo,
Some("audit") | None => Tint::Audit,
Some(other) => {
error!("capture: unknown --tint {other:?}; use `audit` or `demo`");
return;
}
};
let dim = |flag: &str, fallback: u32| -> u32 {
match arg(flag).map(|v| v.parse::<u32>()) {
Some(Ok(n)) if n > 0 => n,
Some(_) => {
warn!("capture: {flag} is not a positive integer; using {fallback}");
fallback
}
None => fallback,
}
};
let (width, height) = (dim("--width", WIDTH), dim("--height", HEIGHT));
let camera = Transform::from_xyz(2.25, 1.35, 2.95).looking_at(Vec3::new(0.0, 0.76, 0.0), Vec3::Y);
let Some(mut rec) = Recorder::new(width, height, camera, &out) else { return };
light_and_floor(rec.world());
spawn_intact(rec.world());
rec.warm_up(4);
for frame in 0..INTACT_FRAMES + BROKEN_FRAMES {
if frame == INTACT_FRAMES {
break_it(&mut rec, tint);
}
rec.shoot();
}
let n = rec.finish();
info!("capture: wrote {n} frames to {out}");
}
#[derive(Clone, Copy, PartialEq)]
enum Tint {
Audit,
Demo,
}
fn spawn_intact(world: &mut World) {
let skin = material(world, Color::srgb(0.30, 0.42, 0.52), 0.85);
for (mesh, xform) in body::subject() {
let mesh = world.resource_mut::<Assets<Mesh>>().add(mesh);
world.spawn((
Intact,
Mesh3d(mesh),
MeshMaterial3d(skin.clone()),
Transform::from_matrix(Mat4::from_translation(body::ORIGIN) * xform),
));
}
}
fn break_it(rec: &mut Recorder, tint: Tint) {
let world = rec.world();
let intact: Vec<Entity> = world.query_filtered::<Entity, With<Intact>>().iter(world).collect();
for e in intact {
world.entity_mut(e).despawn();
}
let owned = body::subject();
let parts: Vec<(&Mesh, Mat4)> = owned.iter().map(|(m, x)| (m, *x)).collect();
let pieces = fracture_mesh(&parts, &body::proxy(), &cut(SEED)).into_leaves();
let verdicts: Vec<Verdict> = pieces.iter().map(Verdict::of).collect();
for v in [Verdict::Solid, Verdict::ClosedNonManifold, Verdict::Open] {
let n = verdicts.iter().filter(|&&x| x == v).count();
info!("capture: {n:>2} of {} fragments — {}", pieces.len(), v.label());
}
let interior = material(world, Color::srgb(0.46, 0.07, 0.07), 0.42);
let skins: Vec<(Verdict, Handle<StandardMaterial>)> =
[Verdict::Solid, Verdict::ClosedNonManifold, Verdict::Open]
.into_iter()
.map(|v| {
let color = match tint {
Tint::Audit => v.color(),
Tint::Demo => Color::srgb(0.30, 0.42, 0.52),
};
(v, material(world, color, 0.85))
})
.collect();
for (i, (piece, verdict)) in pieces.into_iter().zip(verdicts).enumerate() {
let (velocity, spin) = launch(i, piece.center_local, piece.cell.volume());
let lowest = piece.cell.points().iter().map(|p| p.y).fold(f32::INFINITY, f32::min);
let drop_to_rest = (piece.cell.center().y - lowest).max(0.0);
let skin = skins.iter().find(|(v, _)| *v == verdict).map(|(_, h)| h.clone());
let outer = piece.outer.map(|m| world.resource_mut::<Assets<Mesh>>().add(m));
let cap = piece.cap.map(|m| world.resource_mut::<Assets<Mesh>>().add(m));
let chunk = world
.spawn((
Chunk { velocity, spin, drop_to_rest },
Transform::from_translation(body::ORIGIN + piece.center_local),
Visibility::default(),
))
.id();
world.entity_mut(chunk).with_children(|parent| {
if let (Some(mesh), Some(skin)) = (outer, skin) {
parent.spawn((Mesh3d(mesh), MeshMaterial3d(skin)));
}
if let Some(mesh) = cap {
parent.spawn((Mesh3d(mesh), MeshMaterial3d(interior.clone())));
}
});
}
rec.app().main.add_systems(Update, integrate);
}
fn launch(i: usize, center: Vec3, volume: f32) -> (Vec3, Vec3) {
let base = SEED.wrapping_mul(2_246_822_519).wrapping_add((i as u32).wrapping_mul(2_654_435_761));
let (h1, h2, h3, h4) = (
hash_f32(base.wrapping_add(1)),
hash_f32(base.wrapping_add(2)),
hash_f32(base.wrapping_add(3)),
hash_f32(base.wrapping_add(4)),
);
let angle = h1 * std::f32::consts::TAU;
let jitter = Vec3::new(angle.cos(), 0.0, angle.sin()) * 0.5;
let dir = (center.normalize_or_zero() + jitter + Vec3::Y * (0.6 + 0.8 * h3)).normalize_or_zero();
let heft = body::heft(volume);
let spin = Vec3::new(h1 - 0.5, h2 - 0.5, h4 - 0.5).normalize_or_zero() * (8.0 + 8.0 * h2) * heft;
(dir * (3.2 + 2.4 * h4) * heft, spin)
}
fn integrate(mut chunks: Query<(&mut Chunk, &mut Transform)>) {
for (mut chunk, mut transform) in &mut chunks {
chunk.velocity.y -= GRAVITY * DT;
transform.translation += chunk.velocity * DT;
transform.rotate_local_x(chunk.spin.x * DT);
transform.rotate_local_y(chunk.spin.y * DT);
transform.rotate_local_z(chunk.spin.z * DT);
let floor = chunk.drop_to_rest;
if transform.translation.y < floor {
transform.translation.y = floor;
if chunk.velocity.y < 0.0 {
chunk.velocity.y = -chunk.velocity.y * RESTITUTION;
let damp = (1.0 - GROUND_DRAG * DT).max(0.0);
chunk.velocity.x *= damp;
chunk.velocity.z *= damp;
chunk.spin *= damp;
if chunk.velocity.y.abs() < 0.4 {
chunk.velocity.y = 0.0;
}
}
}
}
}