export background

pull/5/head
“christopheseux” 2023-05-19 11:51:05 +02:00
parent 48dddb1905
commit beb85721a9
10 changed files with 416 additions and 120 deletions

View File

@ -36,8 +36,8 @@ if 'bpy' in locals():
import bpy
def register():
print('Register update script handler')
bpy.app.handlers.frame_change_post.append(update_text_strips)
bpy.app.handlers.render_pre.append(update_text_strips)
if bpy.app.background:
return
@ -51,6 +51,7 @@ def register():
def unregister():
bpy.app.handlers.frame_change_post.remove(update_text_strips)
bpy.app.handlers.render_pre.remove(update_text_strips)
try:
bpy.utils.previews.remove(ASSET_PREVIEWS)

View File

@ -2,9 +2,19 @@
"""
Generic Blender functions
"""
import os
from pathlib import Path
import subprocess
import json
from textwrap import dedent
import bpy
def abspath(path):
path = os.path.abspath(bpy.path.abspath(path))
return Path(path)
def get_scene_settings():
return bpy.context.scene.vsetb_settings
@ -17,7 +27,27 @@ def get_strip_settings():
return strip.vsetb_strip_settings
def get_bl_cmd(blender=None, background=False, focus=True, blendfile=None, script=None, **kargs):
def norm_arg(arg_name):
return "--" + arg_name.replace(' ', '-')
def norm_value(value):
if isinstance(value, (tuple, list)):
values = []
for v in value:
if not isinstance(v, str):
v = json.dumps(v)
values.append(v)
return values
if isinstance(value, Path):
return str(value)
if not isinstance(value, str):
value = json.dumps(value)
return value
def get_bl_cmd(blender=None, background=False, focus=True, blendfile=None, script=None, output=None, **kargs):
cmd = [str(blender)] if blender else [bpy.app.binary_path]
if background:
@ -31,6 +61,9 @@ def get_bl_cmd(blender=None, background=False, focus=True, blendfile=None, scrip
if blendfile:
cmd += [str(blendfile)]
if output:
cmd += ['-o', str(output)]
if script:
cmd += ['--python', str(script)]
@ -49,6 +82,28 @@ def get_bl_cmd(blender=None, background=False, focus=True, blendfile=None, scrip
return cmd
def background_render(output=None):
#bpy.context.scene.render.filepath = '{output}'
script_code = dedent(f"""
import bpy
bpy.context.scene.render.filepath = '{str(output)}'
bpy.ops.render.render(animation=True)
bpy.ops.wm.quit_blender()
""")
tmp_blend = Path(bpy.app.tempdir) / Path(bpy.data.filepath).name
bpy.ops.wm.save_as_mainfile(filepath=str(tmp_blend))
script_path = Path(bpy.app.tempdir) / 'render_blender_background.py'
script_path.write_text(script_code)
cmd = get_bl_cmd(blendfile=tmp_blend, script=script_path, background=True)
print('Background Render...')
print(cmd)
subprocess.Popen(cmd)
def get_addon_prefs():
addon_name = __package__.split('.')[0]
return bpy.context.preferences.addons[addon_name].preferences

View File

@ -1,13 +1,13 @@
# SPDX-License-Identifier: GPL-2.0-or-later
from vse_toolbox.operators import (addon, casting, imports, render, sequencer,
from vse_toolbox.operators import (addon, casting, imports, exports, sequencer,
spreadsheet, tracker)
modules = (
addon,
casting,
imports,
render,
exports,
sequencer,
spreadsheet,
tracker

View File

@ -2,7 +2,7 @@
import json
import bpy
from bpy.types import PropertyGroup, Operator
from bpy.types import Context, Event, PropertyGroup, Operator
from bpy.props import (CollectionProperty, EnumProperty, StringProperty)
from vse_toolbox.constants import CASTING_BUFFER
@ -249,15 +249,27 @@ class VSETB_OT_copy_casting(Operator):
class VSETB_OT_paste_casting(Operator):
bl_idname = "vse_toolbox.paste_casting"
bl_label = "Paste Casting"
bl_description = "Paste Casting to active strip"
bl_description = "Paste Casting to active strip (ctrl|shift: Add, alt:Remove)"
bl_options = {"REGISTER", "UNDO"}
mode : EnumProperty(items=[(m, m.title(), '') for m in ('REPLACE', 'ADD', 'REMOVE')])
@classmethod
def poll(cls, context):
active_strip = context.scene.sequence_editor.active_strip
if active_strip:
return True
def invoke(self, context, event):
self.mode = 'REPLACE'
if event.ctrl or event.shift:
self.mode = 'ADD'
elif event.alt:
self.mode = 'REMOVE'
return self.execute(context)
def execute(self, context):
scn = context.scene
strip_settings = get_strip_settings()
@ -267,19 +279,33 @@ class VSETB_OT_paste_casting(Operator):
return {"CANCELLED"}
casting_datas = json.loads(CASTING_BUFFER.read_text())
casting_ids = set(c['id'] for c in casting_datas)
for strip in context.selected_sequences:
strip_settings = strip.vsetb_strip_settings
strip.vsetb_strip_settings.casting.clear()
if self.mode == 'REPLACE':
strip.vsetb_strip_settings.casting.clear()
for casting_data in casting_datas:
item = strip.vsetb_strip_settings.casting.add()
if self.mode == 'REMOVE':
for asset_casting in strip_settings.casting:
index = list(strip_settings.casting).index(asset_casting)
if asset_casting.id in casting_ids:
strip_settings.casting.remove(index)
item.name = casting_data['name']
item.id = casting_data['id']
item['_name'] = casting_data['_name']
if strip_settings.casting_index != 0:
strip_settings.casting_index -= 1
strip_settings.casting.update()
else:
item = strip.vsetb_strip_settings.casting.add()
item.name = casting_data['name']
item.id = casting_data['id']
item['_name'] = casting_data['_name']
strip_settings.casting.update()
return {"FINISHED"}

183
operators/exports.py Normal file
View File

@ -0,0 +1,183 @@
import time
import re
from pathlib import Path
import os
import bpy
from bpy.types import Operator
from bpy.props import BoolProperty
from vse_toolbox.sequencer_utils import (get_strips, render_strip)
from vse_toolbox.bl_utils import (get_scene_settings, background_render)
from vse_toolbox.file_utils import install_module
class VSETB_OT_render(Operator):
bl_idname = "vse_toolbox.strips_render"
bl_label = "Render Shots Strips"
bl_description = "Render Shots Strips"
bl_options = {"REGISTER", "UNDO"}
#selected_only : BoolProperty(name="Selected Only", default=False)
@classmethod
def poll(cls, context):
settings = get_scene_settings()
return settings.active_project
def invoke(self, context, event):
scn = context.scene
settings = get_scene_settings()
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
scn = context.scene
settings = get_scene_settings()
layout = self.layout
col = layout.column()
#col.use_property_split = True
#col.use_property_decorate = False
col.prop(settings.active_project, "render_single_file")
row = col.row()
row.enabled = settings.active_project.render_single_file
row.prop(settings.active_project, "render_template")
col.separator()
col.prop(settings.active_project, "render_per_strip")
row = col.row()
row.enabled = settings.active_project.render_per_strip
row.prop(settings.active_project, "render_strip_template")
#col.prop(settings, 'channel', text='Channel')
#col.prop(self, 'selected_only')
def execute(self, context):
scn = context.scene
settings = get_scene_settings()
project = settings.active_project
format_data = {**settings.format_data, **project.format_data}
start_time = time.perf_counter()
if project.render_single_file:
render_path = project.render_template.format(**format_data)
background_render(output=render_path)
if project.render_per_strip:
for strip in get_strips(channel='Shots', selected_only=True):
strip_settings = strip.vsetb_strip_settings
strip_data = {**format_data, **strip_settings.format_data}
strip_render_path = project.render_strip_template.format(**strip_data)
render_strip(strip, strip_render_path)
self.report({"INFO"}, f'Strips rendered in {time.perf_counter()-start_time} seconds')
return {"FINISHED"}
class VSETB_OT_export_edl(Operator):
bl_idname = "vse_toolbox.export_edl"
bl_label = "Export Edl"
bl_description = "Export Edl"
bl_options = {"REGISTER", "UNDO"}
#selected_only : BoolProperty(name="Selected Only", default=False)
@classmethod
def poll(cls, context):
settings = get_scene_settings()
return settings.active_project
def invoke(self, context, event):
scn = context.scene
settings = get_scene_settings()
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
scn = context.scene
settings = get_scene_settings()
layout = self.layout
col = layout.column()
col.use_property_split = True
col.use_property_decorate = False
#col.prop(settings, 'channel', text='Channel')
#col.prop(self, 'selected_only')
col.prop(settings.active_project, "export_edl_template")
def execute(self, context):
opentimelineio = install_module('opentimelineio')
from opentimelineio.schema import (Clip, Timeline, Track, ExternalReference)
from opentimelineio.opentime import (RationalTime, TimeRange)
start_time = time.perf_counter()
scn = context.scene
settings = get_scene_settings()
fps = scn.render.fps
project = settings.active_project
format_data = {**settings.format_data, **project.format_data}
output_edl = project.export_edl_template.format(**format_data)
output_edl = os.path.abspath(bpy.path.abspath(output_edl))
output_edl = Path(output_edl)
timeline = Timeline(output_edl.stem)
track = Track(f"Track 1")
track.kind = "Video"
timeline.tracks.append(track)
for strip in get_strips(channel='Shots', selected_only=False):
strip_data = {**format_data, **strip.vsetb_strip_settings.format_data}
strip_render_path = project.render_strip_template.format(**strip_data)
clip = Clip(Path(strip_render_path).name)
clip.metadata.setdefault("cmx_3600", {})
clip.metadata['cmx_3600']['reel'] = strip.name
#clip.metadata['cmx_3600']['flags'] = 'IS_CLIP'
#clip.media_reference = ExternalReference(f'//render/{strip.name}.mov')
#clip.media_reference.name = strip.name
#clip.available_range = TimeRange(RationalTime(0, fps), RationalTime(strip.frame_duration, fps))
clip.source_range = TimeRange(RationalTime(strip.frame_offset_start, fps), RationalTime(strip.frame_final_duration, fps))
track.append(clip)
output_edl.parent.mkdir(exist_ok=True, parents=True)
edl = opentimelineio.adapters.write_to_string(timeline, 'cmx_3600')
output_edl.write_text(edl)
#opentimelineio.adapters.write_to_file(timeline, str(output_edl), 'cmx_3600')
self.report({"INFO"}, f'Edl Exported in {time.perf_counter()-start_time} seconds')
return {"FINISHED"}
classes = (
VSETB_OT_render,
VSETB_OT_export_edl
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

View File

@ -1,70 +0,0 @@
import time
import bpy
from bpy.types import Operator
from vse_toolbox.sequencer_utils import (get_strips, render_strips)
from vse_toolbox.bl_utils import get_scene_settings
class VSETB_OT_render(Operator):
bl_idname = "vse_toolbox.strips_render"
bl_label = "Render Shots Strips"
bl_description = "Render Shots Strips"
bl_options = {"REGISTER", "UNDO"}
#selected_only : BoolProperty(name="Selected Only", default=False)
@classmethod
def poll(cls, context):
settings = get_scene_settings()
return settings.active_project
def invoke(self, context, event):
scn = context.scene
settings = get_scene_settings()
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
scn = context.scene
settings = get_scene_settings()
layout = self.layout
col = layout.column()
col.use_property_split = True
col.use_property_decorate = False
#col.prop(settings, 'channel', text='Channel')
#col.prop(self, 'selected_only')
col.prop(settings.active_project, "render_template")
def execute(self, context):
scn = context.scene
settings = get_scene_settings()
strips = get_strips(channel='Shots', selected_only=True)
start_time = time.perf_counter()
render_strips(strips, settings.active_project.render_template)
self.report({"INFO"}, f'Strips rendered in {time.perf_counter()-start_time} seconds')
return {"FINISHED"}
classes = (
VSETB_OT_render,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

View File

@ -171,6 +171,40 @@ class VSETB_OT_spreadsheet_from_clipboard(Operator):
return {"FINISHED"}
class VSETB_OT_spreadsheet_to_clipboard(Operator):
bl_idname = "vse_toolbox.spreadsheet_to_clipboard"
bl_label = "Copy Spreadsheet to clipboard"
bl_description = "Copy Spreadsheet to clipboard"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
scn = context.scene
project = get_scene_settings().active_project
import_cells = project.spreadsheet_import.cells
import_cells.clear()
SPREADSHEET.clear()
spreadsheet = context.window_manager.clipboard
cell_types = project.get_cell_types()
rows = list(csv.reader(StringIO(spreadsheet), delimiter='\t'))
for cell_name in rows[0]:
if not cell_name:
continue
cell = import_cells.add()
cell.name = cell_name
cell.import_name = max(cell_types.keys(), key=lambda x: fuzzy_match(cell_name, x))
cell.enabled = True
project.spreadsheet_import.use_custom_cells = True
SPREADSHEET.extend(rows)
return {"FINISHED"}
class VSETB_OT_import_spreadsheet(Operator):
bl_idname = "vse_toolbox.import_spreadsheet"
bl_label = "Import Spreadsheet"
@ -368,13 +402,7 @@ class VSETB_OT_import_spreadsheet(Operator):
strip_settings.casting.update()
else:
self.report({'WARNING'}, f'Asset {asset_name} not found in Project')
self.report({'WARNING'}, f'Asset {asset_name} not found in Project')
return {"FINISHED"}
@ -446,12 +474,13 @@ class VSETB_OT_export_spreadsheet(Operator):
row.prop(spreadsheet, 'show_settings', text='', icon='PREFERENCES')
if spreadsheet.show_settings:
col.prop(spreadsheet, "separator", expand=True, text='Separator')
if spreadsheet.format == 'CSV':
if spreadsheet.format == 'csv':
col.prop(spreadsheet, "delimiter", expand=True, text='Delimiter')
col.separator()
col.prop(spreadsheet, 'open_folder', text='Open Folder')
col.prop(spreadsheet, 'export_path', text='Export Path')
if spreadsheet.format != 'Clipboard':
col.separator()
col.prop(spreadsheet, 'open_folder', text='Open Folder')
col.prop(spreadsheet, 'export_path', text='Export Path')
def execute(self, context):
#self.report({'ERROR'}, f'Export not implemented yet.')
@ -499,7 +528,7 @@ class VSETB_OT_export_spreadsheet(Operator):
row += [strip.name]
elif cell.field_name == 'DESCRIPTION':
row += [strip.vsetb_strip_settings.description]
elif cell.field_name == 'FRAMES':
elif cell.field_name == 'NB_FRAMES':
row += [strip.frame_final_duration]
rows.append(row)
@ -531,14 +560,14 @@ class VSETB_OT_export_spreadsheet(Operator):
#2023_04_11_kitsu_boris_ep01_shots
export_path.parent.mkdir(parents=True, exist_ok=True)
if spreadsheet.format == 'CSV':
if spreadsheet.format == 'csv':
print('Writing .csv file to', export_path)
with open(str(export_path), 'w', newline='\n', encoding='utf-8') as f:
writer = csv.writer(f, delimiter=spreadsheet.delimiter)
for row in rows:
writer.writerow(row)
elif spreadsheet.format == 'XLSX':
elif spreadsheet.format == 'xlsx':
try:
import openpyxl
except ModuleNotFoundError:
@ -558,6 +587,15 @@ class VSETB_OT_export_spreadsheet(Operator):
# Save the file
workbook.save(str(export_path))
elif spreadsheet.format == 'Clipboard':
csv_buffer = StringIO()
# Write CSV data to the StringIO buffer
csv_writer = csv.writer(csv_buffer, delimiter='\t')
csv_writer.writerows(rows)
context.window_manager.clipboard = csv_buffer.getvalue()
if spreadsheet.open_folder:
open_file(export_path, select=True)
@ -570,6 +608,7 @@ classes = (
VSETB_MT_export_spreadsheet_presets,
VSETB_OT_spreadsheet_from_file,
VSETB_OT_spreadsheet_from_clipboard,
VSETB_OT_spreadsheet_to_clipboard,
VSETB_OT_spreadsheet_cell_move,
VSETB_OT_export_spreadsheet,
VSETB_OT_import_spreadsheet

View File

@ -137,7 +137,7 @@ def get_strip_render_path(strip, template):
strip_data = parse(strip.name, template=project.shot_template)
print(strip_data)
#print(strip_data)
index = int(strip_data['index'])
@ -234,36 +234,31 @@ def render_strips(strips, template):
scn.render.filepath = render_path
'''
def render_strips(strips, template):
def render_strip(strip, output):
output = os.path.abspath(bpy.path.abspath(output))
scn = bpy.context.scene
scene_start = scn.frame_start
scene_end = scn.frame_end
scene_current = scn.frame_current
render_path = scn.render.filepath
for strip in strips:
scn.frame_start = strip.frame_final_start
scn.frame_end = strip.frame_final_end - 1
scn.render.filepath = str(get_strip_render_path(strip, template))
scn.frame_start = strip.frame_final_start
scn.frame_end = strip.frame_final_end - 1
scn.render.filepath = output
print(f'Render Strip to {scn.render.filepath}')
bpy.ops.render.opengl(animation=True, sequencer=True)
print(f'Render Strip to {scn.render.filepath}')
bpy.ops.render.opengl(animation=True, sequencer=True)
scn.frame_start = scene_start
scn.frame_end = scene_end
scn.frame_current = scene_current
scn.render.filepath = render_path
def import_edit(filepath, adapter="cmx_3600", channel='Shots'):
opentimelineio = install_module('opentimelineio')
from opentimelineio.schema import (
Clip,
ExternalReference,
Gap,
ImageSequenceReference,
Stack,
Timeline,
Track,
)
from opentimelineio.schema import Clip
scn = bpy.context.scene
sequencer = scn.sequence_editor.sequences

View File

@ -208,6 +208,7 @@ class VSETB_PT_exports(VSETB_main, Panel):
tracker_label = settings.tracker_name.title().replace('_', ' ')
layout.operator('vse_toolbox.upload_to_tracker', text=f'Upload to {tracker_label}', icon='EXPORT')
layout.operator('vse_toolbox.export_spreadsheet', text='Export Spreadsheet', icon='SPREADSHEET')
layout.operator('vse_toolbox.export_edl', text='Export edl', icon='SEQ_SEQUENCER')
class VSETB_PT_casting(VSETB_main, Panel):

View File

@ -3,6 +3,7 @@
import bpy
import os
from pathlib import Path
import re
from bpy.props import (
BoolProperty,
@ -16,7 +17,8 @@ from bpy.types import PropertyGroup, UIList
from pprint import pprint as pp
from vse_toolbox.bl_utils import get_addon_prefs, get_scene_settings
from vse_toolbox.constants import ASSET_PREVIEWS, TRACKERS, PREVIEWS_DIR
from vse_toolbox.file_utils import norm_str
from vse_toolbox.file_utils import norm_str, parse
from vse_toolbox.sequencer_utils import get_strip_sequence_name
def get_episodes_items(self, context):
@ -187,7 +189,7 @@ def get_custom_name_items(self, context):
class SpreadsheetExport(PropertyGroup):
use_custom_cells: BoolProperty(default=False)
format : EnumProperty(items=[(i, i, '') for i in ('CSV', 'XLSX')])
format : EnumProperty(items=[(i, i, '') for i in ('csv', 'xlsx', 'Clipboard')])
separator : StringProperty(default='\\n')
delimiter : StringProperty(default=';')
export_path : StringProperty(default='//export')
@ -245,7 +247,16 @@ class Project(PropertyGroup):
name="Shot Name", default="{sequence}_sh{index:04d}")
render_template : StringProperty(
name="Render Name", default="//render/{strip_name}.{ext}")
name="Movie Path", default="//render/{project_basename}.{ext}")
render_strip_template : StringProperty(
name="Strip Path", default="//render/shots/{strip}.{ext}")
render_per_strip: BoolProperty(name="Per Strip", default=True)
render_single_file: BoolProperty(name="Single File", default=False)
export_edl_template : StringProperty(
name="Edl Path", default="//render/{project_basename}.edl")
episode_name : EnumProperty(items=get_episodes_items, update=on_episode_updated)
episodes : CollectionProperty(type=Episode)
@ -260,6 +271,23 @@ class Project(PropertyGroup):
type : StringProperty()
@property
def active_episode(self):
return self.episodes.get(self.episode_name)
@property
def format_data(self):
data = {}
data['project'] = norm_str(self.name)
data['project_basename'] = data['project']
if self.active_episode:
data['episode'] = norm_str(self.episode_name)
data['project_basename'] = f"{data['project']}_{data['episode']}"
return data
def get_cell_types(self):
settings = get_scene_settings()
project = settings.active_project
@ -293,7 +321,7 @@ class Project(PropertyGroup):
cell = spreadsheet.cells.add()
cell.name = cell_name
cell.export_name = 'Name' if cell_name == 'Shot' else cell_name
cell.field_name = cell_name.upper()
cell.field_name = norm_str(cell_name, format=str.upper)
cell.type = "SHOT"
for metadata_type in self.metadata_types:
@ -309,7 +337,7 @@ class Project(PropertyGroup):
cell = spreadsheet.cells.add()
cell.name = asset_type.name
cell.export_name = asset_type.name
cell.field_name = asset_type.name.upper()
cell.field_name = norm_str(asset_type.name, format=str.upper)
cell.type = "ASSET_TYPE"
def set_strip_metadata(self):
@ -337,6 +365,8 @@ class Project(PropertyGroup):
setattr(Metadata, field_name, prop)
class VSETB_UL_casting(UIList):
order_by_type : BoolProperty(default=False)
@ -489,8 +519,24 @@ class VSETB_PGT_scene_settings(PropertyGroup):
def active_episode(self):
project = self.active_project
if project:
return project.episodes.get(project.episode_name)
return project.active_episode
@property
def format_data(self):
data = {}
digit_matches = re.findall(r'(\d+)', bpy.data.filepath)
if len(digit_matches) == 1:
data['version'] = int(digit_matches[-1])
elif len(digit_matches) > 1:
data['increment'] = int(digit_matches[-1])
data['version'] = int(digit_matches[-2])
suffix = Path(bpy.context.scene.render.frame_path()).suffix
data['ext'] = suffix[1:]
return data
class VSETB_PGT_strip_settings(PropertyGroup):
casting : CollectionProperty(type=AssetCasting)
@ -506,6 +552,26 @@ class VSETB_PGT_strip_settings(PropertyGroup):
except IndexError:
return
@property
def strip(self):
sequences = bpy.context.scene.sequence_editor.sequences_all
return next(s for s in sequences if s.vsetb_strip_settings == self)
@property
def format_data(self):
scn = bpy.context.scene
settings = get_scene_settings()
project = settings.active_project
strip = self.strip
data = parse(strip.name, template=project.shot_template)
data['index'] = int(data['index'])
data['sequence'] = get_strip_sequence_name(strip)
data['strip'] = strip.name
#data['shot'] = project.shot_template
return data
classes = (
Asset,
AssetCasting,