6
$\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$
8
$\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, dummy):
        self.rendering = True

    def post(self, dummy):
        self.shots.pop(0) # This is just to render the next
                          # image in another path
        self.rendering = False

    def cancelled(self, dummy):
        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$
  • 2
    $\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$ – Vit Kovalcik Jan 10 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 Feb 3 at 12:26
  • $\begingroup$ well, that seems like a bug actually. You could try it with the last build of Blender 2.8 to confirm and then report it. BTW, you could go "Back to Previous" Layout using the button at the top, though this isn't a real solution to the bug. It should go back when pressing Esc. Report it mentioning that. $\endgroup$ – Chaos Feb 6 at 11:36
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$
  • $\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 Jan 17 '17 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 Jan 17 '17 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 Jan 17 '17 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 Jan 20 '17 at 15:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.