Convert Face Corner attribute to UV Map?

I am currently in a desperate need to output a vector attribute from Geometry Nodes into a UV map, but I just can not find a way.

Basically, I have generated tree roots with UV mapping along the root shape. Something that’s impossible to recreate once I apply the GN modifier. I can not use the Shader Editor attribute workaround as it’s a model intended to be exported outside of Blender.

So I am curious if there’s at least any python workaround to do so? I’ve read somewhere that MLoopUV is not accessible from Python, but maybe that changed recently?

1 Like

So I actually did it, but it’s a horrible noob python code:

class OBJECT_OT_ConvertAttributeUVMap(bpy.types.Operator):
"""Converts the mesh attribute called "UVMap" to an actual UV Map of the same name"""
bl_idname = "object.convert_attribute_uvmap"
bl_label = "Convert UVMap attribute to UV Map"

@classmethod
def poll(cls, context):
    return len(bpy.context.selected_objects) > 0

def execute(self, context):
    new_uv = bpy.context.active_object.data.uv_layers.new(name='NewUV')
    uv_coords = []

    for v_attrib in bpy.context.active_object.data.attributes['UVMap'].data:
        uv_coords.append(v_attrib.vector)

    for loop in bpy.context.active_object.data.loops:
        new_uv.data[loop.index].uv[0] = uv_coords[loop.index][0]
        new_uv.data[loop.index].uv[1] = uv_coords[loop.index][1]
    return {'FINISHED'}

I wonder if someone who is not python-disabled like me could actually help me figure out how to do this properly using foreach_get and set.

This could be really useful for not just me, but all the people who want to use GN to generate models with texture maps usable outside of Blender.

2 Likes

So here’s slightly more sophisticated (but still a bit crappy) version:

class OBJECT_OT_ConvertAttributeUVMap(bpy.types.Operator):
"""Converst first found face corner attribuet to UVMap"""
bl_idname = "object.convert_attribute_uvmap"
bl_label = "Convert fist face corner attribute to UV Map"

@classmethod
def poll(cls, context):
    return context.active_object is not None

def execute(self, context):
    source_name = context.active_object.name

    # Duplicate active object
    bpy.ops.object.duplicate()

    # Collapse existing modifiers on the mesh object
    bpy.ops.object.convert(target='MESH')

    # Store and name the duplicated object
    obj = context.active_object
    obj.name = source_name + "_Baked"

    # Create UVMap UV layer if there's none present
    if obj.data.uv_layers.find("UVMap") == -1:
        uvmap = obj.data.uv_layers.new(name="UVMap")
    else:
        uvmap_index = obj.data.uv_layers.find("UVMap")
        uvmap = obj.data.uv_layers[uvmap_index]

    # UV attribute to store
    uv_attrib = None

    # Try to find valid face corner attribute
    for attribute in obj.data.attributes:
        if attribute.domain == 'CORNER':
            uv_attrib = attribute

    # Check if face corner attribute was found
    if uv_attrib is None:
        if context.active_object is obj:
            bpy.ops.object.delete()
        self.report({'ERROR'}, 'No Face Corner attribute found!')
        return {'CANCELLED'}

    for loop in obj.data.loops:
        uvmap.data[loop.index].uv[0] = uv_attrib.data[loop.index].vector[0]
        uvmap.data[loop.index].uv[1] = uv_attrib.data[loop.index].vector[1]

    obj.data.attributes.remove(uv_attrib)

    return {'FINISHED'}

It will find first face corner attribute and turn it into a UVMap, either creating or overriding UVMap channel. This is still bad as it assumes that there is only one Attribute Capture node set to Face Corner domain in the GN network, and that it’s used for UVs, so if someone needed to use Attribute Capture set to face corner multiple times for something else, it’d fail.

I am still curious if there’s a way to gather the attribute vector values using foreach_get and then dump them into the UV map using foreach_get. The face corner attribute is 3D vector while the UV map channel seems to expect 2D vector, so IDK how to do that.

Here’s an example in practice. I’ve made quasi cube UV projection in GN and then used the script to create a copy of the object which converts the projection from attribute into an actual UVMap:

1 Like

This isn’t working for me (blender 3.1 alpha on Linux). Do I need any addons?

The script isn’t working for me either. No button is created or sth similiar to trigger it?
(no not correct, I was just not willing enough to read and invest the five minutes of effort to figure out how to call the operator, see below, I am sorry it was late and I was discouraged a lot by the broken UVs. Thanks Ludvik for fixing this).

The script is obsolete. There is now a built in operator in 3.1
image

good to know :smiley:
for anyone else living in the past, this is the same as Ludviks script above but ready for copy paste execute.

import bpy

class OBJECT_OT_ConvertAttributeUVMap(bpy.types.Operator):
    bl_idname = "object.convert_attribute_uvmap"
    bl_label = "Convert fist face corner attribute to UV Map"

    @classmethod
    def poll(cls, context):
        return context.active_object is not None

    def execute(self, context):
        source_name = context.active_object.name

        # Duplicate active object
        bpy.ops.object.duplicate()

        # Collapse existing modifiers on the mesh object
        bpy.ops.object.convert(target='MESH')

        # Store and name the duplicated object
        obj = context.active_object
        obj.name = source_name + "_Baked"

        # Create UVMap UV layer if there's none present
        if obj.data.uv_layers.find("UVMap") == -1:
            uvmap = obj.data.uv_layers.new(name="UVMap")
        else:
            uvmap_index = obj.data.uv_layers.find("UVMap")
            uvmap = obj.data.uv_layers[uvmap_index]

        # UV attribute to store
        uv_attrib = None

        # Try to find valid face corner attribute
        for attribute in obj.data.attributes:
            if attribute.domain == 'CORNER':
                uv_attrib = attribute

        # Check if face corner attribute was found
        if uv_attrib is None:
            if context.active_object is obj:
                bpy.ops.object.delete()
            self.report({'ERROR'}, 'No Face Corner attribute found!')
            return {'CANCELLED'}

        for loop in obj.data.loops:
            uvmap.data[loop.index].uv[0] = uv_attrib.data[loop.index].vector[0]
            uvmap.data[loop.index].uv[1] = uv_attrib.data[loop.index].vector[1]

        obj.data.attributes.remove(uv_attrib)

        return {'FINISHED'}
   
bpy.ops.object.convert_attribute_uvmap()

Hey, thanks for doing the legwork on this, unfortunately I haven’t been able to get it to work as written. Indentation errors abound. Tried everything I could think of to fix, but I’m not familiar enough with Python’s syntax to fix.

Please update. I need this for a project!

Just read two posts above yours.

Sorry for bugging you further, but I’m still dumb. Found attribute convert in the manual, but doesn’t show up in any editor that I can find. And the post two posts above mine doesn’t say where it’s located.

Thanks

On the mesh tab of the properties panel when a mesh object is selected. There’s attributes rollout in there.

THANK YOU!!! Turns out I was using the wrong version (was in 3.0.1). Again, sorry for being a bother.

I don’t understand your problem… but did you try to convert this attribute by inputting it into your node tree and then output it as a vector face corner overwriting an UVMap already created? I’m doing it by converting vertex colors into vertex groups and it works… not sure if it will work for your case…

    for loop in obj.data.loops:
        uvmap.data[loop.index].uv[0] = uv_attrib.data[loop.index].vector[0]
        uvmap.data[loop.index].uv[1] = uv_attrib.data[loop.index].vector[1]

Bummer foreach_get/set do not work here uh?

We can now use bpy.ops.geometry.attribute_convert(mode="UV_MAP")
which is faster than looping millions of loops via python

Btw, that should be fixed in cpp, special case when applying geonode modifiers perhaps?

1 Like