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'

However, it is indeed tricky to make it work properly...
render.render()
allowing the user to exit). $\endgroup$