Rhino 8 - Joining mesh removes random vertices

Hi all,

I’m adding another mesh bug in Rhino 8.

Some vertices are randomly deleted when joining two meshes, this did not happen in Rhino 7. Sometimes the _Weld command can lead to a similar outcome.

This error might be related to these ones:

  • RH-81661 Random vertices are getting deleted
  • RH-81662 Why ngons are created instead of holes

Rhino7

Rhino 8

Mesh Error 02.3dm (148.9 KB)
Mesh Error 02 R7.3dm (148.4 KB)

Hi Gonzalo- thanks, I am looking it now.

RH-81696 Join - deletes mesh vertices/faces

-Pascal

1 Like

I uploaded your Mesh Error 02.3dm file into my Rhino 8, selected the 2 meshes and typed Join in the Command window and get a result that looks like your Rhino 8 video. There are some vertices being remove on the far right of the smaller mesh after the join as shown below.
Before Join:


After Join:

So this confirms that this is happening not just on your system.

Regards,
Terry.

1 Like

Thanks guys, I’ll let you know if you find anything else.

Regards,
Gonzalo.

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.