import bpy from bpy_extras import view3d_utils def main(context, event): """Run this function on left mouse, execute the ray cast""" # get the context arguments scene = context.scene region = context.region rv3d = context.region_data coord = event.mouse_region_x, event.mouse_region_y # get the ray from the viewport and mouse view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, coord) ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, coord) ray_target = ray_origin + view_vector def visible_objects_and_duplis(): """Loop over (object, matrix) pairs (mesh only)""" depsgraph = context.evaluated_depsgraph_get() for dup in depsgraph.object_instances: if dup.is_instance: # Real dupli instance obj = dup.instance_object yield (obj, dup.matrix_world.copy()) else: # Usual object obj = dup.object yield (obj, obj.matrix_world.copy()) def obj_ray_cast(obj, matrix): """Wrapper for ray casting that moves the ray into object space""" # get the ray relative to the object matrix_inv = matrix.inverted() ray_origin_obj = matrix_inv @ ray_origin ray_target_obj = matrix_inv @ ray_target ray_direction_obj = ray_target_obj - ray_origin_obj # cast the ray success, location, normal, face_index = obj.ray_cast(ray_origin_obj, ray_direction_obj) if success: return location, normal, face_index else: return None, None, None # cast rays and find the closest object best_length_squared = -1.0 best_obj = None for obj, matrix in visible_objects_and_duplis(): if obj.type == 'MESH': hit, normal, face_index = obj_ray_cast(obj, matrix) if hit is not None: hit_world = matrix @ hit scene.cursor.location = hit_world length_squared = (hit_world - ray_origin).length_squared if best_obj is None or length_squared < best_length_squared: best_length_squared = length_squared best_obj = obj # now we have the object under the mouse cursor, # we could do lots of stuff but for the example just select. if best_obj is not None: # for selection etc. we need the original object, # evaluated objects are not in viewlayer best_original = best_obj.original best_original.select_set(True) context.view_layer.objects.active = best_original class ViewOperatorRayCast(bpy.types.Operator): """Modal object selection with a ray cast""" bl_idname = "view3d.modal_operator_raycast" bl_label = "RayCast View Operator" def modal(self, context, event): if event.type in {'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}: # allow navigation return {'PASS_THROUGH'} elif event.type == 'LEFTMOUSE': main(context, event) return {'RUNNING_MODAL'} elif event.type in {'RIGHTMOUSE', 'ESC'}: return {'CANCELLED'} return {'RUNNING_MODAL'} def invoke(self, context, event): if context.space_data.type == 'VIEW_3D': context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} else: self.report({'WARNING'}, "Active space must be a View3d") return {'CANCELLED'} def menu_func(self, context): self.layout.operator(ViewOperatorRayCast.bl_idname, text="Raycast View Modal Operator") # Register and add to the "view" menu (required to also use F3 search "Raycast View Modal Operator" for quick access). def register(): bpy.utils.register_class(ViewOperatorRayCast) bpy.types.VIEW3D_MT_view.append(menu_func) def unregister(): bpy.utils.unregister_class(ViewOperatorRayCast) bpy.types.VIEW3D_MT_view.remove(menu_func) if __name__ == "__main__": register()