Integrating data system; unit tests in Blender

This commit is contained in:
Terry Hancock 2021-05-28 11:54:09 -05:00
parent 48841cc05b
commit 0d81cc5857
28 changed files with 4470 additions and 299 deletions

View File

@ -1,11 +0,0 @@
#script to run:
SCRIPT="/project/terry/Dev/eclipse-workspace/ABX/src/abx.py"
#path to the PyDev folder that contains a file named pydevd.py:
PYDEVD_PATH='/home/terry/.eclipse/360744294_linux_gtk_x86_64/plugins/org.python.pydev.core_7.3.0.201908161924/pysrc/'
#PYDEVD_PATH='/home/terry/.config/blender/2.79/scripts/addons/modules/pydev_debug.py'
import pydev_debug as pydev #pydev_debug.py is in a folder from Blender PYTHONPATH
pydev.debug(SCRIPT, PYDEVD_PATH, trace = True)

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python
# Run the script in the debugger client within Blender:
import subprocess
subprocess.call(['blender', '-P', '/project/terry/Dev/eclipse-workspace/ABX/BlenderRemoteDebug.py'])
subprocess.call(['blender279', '-P', '/project/terry/Dev/Git/abx/scripts/BlenderRemoteDebug.py'])

4
TestInBlender.py Normal file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env python
# Inject the unittest runner script into Blender and run it in batch mode:
import subprocess
subprocess.call(['blender279', '-b', '-P', '/project/terry/Dev/Git/abx/scripts/TestInBlender_bpy.py'])

View File

@ -2,7 +2,7 @@
bl_info = {
"name": "ABX",
"author": "Terry Hancock / Lunatics.TV Project / Anansi Spaceworks",
"version": (0, 2, 5),
"version": (0, 2, 6),
"blender": (2, 79, 0),
"location": "SpaceBar Search -> ABX",
"description": "Anansi Studio Extensions for Blender",
@ -22,17 +22,20 @@ try:
except ImportError:
print("Blender Add-On 'ABX' requires the Blender Python environment to run.")
print("blender_present = ", blender_present)
if blender_present:
if blender_present:
from . import abx_ui
BlendFile = abx_ui.BlendFile
def register():
bpy.utils.register_module(__name__)
abx_ui.register()
#bpy.utils.register_module(__name__)
def unregister():
bpy.utils.unregister_module(__name__)
abx_ui.unregister()
#bpy.utils.unregister_module(__name__)
if __name__ == "__main__":
register()

View File

@ -1,72 +1,113 @@
# DEFAULT ABX SETTINGS
---
project_schema:
- rank: project
delimiter: '-'
words: True
type: string
- rank: sequence
type:
VN: Vague Name
- rank: shot
type: letter
maxlength: 1
- rank: element
type: string
maxlength: 2
abx_default: True
render_profiles:
previz:
name: PreViz,
desc: 'GL/AVI Previz Render for Animatics',
engine: gl
version: any
fps: 30
fps_div: 1000
fps_skip: 1
suffix: GL
format: AVI_JPEG
extension: avi
freestyle: False
project_unit: []
project_schema: []
definitions:
filetypes:
blend: "Blender File"
kdenlive: "Kdenlive Video Editor File"
mlt: "Kdenlive Video Mix Script"
svg: "Scalable Vector Graphics (Inkscape)"
kra: "Krita Graphic File"
xcf: "Gimp Graphic File"
png: "Portable Network Graphics (PNG) Image"
jpg: "Joint Photographic Experts Group (JPEG) Image"
aup: "Audacity Project"
ardour: "Ardour Project"
flac: "Free Lossless Audio Codec (FLAC)"
mp3: "MPEG Audio Layer III (MP3) Audio File"
ogg: "Ogg Vorbis Audio File"
avi: "Audio Video Interleave (AVI) Video Container"
mkv: "Matroska Video Container"
mp4: "Moving Picture Experts Group (MPEG) 4 Format"
txt: "Plain Text File"
quick:
name: 30fps Paint
desc: '30fps Simplified Paint-Only Render'
engine: bi
fps: 30
fps_skip: 3
suffix: PT
format: AVI_JPEG
extension: avi
freestyle: False,
antialias: False,
motionblur: False
roles:
extras: "Extras, crowds, auxillary animated movement"
mech: "Mechanical animation"
anim: "Character animation"
cam: "Camera direction"
vfx: "Visual special effects"
compos: "Compositing"
bkg: "Background 2D image"
bb: "Billboard 2D image"
tex: "Texture 2D image"
foley: "Foley sound"
voice: "Voice recording"
fx: "Sound effects"
music: "Music track"
cue: "Musical cue"
amb: "Ambient sound"
loop: "Ambient sound loop"
edit: "Video edit"
roles_by_filetype:
kdenlive: edit
mlt: edit
omit_ranks: # Controls how much we shorten names
edit: 0
render: 0
filename: 0
scene: 0
abx:
render_profiles:
previz:
name: PreViz,
desc: 'GL/AVI Previz Render for Animatics'
engine: gl
version: any
fps: 30
fps_div: 1000
fps_skip: 1
suffix: GL
format: AVI_JPEG
extension: avi
freestyle: False
check:
name: 1fps Check
desc: '1fps Full-Features Check Renders'
engine: bi
fps: 30
fps_skip: 30
suffix: CH
format: JPEG
extension: jpg
framedigits: 5
freestyle: True
antialias: 8
full:
name: 30fps Full
desc: 'Full Render with all Features Turned On',
engine: bi
fps: 30
fps_skip: 1
suffix: ''
format: PNG
extension: png
framedigits: 5
freestyle: True
antialias: 8
quick:
name: 30fps Paint
desc: '30fps Simplified Paint-Only Render'
engine: bi
fps: 30
fps_skip: 3
suffix: PT
format: AVI_JPEG
extension: avi
freestyle: False,
antialias: False,
motionblur: False
check:
name: 1fps Check
desc: '1fps Full-Features Check Renders'
engine: bi
fps: 30
fps_skip: 30
suffix: CH
format: JPEG
extension: jpg
framedigits: 5
freestyle: True
antialias: 8
full:
name: 30fps Full
desc: 'Full Render with all Features Turned On'
engine: bi
fps: 30
fps_skip: 1
suffix: ''
format: PNG
extension: png
framedigits: 5
freestyle: True
antialias: 8
motionblur: 2
rendersize: 100
compress: 50

View File

@ -23,14 +23,22 @@ run into.
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
import os
import bpy, bpy.utils, bpy.types, bpy.props
from bpy.app.handlers import persistent
from . import file_context
# if bpy.data.filepath:
# BlendfileContext = file_context.FileContext(bpy.data.filepath)
# else:
# BlendfileContext = file_context.FileContext()
#
# abx_data = BlendfileContext.abx_data
from . import copy_anim
from . import std_lunatics_ink
from abx import ink_paint
from . import render_profile
@ -273,8 +281,7 @@ class LunaticsSceneProperties(bpy.types.PropertyGroup):
maxlen=0
)
bpy.utils.register_class(LunaticsSceneProperties)
bpy.types.Scene.lunaprops = bpy.props.PointerProperty(type=LunaticsSceneProperties)
class LunaticsScenePanel(bpy.types.Panel):
"""
@ -315,9 +322,7 @@ class RenderProfileSettings(bpy.types.PropertyGroup):
description="Select from pre-defined profiles of render settings",
default='full')
bpy.utils.register_class(RenderProfileSettings)
bpy.types.Scene.render_profile_settings = bpy.props.PointerProperty(
type=RenderProfileSettings)
class RenderProfilesOperator(bpy.types.Operator):
"""
@ -335,6 +340,7 @@ class RenderProfilesOperator(bpy.types.Operator):
return {'FINISHED'}
class RenderProfilesPanel(bpy.types.Panel):
"""
Add simple drop-down selector for generating common render settings with
@ -354,6 +360,7 @@ class RenderProfilesPanel(bpy.types.Panel):
row.operator('render.render_profiles')
class copy_animation(bpy.types.Operator):
"""
Copy animation from active object to selected objects (select source last!).
@ -399,6 +406,8 @@ class copy_animation(bpy.types.Operator):
return {'FINISHED'}
class copy_animation_settings(bpy.types.PropertyGroup):
"""
Settings for the 'copy_animation' operator.
@ -423,8 +432,7 @@ class copy_animation_settings(bpy.types.PropertyGroup):
description = "Scale factor for scaling animation (Re-Scale w/ 1.0 copies actions)",
default = 1.0)
bpy.utils.register_class(copy_animation_settings)
bpy.types.Scene.copy_anim_settings = bpy.props.PointerProperty(type=copy_animation_settings)
class CharacterPanel(bpy.types.Panel):
bl_space_type = "VIEW_3D" # window type panel is displayed in
@ -443,7 +451,7 @@ class CharacterPanel(bpy.types.Panel):
layout.prop(settings, 'rescale')
layout.prop(settings, 'scale_factor')
class lunatics_compositing_settings(bpy.types.PropertyGroup):
@ -465,9 +473,7 @@ class lunatics_compositing_settings(bpy.types.PropertyGroup):
description = "Render sky separately with compositing support (better shadows)",
default = True)
bpy.utils.register_class(lunatics_compositing_settings)
bpy.types.Scene.lx_compos_settings = bpy.props.PointerProperty(type=lunatics_compositing_settings)
class lunatics_compositing(bpy.types.Operator):
"""
Set up standard Lunatics scene compositing.
@ -483,7 +489,7 @@ class lunatics_compositing(bpy.types.Operator):
"""
scene = context.scene
shot = std_lunatics_ink.LunaticsShot(scene,
shot = ink_paint.LunaticsShot(scene,
inkthru=context.scene.lx_compos_settings.inkthru,
billboards=context.scene.lx_compos_settings.billboards,
sepsky=context.scene.lx_compos_settings.sepsky )
@ -497,7 +503,7 @@ class lunatics_compositing(bpy.types.Operator):
# self.col = self.layout.col()
# col.prop(settings, "inkthru", text="Ink Thru")
# col.prop(settings, "billboards", text="Ink Thru")
class LunaticsPanel(bpy.types.Panel):
@ -515,16 +521,52 @@ class LunaticsPanel(bpy.types.Panel):
layout.prop(settings, 'inkthru', text="Ink-Thru")
layout.prop(settings, 'billboards', text="Billboards")
layout.prop(settings, 'sepsky', text="Separate Sky")
BlendFile = file_context.FileContext()
@persistent
def update_handler(ctxt):
BlendFile.update(bpy.data.filepath)
def register():
bpy.utils.register_module(__name__)
bpy.utils.register_class(LunaticsSceneProperties)
bpy.types.Scene.lunaprops = bpy.props.PointerProperty(type=LunaticsSceneProperties)
bpy.utils.register_class(LunaticsScenePanel)
bpy.utils.register_class(RenderProfileSettings)
bpy.types.Scene.render_profile_settings = bpy.props.PointerProperty(
type=RenderProfileSettings)
bpy.utils.register_class(RenderProfilesOperator)
bpy.utils.register_class(RenderProfilesPanel)
bpy.utils.register_class(copy_animation)
bpy.utils.register_class(copy_animation_settings)
bpy.types.Scene.copy_anim_settings = bpy.props.PointerProperty(type=copy_animation_settings)
bpy.utils.register_class(CharacterPanel)
bpy.utils.register_class(lunatics_compositing_settings)
bpy.types.Scene.lx_compos_settings = bpy.props.PointerProperty(type=lunatics_compositing_settings)
bpy.utils.register_class(lunatics_compositing)
bpy.utils.register_class(LunaticsPanel)
bpy.app.handlers.save_post.append(update_handler)
bpy.app.handlers.load_post.append(update_handler)
bpy.app.handlers.scene_update_post.append(update_handler)
def unregister():
bpy.utils.unregister_module(__name__)
bpy.utils.unregister_class(LunaticsSceneProperties)
bpy.utils.unregister_class(LunaticsScenePanel)
if __name__ == "__main__":
register()
bpy.utils.unregister_class(RenderProfileSettings)
bpy.utils.unregister_class(RenderProfilesOperator)
bpy.utils.unregister_class(RenderProfilesPanel)
bpy.utils.unregister_class(copy_animation)
bpy.utils.unregister_class(copy_animation_settings)
bpy.utils.unregister_class(CharacterPanel)
bpy.utils.unregister_class(lunatics_compositing_settings)
bpy.utils.unregister_class(lunatics_compositing)
bpy.utils.unregister_class(LunaticsPanel)

View File

@ -244,6 +244,10 @@ class RecursiveDict(collections.OrderedDict):
#--------
# Code for collecting the YAML files we need
ABX_YAML = os.path.join(os.path.dirname(
os.path.abspath(os.path.join(__file__))),
'abx.yaml')
def collect_yaml_files(path, stems, dirmatch=False, sidecar=False, root='/'):
"""
@ -327,8 +331,10 @@ def get_project_data(filepath):
kitcat_root = get_project_root(kitcat_paths)
abx_data = combine_yaml(collect_yaml_files(filepath,
'abx', root=kitcat_root))
abx_data = combine_yaml([ABX_YAML])['abx']
abx_data.update(combine_yaml(collect_yaml_files(filepath,
'abx', root=kitcat_root)))
return kitcat_root, kitcat_data, abx_data

View File

@ -58,6 +58,11 @@ Demo:
import os, re, copy, string, collections
import yaml
DEFAULT_YAML = {}
with open(os.path.join(os.path.dirname(__file__), 'abx.yaml')) as def_yaml_file:
DEFAULT_YAML.update(yaml.safe_load(def_yaml_file))
TESTPATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'testdata', 'myproject', 'Episodes', 'A.001-Pilot', 'Seq', 'LP-LastPoint', 'A.001-LP-1-BeginningOfEnd-anim.txt'))
from . import accumulate
@ -66,6 +71,53 @@ from .accumulate import RecursiveDict
wordre = re.compile(r'([A-Z][a-z]+|[a-z]+|[0-9]+|[A-Z][A-Z]+)')
class Enum(dict):
def __init__(self, *options):
for i, option in enumerate(options):
if isinstance(option, list) or isinstance(option, tuple):
name = option[0]
self[i] = tuple(option)
else:
name = str(option)
self[i] = (option, option, option)
self[name] = i
if name not in ('name', 'number', 'options'):
setattr(self, name, i)
@property
def options(self):
"""
This gives the options in a Blender-friendly format, with
tuples of three strings for initializing bpy.props.Enum().
If the Enum was initialized with strings, the options will
contain the same string three times. If initialized with
tuples of strings, they will be used unaltered.
"""
options = []
number_keys = sorted([k for k in self.keys() if type(k) is int])
return [self[i] for i in number_keys]
def name(self, n):
if type(n) is int:
return self[n][0]
elif type(n) is str:
return n
else:
return None
def number(self, n):
if type(n) is str:
return self[n]
elif type(n) is int:
return n
else:
return None
log_level = Enum('DEBUG', 'INFO', 'WARNING', 'ERROR')
NameParsers = {} # Parser registry
def registered_parser(parser):
@ -352,9 +404,12 @@ class Parser_ABX_Schema(object):
start = 0
for start, (schema, name) in enumerate(zip(self.schemas, namepath)):
field, r, s = self._parse_beginning(remainder, schema.delimiter)
if field.lower() == schema.format.format(name).lower():
score += 1.0
break
try:
if field.lower() == schema.format.format(name).lower():
score += 1.0
break
except ValueError:
print(' (365) field, format', field, schema.format)
possible += 1.0
@ -366,11 +421,14 @@ class Parser_ABX_Schema(object):
if not remainder: break
field, remainder, s = self._parse_beginning(remainder, schema.delimiter)
score += s
if ( type(field) == str and
field.lower() == schema.format.format(name).lower()):
fields[schema.rank]={'code':field}
fields['rank'] = schema.rank
score += 1.0
try:
if ( type(field) == str and
field.lower() == schema.format.format(name).lower()):
fields[schema.rank]={'code':field}
fields['rank'] = schema.rank
score += 1.0
except ValueError:
print(' (384) field, format', field, schema.format)
possible += 2.0
# Remaining fields are authoritative (doesn't affect score)
@ -387,6 +445,77 @@ class Parser_ABX_Schema(object):
fields['role'] = self.roles_by_filetype[fields['filetype']]
return score/possible, fields
@registered_parser
class Parser_ABX_Fallback(object):
"""
Highly-tolerant parser to fall back to if the others fail
or can't be used.
"""
name = 'abx_fallback'
filetypes = DEFAULT_YAML['definitions']['filetypes']
roles = DEFAULT_YAML['definitions']['roles']
roles_by_filetype = (
DEFAULT_YAML['definitions']['roles_by_filetype'])
main_sep_re = re.compile(r'\W+') # Any single non-word char
comment_sep_re = re.compile(r'[\W_][\W_]+|[~#$!=+&]+')
def __init__(self, **kwargs):
pass
def _parse_ending(self, filename, separator):
try:
remainder, suffix = filename.rsplit(separator, 1)
score = 1.0
except ValueError:
remainder = filename
suffix = None
score = 0.0
return (suffix, remainder, score)
def __call__(self, filename, namepath):
fields = {}
score = 1.0
possible = 4.5
split = filename.rsplit('.', 1)
if len(split)<2 or split[1] not in self.filetypes:
fields['filetype'] = None
remainder = filename
score += 1.0
else:
fields['filetype'] = split[1]
remainder = split[0]
comment_match = self.comment_sep_re.search(remainder)
if comment_match:
fields['comment'] = remainder[comment_match.end():]
remainder = remainder[:comment_match.start()]
else:
fields['comment'] = None
role = self.main_sep_re.split(remainder)[-1]
if role in self.roles:
fields['role'] = role
remainder = remainder[:-1-len(role)]
score += 1.0
else:
fields['role'] = None
# Implied role
if fields['filetype'] in self.roles_by_filetype:
fields['role'] = self.roles_by_filetype[fields['filetype']]
score += 1.0
words = self.main_sep_re.split(remainder)
fields['code'] = ''.join([w.capitalize() for w in words])
fields['title'] = remainder
return score/possible, fields
class RankNotFound(LookupError):
@ -574,8 +703,19 @@ class NameContext(object):
"""
def __init__(self, container, fields=None, namepath_segment=(), ):
self.schemas = []
self.clear()
if container or fields or namepath_segment:
self.update(container, fields, namepath_segment)
def clear(self):
self.fields = {}
self.schemas = ['project']
self.rank = 0
self.code = 'untitled'
self.container = None
self.namepath_segment = []
def update(self, container=None, fields=None, namepath_segment=()):
self.container = container
if namepath_segment:
@ -584,12 +724,9 @@ class NameContext(object):
self.namepath_segment = []
try:
#self.namepath = self.container.namepath
self.schemas = self.container.schemas
except AttributeError:
self.schemas = []
#self.namepath = []
try:
self.omit_ranks = self.container.omit_ranks
@ -606,12 +743,6 @@ class NameContext(object):
self.fields.update(fields)
elif isinstance(fields, str):
self.fields.update(yaml.safe_load(fields))
# if 'code' in self.fields:
# self.namepath.append(self.fields['code'])
#self.code = self.fields[self.rank]['code']
def update_fields(self, data):
self.fields.update(data)
@ -684,7 +815,7 @@ class NameContext(object):
return None
@rank.setter
def set_rank(self, rank):
def rank(self, rank):
self.fields['rank'] = rank
@property
@ -699,7 +830,7 @@ class NameContext(object):
return ''
@name.setter
def set_name(self, name):
def name(self, name):
self.fields['name'] = name
@property
@ -710,7 +841,7 @@ class NameContext(object):
return self.fields['code']
@code.setter
def code_setter(self, code):
def code(self, code):
if self.rank:
self.fields[self.rank] = {'code': code}
else:
@ -724,7 +855,7 @@ class NameContext(object):
return ''
@description.setter
def set_description(self, description):
def description(self, description):
self.fields['description'] = str(description)
def _get_name_components(self):
@ -797,6 +928,7 @@ class FileContext(NameContext):
# hierarchy = None
#schema = None
# IMMUTABLE DEFAULTS:
filepath = None
root = None
folders = ()
@ -820,97 +952,151 @@ class FileContext(NameContext):
Collect path context information from a given filepath.
(Searches the filesystem for context information).
"""
#self.clear()
self.notes = []
# First init the superclass NameContext
NameContext.__init__(self, None, {})
self.namepath_segment = []
# TODO:
# I need to specify what happens when the path isn't defined.
# (Like we might need to initialize later?)
self.clear()
self.clear_notes()
if path:
self.update(path)
def clear(self):
NameContext.clear(self)
# Identity
self.root = os.path.abspath(os.environ['HOME'])
self.render_root = os.path.join(self.root, 'Renders')
self.filetype = ''
self.role = ''
self.title = ''
self.comment = ''
# Containers
#self.notes = []
self.name_contexts = []
# Status / Settings
self.filepath = None
self.filename = None
self.file_exists = False
self.folder_exists = False
self.omit_ranks = {
'edit': 0,
'render': 0,
'filename': 0,
'scene': 0}
# Defaults
self.provided_data = RecursiveDict(DEFAULT_YAML)
self.abx_fields = DEFAULT_YAML['abx']
def clear_notes(self):
# We use this for logging, so it doesn't get cleared by the
# normal clear process.
self.notes = []
def update(self, path):
# Basic File Path Info
self.filepath = os.path.abspath(path)
self.filename = os.path.basename(path)
# Basic File Path Info
self.filepath = os.path.abspath(path)
self.filename = os.path.basename(path)
# Does the file path exist?
if os.path.exists(path):
self.file_exists = True
self.folder_exists = True
else:
self.file_exists = False
if os.path.exists(os.path.dirname(path)):
self.folder_exists = True
else:
self.folder_exists = False
# Does the file path exist?
# - Should we create it? / Are we creating it?
# We should add a default YAML file in the ABX software to guarantee
# necessary fields are in place, and to document the configuration for
# project developers.
# Data from YAML Files
self._collect_yaml_data()
# Did we find the YAML data for the project?
# Did we find the project root?
# TODO: Bug?
# Note that 'project_schema' might not be correct if overrides are given.
# As things are, I think it will simply append the overrides, and this
# may lead to odd results. We'd need to actively compress the list by
# overwriting according to rank
#
# - Should we create it? / Are we creating it?
# We should add a default YAML file in the ABX software to guarantee
# necessary fields are in place, and to document the configuration for
# project developers.
# Data from YAML Files
#self._collect_yaml_data()
self.provided_data = RecursiveDict(DEFAULT_YAML)
kitcat_root, kitcat_data, abx_data = accumulate.get_project_data(self.filepath)
self.root = kitcat_root
self.provided_data.update(kitcat_data)
path = os.path.abspath(os.path.normpath(self.filepath))
root = os.path.abspath(self.root)
self.folders = [os.path.basename(self.root)]
self.folders.extend(os.path.normpath(os.path.relpath(path, root)).split(os.sep)[:-1])
self.abx_fields = abx_data
# Did we find the YAML data for the project?
# Did we find the project root?
# TODO: Bug?
# Note that 'project_schema' might not be correct if overrides are given.
# As things are, I think it will simply append the overrides, and this
# may lead to odd results. We'd need to actively compress the list by
# overwriting according to rank
#
try:
self._load_schemas(self.provided_data['project_schema'])
self.namepath_segment = [d['code'] for d in self.provided_data['project_unit']]
self.code = self.namepath[-1]
# Was there a "project_schema" section?
# - if not, do we fall back to a default?
# Was there a "project_unit" section?
# - if not, can we construct what we need from project_root & folders?
# Is there a definitions section?
# Do we provide defaults?
try:
self.render_root = os.path.join(self.root,
self.provided_data['definitions']['render_root'])
except KeyError:
self.render_root = os.path.join(self.root, 'Renders')
self.omit_ranks = {}
try:
for key, val in self.provided_data['definitions']['omit_ranks'].items():
self.omit_ranks[key] = int(val)
except KeyError:
self.omit_ranks.update({
'edit': 0,
'render': 1,
'filename': 1,
'scene': 3})
# Data from Parsing the File Name
try:
self.parsers = [NameParsers[self.provided_data['definitions']['parser']](**self.schema['filenames'])]
except (TypeError, KeyError, IndexError):
self.parsers = [
#Parser_ABX_Episode(),
Parser_ABX_Schema(self.schemas, self.provided_data['definitions'])]
except:
print("Errors finding Name Path (is there a 'project_schema' or 'project_unit' defined?")
pass
# print("\n(899) filename = ", self.filename)
# if 'project_schema' in self.provided_data:
# print("(899) project_schema: ", self.provided_data['project_schema'])
# else:
# print("(899) project schema NOT DEFINED")
#
# print("(904) self.namepath_segment = ", self.namepath_segment)
self.parser_chosen, self.parser_score = self._parse_filename()
# Was there a "project_schema" section?
# - if not, do we fall back to a default?
# Was there a "project_unit" section?
# - if not, can we construct what we need from project_root & folders?
# Is there a definitions section?
# Do we provide defaults?
try:
self.render_root = os.path.join(self.root,
self.provided_data['definitions']['render_root'])
except KeyError:
self.render_root = os.path.join(self.root, 'Renders')
self.omit_ranks = {}
try:
for key, val in self.provided_data['definitions']['omit_ranks'].items():
self.omit_ranks[key] = int(val)
except KeyError:
self.omit_ranks.update({
'edit': 0,
'render': 1,
'filename': 1,
'scene': 3})
# Data from Parsing the File Name
try:
self.parsers = [NameParsers[self.provided_data['definitions']['parser']](**self.schema['filenames'])]
except (TypeError, KeyError, IndexError):
self.parsers = [
#Parser_ABX_Episode(),
Parser_ABX_Schema(self.schemas, self.provided_data['definitions'])]
self.filetype = self.fields['filetype']
self.role = self.fields['role']
self.title = self.fields['title']
self.comment = self.fields['comment']
# TODO:
# We don't currently consider the information from the folder names,
# though we could get some additional information this way
# Empty / default attributes
self.name_contexts = []
parser_chosen, parser_score = self._parse_filename()
self.log(log_level.INFO, "Parsed with %s, score: %d" %
(parser_chosen, parser_score))
# TODO:
# We don't currently consider the information from the folder names,
# though we could get some additional information this way
def __repr__(self):
@ -919,7 +1105,18 @@ class FileContext(NameContext):
s = s + str(self.code) + '(' + str(self.rank) + ')'
s = s + ')'
return s
def log(self, level, msg):
if type(level) is str:
level = log_level.index(level)
self.notes.append((level, msg))
def get_log_text(self, level=log_level.INFO):
level = log_level.number(level)
return '\n'.join([
': '.join((log_level.name(note[0]), note[1]))
for note in self.notes
if log_level.number(note[0]) >= level])
def _parse_filename(self):
"""
@ -928,6 +1125,7 @@ class FileContext(NameContext):
"""
fields = {}
best_score = 0.0
best_parser_name = None
for parser in self.parsers:
score, fielddata = parser(self.filename, self.namepath)
if score > best_score:
@ -946,18 +1144,51 @@ class FileContext(NameContext):
self.fields[key] = val
def _collect_yaml_data(self):
self.provided_data = RecursiveDict()
kitcat_root, kitcat_data, abx_data = accumulate.get_project_data(self.filepath)
self.root = kitcat_root
self.provided_data.update(kitcat_data)
path = os.path.abspath(os.path.normpath(self.filepath))
root = os.path.abspath(self.root)
self.folders = [os.path.basename(self.root)]
self.folders.extend(os.path.normpath(os.path.relpath(path, root)).split(os.sep)[:-1])
self.abx_fields = abx_data
# def _collect_yaml_data(self):
@property
def filetype(self):
if 'filetype' in self.fields:
return self.fields['filetype']
else:
return ''
@filetype.setter
def filetype(self, filetype):
self.fields['filetype'] = filetype
@property
def role(self):
if 'role' in self.fields:
return self.fields['role']
else:
return ''
@role.setter
def role(self, role):
self.fields['role'] = role
@property
def title(self):
if 'title' in self.fields:
return self.fields['title']
else:
return ''
@title.setter
def title(self, title):
self.fields['title'] = title
@property
def comment(self):
if 'comment' in self.fields:
return self.fields['comment']
else:
return ''
@comment.setter
def comment(self, comment):
self.fields['comment'] = comment
@classmethod
def deref_implications(cls, values, matchfields):
@ -993,12 +1224,9 @@ class FileContext(NameContext):
fields = {}
fields.update(self.fields)
namepath_segment = []
ranks = [s.rank for s in self.schemas]
i_rank = len(self.namepath)
namepath_segment = []
ranks = [s.rank for s in self.schemas]
i_rank = len(self.namepath)
old_rank = ranks[i_rank -1]
# The new rank will be the highest rank mentioned, or the

View File

@ -3,81 +3,201 @@
Blender Python code to set parameters based on render profiles.
"""
import bpy
import bpy, bpy.types, bpy.utils, bpy.props
from . import std_lunatics_ink
from abx import ink_paint
render_formats = {
# VERY simplified and limited list of formats from Blender that we need:
# <API 'format'>: (<bpy file format>, <filename extension>),
'PNG': ('PNG', 'png'),
'JPG': ('JPEG', 'jpg'),
'EXR': ('OPEN_EXR_MULTILAYER', 'exr'),
'AVI': ('AVI_JPEG', 'avi'),
'MKV': ('FFMPEG', 'mkv')
}
from . import file_context
def set_render_from_profile(scene, profile):
if 'engine' in profile:
if profile['engine'] == 'gl':
pass
elif profile['engine'] == 'bi':
scene.render.engine = 'BLENDER_RENDER'
elif profile['engine'] == 'cycles':
scene.render.engine = 'CYCLES'
elif profile['engine'] == 'bge':
scene.render.engine = 'BLENDER_GAME'
class RenderProfile(object):
render_formats = {
# VERY simplified and limited list of formats from Blender that we need:
# <API 'format'>: (<bpy file format>, <filename extension>),
'PNG': ('PNG', 'png'),
'JPG': ('JPEG', 'jpg'),
'EXR': ('OPEN_EXR_MULTILAYER', 'exr'),
'AVI': ('AVI_JPEG', 'avi'),
'MKV': ('FFMPEG', 'mkv')
}
engines = {
'bi': 'BLENDER_RENDER',
'BLENDER_RENDER': 'BLENDER_RENDER',
'BI': 'BLENDER_RENDER',
'cycles': 'CYCLES',
'CYCLES': 'CYCLES',
'bge': 'BLENDER_GAME',
'BLENDER_GAME': 'BLENDER_GAME',
'BGE': 'BLENDER_GAME',
'gl': None,
'GL': None
}
def __init__(self, fields):
# Note: Settings w/ value *None* are left unaltered
# That is, they remain whatever they were before
# If a setting isn't included in the fields, then
# the attribute will be *None*.
if 'engine' not in fields:
fields['engine'] = None
if 'fps' in profile:
scene.render.fps = profile['fps']
if 'fps_skip' in profile:
scene.frame_step = profile['fps_skip']
if 'format' in profile:
scene.render.image_settings.file_format = render_formats[profile['format']][0]
if 'freestyle' in profile:
scene.render.use_freestyle = profile['freestyle']
if 'antialias' in profile:
if profile['antialias']:
scene.render.use_antialiasing = True
if profile['antialias'] in (5,8,11,16):
scene.render.antialiasing_samples = str(profile['antialias'])
if fields['engine']=='gl':
self.viewport_render = True
self.engine = None
else:
scene.render.use_antialiasing = False
if 'motionblur' in profile:
if profile['motionblur']:
scene.render.use_motion_blur = True
if type(profile['motionblur'])==int:
scene.render.motion_blur_samples = profile['motionblur']
self.viewport_render = False
if fields['engine'] in self.engines:
self.engine = self.engines[fields['engine']]
else:
scene.render.use_motion_blur = False
# Use Lunatics naming scheme for render target:
if 'framedigits' in profile:
framedigits = profile['framedigits']
else:
framedigits = 5
self.engine = None
# Parameters which are stored as-is, without modification:
self.fps = 'fps' in fields and int(fields['fps']) or None
self.fps_skip = 'fps_skip' in fields and int(fields['fps_skip']) or None
self.fps_divisor = 'fps_divisor' in fields and float(fields['fps_divisor']) or None
self.rendersize = 'rendersize' in fields and int(fields['rendersize']) or None
self.compress = 'compress' in fields and int(fields['compress']) or None
if 'suffix' in profile:
suffix = profile['suffix']
else:
suffix = ''
self.format = 'format' in fields and str(fields['format']) or None
if 'format' in profile:
rdr_fmt = render_formats[profile['format']][0]
ext = render_formats[profile['format']][1]
else:
rdr_fmt = 'PNG'
ext = 'png'
path = std_lunatics_ink.LunaticsShot(scene).render_path(
suffix=suffix, framedigits=framedigits, ext=ext, rdr_fmt=rdr_fmt)
scene.render.filepath = path
self.freestyle = 'freestyle' in fields and bool(fields['freestyle']) or None
self.antialiasing_samples = None
self.use_antialiasing = None
if 'antialias' in fields:
if fields['antialias']:
self.use_antialiasing = True
if fields['antialias'] in (5,8,11,16):
self.antialiasing_samples = str(fields['antialias'])
else:
self.use_antialiasing = False
self.use_motion_blur = None
self.motion_blur_samples = None
if 'motionblur' in fields:
if fields['motionblur']:
self.use_motion_blur = True
if type(fields['motionblur'])==int:
self.motion_blur_samples = int(fields['motionblur'])
else:
self.use_motion_blur = False
if 'framedigits' in fields:
self.framedigits = fields['framedigits']
else:
self.framedigits = 5
if 'suffix' in fields:
self.suffix = fields['suffix']
else:
self.suffix = ''
def apply(self, scene):
"""
Apply the profile settings to the given scene.
"""
if self.engine: scene.render.engine = self.engine
if self.fps: scene.render.fps = self.fps
if self.fps_skip: scene.frame_step = self.fps_skip
if self.fps_divisor: scene.render.fps_base = self.fps_divisor
if self.rendersize: scene.render.resolution_percentage = self.rendersize
if self.compress: scene.render.image_settings.compression = self.compress
if self.format:
scene.render.image_settings.file_format = self.render_formats[self.format][0]
if self.freestyle: scene.render.use_freestyle = self.freestyle
if self.use_antialiasing:
scene.render.use_antialiasing = self.use_antialiasing
if self.antialiasing_samples:
scene.render.antialiasing_samples = self.antialiasing_samples
if self.use_motion_blur:
scene.render.use_motion_blur = self.use_motion_blur
if self.motion_blur_samples:
scene.render.motion_blur_samples = self.motion_blur_samples
if self.format:
# prefix = scene.name_context.render_path
# prefix = BlendfileContext.name_contexts[scene.name_context].render_path
prefix = 'path_to_render' # We actually need to get this from NameContext
if self.suffix:
scene.render.filepath = (prefix + '-' + self.suffix + '-' +
'f'+('#'*self.framedigits) + '.' +
self.render_formats[self.format][1])
# def set_render_from_profile(scene, profile):
# if 'engine' in profile:
# if profile['engine'] == 'gl':
# pass
# elif profile['engine'] == 'bi':
# scene.render.engine = 'BLENDER_RENDER'
# elif profile['engine'] == 'cycles':
# scene.render.engine = 'CYCLES'
# elif profile['engine'] == 'bge':
# scene.render.engine = 'BLENDER_GAME'
#
# if 'fps' in profile:
# scene.render.fps = profile['fps']
#
# if 'fps_skip' in profile:
# scene.frame_step = profile['fps_skip']
#
# if 'format' in profile:
# scene.render.image_settings.file_format = render_formats[profile['format']][0]
#
# if 'freestyle' in profile:
# scene.render.use_freestyle = profile['freestyle']
#
# if 'antialias' in profile:
# if profile['antialias']:
# scene.render.use_antialiasing = True
# if profile['antialias'] in (5,8,11,16):
# scene.render.antialiasing_samples = str(profile['antialias'])
# else:
# scene.render.use_antialiasing = False
#
# if 'motionblur' in profile:
# if profile['motionblur']:
# scene.render.use_motion_blur = True
# if type(profile['motionblur'])==int:
# scene.render.motion_blur_samples = profile['motionblur']
# else:
# scene.render.use_motion_blur = False
#
# # Use Lunatics naming scheme for render target:
# if 'framedigits' in profile:
# framedigits = profile['framedigits']
# else:
# framedigits = 5
#
# if 'suffix' in profile:
# suffix = profile['suffix']
# else:
# suffix = ''
#
# if 'format' in profile:
# rdr_fmt = render_formats[profile['format']][0]
# ext = render_formats[profile['format']][1]
# else:
# rdr_fmt = 'PNG'
# ext = 'png'
#
# path = ink_paint.LunaticsShot(scene).render_path(
# suffix=suffix, framedigits=framedigits, ext=ext, rdr_fmt=rdr_fmt)
#
# scene.render.filepath = path

BIN
pkg/abx-0.2.6a.zip Normal file

Binary file not shown.

39
pkg/abx/__init__.py Normal file
View File

@ -0,0 +1,39 @@
bl_info = {
"name": "ABX",
"author": "Terry Hancock / Lunatics.TV Project / Anansi Spaceworks",
"version": (0, 2, 6),
"blender": (2, 79, 0),
"location": "SpaceBar Search -> ABX",
"description": "Anansi Studio Extensions for Blender",
"warning": "",
"wiki_url": "",
"tracker_url": "",
"category": "Object",
}
blender_present = False
try:
# These are protected so we can read the add-on metadata from my
# management scripts, which run in the O/S standard Python 3
import bpy, bpy.utils, bpy.types
blender_present = True
except ImportError:
print("Blender Add-On 'ABX' requires the Blender Python environment to run.")
if blender_present:
from . import abx_ui
def register():
abx_ui.register()
#bpy.utils.register_module(__name__)
def unregister():
abx_ui.unregister()
#bpy.utils.unregister_module(__name__)
if __name__ == "__main__":
register()

113
pkg/abx/abx.yaml Normal file
View File

@ -0,0 +1,113 @@
# DEFAULT ABX SETTINGS
---
abx_default: True
project_unit: []
project_schema: []
definitions:
filetypes:
blend: "Blender File"
kdenlive: "Kdenlive Video Editor File"
mlt: "Kdenlive Video Mix Script"
svg: "Scalable Vector Graphics (Inkscape)"
kra: "Krita Graphic File"
xcf: "Gimp Graphic File"
png: "Portable Network Graphics (PNG) Image"
jpg: "Joint Photographic Experts Group (JPEG) Image"
aup: "Audacity Project"
ardour: "Ardour Project"
flac: "Free Lossless Audio Codec (FLAC)"
mp3: "MPEG Audio Layer III (MP3) Audio File"
ogg: "Ogg Vorbis Audio File"
avi: "Audio Video Interleave (AVI) Video Container"
mkv: "Matroska Video Container"
mp4: "Moving Picture Experts Group (MPEG) 4 Format"
txt: "Plain Text File"
roles:
extras: "Extras, crowds, auxillary animated movement"
mech: "Mechanical animation"
anim: "Character animation"
cam: "Camera direction"
vfx: "Visual special effects"
compos: "Compositing"
bkg: "Background 2D image"
bb: "Billboard 2D image"
tex: "Texture 2D image"
foley: "Foley sound"
voice: "Voice recording"
fx: "Sound effects"
music: "Music track"
cue: "Musical cue"
amb: "Ambient sound"
loop: "Ambient sound loop"
edit: "Video edit"
roles_by_filetype:
kdenlive: edit
mlt: edit
omit_ranks: # Controls how much we shorten names
edit: 0
render: 0
filename: 0
scene: 0
abx:
render_profiles:
previz:
name: PreViz,
desc: 'GL/AVI Previz Render for Animatics'
engine: gl
version: any
fps: 30
fps_div: 1000
fps_skip: 1
suffix: GL
format: AVI_JPEG
extension: avi
freestyle: False
quick:
name: 30fps Paint
desc: '30fps Simplified Paint-Only Render'
engine: bi
fps: 30
fps_skip: 3
suffix: PT
format: AVI_JPEG
extension: avi
freestyle: False,
antialias: False,
motionblur: False
check:
name: 1fps Check
desc: '1fps Full-Features Check Renders'
engine: bi
fps: 30
fps_skip: 30
suffix: CH
format: JPEG
extension: jpg
framedigits: 5
freestyle: True
antialias: 8
full:
name: 30fps Full
desc: 'Full Render with all Features Turned On'
engine: bi
fps: 30
fps_skip: 1
suffix: ''
format: PNG
extension: png
framedigits: 5
freestyle: True
antialias: 8
motionblur: 2
rendersize: 100
compress: 50

560
pkg/abx/abx_ui.py Normal file
View File

@ -0,0 +1,560 @@
# Anansi Studio Extensions for Blender 'ABX'
"""
Collection of Blender extension tools to make our jobs easier.
This is not really meant to be an integrated plugin, but rather
a collection of useful scripts we can run to solve problems we
run into.
"""
#
#Copyright (C) 2019 Terry Hancock
#
# 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.
#
import os
import bpy, bpy.utils, bpy.types, bpy.props
from . import file_context
# if bpy.data.filepath:
# BlendfileContext = file_context.FileContext(bpy.data.filepath)
# else:
# BlendfileContext = file_context.FileContext()
#
# abx_data = BlendfileContext.abx_data
from . import copy_anim
from . import std_lunatics_ink
from . import render_profile
#configfile = os.path.join(os.path.dirname(__file__), 'config.yaml')
#print("Configuration file path: ", os.path.abspath(configfile))
# Lunatics Scene Panel
# Lunatics file/scene properties:
# TODO: This hard-coded table is a temporary solution until I have figured
# out a good way to look these up from the project files (maybe YAML?):
seq_id_table = {
('S1', 0): {'':'', 'mt':'Main Title'},
('S1', 1): {'':'',
'TR':'Train',
'SR':'Soyuz Rollout',
'TB':'Touring Baikonur',
'PC':'Press Conference',
'SU':'Suiting Up',
'LA':'Launch',
'SF':'Soyuz Flight',
'mt':'Main Title',
'ad':'Ad Spot',
'pv':'Preview',
'et':'Episode Titles',
'cr':'Credits'
},
('S1', 2): {'':'',
'MM':'Media Montage',
'mt':'Main Title',
'et':'Episode Titles',
'SS':'Space Station',
'LC':'Loading Cargo',
'TL':'Trans Lunar Injection',
'BT':'Bed Time',
'ad':'Ad Spot',
'pv':'Preview',
'cr':'Credits'
},
('S1', 3): {'':'',
'mt':'Main Title',
'et':'Episode Titles',
'ZG':'Zero G',
'LI':'Lunar Injection',
'LO':'Lunar Orbit',
'ML':'Moon Landing',
'IR':'Iridium',
'TC':'Touring Colony',
'FD':'Family Dinner',
'ad':'Ad Spot',
'pv':'Preview',
'cr':'Credits'
},
('S2', 0): {'':'', 'mt':'Main Title'},
('L', 0): {'':'',
'demo':'Demonstration',
'prop':'Property',
'set': 'Set',
'ext': 'Exterior Set',
'int': 'Interior Set',
'prac':'Practical',
'char':'Character',
'fx': 'Special Effect',
'stock': 'Stock Animation'
},
None: ['']
}
def get_seq_ids(self, context):
#
# Note: To avoid the reference bug mentioned in the Blender documentation,
# we only return values held in the global seq_id_table, which
# should remain defined and therefore hold a reference to the strings.
#
if not context:
seq_ids = seq_id_table[None]
else:
scene = context.scene
series = scene.lunaprops.series_id
episode = scene.lunaprops.episode_id
if (series, episode) in seq_id_table:
seq_ids = seq_id_table[(series, episode)]
else:
seq_ids = seq_id_table[None]
seq_enum_items = [(s, s, seq_id_table[series,episode][s]) for s in seq_ids]
return seq_enum_items
# Another hard-coded table -- for render profiles
render_profile_table = {
'previz': {
'name': 'PreViz',
'desc': 'GL/AVI Previz Render for Animatics',
'engine':'gl',
'version':'any',
'fps': 30,
'fps_div': 1000,
'fps_skip': 1,
'suffix': 'GL',
'format': 'AVI',
'freestyle': False
},
'paint6': {
'name': '6fps Paint',
'desc': '6fps Simplified Paint-Only Render',
'engine':'bi',
'fps': 30,
'fps_skip': 5,
'suffix': 'P6',
'format': 'AVI',
'freestyle': False,
'antialias': False,
'motionblur': False
},
'paint3': {
'name': '3fps Paint',
'desc': '3fps Simplified Paint-Only Render',
'engine': 'bi',
'fps': 30,
'fps_skip': 10,
'suffix': 'P3',
'format': 'AVI',
'freestyle': False,
'antialias': False,
'motionblur': False,
},
'paint': {
'name': '30fps Paint',
'desc': '30fps Simplified Paint-Only Render',
'engine': 'bi',
'fps': 30,
'fps_skip': 1,
'suffix': 'PT',
'format': 'AVI',
'freestyle': False,
'antialias': False,
'motionblur': False
},
'check': {
'name': '1fps Check',
'desc': '1fps Full-Features Check Renders',
'engine': 'bi',
'fps': 30,
'fps_skip': 30,
'suffix': 'CH',
'format': 'JPG',
'framedigits': 5,
'freestyle': True,
'antialias': 8
},
'full': {
'name': '30fps Full',
'desc': 'Full Render with all Features Turned On',
'engine': 'bi',
'fps': 30,
'fps_skip': 1,
'suffix': '',
'format': 'PNG',
'framedigits': 5,
'freestyle': True,
'antialias': 8
},
}
class LunaticsSceneProperties(bpy.types.PropertyGroup):
"""
Properties of the current scene.
"""
series_id = bpy.props.EnumProperty(
items=[
('S1', 'S1', 'Series One'),
('S2', 'S2', 'Series Two'),
('S3', 'S3', 'Series Three'),
('A1', 'Aud','Audiodrama'),
('L', 'Lib','Library')
],
name="Series",
default='S1',
description="Series/Season of Animated Series, Audiodrama, or Library"
)
episode_id = bpy.props.IntProperty(
name="Episode",
default=0,
description="Episode number (0 means multi-use), ignored for Library",
min=0,
max=1000,
soft_max=18
)
seq_id = bpy.props.EnumProperty(
name='',
items=get_seq_ids,
description="Sequence ID"
)
block_id = bpy.props.IntProperty(
name='',
default=1,
min=0,
max=20,
soft_max=10,
description="Block number"
)
use_multicam = bpy.props.BoolProperty(
name="Multicam",
default=False,
description="Use multicam camera/shot numbering?"
)
cam_id = bpy.props.IntProperty(
name="Cam",
default=0,
min=0,
max=20,
soft_max=10,
description="Camera number"
)
shot_id = bpy.props.EnumProperty(
name='Shot',
#items=[('NONE', '', 'Single')]+[(c,c,'Shot '+c) for c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'],
items=[(c,c,'Shot '+c) for c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'],
default='A',
description="Shot ID, normally a single capital letter, can be empty, two letters for transitions"
)
shot_name = bpy.props.StringProperty(
name='Name',
description='Short descriptive codename',
maxlen=0
)
class LunaticsScenePanel(bpy.types.Panel):
"""
Add a panel to the Properties-Scene screen
"""
bl_idname = 'SCENE_PT_lunatics'
bl_label = 'Lunatics Project'
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = 'scene'
def draw(self, context):
lunaprops = bpy.context.scene.lunaprops
self.layout.label(text='Lunatics! Project Properties')
row = self.layout.row()
row.prop(lunaprops, 'series_id')
row.prop(lunaprops, 'episode_id')
row = self.layout.row()
row.prop(lunaprops, 'use_multicam')
row = self.layout.row()
row.prop(lunaprops, 'seq_id')
row.prop(lunaprops, 'block_id')
if lunaprops.use_multicam:
row.prop(lunaprops, 'cam_id')
row.prop(lunaprops, 'shot_id')
row.prop(lunaprops, 'shot_name')
# Buttons
class RenderProfileSettings(bpy.types.PropertyGroup):
"""
Settings for Render Profiles control.
"""
render_profile = bpy.props.EnumProperty(
name='Profile',
items=[(k, v['name'], v['desc'])
for k,v in render_profile_table.items()],
description="Select from pre-defined profiles of render settings",
default='full')
class RenderProfilesOperator(bpy.types.Operator):
"""
Operator invoked implicitly when render profile is changed.
"""
bl_idname = 'render.render_profiles'
bl_label = 'Apply Render Profile'
bl_options = {'UNDO'}
def invoke(self, context, event):
scene = context.scene
profile = render_profile_table[scene.render_profile_settings.render_profile]
render_profile.set_render_from_profile(scene, profile)
return {'FINISHED'}
class RenderProfilesPanel(bpy.types.Panel):
"""
Add simple drop-down selector for generating common render settings with
destination set according to project defaults.
"""
bl_idname = 'SCENE_PT_render_profiles'
bl_label = 'Render Profiles'
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = 'render'
def draw(self, context):
rps = bpy.context.scene.render_profile_settings
row = self.layout.row()
row.prop(rps, 'render_profile')
row = self.layout.row()
row.operator('render.render_profiles')
class copy_animation(bpy.types.Operator):
"""
Copy animation from active object to selected objects (select source last!).
Useful for fixing broken proxy rigs (create a new proxy, and used this tool
to copy all animation from the original -- avoids tedious/error-prone NLA work).
Can also migrate to a re-scaled rig.
"""
bl_idname = 'object.copy_anim'
bl_label = 'Copy Animation'
bl_options = {'UNDO'}
def invoke(self, context, event):
#print("Copy NLA from selected armature to active armatures.")
src_ob = context.active_object
tgt_obs = [ob for ob in context.selected_objects if ob != context.active_object]
# TODO
# Are these type checks necessary?
# Is there any reason to restrict this operator to armature objects?
# I think there isn't.
if src_ob.type != 'ARMATURE':
self.report({'WARNING'}, 'Cannot copy NLA data from object that is not an ARMATURE.')
return {'CANCELLED'}
tgt_arm_obs = []
for ob in tgt_obs:
if ob.type == 'ARMATURE':
tgt_arm_obs.append(ob)
if not tgt_arm_obs:
self.report({'WARNING'}, 'No armature objects selected to copy animation data to.')
return {'CANCELLED'}
copy_anim.copy_object_animation(src_ob, tgt_arm_obs,
dopesheet=context.scene.copy_anim_settings.dopesheet,
nla=context.scene.copy_anim_settings.nla,
rescale=context.scene.copy_anim_settings.rescale,
scale_factor=context.scene.copy_anim_settings.scale_factor,
report=self.report)
return {'FINISHED'}
class copy_animation_settings(bpy.types.PropertyGroup):
"""
Settings for the 'copy_animation' operator.
"""
dopesheet = bpy.props.BoolProperty(
name = "Dope Sheet",
description = "Copy animation from Dope Sheet",
default=True)
nla = bpy.props.BoolProperty(
name = "NLA Strips",
description = "Copy all strips from NLA Editor",
default=True)
rescale = bpy.props.BoolProperty(
name = "Re-Scale/Copy",
description = "Make rescaled COPY of actions instead of LINK to original",
default = False)
scale_factor = bpy.props.FloatProperty(
name = "Scale",
description = "Scale factor for scaling animation (Re-Scale w/ 1.0 copies actions)",
default = 1.0)
class CharacterPanel(bpy.types.Panel):
bl_space_type = "VIEW_3D" # window type panel is displayed in
bl_context = "objectmode"
bl_region_type = "TOOLS" # region of window panel is displayed in
bl_label = "Character"
bl_category = "ABX"
def draw(self, context):
settings = bpy.context.scene.copy_anim_settings
layout = self.layout.column(align = True)
layout.label("Animation Data")
layout.operator('object.copy_anim')
layout.prop(settings, 'dopesheet')
layout.prop(settings, 'nla')
layout.prop(settings, 'rescale')
layout.prop(settings, 'scale_factor')
class lunatics_compositing_settings(bpy.types.PropertyGroup):
"""
Settings for the LX compositor tool.
"""
inkthru = bpy.props.BoolProperty(
name = "Ink-Thru",
description = "Support transparent Freestyle ink effect",
default=True)
billboards = bpy.props.BoolProperty(
name = "Billboards",
description = "Support material pass for correct billboard inking",
default = False)
sepsky = bpy.props.BoolProperty(
name = "Separate Sky",
description = "Render sky separately with compositing support (better shadows)",
default = True)
class lunatics_compositing(bpy.types.Operator):
"""
Set up standard Lunatics scene compositing.
"""
bl_idname = "scene.lunatics_compos"
bl_label = "Ink/Paint Config"
bl_options = {'UNDO'}
bl_description = "Set up standard Lunatics Ink/Paint compositing in scene"
def invoke(self, context, event):
"""
Add standard 'Lunatics!' shot compositing to the currently-selected scene.
"""
scene = context.scene
shot = std_lunatics_ink.LunaticsShot(scene,
inkthru=context.scene.lx_compos_settings.inkthru,
billboards=context.scene.lx_compos_settings.billboards,
sepsky=context.scene.lx_compos_settings.sepsky )
shot.cfg_scene()
return {'FINISHED'}
# def draw(self, context):
# settings = context.scene.lx_compos_settings
# self.col = self.layout.col()
# col.prop(settings, "inkthru", text="Ink Thru")
# col.prop(settings, "billboards", text="Ink Thru")
class LunaticsPanel(bpy.types.Panel):
bl_space_type = "VIEW_3D"
bl_context = "objectmode"
bl_region_type = "TOOLS"
bl_label = "Lunatics"
bl_category = "ABX"
def draw(self, context):
settings = bpy.context.scene.lx_compos_settings
layout = self.layout.column(align = True)
layout.label("Compositing")
layout.operator('scene.lunatics_compos')
layout.prop(settings, 'inkthru', text="Ink-Thru")
layout.prop(settings, 'billboards', text="Billboards")
layout.prop(settings, 'sepsky', text="Separate Sky")
def register():
bpy.utils.register_class(LunaticsSceneProperties)
bpy.types.Scene.lunaprops = bpy.props.PointerProperty(type=LunaticsSceneProperties)
bpy.utils.register_class(LunaticsScenePanel)
bpy.utils.register_class(RenderProfileSettings)
bpy.types.Scene.render_profile_settings = bpy.props.PointerProperty(
type=RenderProfileSettings)
bpy.utils.register_class(RenderProfilesOperator)
bpy.utils.register_class(RenderProfilesPanel)
bpy.utils.register_class(copy_animation)
bpy.utils.register_class(copy_animation_settings)
bpy.types.Scene.copy_anim_settings = bpy.props.PointerProperty(type=copy_animation_settings)
bpy.utils.register_class(CharacterPanel)
bpy.utils.register_class(lunatics_compositing_settings)
bpy.types.Scene.lx_compos_settings = bpy.props.PointerProperty(type=lunatics_compositing_settings)
bpy.utils.register_class(lunatics_compositing)
bpy.utils.register_class(LunaticsPanel)
def unregister():
bpy.utils.unregister_class(LunaticsSceneProperties)
bpy.utils.unregister_class(LunaticsScenePanel)
bpy.utils.unregister_class(RenderProfileSettings)
bpy.utils.unregister_class(RenderProfilesOperator)
bpy.utils.unregister_class(RenderProfilesPanel)
bpy.utils.unregister_class(copy_animation)
bpy.utils.unregister_class(copy_animation_settings)
bpy.utils.unregister_class(CharacterPanel)
bpy.utils.unregister_class(lunatics_compositing_settings)
bpy.utils.unregister_class(lunatics_compositing)
bpy.utils.unregister_class(LunaticsPanel)

342
pkg/abx/accumulate.py Normal file
View File

@ -0,0 +1,342 @@
# accumulate.py
"""
Data structures for accumulating tree-structured data from multiple sources.
Data is acquired from file and directory names and also from yaml files in the
tree. The yaml files are loaded in increasing priority from upper directories
to the local one, starting from the highest level file to contain a "project_root"
key.
The files named for their parent directory are assumed to be KitCAT files (i.e.
"kitcat.yaml" and "<dirname>.yaml" are treated the same way). Only files named
"abx.yaml" are assumed to be configuration files specific to ABX.
We collect these by going up the file path, and then load them coming down. If
we find a "project_root" key, we ditch the previous data and start over. This way
any project files found above the project root will be ignored.
As a use case: if we were to store a new project inside of another project, the
new project's project_root would make it blind to the settings in the containing
project. Other directories in the parent project would still go to the parent
project's root. This avoids having the location the project is stored affect
the project data.
The overall structure is a dictionary. When updating with new data, any element
that is itself a dictionary is treated recursively (that is, it is updated with
directory data when another dictionary is provided for the same key). If an
element is a list, then data from successively-higher directories extends the
list (see UnionList, below). If a scalar replaces a dictionary or list value in
a more specific entry, then it clobbers it and any updated information in it.
@author: Terry Hancock
@copyright: 2019 Anansi Spaceworks.
@license: GNU General Public License, version 2.0 or later. (Python code)
@contact: digitante@gmail.com
Demo:
>>> import accumulate
>>> T1 = accumulate.RecursiveDict(accumulate.TEST_DICT_1)
>>> T2 = accumulate.RecursiveDict(accumulate.TEST_DICT_2)
>>> import copy
>>> Ta = copy.deepcopy(T1)
>>> Tb = copy.deepcopy(T2)
>>> Ta
RecursiveDict({'A': 1, 'B': [1, 2, 3], 'C': {'a': 1, 'b': 2, 'c': 3}, 'D': {}, 'E': None, 'F': {'h': {'i': {'j': {'k': 'abcdefghijk'}}}}})
>>> Tb
RecursiveDict({'C': {'d': 4, 'e': 5, 'f': 6}, 'D': (1, 2, 3), 'B': [4, 5, 6], 'E': 0})
>>> Ta.update(T2)
>>> Ta
RecursiveDict({'A': 1, 'B': [4, 5, 6, 1, 2, 3], 'C': {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 6}, 'D': (1, 2, 3), 'E': 0, 'F': {'h': {'i': {'j': {'k': 'abcdefghijk'}}}}})
>>> Tb.update(T1)
>>> Tb
RecursiveDict({'C': {'d': 4, 'e': 5, 'f': 6, 'a': 1, 'b': 2, 'c': 3}, 'D': {}, 'B': [1, 2, 3, 4, 5, 6], 'E': None, 'A': 1, 'F': {'h': {'i': {'j': {'k': 'abcdefghijk'}}}}})
>>>
"""
TEST_DICT_1 = { 'A':1,
'B':[1,2,3],
'C':{'a':1, 'b':2, 'c':3},
'D':{},
'E':None,
'F':{'h':{'i':{'j':{'k':'abcdefghijk'}}}},
}
TEST_DICT_2 = { 'C':{'d':4, 'e':5, 'f':6},
'D':(1,2,3),
'B':[4,5,6],
'E':0
}
YAML_TEST = """
A: 1
B:
- 4
- 5
- 6
- 1
- 2
- 3
C:
a: 1
b: 2
c: 3
d: 4
e: 5
f: 6
D: (1, 2, 3)
E: 0
F:
h:
i:
j:
k: abcdefghijk
"""
import os, collections.abc, re
import yaml
wordre = re.compile(r'([A-Z]+[a-z]*|[a-z]+|[0-9]+)')
class OrderedSet(collections.abc.Set):
"""
List-based set from Python documentation example.
"""
def __init__(self, iterable):
self.elements = lst = []
for value in iterable:
if value not in lst:
lst.append(value)
def __iter__(self):
return iter(self.elements)
def __contains__(self, value):
return value in self.elements
def __len__(self):
return len(self.elements)
def __repr__(self):
return repr(list(self))
def union(self, other):
return self.__or__(other)
def intersection(self, other):
return self.__and__(other)
class UnionList(list):
"""
Special list-based collection, which implements a "union" operator similar
to the one defined for sets. It only adds options from the other list
which are not already in the current list.
Note that it is intentionally asymmetric. The initial list may repeat values
and they will be kept, so it does not require the list to consist only of
unique entries (unlike Set collections).
This allows us to use this type for loading list-oriented data from data
files, which may or may not contain repetitions for different uses, but
also makes accumulation idempotent (running the union twice will not
increase the size of the result, because no new values will be found).
"""
def union(self, other):
combined = UnionList(self)
for element in other:
if element not in self:
combined.append(element)
return combined
class RecursiveDict(collections.OrderedDict):
"""
A dictionary which updates recursively, updating any values which are
themselves dictionaries when the replacement value is a dictionary, rather
than replacing them, and treating any values which are themselves lists
as UnionLists and applying the union operation to combine them
(when the replacement value is also a list).
"""
def clear(self):
for key in self:
del self[key]
def update(self, mapping):
for key in mapping:
if key in self:
if (isinstance(self[key], collections.abc.Mapping) and
isinstance(mapping[key], collections.abc.Mapping)):
# Subdictionary
newvalue = RecursiveDict(self[key])
newvalue.update(RecursiveDict(mapping[key]))
self[key] = newvalue
elif ((isinstance(self[key], collections.abc.MutableSequence) or
isinstance(self[key], collections.abc.Set)) and
(isinstance(mapping[key], collections.abc.MutableSequence) or
isinstance(mapping[key], collections.abc.Set))):
# Sublist
self[key] = UnionList(self[key]).union(UnionList(mapping[key]))
else: # scalar
self[key] = mapping[key]
else: # new key
self[key] = mapping[key]
def get_data(self):
new = {}
for key in self:
if isinstance(self[key], RecursiveDict):
new[key]=dict(self[key].get_data())
elif isinstance(self[key], UnionList):
new[key]=list(self[key])
else:
new[key]=self[key]
return new
def __setitem__(self, key, value):
if isinstance(value, collections.abc.Mapping):
super().__setitem__(key, RecursiveDict(value))
elif isinstance(value, collections.abc.MutableSequence):
super().__setitem__(key, UnionList(value))
else:
super().__setitem__(key,value)
def __repr__(self, compact=False):
s = ''
if not compact:
s = s + '%s(' % self.__class__.__name__
s = s + '{'
for key in self:
if isinstance(self[key], RecursiveDict):
s = s+"'%s'"%key + ': ' + "%s" % self[key].__repr__(compact=True) + ', '
else:
s = s+ "'%s'"%key + ': ' + "%s" % repr(self[key]) + ', '
if s.endswith(', '): s= s[:-2]
s = s + '}'
if not compact:
s = s + ')'
return s
def from_yaml(self, yaml_string):
self.update(yaml.safe_load(yaml_string))
return self
def from_yaml_file(self, path):
with open(path, 'rt') as yamlfile:
self.update(yaml.safe_load(yamlfile))
return self
def to_yaml(self):
return yaml.dump(self.get_data())
def to_yaml_file(self, path):
with open(path, 'wt') as yamlfile:
yamlfile.write(yaml.dump(self.get_data()))
#--------
# Code for collecting the YAML files we need
ABX_YAML = os.path.join(os.path.dirname(
os.path.abspath(os.path.join(__file__))),
'abx.yaml')
def collect_yaml_files(path, stems, dirmatch=False, sidecar=False, root='/'):
"""
Collect a list of file paths to YAML files.
Does not attempt to read or interpret the files.
@path: The starting point, typically the antecedent filename.
@stems: File stem (or sequence of stems) we recognize (in priority order).
@dirmatch: Also search for stems matching the containing directory name?
@sidecar: Also search for stems matching the antecent filename's stem?
@root: Top level directory to consider (do not search above this).
"Stem" means the name with any extension after "." removed (typically,
the filetype).
"""
yaml_paths = []
if type(stems) is str:
stems = (stems,)
path = os.path.abspath(path)
path, filename = os.path.split(path)
if sidecar:
filestem = os.path.splitext(filename)[0]
sidecar_path = os.path.join(path, filestem + '.yaml')
if os.path.isfile(sidecar_path):
yaml_paths.append(sidecar_path)
while not os.path.abspath(path) == os.path.dirname(root):
path, base = os.path.split(path)
if dirmatch:
yaml_path = os.path.join(path, base, base + '.yaml')
if os.path.isfile(yaml_path):
yaml_paths.append(yaml_path)
for stem in stems:
yaml_path = os.path.join(path, base, stem + '.yaml')
if os.path.isfile(yaml_path):
yaml_paths.append(yaml_path)
yaml_paths.reverse()
return yaml_paths
def has_project_root(yaml_path):
with open(yaml_path, 'rt') as yaml_file:
data = yaml.safe_load(yaml_file)
if 'project_root' in data:
return True
else:
return False
def trim_to_project_root(yaml_paths):
for i in range(len(yaml_paths)-1,-1,-1):
if has_project_root(yaml_paths[i]):
return yaml_paths[i:]
return yaml_paths
def get_project_root(yaml_paths):
trimmed = trim_to_project_root(yaml_paths)
if trimmed:
return os.path.dirname(trimmed[0])
else:
# No project root was found!
return '/'
def combine_yaml(yaml_paths):
data = RecursiveDict()
for path in yaml_paths:
with open(path, 'rt') as yaml_file:
data.update(yaml.safe_load(yaml_file))
return data
def get_project_data(filepath):
# First, get the KitCAT data.
kitcat_paths = collect_yaml_files(filepath,
('kitcat', 'project'), dirmatch=True, sidecar=True)
kitcat_data = combine_yaml(trim_to_project_root(kitcat_paths))
kitcat_root = get_project_root(kitcat_paths)
abx_data = combine_yaml([ABX_YAML])['abx']
abx_data.update(combine_yaml(collect_yaml_files(filepath,
'abx', root=kitcat_root)))
return kitcat_root, kitcat_data, abx_data

169
pkg/abx/blender_context.py Normal file
View File

@ -0,0 +1,169 @@
# blender_context.py
"""
Contextual metadata acquired from internal values in a Blender file.
This module must be invoked from within Blender to work, as it relies on the bpy Blender API
module and the currently-open Blender file's data graph in order to work.
It collects data about scenes, objects, groups, and other datablocks in the Blender file,
as well as data encoded in text blocks in different formats. Overall file data is incorporated
into a PropertyGroup attached to the "WindowManager" object identified as 'WinMan' (normally,
it appears there is only ever one of these in a Blender file, but if there is more than one, this
is the one that will be used).
"""
import io
import bpy, bpy.app, bpy.props, bpy.utils
from bpy.app.handlers import persistent
from accumulate import UnionList, RecursiveDict
import yaml
def EnumFromList(schema, listname):
return [(e, e.capitalize(), e.capitalize()) for e in schema[listname]]
prop_types = {
'string':{
'property': bpy.props.StringProperty,
'keywords': { 'name', 'description', 'default', 'maxlen', 'options', 'subtype'},
'translate': {
'desc': ('description', None)}},
'enum': {
'property': bpy.props.EnumProperty,
'keywords': { 'items', 'name', 'description', 'default', 'options'},
'translate': {
'desc': ('description', None),
'items_from': ('items', EnumFromList)}},
'int': {
'property': bpy.props.IntProperty,
'keywords': { 'name', 'description', 'default', 'min', 'max', 'soft_min', 'soft_max',
'step', 'options', 'subtype'},
'translate': {
'desc': ('description', None)}},
'float': {
'property': bpy.props.FloatProperty,
'keywords': { 'name', 'description', 'default', 'min', 'max', 'soft_min', 'soft_max',
'step', 'options', 'subtype', 'precision', 'unit'},
'translate': {
'desc': ('description', None)}},
'bool': {
'property': bpy.props.BoolProperty,
'keywords': { 'name', 'description', 'default', 'options', 'subtype'},
'translate': {
'desc': ('description', None)}}
}
class AbxMeta(bpy.types.PropertyGroup):
"""
Metadata property group factory for attachment to Blender object types.
Definitions come from a YAML source (or default defined below).
"""
default_schema = yaml.safe_load(io.StringIO("""\
---
blender:
- id: project
type: string
level: project
name: Project Name
desc: Name of the project
maxlen: 32
- id: project_title
type: string
level: project
name: Project Title
desc: Full title for the project
maxlen: 64
- id: project_description
type: string
level: project
name: Project Description
desc: Brief description of the project
maxlen: 128
- id: project_url
type: list string
level: project
name: Project URL
desc: URL for Project home page, or comma-separated list of Project URLs
- id: level
type: enum
items_from: levels
name: Level
desc: Level of the file in the project hierarchy
levels:
- project
- series
- episode
- seq
- subseq
- camera
- shot
- element
- frame
hierarchies:
- library
- episodes
"""))
def __new__(cls, schema=default_schema):
class CustomPropertyGroup(bpy.types.PropertyGroup):
pass
for definition in schema['blender']:
# Translate and filter parameters
try:
propmap = prop_types[definition['type']]
except KeyError:
# If no 'type' specified or 'type' not found, default to string:
propmap = prop_types['string']
filtered = {}
for param in definition:
if 'translate' in propmap and param in propmap['translate']:
filter = propmap['translate'][param][1]
if callable(filter):
# Filtered translation
filtered[propmap['translate'][param][0]] = filter(schema, definition[param])
else:
# Simple translation
filtered[propmap['translate'][param][0]] = definition[param]
# Create the Blender Property object
kwargs = dict((key,filtered[key]) for key in propmap['keywords'] if key in filtered)
setattr(CustomPropertyGroup, definition['id'], propmap['property'](**kwargs))
bpy.utils.register_class(CustomPropertyGroup)
return(CustomPropertyGroup)
class BlenderContext(RecursiveDict):
"""
Dictionary accumulating data from sources within the currently-open Blender file.
"""
filepath = ''
defaults = {}
def __init__(self):
self.clear()
@classmethod
def update(cls):
try:
cls.file_metadata = bpy.data.window_managers['WinMan'].metadata
except AttributeError:
bpy.data.window_managers['WinMan'].new(FileMeta())
def clear(self):
for key in self:
del self[key]
self.update(self.defaults)

26
pkg/abx/context.py Normal file
View File

@ -0,0 +1,26 @@
# context.py
"""
Combines context sources to create AbxContext object (dictionary tree).
"""
import bpy, bpy.app, bpy.data, bpy.ops
from bpy.app.handlers import persistent
#from accumulate import UnionList, RecursiveDict
from . import file_context
if os.path.exists(bpy.data.filepath):
BlendfileContext = file_context.FileContext(bpy.data.filepath)
else:
BlendfileContext = file_context.FileContext()
# Attach a handler to keep our filepath context up to date with Blender
@persistent
def update_handler(ctxt):
BlendfileContext.update(bpy.data.filepath)
bpy.app.handlers.save_post.append(update_handler)
bpy.app.handlers.load_post.append(update_handler)
bpy.app.handlers.scene_update_post.append(update_handler)

126
pkg/abx/copy_anim.py Normal file
View File

@ -0,0 +1,126 @@
# copy_anim.py
"""
Blender Python code to copy animation between armatures or proxy armatures.
"""
import bpy, bpy.types, bpy.utils, bpy.props
#----------------------------------------
## TOOLS
# This might be moved into another module later
def copy_object_animation(sourceObj, targetObjs,
dopesheet=False, nla=False, rescale=False, scale_factor=1.0,
report=print):
"""
Copy Dope Sheet & NLA editor animation from active object to selected objects.
Most useful with armatures. Assumes bones match. Can be rescaled in the process.
From StackExchange post:
https://blender.stackexchange.com/questions/74183/how-can-i-copy-nla-tracks-from-one-armature-to-another
"""
for targetObj in targetObjs:
if targetObj.animation_data is not None:
targetObj.animation_data_clear()
targetObj.animation_data_create()
source_animation_data = sourceObj.animation_data
target_animation_data = targetObj.animation_data
# copy the dopesheet animation (active animation)
if dopesheet:
report({'INFO'}, 'Copying Dopesheet animation')
if source_animation_data.action is None:
report({'WARNING'},
"CLEARING target dope sheet - old animation saved with 'fake user'")
if target_animation_data.action is not None:
target_animation_data.action.use_fake_user = True
target_animation_data.action = None
else:
if rescale:
target_animation_data.action = copy_animation_action_with_rescale(
source_animation_data.action, scale_factor)
else:
target_animation_data.action = copy_animation_action_with_rescale(
source_animation_data.action, scale_factor)
target_animation_data.action.name = targetObj.name + 'Action'
if nla:
report({'INFO'}, 'Copying NLA strips')
if source_animation_data:
# Create new NLA tracks based on the source
for source_nla_track in source_animation_data.nla_tracks:
target_nla_track = target_animation_data.nla_tracks.new()
target_nla_track.name = source_nla_track.name
# In each track, create action strips base on the source
for source_action_strip in source_nla_track.strips:
if rescale:
new_action = copy_animation_action_with_rescale(
source_action_strip.action, scale_factor)
else:
new_action = source_action_strip.action
target_action_strip = target_nla_track.strips.new(
new_action.name,
source_action_strip.frame_start,
new_action)
# For each strip, copy the properties -- EXCEPT the ones we
# need to protect or can't copy
# introspect property names (is there a better way to do this?)
props = [p for p in dir(source_action_strip) if
not p in ('action',)
and not p.startswith('__') and not p.startswith('bl_')
and source_action_strip.is_property_set(p)
and not source_action_strip.is_property_readonly(p)
and not source_action_strip.is_property_hidden(p)]
for prop in props:
setattr(target_action_strip, prop, getattr(source_action_strip, prop))
# Adapted from reference:
# https://www.reddit.com/r/blender/comments/eu3w6m/guide_how_to_scale_a_rigify_rig/
#
def reset_armature_stretch_constraints(rig_object):
"""
Reset stretch-to constraints on an armature object - necessary after rescaling.
"""
bone_count = 0
for bone in rig_object.pose.bones:
for constraint in bone.constraints:
if constraint.type == "STRETCH_TO":
constraint.rest_length = 0
bone_count += 1
return bone_count
def rescale_animation_action_in_place(action, scale_factor):
"""
Rescale a list of animation actions by a scale factor (in-place).
"""
#for fcurve in bpy.data.actions[action].fcurves:
for fcurve in action.fcurves:
data_path = fcurve.data_path
if data_path.startswith('pose.bones[') and data_path.endswith('].location'):
for p in fcurve.keyframe_points:
p.co[1] *= scale_factor
p.handle_left[1] *= scale_factor
p.handle_right[1] *= scale_factor
return action
def copy_animation_action_with_rescale(action, scale_factor):
"""
Copy an animation action, rescaled.
"""
new_action = action.copy()
new_action.name = new_action.name[:-4]+'.rescale'
return rescale_animation_action_in_place(new_action, scale_factor)
#----------------------------------------

1266
pkg/abx/file_context.py Normal file

File diff suppressed because it is too large Load Diff

203
pkg/abx/render_profile.py Normal file
View File

@ -0,0 +1,203 @@
# render_profile.py
"""
Blender Python code to set parameters based on render profiles.
"""
import bpy
import bpy, bpy.types, bpy.utils, bpy.props
from . import std_lunatics_ink
from . import file_context
class RenderProfile(object):
render_formats = {
# VERY simplified and limited list of formats from Blender that we need:
# <API 'format'>: (<bpy file format>, <filename extension>),
'PNG': ('PNG', 'png'),
'JPG': ('JPEG', 'jpg'),
'EXR': ('OPEN_EXR_MULTILAYER', 'exr'),
'AVI': ('AVI_JPEG', 'avi'),
'MKV': ('FFMPEG', 'mkv')
}
engines = {
'bi': 'BLENDER_RENDER',
'BLENDER_RENDER': 'BLENDER_RENDER',
'BI': 'BLENDER_RENDER',
'cycles': 'CYCLES',
'CYCLES': 'CYCLES',
'bge': 'BLENDER_GAME',
'BLENDER_GAME': 'BLENDER_GAME',
'BGE': 'BLENDER_GAME',
'gl': None,
'GL': None
}
def __init__(self, fields):
# Note: Settings w/ value *None* are left unaltered
# That is, they remain whatever they were before
# If a setting isn't included in the fields, then
# the attribute will be *None*.
if 'engine' not in fields:
fields['engine'] = None
if fields['engine']=='gl':
self.viewport_render = True
self.engine = None
else:
self.viewport_render = False
if fields['engine'] in self.engines:
self.engine = self.engines[fields['engine']]
else:
self.engine = None
# Parameters which are stored as-is, without modification:
self.fps = 'fps' in fields and int(fields['fps']) or None
self.fps_skip = 'fps_skip' in fields and int(fields['fps_skip']) or None
self.fps_divisor = 'fps_divisor' in fields and float(fields['fps_divisor']) or None
self.rendersize = 'rendersize' in fields and int(fields['rendersize']) or None
self.compress = 'compress' in fields and int(fields['compress']) or None
self.format = 'format' in fields and str(fields['format']) or None
self.freestyle = 'freestyle' in fields and bool(fields['freestyle']) or None
self.antialiasing_samples = None
self.use_antialiasing = None
if 'antialias' in fields:
if fields['antialias']:
self.use_antialiasing = True
if fields['antialias'] in (5,8,11,16):
self.antialiasing_samples = str(fields['antialias'])
else:
self.use_antialiasing = False
self.use_motion_blur = None
self.motion_blur_samples = None
if 'motionblur' in fields:
if fields['motionblur']:
self.use_motion_blur = True
if type(fields['motionblur'])==int:
self.motion_blur_samples = int(fields['motionblur'])
else:
self.use_motion_blur = False
if 'framedigits' in fields:
self.framedigits = fields['framedigits']
else:
self.framedigits = 5
if 'suffix' in fields:
self.suffix = fields['suffix']
else:
self.suffix = ''
def apply(self, scene):
"""
Apply the profile settings to the given scene.
"""
if self.engine: scene.render.engine = self.engine
if self.fps: scene.render.fps = self.fps
if self.fps_skip: scene.frame_step = self.fps_skip
if self.fps_divisor: scene.render.fps_base = self.fps_divisor
if self.rendersize: scene.render.resolution_percentage = self.rendersize
if self.compress: scene.render.image_settings.compression = self.compress
if self.format:
scene.render.image_settings.file_format = self.render_formats[self.format][0]
if self.freestyle: scene.render.use_freestyle = self.freestyle
if self.use_antialiasing:
scene.render.use_antialiasing = self.use_antialiasing
if self.antialiasing_samples:
scene.render.antialiasing_samples = self.antialiasing_samples
if self.use_motion_blur:
scene.render.use_motion_blur = self.use_motion_blur
if self.motion_blur_samples:
scene.render.motion_blur_samples = self.motion_blur_samples
if self.format:
# prefix = scene.name_context.render_path
# prefix = BlendfileContext.name_contexts[scene.name_context].render_path
prefix = 'path_to_render' # We actually need to get this from NameContext
if self.suffix:
scene.render.filepath = (prefix + '-' + self.suffix + '-' +
'f'+('#'*self.framedigits) + '.' +
self.render_formats[self.format][1])
# def set_render_from_profile(scene, profile):
# if 'engine' in profile:
# if profile['engine'] == 'gl':
# pass
# elif profile['engine'] == 'bi':
# scene.render.engine = 'BLENDER_RENDER'
# elif profile['engine'] == 'cycles':
# scene.render.engine = 'CYCLES'
# elif profile['engine'] == 'bge':
# scene.render.engine = 'BLENDER_GAME'
#
# if 'fps' in profile:
# scene.render.fps = profile['fps']
#
# if 'fps_skip' in profile:
# scene.frame_step = profile['fps_skip']
#
# if 'format' in profile:
# scene.render.image_settings.file_format = render_formats[profile['format']][0]
#
# if 'freestyle' in profile:
# scene.render.use_freestyle = profile['freestyle']
#
# if 'antialias' in profile:
# if profile['antialias']:
# scene.render.use_antialiasing = True
# if profile['antialias'] in (5,8,11,16):
# scene.render.antialiasing_samples = str(profile['antialias'])
# else:
# scene.render.use_antialiasing = False
#
# if 'motionblur' in profile:
# if profile['motionblur']:
# scene.render.use_motion_blur = True
# if type(profile['motionblur'])==int:
# scene.render.motion_blur_samples = profile['motionblur']
# else:
# scene.render.use_motion_blur = False
#
# # Use Lunatics naming scheme for render target:
# if 'framedigits' in profile:
# framedigits = profile['framedigits']
# else:
# framedigits = 5
#
# if 'suffix' in profile:
# suffix = profile['suffix']
# else:
# suffix = ''
#
# if 'format' in profile:
# rdr_fmt = render_formats[profile['format']][0]
# ext = render_formats[profile['format']][1]
# else:
# rdr_fmt = 'PNG'
# ext = 'png'
#
# path = std_lunatics_ink.LunaticsShot(scene).render_path(
# suffix=suffix, framedigits=framedigits, ext=ext, rdr_fmt=rdr_fmt)
#
# scene.render.filepath = path

678
pkg/abx/std_lunatics_ink.py Normal file
View File

@ -0,0 +1,678 @@
# std_lunatics_ink.py
"""
Functions to set up the standard ink and paint compositing arrangement
for "Lunatics"
"""
import os
import bpy, bpy.props, bpy.utils
# Hard-coded default parameters:
INK_THICKNESS = 3
INK_COLOR = (0,0,0)
THRU_INK_THICKNESS = 2
THRU_INK_COLOR = (20,100,50)
# TODO: probably should have a dialog somewhere that can change these through the UI?
class LunaticsShot(object):
"""
General class for Lunatics Blender Scene data.
"""
colorcode = {
'paint': (1.00, 1.00, 1.00),
'ink': (0.75, 0.50, 0.35),
'thru': (0.35, 0.50, 0.75),
'bb': (0.35, 0.75, 0.50),
'bbthru': (0.35, 0.75, 0.75),
'sky': (0.50, 0.25, 0.75),
'compos': (0.75, 0.75, 0.75),
'output': (0.35, 0.35, 0.35)
}
def __init__(self, scene, inkthru=False, billboards=False, sepsky=False):
self.scene = scene
self.inkthru = bool(inkthru)
self.billboards = bool(billboards)
self.sepsky = bool(sepsky)
self.series_id = scene.lunaprops.series_id
self.episode_id = scene.lunaprops.episode_id
self.seq_id = scene.lunaprops.seq_id
self.block_id = scene.lunaprops.block_id
self.shot_id = scene.lunaprops.shot_id
self.cam_id = scene.lunaprops.cam_id
self.shot_name = scene.lunaprops.shot_name
self.render_root = '//../../Renders/'
@property
def fullname(self):
return self.designation + '-' + self.name
@property
def designation(self):
episode_code = "%2.2sE%2.2d" % (self.series_id, self.episode_id)
return episode_code + '-' + self.shortname
@property
def shortname(self):
desig = str(self.seq_id) + '-' + str(self.block_id)
if self.cam_id:
desig = desig + '-Cam' + str(self.cam_id)
if self.shot_id:
desig = desig + '-' + str(self.shot_id)
return desig
@property
def scene_name(self):
if self.shot_name:
return self.shortname + ' ' + self.shot_name
else:
return self.shortname
def render_path(self, suffix='', framedigits=5, ext='png', rdr_fmt='PNG'):
if suffix:
suffix = '-' + suffix
if rdr_fmt in ('AVI', 'MKV'):
path = os.path.join(self.render_root, suffix,
self.designation + suffix + '.' + ext)
else:
path = os.path.join(self.render_root, suffix, self.designation,
self.designation + suffix + '-f' + '#'*framedigits + '.' + ext)
return path
def cfg_scene(self, scene=None, thru=True, exr=True, multicam=False, role='shot'):
if not scene:
scene = self.scene
scene.name = self.scene_name
scene.render.filepath = self.render_path()
#os.path.join(self.render_root, 'PNG', self.designation, self.designation + '-f#####.png')
scene.render.image_settings.file_format='PNG'
scene.render.image_settings.compression = 50
scene.render.image_settings.color_mode = 'RGB'
scene.render.use_freestyle = True
# Create Paint & Ink Render Layers
for rlayer in scene.render.layers:
rlayer.name = '~' + rlayer.name
rlayer.use = False
# Rename & turn off existing layers (but don't delete, in case they were wanted)
scene.render.layers.new('Paint')
self.cfg_paint(scene.render.layers['Paint'])
scene.render.layers.new('Ink')
self.cfg_ink(scene.render.layers['Ink'],
thickness=INK_THICKNESS, color=INK_COLOR)
if self.inkthru:
scene.render.layers.new('Ink-Thru')
self.cfg_ink(scene.render.layers['Ink-Thru'],
thickness=THRU_INK_THICKNESS, color=THRU_INK_COLOR)
if self.billboards:
scene.render.layers.new('BB-Alpha')
self.cfg_bbalpha(scene.render.layers['BB-Alpha'])
scene.render.layers.new('BB-Mat')
self.cfg_bbmat(scene.render.layers['BB-Mat'], thru=False)
if self.billboards and self.inkthru:
scene.render.layers.new('BB-Mat-Thru')
self.cfg_bbmat(scene.render.layers['BB-Mat-Thru'], thru=True)
if self.sepsky:
scene.render.layers.new('Sky')
self.cfg_sky(scene.render.layers['Sky'])
self.cfg_nodes(scene)
def _new_rlayer_in(self, name, scene, rlayer, location, color):
tree = scene.node_tree
rlayer_in = tree.nodes.new('CompositorNodeRLayers')
rlayer_in.name = '_'.join([n.lower() for n in name.split('-')])+'_in'
rlayer_in.label = name+'-In'
rlayer_in.scene = scene
rlayer_in.layer = rlayer
rlayer_in.color = color
rlayer_in.use_custom_color = True
rlayer_in.location = location
return rlayer_in
def cfg_nodes(self, scene):
# Create Compositing Node Tree
scene.use_nodes = True
tree = scene.node_tree
# clear default nodes
for node in tree.nodes:
tree.nodes.remove(node)
# Paint RenderLayer Nodes
paint_in = self._new_rlayer_in('Paint', scene, 'Paint',
(0,1720), self.colorcode['paint'])
if self.sepsky:
sky_in = self._new_rlayer_in('Sky', scene, 'Sky',
(0, 1200), self.colorcode['sky'])
# Configure EXR format
exr_paint = tree.nodes.new('CompositorNodeOutputFile')
exr_paint.name = 'exr_paint'
exr_paint.label = 'Paint EXR'
exr_paint.location = (300,1215)
exr_paint.color = self.colorcode['paint']
exr_paint.use_custom_color = True
exr_paint.format.file_format = 'OPEN_EXR_MULTILAYER'
exr_paint.format.color_mode = 'RGBA'
exr_paint.format.color_depth = '16'
exr_paint.format.exr_codec = 'ZIP'
exr_paint.base_path = os.path.join(self.render_root, 'EXR',
self.designation, self.designation + '-Paint-f#####' + '.exr')
if 'Image' in exr_paint.layer_slots:
exr_paint.layer_slots.remove(exr_paint.inputs['Image'])
# Create EXR layers and connect to render passes
rpasses = ['Image', 'Depth', 'Normal', 'Vector',
'Spec', 'Shadow','Reflect','Emit']
for rpass in rpasses:
exr_paint.layer_slots.new(rpass)
tree.links.new(paint_in.outputs[rpass], exr_paint.inputs[rpass])
if self.sepsky:
exr_paint.layer_slots.new('Sky')
tree.links.new(sky_in.outputs['Image'], exr_paint.inputs['Sky'])
# Ink RenderLayer Nodes
ink_in = self._new_rlayer_in('Ink', scene, 'Ink',
(590, 1275), self.colorcode['ink'])
if self.inkthru:
thru_in = self._new_rlayer_in('Thru', scene, 'Ink-Thru',
(590, 990), self.colorcode['thru'])
if self.billboards:
bb_in = self._new_rlayer_in('BB', scene, 'BB-Alpha',
(0, 870), self.colorcode['bb'])
bb_mat = self._new_rlayer_in('BB-Mat', scene, 'BB-Mat',
(0, 590), self.colorcode['bb'])
if self.inkthru and self.billboards:
bb_mat_thru = self._new_rlayer_in('BB-Mat-Thru', scene, 'BB-Mat-Thru',
(0, 280), self.colorcode['bbthru'])
# Ink EXR
exr_ink = tree.nodes.new('CompositorNodeOutputFile')
exr_ink.name = 'exr_ink'
exr_ink.label = 'Ink EXR'
exr_ink.location = (1150,700)
exr_ink.color = self.colorcode['ink']
exr_ink.use_custom_color = True
exr_ink.format.file_format = 'OPEN_EXR_MULTILAYER'
exr_ink.format.color_mode = 'RGBA'
exr_ink.format.color_depth = '16'
exr_ink.format.exr_codec = 'ZIP'
exr_ink.base_path = os.path.join(self.render_root, 'EXR',
self.designation, self.designation + '-Ink-f#####' + '.exr')
# Create EXR Ink layers and connect
if 'Image' in exr_ink.layer_slots:
exr_ink.layer_slots.remove(exr_ink.inputs['Image'])
exr_ink.layer_slots.new('Ink')
tree.links.new(ink_in.outputs['Image'], exr_ink.inputs['Ink'])
if self.inkthru:
exr_ink.layer_slots.new('Ink-Thru')
tree.links.new(thru_in.outputs['Image'], exr_ink.inputs['Ink-Thru'])
if self.billboards:
exr_ink.layer_slots.new('BB-Alpha')
tree.links.new(bb_in.outputs['Alpha'], exr_ink.inputs['BB-Alpha'])
exr_ink.layer_slots.new('BB-Mat')
tree.links.new(bb_mat.outputs['IndexMA'], exr_ink.inputs['BB-Mat'])
if self.inkthru and self.billboards:
exr_ink.layer_slots.new('BB-Mat-Thru')
tree.links.new(bb_mat_thru.outputs['IndexMA'], exr_ink.inputs['BB-Mat-Thru'])
# Preview Compositing
mix_shadow = tree.nodes.new('CompositorNodeMixRGB')
mix_shadow.name = 'mix_shadow'
mix_shadow.label = 'Mix-Shadow'
mix_shadow.location = (510,1820)
mix_shadow.color = self.colorcode['compos']
mix_shadow.use_custom_color = True
mix_shadow.blend_type = 'MULTIPLY'
mix_shadow.inputs['Fac'].default_value = 0.6
mix_shadow.use_clamp = True
tree.links.new(paint_in.outputs['Image'], mix_shadow.inputs[1])
tree.links.new(paint_in.outputs['Shadow'], mix_shadow.inputs[2])
mix_reflect = tree.nodes.new('CompositorNodeMixRGB')
mix_reflect.name = 'mix_reflect'
mix_reflect.label = 'Mix-Reflect'
mix_reflect.location = (910, 1620)
mix_reflect.color = self.colorcode['compos']
mix_reflect.use_custom_color = True
mix_reflect.blend_type = 'ADD'
mix_reflect.inputs['Fac'].default_value = 1.1
mix_reflect.use_clamp = True
tree.links.new(paint_in.outputs['Reflect'], mix_reflect.inputs[2])
mix_emit = tree.nodes.new('CompositorNodeMixRGB')
mix_emit.name = 'mix_emit'
mix_emit.label = 'Mix-Emit'
mix_emit.location = (1110, 1520)
mix_emit.blend_type = 'ADD'
mix_emit.inputs['Fac'].default_value = 1.1
mix_emit.use_clamp = True
tree.links.new(mix_reflect.outputs['Image'], mix_emit.inputs[1])
tree.links.new(paint_in.outputs['Emit'], mix_emit.inputs[2])
if self.sepsky:
sky_mix = tree.nodes.new('CompositorNodeMixRGB')
sky_mix.name = 'sky_mix'
sky_mix.label = 'Sky Mix'
sky_mix.location = (710,1720)
sky_mix.color = self.colorcode['sky']
sky_mix.use_custom_color = True
sky_mix.blend_type = 'MIX'
sky_mix.use_clamp = True
tree.links.new(sky_in.outputs['Image'], sky_mix.inputs[1])
tree.links.new(paint_in.outputs['Alpha'], sky_mix.inputs['Fac'])
tree.links.new(mix_shadow.outputs['Image'], sky_mix.inputs[2])
tree.links.new(sky_mix.outputs['Image'], mix_reflect.inputs[1])
else:
tree.links.new(mix_shadow.outputs['Image'], mix_reflect.inputs[1])
if self.billboards:
mat_idx = tree.nodes.new('CompositorNodeIDMask')
mat_idx.name = "mat_idx"
mat_idx.label = "BB-ID"
mat_idx.location = (260, 670)
mat_idx.index = 1
mat_idx.use_antialiasing = True
mat_idx.color = self.colorcode['bb']
mat_idx.use_custom_color = True
tree.links.new(bb_mat.outputs['IndexMA'], mat_idx.inputs['ID value'])
combine_bb_ma = tree.nodes.new('CompositorNodeMath')
combine_bb_ma.name = 'combine_bb_ma'
combine_bb_ma.label = 'Material x BB'
combine_bb_ma.location = (440,670)
combine_bb_ma.color = self.colorcode['bb']
combine_bb_ma.use_custom_color = True
combine_bb_ma.operation = 'MULTIPLY'
combine_bb_ma.use_clamp = True
tree.links.new(mat_idx.outputs['Alpha'], combine_bb_ma.inputs[0])
tree.links.new(bb_in.outputs['Alpha'], combine_bb_ma.inputs[1])
invert_bb_mask = tree.nodes.new('CompositorNodeInvert')
invert_bb_mask.name = 'invert_bb_mask'
invert_bb_mask.label = 'Invert Mask'
invert_bb_mask.location = (650,670)
invert_bb_mask.color = self.colorcode['bb']
invert_bb_mask.use_custom_color = True
invert_bb_mask.invert_rgb = True
tree.links.new(combine_bb_ma.outputs['Value'], invert_bb_mask.inputs['Color'])
bb_ink_mask = tree.nodes.new('CompositorNodeMath')
bb_ink_mask.name = 'bb_ink_mask'
bb_ink_mask.label = 'BB Ink Mask'
bb_ink_mask.location = (1150,1315)
bb_ink_mask.color = self.colorcode['bb']
bb_ink_mask.use_custom_color = True
bb_ink_mask.operation = 'MULTIPLY'
bb_ink_mask.use_clamp = True
tree.links.new(invert_bb_mask.outputs['Color'], bb_ink_mask.inputs[0])
blur_ink = tree.nodes.new('CompositorNodeBlur')
blur_ink.name = 'blur_ink'
blur_ink.label = 'Blur-Ink'
blur_ink.location = (1620, 1110)
blur_ink.color = self.colorcode['ink']
blur_ink.use_custom_color = True
blur_ink.filter_type = 'FAST_GAUSS'
blur_ink.size_x = 1.0
blur_ink.size_y = 1.0
blur_ink.use_extended_bounds = False
blur_ink.inputs['Size'].default_value = 1.0
if self.inkthru:
merge_ink_ao = tree.nodes.new('CompositorNodeAlphaOver')
merge_ink_ao.name = 'merge_ink'
merge_ink_ao.label = 'Merge-Ink'
merge_ink_ao.location = (1150,910)
merge_ink_ao.color = self.colorcode['thru']
merge_ink_ao.use_custom_color = True
merge_ink_ao.use_premultiply = False
merge_ink_ao.premul = 0.0
merge_ink_ao.inputs['Fac'].default_value = 1.0
tree.links.new(ink_in.outputs['Image'], merge_ink_ao.inputs[1])
tree.links.new(thru_in.outputs['Image'], merge_ink_ao.inputs[2])
tree.links.new(merge_ink_ao.outputs['Image'], blur_ink.inputs['Image'])
else:
tree.links.new(ink_in.outputs['Image'], blur_ink.inputs['Image'])
overlay_ink = tree.nodes.new('CompositorNodeAlphaOver')
overlay_ink.name = 'Overlay Ink'
overlay_ink.label = 'Overlay Ink'
overlay_ink.location = (1820,1315)
overlay_ink.color = self.colorcode['compos']
overlay_ink.use_custom_color = True
overlay_ink.use_premultiply = False
overlay_ink.premul = 0.0
overlay_ink.inputs['Fac'].default_value = 1.0
tree.links.new(mix_emit.outputs['Image'], overlay_ink.inputs[1])
tree.links.new(blur_ink.outputs['Image'], overlay_ink.inputs[2])
if self.billboards:
tree.links.new(ink_in.outputs['Alpha'], bb_ink_mask.inputs[1])
tree.links.new(bb_ink_mask.outputs['Value'], overlay_ink.inputs['Fac'])
if self.inkthru and self.billboards:
mat_idx_thru = tree.nodes.new('CompositorNodeIDMask')
mat_idx_thru.name = "mat_idx_thru"
mat_idx_thru.label = "BB-ID-Thru"
mat_idx_thru.location = (260, 425)
mat_idx_thru.index = 1
mat_idx_thru.use_antialiasing = True
mat_idx_thru.color = self.colorcode['bbthru']
mat_idx_thru.use_custom_color = True
tree.links.new(bb_mat_thru.outputs['IndexMA'], mat_idx_thru.inputs['ID value'])
combine_bbthru_ma = tree.nodes.new('CompositorNodeMath')
combine_bbthru_ma.name = 'combine_bbthru_ma'
combine_bbthru_ma.label = 'Material x BB-Thru'
combine_bbthru_ma.location = (440,425)
combine_bbthru_ma.color = self.colorcode['bbthru']
combine_bbthru_ma.use_custom_color = True
combine_bbthru_ma.operation = 'MULTIPLY'
combine_bbthru_ma.use_clamp = True
tree.links.new(mat_idx_thru.outputs['Alpha'], combine_bbthru_ma.inputs[0])
tree.links.new(bb_in.outputs['Alpha'], combine_bbthru_ma.inputs[1])
invert_bbthru_mask = tree.nodes.new('CompositorNodeInvert')
invert_bbthru_mask.name = 'invert_bbthru_mask'
invert_bbthru_mask.label = 'Invert Mask'
invert_bbthru_mask.location = (650,425)
invert_bbthru_mask.color = self.colorcode['bbthru']
invert_bbthru_mask.use_custom_color = True
invert_bbthru_mask.invert_rgb = True
tree.links.new(combine_bbthru_ma.outputs['Value'], invert_bbthru_mask.inputs['Color'])
bb_thru_mask = tree.nodes.new('CompositorNodeMath')
bb_thru_mask.name = 'bb_thru_mask'
bb_thru_mask.label = 'BB Ink Thru Mask'
bb_thru_mask.location = (1150,1115)
bb_thru_mask.color = self.colorcode['bbthru']
bb_thru_mask.use_custom_color = True
bb_thru_mask.operation = 'MULTIPLY'
bb_thru_mask.use_clamp = True
tree.links.new(thru_in.outputs['Alpha'], bb_thru_mask.inputs[0])
tree.links.new(invert_bbthru_mask.outputs['Color'], bb_thru_mask.inputs[1])
merge_bb_ink_masks = tree.nodes.new('CompositorNodeMath')
merge_bb_ink_masks.name = 'merge_bb_ink_masks'
merge_bb_ink_masks.label = 'Merge BB Ink Masks'
merge_bb_ink_masks.location = (1415, 1215)
merge_bb_ink_masks.color = self.colorcode['bbthru']
merge_bb_ink_masks.use_custom_color = True
merge_bb_ink_masks.operation = 'ADD'
merge_bb_ink_masks.use_clamp = True
tree.links.new(bb_ink_mask.outputs['Value'], merge_bb_ink_masks.inputs[0])
tree.links.new(bb_thru_mask.outputs['Value'], merge_bb_ink_masks.inputs[1])
tree.links.new(merge_bb_ink_masks.outputs['Value'], overlay_ink.inputs['Fac'])
composite = tree.nodes.new('CompositorNodeComposite')
composite.name = 'Composite'
composite.label = 'Preview Render'
composite.location = (2050,1215)
composite.color = self.colorcode['output']
composite.use_custom_color = True
composite.use_alpha = True
composite.inputs['Alpha'].default_value = 1.0
composite.inputs['Z'].default_value = 1.0
tree.links.new(overlay_ink.outputs['Image'], composite.inputs['Image'])
def _cfg_renderlayer(self, rlayer,
includes=False, passes=False, excludes=False,
layers=range(20)):
# Utility to set all the includes and passes on or off, initially
# Weird Includes (we never use these -- always have to turn these on explicitly)
rlayer.use_zmask = False
rlayer.invert_zmask = False
rlayer.use_all_z = False
# Includes
rlayer.use_solid = includes
rlayer.use_halo = includes
rlayer.use_ztransp = includes
rlayer.use_sky = includes
rlayer.use_edge_enhance = includes
rlayer.use_strand = includes
rlayer.use_freestyle = includes
# Passes
rlayer.use_pass_combined = passes
rlayer.use_pass_z = passes
rlayer.use_pass_vector = passes
rlayer.use_pass_normal = passes
rlayer.use_pass_uv = passes
rlayer.use_pass_mist = passes
rlayer.use_pass_object_index = passes
rlayer.use_pass_material_index = passes
rlayer.use_pass_color = passes
rlayer.use_pass_diffuse = passes
rlayer.use_pass_specular = passes
rlayer.use_pass_shadow = passes
rlayer.use_pass_emit = passes
rlayer.use_pass_ambient_occlusion = passes
rlayer.use_pass_environment = passes
rlayer.use_pass_indirect = passes
rlayer.use_pass_reflection = passes
rlayer.use_pass_refraction = passes
# Exclusions
rlayer.exclude_specular = excludes
rlayer.exclude_shadow = excludes
rlayer.exclude_emit = excludes
rlayer.exclude_ambient_occlusion = excludes
rlayer.exclude_environment = excludes
rlayer.exclude_indirect = excludes
rlayer.exclude_reflection = excludes
rlayer.exclude_refraction = excludes
for i in range(20):
if i in layers:
rlayer.layers[i] = True
else:
rlayer.layers[i] = False
def cfg_paint(self, paint_layer, name="Paint"):
self._cfg_renderlayer(paint_layer,
includes=True, passes=False, excludes=False,
layers = (0,1,2,3,4, 5,6,7, 10,11,12,13,14))
# Includes
if self.sepsky:
paint_layer.use_sky = False
paint_layer.use_freestyle = False
# Passes
paint_layer.use_pass_combined = True
paint_layer.use_pass_z = True
paint_layer.use_pass_vector = True
paint_layer.use_pass_normal = True
paint_layer.use_pass_shadow = True
paint_layer.exclude_shadow = True
paint_layer.use_pass_emit = True
paint_layer.exclude_emit = True
paint_layer.use_pass_specular = True
paint_layer.exclude_specular = True
paint_layer.use_pass_reflection = True
paint_layer.exclude_reflection = True
def cfg_bbalpha(self, bb_render_layer):
self._cfg_renderlayer(bb_render_layer,
includes=False, passes=False, excludes=False,
layers=(5,6, 14))
# Includes
bb_render_layer.use_solid = True
bb_render_layer.use_ztransp = True
# Passes
bb_render_layer.use_pass_combined = True
def cfg_bbmat(self, bb_mat_layer, thru=False):
self._cfg_renderlayer(bb_mat_layer,
includes=False, passes=False, excludes=False,
layers=(0,1,2,3, 5,6,7, 10,11,12,13,14, 15,16))
# Includes
bb_mat_layer.use_solid = True
bb_mat_layer.use_ztransp = True
# Passes
bb_mat_layer.use_pass_combined = True
bb_mat_layer.use_pass_material_index = True
if not thru:
bb_mat_layer.layers[4] = True
def cfg_sky(self, sky_render_layer):
self._cfg_renderlayer(sky_render_layer,
includes=False, passes=False, excludes=False,
layers=(0,1,2,3,4, 5,6,7, 10,11,12,13,14))
# Includes
sky_render_layer.use_sky = True
# Passes
sky_render_layer.use_pass_combined = True
def cfg_ink(self, ink_layer, name="Ink", thickness=3, color=(0,0,0)):
self._cfg_renderlayer(ink_layer,
includes=False, passes=False, excludes=False,
layers=(0,1,2,3, 5,6,7, 10,11,12,13, 15,16))
# Includes
ink_layer.use_freestyle = True
# Passes
ink_layer.use_pass_combined = True
# Freestyle
ink_layer.freestyle_settings.crease_angle = 2.617944
ink_layer.freestyle_settings.use_smoothness = True
ink_layer.freestyle_settings.use_culling = True
if len(ink_layer.freestyle_settings.linesets)>0:
ink_layer.freestyle_settings.linesets[0].name = name
else:
ink_layer.freestyle_settings.linesets.new(name)
lineset = ink_layer.freestyle_settings.linesets[name]
self.cfg_lineset(lineset, thickness, color)
# Turn on the transparency layer for the regular ink:
if ink_layer.name!='Ink-Thru':
ink_layer.layers[4] = True
def cfg_lineset(self, lineset, thickness=3, color=(0,0,0)):
"""
Configure the lineset.
"""
#lineset.name = 'NormalInk'
# Selection options
lineset.select_by_visibility = True
lineset.select_by_edge_types = True
lineset.select_by_image_border = True
lineset.select_by_face_marks = False
lineset.select_by_group = True
# Visibility Option
lineset.visibility = 'VISIBLE'
# Edge Type Options
lineset.edge_type_negation = 'INCLUSIVE'
lineset.edge_type_combination = 'OR'
lineset.select_silhouette = True
lineset.select_border = True
lineset.select_contour = True
lineset.select_crease = True
lineset.select_edge_mark = True
lineset.select_external_contour = True
# No Freestyle Group (If it exists)
if 'No Freestyle' in bpy.data.groups:
lineset.select_by_group = True
lineset.group = bpy.data.groups['No Freestyle']
lineset.group_negation = 'EXCLUSIVE'
else:
lineset.select_by_group = False
# Basic Ink linestyle:
if 'Ink' in bpy.data.linestyles:
lineset.linestyle = bpy.data.linestyles['Ink']
else:
lineset.linestyle.name = 'Ink'
self.cfg_linestyle(lineset.linestyle, thickness, color)
def cfg_linestyle(self, linestyle, thickness=INK_THICKNESS, color=INK_COLOR):
# These are the only changeable parameters:
linestyle.color = color
linestyle.thickness = thickness
# The rest of this function just sets a common fixed style for "Lunatics!"
linestyle.alpha = 1.0
linestyle.thickness_position = 'CENTER'
linestyle.use_chaining = True
linestyle.chaining = 'PLAIN'
linestyle.use_same_object = True
linestyle.caps = 'ROUND'
# ADD THE ALONG-STROKE MODIFIER CURVE
# TODO: try using the .new(type=...) idiom to see if it works?
# This probably needs the scene context set?
# bpy.ops.scene.freestyle_thickness_modifier_add(type='ALONG_STROKE')
linestyle.thickness_modifiers.new(type='ALONG_STROKE', name='taper')
linestyle.thickness_modifiers['taper'].blend = 'MULTIPLY'
linestyle.thickness_modifiers['taper'].mapping = 'CURVE'
# These are defaults, so maybe unnecessary?
linestyle.thickness_modifiers['taper'].influence = 1.0
linestyle.thickness_modifiers['taper'].invert = False
linestyle.thickness_modifiers['taper'].value_min = 0.0
linestyle.thickness_modifiers['taper'].value_max = 1.0
# This API is awful, but what it has to do is to change the location of the first two
# points (which can't be removed), then add a third point. Then update to pick up the
# changes:
linestyle.thickness_modifiers['taper'].curve.curves[0].points[0].location = (0.0,0.0)
linestyle.thickness_modifiers['taper'].curve.curves[0].points[1].location = (0.5,1.0)
linestyle.thickness_modifiers['taper'].curve.curves[0].points.new(1.0,0.0)
linestyle.thickness_modifiers['taper'].curve.update()

View File

@ -0,0 +1,13 @@
#script to run:
SCRIPT="/project/terry/Dev/eclipse-workspace/ABX/abx/abx_ui.py"
#path to the PyDev folder that contains a file named pydevd.py:
#PYDEVD_PATH='/home/terry/.eclipse/360744294_linux_gtk_x86_64/plugins/org.python.pydev.core_7.3.0.201908161924/pysrc/'
PYDEVD_PATH='/home/terry/.eclipse/360744286_linux_gtk_x86_64/plugins/org.python.pydev.core_8.3.0.202104101217/pysrc/'
#PYDEVD_PATH='/home/terry/.config/blender/2.79/scripts/addons/modules/pydev_debug.py'
import pydev_debug as pydev #pydev_debug.py is in a folder from Blender PYTHONPATH
pydev.debug(SCRIPT, PYDEVD_PATH, trace = True)

View File

@ -0,0 +1,15 @@
#
# This testrunner is based on a Stack Overflow answer:
# https://stackoverflow.com/questions/1732438/how-do-i-run-all-python-unit-tests-in-a-directory
#
ABX_PATH = '/project/terry/Dev/Git/abx'
import os, unittest
loader = unittest.TestLoader()
start_dir = os.path.join(ABX_PATH, 'tests')
suite = loader.discover(start_dir)
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -16,6 +16,51 @@ sys.path.append(os.path.normpath(os.path.join(__file__, '..', '..')))
from abx import file_context
class FileContext_Utilities_Tests(unittest.TestCase):
"""
Test utility functions and classes that FileContext features depend on.
"""
def test_enum_class_basics(self):
my_enum = file_context.Enum('ZERO', 'ONE', 'TWO', 'THREE')
self.assertEqual(my_enum.number(my_enum.ZERO), 0)
self.assertEqual(my_enum.number(0), 0)
self.assertEqual(my_enum.number('ZERO'), 0)
self.assertEqual(my_enum.name(my_enum.ZERO), 'ZERO')
self.assertEqual(my_enum.name(0), 'ZERO')
self.assertEqual(my_enum.name('ZERO'), 'ZERO')
self.assertEqual(my_enum.ONE, 1)
self.assertEqual(my_enum.name(my_enum.TWO), 'TWO')
self.assertEqual(my_enum.name(2), 'TWO')
self.assertEqual(my_enum.number('THREE'), 3)
def test_enum_class_blender_enum_options(self):
my_options = file_context.Enum(
('ZP', 'ZeroPoint', 'Zero Point'),
('FP', 'FirstPoint', 'First Point'),
('LP', 'LastPoint', 'Last Point'))
#print("dir(my_options) = ", dir(my_options))
self.assertEqual(my_options.number(my_options.ZP), 0)
self.assertEqual(my_options.number(my_options.FP), 1)
self.assertEqual(my_options.name(my_options.ZP), 'ZP')
self.assertEqual(my_options.name(1), 'FP')
self.assertEqual(my_options.name('LP'), 'LP')
self.assertEqual(my_options[my_options.number('FP')],
('FP', 'FirstPoint', 'First Point'))
self.assertListEqual(my_options.options,
[('ZP', 'ZeroPoint', 'Zero Point'),
('FP', 'FirstPoint', 'First Point'),
('LP', 'LastPoint', 'Last Point')])
class FileContext_NameSchema_Interface_Tests(unittest.TestCase):
"""
Test the interfaces presented by NameSchema.
@ -279,6 +324,46 @@ class FileContext_Parser_UnitTests(unittest.TestCase):
'rank': 'sequence'}
)
def test_parsing_filenames_w_fallback_parser(self):
abx_fallback_parser = file_context.NameParsers['abx_fallback']()
data = abx_fallback_parser('S1E01-SF-4-SoyuzDMInt-cam.blend', None)
self.assertDictEqual(data[1],
{'filetype': 'blend',
'role': 'cam',
'comment': None,
'title': 'S1E01-SF-4-SoyuzDMInt',
'code': 'S1e01Sf4Soyuzdmint'
})
data = abx_fallback_parser('S1E01-SF-4-SoyuzDMInt-cam~~2021-01.blend', None)
self.assertDictEqual(data[1],
{'filetype': 'blend',
'role': 'cam',
'comment': '2021-01',
'title': 'S1E01-SF-4-SoyuzDMInt',
'code': 'S1e01Sf4Soyuzdmint'
})
data = abx_fallback_parser('S1E02-MM-MediaMontage-compos.blend', None)
self.assertDictEqual(data[1],
{'filetype':'blend',
'role':'compos',
'comment': None,
'title': 'S1E02-MM-MediaMontage',
'code': 'S1e02MmMediamontage'
})
data = abx_fallback_parser('S1E01-PC-PressConference', None)
self.assertDictEqual(data[1],
{'filetype': None,
'role': None,
'comment': None,
'title': 'S1E01-PC-PressConference',
'code': 'S1e01PcPressconference'
})
class FileContext_Implementation_UnitTests(unittest.TestCase):
TESTDATA = os.path.abspath(
@ -290,6 +375,11 @@ class FileContext_Implementation_UnitTests(unittest.TestCase):
def test_filecontext_finds_and_loads_file(self):
fc = file_context.FileContext(self.TESTPATH)
# print('\ntest_filecontext_finds_and_loads_file')
# print(fc.get_log_text('INFO'))
# print(dir(self))
self.assertEqual(fc.filename, 'A.001-LP-1-BeginningOfEnd-anim.txt')
self.assertEqual(fc.root, os.path.join(self.TESTDATA, 'myproject'))
self.assertListEqual(fc.folders,
@ -311,6 +401,15 @@ class FileContext_Implementation_UnitTests(unittest.TestCase):
self.assertEqual(fc.role, 'anim')
self.assertEqual(fc.title, 'BeginningOfEnd')
self.assertEqual(fc.comment, None)
def test_filecontext_abx_fields_include_default(self):
fc0 = file_context.FileContext()
fc1 = file_context.FileContext('')
fc2 = file_context.FileContext(self.TESTPATH)
for fc in (fc0, fc1, fc2):
self.assertIn('render_profiles', fc.abx_fields)
class FileContext_API_UnitTests(unittest.TestCase):
@ -416,21 +515,36 @@ class FileContext_FailOver_Tests(unittest.TestCase):
'yaminimal', 'Episodes', 'Ae1-Void', 'Seq', 'VN-VagueName',
'Ae1-VN-1-VoidOfData-anim.txt')
def test_filecontext_finds_default_yaml(self):
self.assertIn('abx_default', file_context.DEFAULT_YAML)
def test_filecontext_no_project_path(self):
fc = file_context.FileContext()
self.assertFalse(fc.file_exists)
self.assertFalse(fc.folder_exists)
self.assertIn('abx_default', fc.provided_data)
# What to test?
# The main thing is that it doesn't crash.
def test_filecontext_failover_empty_project(self):
fc = file_context.FileContext(self.TEST_EMPTY_PROJECT)
self.assertFalse(fc.file_exists)
self.assertTrue(fc.folder_exists)
self.assertIn('abx_default', fc.provided_data)
def test_filecontext_failover_nonexisting_file(self):
fc = file_context.FileContext(self.TEST_NONEXISTENT_PATH)
self.assertFalse(fc.file_exists)
self.assertFalse(fc.folder_exists)
self.assertIn('abx_default', fc.provided_data)
def test_filecontext_failover_no_yaml(self):
fc = file_context.FileContext(self.TEST_NO_YAML)
self.assertIn('abx_default', fc.provided_data)
# It finds the backstop root YAML in the testdata:
self.assertEqual(fc.root, self.TESTDATA)
def test_filecontext_failover_minimal_yaml(self):
fc = file_context.FileContext(self.TEST_MINIMAL_YAML)
self.assertIn('abx_default', fc.provided_data)

View File

@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""
Test the render_profile module.
This has to be run from within Blender.
See:
TestInBlender.py (injector script - call this to run the tests)
TestInBlender_bpy.py (injected test-runner script)
"""
import unittest, os, textwrap
import yaml
import sys
print("__file__ = ", __file__)
sys.path.append(os.path.normpath(os.path.join(__file__, '..', '..')))
TESTDATA = os.path.join(os.path.abspath(__file__), '..', 'testdata')
TESTPATH = os.path.join(TESTDATA, 'myproject', 'Episodes', 'A.001-Pilot',
'Seq', 'LP-LastPoint', 'A.001-LP-1-BeginningOfEnd-anim.txt')
import bpy
import abx
from abx import file_context
from abx import render_profile
class TestRenderProfile_Utils(unittest.TestCase):
def test_bpy_is_present(self):
self.assertTrue(abx.blender_present)
class TestRenderProfile_Implementation(unittest.TestCase):
TESTDATA = os.path.abspath(os.path.join(__file__, '..', 'testdata'))
TESTPATH = os.path.join(TESTDATA, 'myproject', 'Episodes', 'A.001-Pilot',
'Seq', 'LP-LastPoint', 'A.001-LP-1-BeginningOfEnd-anim.blend')
def setUp(self):
self.fc0 = file_context.FileContext(bpy.data.filepath)
self.fc1 = file_context.FileContext(self.TESTPATH)
self.scene = bpy.context.scene
def test_blendfile_context(self):
self.assertEqual(self.fc0.filename, None)
self.assertEqual(self.fc1.filename,
'A.001-LP-1-BeginningOfEnd-anim.blend')
def test_abx_data_retrieved_defaults(self):
self.assertIn('render_profiles', self.fc0.abx_fields)
def test_abx_data_retrieved_file(self):
self.assertIn('render_profiles', self.fc1.abx_fields)
def test_abx_data_default_full_profile_correct(self):
FullProfile = render_profile.RenderProfile(
self.fc0.abx_fields['render_profiles']['full'])
FullProfile.apply(self.scene)
self.assertEqual(self.scene.render.fps, 30)
self.assertEqual(self.scene.render.fps_base, 1.0)
self.assertTrue(self.scene.render.use_motion_blur)
self.assertTrue(self.scene.render.use_antialiasing)
self.assertEqual(self.scene.render.antialiasing_samples, '8')
self.assertEqual(self.scene.render.resolution_percentage, 100)
self.assertEqual(self.scene.render.image_settings.compression, 50)