How to invoke draw_handler_add while running script?

I have a draw_handler_add like this:

import bpy,gpu

def draw():
    global to_draw, begin
    if to_draw:
        
        print("drawing")
        framebuffer = gpu.state.active_framebuffer_get()

        viewport_info = gpu.state.viewport_get()
        width = viewport_info[2]
        height = viewport_info[3]

        framebuffer_image.scale(width, height)

        pixelBuffer = framebuffer.read_color(0, 0, width, height, 4, 0, 'FLOAT')
        
        pixelBuffer.dimensions = width * height * 4
        framebuffer_image.pixels.foreach_set(pixelBuffer)
        framebuffer_image.filepath_raw = "/.../MyImage.png"
        framebuffer_image.save()
        to_draw=False

if __name__ == "__main__":

    global to_draw,b
    to_draw=False
    
    if "color_buffer_copy" in bpy.data.images:
        framebuffer_image = bpy.data.images["color_buffer_copy"]
    else:
        framebuffer_image = bpy.data.images.new("color_buffer_copy" , 32, 32, float_buffer=True,alpha=True)

    bpy.types.SpaceView3D.draw_handler_add(draw, (), 'WINDOW', 'PRE_VIEW')
        
    bpy.data.objects['Cube'].rotation_euler[0] = 1.5
    
    bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
        
    to_draw=True

The problem is that the draw() is being invoked after the script it completed
but I want it to be invoked while the script itself is running, so how do I achieve this?

(just for context my ultimate aim is to setup a server)

Officially you can’t. Draw handlers are called during the rendering pass, which is two stages after python has finished running scripts.

You mention server, so I presume you want blocking semantics, so unofficially you could try this:

  1. Register your draw handler
  2. call the viewport’s tag_redraw method
  3. Follow up with a call to bpy.ops.wm.redraw_timer(type='DRAW_SWAP', iterations=1).

The draw handler should run before the operator returns.

For personal scripts this is fine, but the more sane solution is to have your server block in a separate thread.

2 Likes

I made a smaller example, is this what you meant? because it seems to be working
but we have to update the screen twice, one before invoking draw and after invoking draw

am I doing it right or is there a more efficient method to do this?

import bpy,gpu,time

def draw():
    global to_draw
    if to_draw:
        print("drawing...")
        framebuffer = gpu.state.active_framebuffer_get()

        viewport_info = gpu.state.viewport_get()
        width = viewport_info[2]
        height = viewport_info[3]

        framebuffer_image.scale(width, height)

        pixelBuffer = framebuffer.read_color(0, 0, width, height, 4, 0, 'FLOAT')
        
        pixelBuffer.dimensions = width * height * 4
        framebuffer_image.pixels.foreach_set(pixelBuffer)
        framebuffer_image.filepath_raw = "/.../MyImage.png"
        framebuffer_image.save()
        to_draw=False

def update_draw():
    global to_draw
    for area in bpy.context.screen.areas: 
        if area.type == 'VIEW_3D':
            area.tag_redraw()
    bpy.ops.wm.redraw_timer(type='DRAW_SWAP', iterations=1)
    
    to_draw=True
    
    bpy.ops.wm.redraw_timer(type='DRAW_SWAP', iterations=1)

if __name__ == "__main__":
    if "color_buffer_copy" in bpy.data.images:
        framebuffer_image = bpy.data.images["color_buffer_copy"]
    else:
        framebuffer_image = bpy.data.images.new("color_buffer_copy" , 32, 32, float_buffer=True,alpha=True)
    draw_handler=bpy.types.SpaceView3D.draw_handler_add(draw, (), 'WINDOW', 'PRE_VIEW')
    
    
    bpy.data.objects['Cube'].rotation_euler[0] = 1.5
    update_draw()
    
    time.sleep(10)
    
    bpy.data.objects['Cube'].rotation_euler[1] = 3
    update_draw()
    
    bpy.types.SpaceView3D.draw_handler_remove(draw_handler, 'WINDOW')
    print("we're done")

May I ask why do you get the image in this convoluted way? Won’t bpy.ops.render.opengl() suffice? Or are there problems overriding context for this operator?

Maybe, starting server in another thread and pushing rendering tasks to a synchronized queue like in this example would work:
https://docs.blender.org/api/current/bpy.app.timers.html

1 Like

simple. because it’s faster

for example: rendering takes 1.1 seconds, viewport rendering (bpy.ops.render.opengl) takes 0.7 seconds,
directly getting the image from the buffer takes 0.07 seconds

Edit: Also as far as I’ve seen, starting another thread in blender is a bad idea as it was not designed to handle multiple threads, but I’m open to change if you can show me an example where I can safely execute it

Calling it once should be enough, but if you plan on doing multiple draw/render actions in sequence, you may need to use 'DRAW_WIN_SWAP'. The operator (api page) supports other types of enumerations, too.

In your case it would be:

def update_draw():
    global to_draw
    for area in bpy.context.screen.areas: 
        if area.type == 'VIEW_3D':
            area.tag_redraw()
    
    to_draw = True
    bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)

Edit: I realized you probably are running the script from the text editor and not from the 3d viewport, so 'DRAW_SWAP' might not have an effect since it redraws contextual region. My bad :sweat_smile:

that doesn’t seem to update properly

I need bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
both before and after to_draw = True

def update_draw():
    global to_draw
    bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
    to_draw = True
    bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)

complete code

Also, Is there a way I could run this in background from the command line?

cause now it gives an error:

RuntimeError: Operator bpy.ops.wm.redraw_timer.poll() failed, context is incorrect

but maybe not, because there is no GUI the buffer won’t be generated and hence we have nothing to save to our file? (atleast that’s my guess)

I’m testing your script on Blender 3.2.1.

I assume you intend to write two images with the cube slightly rotated each time?
If that’s the case you need to call

framebuffer_image.update()

after setting the pixels. Otherwise you’ll write the same set of pixels twice to disk.

In background mode Blender doesn’t have an opengl context, so obviously there’s no framebuffers to pull pixels from. Your only option here is to use the render engine, i.e command line rendering.