summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoryum <yum.food.vr@gmail.com>2025-06-06 14:16:36 -0700
committeryum <yum.food.vr@gmail.com>2025-06-06 14:16:36 -0700
commitdf8a2ebab65b255dff8656f6e58f1cd88df732ac (patch)
tree4feae1f1df8a33bf362c38e72c328d988d59ec64
parent9caa286a88e89202b3b6bb95d531aa4be927d206 (diff)
Add ability to pack UVs by submesh Z
-rw-r--r--BakeVertexData.py339
1 files changed, 339 insertions, 0 deletions
diff --git a/BakeVertexData.py b/BakeVertexData.py
index 8724967..7b1e426 100644
--- a/BakeVertexData.py
+++ b/BakeVertexData.py
@@ -674,6 +674,341 @@ class MESH_OT_deduplicate_submeshes(Operator):
layout.label(text="Set to 0 for exact matching", icon='INFO')
+class MESH_OT_pack_uv_islands_by_submesh(Operator):
+ bl_idname = "mesh.pack_uv_islands_by_submesh"
+ bl_label = "Pack UV Islands by Submesh Z"
+ bl_description = "Pack UV islands vertically sorted by submesh Z position"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ padding: FloatProperty(
+ name="Island Padding",
+ description="Padding between UV islands",
+ default=0.02,
+ min=0.0,
+ max=0.1,
+ precision=3
+ )
+
+ @classmethod
+ def poll(cls, context):
+ obj = context.active_object
+ return (obj is not None and
+ obj.type == 'MESH' and
+ context.mode == 'EDIT_MESH' and
+ obj.data.uv_layers.active is not None)
+
+ def get_uv_islands(self, bm, uv_layer):
+ """Find all UV islands in the mesh"""
+ # Build UV edge connectivity
+ uv_vert_map = {} # Maps UV coordinates to vertex indices
+ uv_edges = set() # Set of UV edges as frozensets of UV coords
+
+ for face in bm.faces:
+ if not face.select:
+ continue
+
+ face_uvs = []
+ for loop in face.loops:
+ uv = loop[uv_layer].uv
+ uv_key = (round(uv.x, 6), round(uv.y, 6))
+ face_uvs.append(uv_key)
+
+ if uv_key not in uv_vert_map:
+ uv_vert_map[uv_key] = set()
+ uv_vert_map[uv_key].add(loop.vert.index) # Store index instead of BMVert
+
+ # Create UV edges
+ for i in range(len(face_uvs)):
+ j = (i + 1) % len(face_uvs)
+ edge = frozenset([face_uvs[i], face_uvs[j]])
+ uv_edges.add(edge)
+
+ # Find UV islands using connected components
+ uv_adjacency = {uv: set() for uv in uv_vert_map}
+
+ for edge in uv_edges:
+ uv_list = list(edge)
+ if len(uv_list) == 2:
+ uv_adjacency[uv_list[0]].add(uv_list[1])
+ uv_adjacency[uv_list[1]].add(uv_list[0])
+
+ # Find connected components
+ visited = set()
+ islands = []
+
+ for start_uv in uv_vert_map:
+ if start_uv in visited:
+ continue
+
+ island_uvs = set()
+ island_vert_indices = set() # Store indices instead of BMVerts
+ queue = [start_uv]
+
+ while queue:
+ current_uv = queue.pop(0)
+ if current_uv in visited:
+ continue
+
+ visited.add(current_uv)
+ island_uvs.add(current_uv)
+ island_vert_indices.update(uv_vert_map[current_uv])
+
+ for neighbor in uv_adjacency[current_uv]:
+ if neighbor not in visited:
+ queue.append(neighbor)
+
+ # Store island data with vertex indices
+ islands.append({
+ 'uvs': island_uvs,
+ 'vert_indices': island_vert_indices, # Store indices
+ 'loops': [] # Will be filled later
+ })
+
+ # We'll assign loops later when we have a valid BMesh
+ return islands
+
+ def get_island_bounds(self, island, uv_layer):
+ """Get bounding box of UV island"""
+ if not island['loops']:
+ return 0, 0, 0, 0
+
+ min_u = min_v = float('inf')
+ max_u = max_v = float('-inf')
+
+ for loop in island['loops']:
+ uv = loop[uv_layer].uv
+ min_u = min(min_u, uv.x)
+ max_u = max(max_u, uv.x)
+ min_v = min(min_v, uv.y)
+ max_v = max(max_v, uv.y)
+
+ return min_u, min_v, max_u, max_v
+
+ def get_submesh_of_island(self, island_vert_indices, submeshes):
+ """Find which submesh an island belongs to"""
+ # island_vert_indices is already a set of indices
+
+ # Find submesh with most overlap
+ best_submesh = None
+ best_overlap = 0
+
+ for i, submesh in enumerate(submeshes):
+ overlap = len(island_vert_indices & submesh)
+ if overlap > best_overlap:
+ best_overlap = overlap
+ best_submesh = i
+
+ return best_submesh
+
+ def get_submesh_avg_z(self, mesh, submesh_indices):
+ """Calculate average Z position of a submesh"""
+ if not submesh_indices:
+ return 0.0
+
+ total_z = 0.0
+ for idx in submesh_indices:
+ total_z += mesh.vertices[idx].co.z
+
+ return total_z / len(submesh_indices)
+
+ def execute(self, context):
+ obj = context.active_object
+ mesh = obj.data
+
+ # Get UV layer
+ uv_layer = mesh.uv_layers.active
+ if not uv_layer:
+ self.report({'ERROR'}, "No active UV layer")
+ return {'CANCELLED'}
+
+ # Create BMesh
+ bm = bmesh.from_edit_mesh(mesh)
+ bm_uv_layer = bm.loops.layers.uv.active
+
+ # Get UV islands from selected faces
+ uv_islands = self.get_uv_islands(bm, bm_uv_layer)
+
+ if not uv_islands:
+ self.report({'WARNING'}, "No UV islands found in selection")
+ return {'CANCELLED'}
+
+ # Get submeshes (vertex islands)
+ bpy.ops.object.mode_set(mode='OBJECT')
+
+ # Get selected vertices for submesh detection
+ selected_indices = {v.index for v in mesh.vertices if v.select}
+
+ # Build submeshes (reusing logic from other operators)
+ adjacency = {idx: set() for idx in selected_indices}
+ for edge in mesh.edges:
+ v0, v1 = edge.vertices
+ if v0 in selected_indices and v1 in selected_indices:
+ adjacency[v0].add(v1)
+ adjacency[v1].add(v0)
+
+ submeshes = []
+ visited = set()
+
+ for start_idx in selected_indices:
+ if start_idx in visited:
+ continue
+
+ submesh = set()
+ queue = [start_idx]
+
+ while queue:
+ current = queue.pop(0)
+ if current in visited:
+ continue
+
+ visited.add(current)
+ submesh.add(current)
+
+ for neighbor in adjacency[current]:
+ if neighbor not in visited:
+ queue.append(neighbor)
+
+ submeshes.append(submesh)
+
+ # Calculate average Z for each submesh
+ submesh_z_values = []
+ for submesh in submeshes:
+ avg_z = self.get_submesh_avg_z(mesh, submesh)
+ submesh_z_values.append(avg_z)
+
+ # Return to edit mode to work with BMesh
+ bpy.ops.object.mode_set(mode='EDIT')
+ bm = bmesh.from_edit_mesh(mesh)
+ bm_uv_layer = bm.loops.layers.uv.active
+
+ # Assign loops to islands now that we're back in edit mode
+ for island in uv_islands:
+ for face in bm.faces:
+ if not face.select:
+ continue
+
+ for loop in face.loops:
+ uv = loop[bm_uv_layer].uv
+ uv_key = (round(uv.x, 6), round(uv.y, 6))
+
+ if uv_key in island['uvs']:
+ island['loops'].append(loop)
+
+ # Assign UV islands to submeshes and get bounds
+ island_data = []
+ for island in uv_islands:
+ submesh_idx = self.get_submesh_of_island(island['vert_indices'], submeshes)
+ if submesh_idx is not None and island['loops']: # Make sure we have loops
+ bounds = self.get_island_bounds(island, bm_uv_layer)
+ width = bounds[2] - bounds[0]
+ height = bounds[3] - bounds[1]
+
+ island_data.append({
+ 'island': island,
+ 'submesh_idx': submesh_idx,
+ 'submesh_z': submesh_z_values[submesh_idx],
+ 'bounds': bounds,
+ 'width': width,
+ 'height': height,
+ 'min_u': bounds[0],
+ 'min_v': bounds[1]
+ })
+
+ # Sort islands by submesh Z (descending, so higher Z is at top of UV space)
+ island_data.sort(key=lambda x: x['submesh_z'], reverse=True)
+
+ # Calculate total area needed
+ total_area = 0
+ max_island_size = 0
+ for data in island_data:
+ area = (data['width'] + self.padding) * (data['height'] + self.padding)
+ total_area += area
+ max_island_size = max(max_island_size, max(data['width'], data['height']))
+
+ # Calculate optimal square dimensions
+ # Start with square root of total area, but ensure it's at least as wide as largest island
+ target_size = max(math.sqrt(total_area) * 1.2, max_island_size + 2 * self.padding) # 1.2 for some extra space
+
+ # Scale to fit in UV space (0-1)
+ if target_size > 1.0:
+ scale_factor = 0.95 / target_size # Leave some margin
+ # Scale all islands
+ for data in island_data:
+ data['width'] *= scale_factor
+ data['height'] *= scale_factor
+ target_size = 0.95
+ else:
+ scale_factor = 1.0
+
+ # Pack islands using a simple shelf packing algorithm
+ rows = []
+ current_row = []
+ current_row_height = 0
+ current_row_width = 0
+
+ for data in island_data:
+ width = data['width']
+ height = data['height']
+
+ # Check if island fits in current row
+ if current_row_width + width + self.padding <= target_size or not current_row:
+ # Add to current row
+ current_row.append(data)
+ current_row_width += width + self.padding
+ current_row_height = max(current_row_height, height)
+ else:
+ # Start new row
+ rows.append((current_row, current_row_height))
+ current_row = [data]
+ current_row_width = width + self.padding
+ current_row_height = height
+
+ # Add last row
+ if current_row:
+ rows.append((current_row, current_row_height))
+
+ # Calculate actual bounding box height
+ total_height = sum(row[1] for row in rows) + self.padding * (len(rows) + 1)
+
+ # Center the packed result in UV space
+ start_u = (1.0 - target_size) / 2.0
+ start_v = (1.0 - min(total_height, 1.0)) / 2.0 + min(total_height, 1.0)
+
+ # Place islands
+ current_v = start_v
+
+ for row_islands, row_height in rows:
+ # Center row horizontally
+ row_actual_width = sum(data['width'] for data in row_islands) + self.padding * (len(row_islands) - 1)
+ current_u = start_u + (target_size - row_actual_width) / 2.0
+
+ for data in row_islands:
+ # Calculate offset, accounting for the scale factor
+ offset_u = current_u - data['min_u'] * scale_factor
+ offset_v = (current_v - row_height) - data['min_v'] * scale_factor
+
+ # Move all UVs in this island
+ for loop in data['island']['loops']:
+ uv = loop[bm_uv_layer].uv
+ uv.x = uv.x * scale_factor + offset_u
+ uv.y = uv.y * scale_factor + offset_v
+
+ current_u += data['width'] + self.padding
+
+ current_v -= row_height + self.padding
+
+ # Update mesh
+ bmesh.update_edit_mesh(mesh)
+
+ self.report({'INFO'}, f"Packed {len(island_data)} UV islands from {len(submeshes)} submeshes")
+ return {'FINISHED'}
+
+ def draw(self, context):
+ layout = self.layout
+ layout.prop(self, "padding")
+ layout.label(text="Higher Z submeshes → Higher V position", icon='INFO')
+
+
class MESH_OT_merge_by_distance_in_submeshes(Operator):
bl_idname = "mesh.merge_by_distance_in_submeshes"
bl_label = "Merge by Distance (Per Submesh)"
@@ -875,6 +1210,7 @@ class MESH_PT_bake_vertex_panel(Panel):
col.operator("mesh.select_linked_across_boundaries", icon='LINKED')
col.operator("mesh.deduplicate_submeshes", icon='DUPLICATE')
col.operator("mesh.merge_by_distance_in_submeshes", icon='AUTOMERGE_ON')
+ col.operator("mesh.pack_uv_islands_by_submesh", icon='UV')
col.separator()
col.operator("mesh.bake_vertex_vectors", icon='EXPORT')
@@ -884,6 +1220,7 @@ class MESH_PT_bake_vertex_panel(Panel):
box.label(text="Select Linked Cross: Select linked across boundaries")
box.label(text="Deduplicate: Remove duplicate selected submeshes")
box.label(text="Merge: Merge vertices within submeshes")
+ box.label(text="Pack UV Islands: Sort UV islands by submesh Z position")
box.label(text="Bake: Auto-scale selected vertices")
box.label(text="Toggle Contiguous Groups for separate islands")
box.label(text="Scale factor stored in alpha channel")
@@ -903,6 +1240,7 @@ classes = [
MESH_OT_select_all_linked_submeshes,
MESH_OT_select_linked_across_boundaries,
MESH_OT_deduplicate_submeshes,
+ MESH_OT_pack_uv_islands_by_submesh,
MESH_OT_merge_by_distance_in_submeshes,
MESH_PT_bake_vertex_panel
]
@@ -914,6 +1252,7 @@ def menu_func(self, context):
self.layout.operator("mesh.select_linked_across_boundaries", icon='LINKED')
self.layout.operator("mesh.deduplicate_submeshes", icon='DUPLICATE')
self.layout.operator("mesh.merge_by_distance_in_submeshes", icon='AUTOMERGE_ON')
+ self.layout.operator("mesh.pack_uv_islands_by_submesh", icon='UV')
self.layout.operator("mesh.bake_vertex_vectors", icon='EXPORT')