bl_info = { "name": "VF Segment Mesh", "author": "John Einselen - Vectorform LLC", "version": (0, 8, 2), "blender": (3, 6, 0), "location": "Scene > VF Tools > Segment Mesh", "description": "Divide meshes into grid based segments", "warning": "inexperienced developer, use at your own risk", "doc_url": "https://github.com/jeinselenVF/VF-BlenderSegmentMesh", "tracker_url": "https://github.com/jeinselenVF/VF-BlenderSegmentMesh/issues", "category": "3D View"} import bpy import bmesh from mathutils import Vector from mathutils import Matrix from bpy.app.handlers import persistent ########################################################################### # Main class class VF_SegmentMesh(bpy.types.Operator): bl_idname = "object.vf_segment_mesh" bl_label = "Segment Mesh" bl_description = "Divide large meshes into grid-based components for more efficient rendering in realtime game engines" bl_options = {'REGISTER', 'UNDO'} def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) def draw(self, context): try: layout = self.layout layout.label(text="Blender will be unresponsive while processing, proceed?") except Exception as exc: print(str(exc) + ' | Error in VF Segment Mesh: Begin segmentation confirmation') def execute(self, context): # Set up local variables sizeX = context.scene.vf_segment_mesh_settings.tile_size[0] sizeY = context.scene.vf_segment_mesh_settings.tile_size[1] countX = context.scene.vf_segment_mesh_settings.tile_count[0] countY = context.scene.vf_segment_mesh_settings.tile_count[1] startX = sizeX * float(countX) * -0.5 startY = sizeY * float(countY) * -0.5 segment = context.scene.vf_segment_mesh_settings.tile_segment origin = context.scene.vf_segment_mesh_settings.tile_origin bounds = True if context.scene.vf_segment_mesh_settings.tile_bounds == "OUT" else False attribute_name = "island_position" # Get active object by name instead of by active reference (so the source object doesn't change during processing) object_name = str(context.active_object.name) mesh_object = bpy.data.objects[object_name] # Deselect all bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='DESELECT') bpy.ops.object.mode_set(mode='OBJECT') # Apply all transforms (otherwise world-space calculations are going to be all off) bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) # May need to apply all modifiers if significant changes are made to the geometry via modifiers # bpy.ops.object.apply_all_modifiers() # Calculate island positions using Geometry Nodes (more than hundreds of times faster than manual BMesh calculation) if segment != "POLY": mod = mesh_object.modifiers.new(name="VF-StoreIslandAttributes-TEMP", type='NODES') mod.node_group = store_island_attributes_node_group() bpy.ops.object.modifier_apply(modifier="VF-StoreIslandAttributes-TEMP") bpy.data.node_groups.remove(bpy.data.node_groups["VF-StoreIslandAttributes-TEMP"]) # Save current 3D cursor location and pivot point original_cursor = context.scene.cursor.matrix original_pivot = context.tool_settings.transform_pivot_point # Track names of each created object separated_collection = [] # Loop through each grid space for x in range(countX): # Define min/max for X min_x = startX + (x * sizeX) max_x = min_x + sizeX loc_x = (max_x + min_x) / 2 if bounds: if x == 0: min_x = float('-inf') elif x == countX-1: max_x = float('inf') for y in range(countY): # Define min/max for Y min_y = startY + (y * sizeY) max_y = min_y + sizeY loc_y = (max_y + min_y) / 2 if bounds: if y == 0: min_y = float('-inf') elif y == countY-1: max_y = float('inf') # Prevent out-of-range errors (seems like the attribute indices aren't updated after splitting geometry) mesh_object.data.update() # Re-get the mesh data to ensure everything is up-to-date mesh_data = mesh_object.data # Get attribute data if needed if segment == "AVERAGE": island_info = "island_mean" elif segment == "WEIGHTED": island_info = "island_weighted" else: island_info = False # Create tile name tile_name = mesh_object.name + "-Tile-" + str(x) + "-" + str(y) # Count how many polygons have been selected count = 0 # Select polygons within the specified XYZ area for polygon in mesh_data.polygons: if island_info: # Get precalculated island position element_position = mesh_data.attributes[island_info].data[polygon.index].vector else: # Find average vertex location of individual polygon element_position = Vector((0, 0, 0)) for vertice_index in polygon.vertices: element_position += mesh_data.vertices[vertice_index].co element_position /= len(polygon.vertices) # Check element position against min/max if min_x <= element_position.x <= max_x and min_y <= element_position.y <= max_y: polygon.select = True count += 1 # Only create a new segment if there are 1 or more polygons selected if count > 0: # Separate selected polygons into a new object context.view_layer.objects.active = mesh_object mesh_object.select_set(True) bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.separate(type='SELECTED') bpy.ops.object.mode_set(mode='OBJECT') # Rename the separated object and mesh separated_object = context.selected_objects[1] separated_object.name = tile_name separated_mesh = separated_object.data separated_mesh.name = tile_name separated_object.select_set(False) separated_collection.append(tile_name) # Apply transforms, set the origin, and set the position of the separated object with context.temp_override( active_object=separated_object, editable_objects=[separated_object], object=separated_object, selectable_objects=[separated_object], selected_editable_objects=[separated_object], selected_objects=[separated_object]): if origin == "TILE": context.scene.cursor.matrix = Matrix(((1.0, 0.0, 0.0, loc_x),(0.0, 1.0, 0.0, loc_y),(-0.0, 0.0, 1.0, 0.0),(0.0, 0.0, 0.0, 1.0))) bpy.ops.object.origin_set(type='ORIGIN_CURSOR') elif origin == "BOX": context.tool_settings.transform_pivot_point = "BOUNDING_BOX_CENTER" bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY') elif origin == "MEDIAN": context.tool_settings.transform_pivot_point = "MEDIAN_POINT" bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY') elif origin == "MASS": bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_MASS') elif origin == "VOLUME": bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_VOLUME') # Select all newly created segments for name in separated_collection: bpy.data.objects[name].select_set(True) # If no elements remain in the original source, remove it and set the first tile to active if len(mesh_object.data.vertices) == 0: bpy.data.meshes.remove(mesh_object.data) context.view_layer.objects.active = bpy.data.objects[separated_collection[0]] # Restore original 3D cursor position and pivot point context.scene.cursor.matrix = original_cursor context.tool_settings.transform_pivot_point = original_pivot # Done return {'FINISHED'} # Many thanks to Brendan Parmer for making this easy https://github.com/BrendanParmer/NodeToPython @persistent def store_island_attributes_node_group(): store_island_attributes= bpy.data.node_groups.new(type = 'GeometryNodeTree', name = "VF-StoreIslandAttributes-TEMP") #initialize store_island_attributes nodes #store_island_attributes outputs #output Geometry store_island_attributes.outputs.new('NodeSocketGeometry', "Geometry") store_island_attributes.outputs[0].attribute_domain = 'POINT' #node Group Output group_output = store_island_attributes.nodes.new("NodeGroupOutput") #store_island_attributes inputs #input Geometry store_island_attributes.inputs.new('NodeSocketGeometry', "Geometry") store_island_attributes.inputs[0].attribute_domain = 'POINT' #node Group Input group_input = store_island_attributes.nodes.new("NodeGroupInput") #node Mesh Island mesh_island = store_island_attributes.nodes.new("GeometryNodeInputMeshIsland") #node Position position = store_island_attributes.nodes.new("GeometryNodeInputPosition") #node Face Area face_area = store_island_attributes.nodes.new("GeometryNodeInputMeshFaceArea") #node Accumulate Field accumulate_field = store_island_attributes.nodes.new("GeometryNodeAccumulateField") accumulate_field.data_type = 'FLOAT_VECTOR' accumulate_field.domain = 'POINT' #Value Float accumulate_field.inputs[1].default_value = 1.0 #Value Int accumulate_field.inputs[2].default_value = 1 #node Accumulate Field.001 accumulate_field_001 = store_island_attributes.nodes.new("GeometryNodeAccumulateField") accumulate_field_001.data_type = 'INT' accumulate_field_001.domain = 'POINT' #Value Vector accumulate_field_001.inputs[0].default_value = (1.0, 1.0, 1.0) #Value Float accumulate_field_001.inputs[1].default_value = 1.0 #Value Int accumulate_field_001.inputs[2].default_value = 1 #node Accumulate Field.002 accumulate_field_002 = store_island_attributes.nodes.new("GeometryNodeAccumulateField") accumulate_field_002.data_type = 'FLOAT_VECTOR' accumulate_field_002.domain = 'FACE' #Value Float accumulate_field_002.inputs[1].default_value = 1.0 #Value Int accumulate_field_002.inputs[2].default_value = 1 #node Accumulate Field.003 accumulate_field_003 = store_island_attributes.nodes.new("GeometryNodeAccumulateField") accumulate_field_003.data_type = 'FLOAT' accumulate_field_003.domain = 'FACE' #Value Vector accumulate_field_003.inputs[0].default_value = (1.0, 1.0, 1.0) #Value Int accumulate_field_003.inputs[2].default_value = 1 #node Vector Math vector_math = store_island_attributes.nodes.new("ShaderNodeVectorMath") vector_math.operation = 'DIVIDE' #Vector_002 vector_math.inputs[2].default_value = (0.0, 0.0, 0.0) #Scale vector_math.inputs[3].default_value = 1.0 #node Vector Math.001 vector_math_001 = store_island_attributes.nodes.new("ShaderNodeVectorMath") vector_math_001.operation = 'SCALE' #Vector_001 vector_math_001.inputs[1].default_value = (0.0, 0.0, 0.0) #Vector_002 vector_math_001.inputs[2].default_value = (0.0, 0.0, 0.0) #node Vector Math.002 vector_math_002 = store_island_attributes.nodes.new("ShaderNodeVectorMath") vector_math_002.operation = 'DIVIDE' #Vector_002 vector_math_002.inputs[2].default_value = (0.0, 0.0, 0.0) #Scale vector_math_002.inputs[3].default_value = 1.0 #node Store Named Attribute store_named_attribute = store_island_attributes.nodes.new("GeometryNodeStoreNamedAttribute") store_named_attribute.data_type = 'INT' store_named_attribute.domain = 'FACE' #Selection store_named_attribute.inputs[1].default_value = True #Name store_named_attribute.inputs[2].default_value = "island_index" #Value_Vector store_named_attribute.inputs[3].default_value = (0.0, 0.0, 0.0) #Value_Float store_named_attribute.inputs[4].default_value = 0.0 #Value_Color store_named_attribute.inputs[5].default_value = (0.0, 0.0, 0.0, 0.0) #Value_Bool store_named_attribute.inputs[6].default_value = False #node Store Named Attribute.002 store_named_attribute_002 = store_island_attributes.nodes.new("GeometryNodeStoreNamedAttribute") store_named_attribute_002.data_type = 'FLOAT_VECTOR' store_named_attribute_002.domain = 'FACE' #Selection store_named_attribute_002.inputs[1].default_value = True #Name store_named_attribute_002.inputs[2].default_value = "island_mean" #Value_Float store_named_attribute_002.inputs[4].default_value = 0.0 #Value_Color store_named_attribute_002.inputs[5].default_value = (0.0, 0.0, 0.0, 0.0) #Value_Bool store_named_attribute_002.inputs[6].default_value = False #Value_Int store_named_attribute_002.inputs[7].default_value = 0 #node Store Named Attribute.003 store_named_attribute_003 = store_island_attributes.nodes.new("GeometryNodeStoreNamedAttribute") store_named_attribute_003.data_type = 'FLOAT_VECTOR' store_named_attribute_003.domain = 'FACE' #Selection store_named_attribute_003.inputs[1].default_value = True #Name store_named_attribute_003.inputs[2].default_value = "island_weighted" #Value_Float store_named_attribute_003.inputs[4].default_value = 0.0 #Value_Color store_named_attribute_003.inputs[5].default_value = (0.0, 0.0, 0.0, 0.0) #Value_Bool store_named_attribute_003.inputs[6].default_value = False #Value_Int store_named_attribute_003.inputs[7].default_value = 0 #Set locations group_output.location = (0.0, 0.0) group_input.location = (-720.0, 0.0) mesh_island.location = (-720.0, -100.0) position.location = (-900.0, -220.0) face_area.location = (-1080.0, -720.0) accumulate_field.location = (-720.0, -220.0) accumulate_field_001.location = (-720.0, -440.0) accumulate_field_002.location = (-720.0, -660.0) accumulate_field_003.location = (-720.0, -880.0) vector_math.location = (-540.0, -220.0) vector_math_001.location = (-900.0, -660.0) vector_math_002.location = (-540.0, -660.0) store_named_attribute.location = (-540.0, 0.0) store_named_attribute_002.location = (-360.0, 0.0) store_named_attribute_003.location = (-180.0, 0.0) #Set dimensions group_output.width, group_output.height = 140.0, 100.0 group_input.width, group_input.height = 140.0, 100.0 mesh_island.width, mesh_island.height = 140.0, 100.0 position.width, position.height = 140.0, 100.0 face_area.width, face_area.height = 140.0, 100.0 accumulate_field.width, accumulate_field.height = 140.0, 100.0 accumulate_field_001.width, accumulate_field_001.height = 140.0, 100.0 accumulate_field_002.width, accumulate_field_002.height = 140.0, 100.0 accumulate_field_003.width, accumulate_field_003.height = 140.0, 100.0 vector_math.width, vector_math.height = 140.0, 100.0 vector_math_001.width, vector_math_001.height = 140.0, 100.0 vector_math_002.width, vector_math_002.height = 140.0, 100.0 store_named_attribute.width, store_named_attribute.height = 140.0, 100.0 store_named_attribute_002.width, store_named_attribute_002.height = 140.0, 100.0 store_named_attribute_003.width, store_named_attribute_003.height = 140.0, 100.0 #initialize store_island_attributes links #store_named_attribute_003.Geometry -> group_output.Geometry store_island_attributes.links.new(store_named_attribute_003.outputs[0], group_output.inputs[0]) #face_area.Area -> accumulate_field_003.Value store_island_attributes.links.new(face_area.outputs[0], accumulate_field_003.inputs[1]) #vector_math_001.Vector -> accumulate_field_002.Value store_island_attributes.links.new(vector_math_001.outputs[0], accumulate_field_002.inputs[0]) #accumulate_field_002.Total -> vector_math_002.Vector store_island_attributes.links.new(accumulate_field_002.outputs[6], vector_math_002.inputs[0]) #accumulate_field_003.Total -> vector_math_002.Vector store_island_attributes.links.new(accumulate_field_003.outputs[7], vector_math_002.inputs[1]) #accumulate_field.Total -> vector_math.Vector store_island_attributes.links.new(accumulate_field.outputs[6], vector_math.inputs[0]) #accumulate_field_001.Total -> vector_math.Vector store_island_attributes.links.new(accumulate_field_001.outputs[8], vector_math.inputs[1]) #face_area.Area -> vector_math_001.Scale store_island_attributes.links.new(face_area.outputs[0], vector_math_001.inputs[3]) #position.Position -> accumulate_field.Value store_island_attributes.links.new(position.outputs[0], accumulate_field.inputs[0]) #group_input.Geometry -> store_named_attribute.Geometry store_island_attributes.links.new(group_input.outputs[0], store_named_attribute.inputs[0]) #store_named_attribute.Geometry -> store_named_attribute_002.Geometry store_island_attributes.links.new(store_named_attribute.outputs[0], store_named_attribute_002.inputs[0]) #store_named_attribute_002.Geometry -> store_named_attribute_003.Geometry store_island_attributes.links.new(store_named_attribute_002.outputs[0], store_named_attribute_003.inputs[0]) #mesh_island.Island Index -> store_named_attribute.Value store_island_attributes.links.new(mesh_island.outputs[0], store_named_attribute.inputs[7]) #vector_math.Vector -> store_named_attribute_002.Value store_island_attributes.links.new(vector_math.outputs[0], store_named_attribute_002.inputs[3]) #vector_math_002.Vector -> store_named_attribute_003.Value store_island_attributes.links.new(vector_math_002.outputs[0], store_named_attribute_003.inputs[3]) #mesh_island.Island Index -> accumulate_field.Group ID store_island_attributes.links.new(mesh_island.outputs[0], accumulate_field.inputs[3]) #mesh_island.Island Index -> accumulate_field_001.Group ID store_island_attributes.links.new(mesh_island.outputs[0], accumulate_field_001.inputs[3]) #mesh_island.Island Index -> accumulate_field_002.Group ID store_island_attributes.links.new(mesh_island.outputs[0], accumulate_field_002.inputs[3]) #mesh_island.Island Index -> accumulate_field_003.Group ID store_island_attributes.links.new(mesh_island.outputs[0], accumulate_field_003.inputs[3]) #position.Position -> vector_math_001.Vector store_island_attributes.links.new(position.outputs[0], vector_math_001.inputs[0]) return store_island_attributes @persistent def vf_segment_mesh_preview(self, context): mesh_name = "VF-SegmentMeshPreview-TEMP" # Remove existing mesh data block (and associated object) if it exists if mesh_name in bpy.data.meshes: bpy.data.meshes.remove(bpy.data.meshes[mesh_name]) # Stop now if the preview mesh is disabled if not context.scene.vf_segment_mesh_settings.show_preview: # Done return None # Set up local variables sizeX = context.scene.vf_segment_mesh_settings.tile_size[0] sizeY = context.scene.vf_segment_mesh_settings.tile_size[1] countX = context.scene.vf_segment_mesh_settings.tile_count[0] countY = context.scene.vf_segment_mesh_settings.tile_count[1] # Save the current object selection active_object_name = str(context.active_object.name) if context.active_object else False selected_objects = [obj for obj in context.selected_objects] # Create primitive grid bpy.ops.mesh.primitive_grid_add( x_subdivisions=countX, y_subdivisions=countY, size=1, enter_editmode=False, align='WORLD', location=(0.0, 0.0, 0.0), rotation=(0.0, 0.0, 0.0), scale=(sizeX * countX, sizeY * countY, 1.0)) # Set scale context.active_object.scale = (sizeX * countX, sizeY * countY, 1.0) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) # Convert to wireframe and disable for rendering bpy.ops.object.modifier_add(type='WIREFRAME') context.object.modifiers["Wireframe"].thickness = float(max(sizeX, sizeY)) * 0.05 # bpy.ops.object.modifier_apply() context.object.hide_render = True # Rename object and mesh data block context.active_object.name = mesh_name context.active_object.data.name = mesh_name # Reset selection context.active_object.select_set(False) if active_object_name: context.view_layer.objects.active = bpy.data.objects[active_object_name] # If one or more objects were originally selected, restore that selection set if len(selected_objects) >= 1: # Re-select previously selected objects for obj in selected_objects: obj.select_set(True) # Done return None ########################################################################### # Project settings and UI rendering classes class vfSegmentMeshSettings(bpy.types.PropertyGroup): tile_size: bpy.props.FloatVectorProperty( name='Size', description='Size of each X/Y tile', subtype='XYZ_LENGTH', size=2, default=(100.0, 100.0), step=1, precision=2, soft_min=1.0, soft_max=1000.0, min=0.0, max=10000.0, update=vf_segment_mesh_preview) tile_count: bpy.props.IntVectorProperty( name="Count", description="Number of X/Y tiles", subtype="XYZ", size=2, default=[4, 4], step=1, soft_min=2, soft_max=8, min=1, max=64, update=vf_segment_mesh_preview) tile_bounds: bpy.props.EnumProperty( name = 'Include', description = 'Specify if geometry outside the tile area will be included in the nearest tile or not', items = [ ('IN', 'Only Inside', 'Limits tile content to only the elements that fall within each tile boundary'), ('OUT', 'Extend Edges', 'Includes content beyond the edges of the tile array, ensuring nothing is left out') ], default = 'OUT') tile_segment: bpy.props.EnumProperty( name = 'Segment', description = 'Segment mesh by individual polygons or connected mesh islands', items = [ ('POLY', 'Per Polygon', 'Segment mesh by individual polygons (cuts apart merged elements)'), ('AVERAGE', 'Island Average', 'Segment mesh based on the average vertex positions of each contiguous island (maintains merged elements)'), ('WEIGHTED', 'Island Weighted', 'Segment mesh based on the weighted polygon positions of each contiguous island (maintains merged elements)') ], default = 'WEIGHTED') tile_origin: bpy.props.EnumProperty( name = 'Origin', description = 'Choose the desired origin for each tile', items = [ ('ZERO', 'Zero', 'Leave each tile origin at the local zero point (not ideal in cases where culling algorithms take origin into account)'), ('TILE', 'Tile', 'Set each tile origin to the centre of the tile space (best for predictable placement but may not be as ideal for transparency sorting in some cases)'), ('BOX', 'Bounding Box', 'Set each tile origin to the geometry bounding box'), ('MEDIAN', 'Median', 'Set each tile origin to the geometry median'), ('MASS', 'Mass', 'Set each tile origin to the geometry mass'), ('VOLUME', 'Volume', 'Set each tile origin to the geometry volume') ], default = 'TILE') show_preview: bpy.props.BoolProperty( name="Preview", description="Enable preview grid mesh", default=False, update=vf_segment_mesh_preview) class VFTOOLS_PT_segment_mesh(bpy.types.Panel): bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_category = 'VF Tools' bl_order = 20 bl_options = {'DEFAULT_CLOSED'} bl_label = "Segment Mesh" bl_idname = "VFTOOLS_PT_segment_mesh" @classmethod def poll(cls, context): return True def draw_header(self, context): try: layout = self.layout except Exception as exc: print(str(exc) + " | Error in VF Segment Mesh panel header") def draw(self, context): try: # Check if mesh object is selected if context.active_object and context.active_object.type == 'MESH' and len(context.active_object.data.polygons) > 0: button_enable = True button_title = "Create " + str(context.scene.vf_segment_mesh_settings.tile_count[0] * context.scene.vf_segment_mesh_settings.tile_count[1]) + " Segments" button_icon = "MESH_GRID" else: button_enable = False button_title = "Select Mesh" button_icon = "OUTLINER_DATA_MESH" # UI Layout layout = self.layout layout.use_property_decorate = False # No animation layout.use_property_split = True layout.prop(context.scene.vf_segment_mesh_settings, 'tile_size') layout.prop(context.scene.vf_segment_mesh_settings, 'tile_count') col = layout.column(align=True) col.prop(context.scene.vf_segment_mesh_settings, 'tile_bounds') col.prop(context.scene.vf_segment_mesh_settings, 'tile_segment') col.prop(context.scene.vf_segment_mesh_settings, 'tile_origin') layout.prop(context.scene.vf_segment_mesh_settings, 'show_preview') if button_enable: layout.operator(VF_SegmentMesh.bl_idname, text = button_title, icon = button_icon) else: disabled = layout.row() disabled.active = False disabled.enabled = False disabled.operator(VF_SegmentMesh.bl_idname, text = button_title, icon = button_icon) except Exception as exc: print(str(exc) + " | Error in VF Segment Mesh panel") classes = (VF_SegmentMesh, vfSegmentMeshSettings, VFTOOLS_PT_segment_mesh) ########################################################################### # Addon registration functions def register(): for cls in classes: bpy.utils.register_class(cls) bpy.types.Scene.vf_segment_mesh_settings = bpy.props.PointerProperty(type = vfSegmentMeshSettings) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls) del bpy.types.Scene.vf_segment_mesh_settings if __name__ == "__main__": register()