13
$\begingroup$

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?

$\endgroup$
3
  • $\begingroup$ 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? $\endgroup$
    – MaVCArt
    Nov 25, 2015 at 16:43
  • $\begingroup$ Yes, that is 100% correct $\endgroup$
    – MrDave
    Nov 25, 2015 at 18:54
  • 1
    $\begingroup$ related: blender.stackexchange.com/questions/16107/… $\endgroup$
    – zeffii
    Nov 26, 2015 at 13:53

4 Answers 4

21
$\begingroup$

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

$\endgroup$
11
  • $\begingroup$ 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. $\endgroup$
    – MrDave
    Nov 25, 2015 at 18:54
  • $\begingroup$ 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?? $\endgroup$
    – MrDave
    Nov 25, 2015 at 19:08
  • $\begingroup$ 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. $\endgroup$
    – TLousky
    Nov 25, 2015 at 19:19
  • $\begingroup$ 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 $\endgroup$
    – batFINGER
    Nov 25, 2015 at 19:20
  • 1
    $\begingroup$ This is very helpful for text objects. I appreciate the script! $\endgroup$
    – Jeff Lange
    Jan 23, 2017 at 18:07
10
$\begingroup$

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.

$\endgroup$
2
  • 1
    $\begingroup$ 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, <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>Shft</kbd>+<kbd>D</kbd> and moving to bottom shifts my cube up and left) $\endgroup$
    – Caz
    Oct 18, 2020 at 11:15
  • $\begingroup$ 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 $\endgroup$ Aug 15, 2021 at 1:58
2
$\begingroup$

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  
```
$\endgroup$
-1
$\begingroup$

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()
$\endgroup$
4
  • $\begingroup$ Only works for one at a time $\endgroup$
    – AzulShiva
    May 15, 2017 at 14:48
  • $\begingroup$ Yes, this script is part of an add-on I was working on, the add-on now is complete, works for multiple mesh objects and has many features. Here's a link to the tutorial youtu.be/OaieiTAh5ZA the download link is in the description $\endgroup$
    – Georges D
    May 16, 2017 at 16:11
  • $\begingroup$ " This video is unavailable. Sorry about that." Link is dead $\endgroup$
    – AzulShiva
    May 16, 2017 at 18:42
  • 1
    $\begingroup$ youtu.be/oaieiTAh5ZA $\endgroup$
    – Georges D
    May 20, 2017 at 9:22

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .