using System.Collections;
using System.Collections.Generic;
using UnityEngine;
///
/// Runtime-fractures the attached mesh object using OpenFracture's ,
/// then pushes every produced fragment toward a configurable direction so the pieces
/// "drift"/fly off. Call (or to
/// override the direction) from your own gameplay code.
///
[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(Rigidbody))]
public class FractureAndDrift : MonoBehaviour
{
///
/// How the cut-plane normal is chosen at each subdivision step.
///
public enum SliceMode
{
/// Original OpenFracture behaviour: random normal over the enabled axes.
Random,
/// Always cut perpendicular to the fragment's current longest axis. Best for avoiding stretched
/// pieces on long/thin objects, because long dimensions get subdivided first.
LongestAxis,
/// Random normal biased by . Higher weight on an axis produces more cuts
/// perpendicular to it, i.e. that dimension is subdivided more finely.
AxisWeighted
}
[Header("Fracture")]
[Tooltip("Fragment count / axes / inside material. Same options used by OpenFracture's Fracture component.")]
public FractureOptions fractureOptions = new FractureOptions();
[Tooltip("Random = stock OpenFracture. LongestAxis = always cut the longest side first (best anti-stretch). " +
"AxisWeighted = bias cuts toward the axes with higher weight.")]
public SliceMode sliceMode = SliceMode.Random;
[Tooltip("Only used in AxisWeighted mode. Higher weight on an axis = that dimension is subdivided more (more cuts perpendicular to it). " +
"e.g. (3,1,1) cuts a long-X object roughly 3x more along X.")]
public Vector3 axisWeights = Vector3.one;
[Tooltip("Only used in LongestAxis mode. Random tilt (0..1) added to the cut plane so repeated cuts aren't perfectly parallel. 0 = perfectly axis-aligned cuts.")]
[Range(0f, 1f)]
public float longestAxisJitter = 0.15f;
[Tooltip("If true, logs what the controlled fracture actually did (source mesh stats, how many slices " +
"succeeded vs came back empty, final fragment count). Turn this on once to diagnose 'still big blocks'.")]
public bool fractureDebugLogs = false;
[Header("Drift")]
[Tooltip("Direction the fragments fly toward. Interpreted in the space set by 'Direction Is Local'.")]
public Vector3 driftDirection = Vector3.up;
[Tooltip("If true, driftDirection is relative to this object's rotation; if false it is world space.")]
public bool directionIsLocal = false;
[Tooltip("Base speed (m/s) applied to each fragment along the drift direction.")]
public float driftSpeed = 5f;
[Tooltip("Extra random speed [0..value] added on top of driftSpeed for a natural burst.")]
public float driftSpeedRandomness = 1.5f;
[Tooltip("Random sideways scatter (cone half-angle in degrees) around the drift direction. 0 = perfectly straight.")]
[Range(0f, 90f)]
public float scatterAngle = 15f;
[Tooltip("Extra outward push (m/s) from the fracture center, gives an explosion feel. 0 = disabled.")]
public float explosionSpeed = 0f;
[Tooltip("Random angular velocity (rad/s) applied so fragments tumble.")]
public float spinSpeed = 4f;
[Header("Source Physics")]
[Tooltip("If true, the source object's Rigidbody is frozen (kinematic, no gravity) until Shatter() is called. " +
"This stops the object from falling/jittering under physics before it breaks. Highly recommended: the " +
"source body is only used to compute fragment mass, it does not need to simulate before shattering.")]
public bool freezeSourceUntilShatter = true;
[Header("Fragment Physics")]
[Tooltip("If true, fragments are affected by Unity's global gravity (always straight down) after being launched. " +
"Ignored when 'Use Custom Gravity' is enabled.")]
public bool fragmentsUseGravity = true;
[Tooltip("Caps how fast PhysX may push a fragment when it resolves an overlap (depenetration). This is the REAL " +
"fix for fragments 'accelerating'/exploding: freshly cut convex hulls overlap each other and any nearby " +
"geometry, and by default PhysX separates them with an almost unlimited velocity, injecting huge energy. " +
"A small value (1-2) makes overlaps resolve gently. Applies regardless of collision layers/ignores.")]
public float maxDepenetrationVelocity = 1f;
[Tooltip("If true, the freshly spawned fragments collide with each other. Leave OFF for a clean drift: at birth " +
"the convex fragment hulls overlap, and Unity resolves that overlap with large depenetration impulses " +
"that blast the pieces apart. Ignoring sibling collisions removes that explosion.")]
public bool fragmentsCollideWithEachOther = false;
[Tooltip("If true, fragments will NOT collide with other, still-unshattered FractureAndDrift objects in the scene. " +
"Keep ON: otherwise fragments smash into the solid neighbouring objects and fly off chaotically, which is " +
"why earlier-triggered objects looked wrong while the last-triggered one (no solid neighbours left) drifted cleanly.")]
public bool ignoreOtherSourceObjects = true;
[Tooltip("If true, fragments ignore Unity's global gravity and are instead continuously pulled toward " +
"'Custom Gravity' every physics step. Use this to make pieces drift toward an arbitrary direction.")]
public bool useCustomGravity = false;
[Tooltip("Acceleration vector (m/s^2) applied to every fragment each physics step when 'Use Custom Gravity' is on. " +
"e.g. (0,-9.81,0) = normal down; (5,0,0) = pulled toward +X; interpreted in the space set by 'Custom Gravity Is Local'.")]
public Vector3 customGravity = new Vector3(0f, -9.81f, 0f);
[Tooltip("If true, 'Custom Gravity' is relative to this object's rotation at shatter time; if false it is world space.")]
public bool customGravityIsLocal = false;
[Header("Cleanup")]
[Tooltip("Fallback: if nothing calls ReclaimFragments() first, the fragments are auto-reclaimed this many " +
"seconds after shattering. <= 0 keeps them forever (until something calls ReclaimFragments()).")]
public float fragmentLifetime = 5f;
[Tooltip("When reclaiming, fragments are destroyed one-by-one spread across this many seconds instead of all " +
"at once. This is the 'async collect' behaviour. 0 = destroy the whole batch together.")]
public float reclaimStagger = 1f;
[Tooltip("If true, each fragment smoothly shrinks to nothing before being destroyed, for a graceful collect " +
"instead of a hard pop.")]
public bool reclaimShrink = true;
[Tooltip("If true, this GameObject is deactivated after shattering (mirrors OpenFracture behaviour).")]
public bool deactivateSourceAfterShatter = true;
[Header("Prewarm")]
[Tooltip("If true, the expensive mesh slicing runs asynchronously in Start(): the fragments are built up front " +
"and kept hidden & frozen. Shatter() then becomes a cheap, instant reveal + launch, so there is no " +
"slicing hitch at the moment the track breaks. Leave off to slice on-demand when Shatter() is called.")]
public bool prewarmFragmentsOnStart = false;
[Header("Debug")]
[Tooltip("If true, pressing 'Debug Key' triggers the shatter at runtime. For testing only.")]
public bool enableDebugKey = false;
[Tooltip("Key that triggers the shatter when 'Enable Debug Key' is on.")]
public KeyCode debugKey = KeyCode.F;
private GameObject fragmentRoot;
private FragmentReclaimer fragmentReclaimer;
private bool hasShattered;
// Prewarm state: when prewarmFragmentsOnStart is on, the fragments are built ahead of time and parked
// here (hidden + kinematic). Shatter() then just reveals & launches them instead of slicing on the spot.
private bool prewarmComplete;
private bool prewarmInProgress;
private Vector3 pendingLaunchDirection;
private bool launchPending;
// Name of the dedicated physics layer every fragment is placed on. The layer-collision matrix is
// configured so this layer ignores itself, giving global cross-batch fragment isolation for free.
private const string FragmentLayerName = "FractureFragment";
private static int cachedFragmentLayer = -1;
private static bool fragmentLayerResolved;
private void Awake()
{
if (freezeSourceUntilShatter)
{
// The source body is only needed to read mass for fragment computation. Freeze it so the
// object doesn't fall or jitter under physics before Shatter() is called.
var body = GetComponent();
if (body != null)
{
body.isKinematic = true;
body.useGravity = false;
}
}
}
private void Start()
{
if (prewarmFragmentsOnStart)
{
StartCoroutine(PrewarmRoutine());
}
}
private void Update()
{
if (enableDebugKey && !hasShattered && Input.GetKeyDown(debugKey))
{
Shatter();
}
}
///
/// Fractures the object and launches the fragments along the configured drift direction.
///
public void Shatter()
{
Shatter(ResolveDriftDirection());
}
///
/// Fractures the object and launches the fragments along .
///
/// Direction in world space the fragments should fly toward.
public void Shatter(Vector3 worldDirection)
{
if (hasShattered)
{
return;
}
Vector3 direction = worldDirection.sqrMagnitude > Mathf.Epsilon
? worldDirection.normalized
: transform.up;
// Prewarm path: the expensive slicing already ran (or is running) in Start().
if (prewarmFragmentsOnStart)
{
if (prewarmComplete)
{
// Fragments are pre-built and parked. This is now a cheap, instant reveal + launch.
hasShattered = true;
RevealAndLaunch(direction);
return;
}
if (prewarmInProgress)
{
// Slicing hasn't finished yet; remember the request and launch the moment it completes.
pendingLaunchDirection = direction;
launchPending = true;
return;
}
// Prewarm was requested but Start() hasn't run yet (or failed) — fall through to slice on-demand.
}
MeshFilter meshFilter = GetComponent();
if (meshFilter == null || meshFilter.sharedMesh == null)
{
Debug.LogWarning("[FractureAndDrift] No mesh to fracture.", this);
return;
}
hasShattered = true;
CreateFragmentRoot();
GameObject fragmentTemplate = CreateFragmentTemplate();
if (sliceMode != SliceMode.Random)
{
// Custom subdivision loop that controls cut orientation to avoid stretched fragments.
FractureControlled(fragmentTemplate);
Destroy(fragmentTemplate);
LaunchFragments(direction);
FinishShatter();
}
else if (fractureOptions.asynchronous)
{
StartCoroutine(Fragmenter.FractureAsync(
gameObject,
fractureOptions,
fragmentTemplate,
fragmentRoot.transform,
() =>
{
Destroy(fragmentTemplate);
LaunchFragments(direction);
FinishShatter();
}));
}
else
{
Fragmenter.Fracture(
gameObject,
fractureOptions,
fragmentTemplate,
fragmentRoot.transform);
Destroy(fragmentTemplate);
LaunchFragments(direction);
FinishShatter();
}
}
///
/// Builds all fragments ahead of time in Start() and parks them hidden & inactive, so that the
/// actual Shatter() is a cheap reveal + launch with no slicing hitch. Runs the slicing across frames
/// (the async slicer yields; the controlled/sync slicers run in one frame but still off the break moment).
///
private IEnumerator PrewarmRoutine()
{
MeshFilter meshFilter = GetComponent();
if (meshFilter == null || meshFilter.sharedMesh == null)
{
yield break;
}
prewarmInProgress = true;
CreateFragmentRoot();
// Park the container inactive BEFORE slicing so every fragment spawns inactive and never simulates
// (falls/drifts) during the multi-frame prewarm. Shatter() reveals it later via SetActive(true).
fragmentRoot.SetActive(false);
GameObject fragmentTemplate = CreateFragmentTemplate();
if (sliceMode != SliceMode.Random)
{
FractureControlled(fragmentTemplate);
Destroy(fragmentTemplate);
}
else if (fractureOptions.asynchronous)
{
yield return StartCoroutine(Fragmenter.FractureAsync(
gameObject,
fractureOptions,
fragmentTemplate,
fragmentRoot.transform,
() => { }));
Destroy(fragmentTemplate);
}
else
{
Fragmenter.Fracture(
gameObject,
fractureOptions,
fragmentTemplate,
fragmentRoot.transform);
Destroy(fragmentTemplate);
}
// Container is already inactive (parked before slicing), so nothing simulated during prewarm.
prewarmInProgress = false;
prewarmComplete = true;
// If Shatter() was called while we were still slicing, honour it now.
if (launchPending)
{
launchPending = false;
hasShattered = true;
RevealAndLaunch(pendingLaunchDirection);
}
}
///
/// Reveals the pre-built fragment container and launches its fragments. Used by the prewarm path.
///
private void RevealAndLaunch(Vector3 direction)
{
if (fragmentRoot != null)
{
fragmentRoot.SetActive(true);
}
LaunchFragments(direction);
FinishShatter();
}
///
/// Creates the collector object that holds the produced fragments (matches OpenFracture's convention).
///
private void CreateFragmentRoot()
{
fragmentRoot = new GameObject($"{name}Fragments");
fragmentRoot.transform.SetParent(transform.parent);
fragmentRoot.transform.position = transform.position;
fragmentRoot.transform.rotation = transform.rotation;
fragmentRoot.transform.localScale = Vector3.one;
}
private void FinishShatter()
{
// Attach the reclaimer to the fragment container (which stays active). It waits either for an
// explicit ReclaimFragments() call or, as a fallback, for fragmentLifetime to elapse, then
// collects the fragments asynchronously (staggered) rather than destroying them all at once.
if (fragmentRoot != null)
{
fragmentReclaimer = fragmentRoot.AddComponent();
fragmentReclaimer.Configure(fragmentLifetime, reclaimStagger, reclaimShrink);
}
if (deactivateSourceAfterShatter)
{
gameObject.SetActive(false);
}
}
///
/// Begins the asynchronous, staggered collection of this track's fragments. Safe to call once the
/// object has shattered; a no-op otherwise. This overrides the fragmentLifetime fallback timer.
///
public void ReclaimFragments()
{
if (fragmentReclaimer != null)
{
fragmentReclaimer.BeginReclaim();
}
}
///
/// Iterates the freshly created fragments and applies the launch velocity + spin.
///
private void LaunchFragments(Vector3 direction)
{
if (fragmentRoot == null)
{
return;
}
Vector3 center = transform.position;
var bodies = fragmentRoot.GetComponentsInChildren();
for (int i = 0; i < bodies.Length; i++)
{
Rigidbody body = bodies[i];
if (body == null)
{
continue;
}
// When custom gravity is on, disable Unity's global (down-only) gravity so the
// per-step custom acceleration is the only gravity acting on the fragment.
body.useGravity = !useCustomGravity && fragmentsUseGravity;
// Safety net: guarantee the depenetration clamp is applied to every produced body, even ones
// that might not have inherited it from the template (e.g. extra pieces from FindDisconnectedMeshes).
if (maxDepenetrationVelocity > 0f)
{
body.maxDepenetrationVelocity = maxDepenetrationVelocity;
}
Vector3 launchDir = ApplyScatter(direction);
float speed = driftSpeed + Random.value * Mathf.Max(0f, driftSpeedRandomness);
Vector3 velocity = launchDir * speed;
if (explosionSpeed > 0f)
{
Vector3 fromCenter = body.worldCenterOfMass - center;
if (fromCenter.sqrMagnitude > Mathf.Epsilon)
{
velocity += fromCenter.normalized * explosionSpeed;
}
}
body.linearVelocity = velocity;
if (spinSpeed > 0f)
{
body.angularVelocity = Random.insideUnitSphere * spinSpeed;
}
}
ConfigureFragmentCollisions();
if (useCustomGravity)
{
// The source object gets deactivated after shattering, so the custom-gravity driver
// must live on the fragment container (which stays active) to keep applying force.
Vector3 gravity = customGravityIsLocal ? transform.TransformDirection(customGravity) : customGravity;
var driver = fragmentRoot.AddComponent();
driver.gravity = gravity;
}
}
///
/// Configures fragment collisions via the physics layer-collision matrix instead of per-pair
/// Physics.IgnoreCollision. All fragments live on ; disabling that
/// layer's collision with itself makes EVERY fragment ignore EVERY other fragment - same batch or a
/// different track's batch, at any time, with no per-collider bookkeeping. This is why the earlier
/// pairwise approach kept leaking cross-batch collisions: newly spawned colliders were never paired
/// against batches that shattered later. The matrix rule has no such ordering dependency.
///
private void ConfigureFragmentCollisions()
{
int fragmentLayer = ResolveFragmentLayer();
if (fragmentLayer < 0)
{
return;
}
// Fragments never collide with each other (unless explicitly opted in). This is a global matrix
// rule, so it covers same-batch AND cross-batch (another track shattering at the same time)
// with no per-collider bookkeeping and no ordering dependency.
Physics.IgnoreLayerCollision(fragmentLayer, fragmentLayer, !fragmentsCollideWithEachOther);
if (ignoreOtherSourceObjects)
{
// Fragments ignore the still-solid source objects. Done per-collider (NOT via the layer matrix)
// because sources sit on shared layers like Default that the ground/platforms also use - a
// whole-layer ignore would let fragments fall through the floor. Per-collider keeps it precise.
var fragmentColliders = fragmentRoot.GetComponentsInChildren();
var sources = FindObjectsByType(FindObjectsSortMode.None);
for (int s = 0; s < sources.Length; s++)
{
FractureAndDrift source = sources[s];
// Skip sources that already shattered - their solid collider is gone. Keep 'this' one:
// its collider is still active this frame and the fragments spawn right on top of it.
if (source == null || (source.hasShattered && source != this))
{
continue;
}
var sourceCollider = source.GetComponent();
if (sourceCollider == null)
{
continue;
}
for (int f = 0; f < fragmentColliders.Length; f++)
{
if (fragmentColliders[f] != null)
{
Physics.IgnoreCollision(fragmentColliders[f], sourceCollider, true);
}
}
}
}
}
///
/// Resolves the dedicated fragment layer index, caching the lookup. Returns -1 if the project has no
/// layer named (falls back to no layer assignment).
///
private static int ResolveFragmentLayer()
{
if (!fragmentLayerResolved)
{
cachedFragmentLayer = LayerMask.NameToLayer(FragmentLayerName);
fragmentLayerResolved = true;
if (cachedFragmentLayer < 0)
{
Debug.LogWarning($"[FractureAndDrift] Layer '{FragmentLayerName}' not found. Add it in " +
"Project Settings > Tags and Layers so fragments can be isolated from collisions.");
}
}
return cachedFragmentLayer;
}
///
/// Rotates by a random angle within the scatter cone.
///
private Vector3 ApplyScatter(Vector3 direction)
{
if (scatterAngle <= 0f)
{
return direction;
}
// Random rotation within a cone of half-angle 'scatterAngle' around 'direction'.
float angle = Random.Range(0f, scatterAngle);
float roll = Random.Range(0f, 360f);
Quaternion cone = Quaternion.AngleAxis(angle, Vector3.right) * Quaternion.identity;
Quaternion spinAround = Quaternion.AngleAxis(roll, Vector3.forward);
Quaternion align = Quaternion.FromToRotation(Vector3.forward, direction);
return align * spinAround * cone * Vector3.forward;
}
private Vector3 ResolveDriftDirection()
{
return directionIsLocal ? transform.TransformDirection(driftDirection) : driftDirection;
}
///
/// Builds a template GameObject each fragment clones. Mirrors OpenFracture's Fracture.CreateFragmentTemplate.
///
private GameObject CreateFragmentTemplate()
{
GameObject obj = new GameObject("Fragment") { tag = tag };
// Put every fragment on the dedicated fragment layer. The physics layer-collision matrix is
// configured (once) so this layer never collides with itself, which cleanly stops ANY fragment
// from colliding with ANY other fragment - same batch or a different track's batch, at any time.
int fragmentLayer = ResolveFragmentLayer();
if (fragmentLayer >= 0)
{
obj.layer = fragmentLayer;
}
obj.AddComponent();
// Normal material in slot 0, cut-face material in slot 1.
var meshRenderer = obj.AddComponent();
meshRenderer.sharedMaterials = new Material[2]
{
GetComponent().sharedMaterial,
fractureOptions.insideMaterial
};
var thisCollider = GetComponent();
var fragmentCollider = obj.AddComponent();
fragmentCollider.convex = true;
if (thisCollider != null)
{
fragmentCollider.sharedMaterial = thisCollider.sharedMaterial;
fragmentCollider.isTrigger = thisCollider.isTrigger;
}
var thisRigidBody = GetComponent();
var fragmentRigidBody = obj.AddComponent();
fragmentRigidBody.linearDamping = thisRigidBody.linearDamping;
fragmentRigidBody.angularDamping = thisRigidBody.angularDamping;
fragmentRigidBody.useGravity = fragmentsUseGravity;
// Root cause of the "fragments accelerate" blast: overlapping convex hulls are separated by PhysX
// with a near-unlimited velocity by default. Clamp it so overlaps resolve gently instead of
// launching pieces. Every fragment (both fracture paths) clones this template, so setting it here
// covers all of them, independent of any collision-layer/ignore configuration.
if (maxDepenetrationVelocity > 0f)
{
fragmentRigidBody.maxDepenetrationVelocity = maxDepenetrationVelocity;
}
return obj;
}
///
/// Subdivision loop that mirrors , but chooses the cut-plane
/// normal per instead of a fully random one. This is what lets a given
/// axis be subdivided more finely so long/thin fragments don't come out stretched.
///
private void FractureControlled(GameObject fragmentTemplate)
{
Mesh srcMesh = GetComponent().sharedMesh;
var sourceMesh = new FragmentData(srcMesh);
if (fractureDebugLogs)
{
Debug.Log($"[FractureAndDrift] '{name}' FractureControlled start. mode={sliceMode} " +
$"targetCount={fractureOptions.fragmentCount} sourceVerts={srcMesh.vertexCount} " +
$"sourceTris={srcMesh.triangles.Length / 3} subMeshes={srcMesh.subMeshCount} " +
$"srcBoundsSize={srcMesh.bounds.size} readable={srcMesh.isReadable}", this);
if (srcMesh.subMeshCount > 1)
{
Debug.LogWarning($"[FractureAndDrift] '{name}' source mesh has {srcMesh.subMeshCount} submeshes. " +
"OpenFracture only slices submesh 0 — geometry in other submeshes is dropped. " +
"Combine the model into a single submesh/material if pieces look missing or too coarse.", this);
}
}
var fragments = new Queue();
fragments.Enqueue(sourceMesh);
// Subdivide the largest-remaining fragment each step until we hit the target count.
// Processing the largest one first (rather than stock FIFO) keeps fragment sizes even.
// Guard against degenerate slices: if a piece refuses to split (one side comes back empty),
// re-enqueuing it unchanged would spin forever AND leave that big block intact. We instead
// drop it into a "done" set so it stops being reconsidered, and bail out if nothing splits.
var done = new List();
int guardIterations = fractureOptions.fragmentCount * 8 + 16;
int producedSplits = 0;
int degenerateSlices = 0;
while (fragments.Count + done.Count < fractureOptions.fragmentCount && fragments.Count > 0)
{
if (guardIterations-- <= 0)
{
if (fractureDebugLogs)
{
Debug.LogWarning($"[FractureAndDrift] '{name}' subdivision hit iteration guard; " +
"stopping early. The mesh likely can't be split further with the current settings.", this);
}
break;
}
FragmentData meshData = DequeueLargest(fragments);
meshData.CalculateBounds();
Vector3 normal = ChooseCutNormal(meshData.Bounds);
MeshSlicer.Slice(meshData,
normal,
meshData.Bounds.center,
fractureOptions.textureScale,
fractureOptions.textureOffset,
out FragmentData topSlice,
out FragmentData bottomSlice);
bool topEmpty = topSlice.triangleCount == 0;
bool bottomEmpty = bottomSlice.triangleCount == 0;
// A real split yields geometry on BOTH sides. If one side is empty the plane didn't actually
// divide this piece, so keep it aside as finished instead of looping on it forever.
if (topEmpty || bottomEmpty)
{
degenerateSlices++;
done.Add(meshData);
continue;
}
producedSplits++;
fragments.Enqueue(topSlice);
fragments.Enqueue(bottomSlice);
}
int i = 0;
var parentSize = srcMesh.bounds.size;
var parentMass = GetComponent().mass;
float density = (parentSize.x * parentSize.y * parentSize.z) / Mathf.Max(parentMass, Mathf.Epsilon);
foreach (FragmentData meshData in fragments)
{
CreateControlledFragment(meshData, fragmentTemplate, density, ref i);
}
foreach (FragmentData meshData in done)
{
CreateControlledFragment(meshData, fragmentTemplate, density, ref i);
}
if (fractureDebugLogs)
{
Debug.Log($"[FractureAndDrift] '{name}' FractureControlled done. successfulSplits={producedSplits} " +
$"degenerateSlices={degenerateSlices} fragmentsCreated={i}. " +
(degenerateSlices > producedSplits && producedSplits < 4
? "Most slices failed to divide the mesh -> that is why you still see big blocks. "
: ""), this);
}
}
///
/// Picks the slice-plane normal for the next cut based on the current fragment bounds and slice mode.
///
private Vector3 ChooseCutNormal(Bounds bounds)
{
if (sliceMode == SliceMode.LongestAxis)
{
Vector3 size = bounds.size;
// Normal points along the longest dimension, so the cut plane is perpendicular to it
// and splits that long dimension in half.
Vector3 normal = Vector3.right;
if (size.y >= size.x && size.y >= size.z) normal = Vector3.up;
else if (size.z >= size.x && size.z >= size.y) normal = Vector3.forward;
if (longestAxisJitter > 0f)
{
normal += Random.insideUnitSphere * longestAxisJitter;
}
return normal.sqrMagnitude > Mathf.Epsilon ? normal.normalized : Vector3.up;
}
// AxisWeighted: bias each component by its weight so heavier axes get more perpendicular cuts.
Vector3 w = axisWeights;
Vector3 weighted = new Vector3(
(fractureOptions.xAxis ? 1f : 0f) * Mathf.Max(0f, w.x) * Random.Range(-1f, 1f),
(fractureOptions.yAxis ? 1f : 0f) * Mathf.Max(0f, w.y) * Random.Range(-1f, 1f),
(fractureOptions.zAxis ? 1f : 0f) * Mathf.Max(0f, w.z) * Random.Range(-1f, 1f));
return weighted.sqrMagnitude > Mathf.Epsilon ? weighted.normalized : Vector3.up;
}
///
/// Removes and returns the fragment with the largest bounding-box volume from the queue,
/// preserving the order of the remaining items.
///
private static FragmentData DequeueLargest(Queue fragments)
{
int count = fragments.Count;
FragmentData largest = null;
float largestVolume = float.MinValue;
// Rotate the queue once, tracking the largest, then rotate again dropping that one.
for (int i = 0; i < count; i++)
{
FragmentData candidate = fragments.Dequeue();
candidate.CalculateBounds();
Vector3 s = candidate.Bounds.size;
float volume = s.x * s.y * s.z;
if (volume > largestVolume)
{
largestVolume = volume;
largest = candidate;
}
fragments.Enqueue(candidate);
}
for (int i = 0; i < count; i++)
{
FragmentData candidate = fragments.Dequeue();
if (!ReferenceEquals(candidate, largest))
{
fragments.Enqueue(candidate);
}
}
return largest;
}
///
/// Instantiates a fragment GameObject from mesh data. Mirrors the private Fragmenter.CreateFragment.
///
private void CreateControlledFragment(FragmentData meshData, GameObject fragmentTemplate, float density, ref int i)
{
if (meshData.triangleCount == 0)
{
return;
}
Mesh[] meshes;
Mesh fragmentMesh = meshData.ToMesh();
if (fractureOptions.detectFloatingFragments)
{
meshes = MeshUtils.FindDisconnectedMeshes(fragmentMesh);
}
else
{
meshes = new Mesh[] { fragmentMesh };
}
for (int k = 0; k < meshes.Length; k++)
{
GameObject fragment = Instantiate(fragmentTemplate, fragmentRoot.transform);
fragment.name = $"Fragment{i}";
fragment.transform.localPosition = Vector3.zero;
fragment.transform.localRotation = Quaternion.identity;
fragment.transform.localScale = transform.localScale;
meshes[k].name = System.Guid.NewGuid().ToString();
fragment.GetComponent().sharedMesh = meshes[k];
var collider = fragment.GetComponent();
collider.sharedMesh = meshes[k];
collider.convex = true;
var size = meshes[k].bounds.size;
var rigidBody = fragment.GetComponent();
rigidBody.mass = (size.x * size.y * size.z) / Mathf.Max(density, Mathf.Epsilon);
i++;
}
}
}
///
/// Continuously pulls a set of rigidbodies toward a fixed world-space acceleration every physics step.
/// Attached to the fragment container by when "Use Custom Gravity" is on,
/// so fragments drift toward an arbitrary direction instead of Unity's straight-down global gravity.
/// It lives on the surviving fragment root (not the source object, which is deactivated after shattering).
///
public class FragmentGravityField : MonoBehaviour
{
/// World-space acceleration (m/s^2) applied to every fragment each FixedUpdate.
public Vector3 gravity;
private Rigidbody[] bodies;
private void FixedUpdate()
{
// Refresh the body list lazily: FindDisconnectedMeshes can add fragments a frame late,
// and null entries appear as the container is torn down at end of life.
if (bodies == null)
{
bodies = GetComponentsInChildren();
}
for (int i = 0; i < bodies.Length; i++)
{
Rigidbody body = bodies[i];
if (body == null || body.isKinematic)
{
continue;
}
// ForceMode.Acceleration ignores mass, so every fragment falls at the same rate (like real gravity).
body.AddForce(gravity, ForceMode.Acceleration);
}
}
}
///
/// Collects (destroys) a batch of fragments asynchronously instead of all at once. Attached to the
/// surviving fragment container by . It either waits for an explicit
/// call (driven by the track controller's fade timing) or, as a fallback,
/// auto-starts after fallbackDelay seconds. Fragments are destroyed one-by-one spread across
/// stagger seconds, optionally shrinking each one to nothing first for a graceful collect.
///
public class FragmentReclaimer : MonoBehaviour
{
private float fallbackDelay;
private float stagger;
private bool shrink;
private bool reclaiming;
public void Configure(float fallbackDelay, float stagger, bool shrink)
{
this.fallbackDelay = fallbackDelay;
this.stagger = Mathf.Max(0f, stagger);
this.shrink = shrink;
// Fallback timer: if nothing calls BeginReclaim() first, start on our own after the delay.
if (fallbackDelay > 0f)
{
Invoke(nameof(BeginReclaim), fallbackDelay);
}
}
///
/// Starts the staggered collection. Safe to call multiple times; only the first call takes effect.
///
public void BeginReclaim()
{
if (reclaiming)
{
return;
}
reclaiming = true;
CancelInvoke(nameof(BeginReclaim));
StartCoroutine(ReclaimRoutine());
}
private IEnumerator ReclaimRoutine()
{
// Snapshot the current fragments (skip the top-level container transform itself).
var fragments = new List();
foreach (Transform child in transform)
{
if (child != null)
{
fragments.Add(child);
}
}
int count = fragments.Count;
if (count == 0)
{
Destroy(gameObject);
yield break;
}
// Time budget between consecutive fragment removals.
float interval = count > 1 ? stagger / (count - 1) : 0f;
for (int i = 0; i < count; i++)
{
Transform fragment = fragments[i];
if (fragment != null)
{
if (shrink)
{
StartCoroutine(ShrinkAndDestroy(fragment.gameObject, Mathf.Max(0.05f, interval)));
}
else
{
Destroy(fragment.gameObject);
}
}
if (interval > 0f)
{
yield return new WaitForSeconds(interval);
}
}
// Once the last fragment is gone (allow shrink time to finish), remove the container.
yield return new WaitForSeconds(shrink ? Mathf.Max(0.05f, interval) : 0f);
Destroy(gameObject);
}
private IEnumerator ShrinkAndDestroy(GameObject fragment, float duration)
{
Transform t = fragment.transform;
Vector3 startScale = t.localScale;
float elapsed = 0f;
while (elapsed < duration && fragment != null)
{
elapsed += Time.deltaTime;
float k = Mathf.Clamp01(elapsed / duration);
t.localScale = Vector3.Lerp(startScale, Vector3.zero, k);
yield return null;
}
if (fragment != null)
{
Destroy(fragment);
}
}
}