2021-09-07 23:11:42 +02:00
import bpy
2021-12-16 19:10:00 +01:00
import os
2021-09-07 23:11:42 +02:00
import re
2024-09-04 15:46:15 +02:00
import json
2021-09-07 23:11:42 +02:00
from mathutils import Vector
from pathlib import Path
2024-09-04 15:46:15 +02:00
from itertools import groupby
2021-09-17 18:36:15 +02:00
from math import isclose
2021-09-07 23:11:42 +02:00
from collections import defaultdict
2021-09-30 18:51:49 +02:00
from time import time
2021-09-07 23:11:42 +02:00
2024-04-16 18:01:10 +02:00
from . constant import RD_SCENE_NAME
2022-02-09 12:27:29 +01:00
2022-12-19 20:15:11 +01:00
### -- rules
def is_valid_name ( name ) :
''' return True if name correspond to a valid object
Don ' t start with a dot ' . '
Is not " note "
'''
if name . startswith ( ' . ' ) :
return False
2023-01-18 14:28:27 +01:00
2022-12-19 20:15:11 +01:00
## FIXME: /!\ "note" as an exclude word is not good practice, temporary fix
if name . lower ( ) == ' note ' :
return False
2023-01-18 14:28:27 +01:00
2022-12-19 20:15:11 +01:00
return True
2022-02-09 12:27:29 +01:00
### -- node basic
2021-09-07 23:11:42 +02:00
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 )
2023-01-18 14:28:27 +01:00
2021-09-07 23:11:42 +02:00
return node
2021-09-18 18:07:44 +02:00
def new_aa_node ( tree , * * kargs ) :
2021-09-07 23:11:42 +02:00
''' create AA node '''
aa = create_node ( ' CompositorNodeAntiAliasing ' , tree ) # type = ANTIALIASING
2021-09-18 18:07:44 +02:00
aa . threshold = 1.0 # 0.5
aa . contrast_limit = 0.25 # 0.5
2021-09-07 23:11:42 +02:00
aa . corner_rounding = 0.25
aa . hide = True
2021-09-18 18:07:44 +02:00
for k , v in kargs . items ( ) :
setattr ( aa , k , v )
2021-09-07 23:11:42 +02:00
return aa
2021-09-18 18:07:44 +02:00
def create_aa_nodegroup ( tree ) :
ngroup = bpy . data . node_groups . get ( ' AA ' )
if not ngroup :
ngroup = bpy . data . node_groups . new ( ' AA ' , ' CompositorNodeTree ' )
ng_in = create_node ( ' NodeGroupInput ' , tree = ngroup , location = ( - 600 , 0 ) )
ng_out = create_node ( ' NodeGroupOutput ' , tree = ngroup , location = ( 600 , 0 ) )
2021-09-21 18:23:25 +02:00
sep = create_node ( ' CompositorNodeSepRGBA ' , tree = ngroup , location = ( - 150 , 0 ) )
comb = create_node ( ' CompositorNodeCombRGBA ' , tree = ngroup , location = ( 350 , 25 ) )
2023-01-18 14:28:27 +01:00
2024-01-16 12:27:10 +01:00
if bpy . app . version < ( 4 , 0 , 0 ) :
ngroup . inputs . new ( ' NodeSocketColor ' , ' Image ' )
ngroup . outputs . new ( ' NodeSocketColor ' , ' Image ' )
else :
ngroup . interface . new_socket ( ' Image ' , in_out = ' INPUT ' , socket_type = ' NodeSocketColor ' )
ngroup . interface . new_socket ( ' Image ' , in_out = ' OUTPUT ' , socket_type = ' NodeSocketColor ' )
2023-04-04 15:51:41 +02:00
2021-09-21 18:23:25 +02:00
# in AA
# ngroup.links.new(comb.outputs[0], ng_out.inputs[0]) # <- connect without out AA
aa = new_aa_node ( ngroup , location = ( - 400 , 0 ) )
# ngroup.links.new(ng_in.outputs[0], sep.inputs[0])
ngroup . links . new ( ng_in . outputs [ 0 ] , aa . inputs [ 0 ] )
ngroup . links . new ( aa . outputs [ 0 ] , sep . inputs [ 0 ] )
# ngroup.links.new(ng_in.outputs[0], sep.inputs[0])
2021-09-18 18:07:44 +02:00
for i in range ( 3 ) :
ngroup . links . new ( sep . outputs [ i ] , comb . inputs [ i ] )
2023-01-18 14:28:27 +01:00
2021-09-18 18:07:44 +02:00
# alpha AA
2021-09-21 18:23:25 +02:00
alpha_aa = new_aa_node ( ngroup , location = ( 100 , - 150 ) )
2021-09-18 18:07:44 +02:00
ngroup . links . new ( sep . outputs [ 3 ] , alpha_aa . inputs [ 0 ] )
ngroup . links . new ( alpha_aa . outputs [ 0 ] , comb . inputs [ 3 ] )
2023-01-18 14:28:27 +01:00
2021-09-21 18:23:25 +02:00
ngroup . links . new ( comb . outputs [ 0 ] , ng_out . inputs [ 0 ] )
2021-09-18 18:07:44 +02:00
2023-01-18 14:28:27 +01:00
2021-09-18 18:07:44 +02:00
ng = create_node ( ' CompositorNodeGroup ' , tree = tree )
ng . node_tree = ngroup
ng . name = ngroup . name
ng . hide = True
return ng
2021-09-07 23:11:42 +02:00
2023-01-18 14:28:27 +01:00
## -- object and scene settings
2022-02-09 12:27:29 +01:00
2023-01-17 16:50:42 +01:00
def activate_workspace ( name = ' ' , context = None ) :
if not name :
return
if context is None :
context = bpy . context
if context . window . workspace . name == name :
print ( f ' Already in { name } workspace ' )
return
if ( wkspace := bpy . data . workspaces . get ( name ) ) :
context . window . workspace = wkspace
return True
2023-01-18 14:28:27 +01:00
2023-01-17 16:50:42 +01:00
# Same name with spaces as underscore
dir_name = name . replace ( ' ' , ' _ ' )
filepath = Path ( __file__ ) . parent / ' app_templates ' / dir_name / ' startup.blend '
2023-01-18 14:28:27 +01:00
2023-01-17 16:50:42 +01:00
ret = bpy . ops . workspace . append_activate ( idname = name , filepath = str ( filepath ) )
if ret != { ' FINISHED ' } :
print ( f ' Could not found { name } at { filepath } ' )
return False
return context . window . workspace
2021-09-15 18:28:16 +02:00
def copy_settings ( obj_a , obj_b ) :
exclusion = [ ' bl_rna ' , ' id_data ' , ' identifier ' , ' name_property ' , ' rna_type ' , ' properties ' , ' stamp_note_text ' , ' use_stamp_note ' ,
2023-01-18 14:28:27 +01:00
' settingsFilePath ' , ' settingsStamp ' , ' select ' , ' matrix_local ' , ' matrix_parent_inverse ' ,
2021-09-15 18:28:16 +02:00
' matrix_basis ' , ' location ' , ' rotation_euler ' , ' rotation_quaternion ' , ' rotation_axis_angle ' , ' scale ' ]
for attr in dir ( obj_a ) :
if attr . startswith ( ' __ ' ) :
continue
if attr in exclusion :
continue
# print('attr: ', attr)
# if obj_a.is_property_readonly(attr): # block when things aren't attribute
# continue
try :
val = getattr ( obj_a , attr )
except AttributeError :
2021-09-15 19:36:06 +02:00
# print(f'cant get {attr}')
pass
2021-09-15 18:28:16 +02:00
try :
setattr ( obj_b , attr , val )
except :
2021-09-15 19:36:06 +02:00
# print(f"can't set {attr}")
pass
2021-09-15 18:28:16 +02:00
2021-09-21 18:23:25 +02:00
def set_file_output_format ( fo ) :
2024-02-22 17:29:41 +01:00
# FIXME for now default fallback to multilayer
env_file_format = os . environ . get ( ' FILE_FORMAT ' )
fo . format . file_format = env_file_format if env_file_format else ' OPEN_EXR_MULTILAYER '
# fo.format.file_format = 'OPEN_EXR' # 'OPEN_EXR_MULTILAYER'
2021-09-21 18:23:25 +02:00
fo . format . color_mode = ' RGBA '
2021-09-23 15:13:30 +02:00
fo . format . color_depth = ' 16 '
2024-02-22 17:29:41 +01:00
fo . format . exr_codec = ' PIZ ' # 'ZIP', 'RLE'
2021-09-23 15:13:30 +02:00
2024-02-22 17:29:41 +01:00
## PNG format
2021-09-23 15:13:30 +02:00
# fo.format.file_format = 'PNG'
# fo.format.color_mode = 'RGBA'
# fo.format.color_depth = '8'
# fo.format.compression = 15
2021-09-21 18:23:25 +02:00
2021-10-25 16:02:11 +02:00
def set_scene_aa_settings ( scene = None , aa = True ) :
''' aa == using native AA, else disable scene AA '''
if not scene :
scene = bpy . context . scene
if aa :
scene . eevee . taa_render_samples = 32
scene . grease_pencil_settings . antialias_threshold = 1
else :
scene . eevee . taa_render_samples = 1
scene . grease_pencil_settings . antialias_threshold = 0
def set_settings ( scene = None , aa = True ) :
''' aa == using native AA, else disable scene AA '''
2021-09-07 23:11:42 +02:00
if not scene :
scene = bpy . context . scene
2023-01-18 14:28:27 +01:00
2021-09-07 23:11:42 +02:00
# specify scene settings for these kind of render
2021-10-25 16:02:11 +02:00
set_scene_aa_settings ( scene = scene , aa = aa )
2021-09-10 18:32:50 +02:00
scene . render . film_transparent = True
2021-09-30 18:51:49 +02:00
scene . render . use_compositing = True
scene . render . use_sequencer = False
2021-09-10 18:32:50 +02:00
scene . view_settings . view_transform = ' Standard '
2023-01-18 14:28:27 +01:00
2021-09-23 19:14:48 +02:00
scene . render . resolution_percentage = 100
2021-09-10 18:32:50 +02:00
2021-09-23 15:13:30 +02:00
# output (fast write settings since this is just to delete afterwards...)
2021-10-25 16:02:11 +02:00
scene . render . filepath = f ' //render/preview/ { scene . name } /preview_ '
2023-04-04 11:09:00 +02:00
im_settings = scene . render . image_settings
# im_settings.file_format = 'JPEG'
# im_settings.color_mode = 'RGB'
# im_settings.quality = 0
## Same as output nodes
im_settings . file_format = ' OPEN_EXR '
im_settings . color_mode = ' RGBA '
im_settings . color_depth = ' 16 '
im_settings . exr_codec = ' ZIP '
2021-09-23 15:13:30 +02:00
2021-10-25 16:02:11 +02:00
def scene_aa ( scene = None , toggle = True ) :
''' Change scene AA settings and commute AA nodes according to toggle '''
if not scene :
scene = bpy . context . scene
2024-04-16 18:01:10 +02:00
# Enable/disable native anti-alias on active scene
2023-01-18 14:28:27 +01:00
set_scene_aa_settings ( scene = scene , aa = toggle )
2024-04-16 18:01:10 +02:00
## Set AA on scene where object and viewlayers exists
local_nodes = scene . node_tree . nodes
if ( group_node := next ( ( n for n in local_nodes if n . name . startswith ( ' NG_ ' ) ) , None ) ) :
# Get a viewlayer connected to a NG_ and check which scene is referred
if ( rlayer := next ( ( i . links [ 0 ] . from_node for i in group_node . inputs if i . links and i . links [ 0 ] . from_node . type == ' R_LAYERS ' ) , None ) ) :
if rlayer . scene and rlayer . scene != scene :
print ( f ' Set AA to { toggle } on scene " { rlayer . scene . name } " ' )
set_scene_aa_settings ( scene = rlayer . scene , aa = toggle )
## Mute/Unmute AA nodegroups
for n in local_nodes :
2021-10-25 16:02:11 +02:00
if n . type == ' GROUP ' and n . name . startswith ( ' NG_ ' ) :
# n.mute = False # mute whole nodegroup ?
for gn in n . node_tree . nodes :
if gn . type == ' GROUP ' and gn . node_tree . name == ' AA ' :
gn . mute = toggle
2021-09-23 15:13:30 +02:00
2021-09-30 18:51:49 +02:00
def new_scene_from ( name , src_scn = None , regen = True , crop = True , link_cam = True , link_light = True ) :
''' Get / Create a scene from name and source scene to get settings from '''
scn = bpy . data . scenes . get ( name )
if scn and not regen :
return scn
elif scn and regen :
bpy . data . scenes . remove ( scn )
src_scn = src_scn or bpy . context . scene # given scene, or active scene
scn = bpy . data . scenes . new ( name )
## copy original settings over to new scene
# copy_settings(current, scn) # BAD
for attr in [ ' frame_start ' , ' frame_end ' , ' frame_current ' , ' camera ' , ' world ' ] :
setattr ( scn , attr , getattr ( src_scn , attr ) )
copy_settings ( src_scn . render , scn . render )
2023-01-18 14:28:27 +01:00
2021-09-30 18:51:49 +02:00
## link cameras (and lights ?)
for ob in src_scn . objects :
if link_cam and ob . type == ' CAMERA ' :
scn . collection . objects . link ( ob )
if link_light and ob . type == ' LIGHT ' :
scn . collection . objects . link ( ob )
2021-10-25 20:11:59 +02:00
# set adapted render settings
2021-09-30 18:51:49 +02:00
set_settings ( scn )
2023-01-18 14:28:27 +01:00
2021-09-30 18:51:49 +02:00
if crop :
scn . render . use_border = True
scn . render . use_crop_to_border = True
scn . use_nodes = True
return scn
2021-09-07 23:11:42 +02:00
2024-04-16 18:01:10 +02:00
def get_compo_scene ( scene_name = None , create = True ) :
''' Get / Create a dedicated compositing scene to link GP
use passed scene name , if no name is passed fall back to compo_scene propertie
return None if field is empty '''
scene_name = scene_name or bpy . context . scene . gp_render_settings . node_scene
if not scene_name :
# return None or render scene
return
scn = bpy . data . scenes . get ( scene_name )
if scn :
return scn
if not create :
return
## -- Create render scene
current = bpy . context . scene
## With data
scn = bpy . data . scenes . new ( scene_name )
## copy original settings over to new scene
for attr in [ ' frame_start ' , ' frame_end ' , ' frame_current ' ] : # , 'camera', 'world'
setattr ( scn , attr , getattr ( current , attr ) )
copy_settings ( current . render , scn . render )
2024-07-31 16:48:08 +02:00
## Copy markers
2024-07-31 17:07:16 +02:00
# for marker in current.timeline_markers:
# new_marker = scn.timeline_markers.new(marker.name, frame=marker.frame)
# new_marker.camera = marker.camera
2024-07-31 16:48:08 +02:00
2024-04-16 18:01:10 +02:00
scn . use_nodes = True
## Clear node tree
scn . node_tree . nodes . clear ( )
set_settings ( scn )
scn . gp_render_settings [ ' use_aa ' ] = True
2024-11-14 12:38:53 +01:00
# Set compo scene target in it's own property as well
scn . gp_render_settings . node_scene = scene_name
2024-04-16 18:01:10 +02:00
return scn
def get_render_scene ( scene_name = None , create = True ) :
''' Get / Create a dedicated render scene to link GP '''
scene_name = scene_name or RD_SCENE_NAME
render_scn = bpy . data . scenes . get ( scene_name )
2021-10-18 12:09:52 +02:00
if render_scn :
return render_scn
2024-11-14 12:38:53 +01:00
2024-04-16 18:01:10 +02:00
if not create :
return
2021-10-18 12:09:52 +02:00
2023-01-18 14:28:27 +01:00
## -- Create render scene
2021-10-18 12:09:52 +02:00
current = bpy . context . scene
2023-01-17 15:50:10 +01:00
## With data
2024-04-16 18:01:10 +02:00
render_scn = bpy . data . scenes . new ( scene_name )
2023-01-17 15:50:10 +01:00
2021-10-18 12:09:52 +02:00
## copy original settings over to new scene
# copy_settings(current, render_scn) # BAD
for attr in [ ' frame_start ' , ' frame_end ' , ' frame_current ' , ' camera ' , ' world ' ] :
setattr ( render_scn , attr , getattr ( current , attr ) )
copy_settings ( current . render , render_scn . render )
2023-01-18 14:28:27 +01:00
2021-10-18 12:09:52 +02:00
## link cameras (and lights ?)
2023-01-17 15:50:10 +01:00
for ob in current . objects :
2021-10-18 12:09:52 +02:00
if ob . type in ( ' CAMERA ' , ' LIGHT ' ) :
render_scn . collection . objects . link ( ob )
2024-11-14 12:38:53 +01:00
2024-07-31 16:48:08 +02:00
## Copy markers
for marker in current . timeline_markers :
new_marker = render_scn . timeline_markers . new ( marker . name , frame = marker . frame )
new_marker . camera = marker . camera
2021-09-08 18:29:10 +02:00
2021-09-18 18:07:44 +02:00
render_scn . use_nodes = True
2023-01-18 14:28:27 +01:00
2023-01-05 16:03:25 +01:00
## Clear node tree (initial view layer stuff)
render_scn . node_tree . nodes . clear ( )
# for n in reversed(render_scn.node_tree.nodes):
# render_scn.node_tree.nodes.remove(n)
2021-10-18 12:09:52 +02:00
2021-10-25 20:11:59 +02:00
set_settings ( render_scn )
2024-04-16 18:01:10 +02:00
render_scn . gp_render_settings [ ' use_aa ' ] = True
2021-09-18 18:07:44 +02:00
return render_scn
2021-09-07 23:11:42 +02:00
def get_view_layer ( name , scene = None ) :
''' get viewlayer name
return existing / created viewlayer
'''
2024-04-16 18:01:10 +02:00
scene = scene or get_render_scene ( )
2021-09-07 23:11:42 +02:00
### 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 )
2022-06-01 16:50:59 +02:00
pass_vl . use_pass_z = True
2021-09-07 23:11:42 +02:00
return pass_vl
2023-01-17 15:50:10 +01:00
def set_resolution_from_cam_prop ( cam = None , scene = None ) :
if scene is None :
scene = bpy . context . scene
if not cam :
cam = scene . camera
if not cam :
return ( ' ERROR ' , ' No active camera ' )
res = cam . get ( ' resolution ' )
if not res :
return ( ' ERROR ' , ' Cam has no resolution attribute ' )
rd = scene . render
if rd . resolution_x == res [ 0 ] and rd . resolution_y == res [ 1 ] :
return ( ' INFO ' , f ' Resolution already at { res [ 0 ] } x { res [ 1 ] } ' )
else :
rd . resolution_x , rd . resolution_y = res [ 0 ] , res [ 1 ]
return ( ' INFO ' , f ' Resolution to { res [ 0 ] } x { res [ 1 ] } ' )
2022-02-09 12:27:29 +01:00
## -- node location tweaks
2021-09-07 23:11:42 +02:00
def real_loc ( n ) :
if not n . parent :
return n . location
return n . location + real_loc ( n . parent )
def get_frame_transform ( f , node_tree = None ) :
''' Return real transform location of a frame node
only works with one level of nesting ( not recursive )
'''
if not node_tree :
node_tree = f . id_data
if f . type != ' FRAME ' :
return
# return real_loc(f), f.dimensions
childs = [ n for n in node_tree . nodes if n . parent == f ]
2024-04-16 18:01:10 +02:00
if not childs :
return f . location , f . dimensions
2021-09-07 23:11:42 +02:00
# real_locs = [f.location + n.location for n in childs]
2023-01-18 14:28:27 +01:00
2021-09-07 23:11:42 +02:00
xs = [ n . location . x for n in childs ] + [ n . location . x + n . dimensions . x for n in childs ]
ys = [ n . location . y for n in childs ] + [ n . location . y - n . dimensions . y for n in childs ]
2023-01-18 14:28:27 +01:00
xs . sort ( key = lambda loc : loc ) # x val : ascending
2021-09-07 23:11:42 +02:00
ys . sort ( key = lambda loc : loc ) # ascending # , reversed=True) # y val : descending
2023-01-18 14:28:27 +01:00
2021-09-07 23:11:42 +02:00
loc = Vector ( ( min ( xs ) , max ( ys ) ) )
dim = Vector ( ( max ( xs ) - min ( xs ) + 60 , max ( ys ) - min ( ys ) + 60 ) )
2023-01-18 14:28:27 +01:00
2021-09-07 23:11:42 +02:00
return loc , dim
2022-02-09 12:27:29 +01:00
## -- get all frames with their real transform.
2021-09-07 23:11:42 +02:00
def bbox ( f , frames ) :
xs = [ ]
ys = [ ]
for n in frames [ f ] : # nodes of passed frame
# Better as Vectors ?
if n . type == ' FRAME ' :
if n not in frames . keys ( ) :
# print(f'frame {n.name} not in frame list')
continue
all_xs , all_ys = bbox ( n , frames ) # frames[n]
xs + = all_xs
ys + = all_ys
else :
loc = real_loc ( n )
xs + = [ loc . x , loc . x + n . dimensions . x ] # + (n.dimensions.x/get_dpi_factor())
ys + = [ loc . y , loc . y - n . dimensions . y ] # - (n.dimensions.y/get_dpi_factor())
2023-01-18 14:28:27 +01:00
# margin ~= 30
2021-09-07 23:11:42 +02:00
# return xs and ys
return [ min ( xs ) - 30 , max ( xs ) + 30 ] , [ min ( ys ) - 30 , max ( ys ) + 30 ]
def get_frames_bbox ( node_tree ) :
''' Return a dic with all frames
ex : { frame_node : ( location , dimension ) , . . . }
'''
# create dic of frame object with his direct child nodes nodes
frames = defaultdict ( list )
frames_bbox = { }
for n in node_tree . nodes :
if not n . parent :
continue
# also contains frames
frames [ n . parent ] . append ( n )
2023-01-18 14:28:27 +01:00
2021-09-07 23:11:42 +02:00
# Dic for bbox coord
for f , nodes in frames . items ( ) :
if f . parent :
continue
2023-01-18 14:28:27 +01:00
2021-09-07 23:11:42 +02:00
xs , ys = bbox ( f , frames )
# xs, ys = bbox(nodes, frames)
2023-01-18 14:28:27 +01:00
2021-09-07 23:11:42 +02:00
## returning: list of corner coords
# coords = [
# Vector((xs[0], ys[1])),
# Vector((xs[1], ys[1])),
# Vector((xs[1], ys[0])),
# Vector((xs[0], ys[0])),
# ]
# frames_bbox[f] = coords
## returning: (loc vector, dimensions vector)
frames_bbox [ f ] = Vector ( ( xs [ 0 ] , ys [ 1 ] ) ) , Vector ( ( xs [ 1 ] - xs [ 0 ] , ys [ 1 ] - ys [ 0 ] ) )
return frames_bbox
2022-02-09 12:27:29 +01:00
## -- nodes helper functions
2021-09-07 23:11:42 +02:00
2023-06-08 12:39:39 +02:00
def remove_nodes_by_viewlayer ( viewlayer_list , scene = None ) :
''' Take a list of viewlayer and optionaly a scene to target nodetree
remove nodes related to this viewlayer in nodetree
'''
2023-01-06 15:10:10 +01:00
2023-06-08 12:39:39 +02:00
scene = scene or bpy . context . scene
2023-01-06 15:10:10 +01:00
2023-06-08 12:39:39 +02:00
vl_names = [ v . name for v in viewlayer_list ]
2023-01-06 15:10:10 +01:00
2023-06-08 12:39:39 +02:00
for n in reversed ( scene . node_tree . nodes ) :
2023-01-06 15:10:10 +01:00
if n . type == ' R_LAYERS ' and n . layer in vl_names :
for lnk in n . outputs [ 0 ] . links :
grp = lnk . to_node
if grp . type != ' GROUP ' :
continue
if not grp . name . startswith ( ' NG ' ) :
continue
sockin = lnk . to_socket
sockout = grp . outputs . get ( sockin . name )
if not sockout :
continue
2023-01-18 14:28:27 +01:00
2023-01-06 15:10:10 +01:00
for grplink in sockout . links :
if grplink . to_node . type != ' OUTPUT_FILE ' :
continue
fo_socket = grplink . to_socket
fo = grplink . to_node
fo . file_slots . remove ( fo_socket )
2023-01-18 14:28:27 +01:00
2023-01-06 15:10:10 +01:00
# remove input and output from group
# grp.inputs.remove(sockin) # do not clear inside !!
# grp.outputs.remove(sockout) # do not clear inside !!
2024-01-16 13:56:18 +01:00
2023-01-06 15:10:10 +01:00
ngroup = grp . node_tree
2024-01-16 13:56:18 +01:00
if bpy . app . version < ( 4 , 0 , 0 ) :
for i in range ( len ( grp . inputs ) ) [ : : - 1 ] :
if grp . inputs [ i ] . name == sockin . name :
ngroup . inputs . remove ( ngroup . inputs [ i ] )
break
for i in range ( len ( grp . outputs ) ) [ : : - 1 ] :
if grp . outputs [ i ] . name == sockout . name :
ngroup . outputs . remove ( ngroup . outputs [ i ] )
break
else :
g_inputs = [ s for s in ngroup . interface . items_tree if s . in_out == ' INPUT ' ]
g_outputs = [ s for s in ngroup . interface . items_tree if s . in_out == ' OUTPUT ' ]
for i in range ( len ( grp . inputs ) ) [ : : - 1 ] :
if grp . inputs [ i ] . name == sockin . name :
ngroup . interface . remove ( g_inputs [ i ] )
break
for i in range ( len ( grp . outputs ) ) [ : : - 1 ] :
if grp . outputs [ i ] . name == sockout . name :
ngroup . interface . remove ( g_outputs [ i ] )
break
2023-01-18 14:28:27 +01:00
2023-06-08 12:39:39 +02:00
# Remove render_layer node
scene . node_tree . nodes . remove ( n )
2024-04-16 18:01:10 +02:00
def merge_gplayer_viewlayers ( ob = None , act = None , layers = None , scene = None ) :
2023-06-08 12:39:39 +02:00
''' ob is not needed if active and layers are passed '''
if ob is None :
ob = bpy . context . object
if act is None :
act = ob . data . layers . active
if layers is None :
layers = [ l for l in ob . data . layers if l . select and l != act ]
if act is None :
return ( { ' ERROR ' } , ' Active layer not found. Should be active layer on active object! ' )
2024-04-16 18:01:10 +02:00
rd_scn = scene or get_render_scene ( create = False ) # bpy.context.scene
node_scene = get_compo_scene ( create = False ) or rd_scn
2023-06-08 12:39:39 +02:00
if not act . viewlayer_render :
return ( { ' ERROR ' } , f ' Active layer { act . info } has no viewlayer assigned ' )
2023-06-08 15:28:52 +02:00
# List layers and viewlayers
2023-06-08 12:39:39 +02:00
vls = [ rd_scn . view_layers . get ( l . viewlayer_render ) for l in layers
if l . viewlayer_render and l . viewlayer_render != act . viewlayer_render and rd_scn . view_layers . get ( l . viewlayer_render ) ]
2023-06-08 15:28:52 +02:00
# Remove duplication
vls = list ( set ( vls ) )
2023-06-08 12:39:39 +02:00
2024-04-16 18:01:10 +02:00
2023-06-08 12:39:39 +02:00
# Remove viewlayer related nodes
2024-04-16 18:01:10 +02:00
# FIXME it's possible nodes are nodes searched in the right scene if launched from RenderGP
remove_nodes_by_viewlayer ( vls , node_scene ) # send compositing scene
2023-01-18 14:28:27 +01:00
2023-06-08 12:39:39 +02:00
# Assign view layer from active to selected
2023-01-06 15:10:10 +01:00
for l in layers :
l . viewlayer_render = act . viewlayer_render
2023-06-08 12:39:39 +02:00
## Delete unused viewlayers ()
2023-01-06 15:10:10 +01:00
2023-06-08 12:39:39 +02:00
used_vl_name = [ n . layer for n in rd_scn . node_tree . nodes if n . type == ' R_LAYERS ' and n . layer ]
2023-06-08 15:28:52 +02:00
used_vl_name = list ( set ( used_vl_name ) )
2023-01-06 15:10:10 +01:00
for vl in vls :
2023-06-08 12:39:39 +02:00
# rd_scn.view_layers.remove(vl)
if vl . name == ' exclude ' :
# keep exclude
continue
if not vl . name in used_vl_name :
rd_scn . view_layers . remove ( vl )
2023-01-06 15:10:10 +01:00
def group_adjacent_layer_prefix_rlayer ( ob , excluded_prefix = [ ] , first_name = True ) :
''' Set viewlayer and renderlayers by Gp layer adjacent prefix
Call merge_gplayer_viewlayers with grouped prefix
: excluded_prefix : List of prefixes to exclude from merge or str with comma separated values
: first_name : Keep the viewlayer of the bottom layer in group , else last
'''
2024-09-04 15:46:15 +02:00
pattern = os . environ . get ( ' GP_RENDER_MERGE_PATTERN ' , r ' ^([A-Z] {2} )_ ' )
2024-09-19 18:03:24 +02:00
re_prefix = re . compile ( pattern , flags = re . IGNORECASE )
2023-01-18 14:28:27 +01:00
2023-01-06 15:10:10 +01:00
if isinstance ( excluded_prefix , str ) :
excluded_prefix = [ p . strip ( ) for p in excluded_prefix . split ( ' , ' ) ]
2023-01-18 14:28:27 +01:00
## Create adjacent grp list: [('CO', [layer1, layer2]), ('LN', [layer3, layer4])]
2023-01-06 15:10:10 +01:00
adjacent_prefix_groups = [
2023-01-18 14:28:27 +01:00
( g [ 0 ] , list ( g [ 1 ] ) ) for g in
2023-01-06 15:10:10 +01:00
groupby ( [ l for l in ob . data . layers ] ,
key = lambda l : re_prefix . search ( l . info ) . group ( 1 ) if re_prefix . search ( l . info ) else ' ' )
]
for prefix , layer_grp in adjacent_prefix_groups :
2023-01-11 18:54:33 +01:00
## Remove layer that are in excluded viewlayer
## Else None/exclusion vl can expand rest of the adjacent layers
for l in reversed ( layer_grp ) :
if not l . viewlayer_render or l . viewlayer_render == ' exclude ' :
print ( f ' prefix " { prefix } " : remove " { l . info } " from grouping adjacent layers ' )
layer_grp . remove ( l ) # remove traget the layer directly
2023-01-06 15:10:10 +01:00
if len ( layer_grp ) < 2 :
continue
if not prefix or prefix in excluded_prefix :
continue
ref = layer_grp [ 0 ] if first_name else layer_grp [ - 1 ]
2023-01-11 18:54:33 +01:00
2023-01-06 15:10:10 +01:00
merge_gplayer_viewlayers ( ob , act = ref , layers = layer_grp )
2021-09-07 23:11:42 +02:00
def clear_nodegroup ( name , full_clear = False ) :
''' remove duplication of a nodegroup (.???)
also remove the base one if full_clear True
'''
for ng in reversed ( bpy . data . node_groups ) :
pattern = name + r ' \ . \ d {3} '
2021-09-10 18:32:50 +02:00
if not full_clear and ng . users :
continue
2021-09-07 23:11:42 +02:00
if re . search ( pattern , ng . name ) :
bpy . data . node_groups . remove ( ng )
if full_clear and ng . name == name :
# if full clear
bpy . data . node_groups . remove ( ng )
2021-12-09 12:14:57 +01:00
def rearrange_rlayers_in_frames ( node_tree ) :
''' rearrange RL nodes in all frames in nodetree '''
frames_l = [ n for n in node_tree . nodes if n . type == ' FRAME ' ]
for f in frames_l :
all_in_frames = [ n for n in node_tree . nodes if n . parent == f ]
rlayers = [ n for n in all_in_frames if n . type == ' R_LAYERS ' ]
if not rlayers :
continue
all_in_frames . sort ( key = lambda x : x . location . y , reverse = True ) # descending
rlayers . sort ( key = lambda x : x . location . y , reverse = True ) # descending
top = all_in_frames [ 0 ] . location . y
for rl in rlayers :
# move to top with equal size
rl . location . y = top
2023-01-17 15:50:10 +01:00
if rl . dimensions . y == 0 :
# Newly created nodes
top - = 180 + 20 # down by probable size + gap of 20
else :
top - = rl . dimensions . y + 20 # place next down by height + gap of 20
2021-12-09 12:14:57 +01:00
2021-09-07 23:11:42 +02:00
def rearrange_frames ( node_tree ) :
frame_d = get_frames_bbox ( node_tree ) # dic : {frame_node:(loc vector, dimensions vector), ...}
if not frame_d :
print ( ' no frame found ' )
return
2021-09-15 19:36:06 +02:00
# print([f.name for f in frame_d.keys()])
2021-09-07 23:11:42 +02:00
## order the dict by frame.y location
frame_d = { key : value for key , value in sorted ( frame_d . items ( ) , key = lambda pair : pair [ 1 ] [ 0 ] . y - pair [ 1 ] [ 1 ] . y , reverse = True ) }
frames = [ [ f , v [ 0 ] , v [ 1 ] . y ] for f , v in frame_d . items ( ) ] # [frame_node, real_loc, real dimensions]
2023-01-18 14:28:27 +01:00
2021-09-07 23:11:42 +02:00
top = frames [ 0 ] [ 1 ] . y # upper node location.y
2021-09-16 00:19:57 +02:00
# top = 0 #always start a 0
2021-09-07 23:11:42 +02:00
offset = 0
for f in frames :
## f[1] : real loc Vector
## f[0] : frame
## move frame by offset needed (delta between real_loc and "fake" loc , minus offset)
2021-09-16 00:19:57 +02:00
f [ 0 ] . location . y = ( f [ 1 ] . y - f [ 0 ] . location . y ) - offset # avoid offset when recalculating from 0 top
2021-09-07 23:11:42 +02:00
# f[0].location.y = f[1].y - top - offset
2021-09-16 00:19:57 +02:00
offset + = f [ 2 ] + 200 # gap
2023-01-18 14:28:27 +01:00
2021-09-07 23:11:42 +02:00
f [ 0 ] . update ( )
def reorder_inputs ( ng ) :
rl_nodes = [ s . links [ 0 ] . from_node for s in ng . inputs if s . is_linked and s . links and s . links [ 0 ] . from_node . type == ' R_LAYERS ' ]
rl_nodes . sort ( key = lambda x : x . location . y , reverse = True )
names = [ n . layer for n in rl_nodes ]
inputs_names = [ s . name for s in ng . inputs ]
filtered_names = [ n for n in names if n in inputs_names ]
2024-01-16 12:27:10 +01:00
if bpy . app . version < ( 4 , 0 , 0 ) :
for dest , name in enumerate ( filtered_names ) :
## rebuild list at each iteration so index are good
inputs_names = [ s . name for s in ng . inputs ]
src = inputs_names . index ( name )
# reorder on node_tree not directly on node!
ng . node_tree . inputs . move ( src , dest )
else :
n_inputs = [ s for s in ng . node_tree . interface . items_tree if s . in_out == ' INPUT ' ]
for dest , name in enumerate ( filtered_names ) :
item = next ( ( s for s in ng . node_tree . interface . items_tree if s . in_out == ' INPUT ' and s . name == name ) , None )
if not item : # Dbg
print ( f ' !PROBLEM with input " { name } " ' )
continue
# Need to offset index (inputs are listed after output in item_tree list)
dest = dest + n_inputs [ 0 ] . position
ng . node_tree . interface . move ( item , dest )
ng . node_tree . interface_update ( bpy . context )
2021-09-07 23:11:42 +02:00
def reorder_outputs ( ng ) :
ordered_out_name = [ nis . name for nis in ng . inputs if nis . name in [ o . name for o in ng . outputs ] ]
for s_name in ordered_out_name :
all_outnames = [ o . name for o in ng . outputs ]
# reorder on nodetree, not on node !
2024-01-16 12:27:10 +01:00
if bpy . app . version < ( 4 , 0 , 0 ) :
ng . node_tree . outputs . move ( all_outnames . index ( s_name ) , ordered_out_name . index ( s_name ) )
else :
item = next ( ( s for s in ng . node_tree . interface . items_tree if s . in_out == ' OUTPUT ' and s . name == s_name ) , None )
if not item : # Dbg
print ( f ' !PROBLEM with output " { s_name } " ' )
continue
ng . node_tree . interface . move ( item , ordered_out_name . index ( s_name ) )
ng . node_tree . interface_update ( bpy . context )
2021-09-07 23:11:42 +02:00
def clear_disconnected ( fo ) :
for inp in reversed ( fo . inputs ) :
if not inp . is_linked :
print ( f ' Deleting unlinked fileout slot: { inp . name } ' )
fo . inputs . remove ( inp )
def reorder_fileout ( fo , ng = None ) :
if not ng : # get connected nodegroup
for s in fo . inputs :
if s . is_linked and s . links and s . links [ 0 ] . from_node . type == ' GROUP ' :
ng = s . links [ 0 ] . from_node
break
if not ng :
print ( f ' No nodegroup to refer to filter { fo . name } ' )
return
ordered = [ o . links [ 0 ] . to_socket . name for o in ng . outputs if o . is_linked and o . is_linked and o . links [ 0 ] . to_node == fo ]
for s_name in ordered :
all_outnames = [ s . name for s in fo . inputs ] # same as [fs.path for fs in fo.file_slots]
fo . inputs . move ( all_outnames . index ( s_name ) , ordered . index ( s_name ) )
2021-09-10 18:32:50 +02:00
def reorganise_NG_nodegroup ( ng ) :
''' refit node content to avoid overlap '''
ngroup = ng . node_tree
ng_in = ngroup . nodes . get ( ' Group Input ' )
offset = 35
y = 0
for s in ng_in . outputs :
if s . is_linked :
s . links [ 0 ] . to_node . location . y = y
y - = offset
2021-09-07 23:11:42 +02:00
def connect_to_group_output ( n ) :
for o in n . outputs :
if o . is_linked :
if o . links [ 0 ] . to_node . type == ' GROUP_OUTPUT ' :
return o . links [ 0 ] . to_socket
val = connect_to_group_output ( o . links [ 0 ] . to_node )
if val :
return val
return False
def connect_to_group_input ( n ) :
for i in n . inputs :
if i . is_linked :
if i . links [ 0 ] . from_node . type == ' GROUP_INPUT ' :
return i . links [ 0 ] . from_socket
val = connect_to_group_input ( i . links [ 0 ] . from_node )
if val :
return val
2021-09-08 18:29:10 +02:00
return False
2021-09-23 19:14:48 +02:00
def all_connected_forward ( n , nlist = [ ] ) :
''' return list of all forward connected nodes recursively (include passed nodes) '''
for o in n . outputs :
if o . is_linked :
for lnk in o . links :
if lnk . to_node . type == ' GROUP_OUTPUT ' :
if n not in nlist :
return nlist + [ n ]
else :
return nlist
else :
2023-01-18 14:28:27 +01:00
nlist = all_connected_forward ( lnk . to_node , nlist )
2021-09-23 19:14:48 +02:00
if n in nlist :
return nlist
return nlist + [ n ]
2022-01-26 16:32:33 +01:00
def all_connected_forward_from_socket ( socket ) :
''' return a list of all nodes connected forward after socket '''
node_list = [ ]
for ln in socket . links :
for n in all_connected_forward ( ln . to_node ) :
if n not in node_list :
node_list . append ( n )
# node_list = list(set(node_list))
return node_list
def node_height ( n ) :
return n . height if not n . hide else 30
def reorder_nodegroup_content ( ngroup ) :
if isinstance ( ngroup , bpy . types . Node ) :
ngroup = ngroup . node_tree
grp_in = None
for n in ngroup . nodes :
if n . type == ' GROUP_INPUT ' :
grp_in = n
break
if not grp_in :
return
n_threads = [ ]
for out in grp_in . outputs :
n_thread = all_connected_forward_from_socket ( out )
if n_thread :
n_threads . append ( n_thread )
2023-01-18 14:28:27 +01:00
2022-01-26 16:32:33 +01:00
level = grp_in . location . y
for thread in n_threads :
top = max ( [ n . location . y for n in thread ] )
bottom = min ( [ n . location . y - node_height ( n ) for n in thread ] )
thread_h = top - bottom
# move all nodes to adjust to level
diff_to_add = level - top
for n in thread :
n . location . y + = diff_to_add
# move level to bottom
level - = thread_h + 2 # add a gap of two
2021-09-23 19:14:48 +02:00
2021-09-10 18:32:50 +02:00
def clear_nodegroup_content_if_disconnected ( ngroup ) :
''' Get a nodegroup.node_tree
delete orphan nodes that are not connected from group input node
'''
2022-01-26 16:32:33 +01:00
if isinstance ( ngroup , bpy . types . Node ) :
# case where a node is sent instead of the group
ngroup = ngroup . node_tree
2021-09-10 18:32:50 +02:00
for n in reversed ( ngroup . nodes ) :
if n . type in ( ' GROUP_INPUT ' , ' GROUP_OUTPUT ' ) :
continue
if not connect_to_group_input ( n ) and not connect_to_group_output ( n ) : # is disconnected from both side
ngroup . nodes . remove ( n )
2023-01-18 14:28:27 +01:00
2022-01-26 16:32:33 +01:00
reorder_nodegroup_content ( ngroup )
2021-09-10 18:32:50 +02:00
2021-09-15 18:28:16 +02:00
def clean_nodegroup_inputs ( ng , skip_existing_pass = True ) :
2021-09-10 18:32:50 +02:00
''' Clear inputs to output of passed nodegroup if not connected '''
ngroup = ng . node_tree
2021-09-15 18:28:16 +02:00
rl_nodes = [ n . layer for n in ng . id_data . nodes if n . type == ' R_LAYERS ' ]
2021-09-10 18:32:50 +02:00
for i in range ( len ( ng . inputs ) ) [ : : - 1 ] :
2021-09-15 18:28:16 +02:00
if not ng . inputs [ i ] . is_linked :
if skip_existing_pass and any ( ng . inputs [ i ] . name == x for x in rl_nodes ) :
# a render layer of this name still exists
continue
2021-09-10 18:32:50 +02:00
ngroup . inputs . remove ( ngroup . inputs [ i ] )
# clear_nodegroup_content_if_disconnected(ngroup)
2021-10-01 12:42:21 +02:00
def bridge_reconnect_nodegroup ( ng , socket_name = None ) :
'''
Reconnect group_in and group out that have been disconnected
: socket : only use this specific socket type
'''
ngroup = ng . node_tree
ng_in = ngroup . nodes . get ( ' Group Input ' )
ng_out = ngroup . nodes . get ( ' Group Output ' )
for sockin in ng_in . outputs :
if socket_name and sockin . name != socket_name :
continue
if not sockin . name : # last empty output is listed
continue
sockout = ng_out . inputs . get ( sockin . name )
if not sockout :
continue
if len ( sockin . links ) and connect_to_group_output ( sockin . links [ 0 ] . to_node ) :
continue
## need reconnect
aa = create_aa_nodegroup ( ngroup )
ngroup . links . new ( sockin , aa . inputs [ 0 ] )
ngroup . links . new ( aa . outputs [ 0 ] , sockout )
print ( f ' { ng . name } : Bridged { sockin . name } ' )
2023-01-18 14:28:27 +01:00
2021-10-01 12:42:21 +02:00
2021-09-08 18:29:10 +02:00
def random_color ( alpha = False ) :
import random
if alpha :
return ( random . uniform ( 0 , 1 ) , random . uniform ( 0 , 1 ) , random . uniform ( 0 , 1 ) , 1 )
2021-09-10 18:32:50 +02:00
return ( random . uniform ( 0 , 1 ) , random . uniform ( 0 , 1 ) , random . uniform ( 0 , 1 ) )
2023-04-04 15:51:41 +02:00
def nodegroup_merge_inputs ( ngroup , aa = True ) :
2021-09-10 22:59:52 +02:00
''' Get a nodegroup
merge every group inputs with alpha over
then connect to antialias and a new output
'''
ng_in = ngroup . nodes . get ( ' Group Input ' )
ng_out = ngroup . nodes . get ( ' Group Output ' )
x , y = ng_in . location . x + 200 , 0
offset_x , offset_y = 150 , - 100
# merge all inputs in alphaover nodes
prev = None
for i in range ( len ( ng_in . outputs ) - 1 ) : # skip waiting point
inp = ng_in . outputs [ i ]
if not prev :
prev = ng_in
continue
# live connect
ao = create_node ( ' CompositorNodeAlphaOver ' , tree = ngroup , location = ( x , y ) , hide = True )
ngroup . links . new ( prev . outputs [ 0 ] , ao . inputs [ 1 ] )
ngroup . links . new ( inp , ao . inputs [ 2 ] )
2023-01-18 14:28:27 +01:00
2021-09-10 22:59:52 +02:00
x + = offset_x
y + = offset_y
prev = ao
2024-02-22 11:56:18 +01:00
# Create one output and link
out_name = ' preview '
if bpy . app . version < ( 4 , 0 , 0 ) :
# out_name = ngroup.inputs[0].name # name like first inputout_name
out = ngroup . outputs . new ( ' NodeSocketColor ' , out_name )
else :
# out_name = next((s for s in ngroup.interface.items_tree if s.in_out == 'INPUT')).name # name like first input
out = ngroup . interface . new_socket ( out_name , in_out = ' OUTPUT ' , socket_type = ' NodeSocketColor ' )
2023-04-04 15:51:41 +02:00
## create a merged name as output ??
if aa :
# create AA and link
aa = create_aa_nodegroup ( ngroup ) # new_aa_node(ngroup)
aa . location = ( ao . location . x + 200 , ao . location . y )
ngroup . links . new ( ao . outputs [ 0 ] , aa . inputs [ 0 ] ) # node_tree
ngroup . links . new ( aa . outputs [ 0 ] , ng_out . inputs [ 0 ] )
else :
# link directly
ngroup . links . new ( ao . outputs [ 0 ] , ng_out . inputs [ 0 ] )
2021-09-10 22:59:52 +02:00
2022-02-09 12:27:29 +01:00
## -- renumbering funcs
2021-09-10 18:32:50 +02:00
def get_numbered_output ( out , slot_name ) :
''' Return output slot name without looking for numbering ???_
'''
pattern = r ' ^(?: \ d {3} _)? ' # optional non capture group of 3 digits + _
pattern = f ' { pattern } { slot_name } '
for inp in out . inputs :
if re . match ( pattern , inp . name ) :
return inp
def add_fileslot_number ( fs , number ) :
2024-02-22 17:29:41 +01:00
field_attr = ' name ' if hasattr ( fs , ' name ' ) else ' path '
elems = getattr ( fs , field_attr ) . split ( ' / ' )
2021-09-10 18:32:50 +02:00
for i , e in enumerate ( elems ) :
if re . match ( r ' ^ \ d {3} _ ' , e ) :
elems [ i ] = re . sub ( r ' ^( \ d {3} ) ' , lambda x : str ( number ) . zfill ( 3 ) , e )
else :
elems [ i ] = f ' { str ( number ) . zfill ( 3 ) } _ { e } '
new = ' / ' . join ( elems )
2024-02-22 17:29:41 +01:00
setattr ( fs , field_attr , new )
2021-09-10 18:32:50 +02:00
return new
def renumber ( fo , offset = 10 ) :
''' Force renumber all the slots with a 3 '''
2024-02-22 17:29:41 +01:00
2021-09-10 18:32:50 +02:00
if fo . type != ' OUTPUT_FILE ' : return
2021-09-10 22:59:52 +02:00
ct = 10 # start at 10
2024-02-22 17:29:41 +01:00
slots = fo . layer_slots if fo . format . file_format == ' OPEN_EXR_MULTILAYER ' else fo . file_slots
for fs in slots :
2021-09-10 18:32:50 +02:00
add_fileslot_number ( fs , ct )
ct + = offset
2021-09-10 22:59:52 +02:00
def get_num ( string ) - > int :
''' get a tring or a file_slot object
return leading number or None
'''
if not isinstance ( string , str ) :
2024-02-22 17:29:41 +01:00
if hasattr ( string , ' path ' ) :
string = string . path
else :
string = string . name
2021-09-10 18:32:50 +02:00
num = re . search ( r ' ^( \ d {3} )_ ' , string )
if num :
return int ( num . group ( 1 ) )
2021-09-10 22:59:52 +02:00
def delete_numbering ( fo ) : # padding=3
''' Delete prefix numbering on all slots on passed file output '''
2021-09-10 18:32:50 +02:00
if fo . type != ' OUTPUT_FILE ' : return
2024-02-22 17:29:41 +01:00
if fo . format . file_format == ' OPEN_EXR_MULTILAYER ' :
slots = fo . layer_slots
field_attr = ' name '
else :
slots = fo . file_slots
field_attr = ' path '
for fs in slots :
elems = getattr ( fs , field_attr ) . split ( ' / ' )
2021-09-10 22:59:52 +02:00
for i , e in enumerate ( elems ) :
elems [ i ] = re . sub ( r ' ^ \ d {3} _ ' , ' ' , e )
2023-01-18 14:28:27 +01:00
2021-09-10 22:59:52 +02:00
new = ' / ' . join ( elems )
2024-02-22 17:29:41 +01:00
setattr ( fs , field_attr , new )
2021-09-10 18:32:50 +02:00
2021-09-21 19:14:15 +02:00
def reverse_fileout_inputs ( fo ) :
count = len ( fo . inputs )
for i in range ( count ) :
fo . inputs . move ( count - 1 , i )
def renumber_keep_existing ( fo , offset = 10 , invert = True ) :
2021-09-10 22:59:52 +02:00
''' Renumber by keeping existing numbers and inserting new one whenever possible
Big and ugly function that do the trick nonetheless . . .
'''
if fo . type != ' OUTPUT_FILE ' : return
ct = 10
2023-01-18 14:28:27 +01:00
2021-09-21 19:14:15 +02:00
if invert :
reverse_fileout_inputs ( fo )
2021-09-10 22:59:52 +02:00
2024-02-22 17:29:41 +01:00
fsl = fo . layer_slots if fo . format . file_format == ' OPEN_EXR_MULTILAYER ' else fo . file_slots
2021-09-10 18:32:50 +02:00
2021-09-10 22:59:52 +02:00
last_idx = len ( fsl ) - 1
2021-09-10 18:32:50 +02:00
prev = None
prev_num = None
2021-09-10 22:59:52 +02:00
for idx , fs in enumerate ( fsl ) :
2021-09-15 19:36:06 +02:00
# print('-->', idx, fs.path)
2021-09-10 22:59:52 +02:00
2021-09-10 18:32:50 +02:00
if idx == last_idx : # handle last
2021-09-21 19:14:15 +02:00
if get_num ( fs ) is not None :
break
2021-09-10 18:32:50 +02:00
if idx > 0 :
2021-09-10 22:59:52 +02:00
prev = fsl [ idx - 1 ]
num = get_num ( prev )
if num is not None :
2021-09-15 10:34:35 +02:00
add_fileslot_number ( fs , num + offset )
2021-09-10 18:32:50 +02:00
else :
2021-09-10 22:59:52 +02:00
add_fileslot_number ( fs , ct )
2021-09-10 18:32:50 +02:00
else :
2021-09-10 22:59:52 +02:00
add_fileslot_number ( fs , 10 ) # there is only one slot (maybe don't number ?)
2021-09-10 18:32:50 +02:00
break
# update the ct with the current taken number if any
2021-09-10 22:59:52 +02:00
number = get_num ( fs )
if number is not None :
2021-09-10 18:32:50 +02:00
prev = fs
ct = number + offset
continue # skip already numbered
# analyse all next slots until there is numbered
2021-09-10 22:59:52 +02:00
divider = 0
2021-09-15 19:36:06 +02:00
# print(f'range(1, {len(fsl) - idx}')
2021-09-10 22:59:52 +02:00
for i in range ( 1 , len ( fsl ) - idx ) :
next_num = get_num ( fsl [ idx + i ] )
if next_num is not None :
divider = i + 1
2021-09-10 18:32:50 +02:00
break
2021-09-10 22:59:52 +02:00
if idx == 0 : # handle first
2021-09-10 18:32:50 +02:00
prev_num = 0
prev = None
2021-09-10 22:59:52 +02:00
if next_num is None :
add_fileslot_number ( fs , 0 )
elif next_num == 0 :
print ( f ' Cannot insert value before 0 to { fsl . path } ' )
continue
else :
add_fileslot_number ( fs , int ( next_num / 2 ) )
2021-09-10 18:32:50 +02:00
else :
2021-09-10 22:59:52 +02:00
prev = fsl [ idx - 1 ]
test_prev = get_num ( prev )
if test_prev is not None :
prev_num = test_prev
if not divider :
if prev_num is not None :
add_fileslot_number ( fs , prev_num + offset )
else :
add_fileslot_number ( fs , ct )
2023-01-18 14:28:27 +01:00
2021-09-10 22:59:52 +02:00
else :
if prev_num is not None :
# iterate rename
gap_inc = int ( ( next_num - prev_num ) / divider )
if gap_inc < 1 : # same values !
print ( f ' cannot insert a median value at { fs . path } between { prev_num } and { next_num } ' )
continue
ct = prev_num
for temp_id in range ( idx , idx + i ) :
ct + = gap_inc
add_fileslot_number ( fsl [ temp_id ] , ct )
else :
print ( " what ' s going on ? \n " )
2021-09-10 18:32:50 +02:00
# first check if it has a number (if not bas)
prev = fs
2021-09-17 16:31:26 +02:00
ct + = offset
2021-09-21 19:14:15 +02:00
if invert :
reverse_fileout_inputs ( fo )
2021-09-17 16:31:26 +02:00
2021-09-17 18:36:15 +02:00
def has_channel_color ( layer ) :
''' Return True if gp_layer.channel_color is different than the default (0.2, 0.2, 0.2) '''
if not any ( isclose ( i , 0.2 , abs_tol = 0.001 ) for i in layer . channel_color ) :
return True
2021-09-22 12:06:40 +02:00
def normalize ( text ) :
return text . lower ( ) . replace ( ' - ' , ' _ ' )
PATTERN = r ' ^(?P<grp>- \ s)?(?P<tag>[A-Z] {2} _)?(?P<name>.*?)(?P<sfix>_[A-Z] {2} )?(?P<inc> \ . \ d {3} )?$ ' # numering
2023-06-07 12:13:50 +02:00
def normalize_layer_name ( layer , prefix = ' ' , desc = ' ' , suffix = ' ' , lower = True , dash_to_underscore = True , point_to_underscore = True , get_only = False ) :
2021-09-22 12:06:40 +02:00
''' GET a layer and argument to build and assign name '''
import re
name = layer . info
2023-01-18 14:28:27 +01:00
2021-09-22 12:06:40 +02:00
pattern = PATTERN
sep = ' _ '
res = re . search ( pattern , name . strip ( ) )
grp = ' ' if res . group ( ' grp ' ) is None else res . group ( ' grp ' )
tag = ' ' if res . group ( ' tag ' ) is None else res . group ( ' tag ' )
# tag2 = '' if res.group('tag2') is None else res.group('tag2')
name = ' ' if res . group ( ' name ' ) is None else res . group ( ' name ' )
sfix = ' ' if res . group ( ' sfix ' ) is None else res . group ( ' sfix ' )
inc = ' ' if res . group ( ' inc ' ) is None else res . group ( ' inc ' )
if grp :
grp = ' ' + grp # name is strip(), so grp first spaces are gones.
if prefix :
if prefix == ' prefixkillcode ' :
tag = ' '
else :
tag = prefix . upper ( ) . strip ( ) + sep
# if prefix2:
# tag2 = prefix2.upper().strip() + sep
if desc :
name = desc
2023-01-18 14:28:27 +01:00
2021-09-22 12:06:40 +02:00
if suffix :
if suffix == ' suffixkillcode ' :
sfix = ' '
else :
sfix = sep + suffix . upper ( ) . strip ( )
# check if name is available without the increment ending
if lower :
name = name . lower ( )
if dash_to_underscore :
name = name . replace ( ' - ' , ' _ ' )
2023-06-07 12:13:50 +02:00
if point_to_underscore :
name = name . replace ( ' . ' , ' _ ' )
2021-09-22 12:06:40 +02:00
new = f ' { grp } { tag } { name } { sfix } ' # lower suffix ?
2022-01-24 23:14:12 +01:00
if get_only :
return new
2021-09-22 12:06:40 +02:00
if new != layer . info :
2021-10-01 12:58:46 +02:00
old = layer . info
print ( f ' { old } >> { new } ' )
2021-09-22 12:06:40 +02:00
layer . info = new
2023-01-18 14:28:27 +01:00
2021-10-01 12:58:46 +02:00
# Also change name string in modifier target !
for ob in [ o for o in bpy . data . objects if o . type == ' GPENCIL ' and o . data == layer . id_data ] :
for m in ob . grease_pencil_modifiers :
if hasattr ( m , ' layer ' ) and m . layer :
if m . layer == old :
print ( f ' - updated in { ob . name } modifier { m . name } ' )
m . layer = new
2021-09-22 12:06:40 +02:00
2022-02-07 16:05:47 +01:00
# unused currently
def build_dope_gp_list ( layer_list ) :
''' Take a list of GP layers return a dict with pairs { gp data : own layer list} '''
from collections import defaultdict
gps = defaultdict ( list )
for l in layer_list :
gps [ l . id_data ] . append ( l )
return gps
def build_layers_targets_from_dopesheet ( context ) :
''' Return all selected layers on context GP dopesheet according to seelction and filters '''
ob = context . object
gpl = context . object . data . layers
act = gpl . active
dopeset = context . space_data . dopesheet
2023-01-18 14:28:27 +01:00
2022-02-07 16:05:47 +01:00
if dopeset . show_only_selected :
pool = [ o for o in context . selected_objects if o . type == ' GPENCIL ' ]
else :
pool = [ o for o in context . scene . objects if o . type == ' GPENCIL ' ]
if not dopeset . show_hidden :
pool = [ o for o in pool if o . visible_get ( ) ]
layer_pool = [ l for o in pool for l in o . data . layers ]
layer_pool = list ( set ( layer_pool ) ) # remove dupli-layers from same data source with
2023-01-18 14:28:27 +01:00
2022-02-07 16:05:47 +01:00
# apply search filter
if dopeset . filter_text :
layer_pool = [ l for l in layer_pool if ( dopeset . filter_text . lower ( ) in l . info . lower ( ) ) ^ dopeset . use_filter_invert ]
return layer_pool
2022-01-27 18:08:24 +01:00
""" # old show message gox without operator support
2021-09-17 16:31:26 +02:00
def show_message_box ( _message = " " , _title = " Message Box " , _icon = ' INFO ' ) :
2022-01-27 18:08:24 +01:00
''' get a str to display or a list of [str, str]
can have an icon [ [ str , icon ] , str , [ str , icon ] ]
'''
2021-09-17 16:31:26 +02:00
def draw ( self , context ) :
for l in _message :
if isinstance ( l , str ) :
self . layout . label ( text = l )
else :
self . layout . label ( text = l [ 0 ] , icon = l [ 1 ] )
2023-01-18 14:28:27 +01:00
2021-09-17 16:31:26 +02:00
if isinstance ( _message , str ) :
_message = [ _message ]
2021-09-30 18:51:49 +02:00
bpy . context . window_manager . popup_menu ( draw , title = _title , icon = _icon )
2022-01-27 18:08:24 +01:00
"""
def show_message_box ( _message = " " , _title = " Message Box " , _icon = ' INFO ' ) :
''' Show message box with element passed as string or list
if _message if a list of lists :
if sublist have 2 element :
considered a label [ text , icon ]
if sublist have 3 element :
considered as an operator [ ops_id_name , text , icon ]
'''
def draw ( self , context ) :
for l in _message :
if isinstance ( l , str ) :
self . layout . label ( text = l )
else :
if len ( l ) == 2 : # label with icon
self . layout . label ( text = l [ 0 ] , icon = l [ 1 ] )
elif len ( l ) == 3 : # ops
self . layout . operator_context = " INVOKE_DEFAULT "
self . layout . operator ( l [ 0 ] , text = l [ 1 ] , icon = l [ 2 ] , emboss = False ) # <- highligh the entry
2023-01-18 14:28:27 +01:00
2022-01-27 18:08:24 +01:00
if isinstance ( _message , str ) :
_message = [ _message ]
bpy . context . window_manager . popup_menu ( draw , title = _title , icon = _icon )
2021-09-30 18:51:49 +02:00
2022-02-09 12:27:29 +01:00
## -- camera framing and object anim checks
2023-06-20 14:37:39 +02:00
def get_bbox_3d ( ob ) - > list :
2021-09-30 18:51:49 +02:00
bbox_coords = ob . bound_box
return [ ob . matrix_world @ Vector ( b ) for b in bbox_coords ]
2023-06-20 14:37:39 +02:00
def is_render_included ( o , scn ) - > bool :
2021-11-12 17:02:19 +01:00
''' return True if object is in at least one non-excluded collection
in all passed scene viewlayer
'''
if o . hide_render :
return False
for vl in scn . view_layers :
all_cols = get_collection_childs_recursive ( vl . layer_collection )
for c in all_cols :
print ( c . name )
if o in c . collection . objects [ : ] :
if not c . exclude :
return True
return False
2023-06-20 14:37:39 +02:00
def get_crop_pixel_coord ( scn ) - > dict :
2023-01-18 14:28:27 +01:00
# width height probably not needed. might need
2021-09-30 18:51:49 +02:00
px_width = ( scn . render . border_max_x - scn . render . border_min_x ) * scn . render . resolution_x
px_height = ( scn . render . border_max_y - scn . render . border_min_y ) * scn . render . resolution_y
2023-01-18 14:28:27 +01:00
2021-09-30 18:51:49 +02:00
pos_x = ( scn . render . border_min_x + ( ( scn . render . border_max_x - scn . render . border_min_x ) / 2 ) ) * scn . render . resolution_x
## coord y > image center coord from bottom-left (Blender)
# pos_y = (scn.render.border_min_y + ((scn.render.border_max_y - scn.render.border_min_y) / 2)) * scn.render.resolution_y,
2023-01-18 14:28:27 +01:00
2021-09-30 18:51:49 +02:00
## image center coord from top-left (AE)
pos_y = ( ( 1 - scn . render . border_max_y ) + ( ( scn . render . border_max_y - scn . render . border_min_y ) / 2 ) ) * scn . render . resolution_y
coord = {
' position_x ' : round ( pos_x ) ,
' position_y ' : round ( pos_y ) ,
' width ' : round ( px_width ) ,
' height ' : round ( px_height ) ,
2023-06-20 14:37:39 +02:00
' scene_res_x ' : scn . render . resolution_x ,
' scene_res_y ' : scn . render . resolution_y ,
2021-09-30 18:51:49 +02:00
}
return coord
2023-06-20 14:37:39 +02:00
def export_crop_to_json ( ) - > dict :
''' Export crop to json coords for AE '''
2023-01-18 14:28:27 +01:00
2021-09-30 18:51:49 +02:00
blend = Path ( bpy . data . filepath )
2023-06-20 14:37:39 +02:00
## Use a fixed name (easier to load from After effects)
json_path = blend . parent / ' render ' / ' crop_infos.json '
## Use blend name (to support version)
# json_path = blend.parent / 'render' / f'{blend.stem}.json'
2021-09-30 18:51:49 +02:00
## per scene : json_path = Path(bpy.data.filepath).parent / 'render' / f'{scn.name}.json'
2023-06-20 14:37:39 +02:00
# json_path = Path(bpy.data.filepath).parent / 'render' / f'{scn.name}.json' # f'{ob.name}.json'
2023-01-18 14:28:27 +01:00
2021-09-30 18:51:49 +02:00
coord_dic = { }
2023-01-18 14:28:27 +01:00
2021-09-30 18:51:49 +02:00
for scn in bpy . data . scenes :
2021-11-12 15:42:32 +01:00
if scn . render . use_border and scn . render . use_crop_to_border : # Only usefull if cropped
2021-09-30 18:51:49 +02:00
scn_border = get_crop_pixel_coord ( scn )
2021-11-12 15:42:32 +01:00
## Only scn name (meaning only one name to refer if multiple GP)
# coord_dic[scn.name] = scn_border
2021-11-12 17:02:19 +01:00
## use name of first found visible GP (scene name if no visible GP)
gps = [ o for o in scn . objects if o . type == ' GPENCIL ' if is_render_included ( o , scn ) ] # o.visible_get() < only work on active window
2021-11-12 17:14:34 +01:00
if gps and scn . name != ' Scene ' : # always export Scene with Scene name...
2021-11-12 15:42:32 +01:00
for ob in gps :
coord_dic [ ob . name ] = scn_border
print ( f ' Added gp { ob . name } crop info ' )
else :
coord_dic [ scn . name ] = scn_border
print ( f ' Added scene { scn . name } crop info ' )
2021-09-30 18:51:49 +02:00
2021-10-29 16:42:35 +02:00
if coord_dic :
2021-11-05 11:00:22 +01:00
json_path . parent . mkdir ( parents = False , exist_ok = True )
2021-10-29 16:42:35 +02:00
# save bbox
with json_path . open ( ' w ' ) as fd :
json . dump ( coord_dic , fd , indent = ' \t ' )
2023-01-18 14:28:27 +01:00
2021-11-12 15:42:32 +01:00
print ( f ' Coords saved at: { json_path } ' )
2021-09-30 18:51:49 +02:00
return coord_dic
def set_border_region_from_coord ( coords , scn = None , margin = 30 , export_json = True ) :
2021-10-13 19:00:15 +02:00
''' Get a list of point coord in worldcamera view space (0 to 1) on each axis
set border ( of passed scene : scn : ) with given coordinate
return the coords list as pixel coordinate
'''
2021-09-30 18:51:49 +02:00
scn = scn or bpy . context . scene
2023-01-18 14:28:27 +01:00
2021-09-30 18:51:49 +02:00
coords2d_x = sorted ( [ c [ 0 ] for c in coords ] )
coords2d_y = sorted ( [ c [ 1 ] for c in coords ] )
margin_width = margin / scn . render . resolution_x
margin_height = margin / scn . render . resolution_y
2023-01-18 14:28:27 +01:00
2021-09-30 18:51:49 +02:00
# set crop
scn . render . border_min_x = coords2d_x [ 0 ] - margin_width
scn . render . border_max_x = coords2d_x [ - 1 ] + margin_width
scn . render . border_min_y = coords2d_y [ 0 ] - margin_height
scn . render . border_max_y = coords2d_y [ - 1 ] + margin_height
## get clamped relative value
# relative_bbox2d_coords = [
# (scn.render.border_min_x, scn.render.border_min_y),
# (scn.render.border_min_x, scn.render.border_max_y),
# (scn.render.border_max_x, scn.render.border_max_y),
# (scn.render.border_max_x, scn.render.border_min_y),
# ]
pixel_bbox2d_coords = [
( scn . render . border_min_x * scn . render . resolution_x , scn . render . border_min_y * scn . render . resolution_y ) ,
( scn . render . border_min_x * scn . render . resolution_x , scn . render . border_max_y * scn . render . resolution_y ) ,
( scn . render . border_max_x * scn . render . resolution_x , scn . render . border_max_y * scn . render . resolution_y ) ,
( scn . render . border_max_x * scn . render . resolution_x , scn . render . border_min_y * scn . render . resolution_y ) ,
]
# if export_json:
# export_crop_to_json(scn)
return pixel_bbox2d_coords
def get_gp_box_all_frame ( ob , cam = None ) :
''' set crop to object bounding box considering whole animation. Cam should not be animated (render in bg_cam)
2023-01-18 14:28:27 +01:00
return 2 d bbox in pixels
2021-09-30 18:51:49 +02:00
'''
from bpy_extras . object_utils import world_to_camera_view
coords_cam_list = [ ]
scn = bpy . context . scene
cam = cam or scn . camera
start = time ( )
2023-01-18 14:28:27 +01:00
2021-09-30 18:51:49 +02:00
if ob . animation_data and ob . animation_data . action : # use frame set on all frames
print ( f ' { ob . name } has anim ' )
2023-01-18 14:28:27 +01:00
# frame_nums = sorted(list(set([f.frame_number for l in ob.data.layers if len(l.frames) for f in l.frames if len(f.strokes) and scn.frame_start <= f.frame_number <= scn.frame_end])))
2021-09-30 18:51:49 +02:00
for num in range ( scn . frame_start , scn . frame_end + 1 ) :
scn . frame_set ( num )
for l in ob . data . layers :
if l . hide or l . opacity == 0.0 :
continue
if l . active_frame :
for s in l . active_frame . strokes :
if len ( s . points ) == 1 : # skip isolated points
continue
coords_cam_list + = [ world_to_camera_view ( scn , cam , ob . matrix_world @ p . co ) for p in s . points ]
else :
# if object is not animated no need to frame_set to update object position
print ( f ' { ob . name } no anim ' )
for l in ob . data . layers :
if l . hide or l . opacity == 0.0 :
continue
for f in l . frames :
if not ( scn . frame_start < = f . frame_number < = scn . frame_end ) :
continue
for s in f . strokes :
if len ( s . points ) == 1 : # skip isolated points
continue
coords_cam_list + = [ world_to_camera_view ( scn , cam , ob . matrix_world @ p . co ) for p in s . points ]
2023-01-18 14:28:27 +01:00
2021-09-30 18:51:49 +02:00
print ( time ( ) - start ) # Dbg-time
return coords_cam_list
def has_anim ( ob ) :
# TODO make a better check (check if there is only one key in each channel, count as not animated)
return ob . animation_data and ob . animation_data . action
2021-11-04 22:20:21 +01:00
def has_keyframe ( ob , attr ) :
anim = ob . animation_data
if anim is not None and anim . action is not None :
for fcu in anim . action . fcurves :
if fcu . data_path == attr :
return len ( fcu . keyframe_points ) > 0
return False
2021-10-13 19:00:15 +02:00
def get_gp_box_all_frame_selection ( oblist = None , scn = None , cam = None , timeout = 40 ) :
2021-09-30 18:51:49 +02:00
'''
get points of all selection
2023-01-18 14:28:27 +01:00
return 2 d bbox in pixels
2021-10-13 19:00:15 +02:00
return None if timeout ( too long to process , better to do it visually )
2021-09-30 18:51:49 +02:00
'''
from bpy_extras . object_utils import world_to_camera_view
2021-10-13 19:00:15 +02:00
t0 = time ( )
2021-09-30 18:51:49 +02:00
coords_cam_list = [ ]
scn = scn or bpy . context . scene
oblist = oblist or [ o for o in scn . objects if o . select_get ( ) ]
cam = cam or scn . camera
start = time ( )
2023-01-18 14:28:27 +01:00
2021-09-30 18:51:49 +02:00
if any ( has_anim ( ob ) for ob in oblist ) :
print ( f ' at least one is animated: { oblist } ' )
for num in range ( scn . frame_start , scn . frame_end + 1 ) :
scn . frame_set ( num )
for ob in oblist :
for l in ob . data . layers :
2023-06-21 18:33:50 +02:00
if l . hide or l . opacity == 0.0 or l . info . startswith ( ' MA_ ' ) : # hardcoded Mask exclusion !
2021-09-30 18:51:49 +02:00
continue
if not l . active_frame :
continue
for s in l . active_frame . strokes :
if len ( s . points ) == 1 : # skip isolated points
continue
coords_cam_list + = [ world_to_camera_view ( scn , cam , ob . matrix_world @ p . co ) for p in s . points ]
2023-01-18 14:28:27 +01:00
2021-10-13 19:00:15 +02:00
if time ( ) - t0 > timeout :
print ( f ' timeout (more than { timeout } s to calculate) evaluating frame position of objects { oblist } ' )
return
2021-09-30 18:51:49 +02:00
else :
print ( f ' No anim ' )
for ob in oblist :
# if object is not animated no need to frame_set to update object position
for l in ob . data . layers :
2023-06-21 18:33:50 +02:00
if l . hide or l . opacity == 0.0 or l . info . startswith ( ' MA_ ' ) : # hardcoded Mask exclusion !
2021-09-30 18:51:49 +02:00
continue
for f in l . frames :
2021-10-13 19:00:15 +02:00
if time ( ) - t0 > timeout :
print ( f ' timeout (more than { timeout } s to calculate) evaluating frame position of objects { oblist } ' )
return
2021-09-30 18:51:49 +02:00
if not ( scn . frame_start < = f . frame_number < = scn . frame_end ) :
continue
for s in f . strokes :
if len ( s . points ) == 1 : # skip isolated points
continue
coords_cam_list + = [ world_to_camera_view ( scn , cam , ob . matrix_world @ p . co ) for p in s . points ]
2023-01-18 14:28:27 +01:00
2021-09-30 18:51:49 +02:00
print ( f ' { len ( coords_cam_list ) } gp points listed { time ( ) - start : .1f } s ' )
return coords_cam_list
def get_bbox_2d ( ob , cam = None ) :
from bpy_extras . object_utils import world_to_camera_view
2023-01-18 14:28:27 +01:00
scn = bpy . context . scene
2021-09-30 18:51:49 +02:00
cam = cam or scn . camera
2023-01-18 14:28:27 +01:00
coords2d = [ world_to_camera_view ( scn , cam , p ) for p in get_bbox_3d ( ob ) ]
2021-09-30 18:51:49 +02:00
coords2d_x = sorted ( [ c [ 0 ] for c in coords2d ] )
coords2d_y = sorted ( [ c [ 1 ] for c in coords2d ] )
bbox2d_coords = [
( coords2d_x [ 0 ] , coords2d_y [ 0 ] ) ,
( coords2d_x [ 0 ] , coords2d_y [ - 1 ] ) ,
( coords2d_x [ - 1 ] , coords2d_y [ - 1 ] ) ,
( coords2d_x [ - 1 ] , coords2d_y [ 0 ] ) ,
]
return [ Vector ( b ) for b in bbox2d_coords ]
def set_box_from_selected_objects ( scn = None , cam = None , export_json = False ) :
2023-01-18 14:28:27 +01:00
scn = scn or bpy . context . scene
2021-09-30 18:51:49 +02:00
cam = cam or scn . camera
2023-01-18 14:28:27 +01:00
2021-09-30 18:51:49 +02:00
selection = [ o for o in scn . objects if o . select_get ( ) ] # selected_objects
coords = get_gp_box_all_frame_selection ( oblist = selection , scn = scn , cam = cam )
2021-10-13 19:00:15 +02:00
if not coords :
return f ' Border not set: Timeout during analysis of { len ( selection ) } objects '
2021-09-30 18:51:49 +02:00
_bbox_px = set_border_region_from_coord ( coords , margin = 30 , scn = scn , export_json = export_json )
2022-02-09 12:27:29 +01:00
def get_cam_frame_center_world ( cam ) :
''' get camera frame center world position in 3d space '''
## ortho cam note: scale must be 1,1,1 (parent too) to fit right in cam-frame rectangle
import numpy as np
frame = cam . data . view_frame ( )
mat = cam . matrix_world
frame = [ mat @ v for v in frame ]
# return np.add.reduce(frame) / 4
return Vector ( np . sum ( frame , axis = 0 ) / 4 )
def get_coord_in_cam_space ( scene , cam_ob , co , ae = False ) :
''' Get 2d coordinate of vector in cam space
: scene : scene where camera is used ( needed to get resolution )
: cam_ob : camera object
: co : the Vector3 coordinate to find in cam space
: ae : if True , Return after effects coord , top - left corner origin ( blender is bottom - left )
'''
import bpy_extras
co_2d = bpy_extras . object_utils . world_to_camera_view ( scene , cam_ob , co )
if ae :
# y coordinate from top
co_2d = Vector ( ( co_2d . x , 1 - co_2d . y ) )
## Convert to pixel values based on scene resolution and percentage
render_scale = scene . render . resolution_percentage / 100
render_size = (
int ( scene . render . resolution_x * render_scale ) ,
int ( scene . render . resolution_y * render_scale ) ,
)
return (
round ( co_2d . x * render_size [ 0 ] ) , # x
round ( co_2d . y * render_size [ 1 ] ) , # y
)
## -- After effects exports
def get_ae_keyframe_clipboard_header ( scn ) :
2022-02-09 16:35:07 +01:00
''' Need to use tabs for AE '''
## multiline version, but space instead of tabs break syntax for AE
# import textwrap
# t = f'''\
# Adobe After Effects 8.0 Keyframe Data
# Units Per Second {scn.render.fps}
# Source Width {scn.render.resolution_x}
# Source Height {scn.render.resolution_y}
# Source Pixel Aspect Ratio 1
# Comp Pixel Aspect Ratio 1
# Transform Position
# Frame X pixels Y pixels Z pixels
# '''
# t = textwrap.dedent(t)
# ## spaces to tab
# t = re.sub(r'\s{3-4}', u'\t', t) # ! Still spaces
## Direct use of tabs is safer
t = ' Adobe After Effects 8.0 Keyframe Data \n \n '
t + = ' \t Units Per Second \t %s \n ' % scn . render . fps
t + = ' \t Source Width \t %s \n ' % scn . render . resolution_x
t + = ' \t Source Height \t %s \n ' % scn . render . resolution_y
t + = ' \t Source Pixel Aspect Ratio \t 1 \n '
t + = ' \t Comp Pixel Aspect Ratio \t 1 \n \n '
t + = ' Transform \t Position \n '
2022-07-20 18:09:28 +02:00
t + = ' \t Frame \t X pixels \t Y pixels \t Z pixels \t \n '
2022-02-09 16:35:07 +01:00
return t
2022-02-09 12:27:29 +01:00
## -- Collection handle
2021-09-30 18:51:49 +02:00
2021-11-12 17:02:19 +01:00
def get_collection_childs_recursive ( col , cols = [ ] , include_root = True ) :
2021-09-30 18:51:49 +02:00
''' return a list of all the sub-collections in passed col '''
2021-11-12 17:02:19 +01:00
# force start from fresh list (otherwise same cols list is used at next call)
cols = cols or [ ]
2021-09-30 18:51:49 +02:00
for sub in col . children :
if sub not in cols :
cols . append ( sub )
if len ( sub . children ) :
cols = get_collection_childs_recursive ( sub , cols )
2023-01-18 14:28:27 +01:00
2021-11-12 17:02:19 +01:00
if include_root and col not in cols : # add root col
cols . append ( col )
2023-01-18 14:28:27 +01:00
2021-09-30 18:51:49 +02:00
return cols
def unlink_objects_from_scene ( oblist , scn ) :
2021-11-12 17:02:19 +01:00
all_col = get_collection_childs_recursive ( scn . collection )
2021-09-30 18:51:49 +02:00
for col in all_col :
for ob in reversed ( col . objects ) :
if ob in oblist :
col . objects . unlink ( ob )
def remove_scene_nodes_by_obj_names ( scn , name_list , negative = False ) :
for n in reversed ( scn . node_tree . nodes ) :
if negative :
if ( n . parent and n . parent . label not in name_list ) or ( n . type == ' FRAME ' and n . label not in name_list ) :
scn . node_tree . nodes . remove ( n )
else :
if ( n . parent and n . parent . label in name_list ) or ( n . type == ' FRAME ' and n . label in name_list ) :
scn . node_tree . nodes . remove ( n )
2023-06-26 18:11:17 +02:00
def split_object_to_scene ( objs = None , scene_name = None ) :
2021-09-30 18:51:49 +02:00
''' Create a new scene from object selection '''
2023-06-26 18:11:17 +02:00
if not scene_name :
active = bpy . context . object
scene_name = active . name
if not objs :
objs = [ o for o in bpy . context . selected_objects ]
2023-01-18 14:28:27 +01:00
2021-09-30 18:51:49 +02:00
if bpy . data . scenes . get ( scene_name ) :
print ( f ' Scene " { scene_name } " Already Exists ' )
raise Exception ( f ' Scene " { scene_name } " Already Exists ' )
src = bpy . context . scene
bpy . ops . scene . new ( type = ' LINK_COPY ' )
new = bpy . context . scene
new . name = scene_name
2021-11-11 14:48:53 +01:00
## OPT
## set individual output for composite or go in /tmp ? (might not be needed)
# new.render.filepath = f'//render/preview/{bpy.path.clean_name(new.name.lower())}/preview_'
# new.render.filepath = f'/tmp/'
2021-09-30 18:51:49 +02:00
## unlink unwanted objects from collection
2021-11-12 17:02:19 +01:00
all_col = get_collection_childs_recursive ( new . collection )
2021-09-30 18:51:49 +02:00
for col in all_col :
for sob in reversed ( col . objects ) :
if sob . type in ( ' CAMERA ' , ' LIGHT ' ) :
continue
if sob not in objs :
col . objects . unlink ( sob )
2023-01-18 14:28:27 +01:00
2021-09-30 18:51:49 +02:00
frame_names = [ n . label for n in new . node_tree . nodes if n . type == ' FRAME ' if new . objects . get ( n . label ) ]
remove_scene_nodes_by_obj_names ( new , frame_names , negative = True )
bpy . ops . gp . clean_compo_tree ( )
# add crop
new . render . use_border = True
new . render . use_crop_to_border = True
new . render . use_compositing = True
new . render . use_sequencer = False
## remove asset from original scene
#src_frame_names = [n.label for n in src.node_tree.nodes if n.type == 'FRAME' and n.label in [o.name for o in objs]]
2023-01-18 14:28:27 +01:00
#remove_scene_nodes_by_obj_names(src, src_frame_names)
2021-09-30 18:51:49 +02:00
remove_scene_nodes_by_obj_names ( src , frame_names , negative = False )
# unlink objects ?
unlink_objects_from_scene ( objs , src )
# border to GP objects of the scene
gp_objs = [ o for o in new . objects if o . type == ' GPENCIL ' ]
coords = get_gp_box_all_frame_selection ( oblist = gp_objs , scn = new , cam = new . camera )
2021-10-13 19:00:15 +02:00
if not coords :
return f ' Scene " { scene_name } " created. But Border was not set (Timeout during GP analysis), should be done by hand if needed then use export crop to json '
2023-01-18 14:28:27 +01:00
2021-09-30 18:51:49 +02:00
set_border_region_from_coord ( coords , margin = 30 , scn = new , export_json = True )
export_crop_to_json ( )
2021-10-11 16:47:22 +02:00
def clear_frame_out_of_range ( o , verbose = False ) :
''' get a GP object
delete frame out of active scene range in all layers
return number of deleted frame
'''
scn = bpy . context . scene
ct = 0
if o . type != ' GPENCIL ' :
print ( f ' { o . name } not a Gpencil ' )
return 0
for l in o . data . layers :
first = True
for f in reversed ( l . frames ) :
# after
if f . frame_number > scn . frame_end + 1 :
if verbose :
print ( f ' del: obj { o . name } > layer { l . info } > frame { f . frame_number } ' )
l . frames . remove ( f )
ct + = 1
2023-01-18 14:28:27 +01:00
2021-10-11 16:47:22 +02:00
# before
elif f . frame_number < scn . frame_start - 1 :
if first :
first = False
continue
if verbose :
print ( f ' del: obj { o . name } > layer { l . info } > frame { f . frame_number } ' )
l . frames . remove ( f )
ct + = 1
# print('INFO', f'{ct} frames deleted')
return ct
## not used
def clear_frame_out_of_range_all_object ( ) :
scene = bpy . context . scene
ct = 0
for o in scene . objects :
if o . type == ' GPENCIL ' :
nct = clear_frame_out_of_range ( o , verbose = False )
print ( f ' { o . name } : { nct } frames deleted ' )
ct + = nct
print ( f ' { ct } gp frames deleted ' )
return ct
2021-12-16 19:10:00 +01:00
def set_scene_output_from_active_fileout_item ( ) :
scn = bpy . context . scene
rd = scn . render
ntree = scn . node_tree
fo = ntree . nodes . active
if fo . type != ' OUTPUT_FILE ' :
return
sl = fo . file_slots [ fo . active_input_index ]
full_path = os . path . join ( fo . base_path , sl . path )
rd . filepath = full_path
fmt = fo . format if sl . use_node_format else sl . format
## set those attr first to avoid error settings other attributes in next loop
rd . image_settings . file_format = fmt . file_format
rd . image_settings . color_mode = fmt . color_mode
rd . image_settings . color_depth = fmt . color_depth if fmt . color_depth else 8 # Force set since Sometimes it's weirdly set to "" (not in enum choice)
excluded = [ ' file_format ' , ' color_mode ' , ' color_depth ' ,
' view_settings ' , ' views_format ' ]
2023-01-18 14:28:27 +01:00
2021-12-16 19:10:00 +01:00
''' ## all attrs
# 'cineon_black', 'cineon_gamma', 'cineon_white',
# 'color_depth', 'color_mode', 'compression', 'display_settings',
# 'exr_codec', 'file_format', 'jpeg2k_codec', 'quality',
# 'rna_type', 'stereo_3d_format', 'tiff_codec', 'use_cineon_log',
# 'use_jpeg2k_cinema_48', 'use_jpeg2k_cinema_preset', 'use_jpeg2k_ycc',
# 'use_preview', 'use_zbuffer']
'''
for attr in dir ( fmt ) :
if attr . startswith ( ' __ ' ) or attr . startswith ( ' bl_ ' ) or attr in excluded :
continue
if hasattr ( scn . render . image_settings , attr ) and not scn . render . image_settings . is_property_readonly ( attr ) :
2023-01-10 15:39:05 +01:00
setattr ( scn . render . image_settings , attr , getattr ( fmt , attr ) )
def set_layer_colors ( skip_if_colored = False ) :
''' Set hardcoded color to grease pencil layer channel according to prefixes '''
## UW -> TO (here used fo CU): (0.015996, 0.246201, 0.246201) # Indigo
## invisible (close to violet light) in UW: (0.246201, 0.132868, 0.496933)
prefix_color = {
# 'MA_': (0.09, 0.08, 0.46), # Vivid blue
' MA_ ' : ( 0.65 , 0.4 , 0.6 ) , # Pink Light
' FX_ ' : ( 0.12 , 0.33 , 0.58 ) , # (0.3, 0.49, 0.63) # Blue Light
# 'CO_': (0.35, 0.0085, 0.25),
' CO_ ' : ( 0.5 , 0.1 , 0.5 ) , # Clear Pink
# 'CU': (0.092070, 0.177356, 0.447959), # Blue clear
' CU_ ' : ( 0.02 , 0.27 , 0.27 ) , # Indigo
}
for ob in bpy . context . scene . objects :
if ob . type != ' GPENCIL ' :
continue
if skip_if_colored and any ( [ has_channel_color ( l ) for l in ob . data . layers ] ) :
continue
for l in ob . data . layers :
# if l.info.startswith(prefix_color.keys()):
color = prefix_color . get ( l . info [ : 3 ] )
if not color :
continue
print ( l . info , ' -> ' , color )
l . channel_color = color
2023-01-18 14:28:27 +01:00
2023-01-11 17:36:43 +01:00
bpy . context . preferences . edit . use_anim_channel_group_colors = True
def different_gp_mat ( mata , matb ) :
''' return None if no difference (False), string describing color difference (True) '''
a = mata . grease_pencil
b = matb . grease_pencil
if a . color [ : ] != b . color [ : ] :
return f ' { mata . name } and { matb . name } stroke color is different '
if a . fill_color [ : ] != b . fill_color [ : ] :
return f ' { mata . name } and { matb . name } fill_color color is different '
if a . show_stroke != b . show_stroke :
return f ' { mata . name } and { matb . name } stroke has different state '
if a . show_fill != b . show_fill :
return f ' { mata . name } and { matb . name } fill has different state '
## Clean dups
def clean_mats_duplication ( ob , skip_different_materials = True ) :
''' Clean object material stack of duplication
if a material is named " mat.001 " and a " mat " exists , replace with the one with original name
: skip_different_materials : Don ' t replace a " mat.??? " if orignal " mat " has different color
'''
import re
diff_ct = 0
todel = [ ]
if ob . type != ' GPENCIL ' :
return
if not hasattr ( ob , ' material_slots ' ) :
return
for i , ms in enumerate ( ob . material_slots ) :
mat = ms . material
if not mat :
continue
match = re . search ( r ' (.*) \ . \ d {3} $ ' , mat . name )
if not match :
continue
basemat = bpy . data . materials . get ( match . group ( 1 ) )
if not basemat :
continue
diff = different_gp_mat ( mat , basemat )
if diff :
print ( f ' ! { ob . name } : { diff } ' )
diff_ct + = 1
if skip_different_materials :
continue
if mat not in todel :
todel . append ( mat )
ms . material = basemat
print ( f ' { ob . name } : slot { i } >> replaced { mat . name } ' )
mat . use_fake_user = False
### delete (only when using on all objects loop, else can delete another objects mat...)
## for m in reversed(todel):
## bpy.data.materials.remove(m)
if diff_ct :
print ( f ' { diff_ct } mat skipped >> same name but different color settings! ' )
2023-04-04 11:09:00 +02:00
# return ('INFO', f'{diff_ct} mat skipped >> same name but different color settings!')
2024-01-16 16:55:07 +01:00
2024-01-17 18:37:00 +01:00
def recursive_node_connect_check ( l , target_node ) :
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
2024-06-24 16:10:44 +02:00
def connect_to_file_output ( node_list , file_out = None , base_path = ' ' , excludes = None , remap_names = None , file_format = None ) :
2024-04-18 15:11:59 +02:00
""" 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 .
frame ( bpy . types . CompositorNode , optional ) : If given , create nodes into a frame .
Defaults to None .
Returns :
list [ bpy . types . CompositorNode ] : All nodes created .
"""
2024-01-16 16:55:07 +01:00
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
2024-04-18 15:11:59 +02:00
excludes = excludes or { }
2024-01-16 16:55:07 +01:00
for node in node_list :
2024-04-18 15:11:59 +02:00
exclusions = excludes . get ( node . name ) or [ ]
2024-01-16 16:55:07 +01:00
## create one output facing node and connect all
2024-04-18 15:11:59 +02:00
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 ]
2024-01-16 16:55:07 +01:00
if node . type == ' R_LAYERS ' :
out_base = node . layer
elif node . label :
out_base = node . label
else :
out_base = node . name
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 )
2024-04-18 18:12:18 +02:00
fo . inputs . remove ( fo . inputs [ 0 ] ) # Remove default image input
2024-04-18 15:11:59 +02:00
if file_format :
for k , v in file_format . items ( ) :
setattr ( fo . format , k , v )
else :
set_file_output_format ( fo )
2024-01-16 16:55:07 +01:00
fo . name = out_name
if node . parent :
fo . parent = node . parent
2024-04-17 14:37:44 +02:00
2024-04-18 15:11:59 +02:00
if base_path :
fo . base_path = base_path
2024-04-17 14:37:44 +02:00
else :
2024-04-18 15:11:59 +02:00
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 } '
2024-01-16 16:55:07 +01:00
for o in outs :
2024-01-17 18:37:00 +01:00
if next ( ( l for l in o . links if recursive_node_connect_check ( l , fo ) ) , None ) :
continue
2024-06-24 16:10:44 +02:00
if remap_names and ( custom_name := remap_names . get ( o . name ) ) :
slot_name = bpy . path . clean_name ( custom_name ) # clean name ?
else :
slot_name = bpy . path . clean_name ( o . name )
2024-04-18 18:12:18 +02:00
# 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'
2024-01-16 16:55:07 +01:00
out_input = fo . inputs [ - 1 ]
links . new ( o , out_input )
2024-04-18 15:11:59 +02:00
2024-01-16 16:55:07 +01:00
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 )
2024-04-18 18:12:18 +02:00
fo . inputs . remove ( fo . inputs [ 0 ] ) # Remove default image input
2024-04-18 15:11:59 +02:00
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
2024-01-16 16:55:07 +01:00
fo . name = out_name
if node . parent :
fo . parent = node . parent
2024-04-18 15:11:59 +02:00
if base_path :
fo . base_path = base_path
2024-04-17 14:37:44 +02:00
else :
2024-04-18 15:11:59 +02:00
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 } '
2024-01-16 16:55:07 +01:00
for o in cryptout :
2024-01-17 18:37:00 +01:00
## 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
2024-06-24 16:10:44 +02:00
if remap_names and ( custom_name := remap_names . 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
2024-04-17 14:37:44 +02:00
2024-04-18 18:12:18 +02:00
# 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'
2024-04-18 15:11:59 +02:00
2024-01-16 16:55:07 +01:00
out_input = fo . inputs [ - 1 ]
links . new ( o , out_input )
clear_disconnected ( fo )
fo . update ( )