using System; using System.Collections.Generic; using Unity.VisualScripting; using UnityEngine; public class Slice : MonoBehaviour { /// /// Helper class to combine lists of vertices, UVs, normals, and triangles (indexes). Represents a mesh being built up. /// /// /// You use it by adding data to the different lists then calling MakeMesh /// class InProgressMesh { public List vertices = new(); public List triangles = new(); public List uvs = new(); public List normals = new(); /// /// Create the Unity mesh from the collected data /// /// Fully constructed Unity mesh public Mesh MakeMesh() { var mesh = new Mesh(); mesh.SetVertices(vertices); mesh.SetTriangles(triangles, 0); mesh.SetUVs(0, uvs); mesh.SetNormals(normals); return mesh; } } /// /// A point on the slicing plane /// public Vector3 planePosition = Vector3.zero; /// /// The normal of the slicing plane /// public Vector3 planeNormal = Vector3.up; /// /// The slicing plane /// private Plane _plane; /// /// Visualize the slicing plane /// private void OnDrawGizmos() { // convert the plane position and normal to world space using localToWorldMatrix Vector3 p = transform.localToWorldMatrix * planePosition; Vector3 n = transform.localToWorldMatrix * planeNormal.normalized; // draw the normal in green Gizmos.color = Color.green; Gizmos.DrawLine(p, p + n * 10); // calculate the tangent and bitangent using cross products Vector3 tangent = Vector3.Cross(n, Vector3.up); if (tangent == Vector3.zero) tangent = Vector3.Cross(n, Vector3.right); tangent.Normalize(); Vector3 bitangent = Vector3.Cross(n, tangent).normalized; // plot and draw an X around the point Vector3 A1 = p + tangent * 10f; Vector3 A2 = p - tangent * 10f; Vector3 B1 = p + bitangent * 10f; Vector3 B2 = p - bitangent * 10f; Gizmos.color = Color.blue; Gizmos.DrawLine(A1, A2); Gizmos.DrawLine(B1, B2); } /// /// Finds intersection between plane and line segment. Returns point and distance. /// /// /// Uses Unity's built-in Plane.Raycast method but adapts it to work with a line segment (finite length) instead of /// a Ray (infinite length) /// private static bool IntersectPlaneLine(Plane p, Vector3 p0, Vector3 p1, out float distance, out Vector3 point) { var ray = new Ray(p0, p1 - p0); if (p.Raycast(ray, out var d) && d < (p1 - p0).magnitude) { distance = d; point = ray.origin + ray.direction * distance; return true; } distance = 0; point = default; return false; } void Start() { _plane = new Plane(planeNormal, planePosition); Mesh mesh = GetComponent().mesh; Transform parent = transform; // create two "shards" that will become the two meshes we are generating. the positive shard will contain all // the triangles in front of the slicing plane, the negative shard will contain all the triangles behind it. InProgressMesh positiveShard = new(); InProgressMesh negativeShard = new(); // mesh.triangles stored the indexes of each vertex. its meant to be read three at a time. a better name would have been mesh.indexes. for (int i = 0; i < mesh.triangles.Length; i += 3) { // we get the index of each vertex in this triangle int i0 = mesh.triangles[i]; int i1 = mesh.triangles[i + 1]; int i2 = mesh.triangles[i + 2]; // use the indexes to look up their positions in mesh.vertices. a better name for mesh.vertices would have been mesh.positions. Vector3 p0 = mesh.vertices[i0]; Vector3 p1 = mesh.vertices[i1]; Vector3 p2 = mesh.vertices[i2]; if (_plane.SameSide(p0, p1) && _plane.SameSide(p1, p2)) { // if all three vertexes are on the same side, its the easy case! we just copy this triangle over to the // positive or negative share depending on what side they all are on InProgressMesh targetMesh = _plane.GetSide(p0) ? negativeShard : positiveShard; targetMesh.vertices.AddRange(new[] { p0, p1, p2 }); targetMesh.normals.AddRange(new[] { mesh.normals[i0], mesh.normals[i1], mesh.normals[i2] }); targetMesh.uvs.AddRange(new[] { mesh.uv[i0], mesh.uv[i1], mesh.uv[i2] }); // we are adding three entries to the end of the vertices, normals, and uvs range to make up our three // vertices. we add indexes to connect them, and since theyre three new vertices at the end the indexes // are just indexCount + 0, indexCount + 1, indexCount + 2 var indexCount = targetMesh.vertices.Count; targetMesh.triangles.AddRange(new[] { indexCount + 0, indexCount + 1, indexCount + 2 }); } else { /* * if all three vertices are *not* on the same side, its the more difficult case. this triangle is being * intersected by the slicing plane, and we need to compute new triangles in both shards to reflect the * intersection. fortunately there is only one possible case to consider: * * ov * / \ * / \ one side of the intersection has one vertex -- we call this the "one side" * ----*-----*----- * /iv0 \iv1 * / \ the other side of the intersection has two vertices -- we call this the "two side" * *-----------* * tv0 tv1 * * ov: the vertex on the one side * tv0: the first vertex on the two side * tv1: the second vertex on the two side * iv0: the vertex on the slicing plane between ov and tv0 * iv1: the vertex on the slicing plane between ov and tv1 * * we need to add the triangle { ov, iv0, iv1 } to one shard and the TWO triangles { tv0, tv1, ip1 } and * { tv0, ip1, ip0 } to the other shard. * * we need to figure out which shard the one side is associated with and which side the two side is * associated with -- because that will depend on how the triangle is oriented. */ // we know there's a positive side and a negative side (which we can get from the plane) and a one side // and a two side, part of our job is to figure out which is which var positiveSideIndexes = new List(); var negativeSideIndexes = new List(); List oneSideIndexes; List twoSideIndexes; InProgressMesh oneSideMesh; InProgressMesh twoSideMesh; // add indexes to positiveSideIndexes and negativeSideIndexes based on which side the plane says the // vertices are on if (_plane.GetSide(p0)) positiveSideIndexes.Add(i0); else negativeSideIndexes.Add(i0); if (_plane.GetSide(p1)) positiveSideIndexes.Add(i1); else negativeSideIndexes.Add(i1); if (_plane.GetSide(p2)) positiveSideIndexes.Add(i2); else negativeSideIndexes.Add(i2); // based on the number of vertices that got added in the above code we can determine if the positive // side / negative side is the one side / two side if (positiveSideIndexes.Count == 1 && negativeSideIndexes.Count == 2) { oneSideIndexes = positiveSideIndexes; oneSideMesh = negativeShard; twoSideIndexes = negativeSideIndexes; twoSideMesh = positiveShard; } else if (positiveSideIndexes.Count == 2 && negativeSideIndexes.Count == 1) { oneSideIndexes = negativeSideIndexes; oneSideMesh = positiveShard; twoSideIndexes = positiveSideIndexes; twoSideMesh = negativeShard; } else { // this should never happen! no way for there to be more than three vertices total or for there to // be three vertices in one list! throw new InvalidOperationException("impossible"); } // get position, UV, and normal for ov, the vertex on the one side var ov = mesh.vertices[oneSideIndexes[0]]; var ov_uv = mesh.uv[oneSideIndexes[0]]; var ov_normal = mesh.normals[oneSideIndexes[0]]; // get position, UV, and normal for tv0 and tv1, the vertices on the two side var tv0 = mesh.vertices[twoSideIndexes[0]]; var tv1 = mesh.vertices[twoSideIndexes[1]]; var tv0_uv = mesh.uv[twoSideIndexes[0]]; var tv1_uv = mesh.uv[twoSideIndexes[1]]; var tv0_normal = mesh.normals[twoSideIndexes[0]]; var tv1_normal = mesh.normals[twoSideIndexes[1]]; // intersect the plane with the triangle edges {ov, tv0} and {ov, tv1} to get d0/d1 (the distances of // the intersections from ov) and iv0/iv1 (the intersection positions) IntersectPlaneLine(_plane, ov, tv0, out var d0, out var iv0); IntersectPlaneLine(_plane, ov, tv1, out var d1, out var iv1); // we need UVs and normals for the intersections too! we get those by interpolating between ov and // tv0/tv1 based on the distance d0/d1 var iv0_uv = Vector2.Lerp(ov_uv, tv0_uv, d0 / (ov - tv0).magnitude); var iv1_uv = Vector2.Lerp(ov_uv, tv1_uv, d1 / (ov - tv1).magnitude); var iv0_normal = Vector3.Lerp(ov_normal, tv0_normal, d0 / (ov - tv0).magnitude).normalized; var iv1_normal = Vector3.Lerp(ov_normal, tv1_normal, d1 / (ov - tv1).magnitude).normalized; // add the triangle {ov, ip0, ip1} on the one side to the one side mesh int startIndex = oneSideMesh.vertices.Count; oneSideMesh.vertices.AddRange(new[] { ov, iv0, iv1 }); oneSideMesh.uvs.AddRange(new[] { ov_uv, iv0_uv, iv1_uv }); oneSideMesh.normals.AddRange(new[] { ov_normal, iv0_normal, iv1_normal }); oneSideMesh.triangles.AddRange(new[] { startIndex, startIndex + 1, startIndex + 2 }); // add the triangles {tv0, tv1, ip1} and {tv0, ip1, ip0} on the one two side to the two side mesh int twoStart = twoSideMesh.vertices.Count; twoSideMesh.vertices.AddRange(new[] { tv0, tv1, iv1, iv0 }); twoSideMesh.uvs.AddRange(new[] { tv0_uv, tv1_uv, iv1_uv, iv0_uv }); twoSideMesh.normals.AddRange(new[] { tv0_normal, tv1_normal, iv1_normal, iv0_normal }); twoSideMesh.triangles.AddRange(new[] { twoStart, twoStart + 1, twoStart + 2, // tv0, tv1, ip1 twoStart, twoStart + 2, twoStart + 3 // tv0, ip1, ip0 }); } } // create GameObjects for each shard Material material = GetComponent().material; GameObject negativeShardObject = new GameObject("Negative Shard"); negativeShardObject.transform.position = parent.position; negativeShardObject.transform.rotation = parent.rotation; negativeShardObject.transform.localScale = parent.lossyScale; negativeShardObject.AddComponent().material = material; negativeShardObject.AddComponent().mesh = negativeShard.MakeMesh(); GameObject positiveShardObject = new GameObject("Positive Shard"); positiveShardObject.transform.position = parent.position; positiveShardObject.transform.rotation = parent.rotation; positiveShardObject.transform.localScale = parent.lossyScale; positiveShardObject.AddComponent().material = material; positiveShardObject.AddComponent().mesh = positiveShard.MakeMesh(); // hide this mesh GetComponent().enabled = false; } // Update is called once per frame void Update() { } }