I'm drawing some lines using a 2D_UNIFORM_COLOR shader based on object location. When I rotate my view, the lines update only when the rotation is done: I thought that I can just put a function that updates my coordinates in modal() but this didn't work. So I wonder if there's a way to update the regions while the view is rotated?

enter image description here

P.S. I realise that a 3d shader would work better for drawing axes, but axes here is just an example

My code:

import bpy
from mathutils import Vector
import bgl
import gpu
from gpu_extras.batch import batch_for_shader
from bpy_extras.view3d_utils import location_3d_to_region_2d

def draw_callback_px(self, context):

    # do I really have to create a separate shader for each line? Ugh

    shader_X = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
    shader_Y = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
    shader_Z = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
    v_x = (self.axes[0] - self.ob_location).normalized()
    v_y = (self.axes[1] - self.ob_location).normalized()
    v_z = (self.axes[2] - self.ob_location).normalized()
    batch_X = batch_for_shader(shader_X, 'LINE_STRIP', {"pos": [self.ob_location, self.ob_location + v_x*100]})
    batch_Y = batch_for_shader(shader_Y, 'LINE_STRIP', {"pos": [self.ob_location, self.ob_location + v_y*100]})
    batch_Z = batch_for_shader(shader_Z, 'LINE_STRIP', {"pos": [self.ob_location, self.ob_location + v_z*100]})

    shader_X.uniform_float("color", (1, 0, 0, 0.2))

    shader_Y.uniform_float("color", (0, 1, 0, 0.2))

    shader_Z.uniform_float("color", (0, 0, 1, 0.2))

    # restore opengl defaults

class ModalOperator(bpy.types.Operator):
    """Move an object with the mouse, example"""
    bl_idname = "object.modal_operator_test"
    bl_label = "Simple Modal Operator Test"

    def modal(self, context, event):
        self.update_region(context, event)
        # Navigation
        if event.type in {'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE'} or event.alt:        
            return {'PASS_THROUGH'}

        elif event.type in {'RIGHTMOUSE', 'ESC'}:
            bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
            return {'CANCELLED'}

        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        self.ob = bpy.context.view_layer.objects.active
        if not self.ob:
            return {'CANCELLED'}
        self.update_region(context, event)

        args = (self, context)
        self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_PIXEL')

        return {'RUNNING_MODAL'}
    def update_region(self, context, event):
        self.region = context.region
        self.rv3d = context.space_data.region_3d
        self.ob_location = location_3d_to_region_2d(self.region, self.rv3d, self.ob.location, default=None)
        self.axes = [location_3d_to_region_2d(self.region, self.rv3d, self.ob.location + Vector((1,0,0)), default=None),
                     location_3d_to_region_2d(self.region, self.rv3d, self.ob.location + Vector((0,1,0)), default=None),
                     location_3d_to_region_2d(self.region, self.rv3d, self.ob.location + Vector((0,0,1)), default=None)]
def register():

def unregister():

if __name__ == "__main__":

1 Answer 1


Edit: as I wrote all that is below, I just realized the question was explicitly about 2D shader...

You just missed to call

self.update_region(context, None)

from inside the callback, which is itself called at each drawing.

Though, as it is the calculation is not correct. You should use object world matrix (see the previous answer).

Previous answer (I don't delete it as that could be useful):

As you want to move during the operator, you are in 3d, use:


instead of:


Also, in this case, use POST_VIEW, as indicated in the documentation.

draw_type (str) – Usually POST_PIXEL for 2D drawing and POST_VIEW for 3D drawing. In some cases PRE_VIEW can be used. BACKDROP can be used for backdrops in the node editor.

self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_VIEW')

One shader is needed.

You can start from object axis and convert them in world space:

def draw_callback_px(self, context):

    obj = context.active_object
    mat = obj.matrix_world

    shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')


    origin = mat @ Vector((0, 0, 0)) #Convert coordinates in world space
    x = mat @ Vector((2,0,0))
    y = mat @ Vector((0,2,0))
    z = mat @ Vector((0,0,2))
    batch_X = batch_for_shader(shader, 'LINES', {"pos": [origin, x]}, indices = ((0, 1),) )
    batch_Y = batch_for_shader(shader, 'LINES', {"pos": [origin, y]}, indices = ((0, 1),) )
    batch_Z = batch_for_shader(shader, 'LINES', {"pos": [origin, z]}, indices = ((0, 1),) )

    shader.uniform_float("color", (1, 0, 0, 0.2))

    shader.uniform_float("color", (0, 1, 0, 0.2))

    shader.uniform_float("color", (0, 0, 1, 0.2))

    # restore opengl defaults

enter image description here

  • $\begingroup$ Why POST_VIEW? $\endgroup$
    – brockmann
    Dec 7, 2020 at 13:07
  • $\begingroup$ @brockmann, just edited about that. I don't know why, but it is in the docs... $\endgroup$
    – lemon
    Dec 7, 2020 at 13:11
  • $\begingroup$ Cool! Thanks @lemon. $\endgroup$
    – brockmann
    Dec 7, 2020 at 13:11
  • $\begingroup$ @brockmann... fyi (if needed), the issue was simply... see edited answer... : )) $\endgroup$
    – lemon
    Dec 7, 2020 at 13:37
  • 1
    $\begingroup$ Thank you for the answer! Yes, I knew how to make it work for 3d shader so I was looking for a solution for 2d shader. Putting my region_update to draw callback fixed the issue I had, thank you! $\endgroup$ Dec 7, 2020 at 18:51

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .