Flamenco: determine render output path using add-on prefs and filename.

This commit is contained in:
Sybren A. Stüvel 2017-01-18 14:31:25 +01:00
parent d5f285a381
commit 64e29e695b
3 changed files with 203 additions and 22 deletions

57
README-flamenco.md Normal file
View File

@ -0,0 +1,57 @@
# Flamenco
The Blender Cloud add-on has preliminary support for [Flamenco](https://flamenco.io/).
It requires a project on the [Blender Cloud](https://cloud.blender.org/) that is set up for
Flamenco, and it requires you to be logged in as a user with rights to use Flamenco.
## Quirks
Flamenco support is unpolished, so it has some quirks.
- Project selection happens through the Attract project selector. As a result, you can only
select Attract-enabled projects (even when they are not set up for Flamenco). Be careful
which project you select.
- The top level directory of the project is also set through the Attract properties.
- Defaults are heavily biased for our use in the Blender Institute.
- Settings that should be project-specific are not, i.e. are regular add-on preferences.
- Sending a project to Flamenco will check the "File Extensions" setting in the Output panel,
and save the blend file to the current filename.
## Render job file locations
Rendering via Flamenco roughly comprises of two steps:
1. Packing the file to render with its dependencies, and placing them in the "job file path".
2. Rendering, and placing the output files in the "job output path".
### Job file path
The "job file path" consists of the following path components:
1. The add-on preference "job file path", e.g. `/render/_flamenco/storage`
2. The current date and time, your Blender Cloud username, and the name of the current blend file.
3. The name of the current blend file.
For example:
`/render/_flamenco/storage/2017-01-18-104841.931387-sybren-03_02_A.layout/03_02_A.layout.blend`
### Job output path
The file path of output files consists of the following path components:
1. The add-on preference "job file path", e.g. `/render/agent327/frames`
2. The path of the current blend file, relative to the project directory. The first N components
of this path can be stripped; when N=1 it turns `scenes/03-searching/03_02_A-snooping/` into
`03-searching/03_02_A-snooping/`.
3. The name of the current blend file, without `.blend`.
4. The file name depends on the type of output:
- When rendering to image files: A 5-digit frame number with the required file extension.
- When rendering to a video file: The frame range with the required file extension.
For example:
`/render/agent327/frames/03-searching/03_02_A-snooping/03_02_A.layout/00441.exr`
`/render/agent327/frames/03-searching/03_02_A-snooping/03_02_A.layout/14-51,60-133.mkv`

View File

@ -26,7 +26,7 @@ import os.path
import bpy import bpy
from bpy.types import AddonPreferences, Operator, WindowManager, Scene, PropertyGroup from bpy.types import AddonPreferences, Operator, WindowManager, Scene, PropertyGroup
from bpy.props import StringProperty, EnumProperty, PointerProperty, BoolProperty from bpy.props import StringProperty, EnumProperty, PointerProperty, BoolProperty, IntProperty
import rna_prop_ui import rna_prop_ui
from . import pillar, async_loop, flamenco from . import pillar, async_loop, flamenco
@ -178,13 +178,29 @@ class BlenderCloudPreferences(AddonPreferences):
flamenco_manager = PointerProperty(type=flamenco.FlamencoManagerGroup) flamenco_manager = PointerProperty(type=flamenco.FlamencoManagerGroup)
# TODO: before making Flamenco public, change the defaults to something less Institute-specific. # TODO: before making Flamenco public, change the defaults to something less Institute-specific.
# NOTE: The assumption is that the workers can also find the files in the same path. # NOTE: The assumption is that the workers can also find the files in the same path.
# This assumption is true for the Blender Institute, but we should allow other setups too. # This assumption is true for the Blender Institute.
flamenco_job_file_path = StringProperty( flamenco_job_file_path = StringProperty(
name='Job file path', name='Job file path',
description='Path where to store job files, should be accesible for Workers too', description='Path where to store job files, should be accesible for Workers too',
subtype='DIR_PATH', subtype='DIR_PATH',
default='/render/_flamenco/storage') default='/render/_flamenco/storage')
# TODO: before making Flamenco public, change the defaults to something less Institute-specific.
flamenco_job_output_path = StringProperty(
name='Job output path',
description='Path where to store output files, should be accessible for Workers',
subtype='DIR_PATH',
default='/render/_flamenco/output')
flamenco_job_output_strip_components = IntProperty(
name='Job output path strip components',
description='The final output path comprises of the job output path, and the blend file '
'path relative to the project with this many path components stripped off '
'the front',
min=0,
default=0,
soft_max=4,
)
def draw(self, context): def draw(self, context):
import textwrap import textwrap
@ -271,7 +287,7 @@ class BlenderCloudPreferences(AddonPreferences):
# Flamenco stuff # Flamenco stuff
flamenco_box = layout.box() flamenco_box = layout.box()
self.draw_flamenco_buttons(flamenco_box, self.flamenco_manager) self.draw_flamenco_buttons(flamenco_box, self.flamenco_manager, context)
def draw_subscribe_button(self, layout): def draw_subscribe_button(self, layout):
layout.operator('pillar.subscribe', icon='WORLD') layout.operator('pillar.subscribe', icon='WORLD')
@ -331,7 +347,7 @@ class BlenderCloudPreferences(AddonPreferences):
attract_box.prop(self, 'attract_project_local_path') attract_box.prop(self, 'attract_project_local_path')
def draw_flamenco_buttons(self, flamenco_box, bcp: flamenco.FlamencoManagerGroup): def draw_flamenco_buttons(self, flamenco_box, bcp: flamenco.FlamencoManagerGroup, context):
flamenco_row = flamenco_box.row(align=True) flamenco_row = flamenco_box.row(align=True)
flamenco_row.label('Flamenco', icon_value=icon('CLOUD')) flamenco_row.label('Flamenco', icon_value=icon('CLOUD'))
@ -353,7 +369,25 @@ class BlenderCloudPreferences(AddonPreferences):
path_box = flamenco_box.row(align=True) path_box = flamenco_box.row(align=True)
path_box.prop(self, 'flamenco_job_file_path') path_box.prop(self, 'flamenco_job_file_path')
path_box.operator('flamenco.open_job_file_path', text='', icon='DISK_DRIVE') props = path_box.operator('flamenco.explore_file_path', text='', icon='DISK_DRIVE')
props.path = self.flamenco_job_file_path
job_output_box = flamenco_box.column(align=True)
path_box = job_output_box.row(align=True)
path_box.prop(self, 'flamenco_job_output_path')
props = path_box.operator('flamenco.explore_file_path', text='', icon='DISK_DRIVE')
props.path = self.flamenco_job_output_path
job_output_box.prop(self, 'flamenco_job_output_strip_components',
text='Strip components')
from .flamenco import render_output_path
path_box = job_output_box.row(align=True)
output_path = render_output_path(context)
path_box.label(str(output_path))
props = path_box.operator('flamenco.explore_file_path', text='', icon='DISK_DRIVE')
props.path = str(output_path.parent)
# TODO: make a reusable way to select projects, and use that for Attract and Flamenco. # TODO: make a reusable way to select projects, and use that for Attract and Flamenco.
note_box = flamenco_box.column(align=True) note_box = flamenco_box.column(align=True)
@ -361,7 +395,6 @@ class BlenderCloudPreferences(AddonPreferences):
note_box.label('This will change in a future version of the add-on.') note_box.label('This will change in a future version of the add-on.')
class PillarCredentialsUpdate(pillar.PillarOperatorMixin, class PillarCredentialsUpdate(pillar.PillarOperatorMixin,
Operator): Operator):
"""Updates the Pillar URL and tests the new URL.""" """Updates the Pillar URL and tests the new URL."""

View File

@ -20,9 +20,9 @@
The preferences are managed blender.py, the rest of the Flamenco-specific stuff is here. The preferences are managed blender.py, the rest of the Flamenco-specific stuff is here.
""" """
import functools
import logging import logging
from pathlib import Path from pathlib import Path, PurePath
import typing import typing
import bpy import bpy
@ -135,6 +135,20 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
from ..blender import preferences from ..blender import preferences
filepath = Path(context.blend_data.filepath) filepath = Path(context.blend_data.filepath)
scene = context.scene
# The file extension should be determined by the render settings, not necessarily
# by the setttings in the output panel.
scene.render.use_file_extension = True
bpy.ops.wm.save_mainfile()
# Determine where the render output will be stored.
render_output = render_output_path(context)
if render_output is None:
self.report({'ERROR'}, 'Current file is outside of project path.')
self.quit()
return
self.log.info('Will output render files to %s', render_output)
# BAM-pack the files to the destination directory. # BAM-pack the files to the destination directory.
outfile, missing_sources = await self.bam_pack(filepath) outfile, missing_sources = await self.bam_pack(filepath)
@ -145,11 +159,12 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
# Create the job at Flamenco Server. # Create the job at Flamenco Server.
prefs = preferences() prefs = preferences()
scene = context.scene
settings = {'blender_cmd': '{blender}', settings = {'blender_cmd': '{blender}',
'chunk_size': scene.flamenco_render_chunk_size, 'chunk_size': scene.flamenco_render_chunk_size,
'filepath': str(outfile), 'filepath': str(outfile),
'frames': scene.flamenco_render_frame_range, 'frames': scene.flamenco_render_frame_range,
'render_output': str(render_output),
} }
try: try:
job_info = await create_job(self.user_id, job_info = await create_job(self.user_id,
@ -205,7 +220,7 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
# BAM doesn't like output directories that end in '.blend'. # BAM doesn't like output directories that end in '.blend'.
unique_dir = '%s-%s-%s' % (datetime.now().isoformat('-').replace(':', ''), unique_dir = '%s-%s-%s' % (datetime.now().isoformat('-').replace(':', ''),
self.db_user['username'], self.db_user['username'],
filepath.name.replace('.blend', '')) filepath.stem)
outdir = Path(prefs.flamenco_job_file_path) / unique_dir outdir = Path(prefs.flamenco_job_file_path) / unique_dir
outfile = outdir / filepath.name outfile = outdir / filepath.name
@ -266,26 +281,25 @@ class FLAMENCO_OT_copy_files(Operator,
self.quit() self.quit()
class FLAMENCO_OT_open_job_file_path(Operator): class FLAMENCO_OT_explore_file_path(Operator):
"""Opens the Flamenco job storage path in a file explorer.""" """Opens the Flamenco job storage path in a file explorer."""
bl_idname = 'flamenco.open_job_file_path' bl_idname = 'flamenco.explore_file_path'
bl_label = 'Open in file explorer' bl_label = 'Open in file explorer'
bl_description = __doc__.rstrip('.') bl_description = __doc__.rstrip('.')
path = StringProperty(name='Path', description='Path to explore', subtype='DIR_PATH')
def execute(self, context): def execute(self, context):
import platform import platform
import subprocess import subprocess
import os import os
from ..blender import preferences
path = preferences().flamenco_job_file_path
if platform.system() == "Windows": if platform.system() == "Windows":
os.startfile(path) os.startfile(self.path)
elif platform.system() == "Darwin": elif platform.system() == "Darwin":
subprocess.Popen(["open", path]) subprocess.Popen(["open", self.path])
else: else:
subprocess.Popen(["xdg-open", path]) subprocess.Popen(["xdg-open", self.path])
return {'FINISHED'} return {'FINISHED'}
@ -328,6 +342,69 @@ async def create_job(user_id: str,
return job.to_dict() return job.to_dict()
def is_image_type(render_output_type: str) -> bool:
"""Determines whether the render output type is an image (True) or video (False)."""
# This list is taken from rna_scene.c:273, rna_enum_image_type_items.
video_types = {'AVI_JPEG', 'AVI_RAW', 'FRAMESERVER', 'FFMPEG', 'QUICKTIME'}
return render_output_type not in video_types
@functools.lru_cache(1)
def _render_output_path(
local_project_path: str,
blend_filepath: str,
flamenco_job_output_strip_components: int,
flamenco_job_output_path: str,
render_image_format: str,
flamenco_render_frame_range: str,
) -> typing.Optional[PurePath]:
"""Cached version of render_output_path()
This ensures that redraws of the Flamenco Render and Add-on preferences panels
is fast.
"""
project_path = Path(bpy.path.abspath(local_project_path)).resolve()
blendfile = Path(blend_filepath)
try:
proj_rel = blendfile.parent.relative_to(project_path)
except ValueError:
log.exception('Current file is outside of project path %s', project_path)
return None
rel_parts = proj_rel.parts[flamenco_job_output_strip_components:]
output_top = Path(flamenco_job_output_path)
dir_components = output_top.joinpath(*rel_parts) / blendfile.stem
# Blender will have to append the file extensions by itself.
if is_image_type(render_image_format):
return dir_components / '#####'
return dir_components / flamenco_render_frame_range
def render_output_path(context) -> typing.Optional[PurePath]:
"""Returns the render output path to be sent to Flamenco.
Returns None when the current blend file is outside the project path.
"""
from ..blender import preferences
scene = context.scene
prefs = preferences()
return _render_output_path(
prefs.attract_project_local_path,
context.blend_data.filepath,
prefs.flamenco_job_output_strip_components,
prefs.flamenco_job_output_path,
scene.render.image_settings.file_format,
scene.flamenco_render_frame_range,
)
class FLAMENCO_PT_render(bpy.types.Panel): class FLAMENCO_PT_render(bpy.types.Panel):
bl_label = "Flamenco Render" bl_label = "Flamenco Render"
bl_space_type = 'PROPERTIES' bl_space_type = 'PROPERTIES'
@ -340,6 +417,8 @@ class FLAMENCO_PT_render(bpy.types.Panel):
from ..blender import preferences from ..blender import preferences
prefs = preferences()
layout.prop(context.scene, 'flamenco_render_job_priority') layout.prop(context.scene, 'flamenco_render_job_priority')
layout.prop(context.scene, 'flamenco_render_chunk_size') layout.prop(context.scene, 'flamenco_render_chunk_size')
@ -353,11 +432,23 @@ class FLAMENCO_PT_render(bpy.types.Panel):
prop_btn_row.prop(context.scene, 'flamenco_render_frame_range', text='') prop_btn_row.prop(context.scene, 'flamenco_render_frame_range', text='')
prop_btn_row.operator('flamenco.scene_to_frame_range', text='', icon='ARROW_LEFTRIGHT') prop_btn_row.operator('flamenco.scene_to_frame_range', text='', icon='ARROW_LEFTRIGHT')
labeled_row = layout.split(0.2, align=True) readonly_stuff = layout.column(align=True)
labeled_row = readonly_stuff.split(0.2, align=True)
labeled_row.label('Storage:') labeled_row.label('Storage:')
prop_btn_row = labeled_row.row(align=True) prop_btn_row = labeled_row.row(align=True)
prop_btn_row.label(preferences().flamenco_job_file_path) prop_btn_row.label(prefs.flamenco_job_file_path)
prop_btn_row.operator(FLAMENCO_OT_open_job_file_path.bl_idname, text='', icon='DISK_DRIVE') props = prop_btn_row.operator(FLAMENCO_OT_explore_file_path.bl_idname,
text='', icon='DISK_DRIVE')
props.path = prefs.flamenco_job_file_path
labeled_row = readonly_stuff.split(0.2, align=True)
labeled_row.label('Output:')
prop_btn_row = labeled_row.row(align=True)
render_output = render_output_path(context)
prop_btn_row.label(str(render_output))
props = prop_btn_row.operator(FLAMENCO_OT_explore_file_path.bl_idname,
text='', icon='DISK_DRIVE')
props.path = str(render_output.parent)
layout.operator(FLAMENCO_OT_render.bl_idname, layout.operator(FLAMENCO_OT_render.bl_idname,
text='Render on Flamenco', text='Render on Flamenco',
@ -370,7 +461,7 @@ def register():
bpy.utils.register_class(FLAMENCO_OT_render) bpy.utils.register_class(FLAMENCO_OT_render)
bpy.utils.register_class(FLAMENCO_OT_scene_to_frame_range) bpy.utils.register_class(FLAMENCO_OT_scene_to_frame_range)
bpy.utils.register_class(FLAMENCO_OT_copy_files) bpy.utils.register_class(FLAMENCO_OT_copy_files)
bpy.utils.register_class(FLAMENCO_OT_open_job_file_path) bpy.utils.register_class(FLAMENCO_OT_explore_file_path)
bpy.utils.register_class(FLAMENCO_PT_render) bpy.utils.register_class(FLAMENCO_PT_render)
scene = bpy.types.Scene scene = bpy.types.Scene