using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using Random = UnityEngine.Random; #if UNITY_EDITOR using UnityEditor; #endif public static class Fragmenter { /// /// Generates the mesh fragments based on the provided options. The generated fragment objects are /// stored as children of `fragmentParent` /// /// The source object to fragment. This object must have a MeshFilter, a RigidBody and a Collider. /// Options for the fragmenter /// The template GameObject that each fragment will clone /// The parent transform for the fragment objects /// If true, the generated fragment meshes will be saved to disk so they can be re-used in prefabs. /// The save location for the fragments. /// public static void Fracture(GameObject sourceObject, FractureOptions options, GameObject fragmentTemplate, Transform parent, bool saveToDisk = false, string saveFolderPath = "") { // Define our source mesh data for the fracturing FragmentData sourceMesh = new FragmentData(sourceObject.GetComponent().sharedMesh); // We begin by fragmenting the source mesh, then process each fragment in a FIFO queue // until we achieve the target fragment count. var fragments = new Queue(); fragments.Enqueue(sourceMesh); // Subdivide the mesh into multiple fragments until we reach the fragment limit FragmentData topSlice, bottomSlice; while (fragments.Count < options.fragmentCount) { FragmentData meshData = fragments.Dequeue(); meshData.CalculateBounds(); // Select an arbitrary fracture plane normal Vector3 normal = new Vector3( options.xAxis ? Random.Range(-1f, 1f) : 0f, options.yAxis ? Random.Range(-1f, 1f) : 0f, options.zAxis ? Random.Range(-1f, 1f) : 0f); // Slice and dice! MeshSlicer.Slice(meshData, normal, meshData.Bounds.center, options.textureScale, options.textureOffset, out topSlice, out bottomSlice); fragments.Enqueue(topSlice); fragments.Enqueue(bottomSlice); } int i = 0; foreach(FragmentData meshData in fragments) { CreateFragment(meshData, sourceObject, fragmentTemplate, parent, saveToDisk, saveFolderPath, options.detectFloatingFragments, ref i); } } /// /// Asynchronously generates the mesh fragments based on the provided options. The generated fragment objects are /// stored as children of `fragmentParent` /// /// The source object to fragment. This object must have a MeshFilter, a RigidBody and a Collider. /// Options for the fragmenter /// The template GameObject that each fragment will clone /// The parent transform for the fragment objects /// public static IEnumerator FractureAsync(GameObject sourceObject, FractureOptions options, GameObject fragmentTemplate, Transform parent, Action onCompletion) { // Define our source mesh data for the fracturing FragmentData sourceMesh = new FragmentData(sourceObject.GetComponent().sharedMesh); // We begin by fragmenting the source mesh, then process each fragment in a FIFO queue // until we achieve the target fragment count. var fragments = new Queue(); fragments.Enqueue(sourceMesh); // Subdivide the mesh into multiple fragments until we reach the fragment limit FragmentData topSlice, bottomSlice; while (fragments.Count < options.fragmentCount) { FragmentData meshData = fragments.Dequeue(); meshData.CalculateBounds(); // Select an arbitrary fracture plane normal Vector3 normal = new Vector3( options.xAxis ? Random.Range(-1f, 1f) : 0f, options.yAxis ? Random.Range(-1f, 1f) : 0f, options.zAxis ? Random.Range(-1f, 1f) : 0f); // Slice and dice! MeshSlicer.Slice(meshData, normal, meshData.Bounds.center, options.textureScale, options.textureOffset, out topSlice, out bottomSlice); // Perform next slice on the next frame yield return null; fragments.Enqueue(topSlice); fragments.Enqueue(bottomSlice); } int i = 0; foreach(FragmentData meshData in fragments) { CreateFragment(meshData, sourceObject, fragmentTemplate, parent, false, "", options.detectFloatingFragments, ref i); } onCompletion?.Invoke(); } /// /// Generates the mesh fragments based on the provided options. The generated fragment objects are /// stored as children of `fragmentParent` /// /// The source object to slice. This object must have a MeshFilter, a RigidBody and a Collider. /// The normal of the cut plane in the local frame of sourceObject. /// The origin of the cut plane in the local frame of sourceObject. /// Options for the slicer /// The template GameObject that each slice will clone /// The parent transform for the fragment objects /// public static void Slice(GameObject sourceObject, Vector3 sliceNormal, Vector3 sliceOrigin, SliceOptions options, GameObject fragmentTemplate, Transform parent) { // Define our source mesh data for the fracturing FragmentData sourceMesh = new FragmentData(sourceObject.GetComponent().sharedMesh); // Subdivide the mesh into multiple fragments until we reach the fragment limit FragmentData topSlice, bottomSlice; // Slice and dice! MeshSlicer.Slice(sourceMesh, sliceNormal, sliceOrigin, options.textureScale, options.textureOffset, out topSlice, out bottomSlice); int i = 0; CreateFragment(topSlice, sourceObject, fragmentTemplate, parent, false, "", options.detectFloatingFragments, ref i); CreateFragment(bottomSlice, sourceObject, fragmentTemplate, parent, false, "", options.detectFloatingFragments, ref i); } /// /// Creates a new GameObject from the fragment data /// /// Geometry of the fragment produced by the slicer /// The source object to fragment. This object must have a MeshFilter, a RigidBody and a Collider. /// The template GameObject that each fragment will clone /// The parent transform for the fragment objects /// Fragment counter private static void CreateFragment(FragmentData fragmentMeshData, GameObject sourceObject, GameObject fragmentTemplate, Transform parent, bool saveToDisk, string saveFolderPath, bool detectFloatingFragments, ref int i) { // If there is no mesh data, don't create an object if (fragmentMeshData.Triangles.Length == 0) { return; } Mesh[] meshes; Mesh fragmentMesh = fragmentMeshData.ToMesh(); // If the "Detect Floating Fragments" option is enabled, take the fragment mesh and // identify disconnected sets of geometry within it, treating each of these as a // separate physical object if (detectFloatingFragments) { meshes = MeshUtils.FindDisconnectedMeshes(fragmentMesh); } else { meshes = new Mesh[] { fragmentMesh }; } var parentSize = sourceObject.GetComponent().sharedMesh.bounds.size; var parentMass = sourceObject.GetComponent().mass; for(int k = 0; k < meshes.Length; k++) { GameObject fragment = GameObject.Instantiate(fragmentTemplate, parent); fragment.name = $"Fragment{i}"; fragment.transform.localPosition = Vector3.zero; fragment.transform.localRotation = Quaternion.identity; fragment.transform.localScale = sourceObject.transform.localScale; meshes[k].name = System.Guid.NewGuid().ToString(); // Update mesh to the new sliced mesh var meshFilter = fragment.GetComponent(); meshFilter.sharedMesh = meshes[k]; var collider = fragment.GetComponent(); // If fragment collisions are disabled, collider will be null collider.sharedMesh = meshes[k]; collider.convex = true; collider.sharedMaterial = fragment.GetComponent().sharedMaterial; // Compute mass of the sliced object by dividing mesh bounds by density var parentRigidBody = sourceObject.GetComponent(); var rigidBody = fragment.GetComponent(); var size = fragmentMesh.bounds.size; float density = (parentSize.x * parentSize.y * parentSize.z) / parentMass; rigidBody.mass = (size.x * size.y * size.z) / density; // This code only compiles for the editor #if UNITY_EDITOR if (saveToDisk) { string path = $"{saveFolderPath}/{meshes[k].name}.asset"; AssetDatabase.CreateAsset(meshes[k], path); } #endif i++; } } }