4

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')

1 Answers1

9

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.

FkNWO
  • 74
  • 1
  • 7
lemon
  • 60,295
  • 3
  • 66
  • 136
  • Really nice answer! – brockmann Sep 02 '20 at 19:23
  • 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 – Seyed Morteza Kamali Sep 02 '20 at 19:30
  • @brockmann sorry I don't know why it didn't work. I don't know why modal operators don't work for me – Seyed Morteza Kamali Sep 03 '20 at 01:50
  • @SeyedMortezaKamali have you ran the script before searching? alt+p in the text editor – lemon Sep 03 '20 at 05:05
  • @brockmann, thanks for the edit! – lemon Sep 03 '20 at 05:07
  • @lemon yes I'm sure, It's strange!!! I want to accept your answer but I can't test your modal operator https://i.stack.imgur.com/QJem6.gif – Seyed Morteza Kamali Sep 03 '20 at 05:12
  • @SeyedMortezaKamali, strange... will try to find why... will edit in few minutes as there is a bug (but not related to the issue you have) – lemon Sep 03 '20 at 05:17
  • @lemon thank you. can you add a shortcut or menu because I think it will work by shortcut.because I tested in blender 2.8 and 2.9 and it didn't work – Seyed Morteza Kamali Sep 03 '20 at 05:18
  • @SeyedMortezaKamali, could you check the console after running the script? – lemon Sep 03 '20 at 05:25
  • @lemon I restarted my computer and it works. sorry It seems it's kind of bug related to blender – Seyed Morteza Kamali Sep 03 '20 at 05:39
  • 2
    Can confirm for me it works straight out of the box. @lemon had something similar in mind for https://blender.stackexchange.com/a/163258/15543 – batFINGER Sep 03 '20 at 05:43
  • 1
    @SeyedMortezaKamali, have added ctrl+shift+b as shortcut in meantime (answer edited). – lemon Sep 03 '20 at 05:54
  • @batFINGER, thanks for the link. Any idea how to make it with gizmo? – lemon Sep 03 '20 at 05:54
  • @lemon I was reading your code. can you tell me how can I give the bounding box position for example bottom center position Vector(0,-1,0) because I want to add a label to them – Seyed Morteza Kamali Sep 05 '20 at 05:14
  • @SeyedMortezaKamali, you want to have the 3d co from the code i provided? Is that it? – lemon Sep 05 '20 at 05:46
  • @lemon I want to show something on the vertex position https://i.stack.imgur.com/VuvsK.png my question is did you calculate vertex positions or should I calculate again? – Seyed Morteza Kamali Sep 05 '20 at 06:37
  • 1
    @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) – lemon Sep 05 '20 at 06:42