blast-stress-solver
Rust bindings for the Blast stress solver, packaged for straightforward native
and WebAssembly consumption from Cargo.
This crate gives you two main layers:
ExtStressSolver for Blast-only stress, bond failure, and split handling
blast_stress_solver::rapier::DestructibleSet for keeping Blast actors and
Rapier rigid bodies in sync
Installation
Core solver only:
[dependencies]
blast-stress-solver = "0.1.0"
With built-in scenario builders:
[dependencies]
blast-stress-solver = { version = "0.1.0", features = ["scenarios"] }
With Rapier integration and scenario builders:
[dependencies]
blast-stress-solver = { version = "0.1.0", features = ["rapier", "scenarios"] }
rapier3d = { version = "0.30", default-features = false, features = ["dim3", "f32"] }
Target support
The published crate currently ships packaged backends for:
aarch64-apple-darwin
wasm32-unknown-unknown
For other Apple/Linux native targets, the published crate falls back to
compiling the bundled Blast C++ sources on the consumer machine with a normal
C++17 toolchain. That removes the need to vendor the PhysX monorepo just to
support x86_64 macOS or Linux consumers.
wasm32-unknown-unknown intentionally stays prepackaged: downstream web builds
do not need Emscripten, wasi-sdk, or a second Blast-side wasm/JS loader.
Advanced overrides:
BLAST_STRESS_SOLVER_STATIC_LIB_PATH=/abs/path/to/libblast_stress_solver_ffi.a
BLAST_STRESS_SOLVER_LIB_DIR=/abs/path/to/lib/dir
BLAST_STRESS_SOLVER_FORCE_SOURCE_BUILD=1 for the bundled Apple/Linux native
fallback
Quick Start
The easiest way to understand the API is in two steps:
- Build a destructible wall and drive the Blast solver directly.
- Take the same wall and let
DestructibleSet keep it synchronized with
Rapier rigid bodies.
The examples below are intentionally explicit about which code belongs to your
application and which code is specific to blast-stress-solver.
1. Blast-only example: build and collapse a simple wall
This version does not use Rapier yet. It shows the minimum Blast-side workflow:
- Build a wall scenario.
- Create
ExtStressSolver from its nodes and bonds.
- Apply gravity and an impact force.
- Ask Blast for fracture commands and apply them.
use blast_stress_solver::scenarios::{build_wall_scenario, WallOptions};
use blast_stress_solver::{ExtStressSolver, ForceMode, SolverSettings, Vec3};
fn main() {
let wall_options = WallOptions {
span: 4.0,
height: 2.0,
thickness: 0.30,
span_segments: 8,
height_segments: 4,
layers: 1,
deck_mass: 200.0,
..WallOptions::default()
};
let wall = build_wall_scenario(&wall_options);
let (nodes, bonds) = wall.to_solver_descs();
let settings = SolverSettings {
max_solver_iterations_per_frame: 64,
compression_elastic_limit: 0.01,
compression_fatal_limit: 0.05,
tension_elastic_limit: 0.01,
tension_fatal_limit: 0.05,
shear_elastic_limit: 0.01,
shear_fatal_limit: 0.05,
..SolverSettings::default()
};
let mut solver =
ExtStressSolver::new(&nodes, &bonds, &settings).expect("failed to create solver");
let impact_node =
(wall_options.height_segments - 1) * wall_options.span_segments + wall_options.span_segments / 2;
let impact_position = wall.nodes[impact_node as usize].centroid;
for frame in 0..30 {
solver.add_gravity(Vec3::new(0.0, -9.81, 0.0));
if frame == 0 {
solver.add_force(
impact_node,
impact_position,
Vec3::new(5_000.0, 0.0, 0.0),
ForceMode::Force,
);
}
solver.update();
let commands = solver.generate_fracture_commands();
let broken_bonds: usize = commands.iter().map(|cmd| cmd.bond_fractures.len()).sum();
let split_events = solver.apply_fracture_commands(&commands);
println!(
"frame={frame:02} actors={} overstressed={} broken_bonds={} split_events={}",
solver.actor_count(),
solver.overstressed_bond_count(),
broken_bonds,
split_events.len(),
);
if !split_events.is_empty() {
println!("wall split after frame {frame}");
break;
}
}
}
What to notice:
build_wall_scenario(...) is optional convenience. If you already have your
own nodes and bonds, you can skip the scenarios feature and build
NodeDesc / BondDesc directly.
- The bottom row of the built-in wall is automatically marked as support
(
mass == 0.0), so the wall has something fixed to break away from.
ExtStressSolver owns the Blast family/actor state. After fractures are
applied, actor_count() grows as disconnected pieces split apart.
2. Rapier example: the same wall, now backed by rigid bodies
This version shows where Rapier is actually integrated.
DestructibleSet does not replace Rapier. Your app still owns the Rapier
world, the physics pipeline, and the frame loop. blast-stress-solver handles
the destruction-specific part:
- keep a Blast stress graph for the structure
- detect failed bonds
- split Blast actors
- create/update/destroy the corresponding Rapier bodies and colliders
use blast_stress_solver::rapier::{DestructibleSet, FracturePolicy};
use blast_stress_solver::scenarios::{build_wall_scenario, WallOptions};
use blast_stress_solver::{SolverSettings, Vec3};
use rapier3d::prelude::*;
fn main() {
let wall_options = WallOptions {
span: 4.0,
height: 2.0,
thickness: 0.30,
span_segments: 8,
height_segments: 4,
layers: 1,
deck_mass: 200.0,
..WallOptions::default()
};
let wall = build_wall_scenario(&wall_options);
let settings = SolverSettings {
max_solver_iterations_per_frame: 64,
compression_elastic_limit: 0.01,
compression_fatal_limit: 0.05,
tension_elastic_limit: 0.01,
tension_fatal_limit: 0.05,
shear_elastic_limit: 0.01,
shear_fatal_limit: 0.05,
..SolverSettings::default()
};
let fracture_policy = FracturePolicy {
idle_skip: false,
..FracturePolicy::default()
};
let mut destructible = DestructibleSet::from_scenario(
&wall,
settings,
Vec3::new(0.0, -9.81, 0.0),
fracture_policy,
)
.expect("failed to create destructible set");
let mut rigid_bodies = RigidBodySet::new();
let mut colliders = ColliderSet::new();
let mut island_manager = IslandManager::new();
let mut impulse_joints = ImpulseJointSet::new();
let mut multibody_joints = MultibodyJointSet::new();
let mut physics_pipeline = PhysicsPipeline::new();
let mut broad_phase = BroadPhaseBvh::new();
let mut narrow_phase = NarrowPhase::new();
let mut ccd_solver = CCDSolver::new();
let integration_parameters = IntegrationParameters::default();
let gravity = Vector::new(0.0, -9.81, 0.0);
destructible.initialize(&mut rigid_bodies, &mut colliders);
let impact_node =
(wall_options.height_segments - 1) * wall_options.span_segments + wall_options.span_segments / 2;
let impact_position = wall.nodes[impact_node as usize].centroid;
for frame in 0..120 {
if frame == 5 {
destructible.add_force(impact_node, impact_position, Vec3::new(5_000.0, 0.0, 0.0));
}
let fracture_step = destructible.step(
&mut rigid_bodies,
&mut colliders,
&mut island_manager,
&mut impulse_joints,
&mut multibody_joints,
);
physics_pipeline.step(
&gravity,
&integration_parameters,
&mut island_manager,
&mut broad_phase,
&mut narrow_phase,
&mut rigid_bodies,
&mut colliders,
&mut impulse_joints,
&mut multibody_joints,
&mut ccd_solver,
&(),
&(),
);
println!(
"frame={frame:03} fractures={} split_events={} new_bodies={} actors={} bodies={}",
fracture_step.fractures,
fracture_step.split_events,
fracture_step.new_bodies,
destructible.actor_count(),
destructible.body_count(),
);
if fracture_step.split_events > 0 {
println!("wall has started splitting into separate Rapier bodies");
break;
}
}
}
What to notice:
- Your app still owns
RigidBodySet, ColliderSet, the Rapier pipeline, and
the main frame loop.
DestructibleSet::initialize(...) is the point where Blast’s initial actor
graph becomes actual Rapier bodies and colliders.
DestructibleSet::step(...) is the integration point. That is where this
crate applies gravity to the stress graph, breaks bonds, and rewrites the
Rapier body/collider layout when actors split.
physics_pipeline.step(...) is still your responsibility. This crate does not
hide Rapier from you.
WebAssembly
This crate supports downstream Rust applications that build for
wasm32-unknown-unknown.
The intended model is:
- your application depends on
blast-stress-solver as a normal Rust dependency
- your application builds one final wasm output
- no Blast-specific sidecar wasm or JS loader is required
Downstream wasm-bindgen example
[lib]
crate-type = ["cdylib"]
[dependencies]
blast-stress-solver = "0.1.0"
wasm-bindgen = "0.2"
use blast_stress_solver::{BondDesc, ExtStressSolver, NodeDesc, SolverSettings, Vec3};
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn blast_tick() -> u32 {
let nodes = [
NodeDesc { centroid: Vec3::new(0.0, 0.0, 0.0), mass: 0.0, volume: 1.0 },
NodeDesc { centroid: Vec3::new(0.0, 1.0, 0.0), mass: 10.0, volume: 1.0 },
];
let bonds = [BondDesc {
centroid: Vec3::new(0.0, 0.5, 0.0),
normal: Vec3::new(0.0, 1.0, 0.0),
area: 1.0,
node0: 0,
node1: 1,
}];
let Some(mut solver) = ExtStressSolver::new(&nodes, &bonds, &SolverSettings::default()) else {
return u32::MAX;
};
solver.add_gravity(Vec3::new(0.0, -9.81, 0.0));
solver.update();
solver.node_count()
}
That build still emits one final application wasm file. There is no extra Blast
runtime bundle to host or load manually.
Features
rapier: enables Rapier3D integration helpers
scenarios: enables built-in wall, tower, and bridge scenario builders
Notes
- Local publish-style proof:
scripts/assemble-blast-stress-solver-package.sh --verify-demo-consumer
stages the crate, verifies the packaged native and wasm smoke consumers, and
runs the real blast/blast-stress-demo-rs headless fracture test against the
staged package.
- The published crate is distributed as packaged Rust source plus:
- prebuilt backend artifacts for
aarch64-apple-darwin and wasm32-unknown-unknown
- bundled native C++ sources for the Apple/Linux source-build fallback
- The monorepo development setup can still build the backend from source, but
consumers no longer need to vendor the monorepo to get that native fallback.