5e24e06796
After discovering that sub-packages in Blender add-ons don't work (this seems to be very broken!), I replaced the parsers subpackage with a parsers.py module (almost as simple as concatenating the source files in the package). I may be removing this name-parsing system from ABX entirely, soon, possibly moving it into KitCAT.
602 lines
19 KiB
Python
602 lines
19 KiB
Python
# 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 bpy.app.handlers import persistent
|
|
|
|
from . import file_context
|
|
from . import copy_anim
|
|
from . import ink_paint
|
|
from . import render_profile
|
|
|
|
|
|
# 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):
|
|
"""
|
|
Specific function to retrieve enumerated values for sequence units.
|
|
|
|
NOTE: due to be replaced by file_context features.
|
|
"""
|
|
#
|
|
# 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
|
|
|
|
class ProjectProperties(bpy.types.PropertyGroup):
|
|
"""
|
|
Properties of the scene (and file), based on project context information.
|
|
"""
|
|
name_context_id = bpy.props.StringProperty(options={'HIDDEN', 'LIBRARY_EDITABLE'})
|
|
|
|
@property
|
|
def name_context(self):
|
|
if self.name_context_id in BlendFile.name_contexts:
|
|
return BlendFile.name_contexts[self.name_context_id]
|
|
else:
|
|
name_context = BlendFile.new_name_context()
|
|
self.name_context_id = str(id(name_context))
|
|
return name_context
|
|
|
|
render_folder = bpy.props.StringProperty(
|
|
name = 'Render Folder',
|
|
description = 'Path to the render folder (without filename)',
|
|
subtype = 'FILE_PATH')
|
|
|
|
render_prefix = bpy.props.StringProperty(
|
|
name = 'Render Prefix',
|
|
description = 'Prefix used to create filenames used in rendering',
|
|
subtype = 'FILE_NAME')
|
|
|
|
designation = bpy.props.StringProperty(
|
|
name = 'Designation',
|
|
description = 'Short code for this Blender scene only',
|
|
maxlen=16)
|
|
|
|
role = bpy.props.EnumProperty(
|
|
name = 'Role',
|
|
description = 'Role of this scene in project',
|
|
items = (('cam', 'Camera', 'Camera direction and render to EXR'),
|
|
('compos', 'Compositing', 'Post-compositing from EXR'),
|
|
('anim', 'Animation', 'Character animation scene'),
|
|
('mech', 'Mechanical', 'Mech animation scene'),
|
|
('asset', 'Asset', 'Project model assets'),
|
|
('prop', 'Prop', 'Stage property asset'),
|
|
('char', 'Character', 'Character model asset'),
|
|
('prac', 'Practical', 'Practical property - rigged prop')),
|
|
default='cam')
|
|
|
|
frame_start = bpy.props.IntProperty(
|
|
name = 'Start',
|
|
description = "Start frame of shot (used to set the render start frame)",
|
|
soft_min = 0, soft_max=10000)
|
|
|
|
frame_end = bpy.props.IntProperty(
|
|
name = 'End',
|
|
description = "End frame of shot (used to set the render end frame)",
|
|
soft_min = 0, soft_max=10000)
|
|
|
|
frame_rate = bpy.props.IntProperty(
|
|
default = 30,
|
|
name = 'FPS',
|
|
description = "Frame rate for shot",
|
|
soft_max = 30,
|
|
min = 1, max = 120)
|
|
|
|
ink = bpy.props.EnumProperty(
|
|
items = (('FS', 'Freestyle', 'Uses Freestyle Ink'),
|
|
('EN', 'Edge Node', 'Uses EdgeNode for Ink'),
|
|
('FE', 'FS + EN', 'Uses both Freestyle & EdgeNode for Ink'),
|
|
('NI', 'No Ink', 'Does not use ink (paint render used for final)'),
|
|
('CU', 'Custom', 'Custom setup, do not touch ink settings')),
|
|
default = 'CU',
|
|
name = 'Ink Type',
|
|
description = "Determines how ink will be handled in final shot render")
|
|
|
|
class ProjectPanel(bpy.types.Panel):
|
|
"""
|
|
Add a panel to the Properties-Scene screen with Project Settings.
|
|
"""
|
|
bl_idname = 'SCENE_PT_project'
|
|
bl_label = 'Project Properties'
|
|
bl_space_type = 'PROPERTIES'
|
|
bl_region_type = 'WINDOW'
|
|
bl_context = 'scene'
|
|
|
|
def draw(self, context):
|
|
pp = bpy.context.scene.project_properties
|
|
self.layout.label(text='Project Properties')
|
|
row = self.layout.row()
|
|
row.prop(pp, 'render_folder')
|
|
row = self.layout.row()
|
|
row.prop(pp, 'render_prefix')
|
|
row.prop(pp, 'designation')
|
|
self.layout.label(text='Render Range')
|
|
row = self.layout.row()
|
|
row.prop(pp, 'frame_start')
|
|
row.prop(pp, 'frame_end')
|
|
row.prop(pp, 'frame_rate')
|
|
self.layout.label(text='Extra')
|
|
row = self.layout.row()
|
|
row.prop(pp, 'role')
|
|
row.prop(pp, 'ink')
|
|
|
|
# Buttons
|
|
|
|
|
|
|
|
class LunaticsSceneProperties(bpy.types.PropertyGroup):
|
|
"""
|
|
Properties of the current scene.
|
|
|
|
NOTE: due to be replaced by 'ProjectProperties', using the schema data
|
|
retrieved by file_context.
|
|
"""
|
|
|
|
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
|
|
|
|
NOTE: To be replaced by 'ProjectPropertiesPanel'.
|
|
"""
|
|
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 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 = scene.render_profile_settings.render_profile
|
|
|
|
BlendFile.render_profiles.apply(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):
|
|
"""
|
|
Features for working with characters and armatures.
|
|
|
|
Currently only includes the CopyAnimation operator.
|
|
"""
|
|
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 Ink/Paint Config.
|
|
"""
|
|
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):
|
|
"""
|
|
Ink/Paint Config Operator.
|
|
"""
|
|
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 = 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 )
|
|
|
|
shot.cfg_scene()
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
|
|
class LunaticsPanel(bpy.types.Panel):
|
|
"""
|
|
Ink/Paint Configuration 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")
|
|
|
|
|
|
BlendFile = file_context.FileContext()
|
|
|
|
class RenderProfileSettings(bpy.types.PropertyGroup):
|
|
"""
|
|
Settings for Render Profiles control.
|
|
"""
|
|
render_profile = bpy.props.EnumProperty(
|
|
name='Profile',
|
|
items=render_profile.blender_enum_lookup,
|
|
description="Select from render profiles defined in project")
|
|
|
|
|
|
@persistent
|
|
def update_handler(ctxt):
|
|
"""
|
|
Keeps FileContext up-to-date with Blender file loaded.
|
|
"""
|
|
BlendFile.update(bpy.data.filepath)
|
|
|
|
|
|
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(ProjectProperties)
|
|
bpy.types.Scene.project_properties = bpy.props.PointerProperty(type=ProjectProperties)
|
|
bpy.utils.register_class(ProjectPanel)
|
|
|
|
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_class(LunaticsSceneProperties)
|
|
bpy.utils.unregister_class(LunaticsScenePanel)
|
|
|
|
bpy.utils.unregister_class(ProjectProperties)
|
|
|
|
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)
|