SetTextureCoordinates - RhinoCommon + PY Script

Hi there,

Seems I’m stuck on the following topic. It looks like mesh is imported properly (vertices, normals, poly) though when I hit UVs nothing seems to work here:

mesh = Rhino.Geometry.Mesh();

for pt in extMData.points:
    mesh.Vertices.Add(pt[0],pt[1],pt[2])

for nrm in extMData.normals:
    mesh.Normals.Add(nrm[0],nrm[1],nrm[2])

for poly in extMData.polygons:
    if len(poly) == 3:
        mesh.Faces.AddFace(poly[0],poly[1],poly[2])
    elif len(poly) == 4:
        mesh.Faces.AddFace(poly[0],poly[1],poly[2],poly[3])
    else:
        print("Other poly not supported")

mesh.Normals.UnitizeNormals();

# Create a list to store UV coordinates for each vertex
uv_coordinates = []

# Populate the UV coordinates list
for uv in extMData.uvs:
    uv_coordinates.append(Rhino.Geometry.Point2f(uv[0], uv[1]))

# Set UV coordinates for each face of the mesh
for i in range(mesh.Faces.Count):
    # Get the vertex indices of the current face
    face = mesh.Faces[i]

    # Create a list to hold the vertex indices
    vertex_indices = []
    
    # Append the vertex indices of the current face to the list
    vertex_indices.append(face.A)
    vertex_indices.append(face.B)
    vertex_indices.append(face.C)
    
    if face.IsQuad:
        vertex_indices.append(face.D)

    # Assign UV coordinates to each vertex of the face
    for j, vertex_index in enumerate(vertex_indices):
        mesh.TextureCoordinates.Add(uv_coordinates[vertex_index])

print("Now there are " + str(mesh.TextureCoordinates.Count) + " texcoords")

#SORRY FOR CHANGE BUT THIS SEEMS TO BE CORRECT THAT IT MATCHES NOW INITIAL UVs PAIRS COUNT - BUT STILL only color on object
#OR simpler aproach - though UVs has 200k pairs while mesh only 64k verts - ANYWAY this isn't working as after aplying material seems it jsut changes color/texture is not recognizable
#mesh.TextureCoordinates.SetTextureCoordinates(uv_coordinates)

mesh.TextureCoordinates.NormalizeTextureCoordinates();
mesh.Compact()

#Change Y-Up to Z-Up
mesh.Rotate(math.radians(90),Rhino.Geometry.Vector3d(1,0,0),Rhino.Geometry.Point3d.Origin)

sc.doc.Objects.AddMesh(mesh)
sc.doc.Views.Redraw()

As far as I found uvs are provided in a such way uvs 0,1,2,3 corresponds to verts 0,1,2,3 BUT if faces are joined then next one 4567 seems to be redundant in two points (?)

Any ideas or just my head is tired that much I’m missing something obvious ?

Hi D-W

I’m not sure I understand the exact issue you are running into.
But before you elaborate on that this info might help:

In Rhino a mesh can be defined by 4 lists
3 lists of equal length for vertices, normals per vertex and uv-coordinates per vertex.

1 list of Faces each face is a list of 3 or 4 indices referencing an index within the above 3 lists.

Hope this makes sense so far.

2 or more faces can share indices of the vertex normal and uv
if 2 adjacent faces have a common edge they can share 2 indices. However if that edge is “unwelded”, the indices are unique, as at least the normal of a un unwelded vertex differs per face.

Does this make sense so far?

-Willem

Yes perfectly. Even though I’ve hit an AHA moment that unwelding mesh will result in what I need to deliver but seems this isn’t enough. Now finally I have texture on mesh instead of color but everything seems to be mixed as the texture doesn’t look right.

Can you elaborate on what this extMData is ?
Where does it come form and are you sure the data structure really is like you think it is?

While looking at your example I now see that you Add the texture coordinates within the looping through the Faces.
That seems not correct to me.
How about of you discard any Adding of texturecoordinates within the Face loop.
Just add a line like this after populating the uv_coordinates:
mesh.TextureCoordinates.SetTextureCoordinates(uv_coordinates)

I see you commented that out so it might be I’m wrong.

extMData is ok, well I guess I don’t have to keep this secret, I’m trying to get Laubwerk Plants into Rhino in general so far there was an issue due to CPython need for their lib. Now in R8 it seems to work as expected so I’m trying to do first steps to make some import. There is also great source to follow (blender plug to import called Thicket so reviewing thicket/thicket_lbw.py at 230e96c041e0cf8392ab83bbe6560faf056dad26 · Thicket-Blender/thicket · GitHub I’m sure this just work out of the box)

Yes in general now after unwelding and setting directly whole UVs in one shot is doing the same as face loop. Though result is:

If you skip this, maybe that messes up the texture,

It seems some faces are mapped correctly these might fall into the 0,1 range already.

Yes, I thought this or mesh Compact or any additional calls are impacting stuff during the run but no change, I’m thinking that Unweld does something with the order of vertices, I mean they just don’t break apart in place but are added at the end of vertices list so the order is not matched here. I think without knowing internals here I’m blind, so I guess making a “MAYDAY” call to @Jussi_Aaltonen or @nathanletwory will be excused.

Looks like this is the key here, so those vertices should be skipped when setting UVs OR unweld and find the correct order. Both seem to be painful.

At what point do you Unweld?

Maybe you can make a json of pickle of example extMData so we can test the isolated piece of code.

My thinking is that your code like this would be the starting point to test:

extMData = ....

mesh = Rhino.Geometry.Mesh();

for pt in extMData.points:
    mesh.Vertices.Add(pt[0],pt[1],pt[2])

for nrm in extMData.normals:
    mesh.Normals.Add(nrm[0],nrm[1],nrm[2])

for poly in extMData.polygons:
    if len(poly) == 3:
        mesh.Faces.AddFace(poly[0],poly[1],poly[2])
    elif len(poly) == 4:
        mesh.Faces.AddFace(poly[0],poly[1],poly[2],poly[3])
    else:
        print("Other poly not supported")

# Create a list to store UV coordinates for each vertex
uv_coordinates = []
# Populate the UV coordinates list
for uv in extMData.uvs:
    mesh.TextureCoordinates.Add(Rhino.Geometry.Point2f(uv[0], uv[1]))

sc.doc.Objects.AddMesh(mesh)
sc.doc.Views.Redraw()

Also using a test texture might also give some good insight of possible shifts in the mapping.

Mhm, and I have to Unweld it just before extMData.uvs loop is the only way to see any texture in the viewport, otherwise, when I tried after this loop it was just set as one faint color.

Yup. You’re right about the map, have a couple of those too, thanks for sharing this one (never too much of those) but it doesn’t look promising :sweat_smile:
image

Unwelding prior to setting UVs sounds wrong to me.

Have you checked with _UVEditor what the UVs look like to begin with?

Thought the same however if I set first entire UVs - usually more than double amount of original verts and opening uveditor I can see nothing I mean nothing is shown (Ctrl+A selects also nothing)

Just to confirm earlier considerations - I’ve also lowered initial mesh params to deal with less data for testing this is the “print” diagnosis, read from original, vert ids set in mesh b4 unweld and “is now” after unweld:

EDIT:

Ok so now with simplified mesh data to 41k verts it doesn’t want to create MeshObject - returns 000… Guid - just to be clear if, no uvs added or I just iterate over verts and add as much coords as verts it creates the object.

SO, to summarize, if all original coords are provided to mesh it fails to create, the only way to get a matching number of verts and uv coords is to Unweld, but as it messes up verts ids order thus textures are broken.

You’ll probably have to figure out how the UVs are mapped and then adapt your faces and vertices such that they are unique.

Consider this example of a simple cube. On the left side is the UV. Perhaps not visible in this image but there are tiny margins between each uv island, they are not connected.

Exported to OBJ that is

# Blender 4.0.1
# www.blender.org
o Cube
v -1.000000 -1.000000 1.000000
v -1.000000 1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v -1.000000 1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v 1.000000 1.000000 1.000000
v 1.000000 -1.000000 -1.000000
v 1.000000 1.000000 -1.000000
vn -1.0000 -0.0000 -0.0000
vn -0.0000 -0.0000 -1.0000
vn 1.0000 -0.0000 -0.0000
vn -0.0000 -0.0000 1.0000
vn -0.0000 -1.0000 -0.0000
vn -0.0000 1.0000 -0.0000
vt 0.332332 0.001001
vt 0.001001 0.001001
vt 0.001001 0.332332
vt 0.332332 0.332332
vt 0.332332 0.334334
vt 0.001001 0.334334
vt 0.001001 0.665666
vt 0.332332 0.665666
vt 0.665666 0.001001
vt 0.334334 0.001001
vt 0.334334 0.332332
vt 0.665666 0.332332
vt 0.665666 0.334334
vt 0.334334 0.334334
vt 0.334334 0.665666
vt 0.665666 0.665666
vt 0.332332 0.667668
vt 0.001001 0.667668
vt 0.001001 0.998999
vt 0.332332 0.998999
vt 0.998999 0.001001
vt 0.667668 0.001001
vt 0.667668 0.332332
vt 0.998999 0.332332
s 0
f 1/1/1 2/2/1 4/3/1 3/4/1
f 3/5/2 4/6/2 8/7/2 7/8/2
f 7/9/3 8/10/3 6/11/3 5/12/3
f 5/13/4 6/14/4 2/15/4 1/16/4
f 3/17/5 7/18/5 5/19/5 1/20/5
f 8/21/6 4/22/6 2/23/6 6/24/6

You’ll see that there are 24 UV coordinates, but only 8 vertices, and only 6 faces. You’ll essentially have to extrapolate vertices and faces to be unique, but still match UVs.

I’m guessing this is what you’re seeing in your case.

You might want to query the mesh instance for validity and have a log from that query as well so you can print it out and see what the issue is:

y, l = me.IsValidWithLog()
print(y, l)

Happy hunting (:

Mhm :upside_down_face: Yup I know that. Exactly, that’s I don’t understand why this doesn’t work as it looks like it’s really thought insideout. I ran their “obj writer” example and I got perfect result after importing it:

while the example code is:

    # write texture vertices
    for uv in mesh.uvs:
        objfile.write("vt " + float_to_string(uv[0]) + " " + float_to_string(1.0 - uv[1]) + " 0\n")

So this again makes me feeling more stupid in this entire case :crazy_face:

Output is quite obvious:
False ON_Mesh.m_T.Count() = 86194 (should be 0 or 41272=vertex_count).

I don’t need to actually as those has also mesh.texverts [A,B,C,D] so in theory this should be sufficient but:


so they don’t match the face ids… but the initial verts :open_mouth:

END EDIT:
I think here the best question to ask is how internally mesh is built when dealing with OBJ import, as if this works when writing to OBJ and _-Import then I’m reinventing the wheel here as exported obj has the exact amount of verts and a higher amount extrapolated uvs as in the script, written to OBJ in the same exact order. I guess @Jussi_Aaltonen could chime in here.

OFC I can do a workaround to write obj and the import (far easier) though it rather seems suboptimal at least.

@dale could you let me know if you have any knowledge about OBJ handling when on _Import is called (regarding UVs ofc), the whole thing is surely done on unmanaged side and not accessible but it never hurts to ask.

Hi, sorry I don’t understand the question. Meshes in Rhino always have same number of vertices and texture coordinates.

@Jussi_Aaltonen ok simplifying:

  • imported mesh have 41272 verts, while available uv coords 86194
  • after calling Unweld we have matching number verts and uv coords (but unweld messes vert order)
  • having same data but first writing it to OBJ format and _Import ing it results in correct mapping of less verts than uvcoords

The question: What is the magic behind OBJ _Import that even with lower vert count, higher uv coords count maps correctly?

Is there then own indexing for UV and vertex data?

@Jussi_Aaltonen own ? After _Import, no it is as imported it includes UVs already, BUT the OBJ file has same amount of verts (here 41272) and same amount extrapolated uv coords (here 86194). Order of verts, polys, and uv coords is ready out of the box.

Hi ,

I’m reading along out if interest and want to make sure we are all talking about the same issue.

@D-W
You have a data set coming from an external source that represents a mesh with uv coordinates.
The structure of that dataset is a collection of arrays with

  • “points”(vertrices)
  • “normals”
  • “polygons”(faces by index numbers)
  • “uvs”

That same data set, you can export as OBJ and that does import correct in Rhino.

However when you try to parse the dataset yourself into a RhinoMesh the texturing fails.

Reconstructing the faces works as expected using the points and normals by their index, however if you assign uv’s “to these faces” there is a mismatch.

Is that a good summary?

Would you be able to create a small data set much like the cube example above:

What I see in the OBJ structure is a mapping of vertices and uv’s to faces where the indices do not “line up”.
Each face corner has 3 indices mapping the vertex, normal and uv.
That is a fundamental difference from how Rhino does this, where a single index represents the vertex, the normal and the uv-coordinate. In Rhino all 3 arrays need to be of equal length.

So I expect that the OBJ importer in Rhino does a mapping of these faces with separate indices for vert/normal/uv to a new array’s where vertices and normal are duplicated to match with all the unique uv’s.
The faces are reconstructed, mapping from the old index values to the new ones with equal index for all 3 properties,

Does this makes sense?

HTH
-Willem

Well yes, I’d say perfect. One thing which could be misinterpreted is: assign us’s “to these faces”, I exactly tried that approach but at the end of the day it turned out that adding all coords at once gives same result.

I’ll try however it can be problematic to be “small”. This 40k was already at very low params, however, I’ll do my best - if “small” won’t be that small I’ll just put it on wetransfer. Will that be ok?