Retaining joined meshes texture coordinates - how?

I have 2 mesh objects with custom planar mapping applied to each of them (same material).
When I join them, the mapping gets messed up. How do I make the resulting mesh vertices to remember the original texture coordinates?

Or - how to embed/bake the custom texture coordinates applied via mapping widget to the mesh vertices?

Any help would be much appreciated. Thank you

1 Like

Answering my own question for now - the workaround I found is to export the meshes to *.obj format and bring them back - the texture coordinates are ā€˜baked inā€™ then.
I wonder if there is a way in Rhino to do it without resorting to this trickā€¦

I donā€™t think there is but agree it would be useful. @andy do you know?

Hi @BrianJ, I think it would make sense to be a sticky option with ExtractRenderMesh commandā€¦ at least I would find it very useful and logical. thanks

Agreedā€¦ I filed this feature request for future reference http://mcneel.myjetbrains.com/youtrack/issue/RH-29942

1 Like

great - thanks Brian

Any progress on this issue? Rhino 7, but still the same problem.

Hello - you can check the bug track item linked above to track progress, if any, on any public bugā€¦ this one seems not to have been addressed yetā€¦

-Pascal

Bump, we constantly run into problems with this, when extracting render meshes from multiple objects that have custom UV mapping and joining them into one, the mapping gets all messed up. Workaround with having to export them as OBJ and import back as the only way to have UVā€™s baked and join properly is cumbersome. A new command or perhaps just an option in -ExtractRenderMesh to ā€œbakeā€ the MapUVs (just like Rhino does it for OBJ export) would be very helpful to have. Thanks

2 Likes

Just adding my voice here. Joining meshes messes up original texture coordinates, still having to export to obj and import back in which is pretty cumbersome. I know Rhino isnā€™t blender, but being able to join meshes would be really useful.

A few years on now and still I canā€™t join two meshes and not mess up the texture mappings with custom coordinates. Is there any hope here for Rhino 8? Thanks! John.

tex

3 Likes

Can we please get this feature soon. Itā€™s crucial when working with meshes, and using realtime renderers like Unreal via plugins like Datasmith. You sometimes want to combine meshes into one so that itā€™s easier to use in UE, for various reasons, but you want that combined mesh to combine the material IDs of everything youā€™ve combined. Iā€™m sure other RT renderers work in a similar way. And most, if not all DCCs have had this feature forever. Itā€™s a crucial part of the polygonal modelling workflow. Now I understand Rhino is focused on NURBS, but polygonal modelling features are becoming more and more important in Rhino, e.g., SubD, texture mapping, adding low quality props to the scene.

Thanks,

Aidan

3 Likes

Yup, please, it would be a great feature indeed, thanks!

When joining textured mesh objects the only mapping type that will produce the expected result is the surface mapping. In order to use surface mapping the mesh needs to have its texture mapping baked into its per-vertex surface parameters. This can be done using the SetMeshSurfaceParameters command in Rhino 8. Because each mesh has only one set of surface parameters only 1 texture mapping channel can be baked.

So the steps to join 2 meshes together maintaining the appearance of both texture mappings is:

  1. Run SetMeshSurfaceParameters on both meshes and select the mapping channel you want to bake.
  2. Run ApplySurfaceMapping on both mesh objects to change the texture mapping to a surface mapping.
  3. Run Join to join the meshes.
3 Likes

@Jussi_Aaltonen wouldnā€™t it be possible to just put each mesh texture coordinate set into a new mapping channel on the final mesh?

What do you mean by ā€˜texture coordinate setā€™? Texture coordinate sets created by mapping channels?

Yes, each mesh will have its own coordinates in one mapping channel in the final result. Obviously users may have to adapt their materials for that, but at least original texture coordinates are still available.

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);
3 Likes

It is a bug, not a feature. The mapping feature is not working as expected.

https://mcneel.myjetbrains.com/youtrack/issue/RH-53858/Join-does-not-conserve-individual-surface-UV-mapping

That would work if mesh objects could have multiple materials.