Last active
April 8, 2025 19:14
-
-
Save nasser/6af19c6bd286b10cc869ed9a88894ce6 to your computer and use it in GitHub Desktop.
Slice.cs
This file contains 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 characters
using System; | |
using System.Collections.Generic; | |
using Unity.VisualScripting; | |
using UnityEngine; | |
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(); | |
mesh.SetVertices(vertices); | |
mesh.SetTriangles(triangles, 0); | |
mesh.SetUVs(0, uvs); | |
mesh.SetNormals(normals); | |
return mesh; | |
} | |
} | |
/// <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; | |
Vector3 B2 = p - bitangent * 10f; | |
Gizmos.color = Color.blue; | |
Gizmos.DrawLine(A1, A2); | |
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); | |
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; | |
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; | |
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<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; | |
} | |
// Update is called once per frame | |
void Update() | |
{ | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment