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:

  1. List of cameras (list of camera names you want to render from)
  2. 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.

Get sample scene with the script:

render_Everything.blend

2 Comments

  1. 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 ?

    1. 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.

Post A Comment