import bpy import os import re import json ### --- Manage nodes --- ### def real_loc(n): if not n.parent: return n.location return n.location + real_loc(n.parent) def set_file_output_format(fo): """Default fileout format for output file node Get from OUTPUT_RENDER_FILE_FORMAT environment variable else fallback to multilayer EXR """ env_file_format = json.loads(os.environ.get('OUTPUT_RENDER_FILE_FORMAT', '{}')) if not env_file_format: env_file_format = { 'file_format': 'OPEN_EXR_MULTILAYER', 'exr_codec': 'ZIP', 'color_depth': '16', 'color_mode': 'RGBA' } for k, v in env_file_format.items(): setattr(fo.format, k, v) 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) 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 node = tree.nodes.new(type) for k,v in kargs.items(): setattr(node, k, v) return node def recursive_node_connect_check(l, target_node): '''Get a link and a node return True if link is connected to passed target_node further in the node tree ''' if l.to_node == target_node: return True for o in l.to_node.outputs: for sl in o.links: if recursive_node_connect_check(sl, target_node): return True return False def connect_to_file_output(node_list, file_out=None, base_path='', excludes=None, remap_names=None, file_format=None): """Connect selected nodes output to file output(s) if a file output is selected, add intputs on it Args: node_list (list[bpy.types.Nodes,]): Nodes to connect file_out (bpy.types.CompositorNode, optional): File output node to connect to instead of new Defaults to None base_path (str, optional): Directory of images to render. if not passed, will use source node layer name Defaults to ''. file_format (dict, optionnal): converts each dictionary key into a file output format attribute and assigns the corresponding value. Defaults to None. excludes (dict, optionnal): List of output names to exclude {node_name: [outputs,]}. Defaults toNone. remap_names (dict, optionnal): List of output names to remap {node_name: {output_name: new_name}}. frame (bpy.types.CompositorNode, optional): If given, create nodes into a frame. Defaults to None. Returns: list[bpy.types.CompositorNode]: All nodes created. """ scene = bpy.context.scene nodes = scene.node_tree.nodes links = scene.node_tree.links if not isinstance(node_list, list): node_list = [node_list] node_list = [n for n in node_list if n.type != 'OUTPUT_FILE'] if not node_list: return excludes = excludes or {} for node in node_list: exclusions = excludes.get(node.name) or [] ## create one output facing node and connect all outs = [o for o in node.outputs if not o.is_unavailable and not 'crypto' in o.name.lower() and o.name not in exclusions] cryptout = [o for o in node.outputs if not o.is_unavailable and 'crypto' in o.name.lower() and o.name not in exclusions] if node.type == 'R_LAYERS': out_base = node.layer elif node.label: out_base = node.label else: out_base = node.name out_base = bpy.path.clean_name(out_base) out_name = f'OUT_{out_base}' if outs: fo = file_out if not fo: fo = nodes.get(out_name) if not fo: # color = (0.2,0.3,0.5) fo = create_node('CompositorNodeOutputFile', tree=scene.node_tree, location=(real_loc(node)[0]+500, real_loc(node)[1]+50), width=600) fo.inputs.remove(fo.inputs[0]) # Remove default image input if file_format: for k, v in file_format.items(): setattr(fo.format, k, v) else: set_file_output_format(fo) fo.name = out_name if node.parent: fo.parent = node.parent if base_path: fo.base_path = base_path else: if fo.format.file_format == 'OPEN_EXR_MULTILAYER': fo.base_path = f'//render/{out_base}/{out_base}_' else: fo.base_path = f'//render/{out_base}' for o in outs: 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) # clean name ? else: slot_name = bpy.path.clean_name(o.name) # if fo.format.file_format == 'OPEN_EXR_MULTILAYER': # slot_name = slot_name # else: # slot_name = f'{slot_name}/{slot_name}_' # fo.file_slots.new(slot_name) fs = fo.file_slots.new('tmp') # slot_name) ls = fo.layer_slots.new('tmp') # slot_name + 'layer') ls = fo.layer_slots[-1] ls.name = slot_name fs = fo.file_slots[-1] fs.path = f'{slot_name}/{slot_name}_' # Error 'NodeSocketColor' object has no attribute 'path' out_input = fo.inputs[-1] links.new(o, out_input) clear_disconnected(fo) fo.update() ## Create separate file out for cryptos if cryptout: out_name += '_cryptos' fo = file_out if not fo: fo = nodes.get(out_name) if not fo: # color = (0.2,0.3,0.5) fo = create_node('CompositorNodeOutputFile', tree=scene.node_tree, location=(real_loc(node)[0]+400, real_loc(node)[1]-200), width=220) fo.inputs.remove(fo.inputs[0]) # Remove default image input if file_format: for k, v in file_format.items(): setattr(fo.format, k, v) else: set_file_output_format(fo) # OPEN_EXR_MULTILAYER, RGBA, ZIP fo.format.color_depth = '32' # For crypto force 32bit fo.name = out_name if node.parent: fo.parent = node.parent if base_path: fo.base_path = base_path else: if fo.format.file_format == 'OPEN_EXR_MULTILAYER': ## FIXME: find a better organization for separated crypto pass fo.base_path = f'//render/{out_base}/cryptos/cryptos_' else: fo.base_path = f'//render/{out_base}' for o in cryptout: ## Skip already connected ## TODO Test recusively to find fo (some have interconnected sockets) # if next((l for l in o.links if l.to_node == fo), None): if next((l for l in o.links if recursive_node_connect_check(l, fo)), None): continue # if remap_names and (custom_name := remap_names.get(o.name)): if (socket_remaps := remap_names.get(node.name)) and (custom_name := socket_remaps.get(o.name)): slot_name = bpy.path.clean_name(custom_name) # clean name ? else: slot_name = bpy.path.clean_name(o.name) # directly use name in multi layer exr # if fo.format.file_format == 'OPEN_EXR_MULTILAYER': # slot_name = slot_name # else: # slot_name = f'{slot_name}/{slot_name}_' # fo.file_slots.new(slot_name) # Setting both file_slots and layer_slots... fs = fo.file_slots.new('tmp') ls = fo.layer_slots.new('tmp') ls = fo.layer_slots[-1] ls.name = slot_name fs = fo.file_slots[-1] fs.path = f'{slot_name}/{slot_name}_' # Error 'NodeSocketColor' object has no attribute 'path' out_input = fo.inputs[-1] links.new(o, out_input) clear_disconnected(fo) fo.update()