# batch_render.py Copyright (C) 2013, Jesse Kaukonen # # Allows setting up a queue of renders to be executed in sequence # # ***** BEGIN GPL LICENSE BLOCK ***** # # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ***** END GPL LICENCE BLOCK ***** bl_info = { "name": "Batch Render", "author": "Jesse Kaukonen", "version": (1,4), "blender": (2, 7, 2), "location": "Render > Render", "description": "Set up multiple render tasks to be executed in sequence", "warning": "", "wiki_url": "", "tracker_url": "", "category": "Render"} """ Usage: Launch from "Render > Render > Batch render" Additional links: Author Site: www.jessekaukonen.net e-mail: jesse dot kaukonen at gmail dot com """ import bpy from bpy.props import PointerProperty, StringProperty, BoolProperty, EnumProperty, IntProperty, CollectionProperty # Data structure that contains on/off flags for all layers class LayerSelection(bpy.types.PropertyGroup): active = bpy.props.BoolProperty(name="Active", description="Toggle on if the layer must be rendered", default=False) # Container that keeps track of the settings used for this render batch class BatchSettings(bpy.types.PropertyGroup): start_frame = bpy.props.IntProperty(name="Starting frame of this batch", default=0) end_frame = bpy.props.IntProperty(name="Ending frame of this batch", default=1) reso_x = bpy.props.IntProperty(name="X resolution", description="resoution of this batch", default=1920, min=1, max=10000, soft_min=1, soft_max=10000) reso_y = bpy.props.IntProperty(name="Y resoliution", description="resolution of this batch", default=1080, min=1, max=10000, soft_min=1, soft_max=10000) reso_percentage = bpy.props.IntProperty(name="percentage", description="Percentage of the resolution at which this batch is rendered", default=100, min=1, max=100, soft_min=1, soft_max=100) samples = IntProperty(name='Samples', description='Number of samples that is used (Cycles only)', min=1, max=1000000, soft_min=1, soft_max=100000, default=100) camera = StringProperty(name="Camera", description="Camera to be used for rendering this patch", default="") filepath = bpy.props.StringProperty(subtype='FILE_PATH', default="") layers = bpy.props.CollectionProperty(name="layer container", type=LayerSelection) markedForDeletion = bpy.props.BoolProperty(name="Toggled on if this must be deleted", default=False) # Container that records what frame ranges are to be rendered class BatchRenderData(bpy.types.PropertyGroup): frame_ranges = bpy.props.CollectionProperty(name="Container for frame ranges defined for rendering", type=BatchSettings) active_range = bpy.props.IntProperty(name="Index for currently processed range", default=0) class RenderButtonsPanel(): bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' bl_context = "render" # Updates a list of objects used by a drop down menu #def updateObjectList(): # cameras = [] # for index, obj in enumerate(bpy.context.scene.objects): # #if (obj.type == 'CAMERA'): # cameras.append((str(index), obj.name, str(index))) # bpy.types.Scene.camera_list = EnumProperty(name="Cameras", description="asd", items=cameras, default='0') # Box for selecting objects in a drop down menu # Thanks to Peter Roelants class CUSTOM_OT_SelectObjectButton(bpy.types.Operator): bl_idname = "batch_render.select_object" bl_label = "Select" bl_description = "Select the chosen object" def invoke(self, context, event): updateObjectList() obj = bpy.context.scene.objects[int(bpy.context.scene.camera_list)] #for o in bpy.data.objects: #print(o.name) print(obj.name) bpy.context.scene.objects.active = obj return {'FINISHED'} # Checks if a specified camera exists def checkCamera(c): for obj in bpy.context.scene.objects: if (obj.type == 'CAMERA'): if (obj.name == c): return True return False def getCameras(): out = [] for obj in bpy.context.scene.objects: if (obj.type == 'CAMERA'): out.append(obj.name) return out # A panel in the render section of properties space class BatchRenderPanel(RenderButtonsPanel, bpy.types.Panel): bl_label = "Batch Render" def draw(self, context): layout = self.layout batcher = bpy.context.scene.batch_render layout.operator("batch_render.render", text="Launch rendering") layout.operator("batch_render.add_new", text="Add new set") layout.operator('batch_render.remove', text="Delete selected", icon='CANCEL') layout.row() count = 0 # Print a control knob for every item currently defined for it in batcher.frame_ranges: layout.label(text="Batch " + str(count+1)) layout.prop(it, 'start_frame', text="Start frame") layout.prop(it, 'end_frame', text="End frame") layout.prop(it, 'reso_x', text="Resolution X") layout.prop(it, 'reso_y', text="Resolution Y") layout.prop(it, 'reso_percentage', text="Resolution percentage") layout.prop(it, 'samples', text="Samples (if using Cycles)") layout.prop(it, 'camera', text="Select camera") layout.prop(it, 'filepath', text="Output path") layout.label(text="Enabled layers") row = layout.row() i = 0 for it2 in it.layers: i += 1 row.prop(it2, 'active', text=str(i)) if (i % 5 == 0): row = layout.row() #layout.prop(bpy.context.scene, "camera_list", text="Objects") #layout.operator("batch_render.select_object", "objects") layout.prop(it, 'markedForDeletion', text="Delete") layout.row() count += 1 # Operator that starts the rendering class OBJECT_OT_BatchRenderButton(bpy.types.Operator): bl_idname = "batch_render.render" bl_label = "Batch Render" def execute(self, context): batcher = bpy.context.scene.batch_render sce = bpy.context.scene rd = sce.render batch_count = 0 for it in batcher.frame_ranges: batch_count += 1 print("***********") if (it.end_frame < it.start_frame): print("Skipped batch " + str(it.start_frame) + " - " + str(it.end_frame) + ": Start frame greater than end frame") continue sce.frame_start = it.start_frame sce.frame_end = it.end_frame rd.resolution_x = it.reso_x rd.resolution_y = it.reso_y rd.resolution_percentage = it.reso_percentage if (checkCamera(it.camera) == True): sce.camera = bpy.data.objects[it.camera] else: print("I did not find the specified camera for this batch. The camera was " + it.camera + ". Following cameras exist in the scene:") print(getCameras()) if (rd.engine == 'CYCLES'): sce.cycles.samples = it.samples sce.render.filepath = it.filepath sce.render.filepath += ("batch_" + str(batch_count) + "_" + str(it.reso_x) + "x" + str(it.reso_y) + "_") i = 0 while (i < 20): bpy.context.scene.layers[i] = it.layers[i].active if (bpy.context.scene.layers[i] == True): print("Enabled layer " + str(i+1)) i += 1 print("Rendering frames: " + str(it.start_frame) + " - " + str(it.end_frame)) print("At resolution " + str(it.reso_x) + "x" + str(it.reso_y) + " (" + str(it.reso_percentage) + "%)") if (rd.engine == 'CYCLES'): print("With " + str(it.samples) + " samples") print("using camera " + bpy.context.scene.camera.name) print("Saving frames in " + it.filepath) print("Ok! I'm beginning rendering now. Wait warmly.") bpy.ops.render.render(animation=True) sum = 0 for it in batcher.frame_ranges: if (it.end_frame >= it.start_frame): sum += (it.end_frame - it.start_frame) print("Rendered " + str(len(batcher.frame_ranges)) + " batches containing " + str(sum) + " frames") return {'FINISHED'} # Operator that adds a new frame range to be rendered class OBJECT_OT_BatchRenderAddNew(bpy.types.Operator): bl_idname = "batch_render.add_new" bl_label = "Add new set" def execute(self, context): batcher = bpy.context.scene.batch_render rd = bpy.context.scene.render batcher.frame_ranges.add() last_item = len(batcher.frame_ranges)-1 batcher.frame_ranges[last_item].start_frame = 1 batcher.frame_ranges[last_item].end_frame = 2 batcher.frame_ranges[last_item].samples = bpy.context.scene.cycles.samples batcher.frame_ranges[last_item].reso_x = rd.resolution_x batcher.frame_ranges[last_item].reso_y = rd.resolution_y batcher.frame_ranges[last_item].camera = bpy.context.scene.camera.name batcher.frame_ranges[last_item].filepath = bpy.context.scene.render.filepath i = 0 while (i < 20): batcher.frame_ranges[last_item].layers.add() if (bpy.context.scene.layers[i] == True): batcher.frame_ranges[last_item].layers[i].active = True i += 1 return {'FINISHED'} # Removes items that have been marked for deletion class OBJECT_OT_BatchRenderRemove(bpy.types.Operator): bl_idname = "batch_render.remove" bl_label = "Remove selected sets" def execute(self, context): batcher = bpy.context.scene.batch_render done = False # Ugh, ugly O(n^2) operation here since it's hard to edit these collectionProperties... # Difficult to remove marked entries from lists when you can't delete while iterating, # and you can't make copies of the objects. Unless it's possible somehow. copy.deepcopy # does not work while (done == False): count = 0 if (len(batcher.frame_ranges) < 1): break for it in batcher.frame_ranges: if (it.markedForDeletion == True): batcher.frame_ranges.remove(count) break count += 1 if (count >= (len(batcher.frame_ranges)-1)): done = True return {'FINISHED'} def register(): bpy.utils.register_module(__name__) bpy.types.Scene.batch_render = PointerProperty(type=BatchRenderData, name='Batch Render', description='Settings used for batch rendering') #updateObjectList() def unregister(): bpy.utils.unregister(__name__) if __name__ == "__main__": register()