238 lines
8.8 KiB
Python
238 lines
8.8 KiB
Python
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()
|