Cursor gismow extension (gpu): how to keep a constant size with depth

the default cursor has a constant size, whatever the depth is.
I want to do the same on an extension of the cursor gizmo, to show better the orientation, when running a modal.
so far I did this (I don’t really master the shader colors part…)


code:

import bpy
import bgl
import gpu
from gpu_extras.batch import batch_for_shader

vertex_shader = '''
uniform mat4 ModelViewProjectionMatrix;
in vec3 pos;
in vec4 color;
out vec4 finalColor;
void main()
{
    gl_Position = ModelViewProjectionMatrix * vec4(pos, 1.0);
    gl_Position.z -= 0.001;
    finalColor = color;
}
'''

fragment_shader = """
in vec4 finalColor;
in vec4 fragCoord;
out vec4 fragColor;
out float fragDepth;
void main()
{
    vec2 coord = gl_PointCoord - vec2(0.5, 0.5);
    fragColor = finalColor;
    fragDepth = 0;
}   
"""

def draw_callback_3d(self,context):
    curs = bpy.context.scene.cursor
    loc = curs.location
    coords = [loc, (loc[0]+1, loc[1], loc[2]),
                loc, (loc[0], loc[1]+1, loc[2]),  
                loc, (loc[0], loc[1], loc[2]+1)]  
    colors = ((0.8, 0, 0, 0.9), (0.8, 0, 0, 0.9),    
            (0, 0.8, 0, 0.9), (0, 0.8, 0, 0.9),    
            (0, 0, 0.8, 0.9), (0, 0, 0.8, 0.9), )   
    shader = gpu.types.GPUShader(vertex_shader, fragment_shader) #simpler way?
    batch = batch_for_shader(shader, 'LINES', {"pos": coords, "color": colors})

#    bgl.glEnable(bgl.GL_MULTISAMPLE) #what for? not in the API
    bgl.glEnable(bgl.GL_BLEND)
    bgl.glEnable(bgl.GL_LINE_SMOOTH)
    bgl.glDisable(bgl.GL_DEPTH_TEST) #on top             
    bgl.glLineWidth(3)
    shader.bind()
    batch.draw(shader)

class OT_draw_operator(bpy.types.Operator):
    bl_idname = "object.draw_op"
    bl_label = "Draw operator"

    def invoke(self, context, event):
        args = (self, context)
        self.draw_handle_3d = bpy.types.SpaceView3D.draw_handler_add(draw_callback_3d, args, 'WINDOW', 'POST_VIEW')
        context.area.tag_redraw()
        context.window_manager.modal_handler_add(self)

        return {"RUNNING_MODAL"}
            
    def modal(self, context, event):
                           
        if event.type in {"ESC"} and event.value == "PRESS":
            bpy.types.SpaceView3D.draw_handler_remove(self.draw_handle_3d, 'WINDOW')
            context.area.tag_redraw()
            return {'FINISHED'}            
                            
        return {"PASS_THROUGH"}
        
def register():
    bpy.utils.register_class(OT_draw_operator)

if __name__ == "__main__":
    register()
4 Likes

well finally I have this. you can resize the gizmo pressing right mouse and going up or down
gizmo cursor

import bpy
import bgl
import gpu
from gpu_extras.batch import batch_for_shader
from bpy_extras import view3d_utils

vertex_shader = '''
uniform mat4 ModelViewProjectionMatrix;
in vec3 pos;
in vec4 color;
out vec4 finalColor;
void main()
{
    gl_Position = ModelViewProjectionMatrix * vec4(pos, 1.0);
    gl_Position.z -= 0.001;
    finalColor = color;
}
'''

fragment_shader = """
in vec4 finalColor;
in vec4 fragCoord;
out vec4 fragColor;
out float fragDepth;
void main()
{
    vec2 coord = gl_PointCoord - vec2(0.5, 0.5);
    fragColor = finalColor;
    fragDepth = 0;
}   
"""

def draw_callback_3d(self,context):
    curs = bpy.context.scene.cursor
    loc = curs.location
    
    def to2D(loc):
        region = context.region
        rv3d = context.region_data
        return view3d_utils.location_3d_to_region_2d(region, rv3d, loc) 
    
    offset = self.offset    
    coords = [loc, (loc[0]+offset, loc[1], loc[2]),
                loc, (loc[0], loc[1]+offset, loc[2]),  
                loc, (loc[0], loc[1], loc[2]+offset)] 

    colors = ((0.8, 0, 0, 0.9), (0.8, 0, 0, 0.9),    
            (0, 0.8, 0, 0.9), (0, 0.8, 0, 0.9),    
            (0, 0, 0.8, 0.9), (0, 0, 0.8, 0.9), )   
    shader = gpu.types.GPUShader(vertex_shader, fragment_shader) #simpler way?
    batch = batch_for_shader(shader, 'LINES', {"pos": coords, "color": colors})
    bgl.glEnable(bgl.GL_BLEND)           
    bgl.glEnable(bgl.GL_LINE_SMOOTH)
    bgl.glDisable(bgl.GL_DEPTH_TEST) #on top  
    bgl.glLineWidth(3)
    shader.bind()
    batch.draw(shader)    
    # restore opengl defaults
    bgl.glLineWidth(1)
    bgl.glDisable(bgl.GL_BLEND) 

class OT_draw_operator(bpy.types.Operator):
    bl_idname = "object.draw_op"
    bl_label = "Draw operator"

    def invoke(self, context, event):
        self.right_press = None
        self.right_release = None
        self.pos = None
        self.offset = 1
        self.up = None
        self.down = None
        args = (self, context)
        self.draw_handle_3d = bpy.types.SpaceView3D.draw_handler_add(draw_callback_3d, args, 'WINDOW', 'POST_VIEW')
        context.area.tag_redraw()
        context.window_manager.modal_handler_add(self)

        return {"RUNNING_MODAL"}
            
    def modal(self, context, event):
                           
        if event.type in {"ESC"} and event.value == "PRESS":
            bpy.types.SpaceView3D.draw_handler_remove(self.draw_handle_3d, 'WINDOW')
            context.area.tag_redraw()
            return {'FINISHED'}            

        if event.type == 'RIGHTMOUSE':
            self.right_press = event.value == 'PRESS'
            self.right_release = event.value == 'RELEASE'            
            self.pos = (event.mouse_region_x, event.mouse_region_y)

        if self.right_press and event.type == 'MOUSEMOVE': #WHEELUPMOUSE
            if event.mouse_region_y > self.pos[1]+5:
                self.up = True
                self.down = False
            elif event.mouse_region_y < self.pos[1]-5:
                self.down = True
                self.up = False
            else:
                self.down = False
                self.up = False  
                              
        if (self.up or self.down) and self.right_press and not self.right_release:
            if self.up:
                self.offset += 0.15
                context.area.tag_redraw()
                self.pos  = (event.mouse_region_x, event.mouse_region_y)
            elif self.down:
                if self.offset > 0.16:
                    self.offset -= 0.15 
                context.area.tag_redraw()
                self.pos  = (event.mouse_region_x, event.mouse_region_y)                                        
                   
        return {"RUNNING_MODAL"}
        
def register():
    bpy.utils.register_class(OT_draw_operator)

if __name__ == "__main__":
    register()
2 Likes

from there I should find a way to set this size with the depth. but this is enough for today on this

1 Like

Ok then now I have a constant size and that’ much better!
BTW you can still resize gizmo (rightmouse+wheelmouse)
1200

import bpy
import bgl
import gpu
from mathutils import Vector
from gpu_extras.batch import batch_for_shader

vertex_shader = '''
uniform mat4 ModelViewProjectionMatrix;
in vec3 pos;
in vec4 color;
out vec4 finalColor;
void main()
{
    gl_Position = ModelViewProjectionMatrix * vec4(pos, 1.0);
    gl_Position.z -= 0.001;
    finalColor = color;
}
'''

fragment_shader = """
in vec4 finalColor;
in vec4 fragCoord;
out vec4 fragColor;
out float fragDepth;
void main()
{
    vec2 coord = gl_PointCoord - vec2(0.5, 0.5);
    fragColor = finalColor;
    fragDepth = 0;
}   
"""


def draw_callback_3d(self, context):
    curs = context.scene.cursor
    mat = curs.matrix
    offset = self.offset
    # constant size
    view_info = context.space_data.region_3d.view_matrix.inverted()
    view_loc = view_info.translation
    depth = (view_loc - curs.location).length
    scale = depth/12

    coords = [mat@Vector((0, 0, 0)), mat@Vector((offset*scale, 0, 0)),
              mat@Vector((0, 0, 0)), mat@Vector((0, offset*scale, 0)),
              mat@Vector((0, 0, 0)), mat@Vector((0, 0, offset*scale))]

    colors = ((0.8, 0, 0, 0.9), (0.8, 0, 0, 0.9),
              (0, 0.8, 0, 0.9), (0, 0.8, 0, 0.9),
              (0, 0, 0.8, 0.9), (0, 0, 0.8, 0.9), )
    shader = gpu.types.GPUShader(
        vertex_shader, fragment_shader)  # simpler way?
    batch = batch_for_shader(shader, 'LINES', {"pos": coords, "color": colors})
    bgl.glEnable(bgl.GL_BLEND)
    bgl.glLineWidth(3)
    shader.bind()
    batch.draw(shader)
    # restore opengl defaults
    bgl.glLineWidth(1)
    bgl.glDisable(bgl.GL_BLEND)


class ROTATE_cursor(bpy.types.Operator):
    """rotate cursor R + axe + value / backspace to delete"""
    bl_idname = "rotate.cursor"
    bl_label = " Cursor rotation Modal"

    def modal(self, context, event):

        if event.type == 'RIGHTMOUSE':
            self.right_press = event.value == 'PRESS'
            return {'RUNNING_MODAL'}

        if self.right_press and event.type == 'WHEELUPMOUSE':  # rightmouse+wheel to change gizmo size
            self.offset += 0.15
            context.area.tag_redraw()
            return {'RUNNING_MODAL'}

        if (
            self.right_press
            and event.type == 'WHEELDOWNMOUSE'
            and self.offset > 0.16
        ):
            self.offset -= 0.15
            context.area.tag_redraw()
            return {'RUNNING_MODAL'}

        if event.type == 'RET' and event.value == 'PRESS':
            bpy.types.SpaceView3D.draw_handler_remove(
                self.draw_handle_3d, 'WINDOW')
            context.area.tag_redraw()
            self.report({'WARNING'}, "Modal Finished")
            return {'FINISHED'}

        if event.type in {'LEFTMOUSE', 'MOUSEMOVE', 'EVT_TWEAK_L', 'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE',
                          'NUMPAD_0', 'NUMPAD_1', 'NUMPAD_2', 'NUMPAD_3', 'NUMPAD_4',
                          'NUMPAD_5', 'NUMPAD_6', 'NUMPAD_7', 'NUMPAD_8', 'NUMPAD_9', 'NUMPAD_PERIOD'}:
            return {'PASS_THROUGH'}

        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        self.curs_rot = context.scene.cursor.rotation_euler.copy()
        # gpu variables
        self.right_press = None
        self.offset = 1

        args = (self, context)
        self.draw_handle_3d = bpy.types.SpaceView3D.draw_handler_add(
            draw_callback_3d, args, 'WINDOW', 'POST_VIEW')
        context.area.tag_redraw()
        context.window_manager.modal_handler_add(self)

        return {'RUNNING_MODAL'}


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


def unregister():
    bpy.utils.unregister_class(ROTATE_cursor)


if __name__ == "__main__":
    register()
2 Likes

Loving this! I used to have a hacked-together way of being able to display the gizmos in an inactive state (so I couldn’t use them, it was just a way to see the orientation), but that no longer works. What’s the intent for this? Part of a bigger addon, or an addon that shows the orientation?

It will be part of a much bigger addon. I need to do a modal to rotate cursor in edit mode

1 Like

finally to make a modal to rotate the cursor it took me 2 lines.
it was already there in Blender!
pressing T (transform move) you can do all move operation as G on a normal object, but on the cursor
Then press left mouse/space/enter to confirm
Or press R to turn the operation in rotate. same as R but on cursor.
final1920

import bpy
import bgl
import gpu
from mathutils import Vector
from gpu_extras.batch import batch_for_shader

vertex_shader = '''
uniform mat4 ModelViewProjectionMatrix;
in vec3 pos;
in vec4 color;
out vec4 finalColor;
void main()
{
    gl_Position = ModelViewProjectionMatrix * vec4(pos, 1.0);
    gl_Position.z -= 0.001;
    finalColor = color;
}
'''

fragment_shader = """
in vec4 finalColor;
in vec4 fragCoord;
out vec4 fragColor;
out float fragDepth;
void main()
{
    vec2 coord = gl_PointCoord - vec2(0.5, 0.5);
    fragColor = finalColor;
    fragDepth = 0;
}   
"""


def draw_callback_3d(self, context):
    curs = context.scene.cursor
    mat = curs.matrix
    offset = self.offset
    # constant size
    view_info = context.space_data.region_3d.view_matrix.inverted()
    view_loc = view_info.translation
    depth = (view_loc - curs.location).length
    scale = depth/12

    coords = [mat@Vector((0, 0, 0)), mat@Vector((offset*scale, 0, 0)),
              mat@Vector((0, 0, 0)), mat@Vector((0, offset*scale, 0)),
              mat@Vector((0, 0, 0)), mat@Vector((0, 0, offset*scale))]

    colors = ((0.8, 0, 0, 0.9), (0.8, 0, 0, 0.9),
              (0, 0.8, 0, 0.9), (0, 0.8, 0, 0.9),
              (0, 0, 0.8, 0.9), (0, 0, 0.8, 0.9), )
    shader = gpu.types.GPUShader(
        vertex_shader, fragment_shader)  # simpler way?
    batch = batch_for_shader(shader, 'LINES', {"pos": coords, "color": colors})
    bgl.glEnable(bgl.GL_BLEND)
    bgl.glLineWidth(3)
    bgl.glEnable(bgl.GL_LINE_SMOOTH)
    bgl.glEnable(bgl.GL_MULTISAMPLE)
    shader.bind()
    batch.draw(shader)
    # restore opengl defaults
    bgl.glLineWidth(1)
    bgl.glDisable(bgl.GL_BLEND)


class ROTATE_cursor(bpy.types.Operator):
    """rotate cursor R + axe + value / backspace to delete"""
    bl_idname = "rotate.cursor"
    bl_label = " Cursor rotation Modal"

    def modal(self, context, event):
        
        if event.type == 'T' and event.value == 'PRESS': #then R
            bpy.ops.transform.translate('INVOKE_DEFAULT', cursor_transform=True, release_confirm=False)

        if event.type == 'RIGHTMOUSE':
            self.right_press = event.value == 'PRESS'
            return {'RUNNING_MODAL'}

        if self.right_press and event.type == 'WHEELUPMOUSE':  # rightmouse+wheel to change gizmo size
            self.offset += 0.15
            context.area.tag_redraw()
            return {'RUNNING_MODAL'}

        if (
            self.right_press
            and event.type == 'WHEELDOWNMOUSE'
            and self.offset > 0.16
        ):
            self.offset -= 0.15
            context.area.tag_redraw()
            return {'RUNNING_MODAL'}

        if event.type == 'RET' and event.value == 'PRESS':
            bpy.types.SpaceView3D.draw_handler_remove(
                self.draw_handle_3d, 'WINDOW')
            context.area.tag_redraw()
            self.report({'WARNING'}, "Modal Finished")
            return {'FINISHED'}

        if event.type in {'LEFTMOUSE', 'MOUSEMOVE', 'EVT_TWEAK_L', 'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE',
                          'NUMPAD_0', 'NUMPAD_1', 'NUMPAD_2', 'NUMPAD_3', 'NUMPAD_4',
                          'NUMPAD_5', 'NUMPAD_6', 'NUMPAD_7', 'NUMPAD_8', 'NUMPAD_9', 'NUMPAD_PERIOD'}:
            print(event.type)
            return {'PASS_THROUGH'}

        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        self.curs_rot = context.scene.cursor.rotation_euler.copy()
        # gpu variables
        self.right_press = None
        self.offset = 1

        args = (self, context)
        self.draw_handle_3d = bpy.types.SpaceView3D.draw_handler_add(
            draw_callback_3d, args, 'WINDOW', 'POST_VIEW')
        context.area.tag_redraw()
        context.window_manager.modal_handler_add(self)

        return {'RUNNING_MODAL'}


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


def unregister():
    bpy.utils.unregister_class(ROTATE_cursor)


if __name__ == "__main__":
    register()

script a little more advanced. if the modal crash, restarting it things are ok, the previous handler removed and added. because using a class-method. to adapt it I had to update the handler too if changing gizmo size

import bpy
import bgl
import gpu
from mathutils import Vector
from gpu_extras.batch import batch_for_shader

vertex_shader = '''
uniform mat4 ModelViewProjectionMatrix;
in vec3 pos;
in vec4 color;
out vec4 finalColor;
void main()
{
    gl_Position = ModelViewProjectionMatrix * vec4(pos, 1.0);
    gl_Position.z -= 0.001;
    finalColor = color;
}
'''

fragment_shader = """
in vec4 finalColor;
in vec4 fragCoord;
out vec4 fragColor;
out float fragDepth;
void main()
{
    vec2 coord = gl_PointCoord - vec2(0.5, 0.5);
    fragColor = finalColor;
    fragDepth = 0;
}   
"""


def draw_callback_3d(self, context, offset):
    curs = context.scene.cursor
    mat = curs.matrix
    # constant size
    view_info = context.space_data.region_3d.view_matrix.inverted()
    view_loc = view_info.translation
    depth = (view_loc - curs.location).length
    scale = depth/12

    coords = [mat@Vector((0, 0, 0)), mat@Vector((offset*scale, 0, 0)),
              mat@Vector((0, 0, 0)), mat@Vector((0, offset*scale, 0)),
              mat@Vector((0, 0, 0)), mat@Vector((0, 0, offset*scale))]

    colors = ((0.8, 0, 0, 0.9), (0.8, 0, 0, 0.9),
              (0, 0.8, 0, 0.9), (0, 0.8, 0, 0.9),
              (0, 0, 0.8, 0.9), (0, 0, 0.8, 0.9), )
    shader = gpu.types.GPUShader(
        vertex_shader, fragment_shader)  # simpler way?
    batch = batch_for_shader(shader, 'LINES', {"pos": coords, "color": colors})
    bgl.glEnable(bgl.GL_BLEND)
    bgl.glLineWidth(3)
    bgl.glEnable(bgl.GL_LINE_SMOOTH)
    bgl.glEnable(bgl.GL_MULTISAMPLE)
    shader.bind()
    batch.draw(shader)
    # restore opengl defaults
    bgl.glLineWidth(1)
    bgl.glDisable(bgl.GL_BLEND)


class ROTATE_cursor(bpy.types.Operator):
    """rotate cursor R + axe + value / backspace to delete"""
    bl_idname = "rotate.cursor"
    bl_label = " Cursor rotation Modal"
    _handler1 = _context = None
    offset = 1
    
    @classmethod
    def _add_handler1(cls, context, offset):
        args = draw_callback_3d, (cls, context, offset), 'WINDOW', 'POST_VIEW'
        cls._remove_handler1(context)
        setattr(cls, '_handler1', context.space_data.draw_handler_add(*args))

    @classmethod
    def _remove_handler1(cls, context):
        handler = getattr(cls, '_handler1', None)
        if handler is not None:
            try:
                context.space_data.draw_handler_remove(handler, 'WINDOW')
            except ValueError:
                pass
        setattr(cls, '_handler1', None)

    def modal(self, context, event):
        setattr(__class__, '_context', context)
        if event.type == 'T' and event.value == 'PRESS': #then R
            bpy.ops.transform.translate('INVOKE_DEFAULT', cursor_transform=True, release_confirm=False)

        if event.type == 'RIGHTMOUSE':
            self.right_press = event.value == 'PRESS'
            return {'RUNNING_MODAL'}

        if self.right_press and event.type == 'WHEELUPMOUSE':  # rightmouse+wheel to change gizmo size
            self.offset += 0.15
            self._add_handler1(context, self.offset)
            context.area.tag_redraw()
            return {'RUNNING_MODAL'}

        if (
            self.right_press
            and event.type == 'WHEELDOWNMOUSE'
            and self.offset > 0.16
        ):
            self.offset -= 0.15
            self._add_handler1(context, self.offset)
            context.area.tag_redraw()
            return {'RUNNING_MODAL'}

        if event.type in {'SPACE', 'RET'} and event.value == 'PRESS':
            # bpy.types.SpaceView3D.draw_handler_remove(
                # self.draw_handle_3d, 'WINDOW')
            # context.area.tag_redraw()
            self.report({'WARNING'}, "Modal Finished")
            return {'FINISHED'}

        if event.type in {'LEFTMOUSE', 'MOUSEMOVE', 'EVT_TWEAK_L', 'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE',
                          'NUMPAD_0', 'NUMPAD_1', 'NUMPAD_2', 'NUMPAD_3', 'NUMPAD_4',
                          'NUMPAD_5', 'NUMPAD_6', 'NUMPAD_7', 'NUMPAD_8', 'NUMPAD_9', 'NUMPAD_PERIOD'}:
            print(event.type)
            return {'PASS_THROUGH'}

        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        self.curs_rot = context.scene.cursor.rotation_euler.copy()
        # gpu variables
        self.right_press = None
        # self.offset = 1

        # args = (self, context)
        # self.draw_handle_3d = bpy.types.SpaceView3D.draw_handler_add(
            # draw_callback_3d, args, 'WINDOW', 'POST_VIEW')
        self._add_handler1(context, self.offset)
        context.area.tag_redraw()
        context.window_manager.modal_handler_add(self)

        return {'RUNNING_MODAL'}
        
    def __del__(self):
        context = getattr(__class__, '_context', None)
        if context is not None:
            self._remove_handler1(self._context)
            self._context.area.tag_redraw()

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

def unregister():
    bpy.utils.unregister_class(ROTATE_cursor)

if __name__ == "__main__":
    register()

Hi, can you explain a bit how you were able to remove the handler after the modal has crashed. I have a draw handler getting stuck after the modal has ended due to error.

Is it the setattr that makes this possible?

setattr is creating the class variable even if it doesn’t exist.
the variable _handler is keeping the reference of the previous handler, even if the operator is ended. because if you run an operator an instance is created. so after finish instance variables don’t exist anymore. but with a class variable and some class method to use it, this is still there, because blender has registered the class…

1 Like

Nice I have been looking everywhere for a solution. What is self._context? is this when assigning _handler = _context = None

lol I’ve deleted this explanation thinking it was confusing.
the del() fonction doesn’t accept a context parameter. so he (kaio) creates one when instancing in the invoke still with a class variable. from this script https://github.com/K-410/blender-scripts/blob/master/2.8/object_move_origin.py

but I found an issue using __del __(). you can’t have a bl_options = {‘UNDO’}. so I did a normal function delete(self,context) (not del(), reserved word ^^)