Skip to content
Snippets Groups Projects
render_shots.py 25 KiB
Newer Older
  • Learn to ignore specific revisions
  • # ***** 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": "Render Shots",
        "author": "Aaron Symons",
        "version": (0, 3, 2),
        "blender": (2, 76, 0),
        "location": "Properties > Render > Render Shots",
        "description": "Render an image or animation from different camera views",
        "warning": "",
        "wiki_url": "http://wiki.blender.org/index.php?title=Extensions:2.6/Py"\
                    "/Scripts/Render/Render_Shots",
        "tracker_url": "https://developer.blender.org/maniphest/task/edit/form/2/",
        "category": "Render"}
    
    
    import bpy
    from bpy.props import BoolProperty, IntProperty, StringProperty
    from bpy.app.handlers import persistent
    import os, shutil
    
    
    #####################################
    # Update Functions
    #####################################
    def shape_nav(self, context):
        nav = self.rs_shotshape_nav
    
        if self.rs_shotshape_shape != "":
            shapeVerts = bpy.data.objects[self.rs_shotshape_shape].data.vertices
            max = len(shapeVerts)-1
            min = max - (max+max)
    
            if nav > max or nav < min:
                nav = 0
    
            v = shapeVerts[nav].co
            self.location = (v[0], v[1], v[2])
        return None
    
    
    def is_new_object(ob):
        try:
            isNew = ob["rs_shotshape_use_frames"]
        except:
            isNew = None
    
        return True if isNew is None else False
    
    
    def update_shot_list(scn):
        scn.rs_is_updating = True
        if hasattr(scn, 'rs_create_folders'):
            scn.rs_create_folders = False
    
        for ob in bpy.data.objects:
            if ob.type == 'CAMERA':
                if is_new_object(ob):
                    ob["rs_shot_include"] = True
                    ob["rs_shot_start"] = scn.frame_start
                    ob["rs_shot_end"] = scn.frame_end
                    ob["rs_shot_output"] = ""
                    ob["rs_toggle_panel"] = True
                    ob["rs_settings_use"] = False
                    ob["rs_resolution_x"] = scn.render.resolution_x
                    ob["rs_resolution_y"] = scn.render.resolution_y
                    ob["rs_cycles_samples"] = 10
                    ob["rs_shotshape_use"] = False
                    ob["rs_shotshape_shape"] = ""
                    ob["rs_shotshape_nav"] = 0
                    ob["rs_shotshape_nav_start"] = False
                    ob["rs_shotshape_offset"] = 1
                    ob["rs_shotshape_use_frames"] = False
                else:
                    ob["rs_shot_include"]
                    ob["rs_shot_start"]
                    ob["rs_shot_end"]
                    ob["rs_shot_output"]
                    ob["rs_toggle_panel"]
                    ob["rs_settings_use"]
                    ob["rs_resolution_x"]
                    ob["rs_resolution_y"]
                    ob["rs_cycles_samples"]
                    ob["rs_shotshape_use"]
                    ob["rs_shotshape_shape"]
                    ob["rs_shotshape_nav"]
                    ob["rs_shotshape_nav_start"]
                    ob["rs_shotshape_offset"]
                    ob["rs_shotshape_use_frames"]
    
        scn.rs_is_updating = False
    
    
    #####################################
    # Initialisation
    #####################################
    def init_props():
        object = bpy.types.Object
        scene = bpy.types.Scene
    
        # Camera properties
    
        object.rs_shot_include = BoolProperty(name="",
    
            description="Include this shot during render", default=True)
    
        object.rs_shot_start = IntProperty(name="Start",
            description="First frame in this shot",
            default=0, min=0, max=300000)
    
        object.rs_shot_end = IntProperty(name="End",
    
            description="Last frame in this shot",
    
            default=0, min=0, max=300000)
    
        object.rs_shot_output = StringProperty(name="",
            description="Directory/name to save to", subtype='DIR_PATH')
    
        object.rs_toggle_panel = BoolProperty(name="",
            description="Show/hide options for this shot", default=True)
    
        # Render settings
    
        object.rs_settings_use = BoolProperty(name = "", default=False,
    
            description = "Use specific render settings for this shot")
    
        object.rs_resolution_x = IntProperty(name="X",
            description="Number of horizontal pixels in the rendered image",
            default=2000, min=4, max=10000)
    
        object.rs_resolution_y = IntProperty(name="Y",
            description = "Number of vertical pixels in the rendered image",
            default=2000, min=4, max=10000)
    
        object.rs_cycles_samples = IntProperty(name="Samples",
            description = "Number of samples to render for each pixel",
            default=10, min=1, max=2147483647)
    
        # Shot shapes
        object.rs_shotshape_use = BoolProperty(name="", default=False,
            description="Use a shape to set a series of shots for this camera")
    
    
        object.rs_shotshape_shape = StringProperty(name="Shape:",
    
            description="Select an object")
    
    
        object.rs_shotshape_nav = IntProperty(name="Navigate",
            description="Navigate through this shape's vertices (0 = first vertex)",
    
            default=0, update=shape_nav)
    
    
        object.rs_shotshape_nav_start = BoolProperty(name="Start from here",
    
            default=False,
            description="Start from this vertex (skips previous vertices)")
    
    
        object.rs_shotshape_offset = IntProperty(name="Offset",
            description="Offset between frames (defines animation length)",
    
            default=1, min=1, max=200)
    
        object.rs_shotshape_use_frames = BoolProperty(name="Use frame range",
            description="Use the shot's frame range instead of the object's vertex"\
            " count", default=False)
    
        # Internal
        scene.rs_is_updating = BoolProperty(name="", description="", default=False)
    
        scene.rs_create_folders = BoolProperty(name="", description="", default=False)
    
        scene.rs_main_folder = StringProperty(name="Main Folder",
                    subtype='DIR_PATH', default="",
                    description="Main folder in which to create the sub folders")
    
        scene.rs_overwrite_folders = BoolProperty(name="Overwrite", default=False,
                    description="Overwrite existing folders (this will delete all"\
                    " files inside any existing folders)")
    
    
    
    #####################################
    # Operators and Functions
    #####################################
    RENDER_DONE = True
    RENDER_SETTINGS_HELP = False
    TIMELINE = {"start": 1, "end": 250, "current": 1}
    RENDER_SETTINGS = {"cycles_samples": 10, "res_x": 1920, "res_y": 1080}
    
    
    @persistent
    def render_finished(unused):
        global RENDER_DONE
        RENDER_DONE = True
    
    
    def using_cycles(scn):
        return True if scn.render.engine == 'CYCLES' else False
    
    
    def timeline_handler(scn, mode):
        global TIMELINE
    
        if mode == 'GET':
            TIMELINE["start"] = scn.frame_start
            TIMELINE["end"] = scn.frame_end
            TIMELINE["current"] = scn.frame_current
    
        elif mode == 'SET':
            scn.frame_start = TIMELINE["start"]
            scn.frame_end = TIMELINE["end"]
            scn.frame_current = TIMELINE["current"]
    
    
    def render_settings_handler(scn, mode, cycles_on, ob):
        global RENDER_SETTINGS
    
        if mode == 'GET':
            RENDER_SETTINGS["cycles_samples"] = scn.cycles.samples
            RENDER_SETTINGS["res_x"] = scn.render.resolution_x
            RENDER_SETTINGS["res_y"] = scn.render.resolution_y
    
        elif mode == 'SET':
            if cycles_on:
                scn.cycles.samples = ob["rs_cycles_samples"]
            scn.render.resolution_x = ob["rs_resolution_x"]
            scn.render.resolution_y = ob["rs_resolution_y"]
    
        elif mode == 'REVERT':
            if cycles_on:
                scn.cycles.samples = RENDER_SETTINGS["cycles_samples"]
            scn.render.resolution_x = RENDER_SETTINGS["res_x"]
            scn.render.resolution_y = RENDER_SETTINGS["res_y"]
    
    
    def frames_from_verts(ob, end, shape, mode):
        start = ob.rs_shot_start
        frame_range = (end - start)+1
        verts = len(shape.data.vertices)
    
        if frame_range % verts != 0:
            end += 1
            return create_frames_from_verts(ob, end, shape, mode)
        else:
            if mode == 'OFFSET':
                return frame_range / verts
            elif mode == 'END':
                return end
    
    
    def keyframes_handler(scn, ob, shape, mode):
        bpy.ops.object.select_all(action='DESELECT')
    
        ob.select_set(True)
    
        start = ob.rs_shotshape_nav if ob.rs_shotshape_nav_start else 0
    
        if ob.rs_shotshape_use_frames and shape is not None:
            firstframe = ob.rs_shot_start
            offset = frames_from_verts(ob, ob.rs_shot_end, shape, 'OFFSET')
        else:
            firstframe = 1
            offset = ob.rs_shotshape_offset
    
        if mode == 'SET':
            scn.frame_current = firstframe
            for vert in shape.data.vertices:
                if vert.index >= start:
                    ob.location = vert.co
                    bpy.ops.anim.keyframe_insert_menu(type='Location')
                    scn.frame_current += offset
            return (len(shape.data.vertices) - start) * offset
    
        elif mode == 'WIPE':
            ob.animation_data_clear()
    
    
    class RENDER_OT_RenderShots_create_folders(bpy.types.Operator):
        ''' Create the output folders for all cameras '''
        bl_idname = "render.rendershots_create_folders"
        bl_label = "Create Folders"
    
        mode = IntProperty()
    
        def execute(self, context):
            scn = context.scene
    
            if self.mode == 1: # Display options
                scn.rs_create_folders = True
    
            elif self.mode == 2: # Create folders
                if scn.rs_main_folder != "" and not scn.rs_main_folder.isspace():
                    for ob in bpy.data.objects:
                        if ob.type == 'CAMERA' and not is_new_object(ob):
                            # Name cleaning
                            if "." in ob.name:
                                name = ob.name.split(".")
                                camName = name[0]+name[1]
                            else:
                                camName = ob.name
    
                            mainFolder = scn.rs_main_folder
                            destination = os.path.join(mainFolder, camName)
    
                            # Folder creation
                            if scn.rs_overwrite_folders:
                                if os.path.isdir(destination):
                                    shutil.rmtree(destination)
    
                                os.mkdir(destination)
                                ob.rs_shot_output = destination+"\\"
                            else:
                                if not os.path.isdir(destination):
                                    ob.rs_shot_output = destination+"\\"
    
                                    os.makedirs(destination_path)
                    self.report({'INFO'}, "Output folders created")
                    scn.rs_overwrite_folders = False
                    scn.rs_create_folders = False
                else:
                    self.report({'ERROR'}, "No main folder selected")
    
            elif self.mode == 3: # Cancelled
                scn.rs_overwrite_folders = False
                scn.rs_create_folders = False
    
            return {'FINISHED'}
    
    
    class RENDER_OT_RenderShots_settingshelp(bpy.types.Operator):
        ''' \
            Edit the resolutions and see the changes in 3D View ('ESC' to finish)\
        '''
        bl_idname = "render.rendershots_settingshelp"
        bl_label = "Render Settings Help"
    
        cam = StringProperty()
    
        def execute(self, context):
            global RENDER_SETTINGS_HELP
            RENDER_SETTINGS_HELP = True
    
            scn = context.scene
    
            render_settings_handler(scn, 'GET', using_cycles(scn), None)
            context.window_manager.modal_handler_add(self)
            return {'RUNNING_MODAL'}
    
        def modal(self, context, event):
            scn = context.scene
            ob = bpy.data.objects[self.cam]
    
            if event.type in {'ESC'}:
                global RENDER_SETTINGS_HELP
                RENDER_SETTINGS_HELP = False
                render_settings_handler(scn, 'REVERT', using_cycles(scn), None)
                return {'FINISHED'}
    
            scn.render.resolution_x = ob["rs_resolution_x"]
            scn.render.resolution_y = ob["rs_resolution_y"]
    
            return {'PASS_THROUGH'}
    
    
    class RENDER_OT_RenderShots_constraints_add(bpy.types.Operator):
        ''' Add the tracking constraints and Empty for this camera '''
        bl_idname = "render.rendershots_constraints_add"
        bl_label = "Create Constraints"
    
        cam: StringProperty()
    
        def execute(self, context):
            ob = bpy.data.objects[self.cam]
            ssName = "LookAt_for_"+ob.name
    
            bpy.ops.object.add(type="EMPTY")
            context.active_object.name = ssName
    
            target = bpy.data.objects[ssName]
    
            ob.constraints.new(type="DAMPED_TRACK").name="SS_Damped"
            damped_track = ob.constraints["SS_Damped"]
            damped_track.target = target
            damped_track.track_axis = 'TRACK_NEGATIVE_Z'
            damped_track.influence = 0.994
    
            ob.constraints.new(type="LOCKED_TRACK").name="SS_Locked"
            locked_track = ob.constraints["SS_Locked"]
            locked_track.target = target
            locked_track.track_axis = 'TRACK_Y'
            locked_track.lock_axis = 'LOCK_Z'
            locked_track.influence = 1.0
    
            return {'FINISHED'}
    
    
    class RENTER_OT_rendershots_refresh(bpy.types.Operator):
        ''' Adds newly created cameras to the list '''
        bl_idname = "render.rendershots_refresh"
    
        bl_label = "Refresh"
    
    
        def execute(self, context):
            update_shot_list(context.scene)
            return {'FINISHED'}
    
    
    class RENDER_OT_RenderShots_render(bpy.types.Operator):
        ''' Render shots '''
        bl_idname = "render.rendershots_render"
    
        bl_label = "Render"
    
    
        animation: BoolProperty(default=False)
    
        _timer = None
        _usingShape = False
    
        def execute(self, context):
            global RENDER_DONE
            RENDER_DONE = True
    
            scn = context.scene
            self.camList = []
            self.vertTrack = -1
            self.cam = ""
    
            for ob in bpy.data.objects:
                if ob.type == 'CAMERA' and not is_new_object(ob):
                    if ob["rs_shot_include"]:
                        output = ob["rs_shot_output"]
    
                        addToList = False
    
                        if output != "" and not output.isspace():
                            addToList = True
                        else:
                            message = "\"%s\" has no output destination" % ob.name
                            self.report({'WARNING'}, message)
    
                        if ob["rs_shotshape_use"]:
                            shotShape = ob["rs_shotshape_shape"]
                            if shotShape == "":
                                addToList = False
    
                                self.report({'WARNING'},
    
                                            "\"%s\" has no shot shape" % ob.name)
                            elif bpy.data.objects[shotShape].type != 'MESH':
                                errObj = bpy.data.objects[shotShape].name
                                addToList = False
                                self.report({'ERROR'},
                                            "\"%s\" is not a mesh object" % errObj)
                            #else:
                            #    bpy.data.objects[shotShape].hide_render = True
                        if addToList:
                            self.camList.append(ob.name)
    
            self.camList.reverse()
            timeline_handler(scn, 'GET')
            render_settings_handler(scn, 'GET', using_cycles(scn), None)
            context.window_manager.modal_handler_add(self)
            self._timer = context.window_manager.event_timer_add(3, context.window)
            return {'RUNNING_MODAL'}
    
        def modal(self, context, event):
            global RENDER_DONE
    
            scn = context.scene
    
            if event.type in {'ESC'}:
                context.window_manager.event_timer_remove(self._timer)
                keyframes_handler(scn, bpy.data.objects[self.cam], None, 'WIPE')
                render_settings_handler(scn, 'REVERT', using_cycles(scn), None)
                timeline_handler(scn, 'SET')
                return {'CANCELLED'}
    
            if RENDER_DONE and self.camList:
                RENDER_DONE = False
                objs = bpy.data.objects
                range = 0
    
                if self._usingShape:
                    keyframes_handler(scn, objs[self.cam], None, 'WIPE')
    
                self._usingShape = False
    
                if not self._usingShape and self.camList:
                    self.cam = self.camList.pop()
    
                ob = objs[self.cam]
    
                # Output and name cleaning
                scn.camera = ob
                output = ob["rs_shot_output"]
    
                if output[-1] == "/" or output[-1] == "\\":
                    if "." in self.cam:
                        camName = self.cam.split(".")
                        output += camName[0]+camName[1]
                    else:
                        output += self.cam
    
                # Shot shapes
                if ob["rs_shotshape_use"]:
                    self._usingShape = True
                    shape = ob["rs_shotshape_shape"]
                    range = keyframes_handler(scn, ob, objs[shape], 'SET')
    
                # Render settings
                if ob["rs_settings_use"]:
                    render_settings_handler(scn, 'SET', using_cycles(scn), ob)
                else:
                    render_settings_handler(scn, 'REVERT', using_cycles(scn), None)
    
                context.scene.render.filepath = output
    
                # Render
                ssUsing = ob["rs_shotshape_use"]
                if self.animation and not ssUsing and not self._usingShape:
                    scn.frame_start = ob["rs_shot_start"]
                    scn.frame_end = ob["rs_shot_end"]
                    bpy.ops.render.render('INVOKE_DEFAULT', animation=True)
    
                elif self.animation and ssUsing and self._usingShape:
                    if ob["rs_shotshape_use_frames"]:
                        scn.frame_start = ob.rs_shot_start
    
                        scn.frame_end = frames_from_verts(ob, ob.rs_shot_end,
    
                                                            objs[shape], 'END')
                    else:
                        scn.frame_start = 1
                        scn.frame_end = range
                    bpy.ops.render.render('INVOKE_DEFAULT', animation=True)
    
                elif not self.animation and not ssUsing and not self._usingShape:
                    bpy.ops.render.render('INVOKE_DEFAULT', write_still=True)
    
            elif RENDER_DONE and not self.camList:
                context.window_manager.event_timer_remove(self._timer)
                keyframes_handler(scn, bpy.data.objects[self.cam], None, 'WIPE')
                render_settings_handler(scn, 'REVERT', using_cycles(scn), None)
                timeline_handler(scn, 'SET')
                return {'FINISHED'}
    
            return {'PASS_THROUGH'}
    
    
    class RENDER_OT_RenderShots_previewcamera(bpy.types.Operator):
        ''' Preview this shot (makes this the active camera in 3D View) '''
        bl_idname = "render.rendershots_preview_camera"
        bl_label = "Preview Camera"
    
        camera = bpy.props.StringProperty()
    
        def execute(self, context):
            scn = context.scene
            cam = bpy.data.objects[self.camera]
            scn.objects.active = cam
            scn.camera = cam
            return {'FINISHED'}
    
    
    #####################################
    # UI
    #####################################
    class RENDER_PT_RenderShots(bpy.types.Panel):
        bl_label = "Render Shots"
        bl_space_type = "PROPERTIES"
        bl_region_type = "WINDOW"
        bl_context = "render"
    
        def draw(self, context):
            global RENDER_SETTINGS_HELP
    
            layout = self.layout
            scn = context.scene
    
            ANI_ICO, STILL_ICO = "RENDER_ANIMATION", "RENDER_STILL"
            INCL_ICO = "RESTRICT_RENDER_OFF"
            RENDER_OP = "render.rendershots_render"
    
            row = layout.row()
            row.operator(RENDER_OP, text="Image", icon=STILL_ICO)
            row.operator(RENDER_OP, text="Animation", icon=ANI_ICO).animation=True
    
            row = layout.row()
    
            if scn.rs_create_folders:
    
                row.operator("render.rendershots_create_folders",
    
                            icon="FILE_TICK").mode=2
            else:
    
                row.operator("render.rendershots_create_folders",
    
                            icon="NEWFOLDER").mode=1
    
            row.operator("render.rendershots_refresh", icon="FILE_REFRESH")
    
            if scn.rs_create_folders:
                row = layout.row()
                col = row.column(align=True)
                colrow = col.row()
                colrow.label(text="Main Folder:")
                colrow = col.row()
                colrow.prop(scn, "rs_main_folder", text="")
                colrow = col.row()
                colrow.prop(scn, "rs_overwrite_folders")
    
                colrow.operator("render.rendershots_create_folders", text="Cancel",
    
                                icon="X").mode=3
    
            if not scn.rs_is_updating:
                for ob in bpy.data.objects:
                    if ob.type == 'CAMERA' and not is_new_object(ob):
                        TOGL_ICO = "TRIA_DOWN" if ob["rs_toggle_panel"] else "TRIA_LEFT"
    
                        box = layout.box()
                        box.active = ob["rs_shot_include"]
                        col = box.column()
                        row = col.row()
                        row.label(text="\""+ob.name+"\"")
    
                        row.operator("render.rendershots_preview_camera", text="",
    
                                        icon="OUTLINER_OB_CAMERA").camera=ob.name
                        row.prop(ob, "rs_shotshape_use", icon="MESH_DATA")
                        row.prop(ob, "rs_settings_use", icon="SETTINGS")
                        row.prop(ob, "rs_shot_include", icon=INCL_ICO)
                        row.prop(ob, "rs_toggle_panel", icon=TOGL_ICO, emboss=False)
    
                        if ob["rs_toggle_panel"]:
                            col.separator()
                            row = col.row()
                            rowbox = row.box()
                            col = rowbox.column()
    
                            if ob["rs_shotshape_use"]:
                                row = col.row()
                                row.label(text="Shot Shape:")
                                row = col.row()
    
                                row.prop_search(ob, "rs_shotshape_shape",
                                                scn, "objects", text="",
    
                                                icon="OBJECT_DATA")
                                row = col.row(align=True)
                                row.prop(ob, "rs_shotshape_nav")
                                row.prop(ob, "rs_shotshape_nav_start")
                                row = col.row(align=True)
                                row.prop(ob, "rs_shotshape_offset")
                                row.prop(ob, "rs_shotshape_use_frames")
                                row = col.row()
    
                                row.operator("render.rendershots_constraints_add",
    
                                            icon="CONSTRAINT_DATA").cam=ob.name
                                col.separator()
    
                            if ob["rs_settings_use"]:
                                row = col.row()
                                row.label(text="Render Settings:")
                                row = col.row()
                                rowcol = row.column(align=True)
                                rowcol.prop(ob, "rs_resolution_x")
                                rowcol.prop(ob, "rs_resolution_y")
    
                                rowcol = row.column()
                                if not RENDER_SETTINGS_HELP:
    
                                    rowcol.operator("render.rendershots_settingshelp",
    
                                                text="", icon="HELP").cam=ob.name
                                else:
                                    rowcol.label(icon="TIME")
    
                                if using_cycles(scn):
                                    rowcol.prop(ob, "rs_cycles_samples")
                                else:
                                    rowcol.label()
    
                                col.separator()
    
                            row = col.row()
                            row.label(text="Shot Settings:")
                            row = col.row(align=True)
                            row.prop(ob, "rs_shot_start")
                            row.prop(ob, "rs_shot_end")
                            row = col.row()
                            out = ob["rs_shot_output"]
                            row.alert = False if out != "" and not out.isspace() else True
                            row.prop(ob, "rs_shot_output")
    
    
    def register():
        bpy.utils.register_module(__name__)
        init_props()
        bpy.app.handlers.render_complete.append(render_finished)
    
    def unregister():
        bpy.app.handlers.render_complete.remove(render_finished)
        bpy.utils.unregister_module(__name__)
    
        object = bpy.types.Object
        scene = bpy.types.Scene
    
        # Camera properties
        del object.rs_shot_include
        del object.rs_shot_start
        del object.rs_shot_end
        del object.rs_shot_output
        del object.rs_toggle_panel
    
        # Render settings
        del object.rs_settings_use
        del object.rs_resolution_x
        del object.rs_resolution_y
        del object.rs_cycles_samples
    
        # Shot shapes
        del object.rs_shotshape_use
        del object.rs_shotshape_shape
        del object.rs_shotshape_nav
        del object.rs_shotshape_nav_start
        del object.rs_shotshape_offset
        del object.rs_shotshape_use_frames
    
        # Internal
        del scene.rs_is_updating
        del scene.rs_create_folders
        del scene.rs_main_folder
        del scene.rs_overwrite_folders
    
    if __name__ == '__main__':
        register()