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)