├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── images ├── installation.png ├── output_settings.png ├── properties.png ├── render_settings.png └── strips.png ├── make-dist.sh ├── render_strip.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | */__pycache__ 2 | *.pyc 3 | *.blend 4 | *.blend1 5 | *.txt 6 | *.sublime-project 7 | *.sublime-workspace 8 | *.vscode 9 | *.idea 10 | *.DS_STORE 11 | /render 12 | /render-strip 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Lucky Kadam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Render Strip 2 | **Blender addon to manage animation strips.** 3 | 4 | Author: [Lucky Kadam](https://twitter.com/luckykadam94) 5 | 6 | ## Installation 7 | 8 | 1. Download from [Gumroad](https://gumroad.com/l/renderstrip) for free or Github latest [release](https://github.com/luckykadam/render-strip/releases/download/v1.0/render-strip-v1.0.zip) (do not unzip). 9 | 2. In blender, go to: Edit -> Preferences -> Add-ons -> Install. 10 | 3. Select the downloaded file and click on -> Install Add-on. 11 | 4. Enable it by clicking on checkbox. 12 | 13 | 14 | 15 | You should now see Render Strip panel in Render Properties. 16 | 17 | 18 | 19 | ## Usage 20 | 21 | 1. Create new strip by clicking on "+" button. 22 | 2. Specify the camera, start frame and end frame. 23 | 3. Select the output path. 24 | 4. Hit Render. 25 | 26 | 27 | 28 | 5. Optionally set custom render settings for the strip. 29 | 30 | 31 | 32 | 6. For more control on ouptut, take a look at output settings sub-panel. 33 | 34 | 35 | 36 | ## Resources 37 | 38 | * Demonstration video on [Youtube](https://youtu.be/4OC895dGW0g) 39 | * Support thread on [BlenderArtists](https://blenderartists.org/t/render-strip/1245609) 40 | 41 | This project is inspired by [Render Burst](https://github.com/VertStretch/RenderBurst). 42 | 43 | ## Feedback 44 | 45 | Feel free to report issues or provide feedback on Github. 46 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.utils import register_class, unregister_class 3 | 4 | from .render_strip import RenderStripOperator, RsStrip, RsSettings, RENDER_UL_render_strip_list, RENDER_PT_render_strip, RENDER_PT_render_strip_detail, RENDER_PT_render_strip_settings, OBJECT_OT_NewStrip, OBJECT_OT_DeleteStrip, OBJECT_OT_PlayStrip, OBJECT_OT_CopyRenderSettings, OBJECT_OT_ApplyRenderSettings, OBJECT_MT_RenderSettingsMenu, OBJECT_OT_RenderStrip 5 | 6 | bl_info = { 7 | "name": "Render Strip", 8 | "category": "Render", 9 | "blender": (2, 80, 0), 10 | "author" : "Lucky Kadam ", 11 | "version" : (1, 0, 2), 12 | "description" : "Render camera strips", 13 | } 14 | 15 | classes = [RenderStripOperator, RsStrip, RsSettings, RENDER_UL_render_strip_list, RENDER_PT_render_strip, RENDER_PT_render_strip_detail, RENDER_PT_render_strip_settings, OBJECT_OT_NewStrip, OBJECT_OT_DeleteStrip, OBJECT_OT_PlayStrip, OBJECT_OT_CopyRenderSettings, OBJECT_OT_ApplyRenderSettings, OBJECT_MT_RenderSettingsMenu, OBJECT_OT_RenderStrip] 16 | 17 | def menu_func(self, context): 18 | self.layout.operator(OBJECT_OT_RenderStrip.bl_idname, icon="RENDER_ANIMATION") 19 | 20 | def register(): 21 | for cls in classes: 22 | register_class(cls) 23 | 24 | bpy.types.Scene.rs_settings = bpy.props.PointerProperty(type=RsSettings) 25 | bpy.types.TOPBAR_MT_render.append(menu_func) 26 | 27 | def unregister(): 28 | for cls in classes: 29 | unregister_class(cls) 30 | 31 | bpy.types.TOPBAR_MT_render.remove(menu_func) 32 | 33 | if __name__ == "__main__": 34 | register() 35 | -------------------------------------------------------------------------------- /images/installation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckykadam/render-strip/a8eca174cb807fb215088fe80ceaeb86181dadd3/images/installation.png -------------------------------------------------------------------------------- /images/output_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckykadam/render-strip/a8eca174cb807fb215088fe80ceaeb86181dadd3/images/output_settings.png -------------------------------------------------------------------------------- /images/properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckykadam/render-strip/a8eca174cb807fb215088fe80ceaeb86181dadd3/images/properties.png -------------------------------------------------------------------------------- /images/render_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckykadam/render-strip/a8eca174cb807fb215088fe80ceaeb86181dadd3/images/render_settings.png -------------------------------------------------------------------------------- /images/strips.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckykadam/render-strip/a8eca174cb807fb215088fe80ceaeb86181dadd3/images/strips.png -------------------------------------------------------------------------------- /make-dist.sh: -------------------------------------------------------------------------------- 1 | if [ -z "$1" ] 2 | then 3 | echo "No argument supplied. Usage: ./make-dist v1.0" 4 | exit 1 5 | fi 6 | 7 | echo "preparing release $1" 8 | 9 | addon_zip="Render Strip $1 - Do not unzip!.zip" 10 | mkdir render-strip 11 | cp *.py render-strip 12 | zip -r "$addon_zip" render-strip -x '*/.git/*' -x '*/.DS_Store/*' -x '*/__pycache__/*' -------------------------------------------------------------------------------- /render_strip.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import os 3 | from collections import OrderedDict 4 | import re 5 | 6 | from .utils import apply_render_settings, copy_render_settings, get_available_render_engines, get_available_render_engines_values, ShowMessageBox 7 | 8 | 9 | class RenderStripOperator(bpy.types.Operator): 10 | """Render all strips""" 11 | bl_idname = "render.renderstrip" 12 | bl_label = "Render Strip" 13 | 14 | _timer = None 15 | strips = None 16 | stop = None 17 | rendering = None 18 | 19 | # help revert to original 20 | camera = None 21 | frame_start = None 22 | frame_end = None 23 | path = None 24 | render_engine = None 25 | resolution_x = None 26 | resolution_y = None 27 | resolution_percentage = None 28 | pixel_aspect_x = None 29 | pixel_aspect_y = None 30 | 31 | def _init(self, dummy, thrd = None): 32 | self.rendering = True 33 | 34 | def _complete(self, dummy, thrd = None): 35 | self.strips.popitem(last=False) 36 | self.rendering = False 37 | 38 | def _cancel(self, dummy, thrd = None): 39 | self.stop = True 40 | 41 | def execute(self, context): 42 | try: 43 | self.stop = False 44 | self.rendering = False 45 | scene = bpy.context.scene 46 | active_strips = [strip for strip in scene.rs_settings.strips if strip.enabled] 47 | if any(strip.cam not in scene.objects or scene.objects[strip.cam].type != "CAMERA" for strip in active_strips): 48 | raise Exception("Invalid Camera in strips!") 49 | if not all(strip.name for strip in active_strips): 50 | raise Exception("Invalid Name in strips!") 51 | self.strips = OrderedDict({ 52 | strip.name: strip 53 | for strip in active_strips 54 | }) 55 | 56 | if len(self.strips) == 0: 57 | raise Exception("No active strip") 58 | if len(self.strips) self.get_end(): 129 | self.set_end(self["start"]) 130 | 131 | def get_end(self): 132 | return self.get("end", 1) 133 | 134 | def set_end(self, value): 135 | self["end"] = value 136 | if self["end"] < self.get_start(): 137 | self.set_start(self["end"]) 138 | 139 | def get_name(self): 140 | return self.get("name", "Strip") 141 | 142 | def set_name(self, value): 143 | strips = { strip.name: strip for strip in bpy.context.scene.rs_settings.strips if strip.as_pointer() != self.as_pointer()} 144 | if value not in strips: 145 | self["name"] = value 146 | else: 147 | old_strip = strips[value] 148 | # next free name 149 | def split(x): 150 | match = re.search(r'\.(\d+)$', x) 151 | if match is None: 152 | return x, None 153 | else: 154 | return x[:match.start()],x[match.start()+1:] 155 | base_name,number = split(value) 156 | if number is None: 157 | number = "001" 158 | value = base_name 159 | count = 1 160 | while value in strips: 161 | format_str = "{}.{" + ":0>{}".format(len(number)) + "}" 162 | value = format_str.format(base_name, count) 163 | count += 1 164 | self["name"] = value 165 | 166 | def list_cameras(self, context): 167 | cameras = [] 168 | for object in context.scene.objects: 169 | if object.type == "CAMERA": 170 | cameras.append(object) 171 | return [(cam.name, cam.name, cam.name) for cam in cameras] 172 | 173 | def list_render_engines(self, context): 174 | return get_available_render_engines() 175 | 176 | enabled: bpy.props.BoolProperty(name="Enable", default=True) 177 | name: bpy.props.StringProperty(name="Name", get=get_name, set=set_name) 178 | cam: bpy.props.EnumProperty(name="Camera", items=list_cameras) 179 | start: bpy.props.IntProperty(name="Start Frame", get=get_start, set=set_start, min=1) 180 | end: bpy.props.IntProperty(name="End Frame", get=get_end, set=set_end, min=1) 181 | 182 | # render settings 183 | custom_render: bpy.props.BoolProperty(name="Custom Render settings", default=False) 184 | render_engine: bpy.props.EnumProperty(name="Render Engine", items=list_render_engines) 185 | resolution_x: bpy.props.IntProperty(name="Resolution X", default=1920, min=4, subtype="PIXEL") 186 | resolution_y: bpy.props.IntProperty(name="Resolution Y", default=1080, min=4, subtype="PIXEL") 187 | resolution_percentage: bpy.props.IntProperty(name="Resolution %", default=100, min=1, max=100, subtype="PERCENTAGE") 188 | pixel_aspect_x: bpy.props.FloatProperty(name="Aspect X", default=1, min=1, max=200) 189 | pixel_aspect_y: bpy.props.FloatProperty(name="Aspect Y", default=1, min=1, max=200) 190 | 191 | 192 | def draw(self, context, layout): 193 | row = layout.row() 194 | 195 | cam_field = row.row(align=True) 196 | cam_field.prop(self, 'cam', text="") 197 | cam_field.scale_x = 2 198 | frame_field = row.row(align=True) 199 | frame_field.prop(self, 'start', text="") 200 | frame_field.prop(self, 'end', text="") 201 | layout.separator() 202 | 203 | row = layout.row() 204 | row.prop(self, 'custom_render') 205 | 206 | if self.custom_render: 207 | row.menu('OBJECT_MT_RenderSettingsMenu', text="options", icon='PREFERENCES') 208 | layout.separator() 209 | 210 | col = layout.column() 211 | col.use_property_split = True 212 | col.use_property_decorate = False 213 | 214 | col.prop(self, 'render_engine') 215 | 216 | subcol = col.column(align=True) 217 | subcol.prop(self, "resolution_x", text="Resolution X") 218 | subcol.prop(self, "resolution_y", text="Y") 219 | subcol.prop(self, "resolution_percentage", text="%") 220 | 221 | subcol = col.column(align=True) 222 | subcol.prop(self, "pixel_aspect_x", text="Aspect X") 223 | subcol.prop(self, "pixel_aspect_y", text="Y") 224 | 225 | 226 | def draw_list_item(self, context, layout): 227 | row = layout.row(align=True) 228 | row.prop(self, 'enabled', text="") 229 | row.prop(self, 'name', text="", emboss=False) 230 | row.label(text=self.cam) 231 | row.label(text="{}-{}".format(self.start,self.end)) 232 | 233 | 234 | class RsSettings(bpy.types.PropertyGroup): 235 | separate_dir: bpy.props.BoolProperty(name="Separate Directories", description="Create separate directories for each strip", default=True) 236 | strips: bpy.props.CollectionProperty(type=RsStrip) 237 | active_index: bpy.props.IntProperty(default=0) 238 | 239 | 240 | class RENDER_UL_render_strip_list(bpy.types.UIList): 241 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname): 242 | if self.layout_type in {"DEFAULT", "COMPACT"}: 243 | item.draw_list_item(context, layout) 244 | elif self.layout_type == "GRID": 245 | layout.alignment = "CENTER" 246 | layout.label(text="", icon_value="RENDER_ANIMATION") 247 | 248 | 249 | class RENDER_PT_render_strip(bpy.types.Panel): 250 | bl_label = "Render Strip" 251 | bl_space_type = 'PROPERTIES' 252 | bl_region_type = 'WINDOW' 253 | bl_context = "render" 254 | 255 | def draw(self, context): 256 | layout = self.layout 257 | 258 | row = layout.row() 259 | row.template_list("RENDER_UL_render_strip_list", "", context.scene.rs_settings, "strips", context.scene.rs_settings, "active_index", rows=4 if len(context.scene.rs_settings.strips)>0 else 2) 260 | col = row.column(align=True) 261 | col.operator('rs.newstrip', text="", icon='ADD') 262 | col.operator('rs.delstrip', text="", icon='REMOVE') 263 | 264 | col.separator() 265 | col.operator('rs.playstrip', text="", icon='PLAY') 266 | 267 | # col.separator() 268 | # col.menu('OBJECT_MT_RenderSettingsMenu', text="", icon='DOWNARROW_HLT') 269 | 270 | row = layout.row() 271 | row.operator('rs.renderstrip', text="Render") 272 | 273 | 274 | class RENDER_PT_render_strip_detail(bpy.types.Panel): 275 | bl_label = "Strip" 276 | bl_parent_id = "RENDER_PT_render_strip" 277 | bl_space_type = 'PROPERTIES' 278 | bl_region_type = 'WINDOW' 279 | bl_context = "render" 280 | 281 | def draw(self, context): 282 | layout = self.layout 283 | index = context.scene.rs_settings.active_index 284 | strips = context.scene.rs_settings.strips 285 | if 0<=index and index