358 lines
11 KiB
Python
358 lines
11 KiB
Python
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
|
#
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
import bpy
|
|
import os
|
|
import json
|
|
import idprop
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
def parse_rna_path_to_elements(rna_path, delimiter='.'):
|
|
''' Returns the element strings of an RNA path split by '.' delimiter, disregarding any delimiter in a string within the path.
|
|
'''
|
|
if not delimiter in rna_path:
|
|
return [rna_path]
|
|
|
|
parse = rna_path
|
|
|
|
# replace escape chars with whitespaces
|
|
parse_elements = parse.split(r'\\')
|
|
parse = ' '.join(parse_elements)
|
|
|
|
parse_elements = parse.split('\\')
|
|
parse = parse_elements[0]
|
|
for el in parse_elements[1:]:
|
|
parse += ' '
|
|
parse += el[1:]
|
|
|
|
# replace strings within path with whitespaces
|
|
parse_elements = parse.split('"')
|
|
parse = parse_elements[0]
|
|
for el1, el2 in zip(parse_elements[1::2], parse_elements[2::2]):
|
|
parse += '"'+' '*len(el1)+'"'
|
|
parse += el2
|
|
|
|
parse_elements = parse.split(delimiter)
|
|
|
|
elements = []
|
|
for el in parse_elements:
|
|
elements += [rna_path[:len(el)]]
|
|
rna_path = rna_path[len(el)+len(delimiter):]
|
|
|
|
return elements
|
|
|
|
def parse_rna_path_for_custom_property(rna_path):
|
|
''' Returns the rna path of the datablock and the name of the custom property for an rna path that describes a custom property.
|
|
'''
|
|
if not '][' in rna_path:
|
|
return False
|
|
parse_elements = parse_rna_path_to_elements(rna_path, delimiter='][')
|
|
return parse_elements[0]+']', '"'.join(parse_elements[1].split('"')[1:-1])
|
|
|
|
def mute_fcurve(db, path):
|
|
if not db.animation_data:
|
|
return
|
|
if not db.animation_data.action:
|
|
return
|
|
|
|
fcurve = db.animation_data.action.fcurves.find(path)
|
|
c = 0
|
|
while fcurve or c<=4:
|
|
if fcurve:
|
|
fcurve.mute = True
|
|
c += 1
|
|
fcurve = db.animation_data.action.fcurves.find(path, index=c)
|
|
return
|
|
|
|
def mute_driver(db, path):
|
|
if not db.animation_data:
|
|
return
|
|
if not db.animation_data.drivers:
|
|
return
|
|
|
|
driver = db.animation_data.drivers.find(path)
|
|
c = 0
|
|
while driver or c<=4:
|
|
if driver:
|
|
driver.mute = True
|
|
c += 1
|
|
driver = db.animation_data.drivers.find(path, index=c)
|
|
return
|
|
|
|
def mute_animation_on_rna_path(rna_path):
|
|
path_elements = parse_rna_path_to_elements(rna_path)
|
|
db_path = '.'.join(path_elements[:3])
|
|
if 'session_uid' in dir(eval(db_path)):
|
|
data_block = eval(db_path)
|
|
path = '.'.join(path_elements[3:])
|
|
else: # handle custom props
|
|
db_path, c_prop = parse_rna_path_for_custom_property(rna_path)
|
|
data_block = eval(db_path)
|
|
path = f'["{c_prop}"]'
|
|
|
|
if data_block.id_type in ['ACTION', 'BRUSH', 'COLLECTION', 'IMAGE', 'LIBRARY', 'PALETTE', 'PAINTCURVE', 'SCREEN', 'TEXT', 'WINDOWMANAGER', 'WORKSPACE']:
|
|
return
|
|
|
|
mute_fcurve(data_block, path)
|
|
mute_driver(data_block, path)
|
|
return
|
|
|
|
def split_by_suffix(list, sfx):
|
|
with_suffix = [name[:-len(sfx)] for name in list if name.endswith(sfx)]
|
|
without_suffix = [name for name in list if not name.endswith(sfx)]
|
|
return without_suffix, with_suffix
|
|
def get_properties_bone(ob, prefix='Properties_'):
|
|
|
|
if not ob.type == 'ARMATURE':
|
|
return None
|
|
|
|
for bone in ob.pose.bones:
|
|
if not bone.name.startswith(prefix):
|
|
continue
|
|
return bone
|
|
return None
|
|
|
|
def apply_variable_settings(data):
|
|
''' Applies settings to according nodes in the variables nodegroup.
|
|
'''
|
|
if not data:
|
|
return
|
|
|
|
for ng in bpy.data.node_groups:
|
|
if not ng.name == 'VAR-settings':
|
|
continue
|
|
for name in data:
|
|
node = ng.nodes.get(name)
|
|
if node:
|
|
node.outputs[0].default_value = data[name][0]
|
|
else:
|
|
print(f'Warning: Node {set} in variable settings nodegroup not found.')
|
|
return
|
|
|
|
def apply_motion_blur_settings(data):
|
|
''' Deactivates deformation motion blur for objects in selected collections.
|
|
'''
|
|
if not data:
|
|
return
|
|
|
|
list_unique, list_all = split_by_suffix(data.keys(), ':all')
|
|
|
|
if 'Master Collection' in data.keys() or 'Scene Collection' in data.keys():
|
|
for ob in bpy.data.objects:
|
|
if ob.type == 'CAMERA':
|
|
continue
|
|
ob.cycles.use_motion_blur = False
|
|
return
|
|
|
|
for col in bpy.data.collections:
|
|
for name_col in list_all:
|
|
if not col.name.startswith(name_col):
|
|
continue
|
|
for ob in col.all_objects:
|
|
if ob.type == 'CAMERA':
|
|
continue
|
|
ob.cycles.use_motion_blur = False
|
|
# unique names
|
|
if not col.name in list_unique:
|
|
continue
|
|
for ob in col.all_objects:
|
|
if ob.type == 'CAMERA':
|
|
continue
|
|
ob.cycles.use_motion_blur = False
|
|
return
|
|
|
|
def apply_shader_settings(data):
|
|
''' Assign shader setting properties to helper objects according to specified names.
|
|
'''
|
|
if not data:
|
|
return
|
|
|
|
list_unique, list_all = split_by_suffix(data.keys(), ':all')
|
|
|
|
for ob in bpy.data.objects:
|
|
# group names
|
|
for name_ob in list_all:
|
|
if not ob.name.startswith(name_ob):
|
|
continue
|
|
for name_set in data[name_ob+':all']:
|
|
if not name_set in ob:
|
|
print(f'Warning: Property {name_set} on object {ob.name} not found.')
|
|
continue
|
|
ob[name_set] = data[name_ob+':all'][name_set][0]
|
|
# unique names
|
|
if ob.name in list_unique:
|
|
for name_set in data[ob.name]:
|
|
if name_set in ob:
|
|
ob[name_set] = data[ob.name][name_set][0]
|
|
else:
|
|
print(f'Warning: Property {name_set} on object {ob.name} not found.')
|
|
return
|
|
|
|
def apply_rig_settings(data):
|
|
''' Assign rig setting properties to property bones according to specified names. Mutes fcurves from evaluation on those overriden properties.
|
|
'''
|
|
if not data:
|
|
return
|
|
|
|
list_unique, list_all = split_by_suffix(data.keys(), ':all')
|
|
|
|
for ob in bpy.data.objects:
|
|
# find properties bone (first posebone that starts with 'Properties_')
|
|
if not ob.type == 'ARMATURE':
|
|
continue
|
|
bone_prop = get_properties_bone(ob)
|
|
if not bone_prop:
|
|
continue
|
|
|
|
# group names
|
|
for name_ob in list_all:
|
|
if not ob.name.startswith(name_ob):
|
|
continue
|
|
for name_set in data[name_ob+':all']:
|
|
if not name_set in bone_prop:
|
|
print(f'Warning: Property {name_set} on object {ob.name} not found.')
|
|
continue
|
|
data_path = f'pose.bones["{bone_prop.name}"]["{name_set}"]'
|
|
mute_fcurve(ob, data_path)
|
|
bone_prop[name_set] = data[name_ob+':all'][name_set][0]
|
|
|
|
# unique names
|
|
if ob.name in list_unique:
|
|
for name_set in data[ob.name]:
|
|
if name_set in bone_prop:
|
|
data_path = f'pose.bones["{bone_prop.name}"]["{name_set}"]'
|
|
mute_fcurve(ob, data_path)
|
|
bone_prop[name_set] = data[ob.name][name_set][0]
|
|
else:
|
|
print(f'Warning: Property {name_set} on object {ob.name} not found.')
|
|
return
|
|
|
|
def apply_rna_overrides(data):
|
|
''' Applies custom overrides on specified rna data paths.
|
|
'''
|
|
if not data:
|
|
return
|
|
|
|
for path in data:
|
|
try:
|
|
mute_animation_on_rna_path(path)
|
|
if data[path][1] == 'STRING':
|
|
exec(path+f" = '{data[path][0]}'")
|
|
elif type(eval(path)) == idprop.types.IDPropertyArray:
|
|
exec(path+f'[:] = {data[path][0]}') # workaround for Blender not retaining UI data of property (see https://projects.blender.org/blender/blender/pulls/109203)
|
|
else:
|
|
exec(path+f' = {data[path][0]}')
|
|
except:
|
|
print(f'Warning: Failed to assign property {data[path][2]} at {path}')
|
|
return
|
|
|
|
def apply_settings(data):
|
|
''' Applies settings by categories using the specified category name and apply function.
|
|
'''
|
|
categories = {
|
|
'variable_settings': apply_variable_settings,
|
|
'motion_blur_settings': apply_motion_blur_settings,
|
|
'shader_settings': apply_shader_settings,
|
|
'rig_settings': apply_rig_settings,
|
|
'rna_overrides': apply_rna_overrides,
|
|
}
|
|
|
|
for cat in categories:
|
|
cat_data = data.get(cat)
|
|
if cat_data:
|
|
categories[cat](cat_data)
|
|
return
|
|
|
|
def settings_from_datablock(datablock):
|
|
''' Return the settings dict from the text data-block.
|
|
'''
|
|
settings = {}
|
|
if not datablock:
|
|
return None
|
|
if not datablock.as_string():
|
|
return settings
|
|
settings = json.loads(datablock.as_string())
|
|
return settings
|
|
|
|
def force_reload_external(context, text):
|
|
''' Reloads text datablock from disk.
|
|
'''
|
|
if not text:
|
|
return
|
|
if text.is_in_memory:
|
|
return
|
|
if not (text.is_dirty or text.is_modified):
|
|
return
|
|
path = text.filepath
|
|
path = bpy.path.abspath(path)
|
|
if not os.path.isfile(path):
|
|
return
|
|
new_string = open(path, 'r').read()
|
|
if not text.as_string() == new_string:
|
|
text.from_string(new_string)
|
|
return
|
|
|
|
def load_settings(context, name, path=None):
|
|
''' Return text datablock of the settings specified with a name. If a filepath is specified (re)load from disk.
|
|
'''
|
|
if path:
|
|
path += f'/{name}.settings.json'
|
|
|
|
settings_db = bpy.data.texts.get(f'{name}.settings.json')
|
|
|
|
if settings_db:
|
|
force_reload_external(context, settings_db)
|
|
return settings_from_datablock(settings_db)
|
|
|
|
if path:
|
|
if not os.path.isfile(path):
|
|
open(path, 'a').close()
|
|
bpy.ops.text.open(filepath=bpy.path.relpath(path))
|
|
settings_db = bpy.data.texts.get(f'{name}.settings.json')
|
|
else:
|
|
settings_db = bpy.data.texts.new(f'{name}.settings.json')
|
|
return settings_from_datablock(settings_db)
|
|
|
|
if __name__=="__main__" or __name__=="lighting_overrider.execution":
|
|
# Execution
|
|
context = bpy.context
|
|
|
|
sequence_settings = None
|
|
shot_settings = None
|
|
|
|
try:
|
|
sequence_db = context.scene['LOR_sequence_settings']
|
|
force_reload_external(context, sequence_db)
|
|
except:
|
|
sequence_db = None
|
|
|
|
try:
|
|
shot_db = context.scene['LOR_shot_settings']
|
|
force_reload_external(context, shot_db)
|
|
except:
|
|
shot_db = None
|
|
|
|
sequence_settings = settings_from_datablock(sequence_db)
|
|
shot_settings = settings_from_datablock(shot_db)
|
|
|
|
filepath = Path(bpy.context.blend_data.filepath)
|
|
|
|
sequence_name, shot_name = filepath.parts[-3:-1]
|
|
|
|
sequence_settings_path = filepath.parents[1].as_posix()
|
|
|
|
if not sequence_settings:
|
|
sequence_settings = load_settings(context, sequence_name, sequence_settings_path)
|
|
if not shot_settings:
|
|
shot_settings = load_settings(context, shot_name)
|
|
|
|
apply_settings(sequence_settings)
|
|
apply_settings(shot_settings)
|
|
|
|
# kick re-evaluation
|
|
for ob in bpy.data.objects:
|
|
ob.update_tag()
|