RuntimeError: Operator bpy.ops.render.render.poll() Missing 'window' in context

Hello!
Trying to make rendering automatization script and stuck with the error that I can’t beat and spend a lot of time on it, found also many similar questions but anyway can’t get it worked so I decided to ask for your help.
I prepared a much-simplified example of code that gives the same error because the original script is big, messy, and isn’t easy to read… Currently, I’m using its version without rendering preview window and it’s work fine, but it’s will be great to have a preview window of rendering progress that’s called with INVOKE_DEFAULT option: bpy.ops.render.render(‘INVOKE_DEFAULT’, animation=True, write_still=True, use_viewport=False, layer=’’, scene=’’)
So, here is example that reproduce my error - RuntimeError: Operator bpy.ops.render.render.poll() Missing ‘window’ in context
first time it renders Ok, but second - fails

import bpy

rendercounter = 0

def r():
#    alot of stuff here...
   global rendercounter
   rendercounter += 1
   if rendercounter < 3:
       print("starting render")
       bpy.ops.render.render('INVOKE_DEFAULT', animation=True, write_still=True, use_viewport=False, layer='', scene='')

def rc(a,b):
   print("render complete")
#    alot of stuff here...
   r()

bpy.app.handlers.render_complete.clear()
bpy.app.handlers.render_complete.append(rc)

r()

Thank you for your attention!

1 Like

Use a render macro.

Callbacks are usually not bound to a context and depending on the complexity of the operator, eg. what data and context it needs in order to execute, it might not work. The render and bake operators have a lot of moving parts.

While macros generally increase the boilerplate, they ensure the calls to each operator happens with a proper context. The api for macros is sort of hidden, but you can find some info around the net. Bake/rendering macros require a modal operator to work unattended.

The steps are roughly:

  1. Create a macro (must be re-registered when defining a queue)
  2. Create a modal that ensures the queue runs unattended.
  3. Define the render queue using macro.define("WM_OT_operator", kwargs=...).
  4. Define a finishing operator (can be same operator as modal, but with args), so the modal knows when to end.

Then start the thing using the modal.

Example which does 5 successive renders:

import bpy

def get_macro():
    class RENDER_OT_render_macro(bpy.types.Macro):
        bl_idname = "render.render_macro"
        bl_label = "Render Macro"

        @classmethod
        def define(cls, idname, **kwargs):
            op = super().define(idname).properties
            for key, val in kwargs.items():
                setattr(op, key, val)

    old = getattr(bpy.types, "RENDER_OT_render_macro", False)
    if old:
        bpy.utils.unregister_class(old)
    bpy.utils.register_class(RENDER_OT_render_macro)
    return RENDER_OT_render_macro


class WM_OT_refresh(bpy.types.Operator):
    bl_idname = "wm.refresh"
    bl_label = "Refresh"
    tag_finish: bpy.props.BoolProperty()

    def modal(self, context, event):
        if getattr(type(self), "finished", False):
            self.report({'INFO'}, "Done")
            type(self).finished = False
            context.window_manager.event_timer_remove(self.t)
            return {'FINISHED'}
        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        if self.tag_finish:
            type(self).finished = True
            return {'FINISHED'}
        bpy.ops.render.render_macro('INVOKE_DEFAULT')
        wm = context.window_manager
        self.t = wm.event_timer_add(0.1, window=context.window)
        wm.modal_handler_add(self)
        return {'RUNNING_MODAL'}

if __name__ == "__main__":
    bpy.utils.register_class(WM_OT_refresh)
    macro = get_macro()

    for i in range(5):
        macro.define("RENDER_OT_render",
                     animation=True,
                     write_still=True,
                     use_viewport=False,
                     layer="",
                     scene="")
    macro.define("WM_OT_refresh", tag_finish=True)

    bpy.ops.wm.refresh('INVOKE_DEFAULT')
1 Like

Thank you very much!!! I will try it when I get back to home tomorrow! Looks so big bunch of code for this)) but hope it will work! :slight_smile: didn’t tried macros yet.

Hello again @iceythe !) Tried to implement this method to my script and no success yet, can’t understand where to put code that changes a lot of parameters (like changing animation, render path, changing material etc…) :frowning:
I can attach my script to better understand the “context”… If you have some time for it of course!
I will continue to try, but not sure that will succeed… :flushed:
https://drive.google.com/file/d/1xJxGtGFyLd_chtF1ME7y7ZxTJ-mDYFNl/view?usp=sharing

I’ve made some changes to the script, but I don’t have the means to test it myself. Essentially moved the pre-render visibility changes into own macro defines using the same modal operator (but called with different arguments), which should execute before each render.

Do report back if there’s any issues.

bl_info = {
    "name": "EXOLIFE Character Renderer",
    "author": "MikeMS",
    "version": (0,9),
    "blender": (2,90,0),
    "category": "Render",
    "location": "View3D > EXOLIFE Panel",
    "description": "Automation render of character parts",
    "warning": "",
    "doc_url": "",
    "tracker_url": "",
}


import bpy


def get_macro():
    class RENDER_OT_render_macro(bpy.types.Macro):
        bl_idname = "render.render_macro"
        bl_label = "Render Macro"

        @classmethod
        def define(cls, idname, **kwargs):
            cls._is_set = True
            op = super().define(idname).properties
            for key, val in kwargs.items():
                setattr(op, key, val)

        @classmethod
        def is_set(cls):
            return getattr(cls, "_is_set", False)

    old = getattr(bpy.types, "RENDER_OT_render_macro", False)
    if old:
        bpy.utils.unregister_class(old)
    bpy.utils.register_class(RENDER_OT_render_macro)
    return RENDER_OT_render_macro


class WM_OT_refresh(bpy.types.Operator):
    bl_idname = "wm.refresh"
    bl_label = "Refresh"
    tag_finish: bpy.props.BoolProperty(options={'SKIP_SAVE'})
    bp_settings_set: bpy.props.StringProperty(options={'SKIP_SAVE'})
    use_blur: bpy.props.IntProperty(default=-1, options={'SKIP_SAVE'})

    def modal(self, context, event):
        if getattr(type(self), "finished", False):
            self.report({'INFO'}, "Done")
            type(self).finished = False
            context.window_manager.event_timer_remove(self.t)
            return {'FINISHED'}
        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        if self.tag_finish:
            type(self).finished = True
            return {'FINISHED'}
        elif self.bp_settings_set:
            return self.execute(context)
        elif self.use_blur != -1:
            context.scene.render.use_motion_blur = bool(self.use_blur)
            return {'FINISHED'}
        bpy.ops.render.render_macro('INVOKE_DEFAULT')
        wm = context.window_manager
        self.t = wm.event_timer_add(0.1, window=context.window)
        wm.modal_handler_add(self)
        return {'RUNNING_MODAL'}

    def execute(self, context):
        exolife_render_set_only(self.bp_settings_set)
        return {'FINISHED'}


# Define a macro chain for a specific body part:
# Sets body part visibility and settings.
# Sets a render job.
def macro_define_by_bp(bp, **kwargs):
    render_settings = {"animation":True, "write_still":True,
                       "use_viewport":False, "layer":'', "scene":''}
    if kwargs:
        render_settings.update(kwargs)
    macro.define("RENDER_OT_ExolifeCharVisibility").bp_to_show = bp
    macro.define("WM_OT_refresh").bp_settings_set = bp
    render = macro.define("RENDER_OT_render")
    for key, val in render_settings.items():
        setattr(render, key, val)


#import EXOLIFE_CharRenderer


class RENDER_OT_ExolifeCharRenderer_reInit(bpy.types.Operator):
    bl_idname = "render.exolifecharrenderer_reinit"
    bl_label = "EXOLIFE ExolifeCharRenderer - reInit"
    bl_description = "Create 'exolife_properties' Empy Object to store setting and make it all works!"
    
    def execute(self, context):
        
        try:
            bpy.data.objects['exolife_properties']
        except:
            bpy.ops.object.empty_add(type='SPHERE', align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
            bpy.context.active_object.name = 'exolife_properties'
            
#            bpy.ops.script.python_file_run(filepath=EXOLIFE_CharRenderer.__file__)
        
        return {'FINISHED'}


#def exolife_prop_object():
#    try:
#        bpy.data.objects['exolife_properties']
#    except:
#        bpy.ops.object.empty_add(type='SPHERE', align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
#        bpy.context.active_object.name = 'exolife_properties'
#        
#    return {'FINISHED'}
    
    
class RENDER_OT_ExolifeCharVisibility(bpy.types.Operator):
    """Let's set character visibility!"""
    bl_idname = "render.exolifecharvisibility"
    bl_label = "EXOLIFE CharVisibility"
    bl_options = {'REGISTER','UNDO'}
    
    bp_to_show: bpy.props.StringProperty()


    def execute(self, context):
        
        bpy.data.node_groups["V-Selection"].nodes["Vertex Color"].layer_name = self.bp_to_show
        if self.bp_to_show == "" or self.bp_to_show == "Head": bpy.data.node_groups["V-Selection_HeadHairs"].nodes["Vertex Color"].layer_name = "Head"
        else: bpy.data.node_groups["V-Selection_HeadHairs"].nodes["Vertex Color"].layer_name = ""
        bpy.data.objects['exolife_properties'].Exolife_Settings.bp_active = self.bp_to_show
        
        return {'FINISHED'}



class RENDER_OT_ExolifeCharRenderer(bpy.types.Operator):
    """Let's render character!"""
    bl_idname = "render.exolifecharrenderer"
    bl_label = "EXOLIFE CharRenderer"
    bl_options = {'REGISTER','UNDO'}

    # Replaced eval statements with getattr.
    def execute(data, context):
        macro = get_macro()
        settings = bpy.data.objects['exolife_properties'].Exolife_Settings

        for i in range(settings.anims_count):
            i += 1
            if getattr(settings, f"anim{i}", False):
                print(f"\n!!!!!### RenderProcess - ANIMATION: {getattr(settings, f'anim{i}_name', '')}\n")
                
                macro.define("RENDER_OT_ExolifeAnimSet").anim = i
                use_blur = int(getattr(settings, f"anim{i}_mb"))
                macro.define("WM_OT_refresh").set_render_motion_blur = use_blur

                if settings.bp_hand_l:
                    macro_define_by_bp("HandL")
                if settings.bp_legs:
                    macro_define_by_bp("Legs")
                if settings.bp_torso:
                    macro_define_by_bp("Torso")
                if settings.bp_head:
                    macro_define_by_bp("Head")
                if settings.bp_hand_r:
                    macro_define_by_bp("HandR")

        # Macro has definitions. Start rendering.
        if macro.is_set():
            macro.define("WM_OT_refresh", tag_finish=True)
            bpy.ops.wm.refresh('INVOKE_DEFAULT')
        return {'FINISHED'}
    
def exolife_render(bp):
    print("\n!!!!! RenderProcess - BodyPart: "+bp+"\n")
    bpy.context.scene.render.filepath = bpy.data.objects['exolife_properties'].Exolife_Settings.file_path + "\\" + bpy.data.objects['exolife_properties'].Exolife_Settings.bp_active + bpy.data.objects['exolife_properties'].Exolife_Settings.bp_part_suffix + "\\" + bpy.context.object.animation_data.action.name + "\\###"
    bpy.ops.render.render(animation=True, write_still=True, use_viewport=False, layer='', scene='')
#    bpy.ops.render.render('INVOKE_DEFAULT', animation=True, write_still=True, use_viewport=False, layer='', scene='')


def exolife_render_set_only(bp):
    print("\n!!!!! RenderProcess - BodyPart: "+bp+"\n")
    bpy.context.scene.render.filepath = bpy.data.objects['exolife_properties'].Exolife_Settings.file_path + "\\" + bpy.data.objects['exolife_properties'].Exolife_Settings.bp_active + bpy.data.objects['exolife_properties'].Exolife_Settings.bp_part_suffix + "\\" + bpy.context.object.animation_data.action.name + "\\###"
    

    
    
class VIEW3D_PT_ExolifeCharVisibility(bpy.types.Panel):
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "EXOLIFE"
    bl_label = "Set BodyPart Visible:"
    
    def draw(self, context):
        
        bp_to_show = ""
        props = self.layout.operator('render.exolifecharvisibility', text = "All Visible")
        props.bp_to_show = bp_to_show
        
        bp_to_show = "HandL"
        props = self.layout.operator('render.exolifecharvisibility', text = bp_to_show)
        props.bp_to_show = bp_to_show
        
        bp_to_show = "Legs"
        props = self.layout.operator('render.exolifecharvisibility', text = bp_to_show)
        props.bp_to_show = bp_to_show
        
        bp_to_show = "Torso"
        props = self.layout.operator('render.exolifecharvisibility', text = bp_to_show)
        props.bp_to_show = bp_to_show
        
        bp_to_show = "Head"
        props = self.layout.operator('render.exolifecharvisibility', text = bp_to_show)
        props.bp_to_show = bp_to_show
        
        bp_to_show = "HandR"
        props = self.layout.operator('render.exolifecharvisibility', text = bp_to_show)
        props.bp_to_show = bp_to_show
        


class RENDER_OT_ExolifeAnimSet(bpy.types.Operator):
    """Set this Animation (Action) and it's frame ranges"""
    bl_idname = "render.exolifecharanimset"
    bl_label = "EXOLIFE AnimSet"
    bl_options = {'REGISTER','UNDO'}
    
    anim: bpy.props.IntProperty()
    
    def execute(scene, context):
        
        bpy.ops.object.select_all(action='DESELECT')
        bpy.data.objects[bpy.data.objects['exolife_properties'].Exolife_Settings.rig_name].select_set(True)
        bpy.context.view_layer.objects.active = bpy.context.scene.objects[bpy.data.objects['exolife_properties'].Exolife_Settings.rig_name]
        
        bpy.context.object.animation_data.action = bpy.data.actions[eval("bpy.data.objects['exolife_properties'].Exolife_Settings.anim"+str(scene.anim)+"_name")]
        
        bpy.context.scene.frame_start = eval("bpy.data.objects['exolife_properties'].Exolife_Settings."+"anim"+str(scene.anim)+"_start")
        bpy.context.scene.frame_end = eval("bpy.data.objects['exolife_properties'].Exolife_Settings."+"anim"+str(scene.anim)+"_end")

        return {'FINISHED'}


class ExolifeSettings(bpy.types.PropertyGroup):

    
    file_path: bpy.props.StringProperty(name="", description="Render to \ bodypart folder \ animation folder", default="", maxlen=1024, subtype="DIR_PATH")
    
    rig_name: bpy.props.StringProperty(name="Rig name (necessarily)", default="", description="for example: JordanRig")

    bp_active: bpy.props.StringProperty()
    
    anims_max = 100
    anims_count: bpy.props.IntProperty(name="",default = 1, min=1, max = anims_max, description="Amount of animations to work with")
    try:
        bpy.data.objects['exolife_properties'].Exolife_Settings.anims_count
        anims_count = bpy.data.objects['exolife_properties'].Exolife_Settings.anims_count
    except: anims_count = 1


    for i in range(anims_max):
        i = str(i+1)
        exec('anim'+i+': bpy.props.BoolProperty(name="", default=True, description="Render - On/Off")')
        exec('anim'+i+'_name: bpy.props.StringProperty(name="", default="", description="Animation name (Action name)")')
        exec('anim'+i+'_start: bpy.props.IntProperty(name="",default = 1, description="Start frame")')
        exec('anim'+i+'_end: bpy.props.IntProperty(name="",default = 10, description="End frame")')
        exec('anim'+i+'_mb: bpy.props.BoolProperty(name="MB", default=True, description="Motion Blur - On/Off")')
    

    
    bp_hand_l: bpy.props.BoolProperty(name="HandL", default=True, description="Enable/Disable rendering of this BodyPart")
    bp_legs: bpy.props.BoolProperty(name="Legs", default=True, description="Enable/Disable rendering of this BodyPart")
    bp_torso: bpy.props.BoolProperty(name="Torso", default=True, description="Enable/Disable rendering of this BodyPart")
    bp_head: bpy.props.BoolProperty(name="Head", default=True, description="Enable/Disable rendering of this BodyPart")
    bp_hand_r: bpy.props.BoolProperty(name="HandR", default=True, description="Enable/Disable rendering of this BodyPart")
    
    
    bp_part_suffix: bpy.props.StringProperty(name="BodyPart folder suffix", default="_1")


class VIEW3D_PT_ExolifeCharRenderer(bpy.types.Panel):
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "EXOLIFE"
    bl_label = "Set Render Settings:"
    
    
    def draw(self, context):
        
        try:
            bpy.data.objects['exolife_properties']
        except:
            self.layout.operator('render.exolifecharrenderer_reinit', text = "INITIALIZE!")
            
        Exolife_Settings = bpy.data.objects['exolife_properties'].Exolife_Settings
        
        
        self.layout.label(text="Render Path:")
        row = self.layout.row()
        row.prop(Exolife_Settings, "file_path")
        col = self.layout.column()
        col.alignment = 'RIGHT'
        col.prop(Exolife_Settings, "rig_name")
        row = self.layout.row()
        row.label(text="Animations to render:")
        row.alignment = 'RIGHT'
        row.prop(Exolife_Settings, "anims_count")

#        row.operator('render.exolifecharrenderer_reinit', text = "ReInit!")
        row = self.layout.row(); row = self.layout.row()
        

        for i in range(bpy.data.objects['exolife_properties'].Exolife_Settings.anims_count):
            row.alignment = 'LEFT'
            props = row.operator('render.exolifecharanimset', text = "Set"); props.anim = i+1;
            row.prop(Exolife_Settings, "anim"+str(props.anim))
            row.prop(Exolife_Settings, "anim"+str(props.anim)+"_name")
            row.alignment = 'RIGHT'
            row.prop(Exolife_Settings, "anim"+str(props.anim)+"_start")
            row.prop(Exolife_Settings, "anim"+str(props.anim)+"_end")
            row.prop(Exolife_Settings, "anim"+str(props.anim)+"_mb")
            row = self.layout.row()
    
        
        self.layout.label(text="BodyParts to render:")
        row = self.layout.row()
        
        row.prop(Exolife_Settings, "bp_hand_l")
        row = self.layout.row()
        row.prop(Exolife_Settings, "bp_legs")
        row = self.layout.row()
        row.prop(Exolife_Settings, "bp_torso")
        row = self.layout.row()
        row.prop(Exolife_Settings, "bp_head")
        row = self.layout.row()
        row.prop(Exolife_Settings, "bp_hand_r")
        col = self.layout.column()
        col.alignment = 'RIGHT'
        col.prop(Exolife_Settings, "bp_part_suffix")
        row = self.layout.row()
        row.operator('render.exolifecharrenderer', text = "Render!")
        

        
classes = (
    RENDER_OT_ExolifeCharRenderer_reInit,
    RENDER_OT_ExolifeCharVisibility,
    RENDER_OT_ExolifeCharRenderer,
    RENDER_OT_ExolifeAnimSet,
    VIEW3D_PT_ExolifeCharVisibility,
    ExolifeSettings,
    VIEW3D_PT_ExolifeCharRenderer
)


def register():
    bpy.utils.register_class(WM_OT_refresh)
    for cls in classes:
        bpy.utils.register_class(cls)
    bpy.types.Object.Exolife_Settings = bpy.props.PointerProperty(type=ExolifeSettings)
    

def unregister():
    for cls in classes:
        bpy.utils.unregister_class(cls)
#    del bpy.types.Scene.Exolife_Settings
    

if __name__ == "__main__":
    register()

1 Like

Thank you very much for your help! :pray:t2:
But, when Render button is pressed it give the exception:
ValueError: Macro Define: ‘RENDER_OT_ExolifeCharAnimSet’ is not a valid operator id
I tried to fix this, but gets more errors :flushed: Still not familiar with python scripting and macros…
I created simple test scene (sorry can’t upload original files), maybe it will help…
https://drive.google.com/file/d/1cZJsNaJ59id4d0xYYiVy3p0iTvHZi6P2/view?usp=sharing
Also noticed that in your script INVOKE_DEFAULT is not using in render call - is this means that rendering will be without render process preview? - because this is the main thing that left to achieve to my script :slightly_smiling_face:

exolife_charrenderer_to_macro.py (14.6 KB)

Thanks for the file. Made looking into it a breeze.

Discovered some issues - your class names get mangled by blender due inconsistencies between bl_idname and the class name itself so:

RENDER_OT_ExolifeAnimSet

blender will reference as

RENDER_OT_exolifecharanimset

etc.

I’ve renamed the classes to better match the guidelines for class naming. You can find more info here.

This way, when blender translates between the two qualified names, they will match one-to-one. For eg. I’ve moved Exolife which is part of every class into what’s considered the submodule prefix.
So:

RENDER_OT_exolifecharanimset

becomes

EXOLIFE_OT_char_anim_set

which can be referenced in layouts as:

"exolife.char_anim_set"

and called using:

bpy.ops.exolife.char_anim_set()

This makes it easier to reference each operator and panel since they have matching suffixes (part after _OT_).

1 Like

Hi! Thanks a lot! For clarification about some specific things (above) and for the script!!! I’m so grateful to you @iceythe !!
I tested it a bit and it works! :smiley: Amazing :upside_down_face: I will run it into a more stressful test later with huge amount of animations, but I think it will work as expected!))
One strange thing that I noticed - is that animation count(er) does not match amount of “animation setting lines” in interface (for 1 more or by 1 less line of settings), it’s not important, but when I tried to fix this - in interface all begin looks Ok, but it stops render last animation))) need to dig deeper how it works, it looks so much reworked, I hope it didn’t take too much time for you! Thanks again! :pray:t2:

1 Like

You can try change this in EXOLIFE_OT_char_renderer's execute():

        for i in range(1, settings.anims_count):

to

        for i in range(1, settings.anims_count + 1):

hmm, looks like that was not enough, but I resolved this a bit different, but almost in the same way… ))
and all works fine now! Hurray!
Again, @iceythe thanks for your help, wish you all the best! :wink: :partying_face:

1 Like

Hello!

I stumbled upon your sollution after 4 years. Hopefully you re still active on this forum.

I would like to have a few questions answered before I try to fully understand this approach. First of all, does this use the “EXEC_DEFAULT” method that blocks the main thread? or the “INVOKE_DEFAULT” that does not.

Secondly, (this is my bad sadly), how does this actually achive the task?

And finally, have you managed to find another way in the mean time that works with switching context in the handlers?