Update for Blender 5.2
- use new api compositor access - Fix file output nodes connexion/renumber/rename/slots handlibng - consider the new output types - fixes node position with new API - fix errors with handler LLM assisted update
This commit is contained in:
@@ -11,31 +11,84 @@ def get_addon_prefs():
|
||||
|
||||
# region Manage nodes
|
||||
|
||||
def get_compositor_tree(scene=None):
|
||||
'''Return the compositing node group of passed scene (active scene if not passed)
|
||||
None if the scene has no compositing node group
|
||||
'''
|
||||
scene = scene or bpy.context.scene
|
||||
return scene.compositing_node_group
|
||||
|
||||
def get_group_output_node(tree, create=False):
|
||||
'''Return the active group output node of the tree (compositor output since Blender 5.0)
|
||||
create: create the node, and an "Image" output socket in tree interface, if missing
|
||||
'''
|
||||
out = next((n for n in tree.nodes if n.type == 'GROUP_OUTPUT' and n.is_active_output), None)
|
||||
if out is None:
|
||||
out = next((n for n in tree.nodes if n.type == 'GROUP_OUTPUT'), None)
|
||||
if not create:
|
||||
return out
|
||||
|
||||
if not any(i.item_type == 'SOCKET' and i.in_out == 'OUTPUT' for i in tree.interface.items_tree):
|
||||
tree.interface.new_socket(name='Image', in_out='OUTPUT', socket_type='NodeSocketColor')
|
||||
if out is None:
|
||||
out = tree.nodes.new('NodeGroupOutput')
|
||||
return out
|
||||
|
||||
def real_loc(n):
|
||||
if not n.parent:
|
||||
return n.location
|
||||
return n.location + real_loc(n.parent)
|
||||
return n.location_absolute
|
||||
|
||||
def get_media_type(file_format):
|
||||
'''Return the media type corresponding to passed file format'''
|
||||
if file_format == 'OPEN_EXR_MULTILAYER':
|
||||
return 'MULTI_LAYER_IMAGE'
|
||||
if file_format == 'FFMPEG':
|
||||
return 'VIDEO'
|
||||
return 'IMAGE'
|
||||
|
||||
def set_format_settings(fmt, settings):
|
||||
'''Apply a dict of attributes to an image format settings (e.g: file output node format)
|
||||
media_type and file_format are set first, available values of other attributes depend on them
|
||||
'''
|
||||
settings = dict(settings)
|
||||
file_format = settings.pop('file_format', None)
|
||||
media_type = settings.pop('media_type', None)
|
||||
if file_format and not media_type:
|
||||
media_type = get_media_type(file_format)
|
||||
|
||||
if media_type:
|
||||
fmt.media_type = media_type
|
||||
if file_format:
|
||||
fmt.file_format = file_format
|
||||
for k, v in settings.items():
|
||||
setattr(fmt, k, v)
|
||||
|
||||
def set_scene_output_from_active_fileout_item():
|
||||
scn = bpy.context.scene
|
||||
rd = scn.render
|
||||
ntree = scn.node_tree
|
||||
ntree = get_compositor_tree(scn)
|
||||
fo = ntree.nodes.active
|
||||
|
||||
if fo.type != 'OUTPUT_FILE':
|
||||
return
|
||||
sl = fo.file_slots[fo.active_input_index]
|
||||
full_path = os.path.join(fo.base_path, sl.path)
|
||||
item = fo.file_output_items[fo.active_item_index]
|
||||
|
||||
if fo.format.media_type == 'MULTI_LAYER_IMAGE':
|
||||
## All items are layers of the same file
|
||||
full_path = os.path.join(fo.directory, fo.file_name)
|
||||
fmt = fo.format
|
||||
else:
|
||||
full_path = os.path.join(fo.directory, fo.file_name + item.name)
|
||||
fmt = item.format if item.override_node_format else fo.format
|
||||
|
||||
rd.filepath = full_path
|
||||
|
||||
fmt = fo.format if sl.use_node_format else sl.format
|
||||
## set those attr first to avoid error settings other attributes in next loop
|
||||
rd.image_settings.media_type = fmt.media_type
|
||||
rd.image_settings.file_format = fmt.file_format
|
||||
rd.image_settings.color_mode = fmt.color_mode
|
||||
rd.image_settings.color_depth = fmt.color_depth if fmt.color_depth else 8 # Force set since Sometimes it's weirdly set to "" (not in enum choice)
|
||||
rd.image_settings.color_depth = fmt.color_depth if fmt.color_depth else '8' # Force set since Sometimes it's weirdly set to "" (not in enum choice)
|
||||
|
||||
excluded = ['file_format', 'color_mode', 'color_depth',
|
||||
excluded = ['media_type', 'file_format', 'color_mode', 'color_depth',
|
||||
'view_settings', 'views_format']
|
||||
|
||||
''' ## all attrs
|
||||
@@ -68,21 +121,61 @@ def set_file_output_format(fo):
|
||||
'color_mode': 'RGBA'
|
||||
}
|
||||
|
||||
for k, v in env_file_format.items():
|
||||
setattr(fo.format, k, v)
|
||||
set_format_settings(fo.format, env_file_format)
|
||||
|
||||
def set_file_output_path(fo, path, suffix=''):
|
||||
'''Set directory and file name of a file output node from a single path
|
||||
Multilayer: last element is the file name, ex: "//render/name/name_"
|
||||
Other formats: whole path is the directory, ex: "//render/name/"
|
||||
suffix: optional subfolder added at the end of the directory, ex: "tech/"
|
||||
'''
|
||||
if fo.format.file_format == 'OPEN_EXR_MULTILAYER':
|
||||
directory, file_name = re.match(r'(.*[\\/])?(.*)', path).groups()
|
||||
directory = directory or ''
|
||||
else:
|
||||
directory, file_name = path, ''
|
||||
|
||||
if suffix:
|
||||
directory = directory.rstrip('\\/') + '/' + suffix if directory else suffix
|
||||
|
||||
fo.directory = directory
|
||||
fo.file_name = file_name
|
||||
|
||||
## Source socket type to file output item socket type
|
||||
FILE_OUTPUT_ITEM_TYPES = {
|
||||
'VALUE': 'FLOAT',
|
||||
'INT': 'INT',
|
||||
'BOOLEAN': 'BOOLEAN',
|
||||
'VECTOR': 'VECTOR',
|
||||
'RGBA': 'RGBA',
|
||||
}
|
||||
|
||||
def add_file_output_item(fo, name, socket=None):
|
||||
'''Add an item to file output node
|
||||
socket: source socket, if passed item type (and vector dimensions) match the socket
|
||||
return created item and its input socket
|
||||
'''
|
||||
socket_type = FILE_OUTPUT_ITEM_TYPES.get(socket.type, 'RGBA') if socket is not None else 'RGBA'
|
||||
item = fo.file_output_items.new(socket_type, name)
|
||||
if socket_type == 'VECTOR' and hasattr(socket, 'default_value'):
|
||||
## ex: render layer "Vector" pass is 4D
|
||||
item.vector_socket_dimensions = len(socket.default_value)
|
||||
## items inputs are in the same order (followed by a virtual socket)
|
||||
return item, fo.inputs[len(fo.file_output_items) - 1]
|
||||
|
||||
def clear_disconnected(fo):
|
||||
'''Remove unlinked inputs from file output node'''
|
||||
for inp in reversed(fo.inputs):
|
||||
if not inp.is_linked:
|
||||
print(f'Deleting unlinked fileout slot: {inp.name}')
|
||||
fo.inputs.remove(inp)
|
||||
'''Remove unlinked items from file output node'''
|
||||
items = fo.file_output_items
|
||||
for i in reversed(range(len(items))):
|
||||
if not fo.inputs[i].is_linked:
|
||||
print(f'Deleting unlinked fileout item: {items[i].name}')
|
||||
items.remove(items[i])
|
||||
|
||||
def create_node(type, tree=None, **kargs):
|
||||
'''Get a type, a tree to add in, and optionnaly multiple attribute to set
|
||||
return created node
|
||||
'''
|
||||
tree = tree or bpy.context.scene.node_tree
|
||||
tree = tree or get_compositor_tree()
|
||||
|
||||
node = tree.nodes.new(type)
|
||||
for k,v in kargs.items():
|
||||
@@ -129,79 +222,68 @@ def create_and_connect_file_output(node, outputs, file_out, out_name, base_path,
|
||||
"""
|
||||
if not outputs:
|
||||
return None
|
||||
|
||||
nodes = scene.node_tree.nodes
|
||||
links = scene.node_tree.links
|
||||
|
||||
|
||||
tree = get_compositor_tree(scene)
|
||||
nodes = tree.nodes
|
||||
links = tree.links
|
||||
|
||||
fo = file_out
|
||||
if not fo:
|
||||
fo = nodes.get(out_name)
|
||||
if not fo:
|
||||
fo = create_node('CompositorNodeOutputFile', tree=scene.node_tree,
|
||||
location=(real_loc(node)[0] + location_offset[0],
|
||||
real_loc(node)[1] + location_offset[1]), width=600)
|
||||
fo.inputs.remove(fo.inputs[0]) # Remove default image input
|
||||
|
||||
fo = create_node('CompositorNodeOutputFile', tree=tree, width=600)
|
||||
fo.file_output_items.clear() # Ensure there is no default item
|
||||
|
||||
if file_format:
|
||||
for k, v in file_format.items():
|
||||
setattr(fo.format, k, v)
|
||||
set_format_settings(fo.format, file_format)
|
||||
else:
|
||||
set_file_output_format(fo)
|
||||
|
||||
|
||||
if color_depth:
|
||||
fo.format.color_depth = color_depth
|
||||
|
||||
|
||||
fo.name = out_name
|
||||
if node.parent:
|
||||
fo.parent = node.parent
|
||||
fo.location_absolute = (real_loc(node)[0] + location_offset[0],
|
||||
real_loc(node)[1] + location_offset[1])
|
||||
|
||||
if base_path_template is not None:
|
||||
fo.base_path = build_path_from_template(base_path_template,
|
||||
node_name=node.name.strip(),
|
||||
node_label=node.label.strip(),
|
||||
scene_name=node.scene.name if node.type == 'R_LAYERS' else None,
|
||||
viewlayer_name=node.layer if node.type == 'R_LAYERS' else None,
|
||||
)
|
||||
if suffix:
|
||||
if fo.format.file_format == 'OPEN_EXR_MULTILAYER':
|
||||
## insert suffix right after last slash as folder
|
||||
path_list = re.split('(\\/)', fo.base_path)
|
||||
path_list.insert(-1, suffix)
|
||||
fo.base_path = ''.join(path_list)
|
||||
else:
|
||||
## Ensure slash separator then add suffix
|
||||
fo.base_path = fo.base_path.rstrip('\\/') + '/' + suffix
|
||||
else:
|
||||
path = build_path_from_template(base_path_template,
|
||||
node_name=node.name.strip(),
|
||||
node_label=node.label.strip(),
|
||||
scene_name=node.scene.name if node.type == 'R_LAYERS' else None,
|
||||
viewlayer_name=node.layer if node.type == 'R_LAYERS' else None,
|
||||
)
|
||||
elif base_path:
|
||||
path = base_path
|
||||
elif fo.format.file_format == 'OPEN_EXR_MULTILAYER':
|
||||
# default behavior without template
|
||||
if base_path:
|
||||
fo.base_path = base_path
|
||||
else:
|
||||
if fo.format.file_format == 'OPEN_EXR_MULTILAYER':
|
||||
fo.base_path = f'//render/{out_base}/{suffix}{out_base}_'
|
||||
else:
|
||||
fo.base_path = f'//render/{out_base}/{suffix}'
|
||||
path = f'//render/{out_base}/{out_base}_'
|
||||
else:
|
||||
path = f'//render/{out_base}/'
|
||||
|
||||
## suffix is added as a subfolder of the directory
|
||||
set_file_output_path(fo, path, suffix=suffix)
|
||||
|
||||
for o in outputs:
|
||||
if next((l for l in o.links if recursive_node_connect_check(l, fo)), None):
|
||||
continue
|
||||
|
||||
|
||||
if (socket_remaps := remap_names.get(node.name)) and (custom_name := socket_remaps.get(o.name)):
|
||||
slot_name = bpy.path.clean_name(custom_name)
|
||||
else:
|
||||
slot_name = bpy.path.clean_name(o.name)
|
||||
|
||||
fs = fo.file_slots.new('tmp')
|
||||
ls = fo.layer_slots.new('tmp')
|
||||
## Multilayer: item name is the layer name in the file
|
||||
## Other formats: item name is the subpath added to directory + file name
|
||||
if fo.format.file_format == 'OPEN_EXR_MULTILAYER' or use_slot_template:
|
||||
## use_slot_template always False (currently never use slot template)
|
||||
item_name = slot_name
|
||||
else:
|
||||
item_name = f'{slot_name}/{slot_name}_'
|
||||
|
||||
ls = fo.layer_slots[-1]
|
||||
ls.name = slot_name
|
||||
fs = fo.file_slots[-1]
|
||||
fs.path = f'{slot_name}/{slot_name}_'
|
||||
if use_slot_template:
|
||||
## Always False (currently never use slot template)
|
||||
fs.path = slot_name
|
||||
|
||||
out_input = fo.inputs[-1]
|
||||
_item, out_input = add_file_output_item(fo, item_name, socket=o)
|
||||
links.new(o, out_input)
|
||||
|
||||
clear_disconnected(fo)
|
||||
@@ -451,6 +533,12 @@ def set_properties_editor_tab(tab, skip_if_exists=True):
|
||||
return 1
|
||||
|
||||
|
||||
def get_object_icon(obj):
|
||||
'''Return outliner icon name of passed object'''
|
||||
if obj.type == 'LIGHT_PROBE':
|
||||
return 'OUTLINER_OB_LIGHTPROBE'
|
||||
return f'OUTLINER_OB_{obj.type}'
|
||||
|
||||
def show_and_active_object(obj, make_active=True, select=True, unhide=True):
|
||||
'''
|
||||
Show the object
|
||||
@@ -685,7 +773,7 @@ def store_visibility_states(collection=None):
|
||||
|
||||
|
||||
def store_collection_states(collection, context=None):
|
||||
'''Pass a colelction and store visibility state'''
|
||||
'''Pass a collection and store visibility state'''
|
||||
context = context or bpy.context
|
||||
|
||||
scene = context.scene
|
||||
|
||||
Reference in New Issue
Block a user