463 lines
15 KiB
Python
463 lines
15 KiB
Python
|
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)
|