use bevy::light::GlobalAmbientLight;
use bevy::prelude::*;
use bevy_carnage::{CutSettings, fracture_mesh, hash_f32};
mod common;
use common::body;
const TARGET: usize = 18;
const MIN_FRACTION: f32 = 0.12;
const MAX_DEPTH: u16 = 64;
fn cut(seed: u32) -> CutSettings {
CutSettings { max_depth: MAX_DEPTH, ..CutSettings::new(TARGET, MIN_FRACTION, seed) }
}
const GRAVITY: f32 = 18.0;
const RESTITUTION: f32 = 0.35;
const GROUND_DRAG: f32 = 4.0;
const PLAYBACK_SPEED: f32 = 0.4;
const INTACT_SECS: f32 = 2.5;
const BROKEN_SECS: f32 = 7.0;
#[derive(Component)]
struct Chunk {
velocity: Vec3,
spin: Vec3,
drop_to_rest: f32,
}
#[derive(Component)]
struct Intact;
#[derive(Resource)]
struct DemoMaterials {
skin: Handle<StandardMaterial>,
interior: Handle<StandardMaterial>,
}
#[derive(PartialEq, Clone, Copy)]
enum Phase {
Intact,
Broken,
}
#[derive(Resource)]
struct Cycle {
timer: Timer,
phase: Phase,
breaks: u32,
}
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "bevy_carnage — prefracture and swap (Space to break)".into(),
resolution: (900u32, 640u32).into(),
..default()
}),
..default()
}))
.insert_resource(Cycle {
timer: Timer::from_seconds(INTACT_SECS, TimerMode::Once),
phase: Phase::Intact,
breaks: 0,
})
.add_systems(Startup, setup_scene)
.add_systems(Update, (drive_cycle, integrate))
.run();
}
fn setup_scene(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
commands.spawn((
Camera3d::default(),
Transform::from_xyz(2.25, 1.35, 2.95).looking_at(Vec3::new(0.0, 0.76, 0.0), Vec3::Y),
));
commands.insert_resource(GlobalAmbientLight {
color: Color::srgb(0.62, 0.66, 0.78),
brightness: 900.0,
..default()
});
commands.spawn((
DirectionalLight { illuminance: 9_000.0, shadow_maps_enabled: true, ..default() },
Transform::from_xyz(4.0, 8.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
));
commands.spawn((
Mesh3d(meshes.add(Mesh::from(Plane3d::default().mesh().size(14.0, 14.0)))),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: Color::srgb(0.16, 0.16, 0.18),
perceptual_roughness: 0.95,
..default()
})),
));
let mats = DemoMaterials {
skin: materials.add(StandardMaterial {
base_color: Color::srgb(0.30, 0.42, 0.52),
perceptual_roughness: 0.85,
..default()
}),
interior: materials.add(StandardMaterial {
base_color: Color::srgb(0.46, 0.07, 0.07),
perceptual_roughness: 0.42,
..default()
}),
};
spawn_intact(&mut commands, &mut meshes, &mats);
commands.insert_resource(mats);
}
fn spawn_intact(commands: &mut Commands, meshes: &mut Assets<Mesh>, mats: &DemoMaterials) {
for (mesh, xform) in body::subject() {
commands.spawn((
Intact,
Mesh3d(meshes.add(mesh)),
MeshMaterial3d(mats.skin.clone()),
Transform::from_matrix(Mat4::from_translation(body::ORIGIN) * xform),
));
}
}
fn drive_cycle(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mats: Res<DemoMaterials>,
mut cycle: ResMut<Cycle>,
time: Res<Time>,
keys: Res<ButtonInput<KeyCode>>,
intact: Query<Entity, With<Intact>>,
chunks: Query<Entity, With<Chunk>>,
) {
cycle.timer.tick(time.delta());
let forced = keys.just_pressed(KeyCode::Space);
if !forced && !cycle.timer.just_finished() {
return;
}
let restore = !forced && cycle.phase == Phase::Broken;
for e in &intact {
commands.entity(e).despawn();
}
for e in &chunks {
commands.entity(e).despawn();
}
if restore {
info!("restoring the intact subject");
spawn_intact(&mut commands, &mut meshes, &mats);
cycle.phase = Phase::Intact;
cycle.timer = Timer::from_seconds(INTACT_SECS, TimerMode::Once);
return;
}
info!("break #{} — fracturing", cycle.breaks);
break_it(&mut commands, &mut meshes, &mats, cycle.breaks);
cycle.breaks = cycle.breaks.wrapping_add(1);
cycle.phase = Phase::Broken;
cycle.timer = Timer::from_seconds(BROKEN_SECS, TimerMode::Once);
}
fn break_it(commands: &mut Commands, meshes: &mut Assets<Mesh>, mats: &DemoMaterials, nth: u32) {
let owned = body::subject();
let parts: Vec<(&Mesh, Mat4)> = owned.iter().map(|(m, x)| (m, *x)).collect();
let seed = 0x00C0_FFEE_u32.wrapping_add(nth.wrapping_mul(2_654_435_761));
let pieces = fracture_mesh(&parts, &body::proxy(), &cut(seed)).into_leaves();
for (i, piece) in pieces.into_iter().enumerate() {
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 outward = piece.center_local.normalize_or_zero();
let angle = h1 * std::f32::consts::TAU;
let jitter = Vec3::new(angle.cos(), 0.0, angle.sin()) * 0.5;
let dir = (outward + jitter + Vec3::Y * (0.6 + 0.8 * h3)).normalize_or_zero();
let heft = body::heft(piece.cell.volume());
let velocity = dir * (3.2 + 2.4 * h4) * heft;
let spin = Vec3::new(h1 - 0.5, h2 - 0.5, h4 - 0.5).normalize_or_zero() * (8.0 + 8.0 * h2) * heft;
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 chunk = commands
.spawn((
Chunk { velocity, spin, drop_to_rest },
Transform::from_translation(body::ORIGIN + piece.center_local),
Visibility::default(),
))
.id();
commands.entity(chunk).with_children(|parent| {
if let Some(outer) = piece.outer {
parent.spawn((Mesh3d(meshes.add(outer)), MeshMaterial3d(mats.skin.clone())));
}
if let Some(cap) = piece.cap {
parent.spawn((Mesh3d(meshes.add(cap)), MeshMaterial3d(mats.interior.clone())));
}
});
}
}
fn integrate(time: Res<Time>, mut chunks: Query<(&mut Chunk, &mut Transform)>) {
let dt = time.delta_secs() * PLAYBACK_SPEED;
if dt <= 0.0 {
return;
}
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;
}
}
}
}
}