4
$\begingroup$

I want to make a custom gizmo to set the origin. I mean when we select the 3d model a cube (subdivided cube) like the below image will appear on the 3d model and scale based on 3d model size dimension.

and when we select the vertex it should set the origin based on vertex position.

enter image description here

suppose we have a 3d model. when I select the 3d model gizmos should appear

enter image description here

first I select the point on gizmos

enter image description here

and then I set the origin

enter image description here

and finally, I will delete the custom gizmos because I don't need it

enter image description here

I want to make these steps automatic because I have thousand of 3d models

bpy.ops.object.editmode_toggle()
bpy.ops.view3d.snap_cursor_to_selected()
bpy.ops.object.editmode_toggle()
bpy.ops.object.origin_set(type='ORIGIN_CURSOR', center='MEDIAN')

$\endgroup$
2
  • $\begingroup$ Would you accept a "non gizmo" approach? $\endgroup$
    – lemon
    Sep 2 '20 at 15:12
  • $\begingroup$ @lemon feel free to answer no problem. thank you $\endgroup$ Sep 2 '20 at 15:48
8
$\begingroup$

I propose to do it with a modal operator, not a gizmo:

enter image description here

The principles:

Run the operator (search it using its name F3 and type set origin on boundings). Then the operator creates a sphere to display it as green point as shown above. And it register a draw handler in the 3D view, in order to display the boundings and the small green spheres.

The operator is a modal so it will continue until 'FINISHED' or if 'CANCELED' is returned. In this case, it returns 'CANCELED' when the ESC key is hit. Now for each modal call, it does the following:

  • Get the view location
  • Get the mouse cursor location
  • Get the active object

And from that the bounds of the object are going to be calculated in order to display a black subdivided mesh (some kind of lattice) as well as figuring out which point to highlight in green.

Here we can use the dot product the view location/mouse location with view location/bouding point location and keep the best one. When all that done, validating the point where to set the object origin:

If you left-click (LMB) then the previously identified bounding point will be used to change the origin. It seems to work fine independently of the mode (object, edit, etc.) but I've not tested them all.

You can stop the operator using ESC. Here is the code (some comments, just ask for more information if necessary):

import bpy
import bmesh
import gpu
from gpu_extras.batch import batch_for_shader
from mathutils import Vector, Matrix
from bpy_extras.view3d_utils import region_2d_to_location_3d

# indices to make cube edges from boundings
bounding_indices = (
    (0, 1), (1, 2), (2, 3), (3, 4), #up
    (4, 5), (5, 6), (6, 7), (7, 0),
    (8, 1), (8, 3), (8, 5), (8, 7),
    (9, 10), (10, 11), (11, 12), (12, 13), #slide
    (13, 14), (14, 15), (15, 16), (16, 9), #down  
    (17, 18), (18, 19), (19, 20), (20, 21), 
    (21, 22),(22, 23), (23, 24), (24, 17), 
    (25, 18), (25, 20), (25, 22), (25, 24),
    (0, 9), (1, 10), (2, 11), (3, 12), # transversal
    (4, 13), (5, 14), (6,15), (7, 16),
    (9, 17), (10, 18), (11, 19), (12, 20),
    (13, 21), (14, 22), (15, 23), (16, 24))    
    

# Cut a bounding face in half
def bounding_cuts(b_box, result, indices):
    p0 = Vector(b_box[indices[0]])
    p1 = Vector(b_box[indices[1]])
    p2 = Vector(b_box[indices[2]])
    p3 = Vector(b_box[indices[3]])
    result.append(p0)
    result.append((p0 + p1) / 2.0)
    result.append(p1)
    result.append((p1 + p2) / 2.0)
    result.append(p2)
    result.append((p2 + p3) / 2.0)
    result.append(p3)
    result.append((p3 + p0) / 2.0)
    result.append((p0 + p1 + p2 + p3) / 4.0)
    
def up_down_slide(b_box):
    up = []
    down = []
    slide = []
    bounding_cuts(b_box, up, [0, 1, 2, 3]) 
    bounding_cuts(b_box, down, [4, 5, 6, 7])
    slide = [(p0+p1)/2.0 for p0,p1 in zip(up[:-1],down[:-1])]
    return up, down, slide 

# Get half parts of bounding box
def bounding_points(obj):
    b_box = obj.bound_box[:]
    up, down, slide = up_down_slide(b_box)
    result = up + slide + down
    return result

def draw_callback(self, context):
    obj = context.active_object
    if obj and obj.type == 'MESH':
        # translate bounding in world co
        mat = obj.matrix_world 
        obj_coords = [mat @ Vector(c) for c in bounding_points(obj)]

        # draw boudings        
        shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
        shader.bind()
        batch = batch_for_shader(shader, 'LINES', {"pos": obj_coords}, indices=bounding_indices)
        shader.uniform_float("color", (0, 0, 0, 0.5))
        batch.draw(shader)

        # if close enough to a corner        
        if self.highlight != -1:
            # draw a small sphere on it        
            scale = self.highlight_distance / 100.0
            base_coord = obj_coords[self.highlight]
            h_coords = [base_coord + (Vector(c) * scale) for c in self.highlight_coords]
            batch = batch_for_shader(shader, 'TRIS', {"pos": h_coords})
            shader.uniform_float("color", (0, 1, 0, 0.5))
            batch.draw(shader)

class ModalOperator(bpy.types.Operator):
    """Set origin with the mouse"""
    bl_idname = "object.set_origin_on_boundings"
    bl_label = "Set origin on boundings"
     
    # Create highlight (corner) geometry
    def create_highlight(self):
        # it is based on UV sphere triangulated for the GPU rendering
        bm = bmesh.new()
        bmesh.ops.create_uvsphere(bm, u_segments= 6, v_segments=4, diameter=1)
        bmesh.ops.triangulate(bm, faces=bm.faces)
        self.highlight_coords = [v.co.to_tuple() for f in bm.faces for v in f.verts]

    # Update object information     
    def update_object(self, context, obj, view_loc, mouse_loc):
        prev_highlight = self.highlight
        self.highlight = -1
        if obj and obj.type == 'MESH':
            self.target = obj.name

            # Find the closest corner using a dot product from the view
            mat = obj.matrix_world 
            obj_coords = [mat @ Vector(c) for c in bounding_points(obj)]
            
            axis = (mouse_loc - view_loc).normalized()
            angles = [axis.dot((c - view_loc).normalized()) for c in obj_coords]
            min_angle = max(angles)

            # and keep it if close enough
            if min_angle > 0.999:
                self.highlight = angles.index(min_angle)
                self.highlight_distance = (view_loc - obj_coords[self.highlight]).length

            if self.highlight != prev_highlight:
                context.area.tag_redraw()                

    # Mouse location in 3D 
    def mouse_location(self, context, event):
        x, y = event.mouse_region_x, event.mouse_region_y
        loc = region_2d_to_location_3d(context.region, context.space_data.region_3d, (x, y), (0, 0, 0))
        return loc

    # View location in 3D
    def view_location(self, context):
        camera_info = context.space_data.region_3d.view_matrix.inverted()
        return camera_info.translation

    # Move the object origin to the wanted corner
    def set_origin(self, context, obj, index):
        mat = obj.matrix_world 
        loc = mat @ Vector(bounding_points(obj)[index])
        mode = obj.mode
        me = obj.data
        if mode == 'OBJECT':        
            mat0 =mat.copy()
            t = loc - mat @ Vector()
            mat.translation += t                         
            local = mat.inverted() @ mat0 #move verts back
            me.transform(local)
        else:
            local = mat.inverted() @ loc #move verts back (in prevision of next operation)
            ml = Matrix.Translation(-local)
            bm = bmesh.from_edit_mesh(me)                                
            bm.transform(ml)
            bmesh.update_edit_mesh(me)

            t = loc - mat @ Vector()
            mat.translation += t #move origin and verts                

    # Force the 3D view to redraw when needed
    def redraw(self, context):
        bpy.ops.wm.redraw_timer(type='DRAW_SWAP', iterations=1)
        
    def modal(self, context, event):

        mouse_loc = self.mouse_location(context, event)
        view_loc = self.view_location(context)
        obj = context.active_object

        self.update_object(context, obj, view_loc, mouse_loc)
        
        # when left click, set the origin if a highlight point is known
        if event.type in {'LEFTMOUSE'}:
            if self.highlight != -1:
                self.set_origin(context, obj, self.highlight)
                return {'RUNNING_MODAL'} #We don't want the click to be taken into account further

        # esc is used to quit and remove the draw handler
        elif event.type in {'ESC'}:
            bpy.types.SpaceView3D.draw_handler_remove(self.handle, 'WINDOW')
            context.area.tag_redraw()
            return {'CANCELLED'}

        return {'PASS_THROUGH'}

    def invoke(self, context, event):
        self.highlight = -1
        if context.area.type == 'VIEW_3D':
            self.create_highlight()
            args = (self, context)
            self.handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback, args, 'WINDOW', 'POST_VIEW')
            context.window_manager.modal_handler_add(self)
            context.area.tag_redraw()
            return {'RUNNING_MODAL'}
        return {'FINISHED'}

keymaps = []

def register():
    bpy.utils.register_class(ModalOperator)

    wm = bpy.context.window_manager
    km = wm.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D")

    # Ctrl+Shif+B as shortcut
    kmi = km.keymap_items.new(ModalOperator.bl_idname, 'B', 'PRESS', ctrl=True, shift=True)

    keymaps.append((km, kmi))

def unregister():
    for km, kmi in keymaps:
        km.keymap_items.remove(kmi)
    keymaps.clear()

    bpy.utils.unregister_class(ModalOperator)


if __name__ == "__main__":
    register()

    # test call
    bpy.ops.object.set_origin_on_boundings('INVOKE_DEFAULT')

Run the script and use ctrlshiftB to activate the operator and esc to stop it.

Note: I'm still using right click select, so I've set the trigger that position the origin on the left click. You may want to change that.

$\endgroup$
17
  • $\begingroup$ Really nice answer! $\endgroup$
    – brockmann
    Sep 2 '20 at 19:23
  • $\begingroup$ Hi thanks man this is exactly what I want. I ran the operator and searched for set origin on bounding but I couldn't find that. I use blender 2.83 $\endgroup$ Sep 2 '20 at 19:30
  • 2
    $\begingroup$ Can confirm for me it works straight out of the box. @lemon had something similar in mind for blender.stackexchange.com/a/163258/15543 $\endgroup$
    – batFINGER
    Sep 3 '20 at 5:43
  • 1
    $\begingroup$ @SeyedMortezaKamali, have added ctrl+shift+b as shortcut in meantime (answer edited). $\endgroup$
    – lemon
    Sep 3 '20 at 5:54
  • 1
    $\begingroup$ @SeyedMortezaKamali, the way I did it is not the best for what you want to do as vertices of the boundings are duplicated here (for simplicity when wrote it). I think you should rework it a bit so that bounding_points function return 1 point per location and so you'll need to change the bounding_indices appropriately. Feel free to ask if not clear enough. Once done, the result of bounding_points function will give you the coordinates (just to be transformed with obj world matrix to have them in world space if this is what you want to display) $\endgroup$
    – lemon
    Sep 5 '20 at 6:42

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.