If you are doing any kind of product visualization work, you have probably met with a situation where you need to render multiple camera angles of a product with multiple material variations on it. Doing it by hand can be quite tedious – especially if each render takes longer time. You must wait when the render finishes, change camera or material, change output path.. render again. Not exactly a fun activity and also with high probability of human error.
Lucikly blender has few very useful python callback functions:
bpy.app.handlers.render_pre
bpy.app.handlers.render_post
bpy.app.handlers.render_cancel
You can register your own functions there, and they will be called before render starts, after render finishes and if render is cancelled. This can be utilized to chain together multiple renders in an automatic way. In your render_post
callback function, you can make some changes to scene (like change active camera, output path or material assignments) and then run the render again with bpy.ops.render.render
operator. Just make sure to stop at some point 🙂
I have used this in several projects and it is super useful – especially when there are frequent changes and you need to re-render everything again and again!
While doing this, there is a slight problem that because the python script never finishes running while waiting for render to finish, Blenders UI becomes unresponsive and there is no feedback. To solve this, you can write your render script as a modal operator. It is a kind of operator that runs in the background and allows interaction with the rest of Blender UI (typically used for interactive tools etc.).
I did exactly that – created an operator which when run, builds a render queue based on input parameters and then renders everything on that queue. Because it is an operator with actual input parameters, you could even write an UI for it. I did not write an UI, because for every project requirements can be different and it’s impossible to come up with universal solution and UI. And also It’s not that hard to write render parameters by hand in script using a predefined template.
What this script does is – it builds and renders a render queue based on two input parameters:
- List of cameras (list of camera names you want to render from)
- List of groups of [Object]->[Material] assignment pairs (object name and material name). Each group can have many object->material assignment pairs in it and it is called a “material configuration“.
In other words – you can have several “material configurations” defined for your scene. In each configuration you can set which material goes to which object. The script builds a render queue – for each camera it will render each material configuration. Then as the queue gets rendered – it will update object materials according to active material configuration, change the active camera and render. When render is finished, it takes next item from render queue – changes the scene materials, active camera and renders again. Until queue is finished.
You can also specify render output folder as a parameter. File name will be generated automatically based on camera name and material configuration name.
See the example scene to see a simple demonstration. Just run the script to start rendering.
import bpy from bpy.props import CollectionProperty from bpy.props import StringProperty from bpy.types import PropertyGroup #define special types for passing parameters class StringValue(bpy.types.PropertyGroup): value: StringProperty(name="Value") class StringCollection(bpy.types.PropertyGroup): value: CollectionProperty(type=StringValue) #define operator class class Multi_Render(bpy.types.Operator): bl_idname = "render.everything" bl_label = "Render Everything!" #Operator input parameters #string - Base path to render to.. basePath: StringProperty(name="Path") #List of cameras to render camerasList: CollectionProperty(type=StringValue, name="Cameras") #List of material configurations to render materialCfg: CollectionProperty(type=StringCollection, name="Materials") ##################### # internal variables cancelRender = None #was render cancelled by user rendering = None #is currently rendering renderQueue = None #render queue timerEvent = None #timer #Rendering callback functions def pre_render(self, dummy): self.rendering = True #mark rendering flag def post_render(self, dummy): self.renderQueue.pop(0) #remove finished item from render queue self.rendering = False #clear rendering flag def on_render_cancel(self, dummy): self.cancelRender = True #mark cancel render flag #Main operator function for user execution def execute(self, context): self.cancelRender = False # clear cancel flag self.rendering = False # clear rendering flag #fill renderQueue from input parameters with each camera rendering each material configuration self.renderQueue = [] for mat in self.materialCfg: for cam in self.camerasList: self.renderQueue.append({"Camera":cam.value, "MatCfg":mat.value}) #Register callback functions bpy.app.handlers.render_pre.append(self.pre_render) bpy.app.handlers.render_post.append(self.post_render) bpy.app.handlers.render_cancel.append(self.on_render_cancel) #Create timer event that runs every second to check if render renderQueue needs to be updated self.timerEvent = context.window_manager.event_timer_add(1.0, window=context.window) #register this as running in background context.window_manager.modal_handler_add(self) return {"RUNNING_MODAL"} #modal callback when there is some event.. def modal(self, context, event): #timer event every second if event.type == 'TIMER': # If cancelled or no items in queue to render, finish. if not self.renderQueue or self.cancelRender is True: #remove all render callbacks bpy.app.handlers.render_pre.remove(self.pre_render) bpy.app.handlers.render_post.remove(self.post_render) bpy.app.handlers.render_cancel.remove(self.on_render_cancel) #remove timer context.window_manager.event_timer_remove(self.timerEvent) self.report({"INFO"},"RENDER QUEUE FINISHED") return {"FINISHED"} #nothing is rendering and there are items in queue elif self.rendering is False: sc = bpy.context.scene qitem = self.renderQueue[0] #first item in rendering queue #change scene active camera cameraName = qitem["Camera"] if cameraName in sc.objects: sc.camera = bpy.data.objects[qitem["Camera"]] else: self.report({'ERROR_INVALID_INPUT'}, message="Can not find camera "+cameraName+" in scene!") return {'CANCELLED'} matCfg = qitem["MatCfg"] # for simplcity we store special key __config_name along material assignments to store name for this material configuration configName = matCfg["__config_name"].value self.report({"INFO"}, "Rendering config: " + configName) #set output file path as base path + condig name + camera name sc.render.filepath = self.basePath + configName + "_" + sc.camera.name print("Out Path: " + sc.render.filepath) #Go through and apply material configs for kc in matCfg: if kc.name == "__config_name": continue objName = kc.name matName = kc.value obj = None if objName in sc.objects: obj = bpy.data.objects[objName] else: self.report({'ERROR_INVALID_INPUT'}, message="Can not find object "+objName+" in scene!") if matName in bpy.data.materials and obj is not None: mat = bpy.data.materials[matName] obj.material_slots[0].material = mat #set as fist material .. will not work with multiple materials else: self.report({'ERROR_INVALID_INPUT'}, message="Can not find material "+matName+" in scene!") #start new render bpy.ops.render.render("INVOKE_DEFAULT", write_still=True) return {"PASS_THROUGH"} def register(): bpy.utils.register_class(StringValue) bpy.utils.register_class(StringCollection) bpy.utils.register_class(Multi_Render) def unregister(): bpy.utils.unregister_class(Multi_Render) bpy.utils.unregister_class(StringValue) bpy.utils.unregister_class(StringCollection) if __name__ == "__main__": register() # build input parameters like this: # Cameras: # [ # { "name":"x", "value":"camera_name" }, # { "name":"x", "value":"camera_name" }, # ... # ] # # Material: # [ # { "name":"x", "value": # [ # { "name":"__config_name", "value":"config name value" }, # { "name":"object_name", "value":"material_name" }, # { "name":"object_name", "value":"material_name" }, # ... # ] # }, # ... # ] # #c_cameras = bpy.data.collections['Cameras'] #cameras = [ {"name":str(i), "value":o.name} for i,o in enumerate(c_cameras.objects) ] # Below is a code that builds input parameters and executes operator. You can do that from a separate script if # operator is registered as addon. In that case remove all code below this comment. cameras = [ {"name":"1", "value":"Camera_0"}, {"name":"2", "value":"Camera_1"} ] matConf = [] cfg = { "name":"1", "value": [ {"name":"__config_name", "value":"Config_1"}, {"name":"Cube_1", "value":"Mat_A"}, {"name":"Cube_2", "value":"Mat_B"} ] } matConf.append(cfg) cfg = { "name":"2", "value": [ {"name":"__config_name", "value":"Config_2"}, {"name":"Cube_1", "value":"Mat_B"}, {"name":"Cube_2", "value":"Mat_C"} ] } matConf.append(cfg) cfg = { "name":"3", "value": [ {"name":"__config_name", "value":"Config_3"}, {"name":"Cube_1", "value":"Mat_C"}, {"name":"Cube_2", "value":"Mat_A"} ] } matConf.append(cfg) #This is how you call the operator once it is registered! bpy.ops.render.everything(basePath = "//RenderAll/", camerasList=cameras, materialCfg=matConf)
This might seem a bit long, but it is mostly because of comments and operator parameter definition sample! Feel free to modify it for your own usage needs.
Hi!
Thanks a lot for your code. I’m trying to make it work in my project without success for now. I’m using Blender 2.81.
I copy pasted your Multi_Render class, registered everything. I have multiple cameras but I dont want to change materials so I did:
matConf = []
cfg = { “name”:”1″, “value”:
[ {“name”:”__config_name”, “value”:”Config_null”},
]
}
matConf.append(cfg)
bpy.ops.render.everything(basePath = “outputs\\”, camerasList=cameras, materialCfg=matConf)
But I got two issues:
– the rendering is not run in background. Each time it renders, it opens a window and i can’t go back to the blender GUI
– it always render for the first camera. I have 3 cameras but it constantly generate for the first one, even if the first one has already been generated.
Do you have any idea where these problems could come from ?
For those who may be interested, I finally found the answer. Apparently in Blender 2.81, pre_render and post_render takes 3 arguments as input instead of 2.
So I replaced
def pre_render(self, dummy)
by
def pre_render(self, dummy1, dummy2)
and idem for post_render.
Just copied your code, and did some small modifications.
Instead of assigning different materials to different objects i simply did some of my own manipulation functions instead.
The parameters for my functions i put inside the cfg .
howeveer, for some reason it keeps looping the same render over and over in version 3.0.1…
any fixes?
UPDATE for Blender 3
#Rendering callback functions
def pre_render(self, dummy1, dummy2):
self.rendering = True #mark rendering flag
def post_render(self, dummy1, dummy2):
self.renderQueue.pop(0) #remove finished item from render queue
self.rendering = False #clear rendering flag
def on_render_cancel(self, dummy1, dummy2):
self.cancelRender = True #mark cancel render flag