Remove Vertices that are not in triangle[Solved]

I am using a Boolean mesh script to create a new mesh of the intersection at run-time. This works well, however, when viewing the mesh there is a lot of blank space around it. I believe that the script is deleting the extra triangles, but not the vertices. I have tried exporting the model as an object, and re-importing it later. This results in a much smaller vertex count, although the model is the same.

This would not be an issue, but I need to use the mesh to create a convex mesh collision. The vertices become part of the collision which is very inaccurate.

Is there a way to remove these at run-time?

This was the function I tried, but it didn’t make a difference:

Mesh ClearBlanks(Mesh mesh)
    {
        int[] triangles = mesh.triangles;
        Vector3[] vertices = mesh.vertices;
        Vector2[] uv = mesh.uv;
        Vector3[] normals = mesh.normals;
        List<Vector3> vertList = new List<Vector3>();
        List<Vector2> uvList = new List<Vector2>();
        List<Vector3> normalsList = new List<Vector3>();
        List<int> trianglesList = new List<int>();

        for (int triCount = 0; triCount < triangles.Length; triCount += 3)
        {
            for(int w = 0; w < 3; w++)
            {
                trianglesList.Add(triangles[triCount + w]);
                vertList.Add(vertices[triCount + w]);
                uvList.Add(uv[triCount + w]);
                normalsList.Add(normals[triCount + w]);
            }
        }


        triangles = trianglesList.ToArray();
        vertices = vertList.ToArray();
        uv = uvList.ToArray();
        normals = normalsList.ToArray();
        //mesh.Clear();
        mesh.triangles = triangles;
        mesh.vertices = vertices;
        mesh.uv = uv;
        mesh.normals = normals;
        return mesh;
    }

Thank you!

I think you have some code missing there (like where the variable ā€˜i’ comes from), but I do have a suggestion.

The ā€˜triangles’ array is an index into the ā€˜vertices’ array. The ā€˜vertices’, ā€˜uv’, and ā€˜normals’ arrays all should be the same size.

Here’s the high-level idea: We look at every vertex. If we find it’s not in the list of triangles, we remove it from the list of vertices (and UVs and normals), but then we’ve got a lot of bad indices in the triangle array. To fix that, we have to iterate over all of the triangles and subtract one from every number greater than the index we are testing.

  • Store ā€˜triangles’, ā€˜vertices’, ā€˜uv’, and ā€˜normals’ as List, List, or List as appropriate.

  • Set testVertex to 0.

  • While testVertex < triangles.Count()

  • If testVertex is not in ā€˜triangles’

  • triangles.removeAt( testVertex ); … same with ā€˜uv’ and ā€˜normals’

  • for( int i = 0; i < triangles.Count(); ++i )

  • If triangles > testVertex then triangles*–;*

  • Else if testVertex was in ā€˜triangles’
    - testVertex++

  • Convert the Lists to Arrays and assign them back to the mesh.

Let me know if you have any questions.

Yeah, I forgot to change those. They are updated now. I’ll try your idea and get back to you.

    Mesh ClearBlanks(Mesh mesh)
    {
        int[] triangles = mesh.triangles;
        Vector3[] vertices = mesh.vertices;
        Vector2[] uv = mesh.uv;
        Vector3[] normals = mesh.normals;
        List<Vector3> vertList = vertices.ToList();
        List<Vector2> uvList = uv.ToList();
        List<Vector3> normalsList = normals.ToList();
        List<int> trianglesList = triangles.ToList();

        int testVertex = 0;

        while (testVertex < trianglesList.Count)
        {
            if (trianglesList.Contains(testVertex))
            {
                Debug.Log(testVertex);
                testVertex++;
            }
            else
            {
                vertList.RemoveAt(testVertex);
                uvList.RemoveAt(testVertex);
                normalsList.RemoveAt(testVertex);
                for (int i = 0; i < trianglesList.Count; i++)
                {
                    if (trianglesList[i] > testVertex)
                        trianglesList[i]--;
                }
            }
        }

        triangles = trianglesList.ToArray();
        vertices = vertList.ToArray();
        uv = uvList.ToArray();
        normals = normalsList.ToArray();
     
        mesh.triangles = triangles;
        mesh.vertices = vertices;
        mesh.uv = uv;
        mesh.normals = normals;
        return mesh;
    }

right now, this returns an empty mesh. See any errors? I’ll try to fix it.

(update) Okay. Made a lot of stupid mistakes. Must be tired. Here is an updated code, some of the vertices from the shape are being deleted though none outside seem to be deleted.

(another update) Now I get the error argument is out of range.

        while (testVertex < trianglesList.Count)
        {
            if (trianglesList.Contains(testVertex))
            {
                Debug.Log(testVertex);
                testVertex++;
            }
            else
            {
                vertList.RemoveAt(testVertex);
                uvList.RemoveAt(testVertex);
                normalsList.RemoveAt(testVertex);
                trianglesList.RemoveAt(testVertex);
                for (int i = 0; i < trianglesList.Count; i++)
                {
                    if (trianglesList[i] > testVertex)
                        trianglesList[i]--;
                }
            }
        }

This code runs but the wrong vertices are removed.

    Mesh ClearBlanks(Mesh mesh)
    {
        int[] triangles = mesh.triangles;
        Vector3[] vertices = mesh.vertices;
        Vector2[] uv = mesh.uv;
        Vector3[] normals = mesh.normals;
        List<Vector3> vertList = vertices.ToList();
        List<Vector2> uvList = uv.ToList();
        List<Vector3> normalsList = normals.ToList();
        List<int> trianglesList = triangles.ToList();

        int testVertex = 0;

        while (testVertex < vertList.Count)
        {
            if (trianglesList.Contains(testVertex))
            {
                Debug.Log(testVertex);
                testVertex++;
            }
            else
            {
                vertList.RemoveAt(testVertex);
                uvList.RemoveAt(testVertex);
                normalsList.RemoveAt(testVertex);

                for (int i = 0; i < trianglesList.Count; i++)
                {
                    if (trianglesList[i] > testVertex)
                        trianglesList[i]--;
                }
            }
        }

        triangles = trianglesList.ToArray();
        vertices = vertList.ToArray();
        uv = uvList.ToArray();
        normals = normalsList.ToArray();
       
        mesh.triangles = triangles;
        mesh.vertices = vertices;
        mesh.uv = uv;
        mesh.normals = normals;
        return mesh;
    }
1 Like

That last attempt looks right. Is it working?

(Edit: Just noticed you changed the title to solved. Glad you got it working!)

Yeah! Works great.

For the following code is optimized huge meshes

    void ClearUnusedVerticesOfMesh(Mesh mesh)
    {
        try
        {
            int[] triangles = mesh.triangles;
            Vector3[] vertices = mesh.vertices;
            Vector2[] uv = mesh.uv;
            Vector3[] normals = mesh.normals;

            List<Vector3> vertList = vertices.ToList();
            List<Vector2> uvList = uv.ToList();
            List<Vector3> normalsList = normals.ToList();

            var v3unused = new Vector3(float.MinValue, float.MinValue, float.MinValue);
            var v2unused = new Vector2(float.MinValue, float.MinValue);

            // fill pointer array with used indecise
            var verIndecise = Enumerable.Repeat(-1, vertices.Length).ToArray();
            for (int i = 0; i < triangles.Length; i++)
            {
                verIndecise[triangles] = triangles;
            }

            // remove unused pointers and adjust
            var idx = 0;
            for (int i = 0; i < verIndecise.Length; i++)
            {
                if (verIndecise == -1)
                {
                    // mark for removal
                    vertList = v3unused;
                    uvList = v2unused;
                    normalsList = v3unused;

                    continue;
                }
                // used index
                verIndecise = idx;
                // assign next index
                idx++;
            }

            // adjust triangle
            for (int i = 0; i < triangles.Length; i++)
            {
                triangles = verIndecise[triangles];
            }

            // remove unused
            vertList.RemoveAll(it => it == v3unused);
            normalsList.RemoveAll(it => it == v3unused);
            uvList.RemoveAll(it => it == v2unused);

            if (vertList.Count != idx)
            {
                Debug.LogError($"vertices count {vertList.Count} not equal to pointer list count {idx}");
            }


            // must be set again
            mesh.triangles = triangles.ToArray();
            mesh.vertices = vertList.ToArray();
            mesh.uv = uvList.ToArray();
            mesh.normals = normalsList.ToArray();
        }
        catch (System.Exception ex)
        {
            Debug.LogException(ex);
        }
    }

Besides that fact that your code does not compile because it seems the code is missing some * (did you post it without code tags first?), this approach isn’t that optimised as well. Yes, it avoids the O(n²) nature of the original approach, but instead you’re using some shady techniques like magic values. Since the position of the vertices would not stay as we remove vertices, there’s not really any benefit keeping the exact vertex order (without the ones we want to remove). It would be way simpler to just rebuild the vertices from the triangles and just keep track of the new mapping.*
This should be better both, performance wise and memory wise.
```csharp

  • public static void RemoveUnusedVertices(Mesh aMesh)
    {
    Vector3 vertices = aMesh.vertices;
    Vector3 normals = aMesh.normals;
    Vector4 tangents = aMesh.tangents;
    Vector2 uv = aMesh.uv;
    Vector2 uv2 = aMesh.uv2;
    int triangles = aMesh.triangles;

    Dictionary<int, int> vertMap = new Dictionary<int, int>(vertices.Length);

    List newVerts = new List(vertices.Length);
    List newNormals = new List(vertices.Length);
    List newTangents = new List(vertices.Length);
    List newUV = new List(vertices.Length);
    List newUV2 = new List(vertices.Length);

    System.Func<int, int> remap = (int aIndex) => {
    int res = -1;
    if (!vertMap.TryGetValue(aIndex, out res))
    {
    res = newVerts.Count;
    vertMap.Add(aIndex, res);
    newVerts.Add(vertices[aIndex]);
    if (normals != null && normals.Length > 0)
    newNormals.Add(normals[aIndex]);
    if (tangents != null && tangents.Length > 0)
    newTangents.Add(tangents[aIndex]);
    if (uv != null && uv.Length > 0)
    newUV.Add(uv[aIndex]);
    if (uv2 != null && uv2.Length > 0)
    newUV2.Add(uv2[aIndex]);
    }
    return res;
    };

    for (int i = 0; i < triangles.Length; i++)
    {
    triangles[i] = remap(triangles[i]);
    }
    aMesh.Clear();
    aMesh.SetVertices(newVerts);
    if (newNormals.Count > 0)
    aMesh.SetNormals(newNormals);
    if (newTangents.Count > 0)
    aMesh.SetTangents(newTangents);
    if (newUV.Count > 0)
    aMesh.SetUVs(0, newUV);
    if (newUV2.Count > 0)
    aMesh.SetUVs(1, newUV2);
    aMesh.triangles = triangles;
    }*
    ```
    Note this still does not work with ā€œallā€ kinds of meshes as it only looks at the uv channel 1 and 2. Also it does not care about boneWeights and bindposes at the moment. So if this is an animated mesh you would need to take them into account as well. Same goes with meshes with submeshes or other topologies. This would make the method exponentially more complex.
    All this does is iterating through the triangles array and for each vertex it encounters it simply adds the vertex to a new list (/ lists) and remembers this mapping so if another shared vertex is hit, it will properly reuse the new index. At the end of this the new vertices lists now only contain the used vertices and since the internal ā€œRemapā€ method does inplace adjust the index, the triangle array is also fixed already.
    It excludes vertex attributes which are not present (normals, tangents and uv coordinates). Keep in mind that Unity now support up to 8 UV channels. Also technically UV coordinates could be Vector3 or Vector4 values as well. Unity does have the proper GetUVs / SetUVs methods, however it would be difficult to create propert code path for all possible combinations. GetVertexAttributeDimension can now be used to figure out the dimensionality of uv coordinates. Though that would require the code to use different Lists. If a general purpose method should be created, it probably makes more sense to access the raw vertex buffer.

Your Code does not work.
It works if line 34 is

return res;

not

return aIndex;

I 've built another solution which is mainly ā€œremappingā€ the existing vertices/triangles and put them into new arrays. An execution takes about 1sec for ~300 meshes with ~55k vertices each and ~500 triangles on average:

        /// <summary>
        /// This method is quite complex. What we do is:
        /// We use all the vertices from every mesh and check which ones are used in our submesh (aka are triangles using a vertex?)
        /// We create a new Vertices list and Triangles list based on our changes.
        /// Check the code for technical details.
        ///
        /// Example:
        ///     vertices  => 0=[...], 1=[...], 2=[...]
        ///     triangles => 0=4, 1=5, 2=8, 3=1, 4=1
        ///   
        ///     distinctOrderedTriangles => 0=1, 1=4, 2=5, 3=8
        ///   
        ///     newVertexTriangleMapping => 1=0, 4=1, 5=2, 8=3
        ///   
        ///     newVertices  => 0=[...], 1=[...], 2=[...]
        ///     newTriangles => 0=1, 1=0, 2=3, 3=0, 4=0 <-- values are replaced with new mapping
        /// </summary>
        private void _PrepareMeshFilter(MeshFilter meshFilter, PCBridge_World world, int materialIndex)
        {
            var vertices = world.vertices;
            var triangles = world.triangles[materialIndex];

            var mesh = new Mesh();
            meshFilter.mesh = mesh;

            // Distinct -> We only want to check vertex-indices once for adding to the new mapping
            // Ordered -> We need to check from lowest used vertex-index to highest for the new mapping
            List<int> distinctOrderedTriangles = triangles.Distinct().OrderBy(val => val).ToList();
            Dictionary<int, int> newVertexTriangleMapping = new();
            List<Vector3> newVertices = new();

            // Loop through all the distinctOrderedTriangles
            for (int i=0; i<distinctOrderedTriangles.Count; i++)
            {
                // curVertexIndex == currently lowest vertex index in this loop
                int curVertexIndex = distinctOrderedTriangles[i];
                Vector3 vertexAtIndex = vertices[curVertexIndex];

                // Previously index of vertex is now the new index of loop's >i<.
                // This Dictionary will be used to >replace< the original triangle values later.
                // e.g. previous Vertex-index=5 (key) is now the Vertex-index=0 (value)
                newVertexTriangleMapping.Add(curVertexIndex, i);

                // Add the vertex which was found as new lowest entry
                // e.g. Vertex-index=5 is now Vertex-index=0
                newVertices.Add(vertexAtIndex);
            }

            // Now we replace the triangle values. aka the vertex-indices (value old) with new >mapping< from Dictionary.
            var newTriangles = triangles.Select(originalVertexIndex => newVertexTriangleMapping[originalVertexIndex]);

            mesh.vertices = newVertices.ToArray();
            mesh.triangles = newTriangles.ToArray();
        }

In case anyone find this helpful, I needed the removal of unused vertices to work with submeshes. I have a lot of multi material meshes in my project, so this is the ideal solution for me. I adapted turndapage’s method for this purpose.

I don’t need to keep track of uv’s or normals so this solution is simply for vertex removal.

I have briefly tested it, but it might have some issues I am not aware of at this stage.

public static class UnusedVertexDeleter
{
    /// <summary>
    /// Removes all unused vertices in the mesh, while keeping track of submeshes.
    /// </summary>
    /// <returns>Modified mesh with the unused vertices removed.</returns>
    public static Mesh RemoveUnusedVerticesSubMeshAware(Mesh mesh)
    {
        // Store the triangles for each submesh in a 2D array.
        int[][] submeshTriangles = new int[mesh.subMeshCount][];
        for (int i = 0; i < mesh.subMeshCount; i++)
        {
            submeshTriangles[i] = mesh.GetTriangles(i);
        }

        // Store the vertices in a list for easy removal of unused vertices.
        Vector3[] vertices = mesh.vertices;
        List<Vector3> vertList = new List<Vector3>(vertices);
        int vertexCount = vertices.Length;

        // Loop through all vertices in the mesh.
        int testVertex = 0;
        while (testVertex < vertexCount)
        {
            // Check if the vertex is used in any submesh.
            bool usedBySubmesh = false;
            for (int submeshIndex = 0; submeshIndex < mesh.subMeshCount; submeshIndex++)
            {
                int[] triangles = submeshTriangles[submeshIndex];
                for (int i = 0; i < triangles.Length; i++)
                {
                    if (triangles[i] == testVertex)
                    {
                        usedBySubmesh = true;
                        break;
                    }
                }

                if (usedBySubmesh)
                {
                    break;
                }
            }

            // Remove the vertex if it is not used by any submesh.
            // Update the indices of any triangles affected by the removal.
            if (usedBySubmesh)
            {
                testVertex++;
            }
            else
            {
                vertList.RemoveAt(testVertex);
                vertexCount--;

                for (int submeshIndex = 0; submeshIndex < mesh.subMeshCount; submeshIndex++)
                {
                    int[] triangles = submeshTriangles[submeshIndex];
                    for (int i = 0; i < triangles.Length; i++)
                    {
                        if (triangles[i] > testVertex)
                        {
                            triangles[i]--;
                        }
                    }

                    mesh.SetTriangles(triangles, submeshIndex);
                }
            }
        }

        // Update the mesh with the new vertices and bounds.
        mesh.vertices = vertList.ToArray();
        mesh.RecalculateBounds();

        return mesh;
    }
}

Well, it’s horribly inefficient. If you have say 100000 vertices and several submeshes with say around 30000 triangles, your inner loop logic would run 9000000000 times.

The method I posted above would be way faster as we only iterate through the triangles once and just collect and redistribute the used vertices. Of course when you have submeshes you would simply iterate through the submeshes indices instead of the triangles array, that’s all. Since we use a dictionary to map the vertices, it means shared vertices will stay shared vertices.

Here’s a version that should work with arbitrary submeshes with any kind of topology, not just triangle meshes.

    public static void RemoveUnusedVertices(Mesh aMesh)
    {
        Vector3[] vertices = aMesh.vertices;
        Vector3[] normals = aMesh.normals;
        Vector4[] tangents = aMesh.tangents;
        Vector2[] uv = aMesh.uv;
        Vector2[] uv2 = aMesh.uv2;
        List<int> indices = new List<int>();

        Dictionary<int, int> vertMap = new Dictionary<int, int>(vertices.Length);

        List<Vector3> newVerts = new List<Vector3>(vertices.Length);
        List<Vector3> newNormals = new List<Vector3>(vertices.Length);
        List<Vector4> newTangents = new List<Vector4>(vertices.Length);
        List<Vector2> newUV = new List<Vector2>(vertices.Length);
        List<Vector2> newUV2 = new List<Vector2>(vertices.Length);

        System.Func<int, int> remap = (int aIndex) => {
            int res = -1;
            if (!vertMap.TryGetValue(aIndex, out res))
            {
                res = newVerts.Count;
                vertMap.Add(aIndex, res);
                newVerts.Add(vertices[aIndex]);
                if (normals != null && normals.Length > 0)
                    newNormals.Add(normals[aIndex]);
                if (tangents != null && tangents.Length > 0)
                    newTangents.Add(tangents[aIndex]);
                if (uv != null && uv.Length > 0)
                    newUV.Add(uv[aIndex]);
                if (uv2 != null && uv2.Length > 0)
                    newUV2.Add(uv2[aIndex]);
            }
            return res;
        };
        for (int subMeshIndex = 0; subMeshIndex < aMesh.subMeshCount; subMeshIndex++)
        {
            var topology = aMesh.GetTopology(subMeshIndex);
            indices.Clear();
            aMesh.GetIndices(indices, subMeshIndex);
            for (int i = 0; i < indices.Count; i++)
            {
                indices[i] = remap(indices[i]);
            }
            aMesh.SetIndices(indices, topology, subMeshIndex);
        }
        aMesh.SetVertices(newVerts);
        if (newNormals.Count > 0)
            aMesh.SetNormals(newNormals);
        if (newTangents.Count > 0)
            aMesh.SetTangents(newTangents);
        if (newUV.Count > 0)
            aMesh.SetUVs(0, newUV);
        if (newUV2.Count > 0)
            aMesh.SetUVs(1, newUV2);
    }

Though this is currently limited to UV channel 0 and 1, we now have up to 7 (8 in total) and another issue is that UVs don’t have to be a Vector2. They can be Vector3 or Vector4 as well. Covering all those cases would make the code much more complex. For that it probably makes more sense to use one of the low level mesh data methods and treat a vertex like it is defined on the GPU. Unity now has a few options to manipulate the mesh data.

1 Like

Yeah that makes sense! I’m mostly working with meshes with <50 vertices so performance was not my concern. That’s fantastic, your approach looks great. Will give it a try to replace my previous approach.