here is some c# people can play with, just paste it into Tools > Script > Edit
it joins meshes of selected objects (mesh or not, with edge softening but not other stuff at this point) and adds custom object mappings to the result to retain the uvs of their mapping channels:
using System;
using System.Collections.Generic;
using System.Linq;
using Rhino;
using Rhino.Render;
using Rhino.Geometry;
using Rhino.DocObjects;
//=============================================================================
// Helpers.
//=============================================================================
#region
void WriteInfo(string format, params object[] args)
{
RhinoApp.WriteLine($"[INF] " + string.Format(format, args));
}
void WriteError(string format, params object[] args)
{
RhinoApp.WriteLine($"[ERR] " + string.Format(format, args));
}
void GetSoftening(RhinoObject obj, out bool enabled, out double softening, out bool chamfer, out bool faceted, out double threshold, out bool force)
{
// Guessed all but the "faceted" option, then finally found https://github.com/mcneel/opennurbs/blob/8.x/opennurbs_mesh_modifiers.h.
var id = CustomRenderMeshProvider2.EdgeSofteningId;
enabled = Convert.ToBoolean(obj?.GetCustomRenderMeshParameter(id, "on") ?? false);
softening = Convert.ToDouble(obj?.GetCustomRenderMeshParameter(id, "softening") ?? 0.0);
chamfer = Convert.ToBoolean(obj?.GetCustomRenderMeshParameter(id, "chamfer") ?? false);
faceted = Convert.ToBoolean(obj?.GetCustomRenderMeshParameter(id, "unweld") ?? false);
threshold = Convert.ToDouble(obj?.GetCustomRenderMeshParameter(id, "edge-threshold") ?? 5.0);
force = Convert.ToBoolean(obj?.GetCustomRenderMeshParameter(id, "force-softening") ?? false);
}
void SetSoftening(RhinoObject obj, bool enabled, double softening, bool chamfer, bool faceted, double threshold, bool force)
{
var id = CustomRenderMeshProvider2.EdgeSofteningId;
obj?.SetCustomRenderMeshParameter(id, "on", enabled);
obj?.SetCustomRenderMeshParameter(id, "softening", softening);
obj?.SetCustomRenderMeshParameter(id, "chamfer", chamfer);
obj?.SetCustomRenderMeshParameter(id, "unweld", faceted);
obj?.SetCustomRenderMeshParameter(id, "edge-threshold", threshold);
obj?.SetCustomRenderMeshParameter(id, "force-softening", force);
}
Mesh ChooseMesh(RhinoObject obj, Mesh objMesh)
{
// Here, we'll see if we should try to use the softened version of this mesh.
GetSoftening(obj,
out bool softeningEnabled,
out double softening,
out bool chamfer,
out bool faceted,
out double threshold,
out bool force
);
var mesh = softeningEnabled
? objMesh.WithEdgeSoftening(softening, chamfer, faceted, force, threshold)
: objMesh;
if (!mesh.IsValidWithLog(out string meshLog))
{
if (softeningEnabled) // Been seeing invalid normals & vertexes.
{
mesh.Normals.UnitizeNormals();
mesh.Faces.CullDegenerateFaces();
mesh.Vertices.CullUnused();
}
if (!mesh.IsValidWithLog(out meshLog))
{
WriteError($"Mesh is invalid: {meshLog}");
mesh = objMesh;
}
}
return mesh;
}
#endregion
//=============================================================================
// Join meshes.
//=============================================================================
#region
var doc = RhinoDoc.ActiveDoc;
var objects = doc.Objects.GetSelectedObjects(false, false).ToList();
if (objects.Count < 1)
{
WriteError($"No objects are selected.");
return;
}
// Need to run this to force m_T to be updated.
RhinoApp.RunScript($"_testfillinlegacytexturecoordinates", true);
// The final joined mesh, and a map of materials to # of objects using them.
var joined = new Mesh();
var materials = new Dictionary<RenderMaterial, int>();
// We need to create a custom object mapping for the final mesh, and it will
// need to have enough channels to cover all the selected meshes.
var nChannels = 0;
foreach (var obj in objects)
if (nChannels < obj.GetTextureChannels().Length)
nChannels = obj.GetTextureChannels().Length;
// This is the main joining together of the selected object meshes.
foreach (var obj in objects)
{
if (obj.GetRenderMaterial(true) is RenderMaterial rm)
if (!materials.ContainsKey(rm))
materials[rm] = 0;
else
materials[rm] += 1;
foreach (var objMesh in obj.GetMeshes(MeshType.Render))
{
var mesh = ChooseMesh(obj, objMesh);
var offset = joined.Vertices.Count;
foreach (var f in mesh.Faces)
joined.Faces.AddFace(f.A + offset, f.B + offset, f.C + offset, f.D + offset);
if (mesh.Vertices.Count > 0)
joined.Vertices.AddVertices(mesh.Vertices);
if (mesh.Normals.Count > 0)
joined.Normals.AddRange(mesh.Normals.ToArray());
if (mesh.VertexColors.Count > 0)
joined.VertexColors.AddRange(mesh.VertexColors);
if (mesh.TextureCoordinates.Count > 0)
joined.TextureCoordinates.AddRange(mesh.TextureCoordinates.ToArray());
}
}
// Check the result and add to the document.
var meshOK = joined.IsValidWithLog(out string log);
if (!meshOK)
{
WriteError($"Mesh is invalid: {log}");
return;
}
var meshID = doc.Objects.Add(joined);
if (meshID == Guid.Empty)
{
WriteError($"Failed to create mesh.");
return;
}
var meshObject = doc.Objects.Find(meshID);
if (meshObject == null)
{
WriteError($"Failed to retrieve mesh object.");
return;
}
// Apply the most-used material.
if (materials.Count > 0)
doc.Objects.ModifyRenderMaterial(
meshObject.Id, materials.MaxBy(x => x.Value).Key
);
#endregion
//=============================================================================
// Custom mappings.
//=============================================================================
#region
// For each custom mapping channel we need a copy of our mesh. It will come
// m_T already filled, and we'll just overwrite any tex coords we get.
var mappings = new List<Mesh>();
for (int i = 0; i < nChannels; ++i)
mappings.Add(joined.Duplicate() as Mesh);
// For each channel we'll go thru and get custom mapping coords for it from all
// our object meshes.
for (int iChannel = 0; iChannel < nChannels; ++iChannel)
{
var tcoords = mappings[iChannel].TextureCoordinates.ToArray();
var offset = 0;
foreach (var obj in objects)
{
var mapping = obj.GetTextureMapping(iChannel+1);
foreach (var objMesh in obj.GetMeshes(MeshType.Render))
{
var mesh = ChooseMesh(obj, objMesh);
var nVerts = mesh.Vertices.Count;
if (obj.GetTextureChannels().Contains(iChannel+1))
{
var coords = mesh.GetCachedTextureCoordinates(mapping.Id);
if (coords == null)
{
var xf = Transform.Identity;
mesh.SetCachedTextureCoordinates(mapping, ref xf);
coords = mesh.GetCachedTextureCoordinates(mapping.Id);
}
for (int j = 0; j < coords?.Count; ++j)
if (j + offset < tcoords.Length) // Sanity.
tcoords[j + offset] = new Point2f((float)coords[j].X, (float)coords[j].Y);
}
offset += nVerts;
}
}
mappings[iChannel].TextureCoordinates.SetTextureCoordinates(tcoords);
}
// With our custom mapping meshes done, apply them to our final mesh object.
for (int i = 0; i < mappings.Count; ++i)
meshObject.SetTextureMapping(i+1,
TextureMapping.CreateCustomMeshMapping(mappings[i])
);
#endregion
//=============================================================================
// Done.
//=============================================================================
RhinoApp.RunScript($"_selnone _sellast ", true);