Copying UV data from one map to another is getting near-accurate, but not correct, results

I’m trying to copy the UV map coordinates from one map to another on the same object- my end goal is to export these UV coordinates into a text file. I have three UV maps on my object, at the moment. Using code lifted directly from the documentation, I’m able to get data from all three maps, store it in three dictionaries with vertice indices as the keys, and replace data on Map 0 with data from Map 1. Kind of. Here’s a zoomed-in portion of Map 0:

Here's a zoomed-in portion of Map 1:

The distortion is intentional, by the way.

Here’s what I get when I copy the data from Map 1 to Map 0 by vertex index:

It’s close, it’s really close to accurate, but it’s not. Most of the vertices are in the right place, but quite a few are in the wrong place, and this is my problem.

Here’s my code- again, it’s almost word for word exactly from the documentation:

import bpy

class Globals:
    me = bpy.context.object.data
    uv_layer0 = me.uv_layers[0].data
    uv_layer1 = me.uv_layers[1].data
    uv_layer2 = me.uv_layers[2].data
    uv_data0 = {}
    uv_data1 = {}
    uv_data2 = {}
g = Globals()

def SaveUVData():
    
    for poly in g.me.polygons:
        # range is used here to show how the polygons reference loops,
        # for convenience 'poly.loop_indices' can be used instead.
        for loop_index in range(poly.loop_start, poly.loop_start + poly.loop_total):
            g.uv_data0[g.me.loops[loop_index].vertex_index] = g.uv_layer0[g.me.loops[loop_index].vertex_index].uv
            g.uv_data1[g.me.loops[loop_index].vertex_index] = g.uv_layer1[g.me.loops[loop_index].vertex_index].uv
            g.uv_data2[g.me.loops[loop_index].vertex_index] = g.uv_layer2[g.me.loops[loop_index].vertex_index].uv
        
SaveUVData()
def UVDataTransfer():

    for poly in g.me.polygons:
        for loop_index in range(poly.loop_start, poly.loop_start + poly.loop_total):
            print(g.uv_layer0[g.me.loops[loop_index].vertex_index].uv == g.uv_data0[g.me.loops[loop_index].vertex_index])
            g.uv_layer0[g.me.loops[loop_index].vertex_index].uv = g.uv_data1[g.me.loops[loop_index].vertex_index]
            
UVDataTransfer()     

This line makes things especially puzzling:

print(g.uv_layer0[g.me.loops[loop_index].vertex_index].uv == g.uv_data0[g.me.loops[loop_index].vertex_index])

It verifies that the data in the uv_data dictionaries is being stored correctly- that is to say, it matches the data on the uv_layers correctly. My stored UV data for Map 0 is the same as the actual data on Map 0.

Running this code returns a long list of Trues and no Falses. If the data is being stored correctly, it must not be being transferred from map to map correctly, but I have no idea why. The relevant line that transfers is this one:

 g.uv_layer0[g.me.loops[loop_index].vertex_index].uv = g.uv_data1[g.me.loops[loop_index].vertex_index]

Looking at the screenshots, it seems like some vertices are just being skipped, but I can’t imagine why this would be.

Any help is infinitely appreciated :slight_smile:

you’re caching references to the UV’s vector property itself, not the UV’s value. In other words- your second loop is changing the data of your dictionary directly.

a quick repro example to show you what I mean:

in your SaveUVData function, copy the vectors into the dictionary with .uv.copy()

2 Likes

Thanks a billion for your reply and suggestion! Do I need to change anything in the second function or is that fine?

shouldn’t need to do anything with the second function, it’s only reading from the dictionary and not writing anything

1 Like

I added .copy() to the end of each of these statements in SaveUVData:

g.uv_data0[g.me.loops[loop_index].vertex_index] = g.uv_layer0[g.me.loops[loop_index].vertex_index].uv.copy()
            g.uv_data1[g.me.loops[loop_index].vertex_index] = g.uv_layer1[g.me.loops[loop_index].vertex_index].uv.copy()
            g.uv_data2[g.me.loops[loop_index].vertex_index] = g.uv_layer2[g.me.loops[loop_index].vertex_index].uv.copy()

I’m still seeing the same result though :thinking:

interesting, must be something I missed in your first post. when I’m back at a computer I’ll take another look if somebody hasn’t chimed in by then

1 Like

Sounds good, thank you :slight_smile:

Some notes based on Python classes and objects. Just to double check that you enforce as much as strict typing as you possibly can youself.

import bpy

class Person:
    # static class properties
    # are defined in the class scope
    staticname = 'Bond'

    def __init__(self):
        # class properties are defined with self.zzz
        # in the method scope
        self.name = ''

p1 = Person()
p2 = Person()

p1.name = 'Jim' # setting the property -per- object
p2.name = 'Tim'

print(p1.name, p1.staticname) # compare method property
print(p2.name, p2.staticname) # to static class property


# declaring a static class property
class Globals:
    something = 'blenderartists.org'
    
# initialize a Globals object instance
g = Globals()

# 'Globals' class contains only static members
# 'g' initialization is optional
print(Globals.something)
print(g.something)
assert(type(g) is Globals)

# though if you create the 'g' variable instance
# it can be used as a really handy shortcut
# to prevent you from writing 'Globals.zzz' all the time
# g = Globals()

# but in other way around in terms of design and conceptually
# you are much safer to "lock" your code in much more strict
# terms and refer directly to class as a static reference
gg = Globals 

# in this case you do this but in a more predictable way
# (because you know that 'gg' wont be instantiated or copied anywhere)
assert(type(gg) is not Globals)
assert(type(gg) is type)
print(gg.something)

1 Like

Thank you for this information :slight_smile: I tried changing

g = Globals

this, which didn’t change my results, and then I added the

def __init__(self):
...

to Globals, which also didn’t change my results. Do you have any idea what’s wrong with my code functionally?

1 Like

I think it’s better to sidestep polygons and use the foreach_get/set methods in uv.data.

Something like this:

import bpy

class Globals:
    me = bpy.context.object.data
    num_loops = len(me.loops)
    uv_layer0 = me.uv_layers[0].data
    uv_layer1 = me.uv_layers[1].data
    uv_layer2 = me.uv_layers[2].data
    uv_data0 = [0] * num_loops * 2  # Loops times uv dimensions
    uv_data1 = [0] * num_loops * 2
    uv_data2 = [0] * num_loops * 2
g = Globals()

def SaveUVData():
    g.uv_layer0.foreach_get("uv", g.uv_data0)
    g.uv_layer1.foreach_get("uv", g.uv_data1)
    g.uv_layer2.foreach_get("uv", g.uv_data2)

SaveUVData()
def UVDataTransfer():
    g.uv_layer0.foreach_set("uv", g.uv_data1)

UVDataTransfer()
2 Likes

Wow! This code works! I have no idea why this code works and why polygons didn’t, but I really appreciate it :slight_smile: Thank you!

EDIT: I forgot something pretty important- how can I export the data this way to a text file and reimport it? @iceythe

1 Like

You’re keying the dictionary using vertex indices, which are but a fraction of the total mesh loops.
It’s likely you’re overwriting dictionary entries.


You can verify this in SaveUVData with something like:

        for loop_index in range(poly.loop_start, poly.loop_start + poly.loop_total):
            index = g.me.loops[loop_index].vertex_index
            assert index not in g.uv_data0

            g.uv_data0[index] = g.uv_layer0[index].uv.copy()
            g.uv_data1[index] = g.uv_layer1[index].uv.copy()
            g.uv_data2[index] = g.uv_layer2[index].uv.copy()

Does the data need to be readable, or is binary format fine?
For the latter, you can use the pickle module. It marshals any python primitive data (ints, floats, dicts, tuples, lists) and retains the structure even if you have something like a list of lists.

import pickle

# Write to file
with open("data.bin", "wb") as file:  # "wb" means to write bytes
    pickle.dump(data, file)

# Read from file
with open("data.bin", "rb") as file:  # "rb" means to read bytes
    data = pickle.load(file)

If you need it to be text, I guess something like the json also works.
The api is identical to pickle (dump, load, dumps, loads).

2 Likes

:man_facepalming:t3: I totally spaced that pickle exists, that’s exactly what I need. It’s too early to think clearly :sweat_smile: thanks again, you’re a lifesaver!

1 Like