1
$\begingroup$

I created a script that renders and saves the selected objects. The script is quite large and I don't think it is necessary to post it completely.

def myRenderFunction(self,context):
    for o in context.selected_objects:
        ###some coordinates for camera position
        ###some functions to hide
        myPath = "C\:blah blah" + os.sep + o.name
        data_blocks = {o}
        bpy.data.libraries.write(myPath+".blend", data_blocks)
        bpy.ops.render.render(write_still=True)
        context.scene.render.filepath = myPath

My issue is that I can't find a way to stop it once executed because Blender's Interface is completely frozen while rendering the images.

Q: How can I allow the user to stop the rendering process at any given time?

$\endgroup$
7
  • $\begingroup$ @RayMairlot Thank you very much for reporting, I was looking at this link first, but in fact my script is inside a for loop, and I found that the situation is slightly different $\endgroup$
    – Noob Cat
    Sep 19, 2019 at 14:56
  • $\begingroup$ You want to allow the user hitting a button to abort the rendering process right? If that's your goal, you have to use a modal operator, which leads directly to the linked answer (it's is a hack anyway). Try to understand the answer and also have a look into the modal operator template... Or is there something I didn't understand? @Pastrokkio $\endgroup$
    – brockmann
    Sep 19, 2019 at 19:58
  • $\begingroup$ @brockmann Ty , The problem is that I tried with the modal operator, but I don't know how to place the exit from the loop, I tried to insert the for loop inside the modal, but it remains frozen anyway, for days now I have stopped at the pole of the questions, I'm really frustrated because all the questions and answers I found don't solve my problem, I just want to understand how to place that for loop inside a modal operator correctly. This is not explained in any answer (or at least I have not found) For me these concepts are still little known, so I asked the specific question $\endgroup$
    – Noob Cat
    Sep 19, 2019 at 20:20
  • 1
    $\begingroup$ Yeah, but it's a really broad question and takes a lot of effort to make it work (and testing all that stuff, also your question is incomplete IMO). I'd suggest split up the process into 2 operators, one for rendering (based on the linked code) and another one for loading the objects and to execute the rendering operator (the linked operator is basically a wrapper for render.render() allowing the user to exit). $\endgroup$
    – brockmann
    Sep 19, 2019 at 20:46
  • 1
    $\begingroup$ Minor code changes to make it work: pastecode.xyz/view/e10a812a However, it's just a starting point @Pastrokkio $\endgroup$
    – brockmann
    Sep 19, 2019 at 21:07

1 Answer 1

4
$\begingroup$

Minor code changes are required to make the code from the linked answer work for objects in selection.

Following modal operator is just some kind of barebone code, a good starting point using the timer though. Edit the render path according to your Operating System, run the script, select the objects in 3d view, press F3 and type Render multi...

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/"       # Linux
    #path = "C:\\tmp\\"  # Windows

    # 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) # Remove current object from list
                          # Render next Object 
        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

        # One shot per selected object
        self.shots = [o.name for o in context.selected_objects]

        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('INVOKE_DEFAULT') # Test call

Note: You don't necessarily have to use a for-loop, the post method removes each item from the list in case the rendering is finished. Also I suggest split up the process into 2 operators, one for rendering only and another one to manage the whole process.


In case you don't want the render window to show up for whatever reason, you can either set the display_mode to 'NONE' temporary:

    elif self.rendering is False: # Nothing is currently rendering.
                                  # Proceed to render.
        sc = context.scene
        sc.render.display_mode = 'NONE'

        sc.render.filepath = self.path + self.shots[0]
        bpy.ops.render.render("INVOKE_DEFAULT", write_still=True)

        # Reset display mode
        sc.render.display_mode = 'WINDOW'

and add a check for the ESC event to remove the timer:

def modal(self, context, event):

    if event.type in {'ESC'}:
        context.window_manager.event_timer_remove(self._timer)
        bpy.app.handlers.render_pre.remove(self.pre)
        bpy.app.handlers.render_post.remove(self.post)
        bpy.app.handlers.render_cancel.remove(self.cancelled)

        self.report({'WARNING'}, 'User interuption')
        return {'FINISHED'}
    ...

Or you can call render.render() without INVOKE_DEFAULT to execute the operator directly (without the UI), e.g. based on a given variable (notice that the progress bar does not show up):

        if render_window:
            bpy.ops.render.render("INVOKE_DEFAULT", write_still=True)
        else:
            bpy.ops.render.render(write_still=True)

Also you can add an invoke() method along with a BoolProperty() to the operator so the user can decide whether or not displaying the render window:

render_window: bpy.props.BoolProperty(
    name="Show Render Window",
    description="A bool property",
    default = False
)

def invoke(self, context, event): # Used for user interaction
    wm = context.window_manager
    return wm.invoke_props_dialog(self)

and add a condition for that within the modal method (before rendering):

    elif self.rendering is False: # Nothing is currently rendering.
                                  # Proceed to render.
        sc = context.scene

        if self.render_window == False:
            sc.render.display_mode = 'NONE'

        sc.render.filepath = self.path + self.shots[0]
        bpy.ops.render.render("INVOKE_DEFAULT", write_still=True)

        # Reset display mode
        sc.render.display_mode = 'WINDOW'

enter image description here

However, it is indeed tricky to make it work properly...

$\endgroup$
10
  • $\begingroup$ I thank you for your time, how can I stop this with the esc button? because i not need the preview render windows $\endgroup$
    – Noob Cat
    Sep 20, 2019 at 3:00
  • $\begingroup$ Thank you very much for the great effort, however I have taken inspiration from your phrase "It is not necessary to use a for loop" and in fact that was the trick, when you used the list of objects with the append function, I solved the blocking of the loop for. But I see that it's not easy to get out of the modal, sometimes it comes out and sometimes it's not a known blender problem? $\endgroup$
    – Noob Cat
    Sep 20, 2019 at 15:49
  • $\begingroup$ Not sure what you are talking about: You've got a list with all objects in selection right? Why appending something to that? It's already there and good to go. Anyway, to solve your second issue (1 out of 100 issues here) you can try to decrease the timer value, should make it easier getting out of my modal (not the loop) @Pastrokkio $\endgroup$
    – brockmann
    Sep 20, 2019 at 16:06
  • 1
    $\begingroup$ Working here without any issues. You should have things sorted before rendering, maybe that's your issue... All I could imagine from reading your post plus some extra bits is already part of my answer so I don't know how to improve it even further @Pastrokkio $\endgroup$
    – brockmann
    Sep 20, 2019 at 16:35
  • 1
    $\begingroup$ I think one simple way is adding a global boolean e.g. to the scene and ask for its current state in the same line as checking for the event like if event.type in {'ESC'} and C.scene.your_bool_prop: @SakariNiittymaa $\endgroup$
    – brockmann
    Feb 6, 2020 at 14:40

You must log in to answer this question.

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