ui基本完毕,修了一大把的bug

This commit is contained in:
2026-07-13 02:28:39 +08:00
parent 1e20d73e90
commit fd22501f71
958 changed files with 378289 additions and 41038 deletions
@@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;
// Exposes internal methods of this assembly to the test assembly
[assembly: InternalsVisibleTo("Tests")]
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a037ed3b25d886c4eaf84869106ff6b0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,261 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(Rigidbody))]
public class Fracture : MonoBehaviour
{
public TriggerOptions triggerOptions;
public FractureOptions fractureOptions;
public RefractureOptions refractureOptions;
public CallbackOptions callbackOptions;
/// <summary>
/// The number of times this fragment has been re-fractured.
/// </summary>
[HideInInspector]
public int currentRefractureCount = 0;
/// <summary>
/// Collector object that stores the produced fragments
/// </summary>
private GameObject fragmentRoot;
[ContextMenu("Print Mesh Info")]
public void PrintMeshInfo()
{
var mesh = this.GetComponent<MeshFilter>().mesh;
Debug.Log("Positions");
var positions = mesh.vertices;
var normals = mesh.normals;
var uvs = mesh.uv;
for (int i = 0; i < positions.Length; i++)
{
Debug.Log($"Vertex {i}");
Debug.Log($"POS | X: {positions[i].x} Y: {positions[i].y} Z: {positions[i].z}");
Debug.Log($"NRM | X: {normals[i].x} Y: {normals[i].y} Z: {normals[i].z} LEN: {normals[i].magnitude}");
Debug.Log($"UV | U: {uvs[i].x} V: {uvs[i].y}");
Debug.Log("");
}
}
public void CauseFracture()
{
callbackOptions.CallOnFracture(null, gameObject, transform.position);
this.ComputeFracture();
}
void OnValidate()
{
if (this.transform.parent != null)
{
// When an object is fractured, the fragments are created as children of that object's parent.
// Because of this, they inherit the parent transform. If the parent transform is not scaled
// the same in all axes, the fragments will not be rendered correctly.
var scale = this.transform.parent.localScale;
if ((scale.x != scale.y) || (scale.x != scale.z) || (scale.y != scale.z))
{
Debug.LogWarning($"Warning: Parent transform of fractured object must be uniformly scaled in all axes or fragments will not render correctly.", this.transform);
}
}
}
void OnCollisionEnter(Collision collision)
{
if (triggerOptions.triggerType == TriggerType.Collision)
{
if (collision.contactCount > 0)
{
// Collision force must exceed the minimum force (F = I / T)
var contact = collision.contacts[0];
float collisionForce = collision.impulse.magnitude / Time.fixedDeltaTime;
// Colliding object tag must be in the set of allowed collision tags if filtering by tag is enabled
bool tagAllowed = triggerOptions.IsTagAllowed(contact.otherCollider.gameObject.tag);
// Object is unfrozen if the colliding object has the correct tag (if tag filtering is enabled)
// and the collision force exceeds the minimum collision force.
if (collisionForce > triggerOptions.minimumCollisionForce &&
(triggerOptions.filterCollisionsByTag && tagAllowed))
{
callbackOptions.CallOnFracture(contact.otherCollider, gameObject, contact.point);
this.ComputeFracture();
}
}
}
}
void OnTriggerEnter(Collider collider)
{
if (triggerOptions.triggerType == TriggerType.Trigger)
{
// Colliding object tag must be in the set of allowed collision tags if filtering by tag is enabled
bool tagAllowed = triggerOptions.IsTagAllowed(collider.gameObject.tag);
if (triggerOptions.filterCollisionsByTag && tagAllowed)
{
callbackOptions.CallOnFracture(collider, gameObject, transform.position);
this.ComputeFracture();
}
}
}
void Update()
{
if (triggerOptions.triggerType == TriggerType.Keyboard)
{
if (Input.GetKeyDown(triggerOptions.triggerKey))
{
callbackOptions.CallOnFracture(null, gameObject, transform.position);
this.ComputeFracture();
}
}
}
/// <summary>
/// Compute the fracture and create the fragments
/// </summary>
/// <returns></returns>
private void ComputeFracture()
{
var mesh = this.GetComponent<MeshFilter>().sharedMesh;
if (mesh != null)
{
// If the fragment root object has not yet been created, create it now
if (this.fragmentRoot == null)
{
// Create a game object to contain the fragments
this.fragmentRoot = new GameObject($"{this.name}Fragments");
this.fragmentRoot.transform.SetParent(this.transform.parent);
// Each fragment will handle its own scale
this.fragmentRoot.transform.position = this.transform.position;
this.fragmentRoot.transform.rotation = this.transform.rotation;
this.fragmentRoot.transform.localScale = Vector3.one;
}
var fragmentTemplate = CreateFragmentTemplate();
if (fractureOptions.asynchronous)
{
StartCoroutine(Fragmenter.FractureAsync(
this.gameObject,
this.fractureOptions,
fragmentTemplate,
this.fragmentRoot.transform,
() =>
{
// Done with template, destroy it
GameObject.Destroy(fragmentTemplate);
// Deactivate the original object
this.gameObject.SetActive(false);
// Fire the completion callback
if ((this.currentRefractureCount == 0) ||
(this.currentRefractureCount > 0 && this.refractureOptions.invokeCallbacks))
{
if (callbackOptions.onCompleted != null)
{
callbackOptions.onCompleted.Invoke();
}
}
}
));
}
else
{
Fragmenter.Fracture(this.gameObject,
this.fractureOptions,
fragmentTemplate,
this.fragmentRoot.transform);
// Done with template, destroy it
GameObject.Destroy(fragmentTemplate);
// Deactivate the original object
this.gameObject.SetActive(false);
// Fire the completion callback
if ((this.currentRefractureCount == 0) ||
(this.currentRefractureCount > 0 && this.refractureOptions.invokeCallbacks))
{
if (callbackOptions.onCompleted != null)
{
callbackOptions.onCompleted.Invoke();
}
}
}
}
}
/// <summary>
/// Creates a template object which each fragment will derive from
/// </summary>
/// <param name="preFracture">True if this object is being pre-fractured. This will freeze all of the fragments.</param>
/// <returns></returns>
private GameObject CreateFragmentTemplate()
{
// If pre-fracturing, make the fragments children of this object so they can easily be unfrozen later.
// Otherwise, parent to this object's parent
GameObject obj = new GameObject();
obj.name = "Fragment";
obj.tag = this.tag;
// Update mesh to the new sliced mesh
obj.AddComponent<MeshFilter>();
// Add materials. Normal material goes in slot 1, cut material in slot 2
var meshRenderer = obj.AddComponent<MeshRenderer>();
meshRenderer.sharedMaterials = new Material[2] {
this.GetComponent<MeshRenderer>().sharedMaterial,
this.fractureOptions.insideMaterial
};
// Copy collider properties to fragment
var thisCollider = this.GetComponent<Collider>();
var fragmentCollider = obj.AddComponent<MeshCollider>();
fragmentCollider.convex = true;
fragmentCollider.sharedMaterial = thisCollider.sharedMaterial;
fragmentCollider.isTrigger = thisCollider.isTrigger;
// Copy rigid body properties to fragment
var thisRigidBody = this.GetComponent<Rigidbody>();
var fragmentRigidBody = obj.AddComponent<Rigidbody>();
fragmentRigidBody.linearVelocity = thisRigidBody.linearVelocity;
fragmentRigidBody.angularVelocity = thisRigidBody.angularVelocity;
fragmentRigidBody.linearDamping = thisRigidBody.linearDamping;
fragmentRigidBody.angularDamping = thisRigidBody.angularDamping;
fragmentRigidBody.useGravity = thisRigidBody.useGravity;
// If refracturing is enabled, create a copy of this component and add it to the template fragment object
if (refractureOptions.enableRefracturing &&
(this.currentRefractureCount < refractureOptions.maxRefractureCount))
{
CopyFractureComponent(obj);
}
return obj;
}
/// <summary>
/// Convenience method for copying this component to another component
/// </summary>
/// <param name="obj">The GameObject to copy the component to</param>
private void CopyFractureComponent(GameObject obj)
{
var fractureComponent = obj.AddComponent<Fracture>();
fractureComponent.triggerOptions = this.triggerOptions;
fractureComponent.fractureOptions = this.fractureOptions;
fractureComponent.refractureOptions = this.refractureOptions;
fractureComponent.callbackOptions = this.callbackOptions;
fractureComponent.currentRefractureCount = this.currentRefractureCount + 1;
fractureComponent.fragmentRoot = this.fragmentRoot;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 91fc9178a0b0c3d4bb8b6d91b16d9893
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a4c8ded860f2f6b489482d866cec1a7e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,764 @@
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Class for triangulating a set of 3D points with edge constraints. Supports convex and non-convex polygons
/// as well as polygons with holes.
/// </summary>
public sealed class ConstrainedTriangulator : Triangulator
{
/// <summary>
/// Given an edge E12, E23, E31, this returns the first vertex for that edge (V1, V2, V3, respectively)
/// </summary>
/// <value></value>
private static readonly int[] edgeVertex1 = new int[] { 0, 0, 0, V1, V2, V3 };
/// <summary>
/// Given an edge E12, E23, E31, this returns the second vertex for that edge (V2, V3, V1, respectively)
/// </summary>
/// <value></value>
private static readonly int[] edgeVertex2 = new int[] { 0, 0, 0, V2, V3, V1 };
/// <summary>
/// Given an edge E12, E23, E31, this returns the vertex opposite that edge (V3, V1, V2, respectively)
/// </summary>
/// <value></value>
private static readonly int[] oppositePoint = new int[] { 0, 0, 0, V3, V1, V2 };
/// <summary>
/// Given an edge E12, E23, E31, this returns the next clockwise edge (E23, E31, E12, respectively)
/// </summary>
/// <value></value>
private static readonly int[] nextEdge = new int[] { 0, 0, 0, E23, E31, E12 };
/// <summary>
/// Given an edge E12, E23, E31, this returns the previous clockwise edge (E31, E12, E23, respectively)
/// </summary>
/// <value></value>
private static readonly int[] previousEdge = new int[] { 0, 0, 0, E31, E12, E23 };
/// <summary>
/// List of edge constraints provided during initialization
/// </summary>
private List<EdgeConstraint> constraints;
/// <summary>
/// This array maps each vertex to a triangle in the triangulation that contains it. This helps
/// speed up the search when looking for intersecting edge. It isn't necessary to keep track of
/// every triangle for each vertex.
/// </summary>
private int[] vertexTriangles;
/// <summary>
/// Flag for each triangle to track whether it has been visited or not when finding the starting edge.
/// Define at the class level to prevent unnecessary GC when calling FindStartingEdge multiple times.
/// </summary>
private bool[] visited;
/// <summary>
/// Initializes the triangulator with the vertex data to be triangulated given a set of edge constraints
/// </summary>
/// <param name="inputPoints">The of points to triangulate.</param>
/// <param name="constraints">The list of edge constraints which defines how the vertices in `inputPoints` are connected.</param>
/// <param name="normal">The normal of the plane in which the `inputPoints` lie.</param>
/// <returns></returns>
public ConstrainedTriangulator(List<MeshVertex> inputPoints, List<EdgeConstraint> constraints, Vector3 normal)
: base(inputPoints, normal)
{
this.constraints = constraints;
}
/// <summary>
/// Calculates the triangulation
/// </summary>
/// <returns>Returns an array containing the indices of the triangles, mapped to the list of points passed in during initialization.</returns>
public override int[] Triangulate()
{
// Need at least 3 vertices to triangulate
if (N < 3)
{
return new int[] { };
}
this.AddSuperTriangle();
this.NormalizeCoordinates();
this.ComputeTriangulation();
if (constraints.Count > 0)
{
this.ApplyConstraints();
this.DiscardTrianglesViolatingConstraints();
}
this.DiscardTrianglesWithSuperTriangleVertices();
List<int> triangles = new List<int>(3 * triangleCount);
for (int i = 0; i < triangleCount; i++)
{
// Add all triangles that don't contain a super-triangle vertex
if (!skipTriangle[i])
{
triangles.Add(triangulation[i, V1]);
triangles.Add(triangulation[i, V2]);
triangles.Add(triangulation[i, V3]);
}
}
return triangles.ToArray();
}
/// <summary>
/// Applys the edge constraints to the triangulation
/// </summary>
internal void ApplyConstraints()
{
visited = new bool[triangulation.GetLength(0)];
// Map each vertex to a triangle that contains it
vertexTriangles = new int[N + 3];
for (int i = 0; i < triangulation.GetLength(0); i++)
{
vertexTriangles[triangulation[i, V1]] = i;
vertexTriangles[triangulation[i, V2]] = i;
vertexTriangles[triangulation[i, V3]] = i;
}
// Loop through each edge constraint
foreach (EdgeConstraint constraint in constraints)
{
if (constraint.v1 == constraint.v2) continue;
// We find the edges of the triangulation that intersect the constraint edge and remove them
// For each intersecting edge, we identify the triangles that share that edge (which form a quad)
// The diagonal of this quad is flipped.
Queue<EdgeConstraint> intersectingEdges = FindIntersectingEdges(constraint, vertexTriangles);
RemoveIntersectingEdges(constraint, intersectingEdges);
}
}
/// <summary>
/// Searches through the triangulation to find intersecting edges
/// </summary>
/// <param name="intersectingEdges"></param>
internal Queue<EdgeConstraint> FindIntersectingEdges(EdgeConstraint constraint, int[] vertexTriangles)
{
Queue<EdgeConstraint> intersectingEdges = new Queue<EdgeConstraint>();
// Need to find the first edge that the constraint crosses.
EdgeConstraint startEdge;
if (FindStartingEdge(vertexTriangles, constraint, out startEdge))
{
intersectingEdges.Enqueue(startEdge);
}
else
{
return intersectingEdges;
}
// Search for all triangles that intersect the constraint. Stop when we find a triangle that contains v_j
int t = startEdge.t1;
int edgeIndex = startEdge.t1Edge;
int lastTriangle = t;
bool finalTriangleFound = false;
while (!finalTriangleFound)
{
// Cross the last intersecting edge and inspect the next triangle
lastTriangle = t;
t = triangulation[t, edgeIndex];
// Get coordinates of constraint end points and triangle vertices
Vector2 v_i = points[constraint.v1].coords;
Vector2 v_j = points[constraint.v2].coords;
Vector2 v1 = points[triangulation[t, V1]].coords;
Vector2 v2 = points[triangulation[t, V2]].coords;
Vector2 v3 = points[triangulation[t, V3]].coords;
// If triangle contains the endpoint of the constraint, the search is done
if (TriangleContainsVertex(t, constraint.v2))
{
finalTriangleFound = true;
}
// Otherwise, the constraint must intersect one edge of this triangle. Ignore the edge that we entered from
else if ((triangulation[t, E12] != lastTriangle) && MathUtils.LinesIntersect(v_i, v_j, v1, v2))
{
edgeIndex = E12;
var edge = new EdgeConstraint(triangulation[t, V1], triangulation[t, V2], t, triangulation[t, E12], edgeIndex);
intersectingEdges.Enqueue(edge);
}
else if ((triangulation[t, E23] != lastTriangle) && MathUtils.LinesIntersect(v_i, v_j, v2, v3))
{
edgeIndex = E23;
var edge = new EdgeConstraint(triangulation[t, V2], triangulation[t, V3], t, triangulation[t, E23], edgeIndex);
intersectingEdges.Enqueue(edge);
}
else if ((triangulation[t, E31] != lastTriangle) && MathUtils.LinesIntersect(v_i, v_j, v3, v1))
{
edgeIndex = E31;
var edge = new EdgeConstraint(triangulation[t, V3], triangulation[t, V1], t, triangulation[t, E31], edgeIndex);
intersectingEdges.Enqueue(edge);
}
else
{
// Shouldn't reach this point
Debug.LogWarning("Failed to find final triangle, exiting early.");
break;
}
}
return intersectingEdges;
}
/// <summary>
/// Finds the starting edge for the search to find all edges that intersect the constraint
/// </summary>
/// <param name="constraint">The constraint being used to check for intersections</param>
internal bool FindStartingEdge(int[] vertexTriangles, EdgeConstraint constraint, out EdgeConstraint startingEdge)
{
// Initialize out parameter to default value
startingEdge = new EdgeConstraint(-1, -1);
// v_i->v_j are the start/end points of the constraint, respectively
int v_i = constraint.v1;
int v_j = constraint.v2;
// Start the search with an initial triangle that contains v_i
int tSearch = vertexTriangles[v_i];
// Reset visited states
for (int i = 0; i < visited.Length; i++)
{
visited[i] = false;
}
// Circle v_i until we find a triangle that contains an edge which intersects the constraint edge
// This will be the starting triangle in the search for finding all triangles that intersect the constraint
bool intersectionFound = false;
bool noCandidatesFound = false;
int intersectingEdgeIndex = E12;
int tE12, tE23, tE31;
while (!intersectionFound && !noCandidatesFound)
{
visited[tSearch] = true;
// Triangulation already contains the constraint so we ignore the constraint
if (TriangleContainsConstraint(tSearch, constraint))
{
return false;
}
// Check if the constraint intersects any edges of this triangle
else if (EdgeConstraintIntersectsTriangle(tSearch, constraint, out intersectingEdgeIndex))
{
intersectionFound = true;
break;
}
tE12 = triangulation[tSearch, E12];
tE23 = triangulation[tSearch, E23];
tE31 = triangulation[tSearch, E31];
// If constraint does not intersect this triangle, check adjacent triangles by crossing edges that have v_i as a vertex
// Avoid triangles that we have previously visited in the search
if (tE12 != OUT_OF_BOUNDS && !visited[tE12] && TriangleContainsVertex(tE12, v_i))
{
tSearch = tE12;
}
else if (tE23 != OUT_OF_BOUNDS && !visited[tE23] && TriangleContainsVertex(tE23, v_i))
{
tSearch = tE23;
}
else if (tE31 != OUT_OF_BOUNDS && !visited[tE31] && TriangleContainsVertex(tE31, v_i))
{
tSearch = tE31;
}
else
{
noCandidatesFound = true;
break;
}
}
if (intersectionFound)
{
int v_k = triangulation[tSearch, edgeVertex1[intersectingEdgeIndex]];
int v_l = triangulation[tSearch, edgeVertex2[intersectingEdgeIndex]];
int triangle2 = triangulation[tSearch, intersectingEdgeIndex];
startingEdge = new EdgeConstraint(v_k, v_l, tSearch, triangle2, intersectingEdgeIndex);
return true;
}
else
{
return false;
}
}
/// <summary>
/// Remove the edges from the triangulation that intersect the constraint. Find two triangles that
/// share the intersecting edge, swap the diagonal and repeat until no edges intersect the constraint.
/// </summary>
/// <param name="constraint">The constraint to check against</param>
/// <param name="intersectingEdges">A queue containing the previously found edges that intersect the constraint</param>
internal void RemoveIntersectingEdges(EdgeConstraint constraint, Queue<EdgeConstraint> intersectingEdges)
{
// Remove intersecting edges. Keep track of the new edges that we create
List<EdgeConstraint> newEdges = new List<EdgeConstraint>();
EdgeConstraint edge, newEdge;
// Mark the number of times we have been through the loop. If no new edges
// have been added after all edges have been visited, stop the loop. Every
// time an edge is added to newEdges, reset the counter.
int counter = 0;
// Loop through all intersecting edges until they have been properly resolved
// or they have all been visited with no diagonal swaps.
while (intersectingEdges.Count > 0 && counter <= intersectingEdges.Count)
{
edge = intersectingEdges.Dequeue();
Quad quad;
if (FindQuadFromSharedEdge(edge.t1, edge.t1Edge, out quad))
{
// If the quad is convex, we swap the diagonal (a quad is convex if the diagonals intersect)
// Otherwise push it back into the queue so we can swap the diagonal later on.
if (MathUtils.LinesIntersect(points[quad.q4].coords,
points[quad.q3].coords,
points[quad.q1].coords,
points[quad.q2].coords))
{
// Swap diagonals of the convex quads whose diagonals intersect the constraint
SwapQuadDiagonal(quad, intersectingEdges, newEdges, constraints);
// The new diagonal is between Q3 and Q4
newEdge = new EdgeConstraint(quad.q3, quad.q4, quad.t1, quad.t2, E31);
// If the new diagonal still intersects the constraint edge v_i->v_j,
// put back on the list of intersecting eddges
if (MathUtils.LinesIntersect(points[constraint.v1].coords,
points[constraint.v2].coords,
points[quad.q3].coords,
points[quad.q4].coords))
{
intersectingEdges.Enqueue(newEdge);
}
// Otherwise record in list of new edges
else
{
counter = 0;
newEdges.Add(newEdge);
}
}
else
{
intersectingEdges.Enqueue(edge);
}
}
counter++;
}
// If any new edges were formed due to a diagonal being swapped, restore the Delauney condition
// of the triangulation while respecting the constraints
if (newEdges.Count > 0)
{
RestoreConstrainedDelauneyTriangulation(constraint, newEdges);
}
}
/// <summary>
/// Restores the Delauney triangulation after the constraint has been inserted
/// </summary>
/// <param name="constraint">The constraint that was added to the triangulation</param>
/// <param name="newEdges">The list of new edges that were added</param>
internal void RestoreConstrainedDelauneyTriangulation(EdgeConstraint constraint, List<EdgeConstraint> newEdges)
{
// Iterate over the list of newly created edges and swap non-constraint diagonals until no more swaps take place
bool swapOccurred = true;
int counter = 0;
while (swapOccurred)
{
counter++;
swapOccurred = false;
for (int i = 0; i < newEdges.Count; i++)
{
EdgeConstraint edge = newEdges[i];
// If newly added edge is equal to constraint, we don't want to flip this edge so skip it
if (edge == constraint)
{
continue;
}
Quad quad;
if (FindQuadFromSharedEdge(edge.t1, edge.t1Edge, out quad))
{
if (SwapTest(points[quad.q1].coords, points[quad.q2].coords, points[quad.q3].coords, points[quad.q4].coords))
{
SwapQuadDiagonal(quad, newEdges, constraints, null);
// Enqueue the new diagonal
int v_m = quad.q3;
int v_n = quad.q4;
newEdges[i] = new EdgeConstraint(v_m, v_n, quad.t1, quad.t2, E31);
swapOccurred = true;
}
}
}
}
}
/// <summary>
/// Discards triangles that violate the any of the edge constraints
/// </summary>
internal void DiscardTrianglesViolatingConstraints()
{
// Initialize to all triangles being skipped
for (int i = 0; i < triangleCount; i++)
{
skipTriangle[i] = true;
}
// Identify the boundary edges
HashSet < (int, int) > boundaries = new HashSet < (int, int) > ();
for (int i = 0; i < this.constraints.Count; i++)
{
EdgeConstraint constraint = this.constraints[i];
boundaries.Add((constraint.v1, constraint.v2));
}
// Reset visited states
for (int i = 0; i < visited.Length; i++)
{
visited[i] = false;
}
// Search frontier
Queue<int> frontier = new Queue<int>();
int v1, v2, v3;
bool boundaryE12, boundaryE23, boundaryE31;
for (int i = 0; i < triangleCount; i++)
{
// If we've already visited this triangle, skip it
if (visited[i])
{
continue;
}
v1 = triangulation[i, V1];
v2 = triangulation[i, V2];
v3 = triangulation[i, V3];
boundaryE12 = boundaries.Contains((v1, v2));
boundaryE23 = boundaries.Contains((v2, v3));
boundaryE31 = boundaries.Contains((v3, v1));
// If this triangle has a boundary edge, start searching for adjacent triangles
if (boundaryE12 || boundaryE23 || boundaryE31)
{
skipTriangle[i] = false;
// Search along edges that are not boundary edges
frontier.Clear();
if (!boundaryE12)
{
frontier.Enqueue(triangulation[i, E12]);
}
if (!boundaryE23)
{
frontier.Enqueue(triangulation[i, E23]);
}
if (!boundaryE31)
{
frontier.Enqueue(triangulation[i, E31]);
}
// Recursively search along all non-boundary edges, marking the
// adjacent triangles as "keep"
while (frontier.Count > 0)
{
int k = frontier.Dequeue();
if (k == OUT_OF_BOUNDS || visited[k])
{
continue;
}
skipTriangle[k] = false;
visited[k] = true;
v1 = triangulation[k, V1];
v2 = triangulation[k, V2];
v3 = triangulation[k, V3];
// Continue searching along non-boundary edges
if (!boundaries.Contains((v1, v2)))
{
frontier.Enqueue(triangulation[k, E12]);
}
if (!boundaries.Contains((v2, v3)))
{
frontier.Enqueue(triangulation[k, E23]);
}
if (!boundaries.Contains((v3, v1)))
{
frontier.Enqueue(triangulation[k, E31]);
}
}
}
}
}
/// <summary>
/// Determines if the triangle contains the edge constraint
/// </summary>
/// <param name="t">The triangle to test</param>
/// <param name="constraint">The edge constraint</param>
/// <returns>True if the triangle contains one or both of the endpoints of the constraint</returns>
internal bool TriangleContainsConstraint(int t, EdgeConstraint constraint)
{
return (triangulation[t, V1] == constraint.v1 || triangulation[t, V2] == constraint.v1 || triangulation[t, V3] == constraint.v1) &&
(triangulation[t, V1] == constraint.v2 || triangulation[t, V2] == constraint.v2 || triangulation[t, V3] == constraint.v2);
}
/// <summary>
/// Returns true if the edge constraint intersects an edge of triangle `t`
/// </summary>
/// <param name="t">The triangle to test</param>
/// <param name="constraint">The edge constraint</param>
/// <param name="intersectingEdgeIndex">The index of the intersecting edge (E12, E23, E31)</param>
/// <returns>Returns true if an intersection is found, otherwise false.</returns>
internal bool EdgeConstraintIntersectsTriangle(int t, EdgeConstraint constraint, out int intersectingEdgeIndex)
{
Vector2 v_i = points[constraint.v1].coords;
Vector2 v_j = points[constraint.v2].coords;
Vector2 v1 = points[triangulation[t, V1]].coords;
Vector2 v2 = points[triangulation[t, V2]].coords;
Vector2 v3 = points[triangulation[t, V3]].coords;
if (MathUtils.LinesIntersect(v_i, v_j, v1, v2))
{
intersectingEdgeIndex = E12;
return true;
}
else if (MathUtils.LinesIntersect(v_i, v_j, v2, v3))
{
intersectingEdgeIndex = E23;
return true;
}
else if (MathUtils.LinesIntersect(v_i, v_j, v3, v1))
{
intersectingEdgeIndex = E31;
return true;
}
else
{
intersectingEdgeIndex = -1;
return false;
}
}
/// <summary>
/// Returns the quad formed by triangle `t1` and the other triangle that shares the intersecting edge
/// </summary>
/// <param name="t1">Base triangle</param>
/// <param name="intersectingEdge">Edge index that is being intersected</param>
internal bool FindQuadFromSharedEdge(int t1, int t1SharedEdge, out Quad quad)
{
// q3
// *---------*---------*
// \ / \ /
// \ t2L / \ t2R /
// \ / \ /
// \ / t2 \ /
// q1 *---------* q2
// / \ t1 / \
// / \ / \
// / t1L \ / t1R \
// / \ / \
// *---------*---------*
// q4
int q1, q2, q3, q4;
int t1L, t1R, t2L, t2R;
// t2 is adjacent to t1 along t1Edge
int t2 = triangulation[t1, t1SharedEdge];
int t2SharedEdge;
if (FindSharedEdge(t2, t1, out t2SharedEdge))
{
// Get the top 3 vertices of the quad from t2
if (t2SharedEdge == E12)
{
q2 = triangulation[t2, V1];
q1 = triangulation[t2, V2];
q3 = triangulation[t2, V3];
}
else if (t2SharedEdge == E23)
{
q2 = triangulation[t2, V2];
q1 = triangulation[t2, V3];
q3 = triangulation[t2, V1];
}
else // (t2SharedEdge == E31)
{
q2 = triangulation[t2, V3];
q1 = triangulation[t2, V1];
q3 = triangulation[t2, V2];
}
// q4 is the point in t1 opposite of the shared edge
q4 = triangulation[t1, oppositePoint[t1SharedEdge]];
// Get the adjacent triangles to make updating adjacency easier
t1L = triangulation[t1, previousEdge[t1SharedEdge]];
t1R = triangulation[t1, nextEdge[t1SharedEdge]];
t2L = triangulation[t2, nextEdge[t2SharedEdge]];
t2R = triangulation[t2, previousEdge[t2SharedEdge]];
quad = new Quad(q1, q2, q3, q4, t1, t2, t1L, t1R, t2L, t2R);
return true;
}
quad = new Quad();
return false;
}
/// <summary>
/// Swaps the diagonal of the quadrilateral q0->q1->q2->q3 formed by t1 and t2
/// </summary>
/// <param name="">The quad that will have its diagonal swapped</param>
internal void SwapQuadDiagonal(Quad quad, IEnumerable<EdgeConstraint> edges1, IEnumerable<EdgeConstraint> edges2, IEnumerable<EdgeConstraint> edges3)
{
// BEFORE
// q3
// *---------*---------*
// \ / \ /
// \ t2L / \ t2R /
// \ / \ /
// \ / t2 \ /
// q1 *---------* q2
// / \ t1 / \
// / \ / \
// / t1L \ / t1R \
// / \ / \
// *---------*---------*
// q4
// AFTER
// q3
// *---------*---------*
// \ /|\ /
// \ t2L / | \ t2R /
// \ / | \ /
// \ / | \ /
// q1 * t1 | t2 * q2
// / \ | / \
// / \ | / \
// / t1L \ | / t1R \
// / \|/ \
// *---------*---------*
// q4
int t1 = quad.t1;
int t2 = quad.t2;
int t1R = quad.t1R;
int t1L = quad.t1L;
int t2R = quad.t2R;
int t2L = quad.t2L;
// Perform the swap. As always, put the new vertex as the first vertex of the triangle
triangulation[t1, V1] = quad.q4;
triangulation[t1, V2] = quad.q1;
triangulation[t1, V3] = quad.q3;
triangulation[t2, V1] = quad.q4;
triangulation[t2, V2] = quad.q3;
triangulation[t2, V3] = quad.q2;
triangulation[t1, E12] = t1L;
triangulation[t1, E23] = t2L;
triangulation[t1, E31] = t2;
triangulation[t2, E12] = t1;
triangulation[t2, E23] = t2R;
triangulation[t2, E31] = t1R;
// Update adjacency for the adjacent triangles
UpdateAdjacency(t2L, t2, t1);
UpdateAdjacency(t1R, t1, t2);
// Now that triangles have moved, need to update edges as well
UpdateEdgesAfterSwap(edges1, t1, t2, t1L, t1R, t2L, t2R);
UpdateEdgesAfterSwap(edges2, t1, t2, t1L, t1R, t2L, t2R);
UpdateEdgesAfterSwap(edges3, t1, t2, t1L, t1R, t2L, t2R);
// Also need to update the vertexTriangles array since the vertices q1 and q2
// may have been referencing t2/t1 respectively and they are no longer.
vertexTriangles[quad.q1] = t1;
vertexTriangles[quad.q2] = t2;
}
/// <summary>
/// Update the Edges
/// </summary>
/// <param name="edges"></param>
/// <param name="t1"></param>
/// <param name="t2"></param>
/// <param name="t1L"></param>
/// <param name="t1R"></param>
/// <param name="t2L"></param>
/// <param name="t2R"></param>
internal void UpdateEdgesAfterSwap(IEnumerable<EdgeConstraint> edges, int t1, int t2, int t1L, int t1R, int t2L, int t2R)
{
if (edges == null)
{
return;
}
// Update edges to reflect changes in triangles
foreach (EdgeConstraint edge in edges)
{
if (edge.t1 == t1 && edge.t2 == t1R)
{
edge.t1 = t2;
edge.t2 = t1R;
edge.t1Edge = E31;
}
else if (edge.t1 == t1 && edge.t2 == t1L)
{
// Triangles stay the same
edge.t1Edge = E12;
}
else if (edge.t1 == t1R && edge.t2 == t1)
{
edge.t2 = t2;
}
else if (edge.t1 == t1L && edge.t2 == t1)
{
// Unchanged
}
else if (edge.t1 == t2 && edge.t2 == t2R)
{
// Triangles stay the same
edge.t1Edge = E23;
}
else if (edge.t1 == t2 && edge.t2 == t2L)
{
edge.t1 = t1;
edge.t2 = t2L;
edge.t1Edge = E23;
}
else if (edge.t1 == t2R && edge.t2 == t2)
{
// Unchanged
}
else if (edge.t1 == t2L && edge.t2 == t2)
{
edge.t2 = t1;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dae513e54953d9740a998fa1d26020b2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,87 @@
using UnityEngine.TestTools;
/// <summary>
/// Represents an edge constraint between two vertices in the triangulation
/// </summary>
public class EdgeConstraint
{
/// <summary>
/// Index of the first end point of the constraint
/// </summary>
public int v1;
/// <summary>
/// Index of the second end point of the constraint
/// </summary>
public int v2;
/// <summary>
/// Index of the triangle prior to the edge crossing (v1 -> v2)
/// </summary>
public int t1;
/// <summary>
/// Index of the triangle after the edge crossing (v1 -> v2)
/// </summary>
public int t2;
/// <summary>
/// Index of the edge on the t1 side
/// </summary>
public int t1Edge;
/// <summary>
/// Creates a new edge constraint with the given end points
/// </summary>
public EdgeConstraint(int v1, int v2)
{
this.v1 = v1;
this.v2 = v2;
this.t1 = -1;
this.t2 = -1;
}
/// <summary>
/// Creates a new edge constraint and defines triangles on either side of the edge
/// </summary>
public EdgeConstraint(int v1, int v2, int triangle1, int triangle2, int edge1)
{
this.v1 = v1;
this.v2 = v2;
this.t1 = triangle1;
this.t2 = triangle2;
this.t1Edge = edge1;
}
public override bool Equals(object obj)
{
if (obj is EdgeConstraint)
{
var other = (EdgeConstraint)obj;
return (this.v1 == other.v1 && this.v2 == other.v2) ||
(this.v1 == other.v2 && this.v2 == other.v1);
}
return false;
}
public override int GetHashCode()
{
return new { v1, v2 }.GetHashCode() + new { v2, v1 }.GetHashCode();
}
public static bool operator ==(EdgeConstraint lhs, EdgeConstraint rhs)
{
return lhs.Equals(rhs);
}
public static bool operator !=(EdgeConstraint lhs, EdgeConstraint rhs)
{
return !lhs.Equals(rhs);
}
[ExcludeFromCoverage]
public override string ToString()
{
return $"Edge: T{t1}->T{t2} (V{v1}->V{v2})";
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e465c2db3ee42004bb2588140d2c0275
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,315 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
public enum SlicedMeshSubmesh
{
Default = 0,
CutFace = 1
}
/// <summary>
/// Data structure used for storing mesh data during the fragmenting process
/// </summary>
public class FragmentData
{
/// <summary>
/// Vertex buffer for the non-cut mesh faces
/// </summary>
public List<MeshVertex> Vertices;
/// <summary>
/// Vertex buffer for the cut mesh faces
/// </summary>
public List<MeshVertex> CutVertices;
/// <summary>
/// Index buffer for each submesh
/// </summary>
public List<int>[] Triangles;
/// <summary>
/// List of edges constraints for the cut-face triangulation
/// </summary>
public List<EdgeConstraint> Constraints;
/// <summary>
/// Map between vertex indices in the source mesh and new indices for the sliced mesh
/// </summary>
public int[] IndexMap;
/// <summary>
/// The bounds of the vertex data (must manually call UpdateBounds() to update)
/// </summary>
public Bounds Bounds;
/// <summary>
/// Gets the total number of triangles across all sub meshes
/// </summary>
/// <value></value>
public int triangleCount
{
get
{
int count = 0;
for (int i = 0; i < this.Triangles.Length; i++)
{
count += this.Triangles[i].Count;
}
return count;
}
}
/// <summary>
/// Gets the total number of vertices in the mesh
/// </summary>
/// <value></value>
public int vertexCount
{
get
{
return this.Vertices.Count + this.CutVertices.Count;
}
}
/// <summary>
/// Initializes a new sliced mesh
/// </summary>
/// <param name="name">The name of the mesh</param>
/// <param name="vertexCount">Vertex count used to initialize lists. Initializing lists to approximate size reduces resizes and GC.</param>
/// <param name="triangleCount">Triangle count used to initialize lists. Initializing lists to approximate size reduces resizes and GC.</param>
public FragmentData(int vertexCount, int triangleCount)
{
this.Vertices = new List<MeshVertex>(vertexCount);
this.CutVertices = new List<MeshVertex>(vertexCount / 10);
// Store triangles for each submesh separately
this.Triangles = new List<int>[] {
new List<int>(triangleCount),
new List<int>(triangleCount / 10)
};
this.Constraints = new List<EdgeConstraint>();
this.IndexMap = new int[vertexCount];
}
/// <summary>
/// Creates a new sliced mesh dataset from source mesh data
/// </summary>
/// <param name="mesh">The source mesh data.</param>
public FragmentData(Mesh mesh)
{
var positions = mesh.vertices;
var normals = mesh.normals;
var uv = mesh.uv;
this.Vertices = new List<MeshVertex>(mesh.vertexCount);
this.CutVertices = new List<MeshVertex>(mesh.vertexCount / 10);
this.Constraints = new List<EdgeConstraint>();
this.IndexMap = new int[positions.Length];
// Add mesh vertices
for (int i = 0; i < positions.Length; i++)
{
this.Vertices.Add(new MeshVertex(positions[i], normals[i], uv[i]));
}
// Only meshes with one submesh are currently supported
this.Triangles = new List<int>[2];
this.Triangles[0] = new List<int>(mesh.GetTriangles(0));
if (mesh.subMeshCount >= 2)
{
this.Triangles[1] = new List<int>(mesh.GetTriangles(1));
}
else
{
this.Triangles[1] = new List<int>(mesh.triangles.Length / 10);
}
this.CalculateBounds();
}
/// <summary>
/// Adds a new cut face vertex
/// </summary>
/// <param name="position">The vertex position</param>
/// <param name="normal">The vertex normal</param>
/// <param name="uv">The vertex UV coordinates</param>
/// <returns>Returns the index of the vertex in the cutVertices array</returns>
public void AddCutFaceVertex(Vector3 position, Vector3 normal, Vector2 uv)
{
var vertex = new MeshVertex(position, normal, uv);
// Add the vertex to both the normal mesh vertex data and the cut face vertex data
// The vertex on the cut face will have different normal/uv coordinates which are
// populated with the correct values later in the triangulation process.
this.Vertices.Add(vertex);
this.CutVertices.Add(vertex);
}
/// <summary>
/// Adds a new vertex to this mesh that is mapped to the source mesh
/// </summary>
/// <param name="vertex">Vertex data</param>
/// <param name="sourceIndex">Index of the vertex in the source mesh</param>
public void AddMappedVertex(MeshVertex vertex, int sourceIndex)
{
this.Vertices.Add(vertex);
this.IndexMap[sourceIndex] = this.Vertices.Count - 1;
}
/// <summary>
/// Adds a new triangle to this mesh. The arguments v1, v2, v3 are the indexes of the
/// vertices relative to this mesh's list of vertices; no mapping is performed.
/// </summary>
/// <param name="v1">Index of the first vertex</param>
/// <param name="v2">Index of the second vertex</param>
/// <param name="v3">Index of the third vertex</param>
/// <param name="subMesh">The sub-mesh to add the triangle to</param>
public void AddTriangle(int v1, int v2, int v3, SlicedMeshSubmesh subMesh)
{
this.Triangles[(int)subMesh].Add(v1);
this.Triangles[(int)subMesh].Add(v2);
this.Triangles[(int)subMesh].Add(v3);
}
/// <summary>
/// Adds a new triangle to this mesh. The arguments v1, v2, v3 are the indices of the
/// vertices in the original mesh. These vertices are mapped to the indices in the sliced mesh.
/// </summary>
/// <param name="v1">Index of the first vertex</param>
/// <param name="v2">Index of the second vertex</param>
/// <param name="v3">Index of the third vertex</param>
/// <param name="subMesh">The sub-mesh to add the triangle to</param>
public void AddMappedTriangle(int v1, int v2, int v3, SlicedMeshSubmesh subMesh)
{
this.Triangles[(int)subMesh].Add(IndexMap[v1]);
this.Triangles[(int)subMesh].Add(IndexMap[v2]);
this.Triangles[(int)subMesh].Add(IndexMap[v3]);
}
/// <summary>
/// Finds coincident vertices on the cut face and welds them together.
/// </summary>
public void WeldCutFaceVertices()
{
// Temporary array containing the unique (welded) vertices
// Initialize capacity to current number of cut vertices to prevent
// unnecessary reallocations
List<MeshVertex> weldedVerts = new List<MeshVertex>(CutVertices.Count);
// We also keep track of the index mapping between the skipped vertices
// and the index of the welded vertex so we can update the edges
int[] indexMap = new int[CutVertices.Count];
// Number of welded vertices in the temp array
int k = 0;
// Loop through each vertex, identifying duplicates. Must compare directly
// because floating point inconsistencies cause a hash table to be unreliable
// for vertices that are very close together but not directly coincident
for(int i = 0; i < CutVertices.Count; i++)
{
bool duplicate = false;
for(int j = 0; j < weldedVerts.Count; j++)
{
if (CutVertices[i].position == weldedVerts[j].position)
{
indexMap[i] = j;
duplicate = true;
break;
}
}
if (!duplicate)
{
weldedVerts.Add(CutVertices[i]);
indexMap[i] = k;
k++;
}
}
// Update the edges
for(int i = 0; i < Constraints.Count; i++)
{
var edge = Constraints[i];
edge.v1 = indexMap[edge.v1];
edge.v2 = indexMap[edge.v2];
}
weldedVerts.TrimExcess();
// Update the cut vertices
this.CutVertices = new List<MeshVertex>(weldedVerts);
}
/// <summary>
/// Gets the triangles for the specified sub mesh
/// </summary>
/// <param name="subMeshIndex">The index of the submesh</param>
/// <returns></returns>
public int[] GetTriangles(int subMeshIndex)
{
return this.Triangles[subMeshIndex].ToArray();
}
/// <summary>
/// Calculates the bounds of the mesh data
/// </summary>
public void CalculateBounds()
{
float vertexCount = (float)Vertices.Count;
Vector3 min = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
Vector3 max = new Vector3(float.MinValue, float.MinValue, float.MinValue);
// The cut face does not modify the extents of the object, so we only need to
// loop through the original vertices to determine the bounds
foreach(MeshVertex vertex in Vertices)
{
if (vertex.position.x < min.x) min.x = vertex.position.x;
if (vertex.position.y < min.y) min.y = vertex.position.y;
if (vertex.position.z < min.z) min.z = vertex.position.z;
if (vertex.position.x > max.x) max.x = vertex.position.x;
if (vertex.position.y > max.y) max.y = vertex.position.y;
if (vertex.position.z > max.z) max.z = vertex.position.z;
}
this.Bounds = new Bounds((max + min) / 2f, max - min);
}
/// <summary>
/// Converts the sliced mesh data into a mesh
/// </summary>
/// <returns>Returns the mesh object</returns>
public Mesh ToMesh()
{
Mesh mesh = new Mesh();
var layout = new[]
{
new VertexAttributeDescriptor(VertexAttribute.Position, VertexAttributeFormat.Float32, 3),
new VertexAttributeDescriptor(VertexAttribute.Normal, VertexAttributeFormat.Float32, 3),
new VertexAttributeDescriptor(VertexAttribute.TexCoord0, VertexAttributeFormat.Float32, 2),
};
mesh.SetIndexBufferParams(triangleCount, IndexFormat.UInt32);
mesh.SetVertexBufferParams(vertexCount, layout);
mesh.SetVertexBufferData(Vertices, 0, 0, Vertices.Count);
mesh.SetVertexBufferData(CutVertices, 0, Vertices.Count, CutVertices.Count);
mesh.subMeshCount = Triangles.Length;
int indexStart = 0;
for(int i = 0; i < Triangles.Length; i++)
{
var subMeshIndexBuffer = Triangles[i];
mesh.SetIndexBufferData(subMeshIndexBuffer, 0, indexStart, subMeshIndexBuffer.Count);
mesh.SetSubMesh(i, new SubMeshDescriptor(indexStart, subMeshIndexBuffer.Count));
indexStart += subMeshIndexBuffer.Count;
}
mesh.RecalculateBounds();
return mesh;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 744e2e7cedf47b848b91e43dbf53e383
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,281 @@
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
{
/// <summary>
/// Generates the mesh fragments based on the provided options. The generated fragment objects are
/// stored as children of `fragmentParent`
/// </summary>
/// <param name="sourceObject">The source object to fragment. This object must have a MeshFilter, a RigidBody and a Collider.</param>
/// <param name="options">Options for the fragmenter</param>
/// <param name="fragmentTemplate">The template GameObject that each fragment will clone</param>
/// <param name="parent">The parent transform for the fragment objects</param>
/// <param name="saveToDisk">If true, the generated fragment meshes will be saved to disk so they can be re-used in prefabs.</param>
/// <param name="saveFolderPath">The save location for the fragments.</param>
/// <returns></returns>
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<MeshFilter>().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<FragmentData>();
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);
}
}
/// <summary>
/// Asynchronously generates the mesh fragments based on the provided options. The generated fragment objects are
/// stored as children of `fragmentParent`
/// </summary>
/// <param name="sourceObject">The source object to fragment. This object must have a MeshFilter, a RigidBody and a Collider.</param>
/// <param name="options">Options for the fragmenter</param>
/// <param name="fragmentTemplate">The template GameObject that each fragment will clone</param>
/// <param name="parent">The parent transform for the fragment objects</param>
/// <returns></returns>
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<MeshFilter>().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<FragmentData>();
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();
}
/// <summary>
/// Generates the mesh fragments based on the provided options. The generated fragment objects are
/// stored as children of `fragmentParent`
/// </summary>
/// <param name="sourceObject">The source object to slice. This object must have a MeshFilter, a RigidBody and a Collider.</param>
/// <param name="sliceNormal">The normal of the cut plane in the local frame of sourceObject.</param>
/// <param name="sliceOrigin">The origin of the cut plane in the local frame of sourceObject.</param>
/// <param name="options">Options for the slicer</param>
/// <param name="fragmentTemplate">The template GameObject that each slice will clone</param>
/// <param name="parent">The parent transform for the fragment objects</param>
/// <returns></returns>
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<MeshFilter>().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);
}
/// <summary>
/// Creates a new GameObject from the fragment data
/// </summary>
/// <param name="fragmentMeshData">Geometry of the fragment produced by the slicer</param>
/// <param name="sourceObject">The source object to fragment. This object must have a MeshFilter, a RigidBody and a Collider.</param>
/// <param name="fragmentTemplate">The template GameObject that each fragment will clone</param>
/// <param name="parent">The parent transform for the fragment objects</param>
/// <param name="i">Fragment counter</param>
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<MeshFilter>().sharedMesh.bounds.size;
var parentMass = sourceObject.GetComponent<Rigidbody>().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>();
meshFilter.sharedMesh = meshes[k];
var collider = fragment.GetComponent<MeshCollider>();
// If fragment collisions are disabled, collider will be null
collider.sharedMesh = meshes[k];
collider.convex = true;
collider.sharedMaterial = fragment.GetComponent<Collider>().sharedMaterial;
// Compute mass of the sliced object by dividing mesh bounds by density
var parentRigidBody = sourceObject.GetComponent<Rigidbody>();
var rigidBody = fragment.GetComponent<Rigidbody>();
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++;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c7d041b547ef59d47846c5a02021d424
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,318 @@
using UnityEngine;
/// <summary>
/// Class which handles slicing a mesh into two pieces given the origin and normal of the slice plane.
/// </summary>
public static class MeshSlicer
{
/// <summary>
/// Slices the mesh by the plane specified by `sliceNormal` and `sliceOrigin`
/// The sliced mesh data is return via out parameters.
/// </summary>
/// <param name="meshData"></param>
/// <param name="sliceNormal">The normal of the slice plane (points towards the top slice)</param>
/// <param name="sliceOrigin">The origin of the slice plane</param>
/// <param name="textureScale">Scale factor to apply to UV coordinates</param>
/// <param name="textureOffset">Offset to apply to UV coordinates</param>
/// <param name="topSlice">Out parameter returning fragment mesh data for slice above the plane</param>
/// <param name="bottomSlice">Out parameter returning fragment mesh data for slice below the plane</param>
public static void Slice(FragmentData meshData,
Vector3 sliceNormal,
Vector3 sliceOrigin,
Vector2 textureScale,
Vector2 textureOffset,
out FragmentData topSlice,
out FragmentData bottomSlice)
{
topSlice = new FragmentData(meshData.vertexCount, meshData.triangleCount);
bottomSlice = new FragmentData(meshData.vertexCount, meshData.triangleCount);
// Keep track of what side of the cutting plane each vertex is on
bool[] side = new bool[meshData.vertexCount];
// Go through and identify which vertices are above/below the split plane
for (int i = 0; i < meshData.Vertices.Count; i++)
{
var vertex = meshData.Vertices[i];
side[i] = vertex.position.IsAbovePlane(sliceNormal, sliceOrigin);
var slice = side[i] ? topSlice : bottomSlice;
slice.AddMappedVertex(vertex, i);
}
int offset = meshData.Vertices.Count;
for (int i = 0; i < meshData.CutVertices.Count; i++)
{
var vertex = meshData.CutVertices[i];
side[i + offset] = vertex.position.IsAbovePlane(sliceNormal, sliceOrigin);
var slice = side[i + offset] ? topSlice : bottomSlice;
slice.AddMappedVertex(vertex, i + offset);
}
SplitTriangles(meshData, topSlice, bottomSlice, sliceNormal, sliceOrigin, side, SlicedMeshSubmesh.Default);
SplitTriangles(meshData, topSlice, bottomSlice, sliceNormal, sliceOrigin, side, SlicedMeshSubmesh.CutFace);
// Fill in the cut plane for each mesh.
// The slice normal points to the "above" mesh, so the face normal for the cut face
// on the above mesh is opposite of the slice normal. Conversely, normal for the
// cut face on the "below" mesh is in the direction of the slice normal
FillCutFaces(topSlice, bottomSlice, -sliceNormal, textureScale, textureOffset);
}
/// <summary>
/// Fills the cut faces for each sliced mesh. The `sliceNormal` is the normal for the plane and points
/// in the direction of `topMeshData`
/// </summary>
/// <param name="topSlice">Fragment mesh data for slice above the slice plane</param>
/// <param name="bottomSlice">Fragment mesh data for slice above the slice plane</param>
/// <param name="sliceNormal">Normal of the slice plane (points towards the top slice)</param>
/// <param name="textureScale">Scale factor to apply to UV coordinates</param>
/// <param name="textureOffset">Offset to apply to UV coordinates</param>
private static void FillCutFaces(FragmentData topSlice,
FragmentData bottomSlice,
Vector3 sliceNormal,
Vector2 textureScale,
Vector2 textureOffset)
{
// Since the topSlice and bottomSlice both share the same cut face, we only need to calculate it
// once. Then the same vertex/triangle data for the face will be used for both slices, except
// with the normals reversed.
// First need to weld the coincident vertices for the triangulation to work properly
topSlice.WeldCutFaceVertices();
// Need at least 3 vertices to triangulate
if (topSlice.CutVertices.Count < 3) return;
// Triangulate the cut face
var triangulator = new ConstrainedTriangulator(topSlice.CutVertices, topSlice.Constraints, sliceNormal);
int[] triangles = triangulator.Triangulate();
// Update normal and UV for the cut face vertices
for (int i = 0; i < topSlice.CutVertices.Count; i++)
{
var vertex = topSlice.CutVertices[i];
var point = triangulator.points[i];
// UV coordinates are based off of the 2D coordinates used for triangulation
// During triangulation, coordinates are normalized to [0,1], so need to multiply
// by normalization scale factor to get back to the appropritate scale
Vector2 uv = new Vector2(
(triangulator.normalizationScaleFactor * point.coords.x) * textureScale.x + textureOffset.x,
(triangulator.normalizationScaleFactor * point.coords.y) * textureScale.y + textureOffset.y);
// Update normals and UV coordinates for the cut vertices
var topVertex = vertex;
topVertex.normal = sliceNormal;
topVertex.uv = uv;
var bottomVertex = vertex;
bottomVertex.normal = -sliceNormal;
bottomVertex.uv = uv;
topSlice.CutVertices[i] = topVertex;
bottomSlice.CutVertices[i] = bottomVertex;
}
// Add the new triangles to the top/bottom slices
int offsetTop = topSlice.Vertices.Count;
int offsetBottom = bottomSlice.Vertices.Count;
for (int i = 0; i < triangles.Length; i += 3)
{
topSlice.AddTriangle(
offsetTop + triangles[i],
offsetTop + triangles[i + 1],
offsetTop + triangles[i + 2],
SlicedMeshSubmesh.CutFace);
bottomSlice.AddTriangle(
offsetBottom + triangles[i],
offsetBottom + triangles[i + 2], // Swap two vertices so triangles are wound CW
offsetBottom + triangles[i + 1],
SlicedMeshSubmesh.CutFace);
}
}
/// <summary>
/// Identifies triangles that are intersected by the slice plane and splits them in two
/// </summary>
/// <param name="meshData"></param>
/// <param name="topSlice">Fragment mesh data for slice above the slice plane</param>
/// <param name="bottomSlice">Fragment mesh data for slice above the slice plane</param>
/// <param name="sliceNormal">The normal of the slice plane (points towards the top slice)</param>
/// <param name="sliceOrigin">The origin of the slice plane</param>
/// <param name="side">Array mapping each vertex to either the top/bottom slice</param>
/// <param name="subMesh">Index of the sub mesh</param>
private static void SplitTriangles(FragmentData meshData,
FragmentData topSlice,
FragmentData bottomSlice,
Vector3 sliceNormal,
Vector3 sliceOrigin,
bool[] side,
SlicedMeshSubmesh subMesh)
{
int[] triangles = meshData.GetTriangles((int)subMesh);
// Keep track of vertices that lie on the intersection plane
int a, b, c;
for (int i = 0; i < triangles.Length; i += 3)
{
// Get vertex indexes for this triangle
a = triangles[i];
b = triangles[i + 1];
c = triangles[i + 2];
// Triangle is contained completely within mesh A
if (side[a] && side[b] && side[c])
{
topSlice.AddMappedTriangle(a, b, c, subMesh);
}
// Triangle is contained completely within mesh B
else if (!side[a] && !side[b] && !side[c])
{
bottomSlice.AddMappedTriangle(a, b, c, subMesh);
}
// Triangle is intersected by the slicing plane. Need to subdivide it
else
{
// In these cases, two vertices of the triangle are above the cut plane and one vertex is below
if (side[b] && side[c] && !side[a])
{
SplitTriangle(b, c, a, sliceNormal, sliceOrigin, meshData, topSlice, bottomSlice, subMesh, true);
}
else if (side[c] && side[a] && !side[b])
{
SplitTriangle(c, a, b, sliceNormal, sliceOrigin, meshData, topSlice, bottomSlice, subMesh, true);
}
else if (side[a] && side[b] && !side[c])
{
SplitTriangle(a, b, c, sliceNormal, sliceOrigin, meshData, topSlice, bottomSlice, subMesh, true);
}
// In these cases, two vertices of the triangle are below the cut plane and one vertex is above
else if (!side[b] && !side[c] && side[a])
{
SplitTriangle(b, c, a, sliceNormal, sliceOrigin, meshData, topSlice, bottomSlice, subMesh, false);
}
else if (!side[c] && !side[a] && side[b])
{
SplitTriangle(c, a, b, sliceNormal, sliceOrigin, meshData, topSlice, bottomSlice, subMesh, false);
}
else if (!side[a] && !side[b] && side[c])
{
SplitTriangle(a, b, c, sliceNormal, sliceOrigin, meshData, topSlice, bottomSlice, subMesh, false);
}
}
}
}
/// <summary>
/// Splits triangle defined by the points (v1,v2,v3)
/// </summary>
/// <param name="v1_idx">Index of first vertex in triangle</param>
/// <param name="v2_idx">Index of second vertex in triangle<</param>
/// <param name="v3_idx">Index of third vertex in triangle<</param>
/// <param name="sliceNormal">The normal of the slice plane (points towards the top slice)</param>
/// <param name="sliceOrigin">The origin of the slice plane</param>
/// <param name="meshData">Original mesh data</param>
/// <param name="topSlice">Mesh data for top slice</param>
/// <param name="bottomSlice">Mesh data for bottom slice</param>
/// <param name="subMesh">Index of the submesh that the triangle belongs to</param>
/// <param name="v3BelowCutPlane">Boolean indicating whether v3 is above or below the slice plane.</param>
private static void SplitTriangle(int v1_idx,
int v2_idx,
int v3_idx,
Vector3 sliceNormal,
Vector3 sliceOrigin,
FragmentData meshData,
FragmentData topSlice,
FragmentData bottomSlice,
SlicedMeshSubmesh subMesh,
bool v3BelowCutPlane)
{
// - `v1`, `v2`, `v3` are the indexes of the triangle relative to the original mesh data
// - `v1` and `v2` are on the the side of split plane that belongs to meshA
// - `v3` is on the side of the split plane that belongs to meshB
// - `vertices`, `normals`, `uv` are the original mesh data used for interpolation
//
// v3BelowCutPlane = true
// ======================
//
// v1 *_____________* v2 .
// \ / /|\ cutNormal
// \ / |
// ----*-------*---------*--
// v13 \ / v23 cutOrigin
// \ /
// \ /
// * v3 triangle normal out of screen
//
// v3BelowCutPlane = false
// =======================
//
// * v3 .
// / \ /|\ cutNormal
// v23 / \ v13 |
// -----*-----*----------*--
// / \ cut origin
// / \
// v2 *___________* v1 triangle normal out of screen
//
float s13;
float s23;
Vector3 v13;
Vector3 v23;
MeshVertex v1 = v1_idx < meshData.Vertices.Count ? meshData.Vertices[v1_idx] : meshData.CutVertices[v1_idx - meshData.Vertices.Count];
MeshVertex v2 = v2_idx < meshData.Vertices.Count ? meshData.Vertices[v2_idx] : meshData.CutVertices[v2_idx - meshData.Vertices.Count];
MeshVertex v3 = v3_idx < meshData.Vertices.Count ? meshData.Vertices[v3_idx] : meshData.CutVertices[v3_idx - meshData.Vertices.Count];
if (MathUtils.LinePlaneIntersection(v1.position, v3.position, sliceNormal, sliceOrigin, out v13, out s13) &&
MathUtils.LinePlaneIntersection(v2.position, v3.position, sliceNormal, sliceOrigin, out v23, out s23))
{
// Interpolate normals and UV coordinates
var norm13 = (v1.normal + s13 * (v3.normal - v1.normal)).normalized;
var norm23 = (v2.normal + s23 * (v3.normal - v2.normal)).normalized;
var uv13 = v1.uv + s13 * (v3.uv - v1.uv);
var uv23 = v2.uv + s23 * (v3.uv - v2.uv);
// Add vertices/normals/uv for the intersection points to each mesh
topSlice.AddCutFaceVertex(v13, norm13, uv13);
topSlice.AddCutFaceVertex(v23, norm23, uv23);
bottomSlice.AddCutFaceVertex(v13, norm13, uv13);
bottomSlice.AddCutFaceVertex(v23, norm23, uv23);
// Indices for the intersection vertices (for the original mesh data)
int index13_A = topSlice.Vertices.Count - 2;
int index23_A = topSlice.Vertices.Count - 1;
int index13_B = bottomSlice.Vertices.Count - 2;
int index23_B = bottomSlice.Vertices.Count - 1;
if (v3BelowCutPlane)
{
// Triangle slice above the cutting plane is a quad, so divide into two triangles
topSlice.AddTriangle(index23_A, index13_A, topSlice.IndexMap[v2_idx], subMesh);
topSlice.AddTriangle(index13_A, topSlice.IndexMap[v1_idx], topSlice.IndexMap[v2_idx], subMesh);
// One triangle must be added to mesh 2
bottomSlice.AddTriangle(bottomSlice.IndexMap[v3_idx], index13_B, index23_B, subMesh);
// When looking at the cut-face, the edges should wind counter-clockwise
topSlice.Constraints.Add(new EdgeConstraint(topSlice.CutVertices.Count - 2, topSlice.CutVertices.Count - 1));
bottomSlice.Constraints.Add(new EdgeConstraint(bottomSlice.CutVertices.Count - 1, bottomSlice.CutVertices.Count - 2));
}
else
{
// Triangle slice above the cutting plane is a simple triangle
topSlice.AddTriangle(index13_A, index23_A, topSlice.IndexMap[v3_idx], subMesh);
// Triangle slice below the cutting plane is a quad, so divide into two triangles
bottomSlice.AddTriangle(bottomSlice.IndexMap[v1_idx], bottomSlice.IndexMap[v2_idx], index13_B, subMesh);
bottomSlice.AddTriangle(bottomSlice.IndexMap[v2_idx], index23_B, index13_B, subMesh);
// When looking at the cut-face, the edges should wind counter-clockwise
topSlice.Constraints.Add(new EdgeConstraint(topSlice.CutVertices.Count - 1, topSlice.CutVertices.Count - 2));
bottomSlice.Constraints.Add(new EdgeConstraint(bottomSlice.CutVertices.Count - 2, bottomSlice.CutVertices.Count - 1));
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 838da7c4073cb5544a139058fcac2186
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,54 @@
using UnityEngine;
using UnityEngine.TestTools;
/// <summary>
/// Data structure containing position/normal/UV data for a single vertex
/// </summary>
public struct MeshVertex
{
public Vector3 position;
public Vector3 normal;
public Vector2 uv;
public MeshVertex(Vector3 position)
{
this.position = position;
this.normal = Vector3.zero;
this.uv = Vector2.zero;
}
public MeshVertex(Vector3 position, Vector3 normal, Vector2 uv)
{
this.position = position;
this.normal = normal;
this.uv = uv;
}
public override bool Equals(object obj)
{
if (!(obj is MeshVertex)) return false;
return ((MeshVertex)obj).position.Equals(this.position);
}
public static bool operator ==(MeshVertex lhs, MeshVertex rhs)
{
return lhs.Equals(rhs);
}
public static bool operator !=(MeshVertex lhs, MeshVertex rhs)
{
return !lhs.Equals(rhs);
}
public override int GetHashCode()
{
return this.position.GetHashCode();
}
[ExcludeFromCoverage]
public override string ToString()
{
return $"Position = {position}, Normal = {normal}, UV = {uv}";
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 92df5b8640ebfb243a19d317a09dec51
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,56 @@
using UnityEngine.TestTools;
/// <summary>
/// Data structure that holds triangulation adjacency data for a quad
/// </summary>
public struct Quad
{
// q3
// *---------*---------*
// \ / \ /
// \ t2L / \ t2R /
// \ / \ /
// \ / t2 \ /
// q1 *---------* q2
// / \ t1 / \
// / \ / \
// / t1L \ / t1R \
// / \ / \
// *---------*---------*
// q4
/// <summary>
/// The indices of the quad vertices
/// </summary>
public int q1, q2, q3, q4;
/// <summary>
/// The triangles that make up the quad
/// </summary>
public int t1, t2;
/// <summary>
/// Triangle adjacency data
/// </summary>
public int t1L, t1R, t2L, t2R;
public Quad(int q1, int q2, int q3, int q4, int t1, int t2, int t1L, int t1R, int t2L, int t2R)
{
this.q1 = q1;
this.q2 = q2;
this.q3 = q3;
this.q4 = q4;
this.t1 = t1;
this.t2 = t2;
this.t1L = t1L;
this.t1R = t1R;
this.t2L = t2L;
this.t2R = t2R;
}
[ExcludeFromCoverage]
public override string ToString()
{
return $"T{t1}/T{t2} (V{q1},V{q2},V{q3},V{q4})";
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1124001d561fa9e4dbaef234fcbb0d10
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,40 @@
using UnityEngine;
using UnityEngine.TestTools;
/// <summary>
/// This data structure is used to represent a point during triangulation.
/// </summary>
public class TriangulationPoint: IBinSortable
{
/// <summary>
/// 2D coordinates of the point on the triangulation plane
/// </summary>
public Vector2 coords;
/// <summary>
/// Bin used for sorting points in grid
/// </summary>
public int bin { get; set; }
/// <summary>
/// Original index prior to sorting
/// </summary>
public int index = 0;
/// <summary>
/// Instantiates a new triangulation point
/// </summary>
/// <param name="index">The index of the point in the original point list</param>
/// <param name="coords">The 2D coordinates of the point in the triangulation plane</param>
public TriangulationPoint(int index, Vector2 coords)
{
this.index = index;
this.coords = coords;
}
[ExcludeFromCoverage]
public override string ToString()
{
return $"{coords} -> {bin}";
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: db421ee84ac7f3246925361ae77699bf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,599 @@
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Logic for triangulating a set of 3D points. Only supports convex polygons.
/// </summary>
public class Triangulator
{
// Constants for triangulation array indices
protected const int V1 = 0; // Vertex 1
protected const int V2 = 1; // Vertex 2
protected const int V3 = 2; // Vertex 3
protected const int E12 = 3; // Adjacency data for edge (V1 -> V2)
protected const int E23 = 4; // Adjacency data for edge (V2 -> V3)
protected const int E31 = 5; // Adjacency data for edge (V3 -> V1)
// Index for super triangle
protected const int SUPERTRIANGLE = 0;
// Index for out of bounds triangle (boundary edge)
protected const int OUT_OF_BOUNDS = -1;
// Number of points to be triangulated (excluding super triangle vertices)
protected int N;
// Total number of triangles generated during triangulation
protected int triangleCount;
// Triangle vertex and adjacency data
// Index 0 = Triangle index
// Index 1 = [V1, V2, V3, E12, E23, E32]
protected int[, ] triangulation;
// Points on the plane to triangulate
public TriangulationPoint[] points;
// Array which tracks which triangles should be ignored in the final triangulation
protected bool[] skipTriangle;
// Normal of the plane on which the points lie
protected Vector3 normal;
// Normalization scale factor
public float normalizationScaleFactor = 1f;
/// <summary>
/// Initializes the triangulator with the vertex data to be triangulated
/// </summary>
/// <param name="inputPoints">The points to triangulate</param>
/// <param name="normal">The normal of the triangulation plane</param>
public Triangulator(List<MeshVertex> inputPoints, Vector3 normal)
{
// Need at least three input vertices to triangulate
if (inputPoints == null || inputPoints.Count < 3)
{
return;
}
this.N = inputPoints.Count;
this.triangleCount = 2 * N + 1;
this.triangulation = new int[triangleCount, 6];
this.skipTriangle = new bool[triangleCount];
this.points = new TriangulationPoint[N + 3]; // Extra 3 points used to store super triangle
this.normal = normal;
// Choose two points in the plane as one basis vector
Vector3 e1 = (inputPoints[0].position - inputPoints[1].position).normalized;
Vector3 e2 = normal.normalized;
Vector3 e3 = Vector3.Cross(e1, e2).normalized;
// To find the 2nd basis vector, find the largest component and swap with the smallest, negating the largest
// Project 3D vertex onto the 2D plane
for (int i = 0; i < N; i++)
{
var position = inputPoints[i].position;
var coords = new Vector2(Vector3.Dot(position, e1), Vector3.Dot(position, e3));
this.points[i] = new TriangulationPoint(i, coords);
}
}
/// <summary>
/// Performs the triangulation
/// </summary>
/// <returns>Returns an array containing the indices of the triangles, mapped to the list of points passed in during initialization</returns>
public virtual int[] Triangulate()
{
// Need at least 3 vertices to triangulate
if (N < 3)
{
return new int[] { };
}
this.AddSuperTriangle();
this.NormalizeCoordinates();
this.ComputeTriangulation();
this.DiscardTrianglesWithSuperTriangleVertices();
List<int> triangles = new List<int>(3 * triangleCount);
for (int i = 0; i < triangleCount; i++)
{
// Add all triangles that don't contain a super-triangle vertex
if (!skipTriangle[i])
{
triangles.Add(triangulation[i, V1]);
triangles.Add(triangulation[i, V2]);
triangles.Add(triangulation[i, V3]);
}
}
return triangles.ToArray();
}
/// <summary>
/// Uniformly scales the 2D coordinates of all the points between [0, 1]
/// </summary>
protected void NormalizeCoordinates()
{
// 1) Normalize coordinates. Coordinates are scaled so they lie between 0 and 1
// The scaling should be uniform so relative positions of points are unchanged
float xMin = float.MaxValue;
float xMax = float.MinValue;
float yMin = float.MaxValue;
float yMax = float.MinValue;
// Find min/max points in the set
for (int i = 0; i < N; i++)
{
var point = points[i];
if (point.coords.x < xMin) xMin = point.coords.x;
if (point.coords.y < yMin) yMin = point.coords.y;
if (point.coords.x > xMax) xMax = point.coords.x;
if (point.coords.y > yMax) yMax = point.coords.y;
}
// Normalization coefficient. Using same coefficient for both x & y
// ensures uniform scaling
normalizationScaleFactor = Mathf.Max(xMax - xMin, yMax - yMin);
// Normalize each point
for (int i = 0; i < N; i++)
{
var point = points[i];
var normalizedPos = new Vector2(
(point.coords.x - xMin) / normalizationScaleFactor,
(point.coords.y - yMin) / normalizationScaleFactor);
points[i].coords = normalizedPos;
}
}
/// <summary>
/// Sorts the points into bins using an ordered grid
/// </summary>
/// <returns>Returns the array of sorted points</returns>
protected TriangulationPoint[] SortPointsIntoBins()
{
// Compute the number of bins along each axis
int n = Mathf.RoundToInt(Mathf.Pow((float) N, 0.25f));
// Total bin count
int binCount = n * n;
// Assign bin numbers to each point by taking the normalized coordinates
// and dividing them into a n x n grid.
for (int k = 0; k < N; k++)
{
var point = this.points[k];
int i = (int) (0.99f * n * point.coords.y);
int j = (int) (0.99f * n * point.coords.x);
point.bin = BinSort.GetBinNumber(i, j, n);
}
return BinSort.Sort<TriangulationPoint>(this.points, N, binCount);
}
/// <summary>
/// Computes the triangulation of the point set.
/// </summary>
/// <returns>Returns true if the triangulation was successful</returns>
protected bool ComputeTriangulation()
{
// Index of the current triangle being searched
int tSearch = 0;
// Index of the last triangle formed
int tLast = 0;
var sortedPoints = SortPointsIntoBins();
// Loop through each point and insert it into the triangulation
for (int i = 0; i < N; i++)
{
TriangulationPoint point = sortedPoints[i];
// Insert new point into the triangulation. Start by finding the triangle that contains the point `p`
// Keep track of how many triangles we visited in case search fails and we get stuck in a loop
int counter = 0;
bool pointInserted = false;
while (!pointInserted)
{
if (counter++ > tLast || tSearch == OUT_OF_BOUNDS)
{
break;
}
// Get coordinates of triangle vertices
var v1 = this.points[triangulation[tSearch, V1]].coords;
var v2 = this.points[triangulation[tSearch, V2]].coords;
var v3 = this.points[triangulation[tSearch, V3]].coords;
// Verify that point is on the correct side of each edge of the triangle.
// If a point is on the left side of an edge, move to the adjacent triangle and check again. The search
// continues until a containing triangle is found or the point is outside of all triangles
if (!MathUtils.IsPointOnRightSideOfLine(v1, v2, point.coords))
{
tSearch = triangulation[tSearch, E12];
}
else if (!MathUtils.IsPointOnRightSideOfLine(v2, v3, point.coords))
{
tSearch = triangulation[tSearch, E23];
}
else if (!MathUtils.IsPointOnRightSideOfLine(v3, v1, point.coords))
{
tSearch = triangulation[tSearch, E31];
}
// If it is on the right side of all three edges, it is contained within the triangle (Unity uses CW winding).
else
{
InsertPointIntoTriangle(point, tSearch, tLast);
tLast += 2;
tSearch = tLast;
pointInserted = true;
}
}
}
return true;
}
/// <summary>
/// Initializes the triangulation by inserting the super triangle
/// </summary>
protected void AddSuperTriangle()
{
// Add new points to the end of the points array
this.points[N] = new TriangulationPoint(N, new Vector2(-100f, -100f));
this.points[N + 1] = new TriangulationPoint(N + 1, new Vector2(0f, 100f));
this.points[N + 2] = new TriangulationPoint(N + 2, new Vector2(100f, -100f));
// Store supertriangle in the first column of the vertex and adjacency data
triangulation[SUPERTRIANGLE, V1] = N;
triangulation[SUPERTRIANGLE, V2] = N + 1;
triangulation[SUPERTRIANGLE, V3] = N + 2;
// Zeros signify boundary edges
triangulation[SUPERTRIANGLE, E12] = OUT_OF_BOUNDS;
triangulation[SUPERTRIANGLE, E23] = OUT_OF_BOUNDS;
triangulation[SUPERTRIANGLE, E31] = OUT_OF_BOUNDS;
}
/// <summary>
/// Inserts the point `p` into triangle `t`, replacing it with three new triangles
/// </summary>
/// <param name="p">The index of the point to insert</param>
/// <param name="t">The index of the triangle</param>
/// <param name="triangleCount">Total number of triangles created so far</param>
protected void InsertPointIntoTriangle(TriangulationPoint p, int t, int triangleCount)
{
// V1
// *
// /|\
// /3|2\
// / | \
// / | \
// / | \
// / | \
// / t1 | t3 \
// / | \
// / 1 * 1 \
// / __/1\__ \
// / __/ \__ \
// / 2__/ t2 \__3 \
// / _/3 2\_ \
// *---------------------------*
// V3 V2
int t1 = t;
int t2 = triangleCount + 1;
int t3 = triangleCount + 2;
// Add the vertex & adjacency information for the two new triangles
// New vertex is set to first vertex of each triangle to help with
// restoring the triangulation later on
triangulation[t2, V1] = p.index;
triangulation[t2, V2] = triangulation[t, V2];
triangulation[t2, V3] = triangulation[t, V3];
triangulation[t2, E12] = t3;
triangulation[t2, E23] = triangulation[t, E23];
triangulation[t2, E31] = t1;
triangulation[t3, V1] = p.index;
triangulation[t3, V2] = triangulation[t, V1];
triangulation[t3, V3] = triangulation[t, V2];
triangulation[t3, E12] = t1;
triangulation[t3, E23] = triangulation[t, E12];
triangulation[t3, E31] = t2;
// Triangle index remains the same for E12, no need to update adjacency
UpdateAdjacency(triangulation[t, E12], t, t3);
UpdateAdjacency(triangulation[t, E23], t, t2);
// Replace existing triangle `t` with `t1`
triangulation[t1, V2] = triangulation[t, V3];
triangulation[t1, V3] = triangulation[t, V1];
triangulation[t1, V1] = p.index;
triangulation[t1, E23] = triangulation[t, E31];
triangulation[t1, E12] = t2;
triangulation[t1, E31] = t3;
// After the triangles have been inserted, restore the Delauney triangulation
RestoreDelauneyTriangulation(p, t1, t2, t3);
}
/// <summary>
/// Restores the triangulation to a Delauney triangulation after new triangles have been added.
/// </summary>
/// <param name="p">Index of the inserted point</param>
/// <param name="t1">Index of first triangle to check</param>
/// <param name="t2">Index of second triangle to check</param>
/// <param name="t3">Index of third triangle to check</param>
protected void RestoreDelauneyTriangulation(TriangulationPoint p, int t1, int t2, int t3)
{
int t4;
Stack < (int, int) > s = new Stack < (int, int) > ();
s.Push((t1, triangulation[t1, E23]));
s.Push((t2, triangulation[t2, E23]));
s.Push((t3, triangulation[t3, E23]));
while (s.Count > 0)
{
// Pop next triangle and its adjacent triangle off the stack
// t1 contains the newly added vertex at V1
// t2 is adjacent to t1 along the opposite edge of V1
(t1, t2) = s.Pop();
if (t2 == OUT_OF_BOUNDS)
{
continue;
}
// If t2 circumscribes p, the quadrilateral formed by t1+t2 has the
// diagonal drawn in the wrong direction and needs to be swapped
else if (SwapQuadDiagonalIfNeeded(p.index, t1, t2, out t3, out t4))
{
// Push newly formed triangles onto the stack to see if their diagonals
// need to be swapped
s.Push((t1, t3));
s.Push((t2, t4));
}
}
}
/// <summary>
/// Swaps the diagonal of the quadrilateral formed by triangle `t` and the
/// triangle adjacent to the edge that is opposite of the newly added point
/// </summary>
/// <param name="p">The index of the inserted point</param>
/// <param name="t1">Index of the triangle containing p</param>
/// <param name="t2">Index of the triangle opposite t1 that shares edge E23 with t1</param>
/// <param name="t3">Index of triangle adjacent to t1 after swap</param>
/// <param name="t4">Index of triangle adjacent to t2 after swap</param>
/// <returns>Returns true if the swap was performed. If the swap was not
/// performed (e.g. returns false), t3 and t4 are unused.
/// </returns>
protected bool SwapQuadDiagonalIfNeeded(int p, int t1, int t2, out int t3, out int t4)
{
// 1) Form quadrilateral from t1 + t2 (q0->q1->q2->q3)
// 2) Swap diagonal between q1->q3 to q0->q2
//
// BEFORE AFTER
//
// q3 q3
// *-------------*-------------* *-------------*-------------*
// \ / \ / \ /|\ /
// \ t3 / \ t4 / \ t3 /3|2\ t4 /
// \ / \ / \ / | \ /
// \ / \ / \ / | \ /
// \ / t2 \ / \ / | \ /
// \ / \ / \ / | \ /
// q1 *-------------* q2 q1 * 2 t1 | t2 3 * q2
// \2 3/ \ | /
// \ / \ | /
// \ t1 / \ | /
// \ / \ | /
// \ / \1|1/
// \1/ \|/
// * q4 == p * q4 == p
//
// Get the vertices of the quad. The new vertex is always located at V1 of the triangle
int q4 = p;
int q1, q2, q3;
// Since t2 might be oriented in any direction, find which edge is adjacent to `t`
// The 4th vertex of the quad will be opposite this edge. We also need the two triangles
// t3 and t3 that are adjacent to t2 along the other edges since the adjacency information
// needs to be updated for those triangles.
if (triangulation[t2, E12] == t1)
{
q1 = triangulation[t2, V2];
q2 = triangulation[t2, V1];
q3 = triangulation[t2, V3];
t3 = triangulation[t2, E23];
t4 = triangulation[t2, E31];
}
else if (triangulation[t2, E23] == t1)
{
q1 = triangulation[t2, V3];
q2 = triangulation[t2, V2];
q3 = triangulation[t2, V1];
t3 = triangulation[t2, E31];
t4 = triangulation[t2, E12];
}
else // (triangulation[t2, E31] == t1)
{
q1 = triangulation[t2, V1];
q2 = triangulation[t2, V3];
q3 = triangulation[t2, V2];
t3 = triangulation[t2, E12];
t4 = triangulation[t2, E23];
}
// Perform test to see if p lies in the circumcircle of t2
if (SwapTest(points[q1].coords, points[q2].coords, points[q3].coords, points[q4].coords))
{
// Update adjacency for triangles adjacent to t1 and t2
UpdateAdjacency(t3, t2, t1);
UpdateAdjacency(triangulation[t1, E31], t1, t2);
// Perform the swap. As always, put the new vertex as the first vertex of the triangle
triangulation[t1, V1] = q4;
triangulation[t1, V2] = q1;
triangulation[t1, V3] = q3;
triangulation[t2, V1] = q4;
triangulation[t2, V2] = q3;
triangulation[t2, V3] = q2;
// Update adjacency information (order of operations is important here since we
// are overwriting data).
triangulation[t2, E12] = t1;
triangulation[t2, E23] = t4;
triangulation[t2, E31] = triangulation[t1, E31];
// triangulation[t1, E12] = t2;
triangulation[t1, E23] = t3;
triangulation[t1, E31] = t2;
return true;
}
else
{
return false;
}
}
/// <summary>
/// Marks any triangles that contain super-triangle vertices as discarded
/// </summary>
protected void DiscardTrianglesWithSuperTriangleVertices()
{
for (int i = 0; i < triangleCount; i++)
{
// Add all triangles that don't contain a super-triangle vertex
if (TriangleContainsVertex(i, N) ||
TriangleContainsVertex(i, N + 1) ||
TriangleContainsVertex(i, N + 2))
{
skipTriangle[i] = true;
}
}
}
/// <summary>
/// Checks to see if the triangle formed by points v1->v2->v3 circumscribes point vP
/// </summary>
/// <param name="v1">Coordinates of 1st vertex of triangle</param>
/// <param name="v2">Coordinates of 2nd vertex of triangle</param>
/// <param name="v3">Coordinates of 3rd vertex of triangle</param>
/// <param name="v4">Coordinates of test point</param>
/// <returns> Returns true if the triangle `t` circumscribes the point `p`</returns>
protected bool SwapTest(Vector2 v1, Vector2 v2, Vector2 v3, Vector2 v4)
{
float x13 = v1.x - v3.x;
float x23 = v2.x - v3.x;
float y13 = v1.y - v3.y;
float y23 = v2.y - v3.y;
float x14 = v1.x - v4.x;
float x24 = v2.x - v4.x;
float y14 = v1.y - v4.y;
float y24 = v2.y - v4.y;
float cosA = x13 * x23 + y13 * y23;
float cosB = x24 * x14 + y24 * y14;
if (cosA >= 0 && cosB >= 0)
{
return false;
}
else if (cosA < 0 && cosB < 0)
{
return true;
}
else
{
float sinA = (x13 * y23 - x23 * y13);
float sinB = (x24 * y14 - x14 * y24);
float sinAB = sinA * cosB + sinB * cosA;
return sinAB < 0;
}
}
/// <summary>
/// Checks if the triangle `t` contains the specified vertex
/// </summary>
/// <param name="t">The index of the triangle</param>
/// <param name="v">The index of the vertex</param>
/// <returns>Returns true if the triangle `t` contains the vertex `v`</returns>
protected bool TriangleContainsVertex(int t, int v)
{
return triangulation[t, V1] == v || triangulation[t, V2] == v || triangulation[t, V3] == v;
}
/// <summary>
/// Updates the adjacency information in triangle `t`. Any references to `tOld are
/// replaced with `tNew`
/// </summary>
/// <param name="t">The index of the triangle to update</param>
/// <param name="tOld">The index to be replaced</param>
/// <param name="tNew">The new index to replace with</param>
protected void UpdateAdjacency(int t, int tOld, int tNew)
{
// Boundary edge, no triangle exists
int sharedEdge;
if (t == OUT_OF_BOUNDS)
{
return;
}
else if (FindSharedEdge(t, tOld, out sharedEdge))
{
triangulation[t, sharedEdge] = tNew;
}
}
/// <summary>
/// Finds the edge index for triangle `tOrigin` that is adjacent to triangle `tAdjacent`
/// </summary>
/// <param name="tOrigin">The origin triangle to search</param>
/// <param name="tAdjacent">The triangle index to search for</param>
/// <param name="edgeIndex">Edge index returned as an out parameter</param>
/// <returns>True if `tOrigin` is adjacent to `tAdjacent` and supplies the
/// shared edge index via the out parameter. If `tOrigin` is an invalid index or
/// `tAdjacent` is not adjacent to `tOrigin`, returns false.</returns>
protected bool FindSharedEdge(int tOrigin, int tAdjacent, out int edgeIndex)
{
edgeIndex = 0;
if (tOrigin == OUT_OF_BOUNDS)
{
return false;
}
else if (triangulation[tOrigin, E12] == tAdjacent)
{
edgeIndex = E12;
return true;
}
else if (triangulation[tOrigin, E23] == tAdjacent)
{
edgeIndex = E23;
return true;
}
else if (triangulation[tOrigin, E31] == tAdjacent)
{
edgeIndex = E31;
return true;
}
else
{
return false;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c8e611cd8a0ddf64a802581a422b9733
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,83 @@
using UnityEngine;
using UnityEngine.Events;
public class UnfreezeFragment : MonoBehaviour
{
[Tooltip("Options for triggering the fracture")]
public TriggerOptions triggerOptions;
[Tooltip("If true, all sibling fragments will be unfrozen if the trigger conditions for this fragment are met.")]
public bool unfreezeAll = true;
[Tooltip("This callback is invoked when the fracturing process has been completed.")]
public UnityEvent onFractureCompleted;
// True if this fragment has already been unfrozen
private bool isFrozen = true;
void OnCollisionEnter(Collision collision)
{
if (!this.isFrozen)
{
return;
}
if (collision.contactCount > 0)
{
// Collision force must exceed the minimum force (F = I / T = F)
var contact = collision.contacts[0];
var collisionForce = collision.impulse.magnitude / Time.fixedDeltaTime;
// Colliding object tag must be in the set of allowed collision tags if filtering by tag is enabled
bool colliderTagAllowed = triggerOptions.IsTagAllowed(contact.otherCollider.gameObject.tag);
// Fragment is unfrozen if the colliding object has the correct tag (if tag filtering is enabled)
// and the collision force exceeds the minimum collision force.
if (collisionForce > triggerOptions.minimumCollisionForce &&
(!triggerOptions.filterCollisionsByTag || colliderTagAllowed))
{
this.Unfreeze();
}
}
}
void OnTriggerEnter(Collider collider)
{
if (!this.isFrozen)
{
return;
}
bool tagAllowed = triggerOptions.IsTagAllowed(collider.gameObject.tag);
if (!triggerOptions.filterCollisionsByTag || triggerOptions.IsTagAllowed(collider.gameObject.tag))
{
this.Unfreeze();
}
}
private void Unfreeze()
{
if (this.unfreezeAll)
{
foreach(UnfreezeFragment fragment in this.transform.parent.GetComponentsInChildren<UnfreezeFragment>())
{
fragment.UnfreezeThis();
}
}
else
{
UnfreezeThis();
}
if (this.onFractureCompleted != null)
{
this.onFractureCompleted.Invoke();
}
}
private void UnfreezeThis()
{
this.GetComponent<Rigidbody>().constraints = RigidbodyConstraints.None;
this.isFrozen = false;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8db8defab3610854196e7e67bb44cc26
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,14 @@
{
"name": "RuntimeAssembly",
"rootNamespace": "",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 7b1a8d9f4355a214f9a95471fa510c86
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 26a59f482c449a140b34fcccb46073f4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,24 @@
using System;
using UnityEngine;
using UnityEngine.Events;
[Serializable]
public class CallbackOptions
{
[Tooltip("This callback is invoked when a fracture has been triggered. Not called for slicing and prefracturing.")]
public UnityEvent<Collider, GameObject, Vector3> onFracture;
[Tooltip("This callback is invoked when the fracturing/slicing process has been completed.")]
public UnityEvent onCompleted;
public CallbackOptions()
{
this.onCompleted = null;
}
public void CallOnFracture(Collider instigator, GameObject fracturedObject, Vector3 point)
{
onFracture?.Invoke(instigator, fracturedObject, point);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 348edbe3d0570a6419accc52b0e212bb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,50 @@
using System;
using UnityEngine;
[Serializable]
/// <summary>
/// Options for fracturing a mesh
/// </summary>
public class FractureOptions
{
[Range(1, 1024)]
[Tooltip("Maximum number of times an object and its children are recursively fractured. Larger fragment counts will result in longer computation times.")]
public int fragmentCount;
[Tooltip("Enables fracturing in the local X plane")]
public bool xAxis;
[Tooltip("Enables fracturing in the local Y plane")]
public bool yAxis;
[Tooltip("Enables fracturing in the local Z plane")]
public bool zAxis;
[Tooltip("Enables detection of \"floating\" fragments when fracturing non-convex meshes. This setting has no effect for convex meshes and should be disabled.")]
public bool detectFloatingFragments;
[Tooltip("Fracturing is performed asynchronously on the main thread.")]
public bool asynchronous;
[Tooltip("The material to use for the inside faces")]
public Material insideMaterial;
[Tooltip("Scale factor to apply to texture coordinates")]
public Vector2 textureScale;
[Tooltip("Offset to apply to texture coordinates")]
public Vector2 textureOffset;
public FractureOptions()
{
this.fragmentCount = 10;
this.xAxis = true;
this.yAxis = true;
this.zAxis = true;
this.detectFloatingFragments = false;
this.asynchronous = false;
this.insideMaterial = null;
this.textureScale = Vector2.one;
this.textureOffset = Vector2.zero;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1a403b44a0def994f9876eb8ee5ec510
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,25 @@
using System;
using UnityEngine;
[Serializable]
/// <summary>
/// Options for prefracturing a mesh
/// </summary>
public class PrefractureOptions
{
[Tooltip("For prefractured objects, if this property is enabled, the all fragments will unfreeze if a single fragment is interacted with.")]
public bool unfreezeAll;
[Tooltip("Saves the fragment meshes to disk. Required if the fragments will be used in a prefab.")]
public bool saveFragmentsToDisk;
[Tooltip("Path to save the fragments to if saveToDisk is enabled. Relative to the project directory.")]
public string saveLocation;
public PrefractureOptions()
{
this.unfreezeAll = true;
this.saveFragmentsToDisk = false;
this.saveLocation = "";
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1288923c231259a499e20568accf3230
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using UnityEngine;
[Serializable]
/// <summary>
/// Options for refracturing
/// </summary>
public class RefractureOptions
{
[Tooltip("Enables refracturing of fragments. WARNING: This setting can result in a significant amount of generated fragments. It is recommended to keep FragmentCount low if this is enabled.")]
public bool enableRefracturing;
[Tooltip("Maximum number of times a fragment can be re-fractured.")]
[Range(1, 3)]
public int maxRefractureCount;
[Tooltip("Enable if refracturing should also invoke the callback functions.")]
public bool invokeCallbacks;
public RefractureOptions()
{
this.enableRefracturing = false;
this.maxRefractureCount = 1;
this.invokeCallbacks = false;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 16f54522207146b4080c2b883054c223
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using UnityEngine;
[Serializable]
public class SliceOptions
{
[Tooltip("Enables reslicing of fragments.")]
public bool enableReslicing;
[Tooltip("Maximum number of times a fragment can be re-sliced.")]
[Range(1, 100)]
public int maxResliceCount;
[Tooltip("Enables detection of \"floating\" fragments when slicing non-convex meshes. This setting has no effect for convex meshes and should be disabled.")]
public bool detectFloatingFragments;
[Tooltip("The material to use for the inside faces")]
public Material insideMaterial;
[Tooltip("Scale factor to apply to texture coordinates")]
public Vector2 textureScale;
[Tooltip("Offset to apply to texture coordinates")]
public Vector2 textureOffset;
[Tooltip("Enable if re-slicing should also invoke the callback functions.")]
public bool invokeCallbacks;
public SliceOptions()
{
this.enableReslicing = false;
this.maxResliceCount = 1;
this.insideMaterial = null;
this.textureScale = Vector2.one;
this.textureOffset = Vector2.zero;
this.invokeCallbacks = false;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b00168a0f92544f468d07cb7c6ae6491
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using UnityEngine;
public enum TriggerType
{
Collision,
Trigger,
Keyboard
}
[Serializable]
public class TriggerOptions
{
[Tooltip("The type of input that triggers the fracture.")]
public TriggerType triggerType;
[Tooltip("Minimum contact collision force required to cause the object to fracture.")]
public float minimumCollisionForce;
[Tooltip("If true, only objects with the tags 'Allowed Tags' list will trigger a collision.")]
public bool filterCollisionsByTag;
[Tooltip("If 'Filter Collisions By Tag' is set to true, only objects with the tags in this list will trigger the fracture.")]
public List<string> triggerAllowedTags;
[Tooltip("If the trigger type is Keyboard, this is the key code that will trigger a fracture when pressed.")]
public KeyCode triggerKey;
public TriggerOptions()
{
this.triggerType = TriggerType.Collision;
this.minimumCollisionForce = 0f;
this.filterCollisionsByTag = false;
this.triggerAllowedTags = new List<string>();
this.triggerKey = KeyCode.None;
}
/// <summary>
/// Returns true if the specified tag is allowed to trigger the fracture
/// </summary>
/// <param name="tag">The tag to check</param>
/// <returns></returns>
public bool IsTagAllowed(string tag)
{
return triggerAllowedTags.Contains(tag);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 23860b87355c53642bbc74c4577baacf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,130 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(Rigidbody))]
public class Prefracture : MonoBehaviour
{
public TriggerOptions triggerOptions;
public FractureOptions fractureOptions;
public CallbackOptions callbackOptions;
public PrefractureOptions prefractureOptions;
/// <summary>
/// Collector object that stores the produced fragments
/// </summary>
private GameObject fragmentRoot;
void OnValidate()
{
if (this.transform.parent != null)
{
// When an object is fractured, the fragments are created as children of that object's parent.
// Because of this, they inherit the parent transform. If the parent transform is not scaled
// the same in all axes, the fragments will not be rendered correctly.
var scale = this.transform.parent.localScale;
if ((scale.x != scale.y) || (scale.x != scale.z) || (scale.y != scale.z))
{
Debug.LogWarning($"Warning: Parent transform of fractured object must be uniformly scaled in all axes or fragments will not render correctly.", this.transform);
}
}
}
/// <summary>
/// Compute the fracture and create the fragments
/// </summary>
/// <returns></returns>
[ExecuteInEditMode]
[ContextMenu("Prefracture")]
public void ComputeFracture()
{
// This method should only be called from the editor during design time
if (!Application.isEditor || Application.isPlaying) return;
var mesh = this.GetComponent<MeshFilter>().sharedMesh;
if (mesh != null)
{
// If the fragment root object has not yet been created, create it now
if (this.fragmentRoot == null)
{
// Create a game object to contain the fragments
this.fragmentRoot = new GameObject($"{this.name}Fragments");
this.fragmentRoot.transform.SetParent(this.transform.parent);
// Each fragment will handle its own scale
this.fragmentRoot.transform.position = this.transform.position;
this.fragmentRoot.transform.rotation = this.transform.rotation;
this.fragmentRoot.transform.localScale = Vector3.one;
}
var fragmentTemplate = CreateFragmentTemplate();
Fragmenter.Fracture(this.gameObject,
this.fractureOptions,
fragmentTemplate,
this.fragmentRoot.transform,
prefractureOptions.saveFragmentsToDisk,
prefractureOptions.saveLocation);
// Done with template, destroy it. Since we're in editor, use DestroyImmediate
GameObject.DestroyImmediate(fragmentTemplate);
// Deactivate the original object
this.gameObject.SetActive(false);
// Fire the completion callback
if (callbackOptions.onCompleted != null)
{
callbackOptions.onCompleted.Invoke();
}
}
}
/// <summary>
/// Creates a template object which each fragment will derive from
/// </summary>
/// <returns></returns>
private GameObject CreateFragmentTemplate()
{
// If pre-fracturing, make the fragments children of this object so they can easily be unfrozen later.
// Otherwise, parent to this object's parent
GameObject obj = new GameObject();
obj.name = "Fragment";
obj.tag = this.tag;
// Update mesh to the new sliced mesh
obj.AddComponent<MeshFilter>();
// Add renderer. Default material goes in slot 1, cut material in slot 2
var meshRenderer = obj.AddComponent<MeshRenderer>();
meshRenderer.sharedMaterials = new Material[2] {
this.GetComponent<MeshRenderer>().sharedMaterial,
this.fractureOptions.insideMaterial
};
// Copy collider properties to fragment
var thisCollider = this.GetComponent<Collider>();
var fragmentCollider = obj.AddComponent<MeshCollider>();
fragmentCollider.convex = true;
fragmentCollider.sharedMaterial = thisCollider.sharedMaterial;
fragmentCollider.isTrigger = thisCollider.isTrigger;
// Copy rigid body properties to fragment
var rigidBody = obj.AddComponent<Rigidbody>();
// When pre-fracturing, freeze the rigid body so the fragments don't all crash to the ground when the scene starts.
rigidBody.constraints = RigidbodyConstraints.FreezeAll;
rigidBody.linearDamping = this.GetComponent<Rigidbody>().linearDamping;
rigidBody.angularDamping = this.GetComponent<Rigidbody>().angularDamping;
rigidBody.useGravity = this.GetComponent<Rigidbody>().useGravity;
var unfreeze = obj.AddComponent<UnfreezeFragment>();
unfreeze.unfreezeAll = prefractureOptions.unfreezeAll;
unfreeze.triggerOptions = this.triggerOptions;
unfreeze.onFractureCompleted = callbackOptions.onCompleted;
return obj;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 605444303d16d4544bb76342ce272af3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,132 @@
using UnityEngine;
using UnityEngine.Events;
[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(Rigidbody))]
public class Slice : MonoBehaviour
{
public SliceOptions sliceOptions;
public CallbackOptions callbackOptions;
/// <summary>
/// The number of times this fragment has been re-sliced.
/// </summary>
private int currentSliceCount;
/// <summary>
/// Collector object that stores the produced fragments
/// </summary>
private GameObject fragmentRoot;
/// <summary>
/// Slices the attached mesh along the cut plane
/// </summary>
/// <param name="sliceNormalWorld">The cut plane normal vector in world coordinates.</param>
/// <param name="sliceOriginWorld">The cut plane origin in world coordinates.</param>
public void ComputeSlice(Vector3 sliceNormalWorld, Vector3 sliceOriginWorld)
{
var mesh = this.GetComponent<MeshFilter>().sharedMesh;
if (mesh != null)
{
// If the fragment root object has not yet been created, create it now
if (this.fragmentRoot == null)
{
// Create a game object to contain the fragments
this.fragmentRoot = new GameObject($"{this.name}Slices");
this.fragmentRoot.transform.SetParent(this.transform.parent);
// Each fragment will handle its own scale
this.fragmentRoot.transform.position = this.transform.position;
this.fragmentRoot.transform.rotation = this.transform.rotation;
this.fragmentRoot.transform.localScale = Vector3.one;
}
var sliceTemplate = CreateSliceTemplate();
var sliceNormalLocal = this.transform.InverseTransformDirection(sliceNormalWorld);
var sliceOriginLocal = this.transform.InverseTransformPoint(sliceOriginWorld);
Fragmenter.Slice(this.gameObject,
sliceNormalLocal,
sliceOriginLocal,
this.sliceOptions,
sliceTemplate,
this.fragmentRoot.transform);
// Done with template, destroy it
GameObject.Destroy(sliceTemplate);
// Deactivate the original object
this.gameObject.SetActive(false);
// Fire the completion callback
if (callbackOptions.onCompleted != null)
{
callbackOptions.onCompleted.Invoke();
}
}
}
/// <summary>
/// Creates a template object which each fragment will derive from
/// </summary>
/// <returns></returns>
private GameObject CreateSliceTemplate()
{
// If pre-fracturing, make the fragments children of this object so they can easily be unfrozen later.
// Otherwise, parent to this object's parent
GameObject obj = new GameObject();
obj.name = "Slice";
obj.tag = this.tag;
// Update mesh to the new sliced mesh
obj.AddComponent<MeshFilter>();
// Add materials. Normal material goes in slot 1, cut material in slot 2
var meshRenderer = obj.AddComponent<MeshRenderer>();
meshRenderer.sharedMaterials = new Material[2] {
this.GetComponent<MeshRenderer>().sharedMaterial,
this.sliceOptions.insideMaterial
};
// Copy collider properties to fragment
var thisCollider = this.GetComponent<Collider>();
var fragmentCollider = obj.AddComponent<MeshCollider>();
fragmentCollider.convex = true;
fragmentCollider.sharedMaterial = thisCollider.sharedMaterial;
fragmentCollider.isTrigger = thisCollider.isTrigger;
// Copy rigid body properties to fragment
var thisRigidBody = this.GetComponent<Rigidbody>();
var fragmentRigidBody = obj.AddComponent<Rigidbody>();
fragmentRigidBody.linearVelocity = thisRigidBody.linearVelocity;
fragmentRigidBody.angularVelocity = thisRigidBody.angularVelocity;
fragmentRigidBody.linearDamping = thisRigidBody.linearDamping;
fragmentRigidBody.angularDamping = thisRigidBody.angularDamping;
fragmentRigidBody.useGravity = thisRigidBody.useGravity;
// If refracturing is enabled, create a copy of this component and add it to the template fragment object
if (this.sliceOptions.enableReslicing &&
(this.currentSliceCount < this.sliceOptions.maxResliceCount))
{
CopySliceComponent(obj);
}
return obj;
}
/// <summary>
/// Convenience method for copying this component to another component
/// </summary>
/// <param name="obj">The GameObject to copy this component to</param>
private void CopySliceComponent(GameObject obj)
{
var sliceComponent = obj.AddComponent<Slice>();
sliceComponent.sliceOptions = this.sliceOptions;
sliceComponent.callbackOptions = this.callbackOptions;
sliceComponent.currentSliceCount = this.currentSliceCount + 1;
sliceComponent.fragmentRoot = this.fragmentRoot;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fc1ac885d6b125f44935bf90b4e492a8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c3e85072fad0cb14eb428bfc7fa67615
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,66 @@
using UnityEngine;
using UnityEngine.TestTools;
[ExcludeFromCoverage]
public class PlaneSlicer : MonoBehaviour
{
public float RotationSensitivity = 1f;
public void OnTriggerStay(Collider collider)
{
var material = collider.gameObject.GetComponent<MeshRenderer>().material;
if (material.name.StartsWith("HighlightSlice"))
{
material.SetVector("CutPlaneNormal", this.transform.up);
material.SetVector("CutPlaneOrigin", this.transform.position);
}
}
public void OnTriggerExit(Collider collider)
{
var material = collider.gameObject.GetComponent<MeshRenderer>().material;
if (material.name.StartsWith("HighlightSlice"))
{
material.SetVector("CutPlaneOrigin", Vector3.positiveInfinity);
}
}
// Update is called once per frame
void Update()
{
if (Input.GetKey(KeyCode.Q))
{
this.transform.Rotate(Vector3.forward, RotationSensitivity, Space.Self);
}
if (Input.GetKey(KeyCode.E))
{
this.transform.Rotate(Vector3.forward, -RotationSensitivity, Space.Self);
}
if (Input.GetKeyDown(KeyCode.LeftShift))
{
var mesh = this.GetComponent<MeshFilter>().sharedMesh;
var center = mesh.bounds.center;
var extents = mesh.bounds.extents;
extents = new Vector3(extents.x * this.transform.localScale.x,
extents.y * this.transform.localScale.y,
extents.z * this.transform.localScale.z);
// Cast a ray and find the nearest object
RaycastHit[] hits = Physics.BoxCastAll(this.transform.position, extents, this.transform.forward, this.transform.rotation, extents.z);
foreach(RaycastHit hit in hits)
{
var obj = hit.collider.gameObject;
var sliceObj = obj.GetComponent<Slice>();
if (sliceObj != null)
{
sliceObj.GetComponent<MeshRenderer>()?.material.SetVector("CutPlaneOrigin", Vector3.positiveInfinity);
sliceObj.ComputeSlice(this.transform.up, this.transform.position);
}
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d327cd6cfb3516d49ad96107a8b6d8ac
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5515d45c7935d204bbce3e79310fdba2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,76 @@
using UnityEngine;
using UnityEngine.TestTools;
[ExcludeFromCoverage]
public class CameraController : MonoBehaviour
{
[Tooltip("Acceleration of the player")]
public float acceleration = 100.0f;
[Tooltip("Maximum speed of the player while walking")]
public float maxSpeed = 5.0f;
[Tooltip("Sensitivity of the mouse for pan / tilt.")]
public float mouseSensitivity = 5.0f;
private float startTime = 0f;
private float elapsedTime = 0f;
void Start()
{
startTime = Time.time;
}
void Update()
{
float dx = Input.GetAxis("Mouse X") * mouseSensitivity;
float dy = Input.GetAxis("Mouse Y") * mouseSensitivity;
if (elapsedTime > 0.5f)
{
this.transform.parent.Rotate(Vector3.up, dx);
// Clamp pitch to [-80, 80] degrees
var currentPitch = this.transform.eulerAngles.x;
if (currentPitch > 180f) currentPitch -= 360f;
var newPitch = Mathf.Clamp(currentPitch - dy, -80f, 80f);
this.transform.localEulerAngles = new Vector3(newPitch, 0, 0);
}
else
{
elapsedTime = Time.time - startTime;
}
}
// Update is called once per frame
void FixedUpdate()
{
// Check for player movement. We can handle input here because it is continuous and
// not instantaneous like jumping.
var rigidbody = this.transform.parent.GetComponent<Rigidbody>();
if (Input.GetKey(KeyCode.W))
{
rigidbody.AddRelativeForce(Vector3.forward * acceleration, ForceMode.Acceleration);
}
if (Input.GetKey(KeyCode.A))
{
rigidbody.AddRelativeForce(Vector3.left * acceleration, ForceMode.Acceleration);
}
if (Input.GetKey(KeyCode.S))
{
rigidbody.AddRelativeForce(Vector3.back * acceleration, ForceMode.Acceleration);
}
if (Input.GetKey(KeyCode.D))
{
rigidbody.AddRelativeForce(Vector3.right * acceleration, ForceMode.Acceleration);
}
// Clamp the player's velocity in the X and Z directions
Vector2 xzVelocity = new Vector2(rigidbody.linearVelocity.x, rigidbody.linearVelocity.z);
if (xzVelocity.magnitude > maxSpeed)
{
var xzClampedVelocity = maxSpeed * xzVelocity.normalized;
rigidbody.linearVelocity = new Vector3(xzClampedVelocity.x, rigidbody.linearVelocity.y, xzClampedVelocity.y);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dca2007b8cabd9747a798832c390aaba
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,26 @@
using UnityEngine;
using UnityEngine.TestTools;
[ExcludeFromCoverage]
public class Projectile : MonoBehaviour
{
public GameObject projectile;
public float initialVelocity;
public KeyCode FireKey;
// Update is called once per frame
void Update()
{
if (Input.GetKeyDown(FireKey))
{
// Remove other projectiles from the scene
foreach(GameObject obj in GameObject.FindGameObjectsWithTag("Projectile"))
{
GameObject.Destroy(obj);
}
var projectileInstance = GameObject.Instantiate(projectile, this.transform.position, Quaternion.identity);
projectileInstance.GetComponent<Rigidbody>().linearVelocity = initialVelocity * this.transform.forward;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bad14770ea7af9e4baaa4dc184db69a3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,17 @@
using UnityEngine;
public class ToggleText : MonoBehaviour
{
public KeyCode toggleKey;
public GameObject textObject;
// Start is called before the first frame update
void Update()
{
if (Input.GetKeyDown(toggleKey))
{
textObject.SetActive(!textObject.activeSelf);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0f4acb25588adc34a9d8453173077e89
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,13 @@
using UnityEngine;
public class UniqueMaterial : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
// Creates a unique instance of the material, decoupling it from the other objects.
// This script is only used for the Slice demo to highlight slices and is not essential
// for the fracturing/slicing code to work.
this.GetComponent<MeshRenderer>().material = this.GetComponent<MeshRenderer>().material;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f4fe57792e36ffc489b628556d1dc576
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 565bd121a6124b54f97b6266e15ab13a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,96 @@
/// <summary>
/// Defines an interface for an object that is sorted by bin number
/// </summary>
public interface IBinSortable
{
int bin { get; set; }
}
/// <summary>
/// Methods for sorting objects on an ordered grid by bin number.
///
/// The grid ordering is shown by example below. Even rows (row 0 = bottom row) are ordered
/// right-to-left while odd rows are ordered left-to-right.
/// _____ _____ _____
/// | | | |
/// | 6 | 7 | 8 |
/// |_____|_____|_____|
/// | | | |
/// | 5 | 4 | 3 |
/// |_____|_____|_____|
/// | | | |
/// | 0 | 1 | 2 |
/// |_____|_____|_____|
///
/// </summary>
public class BinSort
{
/// <summary>
/// Computes the bin number for the set of grid coordinates
/// </summary>
/// <param name="i">Grid row</param>
/// <param name="j">Grid column</param>
/// <param name="n">Grid size</param>
/// <returns></returns>
internal static int GetBinNumber(int i, int j, int n)
{
return (i % 2 == 0) ? (i * n) + j : (i + 1) * n - j - 1;
}
/// <summary>
/// Performs a counting sort of the input points based on their bin number. Only
/// sorts the elements in the index range [0, count]. If binCount is <= 1, no sorting
/// is performed. If lastIndex > input.Length, the entire input array is sorted.
/// </summary>
/// <param name="input">The input array to sort</param>
/// <param name="lastIndex">The index of the last element in `input` to sort. Only the
/// elements [0, lastIndex) are sorted.</param>
/// <param name="binCount">Number of bins</param>
internal static T[] Sort<T>(T[] input, int lastIndex, int binCount) where T: IBinSortable
{
int[] count = new int[binCount];
T[] output = new T[input.Length];
#region Validation
// Need at least two bins to sort
if (binCount <= 1)
{
return input;
}
// If lastIndex is out of range, default to sorting the entire input array
if (lastIndex > input.Length)
{
lastIndex = input.Length;
}
#endregion
// Only sort the first [0, count] points, don't want to sort super-triangle vertices
for (int i = 0; i < lastIndex; i++)
{
int j = input[i].bin;
count[j] += 1;
}
for (int i = 1; i < binCount; i++)
{
count[i] += count[i - 1];
}
for (int i = lastIndex - 1; i >= 0; i--)
{
int j = input[i].bin;
count[j] -= 1;
output[count[j]] = input[i];
}
// Copy over the rest of the un-sorted points
for (int i = lastIndex; i < output.Length; i++)
{
output[i] = input[i];
}
return output;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fa6520366fb0c0d418e02cb5b731ad3a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,128 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public static class MathUtils
{
/// <summary>
/// Returns true if the quad specified by the two diagonals a1->a2 and b1->b2 is convex
/// Quad is convex if a1->a2 and b1->b2 intersect each other
/// </summary>
/// <param name="a1">Start point of diagonal A</param>
/// <param name="a2">End point of diagonal A</param>
/// <param name="b1">Start point of diagonal B</param>
/// <param name="b2">End point of diagonal B</param>
/// <returns></returns>
public static bool IsQuadConvex(Vector2 a1, Vector2 a2, Vector2 b1, Vector2 b2)
{
return LinesIntersectInternal(a1, a2, b1, b2, true);
}
/// <summary>
/// Returns true lines a1->a2 and b1->b2 is intersect
/// </summary>
/// <param name="a1">Start point of line A</param>
/// <param name="a2">End point of line A</param>
/// <param name="b1">Start point of line B</param>
/// <param name="b2">End point of line B</param>
/// <returns></returns>
public static bool LinesIntersect(Vector2 a1, Vector2 a2, Vector2 b1, Vector2 b2)
{
return LinesIntersectInternal(a1, a2, b1, b2, false);
}
/// <summary>
/// Returns true lines a1->a2 and b1->b2 is intersect
/// </summary>
/// <param name="a1">Start point of line A</param>
/// <param name="a2">End point of line A</param>
/// <param name="b1">Start point of line B</param>
/// <param name="b2">End point of line B</param>
/// <returns></returns>
private static bool LinesIntersectInternal(Vector2 a1, Vector2 a2, Vector2 b1, Vector2 b2, bool includeSharedEndpoints)
{
Vector2 a12 = new Vector2(a2.x - a1.x, a2.y - a1.y);
Vector2 b12 = new Vector2(b2.x - b1.x, b2.y - b1.y);
// If any of the vertices are shared between the two diagonals,
// the quad collapses into a triangle and is convex by default.
if (a1 == b1 || a1 == b2 || a2 == b1 || a2 == b2)
{
return includeSharedEndpoints;
}
else
{
// Compute cross product between each point and the opposite diagonal
// Look at sign of the Z component to see which side of line point is on
float a1xb = (a1.x - b1.x) * b12.y - (a1.y - b1.y) * b12.x;
float a2xb = (a2.x - b1.x) * b12.y - (a2.y - b1.y) * b12.x;
float b1xa = (b1.x - a1.x) * a12.y - (b1.y - a1.y) * a12.x;
float b2xa = (b2.x - a1.x) * a12.y - (b2.y - a1.y) * a12.x;
// Check that the points for each diagonal lie on opposite sides of the other
// diagonal. Quad is also convex if a1/a2 lie on b1->b2 (and vice versa) since
// the shape collapses into a triangle (hence >= instead of >)
return ((a1xb >= 0 && a2xb <= 0) || (a1xb <= 0 && a2xb >= 0)) &&
((b1xa >= 0 && b2xa <= 0) || (b1xa <= 0 && b2xa >= 0));
}
}
/// <summary>
/// Determines the intersection between the line segment a->b and the plane defined by the specified normal and origin point. If an intersection point exists, it is returned via the out parameter `intersection`. The parameter `s` is defined below and is used to properly interpolate normals/uvs for intersection vertices.
/// </summary>
/// <param name="a">Start point of line</param>
/// <param name="b">End point of line</param>
/// <param name="n">Plane normal</param>
/// <param name="p0">Plane origin</param>
/// <param name="x">If intersection exists, intersection point return as out parameter.</param>
/// <param name="s">Returns the parameterization of the intersection where x = a + (b - a) * s</param>
/// <returns></returns>
public static bool LinePlaneIntersection(Vector3 a,
Vector3 b,
Vector3 n,
Vector3 p0,
out Vector3 x,
out float s)
{
// Initialize out params
s = 0;
x = Vector3.zero;
// Handle degenerate cases
if (a == b)
{
return false;
}
else if (n == Vector3.zero)
{
return false;
}
// `s` is the parameter for the line segment a -> b where 0.0 <= s <= 1.0
s = Vector3.Dot(p0 - a, n) / Vector3.Dot(b - a, n);
if (s >= 0 && s <= 1)
{
x = a + (b - a) * s;
return true;
}
return false;
}
/// <summary>
/// Returns true of the point `p` is on the left side of the directed line segment `i` -> `j`
/// Use for checking if a point is inside of a triangle. Since triangle vertices oriented
/// CCW, a point on the left side of a triangle edge is "inside" that edge of the triangle.
/// </summary>
/// <param name="p">Index of test point in `points` array</param>
/// <param name="i">Index of first vertex of the edge in the `points` array</param>
/// /// <param name="j">Index of second vertex of the edge in the `points` array</param>
/// <returns>True if the point `p` is on the left side of the line `i`->`j`</returns>
public static bool IsPointOnRightSideOfLine(Vector2 a, Vector2 b, Vector2 c)
{
// The <= is essential; if it is <, the whole thing falls apart
return ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)) <= 0;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2d37a4c4b4b021b4dafc4db72c4a6fc2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,239 @@
using System.Collections.Generic;
using UnityEngine;
using Unity.Collections;
using UnityEngine.Rendering;
public static class MeshUtils
{
// Description of vertex attributes for the island mesh
private static VertexAttributeDescriptor[] layout = new[]
{
new VertexAttributeDescriptor(VertexAttribute.Position, VertexAttributeFormat.Float32, 3),
new VertexAttributeDescriptor(VertexAttribute.Normal, VertexAttributeFormat.Float32, 3),
new VertexAttributeDescriptor(VertexAttribute.TexCoord0, VertexAttributeFormat.Float32, 2),
};
/// <summary>
/// Identifies all disconnected sets of geometry contained within the mesh.
/// Each set of geometry is split into a separate meshes.
/// </summary>
/// <param name="mesh">The mesh to search</param>
/// <returns>Returns an array of all disconnected meshes found.</returns>
public static Mesh[] FindDisconnectedMeshes(Mesh mesh)
{
// Each disconnected set of geometry is referred to as an "island"
List<Mesh> islands = new List<Mesh>();
#region Preliminaries
// Extract mesh data
var vertices = mesh.vertices;
var triangles = mesh.triangles;
var normals = mesh.normals;
var uvs = mesh.uv;
// For each triangle, find the corresponding sub-mesh index. (Mesh.triangles contains
// the triangles for all sub-meshes)
int[] triangleSubMesh = new int[triangles.Length / 3];
int subMeshIndex = 0;
int subMeshSize = mesh.GetTriangles(subMeshIndex).Length / 3;
for (int i = 0; i < triangles.Length / 3; i++)
{
if (i >= subMeshSize)
{
subMeshIndex++;
subMeshSize += mesh.GetTriangles(subMeshIndex).Length / 3;
}
triangleSubMesh[i] = subMeshIndex;
}
// Identify coincident vertices
List<int>[] coincidentVertices = new List<int>[vertices.Length];
for(int i = 0; i < vertices.Length; i++)
{
coincidentVertices[i] = new List<int>();
}
for(int i = 0; i < vertices.Length; i++)
{
Vector3 v_i = vertices[i];
for (int k = i + 1; k < vertices.Length; k++)
{
Vector3 v_k = vertices[k];
if (v_i == v_k)
{
coincidentVertices[k].Add(i);
coincidentVertices[i].Add(k);
}
}
}
// Find the triangles the each vertex belongs to. Need to do this for each submesh
List<int>[] vertexTriangles = new List<int>[vertices.Length];
for (int i = 0; i < vertices.Length; i++)
{
vertexTriangles[i] = new List<int>();
}
int v1, v2, v3;
for (int i = 0; i < triangles.Length; i += 3)
{
// Index of the triangle
int t = i / 3;
v1 = triangles[i];
v2 = triangles[i + 1];
v3 = triangles[i + 2];
vertexTriangles[v1].Add(t);
vertexTriangles[v2].Add(t);
vertexTriangles[v3].Add(t);
}
#endregion
// Search the mesh geometry and identify all islands
// 1) Start by finding a vertex that has not yet been visited
// 2) Insert the vertex into a queue, begin a breadth-first search
// 3) Dequeue the next vertex 'v'
// 4) Find all triangles that 'v' is connected to. Add each triangle to a list
// 5) Enqueue the vertices for each connected triangle if they haven't been visited yet
// 6) Enqueue all vertices coincident with 'v' if they haven't been visited yet
// 7) Repeat Steps 3-6 until the queue is empty
// 8) Take the list of triangles and use the existing mesh data to create a new island mesh
// 9) Go back to Step 1, continue until all vertices have been visited.
bool[] visitedVertices = new bool[vertices.Length];
bool[] visitedTriangles = new bool[triangles.Length];
Queue<int> frontier = new Queue<int>();
// Vertex data for the island mesh. Only initialize once and keep track of pointer to last element to minimize GC
NativeArray<MeshVertex> islandVertices = new NativeArray<MeshVertex>(vertices.Length, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
// Array containing triangle data for the island mesh. Need to keep track of triangles for each sub-mesh separately
int[][] islandTriangles = new int[mesh.subMeshCount][];
for (int i = 0; i < mesh.subMeshCount; i++)
{
islandTriangles[i] = new int[triangles.Length];
}
// Counters to keep track of how many vertices
int vertexCount = 0;
int totalIndexCount = 0;
int[] subMeshIndexCounts = new int[mesh.subMeshCount];
for (int i = 0; i < vertices.Length; i++)
{
if (visitedVertices[i]) continue;
// Reset the vertex/triangle counts
vertexCount = 0;
totalIndexCount = 0;
for(int j = 0; j < mesh.subMeshCount; j++)
{
subMeshIndexCounts[j] = 0;
}
// Search the mesh geometry starting at vertex 'i'. Search is performed by looking up
// the triangles that contain each vertex, adding their vertices, etc. until all
// triangles have been visited.
frontier.Enqueue(i);
// Index map between source mesh vertex array and the sub mesh vertex arrays
int[] vertexMap = new int[vertices.Length];
// Initialize map to '-1' to serve as "unmapped" value
for(int j = 0; j < vertices.Length; j++)
{
vertexMap[j] = -1;
}
while (frontier.Count > 0)
{
int k = frontier.Dequeue();
// Ignore vertex if we've already visited it
if (visitedVertices[k])
{
continue;
}
else
{
visitedVertices[k] = true;
}
// Add this vertex array for the island mesh
// Map between the original vertex index to the vertex's new index in the island
// mesh vertex array. This will be used to update the indices for the triangles later
vertexMap[k] = vertexCount;
islandVertices[vertexCount++] = new MeshVertex(vertices[k], normals[k], uvs[k]);
// Get the list of all triangles that this vertex is a part of
foreach(int t in vertexTriangles[k])
{
// If triangle is already included, skip it
if (!visitedTriangles[t])
{
visitedTriangles[t] = true;
// Loop through each vertex of the triangle and add the non-visited ones
// to the search frontier
for (int m = t * 3; m < t * 3 + 3; m++)
{
int v = triangles[m];
subMeshIndex = triangleSubMesh[t];
islandTriangles[subMeshIndex][subMeshIndexCounts[subMeshIndex]++] = v;
totalIndexCount++;
frontier.Enqueue(v);
// If this vertex is coincident with other vertices, add those to the search frontier
foreach(int cv in coincidentVertices[v])
{
frontier.Enqueue(cv);
}
}
}
}
}
// If the island contains at least one triangle, create a new mesh
if (vertexCount > 0)
{
Mesh island = new Mesh();
island.SetIndexBufferParams(totalIndexCount, IndexFormat.UInt32);
island.SetVertexBufferParams(vertexCount, layout);
island.SetVertexBufferData(islandVertices, 0, 0, vertexCount);
// Set the triangles for each submesh
island.subMeshCount = mesh.subMeshCount;
int indexStart = 0;
for (subMeshIndex = 0; subMeshIndex < mesh.subMeshCount; subMeshIndex++)
{
var subMeshIndexBuffer = islandTriangles[subMeshIndex];
var subMeshIndexCount = subMeshIndexCounts[subMeshIndex];
// Map vertex indexes from the original mesh to the island mesh
for(int k = 0; k < subMeshIndexCount; k++)
{
int originalIndex = subMeshIndexBuffer[k];
subMeshIndexBuffer[k] = vertexMap[originalIndex];
}
// Set the index data for this sub mesh
island.SetIndexBufferData(subMeshIndexBuffer, 0, indexStart, (int)subMeshIndexCount);
island.SetSubMesh(subMeshIndex, new SubMeshDescriptor(indexStart, subMeshIndexCount));
indexStart += subMeshIndexCount;
}
island.RecalculateBounds();
islands.Add(island);
}
}
// Loop through rest of triangles
return islands.ToArray();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fe28a9177c0175f479eb9155cc4aa3a4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,22 @@
using System;
using UnityEngine;
public static class Vector3Extensions
{
//
// that the normal is pointing to
// - p: The point being checked
// - n: The normal of the plane
// - o: The origin of the plane
/// <summary>
/// Returns true if the point is either on or above the plane. "Above" is the side of the place in the direction of the normal.
/// </summary>
/// <param name="p">The test point</param>
/// <param name="n">The plane normal</param>
/// <param name="o">The plane origin</param>
/// <returns></returns>
public static bool IsAbovePlane(this Vector3 p, Vector3 n, Vector3 o)
{
return (n.x * (p.x - o.x) + n.y * (p.y - o.y) + n.z * (p.z - o.z)) >= 0;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d0cb4a40ec03ec24485fca5a7cd3ad16
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: