3
$\begingroup$

I want to start a render from a callback, but I also need the render preview to update while it renders.

If I call bpy.ops.render.render() then there is no preview.

If I call bpy.ops.render.render("INVOKE_DEFAULT") then the preview gets updated normally.

However when I call bpy.ops.render.render("INVOKE_DEFAULT") from a callback, then I get the error "RuntimeError: Operator bpy.ops.render.render.poll() Missing 'window' in context".

Here's an example script that will generate the error:

import bpy

def in_1_seconds():
    bpy.ops.render.render("INVOKE_DEFAULT") #RuntimeError: Operator bpy.ops.render.render.poll() Missing 'window' in context

bpy.app.timers.register(in_1_seconds, first_interval=1)

I can not find a way around, I have tried all:

(‘INVOKE_DEFAULT’, ‘INVOKE_REGION_WIN’, ‘INVOKE_REGION_CHANNELS’, ‘INVOKE_REGION_PREVIEW’, ‘INVOKE_AREA’, ‘INVOKE_SCREEN’, ‘EXEC_DEFAULT’, ‘EXEC_REGION_WIN’, ‘EXEC_REGION_CHANNELS’, ‘EXEC_REGION_PREVIEW’, ‘EXEC_AREA’, ‘EXEC_SCREEN’)

The "INVOKE" types cause the error to be displayed while the "EXEC" types cause Blender to completely freeze until rendering finishes.

I need a way to start a render from Python exactly like pressing "F12" with live preview. Any ideas how to do achieve this from callbacks?

[EDIT]

This is not a duplicate of "Context is incorrect when calling from a timer" Because after trying to impliment the answer from that post I get the exact same error and/or no render preview.

For example this does not work:

import time, bpy, functools
from bpy import context

def render(od):
    timestr = time.strftime("%Y%m%d-%H%M%S")
    print(timestr)
    # uncomment below line for error...
    bpy.ops.render.render("INVOKE_DEFAULT")
    return 5.0

def loop(idx):
    print("LOOP")
    window = context.window_manager.windows[idx]
    screen = window.screen
    views_3d = sorted(
            [a for a in screen.areas if a.type == 'VIEW_3D'],
            key=lambda a: (a.width * a.height))
    if views_3d:
        a = views_3d[0]
        # override
        o = {"window" : window,
             "screen" : screen,
             "area" : a,
             "space_data": a.spaces.active,
             "region" : a.regions[-1]
         }
        return render(o)
    return 2.0

#f = functools.partial(loop, bpy.context.screen.name)

if __name__ == '__main__':
    idx = bpy.context.window_manager.windows[:].index(bpy.context.window)
    bpy.app.timers.register(functools.partial(loop, idx), first_interval=5.0)

[EDIT]

More specifically I would like to start a render after the previous render completes:

The following script simply starts rendering, and when the render_complete callback is fired it increments the current frame then renders the next image (cycle repeats until endframe reached):

import bpy

endframe = 10 #bpy.context.scene.frame_end

def complete(*args, **kwargs):
    if bpy.context.scene.frame_current <= endframe:
        bpy.context.scene.frame_current += 1
        #bpy.ops.render.render("INVOKE_DEFAULT") #I cause errors!!!   
        bpy.ops.render.render()  

bpy.app.handlers.render_complete.append(complete)

bpy.ops.render.render("INVOKE_DEFAULT")

In the above script only the very first render shows the preview updating during the render, while subsequent renders only show the preview update on completion of that frame. I would like all frames to show a updating preview as the frame is being rendered but I am completely stuck and don't know how to override context so I can use "INVOKE_DEFAULT"??

$\endgroup$

2 Answers 2

2
$\begingroup$

I noticed that when zooming the Image Editor during rendering the preview was being updated.

So I figure that a timer could do that for me:

import bpy

def update_image_editor_preview():
    print("update_image_editor_preview")
    #idx = bpy.context.window_manager.windows[:].index(bpy.context.window)

    window = bpy.context.window_manager.windows[0]
    screen = window.screen
    views_3d = [a for a in screen.areas if a.type == 'IMAGE_EDITOR']# sorted(

    if views_3d:        
        views_3d[0].tag_redraw()

    return 0.5

def register():    
    bpy.app.timers.register(update_image_editor_preview)    

def unregister():
    bpy.app.timers.unregister(update_image_editor_preview)   


if __name__ == '__main__':
    register()

This probably is not be the most elegant solution, however it does update the render preview during rendering, which is all I really needed.

[EDIT]

I would like to clarify a little about what I am trying to accomplish. The script that I am trying to create is a super simple shared-folder render farm. Each PC in the farm must be running the same .blend file, and each must point the output renders to a shared network folder. Then this script selects which frame to render next depending on which frames have already been consumed by other nodes on in the farm.

This is a variant of my script, it shows my best attempt to adopt BatFinger's recomendations. When I run it though, the first image processes, but then nothing else happens, as if bpy.ops.render.render(od, "INVOKE_DEFAULT") just fails silently:

#Shared Rendering - load the same .blend on all PC's in the farm, then select the destination
#directory for output images to a shared network folder. This script will pick and render frames 
#that have not already been consumed by other nodes in the farm.
import bpy
from os import listdir, remove
from os.path import isfile, join

global cur_temp_file
cur_temp_file = None

startframe = bpy.context.scene.frame_start
endframe = bpy.context.scene.frame_end

def update_image_editor_preview(): #NEEDED FOR TIMED PREVIEW
    #idx = bpy.context.window_manager.windows[:].index(bpy.context.window)

    window = bpy.context.window_manager.windows[0]
    screen = window.screen
    views_3d = [a for a in screen.areas if a.type == 'IMAGE_EDITOR']# sorted(

    if views_3d:        
        views_3d[0].tag_redraw()

    return 0.5


def register():    
    #idx = bpy.context.window_manager.windows[:].index(bpy.context.window)
    #bpy.app.timers.register(functools.partial(loop, idx), first_interval=1.0)
    bpy.app.timers.register(update_image_editor_preview)    

def unregister():
    if bpy.app.timers.is_registered(update_image_editor_preview):
        bpy.app.timers.unregister(update_image_editor_preview)   

def clear_callbacks():
    while len(bpy.app.handlers.render_cancel) > 0:
        bpy.app.handlers.render_cancel.pop(0)
    while len(bpy.app.handlers.render_pre) > 0:
        bpy.app.handlers.render_pre.pop(0)
    while len(bpy.app.handlers.render_complete) > 0:
        bpy.app.handlers.render_complete.pop(0)
    unregister()

def check_file_exists():
    mypath = bpy.context.scene.render.filepath
    onlyfiles = [f for f in listdir(mypath) if isfile(join(mypath, f))]
    while True:
        bpy.context.scene.frame_current += 1
        if bpy.context.scene.frame_current > endframe:
            return False

        tempnext = "frame%s.tmp" % bpy.context.scene.frame_current

        if (tempnext in onlyfiles):
            continue
        imagenext = "frame%s.png" % bpy.context.scene.frame_current
        if (imagenext in onlyfiles):
            continue

        break
    return True


def render(od):
    bpy.ops.render.render(od, "INVOKE_DEFAULT")


def cancel(*args, **kwargs):
    global cur_temp_file
    if cur_temp_file is not None: #clean up
        mypath = bpy.context.scene.render.filepath
        remove(join(mypath,cur_temp_file))
        cur_temp_file = None
    clear_callbacks()

def pre(*args, **kwargs):
    mypath = bpy.context.scene.render.filepath
    tmpname = "frame%s.tmp" % bpy.context.scene.frame_current
    temppath = join(mypath, tmpname)

    open(temppath, 'a').close()

    global cur_temp_file
    cur_temp_file = tmpname

def complete(*args, **kwargs):
    mypath = bpy.context.scene.render.filepath
    imagepath = join(mypath, "frame%s.png" % bpy.context.scene.frame_current)
    tmpname = "frame%s.tmp" % bpy.context.scene.frame_current
    temppath = join(mypath, tmpname)

    img = bpy.data.images['Render Result']
    img.save_render(imagepath)

    onlyfiles = [f for f in listdir(mypath) if isfile(join(mypath, f))]
    if (tmpname in onlyfiles):
        remove(temppath)
        global cur_temp_file
        cur_temp_file = None

    if check_file_exists():

        window = bpy.context.window_manager.windows[0]
        screen = window.screen        

        o = {"window" : window}      
        render(o) #render with context override - rendering does not begin

        #bpy.ops.render.render() #UNCOMMENT ME FOR TIMED PREVIEW
    else:
        clear_callbacks()


if __name__ == '__main__':
    clear_callbacks()

    bpy.app.handlers.render_cancel.append(cancel)
    bpy.app.handlers.render_pre.append(pre)
    bpy.app.handlers.render_complete.append(complete)

    bpy.context.scene.frame_current = startframe

    #register() #UNCOMMENT ME FOR TIMED PREVIEW

    if check_file_exists():  
        bpy.ops.render.render("INVOKE_DEFAULT")

If you uncommnet the two lines with #UNCOMMENT ME FOR TIMED PREVIEW and remove render() then the timer is used again (eg. shown in the above code) which works..

I sure hope these scripts are handy to someone.

$\endgroup$
2
$\begingroup$

Create an override dictionary.

  • Pass the context dictionary

    bpy.ops.render.render(od, "INVOKE_DEFAULT")

  • Made a complete copy of context. This is somewhat dangerous since an object reference to a removed object may be accessed which will core dump blender. I have a feeling the context members required to override hinges on the render display mode selected. with new window scene.render.display_mode = 'WINDOW' mode, it appears only the context.window is needed.

Have put in a count to stop after 5 renders to test.

enter image description here Test run, code slightly altered (highlighted) to render frames 50, 40, 30, 20 and 10. In image editor display mode, renders to the image editor (if no open images) or opens one in the largest area

import time, bpy, functools

count = 5
def render(od):
    global count
    count -= 1
    timestr = time.strftime("%Y%m%d-%H%M%S")
    print(timestr)
    # I get no error in the line below
    bpy.ops.render.render(od, "INVOKE_DEFAULT")

    return 2 if count else None

def loop(c):
    print("LOOP", count)

    # override
    c["scene"].frame_set(10 * count)
    return render(c)


if __name__ == '__main__':
    f = functools.partial(loop, bpy.context.copy())

    bpy.app.timers.register(f, first_interval=5.0)
$\endgroup$
2
  • $\begingroup$ Thank you batFINGER, I truly do appreciate your assistance. But I'm afraid it still does not work. I don't get an error this time, but now nothing happens and no new renders begin.. I have added an edit to my last post with a full example. And I am using Blender 2.8 if that gives any clues.. Sorry about the re-post. $\endgroup$
    – Logic1
    Commented Jul 5, 2019 at 12:26
  • $\begingroup$ Was off AFK and it dawned on me there that I am using new window as a default render display mode. Have had a play and it appears that more context members are required depending on the display mode, _can get away with pretty much only window in window display mode. Apologies also for being sarcy in original answer. Pretty much only scripting in freshly built 2.8 too. $\endgroup$
    – batFINGER
    Commented Jul 5, 2019 at 14:42

You must log in to answer this question.

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