├── .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