Improved Destruction
Fracture Destruction System (improve-destruction-v3 branch)
A production-grade destructible-object system located at Engine/Source/Physics/Fracture*.cpp plus Engine/Source/Game/Objects/Fractured.cpp.
The existing DestructMesh / Game::Destructible implementation from Tutorial 23 is left untouched. The new system ships alongside it under new type names.
Core idea:
A FractureMesh asset pre-bakes a mesh into chunks, a cluster hierarchy, and a connection graph that records which chunks share faces, with per-edge breaking strain.
A Game::Fractured runtime object owns the per-instance state. At runtime, it exists as either a single intact-cluster Actor (cluster proxy) or an array of per-chunk Actors connected by breakable PhysX joints.
Damage accumulates per node, or on the cluster as a whole. Once the threshold is crossed, the object transitions from INTACT to SHATTERED by destroying the proxy and spawning per-chunk actors.
Mass is force-set from bake-time chunk volume so the convex-hull proxy’s mass overestimate does not cause a visible pop at the moment of breakup.
Strain then propagates along the connection graph, so a hit on one chunk weakens its neighbours. This reproduces “spider cracks” style breakage without requiring a full spatial-field solver.
Shipped features:
[]Five bake patterns (FRACTURE_PATTERN_) —
PLANAR_SLICES uses random plane cuts and mirrors the classic DestructMesh algorithm.
UNIFORM_VORONOI and CLUSTERED_VORONOI use iterative SplitMeshSolid with perpendicular-bisector planes; the older CSG-based spike was dropped because it was numerically unstable.
RADIAL produces true pie-slice wedges radiating from an impact_point through the wall’s thickness axis.
BRICK uses a dedicated axis-aligned 6-plane-per-cell path (BakeBrickChunks) with shared-boundary jitter: adjacent bricks read the same boundary entry for their shared seam, so jitter never produces gaps between neighbours.
- Cluster proxy (FractureSettings::build_cluster_proxy) — the bake computes a single convex hull over all chunks and stores it on the root Cluster, plus the sum of per-chunk volumes as mass_sum. At spawn, the Fractured object creates one kinematic Actor using this proxy instead of N per-chunk actors. 500 intact crates cost 500 bodies, not 10,000.
- Localised release cascade — when damage crosses cluster_break_threshold, Fractured::releaseRootCluster(impact_pos, velocity_delta) destroys the proxy and spawns per-chunk actors, inheriting the proxy’s linear and angular velocity and force-setting each chunk’s mass to the bake-time volume estimate. A distance-falloff kick is then applied only to chunks within 1.5 m of the impact (mass-scaled, velocity-capped at 4 m/s), so a hit near the bottom of a wall does not catapult distant chunks and make anchored sections rubber-band through joints.
- Deferred release — applyPointDamage is called from inside PhysX’s onContact callback; destroying and creating actors there corrupts the scene and crashes the next PxRigidActor::is<>() cast. Instead, threshold crossing sets a pending flag plus the impact position and velocity, and the next update() tick flushes the release outside any callback.
- Connection-graph strain propagation — Fractured::propagateStrain walks live_edges each frame when damage_dirty. Damage above the weakest-link elastic limit (0.9 * break_threshold) bleeds across edges; smaller hits dissipate via damage_decay_tau without creeping. Broken joints are removed by BFS; any connected component that does not reach an anchor becomes dynamic.
- Bake-time anchors — FractureSettings::anchor_volume marks chunks whose centroid is inside the box with FRAC_FLAG_ANCHORED_AT_BAKE. They spawn kinematic, or the cluster proxy spawns kinematic if any member is anchored. Runtime Fracture::setAnchor(obj, chunk_index, anchored) can flip the anchor state live.
- Damage API —
Fracture::applyRadialDamage(pos, radius, strength, impulse, type)
applyContactDamage(a, b, contacts, n, type)
breakNode(obj, node_index, dir, type)
setAnchor(...)
All accept a DAMAGE_TYPE enum (KINETIC, EXPLOSIVE, PIERCING, THERMAL, CUSTOM) scaled by a global damage_type_scale[5] table, so game code can do things like make grenades 1.5x stronger against concrete with a single tweak.
- Multi-subscriber BreakEventCallback — Fracture::addBreakEventCallback is void-return and never consumes the event. addBreakEventCallbackB is bool-return; returning true consumes the event and stops the chain. Legacy setBreakEventCallback is preserved as a single-slot wrapper.
- Debris pool — Fracture::setVfxPoolCap, setSfxVoicesCap, debrisActiveCount, plus low-energy sleep and debris-pool caps in updateDebrisHeuristics. Chunks at rest for debris_sleep_timeout seconds are put to sleep so PhysX stops simulating them.
- Save / Load — FractureMesh::save(path) and load(path) round-trip the whole asset (chunks, cooked PhysX convex hulls, cluster, and connection graph) through a versioned CC4('F','R','A','C') binary format.
FractureMesh::adjustStorage(universal, physx, bullet) matches the PhysBody contract and allows the asset to be reloaded on a different backend. A cache (Cache<FractureMesh> FractureMeshes("Fracture Mesh")) is preserved so UID-addressed baked assets can live inside pak files like any other engine asset.
[]Instance sharing — many Game::Fractured instances can point at the same FractureMesh. Geometry and cooked hulls are shared; only runtime state (damage, actor transforms, live joints) is per-instance.
Non-invasive integration:
Zero changes to DestructMesh, Game::Destructible, or Tutorial 23.
PhysX’s Physics.reportContact is not hijacked. The host app registers its own contact callback and can optionally call Fracture::applyContactDamage from inside it.
Contact damage is PhysX-only, since Bullet does not deliver contact events in this engine. Radial damage works on both backends.
Deferred work:
- Multi-level hierarchical fracture (more than 2 levels deep, with per-level break thresholds and cluster-tree view-level LOD). The data model already supports it via Cluster::parent and Cluster::children; the runtime currently walks one level.
- Runtime on-the-fly fracture (CSG against an unbaked mesh). Pre-bake only is the shipping path.
- View-distance simulation LOD (far objects deferred until the player enters a bubble). The cluster proxy is the foundation; bubble activation logic is planned as a later follow-up.
- Multi-platform cooked-hull round-trip. Saved files are currently PhysX-specific; cross-platform support needs adjustStorage(universal=true, ...) before save.
- Titan Editor UI for anchor-volume painting, per-chunk material picking, and interactive fracture preview. This branch is intentionally code-only.
- Auto-dispatch BreakPreset VFX/SFX from a material tag, so Tutorial 31’s 40-line OnBreak puff could collapse to 5 lines. The scaffolding is already in place (multi-subscriber callbacks, damage types), but the preset registry is deferred.
in this fork:
https://github.com/DrewGilpin/EsenthelEngine
|