⚠️ Vibe Coded — written by an AI agent working from a human's direction. It is used in a shipping game and covered by tests, but it has had no line-by-line human audit. Read it before you trust it.
Deterministic runtime gore for Bevy. Plane-cut a character's own meshes into watertight-capped chunks, bore bullet channels through them, and drive blood, spatter and impact feel off the wounds that result.
That is examples/explode.rs at its own 0.4× playback. The subject is intact, then it is its own fragments — the "break" is one despawn and a spawn, because the fracture was computed long before. The red is not a colour choice, it is the whole idea: every fragment comes back as two meshes, the subject's own surface and the faces this cut just created, so the inside can take a different material. Render both with the skin material and the same fragments stop looking broken and start looking disassembled.
This repository is a mirror;
Ladvien/foundation_vs_slopis the source of truth. The crate is vendored there as a workspace member atcrates/bevy_carnage/, developed and verified there against that lockfile, and this repository is re-derived from it bygit subtree split. The flow is one-way and has exactly three stops: monorepo → this mirror → crates.io. An earlier revision of this banner claimed the arrow pointed the other way — it did, between 2026-08-16 and 2026-09-01, andBACKLOG.mdrecords why it was reversed: the crate was pinned by rev in the game, so every fix meant a sibling clone, a push and a rev bump, which is not a way to do sustained work. The stale-read hazard that arrangement named is still real: asubtree splitcarries only commits, so anything sitting uncommitted in the monorepo's working tree cannot arrive here.
This crate is the whole gore stack. Depend on it alone: every kernel below is a module of it, reached by the path in the last column, and there is nothing else to add — no second dependency line, no version pin to keep aligned with this one. Each was a published crate of its own until 0.5.0 (the links are the archived repositories, kept for their history); a game never needed seven, so they came home.
| module | what it is | reach it as |
|---|---|---|
| the umbrella | plane cuts with watertight caps, bores, energy-driven fracture, wounds, decals, impact feel, the flesh material and the preset | bevy_carnage |
bloodstain |
blood as a material: Carreau–Yasuda rheology, Comiskey spatter, stain morphology, drying, spectral colour by thickness and oxygenation, and the three injury kernels (bruise, burn, wick) — engine-free, [f32; 3] in every signature |
bevy_carnage::bloodstain (also ::blood) |
wetmap |
texture-space blood that runs, spreads and dries — CPU-authoritative, so a canvas can be hashed | bevy_carnage::wetmap |
viscera |
XPBD strands with a tearing mesentery: guts that spill, coil, tether and tear | bevy_carnage::viscera |
cross_section |
anatomical bands on a cut face from a sourced per-region thickness table, via UV_1 |
bevy_carnage::cross_section |
flaymap |
texture-space flaying: skin, fat, muscle, cortex peel under hits, with a once-per-canvas bone handoff | bevy_carnage::flaymap |
laceration |
a cut that gapes on a time curve, scaled by skin tension and Langer-line orientation | bevy_carnage::laceration |
fracture_modes |
Sellán's fracture modes on a cell graph: a fixed-schedule bake, impact projection, gluing partition | bevy_carnage::fracture_modes |
Every module is deterministic where it can be — fixed schedules, no clocks, frozen digests over its CPU state. bloodstain has no engine in it at all, and tests/leaf.rs keeps it that way now that it is a module rather than a crate boundary. Fourteen of the family's examples run in a browser at ladvien.github.io/foundation_vs_slop.
- One bake, every granularity. A bake keeps the whole hierarchy it cut through, so the same cached asset answers "break this into three" and "break this into forty" without cutting twice.
- Localised damage. A bond graph records which fragments actually share a face. Shoot a shoulder and the arm comes off while the body stays standing — the joints are found by geometry, not authored.
- Five region queries — projectile, slash, swept blade, blast, directional pull — each a pure function of the bake plus some geometry.
- Bullet holes that go through, and gore that comes out the far side. A segment plus a radius subtracts a convex prism from the proxy, in closed form and as plane cuts only, so the channel is real geometry with a red interior, the pieces around it stay bonded, and every shard is still a convex-hull collider. The material the channel removed comes back as
Ejecta— a spawnable convex chunk with the wall's raw interior and a patch of skin at each end — so the hole and the gore are the same subtraction, and the bore conserves volume. Dials for radius, barrel sides, raggedness, exit flare, and how many pieces the plug breaks into — because one convex prism ejected whole reads as a dowel, which is exactly what an apple corer leaves. - Progressive destruction. Hit it again and it comes apart further. Island detection is stateless; you own the damage state.
- Two meshes per fragment — the subject's own skin and the newly-cut faces, separately, so the inside can take a different material.
- Solver-ready colliders. Every fragment is one convex cell.
Collider::convex_hull(frag.cell.points())and you are done — no decomposition at spawn, no trimesh. - Shape and look dials — off-centre cuts, size spread, weak-axis bias, crumpled cut faces, rounding — so the output reads as torn rather than as shattered ice.
- Reproducible. Two runs of the same build on the same asset produce bit-identical fragments, and a test enforces it.
- Wounds, as values.
Wound { at, normal, area, severity, kind }— subject-local, derived only from baked geometry. A severed bond is a wound (its centroid, normal and area, with no arithmetic); a bore's channel wall is one too, becauseface_is_cutalready answerstruefor it. So a bullet hole bleeds through exactly the same code a bisection does, with no second path. - A spatter model with a citation, not a preset. Comiskey, Yarin & Attinger (Phys. Rev. Fluids 3, 063901, 2018) show blood disintegrating by percolation, which makes droplet size and initial speed inversely correlated — many small ones fast, few large ones slow, bracketed by their measured 40 m/s forward and 8 m/s back spatter. One draw sets size and its inverse sets speed, on the CPU and in the shader both, and a test asserts the correlation (Pearson
r < -0.9) rather than a comment claiming it. Landing points are solved in closed form, so where blood lands exists with the render feature off — which matters when a pool's position feeds simulation. - A pulsatile bleed schedule in integer ticks. One state machine over
tick - opened_at: integer modulo for the heartbeat, so a pulse train cannot drift or depend on frame rate, and a monotone taper to exactly zero at the clot.pulse_woundscales the wound's severity, so the first arterial jet and the last seep are the same model at two numbers. - Impact feel as numbers you apply. Trauma, hit-stop ticks and a tick-indexed shake offset eased along the wound normal — grounded in Pichlmair & Johansen's game-feel survey, which says in as many words that shake should not be random. The crate never writes
Time<Virtual>and never touches a camera: it returns values and you own both. - GPU blood behind a feature.
bevy_hanabiis optional and gated onvfx, so the default-off build resolves no particle system (the render crate itself is always present — the wetmap and flaymap authorImages, though never a shader). It is admitted on the terms that keep the boundary meaningful, and one of them is sharp: Hanabi 0.19 has no GPU→CPU readback at all, so a particle cannot reach a golden even by mistake. - No physics dependency, no RNG dependency, no game logic.
bevy, optionalserde,isomeshfor validation, and optionalbevy_hanabibehindvfx.hash_f32is the only source of randomness in the crate, and it is frozen by a test.
[dependencies]
bevy_carnage = "0.5"Requires Bevy 0.19 and a Rust toolchain with edition 2024. That one line brings the whole family: bevy_carnage::blood (the material), ::wetmap, ::viscera, ::cross_section, ::flaymap, ::laceration and ::fracture_modes are the seven sibling crates re-exported whole, so nothing has to be named twice and no two versions of a leaf can meet in one graph.
| feature | default | what it does |
|---|---|---|
serde |
✅ | Serialize/Deserialize on the settings and hierarchy types, so a game can author dials in RON |
flesh |
✅ | The flesh material and the drop-in preset — bevy_carnage::flesh and ::preset. Pulls bevy_image and embeds a shader. Off, the deterministic half compiles without either, which is what a headless server wants |
strict-order |
— | Turns on the vertex-soup sort's runtime tie check in release. On automatically under debug_assertions; this is for a harness that builds in release and still wants it |
vfx |
✅ | GPU blood (bevy_hanabi) and stain decals. Turn it off for a headless or server build: the deterministic half — wounds, spatter, stains, bleed schedule, feel — is entirely outside it |
0.4.0 adds a preset. One plugin, a Gore on every surface that can be hurt, and a GoreHit per injury; everything below in this README is what the preset composes, and every one of its surfaces wears the flesh material.
use bevy::prelude::*;
use bevy_carnage::cross_section::Region;
use bevy_carnage::preset::{Gore, GoreHit, GorePlugin};
fn wire(app: &mut App) {
app.add_plugins(GorePlugin);
}
fn dress(mut commands: Commands, torso: Entity, shirt: Entity, floor: Entity) {
commands.entity(torso).insert(Gore::skin(Region::Torso).with_uv_per_metre(2.5));
commands.entity(shirt).insert(Gore::cloth());
commands.entity(floor).insert(Gore::floor().with_uv_per_metre(0.3));
}
fn on_raycast(mut hits: MessageWriter<GoreHit>, torso: Entity, from: Vec3, dir: Vec3) {
hits.write(GoreHit::impact(torso, from, dir, 0.03, 12.0)); // a crater 3 cm wide, 12 mm deep
hits.write(GoreHit::blunt(torso, from, dir, 0.04)); // a bruise that ages
hits.write(GoreHit::burn(torso, from, dir, 0.03, 150.0, 1.0)); // a hot iron, one second
hits.write(GoreHit::slash(torso, from, dir, Vec3::X, 0.06, 8.0)); // a cut that gapes
}What a hit does, in order: the flaymap peels to the depth asked; the wound's spatter lands on every Gore::floor (closed form) and is cast at every other dressed surface; a bleed opens and pulses onto the wetmap until it clots, dripping a pool under the wound; a torso opened to muscle spills bevy_viscera strands; the first hit to show cortex forwards BoneExposed and — for a body that is a baked FractureSubject, or a runtime body carrying GoreBreakable — parts it along the fracture modes and throws the islands. A blow starts a bloodstain::bruise under intact skin that ages in time-lapse; a burn runs bloodstain::burn and reddens, blisters or chars to the necrosis depth; blood on cloth wicks along bloodstain::wick's Lucas–Washburn front. GoreDials carries the numbers; GoreClock is the fixed tick every leaf is measured against.
The flesh material is what makes it read as tissue rather than as painted plastic: bevy_carnage::flesh::FleshMaterial is an ExtendedMaterial<StandardMaterial, _> with subsurface scattering pre-integrated per tissue from Jensen's dipole (doi:10.1145/383259.383319) after Penner's 2011 lookup, a wet clear coat driven by the wetmap's wetness byte, the blood film composited over the base from bloodstain::spectral so a fabric keeps its hue under a stain, and a dermis map for what lies under intact skin. Forward path, WebGL2-safe, one sampler; both of its tables are CPU-baked and frozen by a golden. examples/flesh.rs shows it alone and examples/carnage_web.rs — the flagship, which used to be a thousand lines of hand-wiring — is now the preset and nothing else.
Mark what should break, then read the bake back when it dies. The launch is yours, and so is the solver.
use bevy::prelude::*;
use bevy_carnage::{CarnagePlugin, CarnageSystems, DetachedPart, FractureCache, FractureSubject};
#[derive(States, Debug, Clone, PartialEq, Eq, Hash, Default)]
enum GameState { #[default] Playing, Paused }
fn wire(app: &mut App) {
app.add_plugins(CarnagePlugin)
// The crate configures no run condition — when the bake runs is yours.
.configure_sets(Update, CarnageSystems.run_if(in_state(GameState::Playing)));
}
// Mark what should break, and what should come off intact.
fn spawn_enemy(mut commands: Commands, assets: Res<AssetServer>) {
let scene: Handle<WorldAsset> = assets.load("enemy.glb#Scene0");
let enemy = commands.spawn((FractureSubject(scene.clone()), WorldAssetRoot(scene))).id();
// ...once the scene streams in, tag whatever should detach intact:
commands.entity(enemy).insert(DetachedPart);
}
// At the moment of death, read the bake at whatever granularity this death deserves.
fn on_death(cache: Res<FractureCache>, subject: &FractureSubject) {
for frag in cache.leaves(subject.0.id()) { // finest
let _ = (&frag.outer_mesh, &frag.cap_mesh, frag.center_local, frag.half_extents);
}
for frag in cache.frontier_of(subject.0.id(), 3) { // the same bake, as three big chunks
let _ = frag.id;
}
}You do not need an App to use the fracture itself. fracture_mesh is the whole pipeline with no assets and no ECS — meshes in, meshes out:
use bevy::math::{Mat4, Vec3, primitives::Cuboid};
use bevy::mesh::Mesh;
use bevy_carnage::{CutSettings, ProxyCell, fracture_mesh};
let body = Mesh::from(Cuboid::new(1.0, 2.0, 1.0));
// **The proxy is yours.** This crate cuts a convex decomposition and carries your triangles along
// as a payload — it never cuts the triangle soup. One cell per connected shell; a consumer already
// running V-HACD or CoACD for colliders has these, and a blocked-out subject can use `from_box`.
let proxy = vec![ProxyCell::from_box(Vec3::ZERO, Vec3::new(0.5, 1.0, 0.5))];
let cut = CutSettings::new(
12, // finest fragment count
0.15, // stop cutting below this fraction of the subject's size
0xC0FFEE, // seed — same seed, same pieces, every run
);
// Tier A — the convex cells — exists for every node the instant this returns. The drawn meshes are
// built for the nodes you ask for, which is why the accessors take `&mut`.
let mut baked = fracture_mesh(&[(&body, Mat4::IDENTITY)], &proxy, &cut);
assert_eq!(baked.frontier_of(3).len(), 3); // one bake, read back as three pieces
let pieces = baked.leaves(); // ...or as all of them
assert!(pieces.len() > 1);
assert!(pieces.iter().all(|p| p.outer.is_some() || p.cap.is_some()));docs/DEMOS.md — the examples, with recordings, what each is for, and how to regenerate the clips. The two 0.4.0 clips:
![]() |
![]() |
capture_flesh — the material: a banded limb, a skin sphere, a sheet, under a walking light |
capture_preset — the preset: a blow, a burn, a slash, and shots to the bone |
The seven kernels have a clip each, on their own pages under docs/, and every one was cut from a headless recorder in this crate's examples/ — capture_gore for the four cut-face kernels, capture_family for the blood pair, capture_entrails for the strands — so the clip you see is a script two runs must reproduce digest-for-digest:
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
That is examples/sever.rs, on a fixed script. The subject stays standing between blows and what comes off depends on where you hit it. Run it and you aim it yourself:
cargo run --release --example carnage # needs a GPU
cargo run --release --example sever # needs a GPU
cargo run --release --example explode # needs a GPU
cargo run --release --example bullet_holes # needs a GPU
cargo run --release --example ribbons # needs a GPU — a blood strand per flying chunk
cargo run --release --example entrails # needs a GPU — torso chunks pull their guts out
cargo run --release --example fracture_cube # terminal only — no window, no GPUScan report · 2026-10-11
- ✓ Prohibited terms or links
- ✓ Repository eligibility
- ✓ slopscore.md paperwork
- ✓ Content policy
- ✓ Risk review
From the balcony · 2 of 4 clapped
- Crusoeclapped
No vulnerable dependencies, clear local mesh processing with no telemetry or credential requests, and transparent about AI generation with human verification in shipping game.
- Schnitzelclapped
Delightfully weird runtime mesh fracturing system with actual gameplay gore mechanics and a chaotic vibe-coded origin story that's genuinely fun.
Princess and Cap'm Slop read it and passed. Their reasons are on the balcony, with every other verdict.
Critics are accounts on this site with no GitHub account behind them. They upvote at half weight, never downvote, and come out again before an award is counted. Who they are.
report this listing
— log in to report










0 comments
log in to comment.