If I joint the meshes using my C++ DLL then all the vertices and faces are preserved:
Joined Mesh
The C++ code I am using is called from a Python script:
meshes = rs.ObjectsByType(32)
nums = len(meshes)
# Get geometry and serial numbers of all meshes.
cserial_numbers = (c_uint32 * (nums))()
cserial_number = c_uint32()
for i in range(nums):
mesh = meshes[i]
meshObj = doc.Objects.Find(mesh)
cserial_numbers[i] = meshObj.RuntimeSerialNumber
soObjImport.join_meshes(doc.RuntimeSerialNumber,
c_byref(cserial_numbers), c_byref(cserial_number), nums)
This is the heart of the C++ code:
for (int32_t i = 0; i < nums; i++) {
// Get each mesh from Rhino Doc using its serial number.
obj = pDoc->LookupObjectByRuntimeSerialNumber(serial_numbers[i]);
// Cast the object to a constant mesh object.
meshObj = CRhinoMeshObject::Cast(obj);
// Get mesh from mesh object.
mesh = meshObj->Mesh();
// Sum up number of vertices, textures, normals, faces and face normals
// in all meshes so capacities of joined mesh can be set.
num_V += mesh->m_V.Count();
num_C += mesh->m_C.Count();
num_T += mesh->m_T.Count();
num_N += mesh->m_N.Count();
num_F += mesh->m_F.Count();
num_FN += mesh->m_FN.Count();
}
time1 = chrono::steady_clock::now();
// Create new mesh for the joined meshes.
ON_Mesh* joined_mesh = new ON_Mesh();
// Set capacities for v/c/t/n/f/fn elements.
::parallel_invoke(
[&] { joined_mesh->m_V.SetCapacity(num_V); },
[&] { joined_mesh->m_C.SetCapacity(num_C); },
[&] { joined_mesh->m_T.SetCapacity(num_T); },
[&] { joined_mesh->m_N.SetCapacity(num_N); },
[&] { joined_mesh->m_F.SetCapacity(num_F); },
[&] { joined_mesh->m_FN.SetCapacity(num_FN); }
);
time2 = chrono::steady_clock::now();
// Create counters for number of v/c/t/n/f/fn added to joined mesh.
// These are used to offset the copy destination of elements in successive meshes.
int32_t sum_num_V = 0, sum_num_C = 0, sum_num_T = 0, sum_num_N = 0,
sum_num_F = 0, sum_num_FN = 0;
// Get pointer to faces list in joined mesh.
joined_faces = joined_mesh->m_F.Array();
// Copy meshes into joined mesh. For faces, offset the face vertex indices
// as each mesh is added. For all other elements, just add them to the end
// of their list in joined mesh.
for (int32_t i = 0; i < nums; i++) {
// Get each mesh from Rhino Doc using its serial number.
obj = pDoc->LookupObjectByRuntimeSerialNumber(serial_numbers[i]);
// Cast the object to a constant mesh object.
meshObj = CRhinoMeshObject::Cast(obj);
// Get mesh from mesh object.
mesh = meshObj->Mesh();
// Get number of elements of each type in this mesh.
num_V = mesh->m_V.Count();
num_C = mesh->m_C.Count();
num_T = mesh->m_T.Count();
num_N = mesh->m_N.Count();
num_F = mesh->m_F.Count();
num_FN = mesh->m_FN.Count();
// For first mesh, just copy over the faces without offsetting.
if (i == 0) { ::memcpy(joined_mesh->m_F.Array() + sum_num_F, mesh->m_F.Array(),
num_F * sizeof(ON_MeshFace)); joined_mesh->m_F.SetCount(sum_num_F + num_F); }
// For additional meshes, offset the vertex indices in each face.
else {
// Offset the face vertex indices of each mesh in the joined mesh
// by the accumulated number of vertices added.
offset = sum_num_V;
// Get pointer to faces list of this mesh.
faces = mesh->m_F.Array();
// Process each face of this mesh in npar parallel groups.
// Parallel runs in 0.5404 sec vs 0.966 sec in series for time3 - time2
int32_t Q1 = num_F / npar;
::parallel_for_each(a.begin(), a.end(), [&](int32_t j) {
//for_each(a.begin(), a.end(), [&](int32_t j) {
int32_t kstart = j * Q1, kend = kstart + Q1;
if (j == a.back()) kend = num_F;
for (int32_t k = kstart; k < kend; k++) {
// Get pointer to list of vertex indices in this mesh.
const int32_t* f_vi = faces[k].vi;
// Offset addition of face vertex indices to location
// of this mesh in joined mesh.
int32_t* jf_vi = joined_faces[k + sum_num_F].vi;
// Offset the 4 vertices in each face.
jf_vi[0] = f_vi[0] + offset;
jf_vi[1] = f_vi[1] + offset;
jf_vi[2] = f_vi[2] + offset;
jf_vi[3] = f_vi[3] + offset;
}
});
// Set number of faces in joined mesh.
joined_mesh->m_F.SetCount(sum_num_F + num_F);
}
//
// Combine mesh elements incrementally, updating count with each addition.
// This seemed to be more stable than updating count only after all meshes
// have been combined.
//
::parallel_invoke(
// Add vertices to joined mesh using memcpy.
[&] { ::memcpy(joined_mesh->m_V.Array() + sum_num_V, mesh->m_V.Array(), num_V * sizeof(ON_3fPoint)); joined_mesh->m_V.SetCount(sum_num_V + num_V); },
// Add colors to joined mesh using memcpy.
[&] { if (num_C > 0) { ::memcpy(joined_mesh->m_C.Array() + sum_num_C, mesh->m_C.Array(), num_C * sizeof(ON_Color)); joined_mesh->m_C.SetCount(sum_num_C + num_C); ::memset(&(joined_mesh->m_Ctag), 0, sizeof(joined_mesh->m_Ctag)); } },
// If texture coordinates are present, add textures to joined mesh using memcpy.
[&] { if (num_T > 0) { ::memcpy(joined_mesh->m_T.Array() + sum_num_T, mesh->m_T.Array(), num_T * sizeof(ON_2fPoint)); joined_mesh->m_T.SetCount(sum_num_T + num_T); } },
// If vertex normals are present, add vertex normals to joined mesh using memcpy.
[&] { if (num_N > 0) { ::memcpy(joined_mesh->m_N.Array() + sum_num_N, mesh->m_N.Array(), num_N * sizeof(ON_3fVector)); joined_mesh->m_N.SetCount(sum_num_N + num_N); } },
// If face normals are present, add face normals to mesh using memcpy.
[&] { if (num_FN > 0) { ::memcpy(joined_mesh->m_FN.Array() + sum_num_FN, mesh->m_FN.Array(), num_FN * sizeof(ON_3fVector)); joined_mesh->m_FN.SetCount(sum_num_FN + num_FN); } }
);
// Add this mesh's counts to v/c/t/n/f/fn totals. This is used to offset the storage of the elements from the next mesh in the joined mesh.
sum_num_V += num_V;
sum_num_C += num_C;
sum_num_T += num_T;
sum_num_N += num_N;
sum_num_F += num_F;
sum_num_FN += num_FN;
// Delete this mesh.
pDoc->DeleteObject(obj);
}
time3 = chrono::steady_clock::now();
//joined_mesh->Compact(); // These 2 lines used for bebugging.
//bool isvalid = joined_mesh->IsValid();
// Create attributes for mesh.
ON_3dmObjectAttributes attribs;
// Assign default values.
pDoc->GetDefaultObjectAttributes(attribs);
// Give mesh a name.
attribs.SetName(L"joined mesh", true);
// If layer found, put mesh on it.
if (mesh_layer_index > 0) { attribs.m_layer_index = mesh_layer_index; }
CRhinoMeshObject* meshObject = new CRhinoMeshObject(attribs);
meshObject->SetMesh(joined_mesh);
// This adds mesh only to heap.
if (!pDoc->AddObject(meshObject)) {
// Remove object from Heap if it was not added to doc.
delete meshObject;
meshObject = NULL;
}
// If successful, get the serial number of the mesh so it can be easily accessed in Python.
else { serial_number = (meshObject) ? meshObject->RuntimeSerialNumber() : 0; }
For your meshes, the C++ timings for joining them are:
Time to count v/c/t/n/f/fn in 2 meshes = 0.007173 sec
Time to set capacities for joined mesh = 0.000490 sec
Time to copy meshes into joined mesh = 0.000562 sec
Time to add joined mesh to document = 0.000104 sec
Total time to join meshes = 0.008331 sec
I created the C++ code a few years back because the Rhino Join command was so slow when I tried to combine large meshes; I waited 4.5 hours to join 50 meshes with 2M faces and did not get a result while the C++ code took only 4.5 sec. Apparently the Join command, as you have also discovered, has some issues.
Regards,
Terry.