Search Unity

Question Mesh Combine That Preserves Shared Vertices

Discussion in 'Scripting' started by ParadoxSolutions, Sep 9, 2020.

  1. ParadoxSolutions

    ParadoxSolutions

    Joined:
    Jul 27, 2015
    Posts:
    325
    I have noticed that when using Mesh.Combine that it removes shared vertices (welds). This is really annoying when exporting a mesh to a 3D file format or using vertex displacement since I like my triangles connected.

    Unity's Mesh.Combine method pretty much does the equivalent to adding all mesh data to lists and assigning it to a new mesh. I'm hoping the community can help me out in making an improved combiner that takes welded vertices into account.

    Combine Docs: https://docs.unity3d.com/ScriptReference/Mesh.CombineMeshes.html

    Code (CSharp):
    1.  
    2. Public Mesh CombineMesh(List<MeshFilter> filters)
    3. {
    4. Mesh m = new Mesh();
    5.                 if (filters.Count > 0)
    6.                 {
    7. List<Vector3> verts = new List<Vector3>();//Note: A hashset wouldn't contain duplicates...
    8.                 List<Vector2> uvs = new List<Vector2>();
    9.                 List<Vector3> normals = new List<Vector3>();
    10.                 List<int> tris = new List<int>();
    11.  
    12.                     for (int i = 0; i < filters.Count; i++)
    13.                     {
    14.                         if (filters[i] != null)
    15.                         {
    16.          
    17.                             for (int v = 0; v <= filters[i].mesh.vertices.Length - 1; v++)
    18.                             {
    19.                                 if (!verts.Contains(filters[i].mesh.vertices[v]))//if duplicate vert...
    20.                                 {
    21.                                     verts.Add(filters[i].mesh.vertices[v]);
    22.                                     uvs.Add(filters[i].mesh.uv[v]);
    23.                                     normals.Add(filters[i].mesh.normals[v]);
    24.  
    25.                                     tris.Add(filters[i].mesh.triangles[v]);
    26.                                     tris.Add(filters[i].mesh.triangles[v + 1]);
    27.                                     tris.Add(filters[i].mesh.triangles[v + 2]);
    28.  
    29.              
    30.                                 }
    31.                                 //else don't add duplicates...
    32.                             }
    33.                         }
    34.                     }
    35.                     m.SetVertices(verts);
    36.                     m.SetUVs(0, uvs));
    37.                     m.SetNormals(normals);
    38.                     m.SetTriangles(tris, 0);
    39.                 }
    40. return m;
    41.  
    All I really have to go off of is this guy's answer:
    https://answers.unity.com/questions/44208/merge-vertices-at-runtime.html

    and this implementation of a weld function that does not include UV or normal data:
    http://wiki.unity3d.com/index.php/Mesh_simplification_(for_MeshCollider,_lossless)

    Please do not point me toward a paid asset, I'm aware of MeshBaker and MeshToolkit but I need an open source solution.


    I have tried using Probuilder's Runtime API: https://docs.unity3d.com/Packages/c....ProBuilder.MeshOperations.VertexEditing.html

    but the best result has only been able to weld one vertex in each triangle after trying a dozen different implementations of both the merge and weld functions.

    Would be a benefit to everyone combining at runtime if we can come up with a solution.
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,726
    Are you sure that two vertices NOT combined meet these criteria:

    - contain precisely the same UV coordinates.
    - are assigned to the same material instance (i.e., submesh #)

    There may be more reasons that the resultant mesh must not share a vert, but those above are a starter.

    It's also possible that due to floating point error the resultant verts from different shapes actually don't line up, in which case you probably need to hack your own notion of close enough to weld (line 19) to accept small errors, keeping in mind the limitations above.

    Easiest way to track it down is make 2 meshes each with at least one shared vert with the other, and a single triangle connecting the three verts. That way the dataset is small enough you could print out the decisions of the run to Debug.Log() and reason about what is happening.
     
    eses likes this.
  3. adamgolden

    adamgolden

    Joined:
    Jun 17, 2019
    Posts:
    1,555
    This would be a rather complex class before it's functional. A quick look, some feedback..
    Code (CSharp):
    1. tris.Add(filters[i].mesh.triangles[v]);
    First, you're adding 3 indices to your triangles list for each vertex, which won't work. For example with just 3 vertices you would have added a total of 9 indices (3 actual triangles) and all indices with values above 2 would refer to a vertex that doesn't exist, because you've only added 3 vertices ..i.e. you're adding values of 2, 3 and 4 in that loop for the 3rd vertex. Once you have that dealt with, it'll still come out wrong as soon as you skip adding any given vertex. Any triangle indices of the given mesh that are higher than the vertex being skipped all need to have 1 subtracted each time you exclude a vertex with an index of lesser value. Once that's taken into account, it still won't work beyond the first mesh, because the indices being referred to in triangles won't be the same in the result.. for example in mesh #2, triangle index 0 copied as-is would refer to the first vertex of the first mesh you added to the new mesh.
     
  4. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,726
  5. ParadoxSolutions

    ParadoxSolutions

    Joined:
    Jul 27, 2015
    Posts:
    325
    @Kurt-Dekker, @polemical
    Response:
    For my purposes meshes always have the same material and are assigned unique UVs (I pack them in an atlas) before the combine. Does Unity's builtin combine require the same UV layout for each mesh to maintain welds?

    Regardless I'm looking for something that can detect and correct for loss of welds in the cases where Unity's built-in combiner does not.

    ___________________________________________________________________


    The code I posted above is a very rough starting point for sure. There will likely have to be a distance check between vertices and keeping the triangle order when removing vertices will be tricky.

    What I wonder is would it be easier to make a new combine method from scratch that retains welds or a method that corrects welds after a combine method like Unitys built-in. The weld function in that script from the wiki is a good starting place although it is undocumented and does not support normals or UVs.
     
  6. ParadoxSolutions

    ParadoxSolutions

    Joined:
    Jul 27, 2015
    Posts:
    325
    So we can see the Weld function from the wiki:
    Code (CSharp):
    1. public static void Weld(this Mesh mesh, float threshold, float bucketStep)
    2.         {
    3.             Vector3[] oldVertices = mesh.vertices;
    4.             Vector3[] newVertices = new Vector3[oldVertices.Length];
    5.             int[] old2new = new int[oldVertices.Length];
    6.             int newSize = 0;
    7.  
    8.             // Find AABB
    9.             Vector3 min = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
    10.             Vector3 max = new Vector3(float.MinValue, float.MinValue, float.MinValue);
    11.             for (int i = 0; i < oldVertices.Length; i++)
    12.             {
    13.                 if (oldVertices[i].x < min.x) min.x = oldVertices[i].x;
    14.                 if (oldVertices[i].y < min.y) min.y = oldVertices[i].y;
    15.                 if (oldVertices[i].z < min.z) min.z = oldVertices[i].z;
    16.                 if (oldVertices[i].x > max.x) max.x = oldVertices[i].x;
    17.                 if (oldVertices[i].y > max.y) max.y = oldVertices[i].y;
    18.                 if (oldVertices[i].z > max.z) max.z = oldVertices[i].z;
    19.             }
    20.             min -= Vector3.one * 0.111111f;
    21.             max += Vector3.one * 0.899999f;
    22.  
    23.             // Make cubic buckets, each with dimensions "bucketStep"
    24.             int bucketSizeX = Mathf.FloorToInt((max.x - min.x) / bucketStep) + 1;
    25.             int bucketSizeY = Mathf.FloorToInt((max.y - min.y) / bucketStep) + 1;
    26.             int bucketSizeZ = Mathf.FloorToInt((max.z - min.z) / bucketStep) + 1;
    27.             List<int>[,,] buckets = new List<int>[bucketSizeX, bucketSizeY, bucketSizeZ];
    28.  
    29.             // Make new vertices
    30.             for (int i = 0; i < oldVertices.Length; i++)
    31.             {
    32.                 // Determine which bucket it belongs to
    33.                 int x = Mathf.FloorToInt((oldVertices[i].x - min.x) / bucketStep);
    34.                 int y = Mathf.FloorToInt((oldVertices[i].y - min.y) / bucketStep);
    35.                 int z = Mathf.FloorToInt((oldVertices[i].z - min.z) / bucketStep);
    36.  
    37.                 // Check to see if it's already been added
    38.                 if (buckets[x, y, z] == null)
    39.                     buckets[x, y, z] = new List<int>(); // Make buckets lazily
    40.  
    41.                 for (int j = 0; j < buckets[x, y, z].Count; j++)
    42.                 {
    43.                     Vector3 to = newVertices[buckets[x, y, z][j]] - oldVertices[i];
    44.                     if (Vector3.SqrMagnitude(to) < 0.001f)
    45.                     {
    46.                         old2new[i] = buckets[x, y, z][j];
    47.                         goto skip; // Skip to next old vertex if this one is already there
    48.                     }
    49.                 }
    50.  
    51.                 // Add new vertex
    52.                 newVertices[newSize] = oldVertices[i];
    53.                 buckets[x, y, z].Add(newSize);
    54.                 old2new[i] = newSize;
    55.                 newSize++;
    56.  
    57.                 skip:;
    58.             }
    59.  
    60.             // Make new triangles
    61.             int[] oldTris = mesh.triangles;
    62.             int[] newTris = new int[oldTris.Length];
    63.             for (int i = 0; i < oldTris.Length; i++)
    64.             {
    65.                 newTris[i] = old2new[oldTris[i]];
    66.             }
    67.  
    68.             Vector3[] finalVertices = new Vector3[newSize];
    69.             for (int i = 0; i < newSize; i++)
    70.                 finalVertices[i] = newVertices[i];
    71.  
    72.             mesh.Clear();
    73.             mesh.vertices = finalVertices;
    74.             mesh.triangles = newTris;
    75. }
    Source:
    http://wiki.unity3d.com/index.php/Mesh_simplification_(for_MeshCollider,_lossless)
     
    migueltuliompg and FaffyWaffles like this.