About Store Forum Documentation Contact



Post Reply 
Improved Destruction
Author Message
Fex Offline
Gold Supporter

Post: #1
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.

[Image: Screenshot-from-2026-04-19-22-14-24.png]

[Image: Screenshot-from-2026-04-19-21-51-13.png]

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
04-20-2026 03:21 AM
Find all posts by this user Quote this message in a reply
Post Reply