ui基本完毕,修了一大把的bug
This commit is contained in:
@@ -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:
|
||||
+764
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -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}";
|
||||
}
|
||||
}
|
||||
+11
@@ -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;
|
||||
}
|
||||
}
|
||||
+11
@@ -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 = "";
|
||||
}
|
||||
}
|
||||
+11
@@ -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;
|
||||
}
|
||||
}
|
||||
+11
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -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;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d0cb4a40ec03ec24485fca5a7cd3ad16
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user