Home Technical Talk

Is there a efficient way to auto Unwrap Uvs by material ID?

Kyle_butler
polycounter lvl 11
Offline / Send Message
Kyle_butler polycounter lvl 11
Hi all!

I'm facing the following issue:

I have 3d models with multiple material IDs, each with its own texture:


In order to increase efficiency and reduce the number of draw calls , I want to unify all of those into a single UV atlas, like so:



However, as you can see, it repeats the same textures over and over across the UV space (highlighted a few with matching colors to exemplify):


I'm trying to use auto Unwrap UVW and UV-packer script in 3ds max.

Is there any way to, while keeping automatic, unwrap the textures by material ID, so that each texture only appears once on the UV space?

I'm not restricted to 3ds max solutions, I'm open to any suggestions in different packages.

Thanks!




Replies

  • poopipe
    Options
    Offline / Send Message
    poopipe grand marshal polycounter
    assign the same unwrap UV to all of the objects, use the material ID filter dropdown (bottom of UV editor window), select all the filtered faces and pack based on your selection

    i think to do it as part of an auto unwrap you'd have to script it
  • gnoop
    Options
    Offline / Send Message
    gnoop polycounter
    i attached a quick Blenders  geometry node example  that  packs  all same mat ID UV islands   in sort of automated  style as  rows.    it uses material id  index to shift  UV  in U  direction each  in 1 . 

    So each UV islands having mat ID 1   shifts to 1  ,  islands  with matID2  shifts to 2   etc.   They all scales back to 0-1  in the end .

    I have 4 materials . Then with simple arithmetic  and "compare" node  that returns  a binary  0 or 1 sort of  selection   I shift  all uv islands with matID  more than  say  2  or given input  integer  to next row with  V 1 shift and  U -1  etc .  
    it works with 2 rows currently   but it would be  not that complicated to add more rows  for more  complex pattern .  

    The modifier has  UVx100 checkbox  that previews  UV as flat geometry   ( you need the initial  geometry to have it's origin in world zero )

    Sure it's possible in MAx creation graph too but I already forgot everything I learned about it   since  Blender is way  more easy .

    ps. you need to rename  UV chanel to UVMap  if import from MAx or adjust the node graph with  UVchannel_1  (max naming style)  instead.

     it may not work same well  if your initial  buildings have  tillable textures  beyond 0-1  .   You would need to do it in one row /line  long  texture with strictly vertical tiling .

    ps2 . BTW, if your game engine supports  texture arrays     you could just make it UDIM stile  . Not be packed in one 0-1  texture .   Not 100% sure  but perhaps texture arrays make that  draw call issue irrelevant . At least we stopped to do that sort of manual atlases  decade ago.   Still might be necessary for mobile games although.
  • gnoop
    Options
    Offline / Send Message
    gnoop polycounter
    I asked chat GPT  to make me script using same  idea.    Packing matID as  UDIMs  pattern  4x4   first   then normalize for selected objects . Took it seconds and it understand the idea  quicker than a typical programmer  adding extra helpful features I have not even thought about like  working  with 12 materials  and doing 4x3 pattern  .    It catches  it on the fly  even written with bad typos and grammar .

    Here it is.

     import bpy

    def shift_uv(uv_layer, faces, u_shift, v_shift):
        for face in faces:
            for loop_index in face.loop_indices:
                loop_uv = uv_layer.data[loop_index].uv
                loop_uv.x += u_shift
                loop_uv.y += v_shift

    def get_udim_shift(material_id):
        row = material_id // 4
        col = material_id % 4
        u_shift = col  # Shift right
        v_shift = -row  # Shift down
        return u_shift, v_shift

    def get_collective_uv_bounds(objects):
        min_u, min_v = float('inf'), float('inf')
        max_u, max_v = float('-inf'), float('-inf')
        
        for obj in objects:
            if obj.type == 'MESH' and obj.data.uv_layers:
                uv_layer = obj.data.uv_layers.active
                for uv in uv_layer.data:
                    min_u = min(min_u, uv.uv.x)
                    min_v = min(min_v, uv.uv.y)
                    max_u = max(max_u, uv.uv.x)
                    max_v = max(max_v, uv.uv.y)
        
        return min_u, min_v, max_u, max_v

    def normalize_uv(objects, min_u, min_v, max_u, max_v):
        scale_u = 1.0 / (max_u - min_u)
        scale_v = 1.0 / (max_v - min_v)
        
        for obj in objects:
            if obj.type == 'MESH' and obj.data.uv_layers:
                uv_layer = obj.data.uv_layers.active
                for uv in uv_layer.data:
                    uv.uv.x = (uv.uv.x - min_u) * scale_u
                    uv.uv.y = (uv.uv.y - min_v) * scale_v

    def pack_udim_and_normalize_collectively():
        selected_objects = bpy.context.selected_objects
        # Step 1: Shift UVs based on material ID for each object
        for obj in selected_objects:
            if obj.type == 'MESH':
                mesh = obj.data
                if not mesh.uv_layers:
                    continue
                uv_layer = mesh.uv_layers.active
                for mat_id, material in enumerate(mesh.materials):
                    faces = [face for face in mesh.polygons if face.material_index == mat_id]
                    u_shift, v_shift = get_udim_shift(mat_id)
                    shift_uv(uv_layer, faces, u_shift, v_shift)
                    
        # Step 2: Calculate collective bounds of all UVs across selected objects
        min_u, min_v, max_u, max_v = get_collective_uv_bounds(selected_objects)
        
        # Step 3: Normalize all UVs based on collective bounds
        normalize_uv(selected_objects, min_u, min_v, max_u, max_v)

    # Run the function
    pack_udim_and_normalize_collectively()


    Works  perfectly .   Chat is super helpful in Blender  vs 3dmax where it's a challenge just to persuade it to  make a maxscript .  When I ask it to write a maxscript  it never works  till a day wasted on many approaches .  My guess   it's really well trained to do python for Blender.
  • poopipe
    Options
    Offline / Send Message
    poopipe grand marshal polycounter
    you can use python in max  - chatgpt might do better with that


    personally I'd recommend working out how to write the code yourself - at least you'll be able to see where the robot makes mistakes

  • gnoop
    Options
    Offline / Send Message
    gnoop polycounter
    poopipe said:
    personally I'd recommend working out how to write the code yourself - at least you'll be able to see where the robot makes mistakes


    I am thinking about it for a while already  but   still not  sure.  Looks like  such kind  of scripting   is going to become  completely AI territory soon and  would decimate  coders in masse.     Way before  image generative AI would  kill all the game art  specific jobs.  
     Will be like lingva latina and philosophy , things  you tried to learn in an art school  instead of the math and never  actually  needed  them since.       

    in a word  if you didn't learn it decade ago already  now is not a best time to start probably.     GPT gets better with both Python and javascript  each couple  months  passing.  

  • poopipe
    Options
    Offline / Send Message
    poopipe grand marshal polycounter
    programmers aren't scared of robots writing code  because software development is 10% writing code and 90% figuring out what code to write.



  • Kyle_butler
    Options
    Offline / Send Message
    Kyle_butler polycounter lvl 11
    Wow, guys can't thank you enough for putting so much care in the answers.

    @poopipe I couldn't make it work with several geometries as in the example.
    I'm using Unwrap -  Edit Uvs Mapping -Flatten - material ID checked
    Instead of overlapping the textures with the same material ID, they are still split into different shells:



    @gnoop This is fantastic! It will take me a minute to fully understand it but I'll dive into it this week. Thank you very much for the file and detailed answer!
    I might give it a try to the scripting side as well =) 


  • poopipe
    Options
    Offline / Send Message
    poopipe grand marshal polycounter
    unwrap on multiple objects is one of those things that seems to break from version to version - i think it's reliable on 24 but don't quote me on that. 

    fwiw. i've usually dealt with this sort of thing by merging everything into the same mesh and breaking it up by ID before packing. you lose your pivot info doing that but there's ways to solve that problem if it's important. 

    gnoops solution seems like a good shout though - if you don't mind going through blender it's proably the path of least resistance



  • Eric Chadwick
    Options
    Offline / Send Message
    I usually delete the copies, uv one of them along with everything else, then reduplicate the copies. That way the uvs are just overlaid automagically.
  • Kyle_butler
    Options
    Offline / Send Message
    Kyle_butler polycounter lvl 11
    @poopipe thanks for the follow up. Let me break it down your solution to see if I really understood it:
    You detach you mesh by material ID and unwrap each object individually, then combine then e pack the final mesh into a single UV atlas?


    @Eric Chadwick Thanks for sharing your method. Could you explain a little bit more about your process? 

  • Eric Chadwick
    Options
    Offline / Send Message
    What’s unclear about it? Not sure what to expand upon…
Sign In or Register to comment.