render_toolbox/fn.py

276 lines
9.5 KiB
Python
Executable File

import bpy
import os
import re
import json
from .constant import TECH_PASS_KEYWORDS
### --- 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 create_and_connect_file_output(node, outputs, file_out, out_name, base_path, out_base,
scene, remap_names, file_format, suffix='', color_depth=None,
location_offset=(500, 50)):
"""Helper function to create file output node and connect it to outputs
Args:
node: Source node
outputs: List of outputs to connect
file_out: Existing file output node or None
out_name: Name for the file output node
base_path: Base path for output files
out_base: Base name for output files
scene: Blender scene
remap_names: Dictionary for remapping output names
file_format: Format settings
suffix: Optional suffix for paths
color_depth: Optional override for color depth
location_offset: Offset for the node position
Returns:
bpy.types.CompositorNode: Created or used file output node
"""
if not outputs:
return None
nodes = scene.node_tree.nodes
links = scene.node_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
if file_format:
for k, v in file_format.items():
setattr(fo.format, k, v)
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
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}'
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')
ls = fo.layer_slots[-1]
ls.name = slot_name
fs = fo.file_slots[-1]
fs.path = f'{slot_name}/{slot_name}_'
out_input = fo.inputs[-1]
links.new(o, out_input)
clear_disconnected(fo)
fo.update()
return fo
def connect_to_file_output(node_list, file_out=None, base_path='', excludes=None, remap_names=None,
file_format=None, split_tech_passes=False):
"""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}}.
split_tech_passes (bool, optional): When True, create a separate file output for technical passes
Defaults to False.
Returns:
list[bpy.types.CompositorNode]: All nodes created.
"""
scene = bpy.context.scene
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 []
created_nodes = []
excludes = excludes or {}
remap_names = remap_names or {}
tech_file_format = file_format.copy() if file_format else None
if tech_file_format:
## TODO: Set to EXR if provioded is not ERX or EXR multilayer ?
# ->> if tech_file_format.get('file_format')) is not in ('OPEN_EXR', 'OPEN_EXR_MULTILAYER'): ...
## Force 32 bit
tech_file_format['color_depth'] = '32'
## Enforce a lossless format if provided is not
if (codec := tech_file_format.get('exr_codec')) and codec not in ['ZIP','PIZ','RLE','ZIPS']:
tech_file_format['exr_codec'] = 'PIZ'
for node in node_list:
exclusions = excludes.get(node.name) or []
# Get all available outputs excluding those in exclusions
all_outputs = [o for o in node.outputs if not o.is_unavailable and o.name not in exclusions]
# Base name for output nodes
if node.label:
out_base = node.label
else:
out_base = node.name
out_base = bpy.path.clean_name(out_base)
# Categorize outputs
crypto_outputs = [o for o in all_outputs if 'crypto' in o.name.lower()]
if split_tech_passes:
# Filter tech passes
tech_outputs = [o for o in all_outputs if o.name.lower() in TECH_PASS_KEYWORDS] # any(keyword in o.name.lower() for keyword in TECH_PASS_KEYWORDS)]
# Regular outputs (excluding crypto and tech passes)
regular_outputs = [o for o in all_outputs if o not in crypto_outputs and o not in tech_outputs]
else:
# If not splitting tech passes, include them with regular outputs
regular_outputs = [o for o in all_outputs if o not in crypto_outputs]
tech_outputs = []
y_offset = 50
node_margin = 100
# Create and connect regular outputs
if regular_outputs:
out_name = f'OUT_{out_base}'
fo_regular = create_and_connect_file_output(
node, regular_outputs, file_out, out_name, base_path,
out_base, scene, remap_names, file_format,
location_offset=(500, y_offset)
)
if fo_regular and fo_regular not in created_nodes:
created_nodes.append(fo_regular)
y_offset += -22 * len(regular_outputs) - node_margin
# Create and connect tech outputs with 32-bit depth if split_tech_passes is True
if tech_outputs:
out_name = f'OUT_{out_base}_tech'
fo_tech = create_and_connect_file_output(
node, tech_outputs, None, out_name, base_path,
out_base, scene, remap_names, tech_file_format,
suffix='tech/', color_depth='32',
location_offset=(500, y_offset)
)
if fo_tech and fo_tech not in created_nodes:
created_nodes.append(fo_tech)
y_offset -= node_margin
y_offset += -22 * len(tech_outputs)
# Create and connect crypto outputs with 32-bit depth
if crypto_outputs:
out_name = f'OUT_{out_base}_cryptos'
fo_crypto = create_and_connect_file_output(
node, crypto_outputs, None, out_name, base_path,
out_base, scene, remap_names, tech_file_format,
suffix='cryptos/', color_depth='32',
location_offset=(500, y_offset)
)
if fo_crypto and fo_crypto not in created_nodes:
created_nodes.append(fo_crypto)
return created_nodes