#!/usr/bin/env python """ collisionbox Lua generator 1. Select a mob mesh or an Empty 2. Press the "Run Script" button below - The script copies the collisionbox the the clipboard. - An 'Empty' with the object's name will appear, which visually represents the collisionbox (which MUST be symmetrical on horizontal axes and centered at 0,0,0 for Minetest, since collision boxes to not turn). 3. To adjust, scale the Empty.collisionbox.* object in Blender then repeat steps 1-2. To keep the Empty symmetrical for Minetest, scale ONLY with one of the following hotkey sequences (with the mouse pointer in the 3D View): - 's' - 's', 'z' - 's', "shift z" How to use: Paste this script into a Blender" Text Editor panel, select an object," press the 'Run Script' button") """ # from mathutils import Matrix from mathutils import Vector # from mathutils import Euler print(""" STARTING generate_lua_collisionbox... """) y_up = True enable_minetest = True enable_lowest_h = False enable_center_h = False if enable_minetest: enable_lowest_h = True enable_center_h = True hs = (0, 1) # horizontal axis indices v = 2 # vertical axis index # Do NOT swap until end. # if y_up: # hs = (0, 2) # v = 1 try: import bpy except ImportError: print(__doc__) exit(1) def calculate_one(): ob1 = None ob1 = bpy.context.active_object # works with 2.7 or 2.8 calculate_collisionbox(ob1) class MessageBox(bpy.types.Operator): bl_idname = "message.messagebox" bl_label = "" message = bpy.props.StringProperty( name="message", description="message", default='' ) def execute(self, context): self.report({'INFO'}, self.message) # print(self.message) return {'FINISHED'} def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self, width=400) def draw(self, context): self.layout.label(text=self.message) self.layout.label(text="") # col = self.layout.column(align = True) # col.prop(context.scene, "my_string_prop") bpy.utils.register_class(MessageBox) msgSuffix = "" def calculate_collisionbox(ob1): global msgSuffix mesh = None if ob1 is None: if len(bpy.context.selected_objects) > 0: ob1 = bpy.context.selected_objects[0] if ob1 is not None: mesh = ob1.data if ob1 is None: msg = "Nothing is selected." bpy.ops.message.messagebox('INVOKE_DEFAULT', message=msg) elif (mesh is not None) and (not hasattr(mesh, 'vertices')): msg = ("A collision box cannot be calculated for a non-mesh" " object.") bpy.ops.message.messagebox('INVOKE_DEFAULT', message=msg) else: # extents1 = ob1.dimensions.copy() obj1Loc = ob1.location # See # bbox_corners = [(ob1.matrix_world # * Vector(corner)) for corner in ob1.bound_box] # See # print("mesh:" + str(mesh)) # print("hasattr(mesh, 'vertices'):" # + str(hasattr(mesh, 'vertices')))] mins = [None, None, None] # minimums; in outer scope for check maxes = [None, None, None] # minimums; in outer scope for check if mesh is not None: wm = ob1.matrix_world for vert in mesh.vertices: # This matrix multiplication is NOT transitive. try: loc = wm @ vert.co except TypeError: loc = wm * vert.co # Blender <2.8 # NOTE: swap y and z for Minetest (y-up) LATER coords = (loc.x, loc.y, loc.z) for i in range(3): if (mins[i] is None) or (coords[i] < mins[i]): mins[i] = coords[i] if (maxes[i] is None) or (coords[i] > maxes[i]): maxes[i] = coords[i] # print(str(extents1)) # print("--by vertices (raw):") # print(" collisionbox = {{{:.2f}, {:.2f}, {:.2f}," # " {:.2f}, {:.2f}, {:.2f}}}".format(mins[0], mins[2], # mins[1], maxes[0], # maxes[2], maxes[1])) # Use ob1.matrix_world (above) instead of incrementing # ob1.location.x, y, and z # newNamePrefix = "Empty.EDGE." + ob1.name # i = 0 # wm = ob1.matrix_world # for vert in mesh.vertices: # newName = newNamePrefix + "." + str(i) # # This matrix multiplication is NOT transitive. # try: # loc = wm @ vert.co # except TypeError: # loc = wm * vert.co # Blender <2.8 # isFar = False # if (loc.x == maxes[0] or loc.y == maxes[1] or # loc.z == maxes[2]): # isFar = True # elif (loc.x == mins[0] or loc.y == mins[1] or # loc.z == mins[2]): # isFar = True # if isFar: # pass # # result = bpy.ops.object.add(type='EMPTY', radius=.25, # # location=loc) # # NOTE: result is merely {'FINISHED'} # # print("{:.2f}, {:.2f}, {:.2f}".format(loc.x, loc.y, # # loc.z)) # bpy.ops.object.add_named(name=newName, type='EMPTY', # radius=.25, location=loc) # i += 1 else: extents1 = ob1.scale.copy() print(dir(ob1)) # Object is an empty, so scale up for Minetest try: extents1.x = extents1.x * (ob1.empty_display_size * 2.0) extents1.y = extents1.y * (ob1.empty_display_size * 2.0) extents1.z = extents1.z * (ob1.empty_display_size * 2.0) except AttributeError: # Blender 2.7 extents1.x = extents1.x * (ob1.empty_draw_size * 2.0) extents1.y = extents1.y * (ob1.empty_draw_size * 2.0) extents1.z = extents1.z * (ob1.empty_draw_size * 2.0) mins[0] = obj1Loc.x - extents1.x / 2.0 maxes[0] = obj1Loc.x + extents1.x / 2.0 mins[1] = obj1Loc.y - extents1.y / 2.0 maxes[1] = obj1Loc.y + extents1.y / 2.0 mins[2] = obj1Loc.z - extents1.z / 2.0 maxes[2] = obj1Loc.z + extents1.z / 2.0 msgSuffix = " (using Empty object's scale)" # print("--using empty object:") # use ground as bottom (don't do this--it is not the Minetest way) # if mins[2] < 0.0: # maxes[1] -= mins[1] # mins[1] = 0.0 # print(" collisionbox = {{{:.2f}, {:.2f}, {:.2f}, {:.2f}," # " {:.2f}, {:.2f}}}".format(mins[0], mins[1], mins[2], # maxes[0], maxes[1], # maxes[2])) sizes = [None, None, None] centers = [None, None, None] for i in range(3): sizes[i] = maxes[i] - mins[i] centers[i] = mins[i] + sizes[i] / 2.0 if enable_lowest_h: # OK to use z as up, since will y&z will be swapped if y_up hSize = None for i in range(len(hs)): axis_i = hs[i] if (hSize is None) or (sizes[axis_i] < hSize): hSize = sizes[axis_i] for i in range(len(hs)): axis_i = hs[i] sizes[axis_i] = hSize mins[axis_i] = centers[axis_i] - hSize / 2 maxes[axis_i] = mins[axis_i] + hSize if enable_center_h: for i in range(len(hs)): axis_i = hs[i] centers[i] = 0 mins[axis_i] = centers[axis_i] - sizes[axis_i] / 2 maxes[axis_i] = mins[axis_i] + sizes[axis_i] loc = (centers[0], centers[1], centers[2]) bpy.ops.object.add(type='EMPTY', radius=.5, location=loc) collisionboxName = "Empty.collisionbox." + ob1.name newEmpty = bpy.context.object # works with 2.7 or 2.8 # newEmpty = bpy.context.selected_objects[0] try: pass # bpy.context.scene.collection.objects.link(newEmpty) except AttributeError: # Blender 2.7 pass newEmpty.name = collisionboxName newEmpty.location = (centers[0], centers[1], centers[2]) try: newEmpty.empty_display_type = 'CUBE' # newEmpty.empty_display_size = (sizes[0], sizes[1], sizes[2]) except AttributeError: # Blender 2.7: newEmpty.empty_draw_type = 'CUBE' # newEmpty.empty_draw_size = (sizes[0], sizes[1], sizes[2]) # newEmpty.dimensions = (sizes[0], sizes[1], sizes[2]) # newEmpty.scale = (sizes[0]/2.0, sizes[1]/2.0, sizes[2]/2.0) newEmpty.scale = (sizes[0], sizes[1], sizes[2]) if enable_minetest: for i in range(3): mins[i] /= 10.0 maxes[i] /= 10.0 msg = ('Size is not available. Make sure you have a mesh object' ' selected.') if mins[0] is not None: # swap y and z for Minetest (y-up): if y_up: tmp = mins[1] mins[1] = mins[2] mins[2] = tmp tmp = maxes[1] maxes[1] = maxes[2] maxes[2] = tmp msg = (" collisionbox = {{{:.2f}, {:.2f}, {:.2f}, {:.2f}," " {:.2f}, {:.2f}}}".format(mins[0], mins[1], mins[2], maxes[0], maxes[1], maxes[2])) if len(msgSuffix) > 0: msgSuffix = " -- " + msgSuffix bpy.context.window_manager.clipboard = msg + msgSuffix msg += " --copied to clipboard" # if enable_minetest: # msg += " -- *10" print(msg) bpy.ops.message.messagebox('INVOKE_DEFAULT', message=msg) # Unregistering before user clicks the MessageBox will crash Blender! # bpy.utils.unregister_class(MessageBox) calculate_one()