Skip to content

Instantly share code, notes, and snippets.

@nasser
Last active April 8, 2025 19:14
Show Gist options
  • Select an option

  • Save nasser/6af19c6bd286b10cc869ed9a88894ce6 to your computer and use it in GitHub Desktop.

Select an option

Save nasser/6af19c6bd286b10cc869ed9a88894ce6 to your computer and use it in GitHub Desktop.

Revisions

  1. nasser revised this gist Apr 8, 2025. 1 changed file with 118 additions and 47 deletions.
    165 changes: 118 additions & 47 deletions Slice.cs
    Original 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()
    }
    }

    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);
    }

    /// <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;

    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);
    _plane = new Plane(planeNormal, planePosition);
    Mesh mesh = GetComponent<MeshFilter>().mesh;
    Material material = GetComponent<MeshRenderer>().material;
    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 (_plane.SameSide(p0, p1) && _plane.SameSide(p1, p2))
    {
    InProgressMesh targetMesh = plane.GetSide(p0) ? negativeShard : positiveShard;
    var indexCount = targetMesh.vertices.Count;
    // 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.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] });
    // 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;
    if (plane.GetSide(p0)) positiveSideIndexes.Add(i0);

    // 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);
    if (_plane.GetSide(p1)) positiveSideIndexes.Add(i1);
    else negativeSideIndexes.Add(i1);
    if (plane.GetSide(p2)) positiveSideIndexes.Add(i2);
    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 original vertex, UV, and normal
    // 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 the two other vertices on the other side
    // 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]];

    // Intersections
    IntersectPlaneLine(plane, ov, tv0, out var d0, out var ip0);
    IntersectPlaneLine(plane, ov, tv1, out var d1, out var ip1);
    // 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);

    // 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;
    // 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;

    // Triangle on the one-vertex side (triangle: ov, ip0, ip1)
    // 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, ip0, ip1 });
    oneSideMesh.uvs.AddRange(new[] { ov_uv, ip0_uv, ip1_uv });
    oneSideMesh.normals.AddRange(new[] { ov_normal, ip0_normal, ip1_normal });
    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 });

    // Quad split into two triangles on the two-vertex side
    // First triangle: tv0, tv1, ip1
    // Second triangle: tv0, ip1, ip0

    // 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, 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.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
    // 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();
    // 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>();

    // hide this mesh
    GetComponent<MeshRenderer>().enabled = false;
    }

  2. nasser created this gist Apr 7, 2025.
    209 changes: 209 additions & 0 deletions Slice.cs
    Original 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()
    {
    }
    }