Fastest way to combine meshes

I have not been able to decipher how to use mesh.Vertices.GetEnumerator in the following code to speed up copying the vertices of one mesh into another mesh. I use this code to combine several meshes.

import  rhinoscriptsyntax as rs
from Rhino.Geometry import Mesh, Point3f
from scriptcontext import doc 
P3f = Point3f

# Part 1 Works using .Add to add vertices to new_mesh which is used to combine several meshes.

# Define very simple mesh.
meshGeo = Mesh()
meshGeo.Vertices.Add(P3f(0.0,0.0,0.0))
meshGeo.Vertices.Add(P3f(1.0,1.0,0.0))
meshGeo.Vertices.Add(P3f(1.0,-1.0,0.0))
meshGeo.Faces.AddFace(0,1,2)
meshGeo.Vertices.Add(P3f(1.0,1.0,0.0))
meshGeo.Vertices.Add(P3f(2.0,0.0,0.0))
meshGeo.Vertices.Add(P3f(1.0,-1.0,0.0))
meshGeo.Faces.AddFace(3,4,5)
vertices = meshGeo.Vertices
vcount = vertices.Count
faces = meshGeo.Faces
# At start, set offset to 0.  For each following mesh to be combined, set offset to sum of vcount of prior combined meshes.
offset = 0
# Define new mesh for combining several meshes (with only 1 combined in this example).
new_meshGeo = Mesh()
new_vertices = new_meshGeo.Vertices
new_faces = new_meshGeo.Faces
for i in xrange(vcount): new_vertices.Add(vertices[i])
f_indices = faces.ToIntArray(True)
for i in xrange(faces.Count):
	ii = 3*i
	i1,i2,i3 = f_indices[ii+offset], f_indices[ii+1+offset], f_indices[ii+2+offset]
	new_faces.AddFace(i1,i2,i3)
print new_meshGeo.IsValid
doc.Objects.AddMesh(new_meshGeo)
[rs.ViewDisplayMode(view, 'Wireframe') for view in rs.ViewNames()]


# Part 2 Fails using .AddVertices with ienumberator obtained from mesh to be added to new_mesh

# Define very simple mesh.
meshGeo = Mesh()
meshGeo.Vertices.Add(P3f(3.0,0.0,0.0))
meshGeo.Vertices.Add(P3f(4.0,1.0,0.0))
meshGeo.Vertices.Add(P3f(4.0,-1.0,0.0))
meshGeo.Faces.AddFace(0,1,2)
meshGeo.Vertices.Add(P3f(4.0,1.0,0.0))
meshGeo.Vertices.Add(P3f(5.0,0.0,0.0))
meshGeo.Vertices.Add(P3f(4.0,-1.0,0.0))
meshGeo.Faces.AddFace(3,4,5)
vertices = meshGeo.Vertices
vcount = vertices.Count
faces = meshGeo.Faces
new_meshGeo = Mesh()
new_vertices = new_meshGeo.Vertices
new_faces = new_meshGeo.Faces
# Get Ienumerator for vertices.
ienumerator = vertices.GetEnumerator()
print ienumerator
new_vertices.AddVertices(ienumerator) # This line fails with message: expected IEnumerable[Point3f], got <GetEnumerator>d__72
f_indices = faces.ToIntArray(True)
for i in xrange(faces.Count):
	ii = 3*i
	i1,i2,i3 = f_indices[ii+offset], f_indices[ii+1+offset], f_indices[ii+2+offset]
	new_faces.AddFace(i1,i2,i3)
print new_meshGeo.IsValid
doc.Objects.AddMesh(new_meshGeo)
[rs.ViewDisplayMode(view, 'Wireframe') for view in rs.ViewNames()]

The code in Part 2 above fails for the line:

new_vertices.AddVertices(ienumerator)

with the message:

Message: expected IEnumerable[Point3f], got <GetEnumerator>d__72

Does someone know how to fix this?

Regards,
Terry.

Hi Terry,
I don’t know these methods but tried replacing your line with new_vertices.AddVertices(vertices) and got no errors.
If I understand correctly then Mesh.Vertices is in itself IEnumerable
Happy New Year!
Graham

1 Like

IEnumerable is an interface, which is a contract.

Any class implementing this interface guaranties that it contains methods or other class members which are defined by the interface.

In contrast to an abstract or concrete parent class it doesn‘t need to be related to each other.

In particular IEnumerable defines collection-like behaviour.
Now if you have a method like AddVertices, it only demands an object with that interface and such you can feed in an array, a list or any other type of collection of type Point3f which implements from that. Confusing since Python doesn‘t directly support the concept of interfaces.

1 Like

Are you aware of Mesh.Append()? If you don’t need to control the indexing, something like:

import Rhino as R
import rhinoscriptsyntax as rs
import scriptcontext as sc


mesh_guid = rs.GetObject('select mesh to be joined')
mesh_geo = rs.coercegeometry(mesh_guid)

new_mesh_guid = rs.GetObject('select mesh to join to')
new_mesh_geo = rs.coercegeometry(new_mesh_guid)

new_mesh_geo.Append(mesh_geo)

sc.doc.Objects.AddMesh(new_mesh_geo)

Or:

mesh_guids = rs.GetObjects('select meshes to combine')

new_mesh = R.Geometry.Mesh()

for mesh_guid in mesh_guids:
    geo = rs.coercegeometry(mesh_guid)
    new_mesh.Append(geo)
    
sc.doc.Objects.AddMesh(new_mesh)

Result:

I do this when I need a representative mesh of several objects. I’ve also found it is good to run Mesh.UnifyNormals() and Mesh.Normals.ComputeNormals() after joining.

2 Likes

Graham,

Thanks for your answer. This method works fine for combining my large meshes, speeds up the process 20% and makes for simpler coding. I can also use this other places in my large script.

Regards,
Terry.

1 Like

Tom,

Thanks for the information on IEnumerable.

Regards,
Terry.

Nathan,

This is a great shortcut. This makes merging my large meshes 5X faster. Plus the coding is much simpler.
Really a great improvement!

Regards,
Terry.

Glad it helped.

After reading the documentation on Mesh.Append I discovered I could use:

from scriptcontext import doc
from Rhino.Geometry import Mesh
def getGeo(id):
   obj = doc.Objects.Find(id)
   return obj.Geometry

new_meshGeo = Mesh()
meshGeos = [getGeo(mesh) for mesh in meshes]
new_meshGeo.Append(meshGeos)

Now the combining operation is 10X faster. Really fantastic!

Because my meshes are touching, I use:

# Get vertex count before identical vertices are removed.
vb = new_meshGeo.Vertices.Count
# Combine identical vertices.
rc = new_meshGeo.Vertices.CombineIdentical(True, True)
if rc: print '    Found and removed {0:,} identical vertices leaving {1:,} vertices and {2:,} faces in {3:.4f} sec'.format(vb - new_meshGeo.Vertices.Count, new_meshGeo.Vertices.Count, new_meshGeo.Faces.Count, time() - timeb)
else: print '    Mesh has no identical vertices found in {0:.4f} sec'.format(time() - timeb)

which, for one of my cases, results in:

Found and removed 33,448 identical vertices leaving 1,023,945 vertices and 2,047,793 faces in 0.2656 sec

I then also compact the mesh:

# Compact mesh.
mem_before = 1e-6*new_meshGeo.MemoryEstimate()
timeb = time()
print '    Compact mesh = ',new_meshGeo.Compact()
mem_after = 1e-6*new_meshGeo.MemoryEstimate()
valid = new_meshGeo.IsValid
print '    Time to compact mesh = {0:.4f} sec which saved {1:.2f} MB'.format(time() - timeb, mem_before - mem_after)
print '    After compacting, new mesh has {0:,} vertices and {1:,} faces and is valid = {2}'.format(new_meshGeo.Vertices.Count, new_meshGeo.Faces.Count, valid)

which results in:

Compact mesh =  True
Time to compact mesh = 0.1094 sec which saved 30.41 MB
After compacting, new mesh has 1,023,945 vertices and 2,047,793 faces and is valid = True

And then I redo the Normals as you outlined.

Regards,
Terry.

1 Like

That seems like acceptable performance. I guess the more you can do down in the rhino geometry layer the more can be optimized.

I’ve never used .CombineIdentical() or .Compact(), but they seem fast.

Because my meshes can have colored vertices, I remove the duplicate vertices first using .CombineIdentical so that it is more straightforward to re-color the vertices in the combined mesh.

Attached is the complete Python script that I use to combine meshes. If the meshes have vertex colors, they are transferred to the combined mesh even for meshes that touch and share vertices/faces.

combine_meshes.py (3.0 KB)

Here is the text:

from scriptcontext import doc 
import Rhino.DocObjects
from Rhino.Geometry import Mesh
from System import Array
from System.Drawing import Color
from time import time

def getGeo(id):
	obj = doc.Objects.Find(id)
	return obj.Geometry

# Combine multiple meshes into one, preserving vertex colors if they are present.
def combine_meshes(meshes):
	timea = time()
	print 'Multiple meshes found and will be combined into one mesh for use in the display of maps.'
	# Get the geometry of the meshes.
	meshGeos = [getGeo(mesh) for mesh in meshes]
	# Check if meshes have vertex colors. The vertex colors will be copied to the combined mesh.
	timeb = time()
	has_colors = True
	# Make dictionary for translating vertex to color.
	p2c = {}
	# Check to see if all meshes have a color for each vertex and save them if they exist.
	for meshGeo in meshGeos:
		count = meshGeo.VertexColors.Count; vcount = meshGeo.Vertices.Count
		if count != vcount: has_colors = False; break
		# Save colors using vertex as pointer to color.
		vertices = meshGeo.Vertices
		for i in xrange(vcount): p2c[vertices[i]] = meshGeo.VertexColors[i]
	print '    Time to check for vertex colors and save them if present = {0:.4f} sec'.format(time() - timeb)
	# Combine mesh geometries.
	timeb = time()
	new_meshGeo = Mesh()
	new_meshGeo.Append(meshGeos)
	vertices = new_meshGeo.Vertices
	vcount = vertices.Count
	print '    Made 1 mesh with {0:,} vertices and {1:,} faces in {2:.4f} sec'.format(vcount, new_meshGeo.Faces.Count, time() - timeb)
	timeb = time()
	# Save vertex count before identical vertices are removed.
	vb = vcount
	# Combine identical vertices.
	rc = vertices.CombineIdentical(True, True)
	if rc: print '    Found and removed {0:,} identical vertices leaving {1:,} vertices and {2:,} faces in {3:.4f} sec'.format(vb - vertices.Count, vertices.Count, new_meshGeo.Faces.Count, time() - timeb)
	else: print '    Mesh has no identical vertices found in {0:.4f} sec'.format(time() - timeb)
	# Compact mesh.
	timeb = time()
	mem_before = 1e-6*new_meshGeo.MemoryEstimate(); new_meshGeo.Compact(); mem_after = 1e-6*new_meshGeo.MemoryEstimate()
	valid = new_meshGeo.IsValid
	# Update vertex count.
	vcount = vertices.Count
	print '    Time to compact mesh = {0:.4f} sec which saved {1:.2f} MB and resulted in mesh with {2:,} vertices and {3:,} faces and is valid = {4}'.format(time() - timeb, mem_before - mem_after, vcount, new_meshGeo.Faces.Count, valid)
	# If the meshes had colored vertices, apply their vertex colors to the combined mesh.
	if valid:
		if has_colors:
			timeb = time()
			colors = Array.CreateInstance(Color, vcount)
			for i in xrange(vcount): colors[i] = p2c[vertices[i]]
			new_meshGeo.VertexColors.Clear(); new_meshGeo.VertexColors.SetColors(colors)
			print '    Time to apply vertex colors = {0:.4f} sec'.format(time() - timeb)
		# Add visible mesh to document.
		mesh = doc.Objects.AddMesh(new_meshGeo)
		print 'Total time to combine meshes = {0:.4f} sec'.format(time() - timea)
		return mesh
	else:
		print 'ERROR: Combined mesh is not valid'; return None