Last active
April 8, 2025 19:14
-
-
Save nasser/6af19c6bd286b10cc869ed9a88894ce6 to your computer and use it in GitHub Desktop.
Revisions
-
nasser revised this gist
Apr 8, 2025 . 1 changed file with 118 additions and 47 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -5,13 +5,23 @@ public class Slice : MonoBehaviour { /// <summary> /// Helper class to combine lists of vertices, UVs, normals, and triangles (indexes). Represents a mesh being built up. /// </summary> /// <remarks> /// You use it by adding data to the different lists then calling MakeMesh /// </remarks> class InProgressMesh { public List<Vector3> vertices = new(); public List<int> triangles = new(); public List<Vector2> uvs = new(); public List<Vector3> normals = new(); /// <summary> /// Create the Unity mesh from the collected data /// </summary> /// <returns>Fully constructed Unity mesh</returns> public Mesh MakeMesh() { var mesh = new Mesh(); @@ -23,30 +33,42 @@ public Mesh MakeMesh() } } /// <summary> /// A point on the slicing plane /// </summary> public Vector3 planePosition = Vector3.zero; /// <summary> /// The normal of the slicing plane /// </summary> public Vector3 planeNormal = Vector3.up; /// <summary> /// The slicing plane /// </summary> private Plane _plane; /// <summary> /// Visualize the slicing plane /// </summary> 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; @@ -56,6 +78,13 @@ private void OnDrawGizmos() Gizmos.DrawLine(B1, B2); } /// <summary> /// Finds intersection between plane and line segment. Returns point and distance. /// </summary> /// <remarks> /// 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) /// </remarks> private static bool IntersectPlaneLine(Plane p, Vector3 p0, Vector3 p1, out float distance, out Vector3 point) { var ray = new Ray(p0, p1 - p0); @@ -73,47 +102,90 @@ private static bool IntersectPlaneLine(Plane p, Vector3 p0, Vector3 p1, out floa void Start() { _plane = new Plane(planeNormal, planePosition); Mesh mesh = GetComponent<MeshFilter>().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<int>(); var negativeSideIndexes = new List<int>(); List<int> oneSideIndexes; List<int> 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; @@ -130,46 +202,48 @@ void Start() } 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 @@ -178,27 +252,24 @@ void Start() } } // create GameObjects for each shard Material material = GetComponent<MeshRenderer>().material; GameObject negativeShardObject = new GameObject("Negative Shard"); negativeShardObject.transform.position = parent.position; negativeShardObject.transform.rotation = parent.rotation; negativeShardObject.transform.localScale = parent.lossyScale; negativeShardObject.AddComponent<MeshRenderer>().material = material; negativeShardObject.AddComponent<MeshFilter>().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<MeshRenderer>().material = material; positiveShardObject.AddComponent<MeshFilter>().mesh = positiveShard.MakeMesh(); // hide this mesh GetComponent<MeshRenderer>().enabled = false; } -
nasser created this gist
Apr 7, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,209 @@ using System; using System.Collections.Generic; using Unity.VisualScripting; using UnityEngine; public class Slice : MonoBehaviour { class InProgressMesh { public List<Vector3> vertices = new(); public List<int> triangles = new(); public List<Vector2> uvs = new(); public List<Vector3> normals = new(); public Mesh MakeMesh() { var mesh = new Mesh(); mesh.SetVertices(vertices); mesh.SetTriangles(triangles, 0); mesh.SetUVs(0, uvs); mesh.SetNormals(normals); return mesh; } } private Mesh mesh; Vector3 IntersectEdge(Vector3 a, Vector3 b, float da, float db) { float t = da / (da - db); // always between 0 and 1 return a + t * (b - a); } public Vector3 planePosition = Vector3.zero; public Vector3 planeNormal = Vector3.up; private Plane plane; private void OnDrawGizmos() { Vector3 p = transform.localToWorldMatrix * planePosition; Vector3 n = transform.localToWorldMatrix * planeNormal.normalized; Gizmos.color = Color.green; Gizmos.DrawLine(p, p + n * 10); 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; 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); } 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<MeshFilter>().mesh; Material material = GetComponent<MeshRenderer>().material; Transform parent = transform; InProgressMesh positiveShard = new(); InProgressMesh negativeShard = new(); for (int i = 0; i < mesh.triangles.Length; i += 3) { int i0 = mesh.triangles[i]; int i1 = mesh.triangles[i + 1]; int i2 = mesh.triangles[i + 2]; Vector3 p0 = mesh.vertices[i0]; Vector3 p1 = mesh.vertices[i1]; Vector3 p2 = mesh.vertices[i2]; if (plane.SameSide(p0, p1) && plane.SameSide(p1, p2)) { InProgressMesh targetMesh = plane.GetSide(p0) ? negativeShard : positiveShard; var indexCount = targetMesh.vertices.Count; targetMesh.vertices.AddRange(new[] { p0, p1, p2 }); targetMesh.triangles.AddRange(new[] { indexCount + 0, indexCount + 1, indexCount + 2 }); 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] }); } else { var positiveSideIndexes = new List<int>(); var negativeSideIndexes = new List<int>(); List<int> oneSideIndexes; List<int> twoSideIndexes; InProgressMesh oneSideMesh; InProgressMesh twoSideMesh; 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); 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 { throw new InvalidOperationException("impossible"); } // Get original vertex, UV, and normal var ov = mesh.vertices[oneSideIndexes[0]]; var ov_uv = mesh.uv[oneSideIndexes[0]]; var ov_normal = mesh.normals[oneSideIndexes[0]]; // Get the two other vertices on the other 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]]; // Intersections IntersectPlaneLine(plane, ov, tv0, out var d0, out var ip0); IntersectPlaneLine(plane, ov, tv1, out var d1, out var ip1); // Interpolated UVs and normals var ip0_uv = Vector2.Lerp(ov_uv, tv0_uv, d0 / (ov - tv0).magnitude); var ip1_uv = Vector2.Lerp(ov_uv, tv1_uv, d1 / (ov - tv1).magnitude); var ip0_normal = Vector3.Lerp(ov_normal, tv0_normal, d0 / (ov - tv0).magnitude).normalized; var ip1_normal = Vector3.Lerp(ov_normal, tv1_normal, d1 / (ov - tv1).magnitude).normalized; // Triangle on the one-vertex side (triangle: ov, ip0, ip1) int startIndex = oneSideMesh.vertices.Count; oneSideMesh.vertices.AddRange(new[] { ov, ip0, ip1 }); oneSideMesh.uvs.AddRange(new[] { ov_uv, ip0_uv, ip1_uv }); oneSideMesh.normals.AddRange(new[] { ov_normal, ip0_normal, ip1_normal }); oneSideMesh.triangles.AddRange(new[] { startIndex, startIndex + 1, startIndex + 2 }); // Quad split into two triangles on the two-vertex side // First triangle: tv0, tv1, ip1 // Second triangle: tv0, ip1, ip0 int twoStart = twoSideMesh.vertices.Count; twoSideMesh.vertices.AddRange(new[] { tv0, tv1, ip1, ip0 }); twoSideMesh.uvs.AddRange(new[] { tv0_uv, tv1_uv, ip1_uv, ip0_uv }); twoSideMesh.normals.AddRange(new[] { tv0_normal, tv1_normal, ip1_normal, ip0_normal }); twoSideMesh.triangles.AddRange(new[] { twoStart, twoStart + 1, twoStart + 2, // tv0, tv1, ip1 twoStart, twoStart + 2, twoStart + 3 // tv0, ip1, ip0 }); } } // Create GameObjects GameObject negativeShardObject = new GameObject("Negative Shard"); negativeShardObject.transform.position = parent.position; negativeShardObject.transform.rotation = parent.rotation; negativeShardObject.transform.localScale = parent.lossyScale; negativeShardObject.AddComponent<MeshRenderer>().material = material; negativeShardObject.AddComponent<MeshFilter>().mesh = negativeShard.MakeMesh(); // negativeShardObject.AddComponent<Rigidbody>(); // negativeShardObject.AddComponent<MeshCollider>(); GameObject positiveShardObject = new GameObject("Positive Shard"); positiveShardObject.transform.position = parent.position; positiveShardObject.transform.rotation = parent.rotation; positiveShardObject.transform.localScale = parent.lossyScale; positiveShardObject.AddComponent<MeshRenderer>().material = material; positiveShardObject.AddComponent<MeshFilter>().mesh = positiveShard.MakeMesh(); // positiveShardObject.AddComponent<Rigidbody>(); // positiveShardObject.AddComponent<MeshCollider>(); GetComponent<MeshRenderer>().enabled = false; } // Update is called once per frame void Update() { } }