Files
2026-03-17 14:58:51 -06:00

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()