Imported from sinanata/unity-mesh-fracture (
AGENTS.md). Install upstream withnpx skills add sinanata/unity-mesh-fracture. Copyright stays with the author.
AGENTS.md
Guidance for AI coding agents (Codex, Cursor, GitHub Copilot, Claude Code, Windsurf, Aider, Zed, and others) working in this repository or wiring this tool into another project. Humans: this is a fast, accurate map. Deeper docs are linked at the bottom.
What this project is
A drop-in Voronoi mesh fracturer for Unity 6 + URP. Pure-C# fragmentation (zero dependencies beyond UnityEngine) that pre-bakes watertight two-submesh fragments — with cooked convex hulls — at load time, then plays them as a fade-out debris burst with optional Unity physics. The entire runtime is three files under Assets/MeshFracture/Runtime/. Namespace: MeshFracture. License: MIT. Battle-tested in the shipping cross-platform game Leap of Legends, where every character that explodes runs through this pipeline.
Golden rules (do not violate)
- "Burst" here is the explosion, not Unity's Burst compiler.
FractureBurstis a plainMonoBehaviour; the whole runtime is single-threaded managed C# overUnityEnginetypes (Mesh,Vector3,List<T>,Quaternion). Do not add[BurstCompile],IJob*,NativeArray<T>,Unity.Mathematics.float3, orusing Unity.Collections / Unity.Burst / Unity.Jobs. None are dependencies; none are used. Pattern-matching "Burst mesh fracture" onto the DOTS/Jobs stack is the single most common wrong turn here. - Everything runs on the main thread; bake at load, look up at impact.
MeshFragmenter.Fragmentis a synchronous, O(n²)-in-fragment-count CPU call. Never call it per-impact during gameplay. UseFragmentCache.RequestPreBake(async, one bake per frame) orFragmentCache.BakeSynchronous(blocking, loading-screen only) at load time, thenFragmentCache.TryGetat the moment of impact.TryGetreturningfalsemeans "not baked yet" — fall back to a particle/smoke puff, don't block. - Source meshes must be Read/Write-enabled.
Fragmentreadssource.vertices / normals / uv / triangles; on a non-readable imported mesh those come back empty and you get a single-fragment fallback. Enable Read/Write on the model importer (the demo'sDemoModelImporterdoes this for its FBXes). - Fragment materials must be transparent and owned per-burst.
Initializeanimates_BaseColor.a1 → 0 by mutating the material instance directly (MaterialPropertyBlock overrides are silently dropped on some URP / SRP-Batcher / WebGL combinations). Hand each burst its ownMaterialinstances (new Material(interiorMat)), configured transparent + Cull Off + ZWrite ON — copyMaterialFactory.ConfigureFractureTransparent. ZWrite must stay ON, or solid chunks render see-through-to-interior. - Two submeshes per fragment: submesh 0 = exterior (the original textured surface), submesh 1 = cap/interior (raw cut faces). Always pass both an exterior and an interior material to
Initialize. - No custom
.shaderassets. The runtime ships zero.shaderand zero.matfiles on purpose — a hand-rolled URP shader rendered every fragment solid black on WebGL2 (an HLSL → GLSL ES 3.0 cross-compile bug). Stock URP/Lit configured at runtime sidesteps it. Do not reintroduce a custom shader. - No
using LeapOfLegends.*or other product-specific imports. New runtime code lives in theMeshFracturenamespace only; demo-only code lives underMeshFractureDemo.
Wire it into your project
The runtime is one folder — Assets/MeshFracture/ — dropped into your Assets/. The full gameplay wiring is:
using MeshFracture;
// 1. At load (a loading screen), pre-bake. Voronoi is ~3-9 ms/character on desktop.
int key = prefab.GetInstanceID() ^ (fragmentCount * 7919);
FragmentCache.RequestPreBake(key, prefab, fragmentCount);
// 2. At impact, look up and spawn the burst.
if (!FragmentCache.TryGet(key, out var cached)) return; // not baked yet -> particle fallback
Vector3 offset = deathPos - cached.MeshCenter;
var fragments = new FragmentResult[cached.Meshes.Length];
for (int i = 0; i < fragments.Length; i++)
fragments[i] = new FragmentResult { Mesh = cached.Meshes[i], Centroid = cached.LocalCentroids[i] + offset };
var burst = new GameObject("FractureBurst").AddComponent<FractureBurst>();
burst.transform.position = deathPos;
burst.Initialize(fragments, exteriorMat, new Material(interiorMat),
explosionForce: 18f, meshRotation: cached.MeshRotation);
For bouncing, collision-aware chunks, set these before Initialize: burst.UseUnityPhysics = true; burst.PreBakedColliderMeshes = cached.ColliderMeshes; burst.DissolveAfterSettle = true;. The canonical minimal example is Assets/MeshFracture/Demo/MeshFractureDemo.cs (~140 lines, drop-on-a-cube).
Public API
MeshFragmenter (static — Assets/MeshFracture/Runtime/MeshFragmenter.cs):
static FragmentResult[] Fragment(Mesh source, int count, Vector3 center); // deterministic from (source, center)
static Mesh BakeSkinnedMesh(SkinnedMeshRenderer smr); // SMR-local space; BakeMesh(useScale:false) — do not double-scale
struct FragmentResult { Mesh Mesh; Vector3 Centroid; }
FragmentCache (static — FragmentCache.cs):
static void RequestPreBake(int key, GameObject modelPrefab, int fragmentCount); // async, one bake/frame
static void BakeSynchronous(int key, Mesh sourceMesh, Quaternion meshRotation, int fragmentCount);
static bool TryGet(int key, out CachedData data); // O(1) impact-time lookup
static Mesh BuildAndBakeColliderMesh(Mesh source);
static void Evict(int key); static void Clear();
struct CachedData { Mesh[] Meshes; Vector3[] LocalCentroids; Vector3 MeshCenter; Quaternion MeshRotation; Mesh[] ColliderMeshes; }
FractureBurst : MonoBehaviour (FractureBurst.cs):
void Initialize(FragmentResult[] fragments, Material exteriorMaterial, Material interiorMaterial,
float explosionForce = 18f, float trailWidth = 0.12f,
Gradient trailGradient = null, Quaternion meshRotation = default);
// Tunables (set before Initialize): Lifetime, DissolveDuration, DissolveAfterSettle, SettleHoldDuration,
// UseUnityPhysics, Bounciness, Friction, FragmentMass, GravityVector, LockToXY, LockPlaneNormal,
// EnableTrails, PreBakedColliderMeshes. Read-only: IsFullyInitialized.
CPU sim is the default (GravityVector, a cheap y = -1 floor). UseUnityPhysics = true swaps in a Rigidbody + convex MeshCollider per fragment and uses Physics.gravity; pass PreBakedColliderMeshes = cached.ColliderMeshes to skip the per-spawn hull cook. LockToXY / LockPlaneNormal are CPU-sim-only (2D / sprite bursts — physics mode has no arbitrary-plane constraint).
Repository layout
Assets/MeshFracture/— the shippable tool, and the only folder a consumer copies.Runtime/(MeshFragmenter, FragmentCache, FractureBurst) +Demo/MeshFractureDemo.cs(the minimal example).Assets/Demo/— the WebGL showcase host project (eight pedestals, a UI Toolkit overlay, sprite destructibles). NamespaceMeshFractureDemo. Not part of the tool: do not copy it into a consuming project, and do not reference it from runtime code.Assets/Editor/,Assets/Settings/,Assets/WebGLTemplates/— build + URP scaffolding for the demo.Tools/— the shared build orchestrator (Tools/.orchestratorsubmodule) plus a thinBuild-Demo.ps1shim.Vendor/— the design-system and sprite-baker submodules, used only by the demo.
Conventions when editing
- Comments answer "why", not "what". Explain why 0.8 and not 0.5 — the trade-off, not the line.
- Watertightness is load-bearing.
MeshFragmenterkeeps one triangle list with per-triangle submesh tags and re-clips caps against every plane; the dedup-cut-edges-by-position step is what keeps concave / hollow meshes from showing through. Read the comments before touching the clip / cap math. BakeSkinnedMeshmust not double-apply scale. It callssmr.BakeMesh(baked, useScale: false)because the no-arg overload already multiplies by lossy scale on Unity 2021+; a caller that also pre-scales gets 1.69×-too-big fragments on a 1.3× character.- Determinism:
Fragmentis deterministic from (source mesh, center) within a run — same inputs, same output frame after frame.
Build, preview, validate
Windows-first Unity 6 project (host editor 6000.3.8f1). There is no unit-test suite; validation is visual, through the two demos.
- Editor preview: open
Assets/Demo/Scenes/MeshFractureDemo.unityand press Play. The minimal example isAssets/MeshFracture/Demo/MeshFractureDemo.cs— drop it on a Cube with a MeshFilter/MeshRenderer and press Space. - WebGL build (what visitors see), from the repo root in PowerShell:
git submodule update --init --recursive # first time: the orchestrator is a submodule copy Tools\Build\config.example.json Tools\Build\config.local.json .\Tools\Build\Build-Demo.ps1 -Serve # builds to build/WebGL/ and serves http://localhost:3000-Serveruns a local server,-Deployforce-pushes a single commit togh-pages,-ClearCacherecovers from a stale Burst-AOT cache. - Verify both demos after any API change (drop-on-a-cube + the WebGL scene), and confirm the WebGL build matches the editor — that is what catches the WebGL2 shader-black regression.
Pull request checklist (summary; full list in CONTRIBUTING.md)
- Simple demo still works (drop on a Cube, Space, fragments fall → fade → gone at
Lifetime). - WebGL demo still builds + works (
Tools\Build\Build-Demo.ps1 -Serve). - No
using LeapOfLegends.*or product-specific imports. - Comments answer why, not what.
- If you touched the transparent material setup: ZWrite stays ON.
- No new
.shaderassets (the WebGL2 black-fragment bug lives there). CHANGELOG.mdupdated; README updated if the public API or behaviour changed.
Deeper docs
- Full walkthrough, architecture, and the material recipe:
README.md - Contribution rules and PR checklist:
CONTRIBUTING.md - Version history:
CHANGELOG.md - Machine-readable index:
llms.txt - Sibling tools: design system · 3D-to-sprite baker · prefab-thumbnail renderer · build orchestrator