Skip to content

Instantly share code, notes, and snippets.

@nasser
Last active April 8, 2025 19:14
Show Gist options
  • Save nasser/6af19c6bd286b10cc869ed9a88894ce6 to your computer and use it in GitHub Desktop.
Save nasser/6af19c6bd286b10cc869ed9a88894ce6 to your computer and use it in GitHub Desktop.
Slice.cs
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