gp_render/setup_passes.py

463 lines
15 KiB
Python
Raw Normal View History

2021-09-07 18:06:54 +02:00
info = {
'icon': 'NODE_COMPOSITING',
'description': 'Setup GP compositing passes',
}
import fnmatch
import glob
import os
import re
from math import degrees, radians
from os import listdir
from os.path import basename, dirname, exists, isdir, isfile, join, splitext
from pathlib import Path
import bpy
from mathutils import Matrix, Vector
C = bpy.context
D = bpy.data
scene = C.scene
## GLOBAL VARIABLES
rebuild = True
white = (1,1,1)
black = (0,0,0)
rest = re.compile(r'^[A-Z]{2}_')
# allowed_prefixes = ['SP','LN','LT','DK','DE','TX','CO','MA','SH','CC',] # Mars express # 'PO','AN' # AN and posing are not rendered
# Unicorn wars tag set
allowed_prefixes = ['CU','TO','CO','FX'] # 'MA',
excluded_prefixes = ['PR','RG','TD',] # not used
## TODO
# - create a json file with layer order, frame per GP and layer order
# - rules should be dynamic to regenerate
def link_node_group(filepath, group_name, link=True):
'''Link a node_group by name from a file, if link is False, append instead of linking'''
with bpy.data.libraries.load(filepath, link=link) as (data_from, data_to):
# data_to.node_groups = [c for c in data_from.node_groups if c.startswith(group_name)]
data_to.node_groups = [c for c in data_from.node_groups if c == group_name]
if data_to.node_groups:
return data_to.node_groups[0]
# return data_to.node_groups
def clear_view_layer():
for i in range(len(C.scene.view_layers))[::-1]:
vl = C.scene.view_layers[i]
if not '_' in vl.name:
continue
if not vl.name.startswith('View'): # maybe not needed...
C.scene.view_layers.remove(vl)
def get_view_layer(name):
'''get viewlayer name
return existing/created viewlayer
'''
### pass double letter prefix as suffix
## pass_name = re.sub(r'^([A-Z]{2})(_)(.*)', r'\3\2\1', 'name')
## pass_name = f'{name}_{passe}'
pass_vl = scene.view_layers.get(name)
if not pass_vl:
pass_vl = scene.view_layers.new(name)
return pass_vl
def linkin(col, parent):
'''take tow collection, link col into parent.childs'''
if not col in [c for c in parent.children]:
parent.children.link(col)
def get_col(name):
'''get collection by name (create if not found)'''
col = bpy.data.collections.get(name)
if not col:
col = bpy.data.collections.new(name)
return col
def set_layer_col_attr(attr, value, lcol=None, filter=None):
'''set and attribute attr to set with a value on a viewlayer collection lcol'''
lcol = lcol or bpy.context.view_layer.layer_collection
for c in lcol.children:
if filter is None or filter(c):
setattr(c, attr, value(c) if callable(value) else value)
set_layer_col_attr(attr, value, lcol=c, filter=filter)
def set_passes_gp(ob):
if not ob.name.endswith('_PASSES'):
print(f'{ob.name} has not a _PASSES suffix')
return
collec_name = ob.name
pass_name = ob.name.replace('_PASSES', '')
for l in ob.data.layers:
vl = None
## Color to white
if l.info.startswith('CO_'): # Colors
# l.tint_factor = 1
# l.tint_color = white
vl = get_view_layer(pass_name+'_CO')
l.viewlayer_render = '' # remove viewlayer (should be on all VL)
elif l.info.startswith('TO_'): # Tones
# l.tint_color = black
# l.tint_factor = 1
vl = get_view_layer(pass_name+'_TO')
l.viewlayer_render = vl.name
## line at full opacity
elif l.info.startswith('CU_'): # CleanUp
vl = get_view_layer(pass_name+'_CU')
l.viewlayer_render = vl.name
l.opacity = 1
## spec switch to black (else white on white) full opacity
# elif l.info.startswith('SP_'):
# vl = get_view_layer(pass_name+'_SP')
# l.viewlayer_render = vl.name
# l.tint_color = black
# l.tint_factor = 1
# l.opacity = 1
#?# opacity to max ??
elif l.info.startswith('FX_'): # FX
vl = get_view_layer(pass_name+'_FX')
l.viewlayer_render = vl.name
elif l.info.startswith('MA_'): # Masks
l.opacity = 0 # put masks opacity to 0
if l.hide:
print(f'{l.info} is hidden')
## Add other prefixes even if they have no specific rules yet
# elif l.info.split('_')[0] in allowed_prefixes:
# pfix = l.info.split('_')[0]
# vl = get_view_layer(pass_name+f'_{pfix}')
# l.viewlayer_render = vl.name
# elif l.info.startswith(('PR','RG','TD'))
else:
# assign exclude viewlayers
# vl = get_view_layer('_excluded')
# l.viewlayer_render = vl.name
l.viewlayer_render = get_view_layer('_excluded').name # do not assign vl to layer
## enable only the _passe col in those viewlayers
# TODO ! exclude other viewlayer collection than PASSE and it's parents
if vl:
set_layer_col_attr('exclude', True, vl.layer_collection)
set_layer_col_attr('exclude', False, vl.layer_collection, filter=lambda x: x.name == collec_name)
def clear_gp(name):
ob = bpy.data.objects.get(name)
if ob:
dat = ob.data
bpy.data.objects.remove(ob)
bpy.data.grease_pencils.remove(dat)
def dup_gp(ob, name):
nob = ob.copy()
nob.name = name
nob.data = ob.data.copy()
nob.data.name = name
return nob
def add_rlayer(layer_name, location=None, color=None, node_name=None):
'''create a render layer node
if node_name is not specified, use passed layer name
'''
# connect to fileoutput
if not node_name:
node_name = layer_name # 'RL_' +
nodes = bpy.context.scene.node_tree.nodes
comp = nodes.get(node_name)
if comp:
if rebuild:
location = comp.location.copy() # keep previous loc
nodes.remove(comp)
else:
return comp
comp = nodes.new('CompositorNodeRLayers')
comp.name = node_name
comp.layer = layer_name
comp.label = layer_name
if location:
comp.location = location
if color:
comp.color = color
return comp
def get_create_composite():
'''return composite output (create if needed) and replace it'''
nodes = bpy.context.scene.node_tree.nodes
compout = [n for n in nodes if n.type == 'COMPOSITE']
if compout:
compout = compout[0]
for lnk in compout.inputs[0].links:
lnk.from_node.location.y = 1000
else:
compout = nodes.new('CompositorNodeComposite')
compout.location = (1000,1000)
return compout
def connect_node_group(out_socket, name, source_path):
'''get a node socket to connect from, name of the node group, source path where to find the nodegroup'''
nodes = bpy.context.scene.node_tree.nodes
links = bpy.context.scene.node_tree.links
### TODO get create nodegroup node and connect from node socket
# check if node group exists in file
tree = bpy.data.node_groups.get(name)
print('tree')
if tree:
print('in tree')
# if the group tree exists delete laready connected node to recreate
for n in nodes:
if n.type != 'GROUP':
continue
if not n.node_tree or n.node_tree != tree:
continue
print('same group', n.name)
if len(n.inputs[0].links) < 1:
continue
print('has links')
if out_socket.node == n.inputs[0].links[0].from_node:
print(n.name)
nodes.remove(n)
break
print('no same from nodes:', n.inputs[0].links[0].from_node)
else:
# always relink tree ??
tree = link_node_group(source_path, name, link=False) # should not duplicate
ng = nodes.new('CompositorNodeGroup')
ng.node_tree = tree
# create the link
links.new(out_socket, ng.inputs[0])
return ng
## create individual collection
def gp_output(gpo):
# get / create grease pencil passes
out = get_col('OUTPUT')
linkin(out, bpy.context.scene.collection)
name = gpo.name
col_out_name = name + '_OUTPUT'
passe_name = name + '_PASSES'
# create and link a collection
gpout = get_col(col_out_name)
linkin(gpout, out)
## Passes
col_passe = get_col(passe_name)
linkin(col_passe, gpout)
## Clean
clear_gp(passe_name)
## duplicate
gp_passe = dup_gp(gpo, passe_name)
col_passe.objects.link(gp_passe)
## Set the passes in layers
set_passes_gp(gp_passe)
## create viewlayers and compo_tree
prefixes = [l.info.split('_')[0] for l in gpo.data.layers if rest.match(l.info.strip(' -'))]
prefixes = list(set(prefixes))
nodes = bpy.context.scene.node_tree.nodes
links = bpy.context.scene.node_tree.links
## get composite output
# compout = get_create_composite()
bottom = min([n.location.y for n in nodes]) - 250
x_rlayers_loc = [n.location.x for n in nodes if n.type == 'R_LAYERS']
if x_rlayers_loc:
left_rlayer = min(x_rlayers_loc)
else:
left_rlayer = 0
## sort prefixes according to given prefix list and keep non-listed at the list tail
new_prefixes = sorted([p for p in prefixes if p not in allowed_prefixes]) # non prelisted prefixes
prefixes = [p for p in allowed_prefixes if p in prefixes] # sorted prelisted prefixes
# prefixes += new_prefixes # add new prefixes to the end of the list
if new_prefixes:
print(r'/!\ warning, some prefixes are not listed :', new_prefixes)
### ------------------
## fileoutput
fo_name = name + '_FILEOUT'
fo = nodes.get(fo_name)
if not fo:
fo = nodes.new('CompositorNodeOutputFile')
fo.location = (left_rlayer + 800, bottom)
fo.name = fo_name
fo.width = 400
fo.file_slots.remove(fo.inputs[0]) # remove default Image first slot
else:
# clear all inputs (could also fully delete node and recreate...)
for i in range(0, len(fo.file_slots))[::-1]:
print(i, fo.file_slots[i].path)
for lnk in fo.inputs[i].links:
links.remove(lnk)
fo.file_slots.remove(fo.inputs[i]) # fo.file_slots[i]
# TODO specifier un chemin d'output via env/template
fo.base_path = f'//sequences/{name}'
# Create render layers nodes from available prefixes
print('prefixes:', prefixes)
first=True
for pfix in prefixes:
## get previously created render layer and connect to file out
passe = f'{name}_{pfix}'
if first: # avoid first
first=False
else:
bottom -= 200
comp = add_rlayer(passe, location=(left_rlayer, bottom), color=None)
comp.show_preview = False
rl_node = nodes.get(passe)
if not rl_node:
print(f'/!\ missing {passe}')
continue
# Connect to fileoutput
subpath = f'{passe}/{passe}_'
sl = fo.file_slots.get(subpath)
if not sl:
sl = fo.file_slots.new(subpath)
ng = None
## TODO conditions according to type (8/16 bits, png, alpha...)
if pfix == 'SP':
# TODO need to pass link as True and dynamically define nodegroup library (using env)
ng = connect_node_group(rl_node.outputs[0], 'invert_keep_alpha', r'/z/___LONGS/UNICORN_WARS/library/nodegroups/invert_keep_alpha.blend')
links.new(ng.outputs[0], sl)
else:
links.new(rl_node.outputs[0], sl)
# replace node_group if any
if ng:
rloc = rl_node.location
ng.location = (rloc.x + 300, rloc.y + 60)
## generate compo
def connect_main_vl():
nodes = bpy.context.scene.node_tree.nodes
links = bpy.context.scene.node_tree.links
vl = bpy.context.scene.view_layers.get('View Layer')
if not vl:
print('No viewlayer named "View Layer" !')
# trying to autofetch
vlist = [vl for vl in bpy.context.scene.view_layers if not re.search(r'_[A-Z]{2}$', vl.name)]
if not vlist:
print('Cancelling, No candidate found...')
return
if len(vlist) > 1:
print('Cancelling, Multiple candidate found :', vlist)
return
vl = vlist[0]
print('Using autodetected view layer name:', vl.name)
render_vl = [n for n in nodes if n.type == 'R_LAYERS' and n.layer == vl.name]
compout = get_create_composite()
main_loc = (compout.location.x - 1000, compout.location.y - 24)
if not render_vl:
render_vl = add_rlayer(vl.name, location=main_loc, node_name='Render Layers')
else:
render_vl = render_vl[0]
render_vl.location = main_loc
is_linked = [lnk for lnk in render_vl.outputs[0].links if lnk.to_node == compout]
if not is_linked:
outlinks = [lnk for lnk in compout.inputs[0].links]
if outlinks:
print(f'cannot link {render_vl.name} to composite, already linked from {outlinks[0].from_node.name}')
return
links.new(render_vl.outputs[0], compout.inputs[0])
else:
print(f'{vl.name} already linked to composite')
def generate_all_comp():
bpy.context.scene.use_nodes = True
## sepcial check : mandatory 2D collection (Mars express)
col = bpy.data.collections.get('2D')
if not col:
print('No 2D collection in file (grease pencil comp is created from GP object within this collection)')
col = bpy.data.collections.get('GP')
if not col:
print('\n\nNo GP collection in file (need 2D or GP)\n\n')
return
connect_main_vl()
# exclude_filter = ('old',)
# fetch targets
gp_objects = [o for o in col.all_objects if o.type == 'GPENCIL'
and not bpy.context.view_layer.objects[o.name].hide_get()] # and not any(x in o.name.lower() for x in exclude_filter)
print()
print(f'Working on {len(gp_objects)} GP objects:')
print('\n'.join([o.name for o in gp_objects]))
# build comp for every GP
for gpo in gp_objects:
gp_output(gpo)
vl = bpy.context.scene.view_layers.get('View Layer')
if vl:
set_layer_col_attr('exclude', True, vl.layer_collection, filter=lambda x: x.name == 'OUTPUT')
# export
def single_comp(ob):
if not ob:
print('No active object')
return
if ob.type != 'GPENCIL':
print('current active object is not a grease pencil')
return
bpy.context.scene.use_nodes = True
col_out = bpy.data.collections.get('OUTPUT')
if col_out and ob in col_out.all_objects[:]:
print('WARNING', f'Object {ob.name} is part of the OUTPUT collection !')
return
gp_output(ob)
generate_all_comp()
## from selection
# for ob in bpy.context.selected_objects:
# if ob.type == 'GPENCIL':
# single_comp(ob)