15
$\begingroup$

I'm trying to make a script where I have to render several images of the same scene at the same frame, and write to disk with appropriate names. The problem is that although this works fine with a non-responsive UI, it doesn't work well when the render progress appears in the Blender UI.

I mean,

bpy.ops.render.render()

uses "EXEC_DEFAULT" when called from a script. This leaves a blocked User Interface until the render finishes, meaning that you can't cancel the process, except by killing Blender. The problem is that the render may be too big in resolution, and the user could want to stop the process when realizing that the render could take hours, so the idea is to cancel this process without losing the currently opened project.

If I call,

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

I get a nice preview bar in the Info Window with a clickable "X" button. I want the user to be able to press that button to cancel the operation if needed, but for a series of renders (not animation).

The problem here seems to be when trying to loop this command with the INVOKE_DEFAULT context to write several renders. I made a lot of testing, and this way of calling the render operator seems to continue with whatever is next in the script, so it loops very fast and ends before the rendering actually finishes.

I think that I need some way to stop the script and make it wait until render finishes to continue looping with the next ones. I tried the "render_complete" and "render_cancel" handlers, but I can't use them to start a new render or even make the rest of the script wait until the loop of renders is complete. I tried threading and events without success either. Maybe I'm doing something wrong.

import bpy

render = bpy.context.scene.render
path = render.filepath = "/tmp/"
shots = ["one", "two", "three"]

for x in shots:
    render.filepath = path + x
    bpy.ops.render.render("INVOKE_DEFAULT", write_still=True) # this doesn't work

    # but if I use:
    # bpy.ops.render.render(write_still=True)
    # without the INVOKE thing, images are written as expected,
    # but losing the possibility to see the progress and cancel rendering
$\endgroup$

2 Answers 2

24
$\begingroup$

Yes! This is possible.

I finally managed to get it to work in a loop. The current solution allows to cancel the operation with the X button in the Info Window and the ESC key. The images are rendered in sequence on the same frame without the need to create additional scenes or keyframes. Render handlers are used to know the moment when a new render can be started. Then, a modal timer is used in a loop to start the render when it becomes available. I think this timer should be similar to the wait method of the threading.Event() object, but I failed making it to work with the render operator that way.

The problem with builtin modal operators like render.render is that these need to be wrapped and executed inside a modal loop to work correctly. If this is not the case, the older job locks, and prevents subsequent renders to spawn and start, making basic loops pretty useless. This is why additional scenes don't make a difference, because a render job can be executed once at a time in a blender instance.

So, to make it work, a new operator needs to be created. The actual render process stays inside the modal method, and handlers take care of signaling.

import bpy

class Multi_Render(bpy.types.Operator):
    """Docstring"""
    bl_idname = "render.multi"
    bl_label = "Render multiple times"
    
    # Define some variables to register
    _timer = None
    shots = None
    stop = None
    rendering = None
    path = "/tmp/"
    
    # Define the handler functions. I use pre and
    # post to know if Blender "is rendering"
    def pre(self, scene, context=None):
        self.rendering = True
        
    def post(self, scene, context=None):
        self.shots.pop(0) # This is just to render the next
                          # image in another path
        self.rendering = False

    def cancelled(self, scene, context=None):
        self.stop = True

    def execute(self, context):
        # Define the variables during execution. This allows
        # to define when called from a button
        self.stop = False
        self.rendering = False
        self.shots = ["one.png",   # I'm just rendering 3 images but you
                      "two.png",   # can adapt to your needs
                      "three.png"]
        
        context.scene.render.filepath = self.path
        
        bpy.app.handlers.render_pre.append(self.pre)
        bpy.app.handlers.render_post.append(self.post)
        bpy.app.handlers.render_cancel.append(self.cancelled)
        
        # The timer gets created and the modal handler
        # is added to the window manager
        self._timer = context.window_manager.event_timer_add(0.5, window=context.window)
        context.window_manager.modal_handler_add(self)
        
        return {"RUNNING_MODAL"}
        
    def modal(self, context, event):
        if event.type == 'TIMER': # This event is signaled every half a second
                                  # and will start the render if available
            
            # If cancelled or no more shots to render, finish.
            if True in (not self.shots, self.stop is True): 
                
                # We remove the handlers and the modal timer to clean everything
                bpy.app.handlers.render_pre.remove(self.pre)
                bpy.app.handlers.render_post.remove(self.post)
                bpy.app.handlers.render_cancel.remove(self.cancelled)
                context.window_manager.event_timer_remove(self._timer)
                
                return {"FINISHED"} # I didn't separate the cancel and finish
                                    # events, because in my case I don't need to,
                                    # but you can create them as you need
            
            elif self.rendering is False: # Nothing is currently rendering.
                                          # Proceed to render.
                sc = context.scene
                
                # I'm using cameras named just as the output files,
                # but adapt to your needs
                sc.camera = bpy.data.objects[self.shots[0]] 
                                                            
                sc.render.filepath = self.path + self.shots[0]
                bpy.ops.render.render("INVOKE_DEFAULT", write_still=True)

        return {"PASS_THROUGH"}
        # This is very important! If we used "RUNNING_MODAL", this new modal function
        # would prevent the use of the X button to cancel rendering, because this
        # button is managed by the modal function of the render operator,
        # not this new operator!

def register():
    bpy.utils.register_class(Multi_Render)
 
def unregister():
    bpy.utils.unregister_class(Multi_Render)

if __name__ == "__main__":
    register()
    
    bpy.ops.render.multi() # Test call

I hope this can help others if in the same situation.

$\endgroup$
14
  • 3
    $\begingroup$ Very cool, thanks! With minor changes it works under Blender 2.80 too. Instead of register_module(__name__) there is now (un)register_class(Multi_Render) and event_timer_add needs to change the second parameter to window=context.window. $\endgroup$ Commented Jan 10, 2019 at 19:20
  • $\begingroup$ Do you guys have a problem with running this modal in "Full Screen"? (Top menu -> Render -> Display Mode -> Full Screen). It changes my layout to just Image Editor and destroys current layout completeley... $\endgroup$
    – kilbee
    Commented Feb 3, 2019 at 12:26
  • 1
    $\begingroup$ @AnsonSavage Well, you should be able to set the filepath in the modal function, instead of the pre handler. It seems that setting the filepath in the pre handler is just too late in the execution, and the render.render operator reads the information from the scene before calling the pre handler. This is why in the example, the filepath is set in the modal, just before calling the render operator. On the other hand, I don't see the point of setting the filepath in the post handler. You can just do it before calling the next operator and it should be fine. $\endgroup$
    – Chaos
    Commented Apr 11, 2022 at 19:34
  • 1
    $\begingroup$ @AnsonSavage Naming that parameter 'context' is not mandatory. The handlers system never passes that object during rendering. It seems that in the source, handlers are defined generically with a tuple of two positional arguments. Some handlers use two, one, or no arguments at all. This could explain why the word 'dummy' was used in the past, instead of 'context', because it's a positional argument and it must be declared, otherwise captured by *args. It is also more realistic to think of this parameter as an optional object that could come in different types, not necessarily a context object. $\endgroup$
    – Chaos
    Commented Jul 27, 2022 at 2:28
  • 1
    $\begingroup$ @AnsonSavage If a name will be required instead of 'dummy', I think 'depsgraph' is more accurate, because I haven't seen any handler receiving a proper 'context' object before. It's much more realistic and safe to assume that in this state the context to use in a handler is the global (bpy.context), because during execution of the handler functions, the context may have changed from the initial user interaction. $\endgroup$
    – Chaos
    Commented Jul 27, 2022 at 2:32
1
$\begingroup$

If you have the luxury of arranging the sequence of renders to just be frames of a single animation (by clever use of keyframes) then you could let them use the regular blender render menu option.

I have done this for rendering a skybox: by keyframing the camera through 6 frames pointing in the 6 directions.

I do not doubt that many scenarios would be hard to fit into this technique.

$\endgroup$
4
  • $\begingroup$ Yes! I'm doing the same thing. It's a skybox. But it's important to keep the same frame, so, making an animation of 6 frames forward doesn't sound very good to me. But! The idea of moving the frame forward and then backwards to stay in the same frame and "trigger" some kind of update, sounds good to me. Will have to try that. $\endgroup$
    – Chaos
    Commented Jan 17, 2017 at 2:41
  • $\begingroup$ Could you try an alternative of 6 scenes, each with a different camera, and all with a single frame, so 6 scenes * 1 frames? $\endgroup$
    – Mutant Bob
    Commented Jan 17, 2017 at 3:23
  • $\begingroup$ Maybe, but my current solution is based on 6 independent cameras, which iterate and render in sequence. I'm suspecting that the render operator will not care about if I have new scenes or cameras, but more on the execution context. But I'll try. $\endgroup$
    – Chaos
    Commented Jan 17, 2017 at 4:03
  • $\begingroup$ The idea of the frame change or the linked scenes didn't work. Just check the final answer for details. $\endgroup$
    – Chaos
    Commented Jan 20, 2017 at 15:01

You must log in to answer this question.

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