14

I have a lot of 3D objects of various sizes, shapes in blender 2.76b and I would like to set the origin to the bottom center of the objects bounds.

I cannot select the bottom edge and snap to cursor etc as each shapes edges are in different X/Y positions.

Is there a script or tool that will allow me to set the origin to the lowest edge z position?

iKlsR
  • 43,379
  • 12
  • 156
  • 189
MrDave
  • 141
  • 1
  • 1
  • 3
  • If I'm not mistaken what you're asking for is a way to set an object's pivot to the center of the bottom of its bounds in world space, is that correct? – MaVCArt Nov 25 '15 at 16:43
  • Yes, that is 100% correct – MrDave Nov 25 '15 at 18:54
  • 1
    related: http://blender.stackexchange.com/questions/16107/is-there-a-low-level-alternative-for-bpy-ops-object-origin-set – zeffii Nov 26 '15 at 13:53

4 Answers4

21

Move the origin to bottom of all selected mesh objects.

EDIT updated for 2.8. See prior revision for prior.

Object mode script. Moves origin to bottom of mesh, leaving object in place. Pass the matrix world to move origin to global z.

  • Looks at the 8 coords of objects bounding box. The bounding box is in local coordinates. To change space pass a matrix. eg the matrix world of the object will determine lowest z in global space.

  • o is the bounding box center. Set o.z for new origin from min z of bounding box points.

  • Transform all vertices by -o

  • Move the object back globally by o to keep the object in place.

 

import bpy
from mathutils import Matrix, Vector


def origin_to_bottom(ob, matrix=Matrix()):
    me = ob.data
    mw = ob.matrix_world
    local_verts = [matrix @ Vector(v[:]) for v in ob.bound_box]
    o = sum(local_verts, Vector()) / 8
    o.z = min(v.z for v in local_verts)
    o = matrix.inverted() @ o
    me.transform(Matrix.Translation(-o))

    mw.translation = mw @ o

for o in bpy.context.scene.objects:
    if o.type == 'MESH':
        origin_to_bottom(o)
        #origin_to_bottom(o, matrix=o.matrix_world) # global

With some numpy

The global version above drops in global z direction from bbox center of geom, which can lead to an origin outside the bounding box.

Instead here is a version that finds all z minima and sets the origin to their mean

from mathutils import Matrix, Vector
import numpy as np

def origin_to_bottom(ob, matrix=Matrix(), use_verts=False):
    me = ob.data
    mw = ob.matrix_world
    if use_verts:
        data = (v.co for v in me.vertices)
    else:
        data = (Vector(v) for v in ob.bound_box)


    coords = np.array([matrix @ v for v in data])
    z = coords.T[2]
    mins = np.take(coords, np.where(z == z.min())[0], axis=0)

    o = Vector(np.mean(mins, axis=0))
    o = matrix.inverted() @ o
    me.transform(Matrix.Translation(-o))

    mw.translation = mw @ o    

This could be further enhanced by using methods outlined in this excellent Q / A

Replace matrix @ vector list comprehensions with something more efficient

Note: Remember objects can share a mesh. The test code above runs over all mesh objects in the scene without checking for this.

Related: Origins to the down of the object by default

batFINGER
  • 84,216
  • 10
  • 108
  • 233
  • That doesn't work, it seems to set the origin to the center of the bounds in all axis, I need Z position to be bottom of the object bounds. – MrDave Nov 25 '15 at 18:54
  • actually it seems to set the x origin position to the highest x vert position ? Strange as the code looks like it should work for lowest z?? – MrDave Nov 25 '15 at 19:08
  • I'd suggest calculating the average XY and minimum Z of all the global vertex coordinates (rather than calculating it for the local coordinates, then calculating the global value only for the resulting origin), because rotations, scale, etc can give you weird results which will result with a wrong origin. – TLousky Nov 25 '15 at 19:19
  • Added the origin to geometry operator as the code only worked for origins within a mesh. The position given is local (put 3d cursor in local mode) and the origin will be on minz, is your object rotated? Do you want the origin of the object to be relative to their current world position? If so perhaps adding an empty parent as a handle would be another approach – batFINGER Nov 25 '15 at 19:20
  • Sorry, my bad yes the object was rotated. I applied the rotation and it seems to work now for most objects. The origin for 1 or 2 objects don't seem to be centered in the Y position...any ideas? I think the center calculation needs to be center of the bounds not the average of the verts?? Thanks – MrDave Nov 25 '15 at 19:35
  • Added a method to make the lowest global z the origin of a mesh... kept in the local method too as it is useful too.. extending to set any all x, y, z to min, average, max – batFINGER Nov 25 '15 at 19:38
  • y_min = min([v.co.y for v in bm.verts]) y_max = max([v.co.y for v in bm.verts]) y = ((y_max+y_min)/2) – MrDave Nov 25 '15 at 19:51
  • @batFINGER added the above to get the average of the bounds. Not sure how to format code on here sorry. Thanks for all your help – MrDave Nov 25 '15 at 19:52
  • @MrDave updated to a version that uses the bounding box. Test with or without the x and y commented part. If you have an array modifier count 5 for instance without commenting puts the origin on z bottom of array item 3 (middle), or as coded currently the z bottom of unmodified unit. The global version puts origin at global z bottom of how the object is currently scaled / aligned. – batFINGER Nov 26 '15 at 12:04
  • @batFINGER perfect. Sorry for the late reply. – MrDave Nov 27 '15 at 16:02
  • 1
    This is very helpful for text objects. I appreciate the script! – Jeff Lange Jan 23 '17 at 18:07
  • @batFINGER am I correct that it currently still is not possible to move origin to the center + bottom Z of multiple objects? All info on this page works but only for separate objects. Unless I'm missing something? – JeeperCreeper Feb 29 '24 at 02:00
10

Just wanted to add this here:

bl_info = {
    "name": "Origin to...",
    "author": "Martynas Žiemys",
    "version": (1, 0),
    "blender": (3, 4, 1),
    "location": "3D Viewport, Alt+Shift+Ctrl+D",
    "description": "Moves the origin point to top, botom and sides of objects' bounding boxes in global space.",
    "category": "Pie Menus",
}
import bpy
from mathutils import Vector
from bpy.props import (
    BoolProperty,
    EnumProperty,
)
class OBJECT_OT_origin_to(bpy.types.Operator):
    """Move the origin to top, botom or sides(global space)"""
    bl_idname = "object.origin_to"
    bl_label = "Origin to..."
    bl_options = {'REGISTER', 'UNDO'}
center: BoolProperty(
    name="Center in other axis",
    description="Center in other axis",
    default=True,
)
move: BoolProperty(
    name="Move to 3D Cursor",
    description="Move to 3D Cursor",
    default=False,
)

sides =  (
    ('X', "X", "X"),
    ('-X', "-X", "-X"),
    ('Y', "Y", "Y"),
    ('-Y', "-Y", "-Y"),
    ('Z', "Z", "Z"),
    ('-Z', "-Z", "-Z"), 
)
side: EnumProperty(
    name="Side",
    items=sides,
    default='-Z',
)
@classmethod
def poll(cls, context):
    return context.object is not None

def execute(self, context):
    switch_mode = False
    if context.mode != 'OBJECT':
        switch_mode = True
        bpy.ops.object.mode_set(mode='OBJECT', toggle=True)

    for o in context.selected_objects:
        if o.type == "MESH":
            override = bpy.context.copy()
            override["object"] = o
            with bpy.context.temp_override(**override):
                bpy.ops.object.transform_apply(
                    location=False, 
                    rotation=False, 
                    scale=True, 
                    properties=False
                    )
            d = o.data
            m = o.matrix_world
            if self.center:
                bounds_center = (sum((m @ Vector(b) for b in o.bound_box), Vector()))/8
                difference = m.translation - bounds_center
                local_difference = difference @ m
                for v in d.vertices:
                    v.co += local_difference
                m.translation -= difference                   
            difference = Vector((0,0,0))  
            if self.side == 'X':
                bound = max((m @ v.co).x for v in d.vertices)
                difference.x = m.translation.x - bound
            elif self.side == '-X':
                bound = min((m @ v.co).x for v in d.vertices)
                difference.x = m.translation.x - bound
            elif self.side == 'Y':
                bound = max((m @ v.co).y for v in d.vertices)
                difference.y = m.translation.y - bound
            elif self.side == '-Y':
                bound = min((m @ v.co).y for v in d.vertices)
                difference.y = m.translation.y - bound
            elif self.side == 'Z':
                bound = max((m @ v.co).z for v in d.vertices)
                difference.z = m.translation.z - bound
            elif self.side == '-Z':
                bound = min((m @ v.co).z for v in d.vertices)
                difference.z = m.translation.z - bound
            local_difference = difference @ m
            for v in d.vertices:
                v.co += local_difference
            m.translation -= difference 

            if self.move:
                o.location = context.scene.cursor.location  
    if switch_mode:
        bpy.ops.object.mode_set(mode='OBJECT', toggle=True)     

    return {'FINISHED'}

class VIEW3D_MT_PIE_origin_to_pie(bpy.types.Menu): bl_idname = "VIEW3D_MT_origin_to_pie" bl_label = "Origin to..." def draw(self, context): layout = self.layout pie = layout.menu_pie() op = pie.operator("object.origin_to", text = "Origin to Left(-X)") op.side = '-X' op.move = False op = pie.operator("object.origin_to", text = "Origin to Right(X)") op.side = 'X' op.move = False op = pie.operator("object.origin_to", text = "Origin to Bottom(-Z)") op.side = '-Z' op.move = False op = pie.operator("object.origin_to", text = "Origin to Top(Z)") op.side = 'Z' op.move = False op = pie.operator("object.origin_to", text = "Origin to Back(Y)") op.side = 'Y' op.move = False op = pie.operator("object.origin_to", text = "Origin to Top and cursor") op.side = 'Z' op.move = True op = pie.operator("object.origin_to", text = "Origin to Front(-Y)") op.side = '-Y' op.move = False op = pie.operator("object.origin_to", text = "Origin to Bottom and cursor") op.side = '-Z' op.move = True addon_keymaps = [] def register(): bpy.utils.register_class(OBJECT_OT_origin_to) bpy.utils.register_class(VIEW3D_MT_PIE_origin_to_pie)

kcfg = bpy.context.window_manager.keyconfigs.addon
if kcfg:
    km = kcfg.keymaps.new(name='3D View Generic', space_type='VIEW_3D')
    kmi = km.keymap_items.new("wm.call_menu_pie", 'D', 'PRESS', any=False, alt=True, ctrl=True,shift=True)
    kmi.properties.name = "VIEW3D_MT_origin_to_pie"
    addon_keymaps.append((km, kmi.idname))

def unregister():

for km, kmi_idname in addon_keymaps:
    for kmi in km.keymap_items:
        if kmi.idname == kmi_idname:
            km.keymap_items.remove(kmi)
addon_keymaps.clear()

bpy.utils.unregister_class(VIEW3D_MT_PIE_origin_to_pie)
bpy.utils.unregister_class(OBJECT_OT_origin_to)

if name == "main": register() # test calls #bpy.ops.object.origin_to() #bpy.ops.wm.call_menu_pie(name="VIEW3D_MT_PIE_origin_to")

That's made into a little add-on, that if saved to a file with .py extension from Blender's text editor(utf-8 encoding) can be installed as any add-on and creates a pie menu called with Alt+Ctrl+Shift+D:

enter image description here

I wanted to make it for myself for quite some time to help me with placing objects in my scene like ceiling lights, chairs, books or accessories on shelves and so on. I suppose you do not need to manipulate the origin for that, but I find it convenient so since I wrote it now and it's quite related to the question, I can share it here. It works on all selected objects and if you happen to be in edit mode, it switches back and forth as well.

mml
  • 443
  • 2
  • 9
Martynas Žiemys
  • 24,274
  • 2
  • 34
  • 77
  • 1
    Works really nice when testing on a cube I add fresh to my scene. However if the object has been rotated the actual object starts moving rather than just the origin. (Add cube, scale along an axis, rotate along different axis, Ctrl+Alt+Shft+D and moving to bottom shifts my cube up and left) – Caz Oct 18 '20 at 11:15
  • which blender version is assumed here? I'm registered this addson, but can't call it with the key combination indicated in v 2.93.x – Dima Lituiev Aug 15 '21 at 01:58
2

here is a script working in object mode or in edit mode but notice if there are some shape keys, we have to go in edit mode

import bpy, bmesh
from bpy import context
from mathutils import Matrix, Vector

cao=bpy.context.active_object me = cao.data mw = cao.matrix_world difference = Vector((0,0,0)) local_verts = [mw @ Vector(v) for v in cao.bound_box] o = sum(local_verts, Vector()) / 8 o.z = min(v.z for v in local_verts)

difference = mw.translation - o #difference.z = mw.translation.z - o.z #only z local_difference = difference @ mw

if context.object.mode == 'OBJECT': if me.shape_keys: # select basis cao.active_shape_key_index = 0 bpy.ops.object.editmode_toggle() bm = bmesh.from_edit_mesh(me) for v in bm.verts: v.co += local_difference bpy.ops.object.editmode_toggle() else: for v in me.vertices: v.co += local_difference else: bm = bmesh.from_edit_mesh(me) for v in bm.verts: v.co += local_difference bmesh.update_edit_mesh(me)

mw.translation -= difference
```

-1

This script will move the origin to the median point of the vertex/vertices with the lowest Z value (not necessarily the bottom center of the object).

Copy this script to Text window and Run Script, this will work in Edit mode and object mode, results:
1- Object mode is activated in the end result.
2- Origin set to the median point of the vertex/vertices with the lowest Z value.
4- 3D cursor is set at the new origin.

    import bpy

def select_lower_Z():

    o = bpy.context.object  # active object
    mw = o.matrix_world      # Active object's world matrix
    glob_vertex_coordinates = [ mw * v.co for v in o.data.vertices ] # Global coordinates of vertices

    bpy.ops.object.mode_set(mode = 'EDIT') #Change mode of selected object to Edit mode
    bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT') #Set the type in Edit mode to Vertices
    bpy.ops.mesh.select_all(action = 'DESELECT') #Deselect all 

    bpy.ops.object.mode_set(mode = 'OBJECT') #Change mode of selected object to Object mode

    # Find the lowest Z value amongst the object's verts
    minZ = min( [ co.z for co in glob_vertex_coordinates ] )    

    # Select all the vertices that are on the lowest Z
    for v in o.data.vertices:
        if (mw * v.co).z == minZ: 
            v.select = True

    bpy.ops.object.mode_set(mode = 'EDIT') #Change mode of selected object to Edit mode

    current_area_type = bpy.context.area.type#Save the current area type to a variable   
    area = bpy.context.area #Change the area to 3D view in order to get rid of wrong context error
    old_type = area.type #Change the area to 3D view in order to get rid of wrong context error
    area.type = 'VIEW_3D' #Change the area to 3D view in order to get rid of wrong context error
    bpy.ops.view3d.snap_cursor_to_selected() #Move the cursor to selected
    bpy.ops.object.mode_set(mode= 'OBJECT') #Set the mode back to Object mode
    bpy.ops.object.origin_set(type='ORIGIN_CURSOR') #Move the selected object origing's to the 3D cursor's location
    bpy.context.area.type = current_area_type #Set the area type back to what it was before changing it to 3D view


select_lower_Z()
Georges D
  • 4,962
  • 6
  • 40
  • 70