9
$\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 '15 at 16:43
  • $\begingroup$ Yes, that is 100% correct $\endgroup$ – MrDave Nov 25 '15 at 18:54
  • 1
    $\begingroup$ related: blender.stackexchange.com/questions/16107/… $\endgroup$ – zeffii Nov 26 '15 at 13:53
15
$\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 '15 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 '15 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 '15 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 '15 at 19:20
  • 1
    $\begingroup$ This is very helpful for text objects. I appreciate the script! $\endgroup$ – Jeff Lange Jan 23 '17 at 18:07
6
$\begingroup$

Just wanted to add this here:

bl_info = {
    "name": "Origin to...",
    "author": "Martynas Žiemys",
    "version": (1, 0),
    "blender": (2, 82, 0),
    "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":
                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.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.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
  • $\begingroup$ Oh, the mode is not checked for because I want it to work from other modes. I switch to Object mode if it's different and the object type is also taken into consideration as it only acts on mesh objects, I didn't think there was a need to loop through all the selection all the time just to check if I have mesh objects selected and I want it to work on all mesh objects that are amongst selected objects, so that was a conscious decision. $\endgroup$ – Martynas Žiemys Apr 21 '20 at 15:06
  • 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 '20 at 11:15
0
$\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 '17 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 May 16 '17 at 16:11
  • $\begingroup$ " This video is unavailable. Sorry about that." Link is dead $\endgroup$ – AzulShiva May 16 '17 at 18:42
  • 1
    $\begingroup$ youtu.be/oaieiTAh5ZA $\endgroup$ – Georges May 20 '17 at 9:22
0
$\begingroup$

well someone noticed it was not working when adding shape keys. here is a solution

import bpy
import bmesh
from mathutils import Matrix, Vector

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

if obj.data.shape_keys:
    difference.z = mw.translation.z - o.z
    local_difference = difference @ mw
    # select basis
    obj.active_shape_key_index = 0
    # edit verts
    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()
    mw.translation -= difference

but the interest of this code go further you can move origin in editmode

import bpy
import bmesh
from mathutils import Matrix, Vector

bpy.ops.object.mode_set(mode='EDIT')
obj=bpy.context.object
me = obj.data
mw = obj.matrix_world
difference = Vector((0,0,0))
local_verts = [mw @ Vector(v[:]) for v in obj.bound_box]
o = sum(local_verts, Vector()) / 8
o.z = min(v.z for v in local_verts)

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

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$

Set Origin Plus addon might help you in such situation. Please follow the video below as tutorial. (This addon is not free..Sorry for that..)

n.b: It sets the origin to the center of selected vertices.

This procedure will work on blender 2.8 and above

Steps:

  1. Install addon - Set Origin Plus
  2. You will get an option Set Origin Plus in your sidebar
  3. Now select all objects
  4. Go to Edit mode
  5. Select all faces at whose center, the origins will be set
  6. Open side bar
  7. Go to set origin plus
  8. Click on update origins
  9. Job is done

Set origin plus - Youtube tutorial

Gumroad link for addon

Sample image of addon

$\endgroup$
1
  • 1
    $\begingroup$ Argh, went through the bother of purchasing that "Addon".. Turns out it's just a "Set pivot to selected", nothing like "set pivot to lower-left-corner-of-bounding-box-with-options that one would expect from a paid addon. Completely useless, there's many better free scripts out there! $\endgroup$ – Fritsl Apr 2 at 16:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.