Compare commits

...
30 Commits
Author SHA1 Message Date
pullusb 3904ffa024 Remove swapping to "bg_cam" in autobuild
1.8.18

- fixed: remove the switch to "bg_cam" (when exists) in autobuild
2025-06-05 11:42:19 +02:00
pullusb 0dae5157b1 skip hidden object (viewport or in render) in autobuild
1.8.17

- changed: in autobuild, ignore object that are not visible in scene or with render toggle off
2025-06-02 15:08:28 +02:00
pullusb d2a19ae9e3 more robust reorder 2025-04-10 15:53:25 +02:00
pullusb cc1646081b bump version
1.8.16

- fixed: Better reorder socket. Still not perfect, can have bad result if there some unconnected sockets on both sides.
2025-04-10 15:28:08 +02:00
pullusb 26bcb56815 change clean method based on connection order 2025-04-10 15:07:18 +02:00
pullusb af2d8e1ad4 Exclude GP using is_background in autobuild by default
1.8.15

- changed: exclude background GP by default in autobuild (`is_background` is a custom property set on some GP by the addon "Background Plane Manager")
- added: autobuild bool argument `exclude_background_gp` (Default to True).
2025-04-02 15:55:34 +02:00
pullusb 52e1f7ad0e fix clean name
1.8.15
2025-04-01 11:56:03 +02:00
pullusb 9b76e380ae fix viewlayer removal error
1.8.14

- fixed: error when trying remove a viewlayer
2025-03-10 18:20:33 +01:00
pullusb 35a99ff6cd Improve connect to file output feature with better name besed on source socket and node
1.8.13

1.8.13

- changed: improve `connect to fileoutput` feature:
  - possible to choose `node_name + socket name` or `node name` instead of `socket name` only
  - default is now `node_name_socket_name`
  - added some exception behavior:
    - if a node has single output : write the node name only
    - render layers use `scene_viewlayer` as node_name intead of "Render Layer"
    - interface shows source node label (with source node name in parenthesis)
    - interface shows render layer scene/viewlayer name (with name in parenthesis)
2024-12-17 15:54:21 +01:00
florentin.luce 09a47413c3 Use GP_RENDER_FILE_FORMAT env var to set file output nodes 2024-12-04 16:22:34 +01:00
pullusb 19c43e6268 Fix material duplication clean comparing with non-GP mats
1.8.11

- fixed: cleaning materials duplication checked against name without checking material GP type
2024-11-27 14:33:20 +01:00
pullusb 772af8cb3f fix auto-merge by prefix in autobuild
1.8.10

- fixed: node not deleted after a merge using (compo scene not referenced correctly to point node_trees)
2024-11-14 12:38:53 +01:00
pullusb bd7b022af8 avoid error when setting object active at the end of autobuild
1.8.9
2024-11-06 14:23:25 +01:00
pullusb c5d84ddb16 fix ae trnasform exporter to use directory without export helper 2024-10-16 12:05:28 +02:00
pullusb adbb813410 fix camera always exported even when unchecked 2024-10-15 16:04:27 +02:00
pullusb 09eca87060 Fix #4 export transformation to AE with export helper
1.8.8

- added: Add `export transformation to AE` in export menu:
 - export camera
 - export objects transformation
 - export exposition (object, GPencil keys, etc)
2024-10-15 15:07:11 +02:00
pullusb 5c69bd0185 First implementation of export helper for AE keyuframe data 2024-10-14 18:42:18 +02:00
pullusb d6a102b044 less flashy color on render layer node color - eye care update
1.8.7

- changed: Layer `channel_colors` have _value component_ clamped to 0.65 when transfered to compo node (eye care update)
2024-10-01 17:16:44 +02:00
pullusb e67adcb3f7 remove masks exclusion from list (sometimes wanted in export)
1.8.6

- changed: removed mask terms from exclude export list `MA, MASK, mask, MSK, msk` (still excluded when opacity is at 0)
2024-09-23 15:03:22 +02:00
pullusb b0f62a732d group adjacent prefix now ignore case
1.8.5

- changed: `group adjacent prefix` prefix pattern now ignore case
2024-09-19 18:03:24 +02:00
pullusb 8be44fa1e5 fix select node from layer and adjust object numbering check
1.8.4

- fix: select node from layer
- changed: Check for problems: object numbering check does not list as error if parent part has number prefix (previously checked only for last part)
2024-09-11 18:32:38 +02:00
pullusb ea0561e14e Add more default prefix for mask layer exclusion
1.8.3

- changed: for layer with 0 opacity, do not send to render and keep opacity
- removed: `GP` prefix exclusion to send GP to render
- added: more default prefix exclusion for _layer to render_ and _skip opacity reset_ (`MASK, mask, MSK, msk`)
2024-09-09 14:38:16 +02:00
christophe.seux 2f8efed73b restore state on selected gp 2024-09-09 11:06:02 +02:00
christophe.seux 85a42939ca Restore the layer state operator 2024-09-06 17:55:04 +02:00
pullusb 734714be09 add a no_popup arg in check layer for autobuild background mode 2024-09-04 17:38:00 +02:00
pullusb 747638eca9 Fix reset layer state in autobuild 2024-09-04 17:21:43 +02:00
pullusb 1121c489a8 remove poll in check layer 2024-09-04 17:03:33 +02:00
pullusb 2e9b379144 version bump 2024-09-04 15:54:14 +02:00
pullusb e97a86b624 Merge branch 'main' of ssh://git.autourdeminuit.com:222/autour_de_minuit/gp_render into main 2024-09-04 15:53:10 +02:00
pullusb aefc84fd7a Add pattern for layer group merge and check layer in autobuild
1.8.1

- added: `Reset Layer State` (Check layer) in Autobuild
- added: environment variable `GP_RENDER_LAYER_GROUP_PATTERN` for layer merging regex pattern
2024-09-04 15:46:15 +02:00
12 changed files with 795 additions and 90 deletions
+87
View File
@@ -14,6 +14,93 @@ Activate / deactivate layer opacity according to prefix
Activate / deactivate all masks using MA layers
-->
1.8.18
- fixed: remove the switch to "bg_cam" (when exists) in autobuild
1.8.17
- changed: in autobuild, skip object that are not visible in scene or with render toggle off
1.8.16
- fixed: Better reorder socket. Still not perfect, can have bad result if there some unconnected sockets on both sides.
1.8.15
- changed: exclude background GP by default in autobuild (`is_background` is a custom property set on some GP by the addon "Background Plane Manager")
- added: autobuild bool argument `exclude_background_gp` (Default to True).
- fixed: clean name in file output gernerator
1.8.14
- fixed: error when trying remove a viewlayer
1.8.13
- changed: improve `connect to fileoutput` feature:
- possible to choose `node_name + socket name` or `node name` instead of `socket name` only
- default is now `node_name_socket_name`
- added some exception behavior:
- if a node has single output : write the node name only
- render layers use `scene_viewlayer` as node_name intead of "Render Layer"
- interface shows source node label (with source node name in parenthesis)
- interface shows render layer scene/viewlayer name (with name in parenthesis)
- fixed: a potential name overlapping bug in connect to fileoutput
1.8.12
- changed: Use GP_RENDER_FILE_FORMAT env var to set file output nodes
1.8.11
- fixed: cleaning materials duplication checked against name without checking material GP type
1.8.10
- fixed: node not deleted after a merge using (compo scene not referenced correctly to point node_trees)
1.8.9
- fixed: error when setting object active at the end of autobuild
1.8.8
- added: Add `export transformation to AE` in export menu:
- export camera
- export objects transformation
- export exposition (object, GPencil keys, etc)
1.8.7
- changed: Layer `channel_colors` have _value component_ clamped to 0.65 when transfered to compo node to limit color flashyness (eye care update)
1.8.6
- changed: removed mask terms from exclude export list `MA, MASK, mask, MSK, msk` (still excluded when opacity is at 0)
1.8.5
- changed: `group adjacent prefix` prefix pattern now ignore case
1.8.4
- fix: select node from layer
- changed: Check for problems: object numbering check does not list as error if parent part has number prefix (previously checked only for last part)
1.8.3
- changed: for layer with 0 opacity, do not send to render and keep opacity
- removed: `GP` prefix exclusion to send GP to render
- added: more default prefix exclusion for _layer to render_ and _skip opacity reset_ (`MASK, mask, MSK, msk`)
- added: option to restore layer state from json (commit by Christophe.S)
1.8.2
- added: `Reset Layer State` (Check layer) in Autobuild
- added: environment variable `GP_RENDER_LAYER_GROUP_PATTERN` for layer merging regex pattern
1.8.1
- added: transfer timeline marker to render scene
+38 -17
View File
@@ -34,9 +34,10 @@ def batch_setup_render_scene(context=None, render_scn=None, node_scene=None, pre
# print('Swap to bg cam')
# bpy.ops.gp.swap_render_cams()
if render_scn.objects.get('bg_cam'):
render_scn.camera = render_scn.objects.get('bg_cam')
fn.set_resolution_from_cam_prop(scene=render_scn)
## Switch to bg_cam (bad to set a camera by name)
# if render_scn.objects.get('bg_cam'):
# render_scn.camera = render_scn.objects.get('bg_cam')
# fn.set_resolution_from_cam_prop(scene=render_scn)
## Go to camera view in visible viewports (! Need timer + Already done in workspace script!)
# if not bpy.app.background:
@@ -77,7 +78,7 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator):
description='Set single user on all objects GP data')
excluded_prefix : StringProperty(
name='Excluded Layer By Prefix', default='GP, RG, PO, MA',
name='Excluded Layer By Prefix', default='RG, PO', # MA, MASK, mask, MSK, msk
description='Exclude layer to send to render by prefix (comma separated list)')
clean_name_and_visibility : BoolProperty(name='Clean Name And Visibility', default=True,
@@ -88,7 +89,7 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator):
description='Clean material stack. i.e: Replace "mat.001" in material stack if "mat" exists and has same color')
prefix_filter : StringProperty(name='Prefix Filter', default='CO, CU, FX, TO', # , MA # exclude MA if mask are applied
description='Comma separated prefix to render. Set the other prefix and non-prefixed layer to exluded viewlayer')
description='Comma separated prefix to render, Layer with those prefix will be made visible')
set_layers_colors : BoolProperty(name='Set Layers Colors', default=True,
description='Set colors for on layers according to prefix (hadrcoded color set)')
@@ -102,6 +103,9 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator):
export_layer_infos : BoolProperty(name='Export Layer Infos', default=True,
description='Export layers infos to a Json file')
reset_layers_state : BoolProperty(name='Reset Layer State', default=True,
description='Set layer opacity to 100%, Disable use light on all layer, set regular blend mode, hide invisible')
group_all_adjacent_layer_type : BoolProperty(name='Group All Adjacent Layer Type', default=False,
description='Fuse output Viewlayer according to adjacent Prefix in layer stack')
@@ -126,6 +130,9 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator):
add_preview : BoolProperty(name='Add Preview', default=True,
description='Create preview with stacked alpha over on render layers')
exclude_background_gp : BoolProperty(name='Exclude Background GP', default=True,
description='Exclude GP object with boolean custom property "is_background" at True (created by "Background Plane Manager" addon)')
# File output templates
base_path : bpy.props.StringProperty(name='Base Path', default='', options={'SKIP_SAVE'})
file_slot : bpy.props.StringProperty(name='File Slot', default='', options={'SKIP_SAVE'})
@@ -150,6 +157,7 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator):
col.prop(self, 'trigger_rename_lowercase')
col.prop(self, 'trigger_renumber_by_distance')
col.prop(self, 'export_layer_infos')
col.prop(self, 'reset_layers_state')
col.label(text='Send prefixed layer to render scene (except excluded)')
col.prop(self, 'excluded_prefix', text='Excluded')
@@ -190,9 +198,18 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator):
if not node_scene:
node_scene = render_scn
all_gp_objects = [o for o in context.scene.objects if o.type == 'GPENCIL']
## Propagate render scene and compo scene target properties on all scenes
for scn in bpy.data.scenes:
scn.gp_render_settings.node_scene = node_scene.name
scn.gp_render_settings.render_scene = render_scn.name
## clean name and visibility
all_gp_objects = [o for o in context.scene.objects if o.type == 'GPENCIL' and o.visible_get() and not o.hide_render]
## Exclude Background GP (is_background is a custom property set by the addon "Background Plane Manager")
if self.exclude_background_gp:
all_gp_objects = [o for o in all_gp_objects if not o.get('is_background')]
## Clean name and visibility
if self.clean_name_and_visibility:
for o in all_gp_objects:
if o.hide_render:
@@ -204,10 +221,10 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator):
l.info = l.info + o.name.lower()
## Make used prefix visible ?? (maybe some layer were intentionally hidden...)
if (res := re.search(r'^([A-Z]{2})_', l.info)):
if res.group(1) in prefix_to_render and l.hide == True and not 'invisible' in l.info:
print(f'{o.name} -> {l.info} : Switch visibility On')
l.hide = False
# if (res := re.search(r'^([A-Z]{2})_', l.info)):
# if res.group(1) in prefix_to_render and l.hide == True and not 'invisible' in l.info:
# print(f'{o.name} -> {l.info} : Switch visibility On')
# l.hide = False
if self.clean_material_duplication:
print('Clean material duplicates')
@@ -249,6 +266,10 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator):
print('Export layer infos (skip if json already exists)')
bpy.ops.gp.export_infos_for_compo('INVOKE_DEFAULT', skip_check=True)
## Reset layer states -> 100% opacity (except mask layer 'MA' by default), remove use light, set default blend mode, hide material named "invisible".
if self.reset_layers_state:
bpy.ops.gp.layers_state('EXEC_DEFAULT', no_popup=True)
## Set GP object data to single user (Individual viewlayers)
if self.make_gp_single_user:
for o in ob_list:
@@ -268,17 +289,17 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator):
self.report({'ERROR'}, 'No render scene found')
return {'CANCELLED'}
if self.set_active_scene == 'RENDER':
context.window.scene = render_scn
elif self.set_active_scene == 'NODES':
context.window.scene = node_scene
## Group all adjacent layer type
if self.group_all_adjacent_layer_type:
print('Group all adjacent layer type')
for ob in ob_list:
fn.group_adjacent_layer_prefix_rlayer(ob, excluded_prefix=self.excluded_prefix, first_name=True)
if self.set_active_scene == 'RENDER':
context.window.scene = render_scn
elif self.set_active_scene == 'NODES':
context.window.scene = node_scene
# bpy.ops.gp_export.render_scene_setup() # next render scene setup at once
## attempt to refresh scene
# render_scn.node_tree.nodes.update()
@@ -321,7 +342,7 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator):
# batch_setup_render_scene(render_scn=render_scn)
## set at least one GP object active
gp_ob = next((o for o in render_scn.objects if o.type == 'GPENCIL'), None)
gp_ob = next((o for o in context.view_layer.objects if o.type == 'GPENCIL'), None)
if gp_ob:
context.view_layer.objects.active = gp_ob
+11 -1
View File
@@ -68,8 +68,18 @@ def check_file_output_numbering(reports=None):
return reports
for fo in file_outs:
if not prenum.match(fo.base_path.split('/')[-1]):
### Check for object prefix number in path
split_path = fo.base_path.split('/')
if not prenum.match(split_path[-1]):
report_missing_number = True
# No prefix-number in tail part name
if len(split_path) >= 2 and prenum.match(split_path[-2]):
# report if no prefix-number on parent path part either
report_missing_number = False
if report_missing_number:
reports.append(f'No object numbering : node {fo.name}')
pct = 0
if fo.format.file_format == 'OPEN_EXR_MULTILAYER':
## multilayer use layer_slots > slot.name
+7 -1
View File
@@ -90,10 +90,16 @@ class GPEXP_OT_clean_compo_tree(bpy.types.Operator):
if self.clear_unused_view_layers:
used_rlayer_names = [n.layer for n in nodes if n.type == 'R_LAYERS']
vl_number = len(scn.view_layers)
if vl_number > 1:
for rl in reversed(scn.view_layers):
if rl.name in used_rlayer_names or rl.name == 'View Layer':
if vl_number <= 1:
## Do not try to remove the last view layer (can use try except with RuntimeError check)
break
if rl.name in used_rlayer_names or rl.name in ('View Layer', 'ViewLayer'):
continue
scn.view_layers.remove(rl)
vl_number -= 1
if self.arrange_rl_nodes:
fn.rearrange_rlayers_in_frames(node_scene.node_tree)
+415 -4
View File
@@ -1,12 +1,24 @@
from encodings import utf_8
import bpy
import json
# import bpy_extras
from bpy_extras.object_utils import world_to_camera_view # as cam_space
from bpy_extras.io_utils import ExportHelper
from mathutils import Vector
from pathlib import Path
import json
from . import fn
from bpy.props import (
StringProperty,
BoolProperty,
FloatProperty,
EnumProperty,
CollectionProperty,
)
from bpy.app.translations import pgettext_data as data_
'''
def Export_AE_2d_position_json_data():
scn = bpy.context.scene
@@ -26,6 +38,399 @@ def Export_AE_2d_position_json_data():
pix_pos = Vector((pos[0]*rx, (1-pos[1])*ry))
'''
def export_ae_transforms(directory, selection=None, camera=None, exposition=True, prefix='ae_', suffix='', fr=False, export_format='txt', export_cam=True):
"""
Export After Effects transform data for selected objects and camera in Blender.
This function exports keyframe data for position, scale, and optional exposition for
selected objects and the camera. The data can be exported in TXT and/or JSON formats,
which can be imported into Adobe After Effects.
Parameters:
directory (str): The directory path where the exported files will be saved.
selection (list, optional): List of Blender objects or pose bones to export. Defaults to None.
camera (bpy.types.Object, optional): The camera object to use for calculations. Defaults to the scene's active camera.
exposition (bool, optional): Whether to export exposition data. Defaults to True.
prefix (str, optional): Prefix for the exported filenames. Defaults to 'ae_'.
suffix (str, optional): Suffix for the exported filenames. Defaults to ''.
fr (bool, optional): If True, uses French labels for exposition data. Defaults to False.
export_format (str, optional): The format(s) to export. Can be 'txt', 'json', or 'txt,json'. Defaults to 'txt'.
Example usage:
export_ae_transforms('/path/to/export', selection=bpy.context.selected_objects, export_format='txt,json')
"""
scn = bpy.context.scene
res = Vector((scn.render.resolution_x, scn.render.resolution_y))
def get_keyframe_data(coords_2d, exposition=False):
frame_start, org_2d, _ = coords_2d[0]
org_distance = org_2d[-1]
keyframe_data = {'position': [], 'scale': []}
for frame, co_2d, is_animated in coords_2d:
position = Vector((co_2d[0], 1-co_2d[1]))
position *= res # Multiply by resolution to get pixels value
keyframe_data['position'].append((frame-frame_start, (round(position[0]), round(position[1]))))
scale = (org_distance / co_2d[-1]) * 100
keyframe_data['scale'].append((frame-frame_start, (round(scale, 5),)*3))
if is_animated:
keyframe_data.setdefault('exposition', []).append(frame-frame_start)
return keyframe_data
def get_keyframe_data_txt(keyframe_data, exposition=True):
keyframe_data_txt = (
'Adobe After Effects 8.0 Keyframe Data\n\n'
f'\tUnits Per Second\t{scn.render.fps}\n'
f'\tSource Width\t{scn.render.resolution_x}\n'
f'\tSource Height\t{scn.render.resolution_y}\n'
'\tSource Pixel Aspect Ratio\t1\n'
'\tComp Pixel Aspect Ratio\t1\n\n'
)
# Position
keyframe_data_txt += (
'\nTransform\tPosition\n'
'\tFrame\tX pixels\tY pixels\tZ pixels\t\n'
)
for frame, position in keyframe_data['position']:
keyframe_data_txt += f'\t{frame}\t{position[0]}\t{position[1]}\t0\t\n'
# Scale
keyframe_data_txt += (
'\nTransform\tScale\n'
'\tFrame\tX percent\tY percent\tZ percent\t\n'
)
for frame, scale in keyframe_data['scale']:
keyframe_data_txt += f'\t{frame}\t{scale[0]}\t{scale[1]}\t{scale[2]}\t\n'
if exposition:
if fr:
keyframe_data_txt += '\nEffects\tParamètre case #1\tCase #2\t\n'
else:
keyframe_data_txt += '\nEffects\tCheckbox Control #1\tCheckbox #2\t\n'
keyframe_data_txt += '\tFrame\t\t\n'
for i, frame in enumerate(keyframe_data['exposition']):
keyframe_data_txt += f'\t{frame}\t{i%2}\t\n'
keyframe_data_txt += '\nEnd of Keyframe Data\n'
return keyframe_data_txt
if selection is None:
selection = []
if camera is None:
camera = scn.camera
use_simplify = scn.render.use_simplify
simplify_subdivision = scn.render.simplify_subdivision
scn.render.use_simplify = True
scn.render.simplify_subdivision = 0
# Get the camera at the first frame for reference
scn.frame_set(scn.frame_start)
cam_org_3d = camera.matrix_world @ Vector((0, 0, -100))
camera_coords = []
ob_coords = {}
animation = {}
for i in range(scn.frame_start, scn.frame_end + 1):
scn.frame_set(i)
if camera is not None:
co_2d = world_to_camera_view(scn, camera, cam_org_3d)
camera_coords.append((i, co_2d, False))
for item in selection:
if isinstance(item, bpy.types.PoseBone):
co_3d = item.id_data.matrix_world @ item.matrix.to_translation()
name = f'{item.id_data.name}_{item.name}'
rig_transforms = [tuple(round(x, 5) for v in b.matrix for x in v) for b in item.id_data.pose.bones]
is_animated = rig_transforms != animation.get(name)
animation[name] = rig_transforms
elif item.type == 'GPENCIL': # isinstance(item.data, bpy.types.GreasePencil):
name = item.name
co_3d = item.matrix_world.to_translation()
## Check if there is a GP-scene at scene-frame (i) on any visible layer
is_animated = i in [f.frame_number for l in item.data.layers for f in l.frames if not l.hide]
animation[name] = co_3d
else:
name = item.name
co_3d = item.matrix_world.to_translation()
is_animated = co_3d != animation.get(name)
animation[name] = co_3d
co_2d = world_to_camera_view(scn, camera, co_3d)
ob_coords.setdefault(name, []).append((i, co_2d, is_animated))
cam_key_frame_data = get_keyframe_data(camera_coords)
objs_keyframe_data = {name: get_keyframe_data(co, exposition=exposition) for name, co in ob_coords.items()}
if 'json' in export_format or export_format == 'json':
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,
'objects': objs_keyframe_data
}
if export_cam: #camera is not None:
keyframe_data['camera'] = cam_key_frame_data
Path(directory, f'{prefix}keyframe_data{suffix}.json').write_text(json.dumps(keyframe_data, indent=4), encoding='utf-8')
if 'txt' in export_format or export_format == 'txt':
for name, keyframe_data in objs_keyframe_data.items():
name = name.replace('.', '_')
keyframe_data_txt = get_keyframe_data_txt(keyframe_data, exposition=exposition)
Path(directory, f'{prefix}{name}{suffix}.txt').write_text(keyframe_data_txt, newline='\r\n')
if export_cam: #camera is not None:
keyframe_data_txt = get_keyframe_data_txt(cam_key_frame_data, exposition=False)
Path(directory, f'{prefix}{camera.name}{suffix}.txt').write_text(keyframe_data_txt, newline='\r\n')
scn.render.use_simplify = use_simplify
scn.render.simplify_subdivision = simplify_subdivision
## Export operator without export_helper
class GPEXP_OT_export_anim_to_ae(bpy.types.Operator):
bl_idname = "gp.export_anim_to_ae"
bl_label = "Export AE Files"
bl_description = "Export the animation to After Effects, 2D transform of objects, camera\
\nand/or exposition (including greasepencil frames)"
bl_options = {"REGISTER"}
# filter_glob: StringProperty(default='*.*', options={'HIDDEN'})# *.jpeg;*.png;*.tif;*.tiff;*.bmp
# filename_ext = ''
# filepath : StringProperty(
# name="File Path",
# description="File path used for export",
# maxlen= 1024)
## Only need directory
directory : StringProperty(
name="File Path",
description="File path used for export",
maxlen= 1024,
subtype='DIR_PATH'
)
prefix : StringProperty(
name="Prefix",
default='ae_',
description="Prefix name for exported txt and json files",
maxlen= 1024,
)
use_selection: BoolProperty(
name="Selected Objects",
description="Export selected and visible objects only",
default=True,
)
use_visible: BoolProperty(
name='Visible Objects',
description='Export visible objects only',
default=False
)
use_active_collection: BoolProperty(
name="Active Collection",
description="Export only objects from the active collection (and its children)",
default=False,
)
object_types: EnumProperty(
name="Object Types",
options={'ENUM_FLAG'},
items=(('EMPTY', "Empty", ""),
('CAMERA', "Camera", ""),
('LIGHT', "Lamp", ""),
('ARMATURE', "Armature", "WARNING: not supported in dupli/group instances"),
('MESH', "Mesh", ""),
('GPENCIL', "Grease Pencil", ""),
('OTHER', "Other", "Other geometry types, like curve, metaball, etc. (converted to meshes)"),
),
description="Which kind of object to export",
default={'EMPTY', 'CAMERA', 'LIGHT', 'ARMATURE', 'MESH', 'GPENCIL', 'OTHER'},
)
exposition: BoolProperty(
name='Exposition',
description='Export the exposition of the keys',
default=True
)
# use_object_keys: BoolProperty(
# name='Object Keys',
# description='Consider object transform keys for animated exposition',
# default=True
# )
use_active_camera: BoolProperty(
name='Active Camera',
description='Export active camera keys',
default=True
)
data_lang: EnumProperty(
name="AE Language",
items=(('FR', "French", ""),
('EN', "English", ""),
),
description="Clipboard keyframe data language",
default='FR',
)
file_format: EnumProperty(
name="File Type",
options={'ENUM_FLAG'},
items=(('txt', "txt", ""),
('json', "json", ""),
),
description="File format to export (possible to select multiple choices with Shift + Click)",
default={'txt'},
)
def invoke(self, context, _event):
if not self.directory:
blend_filepath = context.blend_data.filepath
if blend_filepath:
dest_folder = Path(blend_filepath).parent
## If pre-enter a specific subfolder exists
## (Commented, should be used with a project environment variable)
# output_folder = dest_folder / 'render'
# if output_folder.exists() and output_folder.is_dir():
# dest_folder = output_folder
self.directory = str(dest_folder)
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False # No animation.
# Are we inside the File browser
is_file_browser = context.space_data.type == 'FILE_BROWSER'
export_main(layout, self, is_file_browser)
export_panel_format(layout, self, is_file_browser)
export_panel_include(layout, self, is_file_browser)
def execute(self, context):
objects_selection = get_object_selection(use_selection=self.use_selection,
use_visible=self.use_visible,
use_active_collection=self.use_active_collection,
object_types=self.object_types
)
## Ensure output path is directory
output_path = Path(self.directory)
if not output_path.is_dir():
output_path = output_path.parent
print('Output directory: ', output_path)
cam = None
if context.scene.camera:
cam = context.scene.camera
if cam and cam in objects_selection:
## Remove active camera from objects list
objects_selection.pop(objects_selection.index(cam))
print('Export AE transform from objects:')
for o in objects_selection:
print('- ', o.name)
export_ae_transforms(directory=output_path,
selection=objects_selection,
camera=cam,
exposition=self.exposition,
prefix=self.prefix,
fr=self.data_lang == 'FR',
export_format=self.file_format,
export_cam=self.use_active_camera)
self.report({'INFO'}, f'File(s) saved in folder: {output_path}')
return {"FINISHED"}
def get_object_selection(use_selection=False, use_visible=False, use_active_collection=False, object_types=None):
context = bpy.context
## determine selection based on filters4
source_collection = None
if use_active_collection:
source_collection = context.view_layer.active_layer_collection.collection
if source_collection:
if use_selection:
ctx_objects = tuple(obj for obj in source_collection.all_objects if obj.select_get())
else:
ctx_objects = source_collection.all_objects
else:
if use_selection:
ctx_objects = context.selected_objects
else:
ctx_objects = context.view_layer.objects
if use_visible:
ctx_objects = tuple(obj for obj in ctx_objects if obj.visible_get())
## Filter by object type
if object_types is None:
object_types = {'EMPTY', 'CAMERA', 'LIGHT', 'ARMATURE', 'MESH', 'GPENCIL', 'OTHER'}
ctx_objects = [obj for obj in ctx_objects if obj.type in object_types]
## Constant for compatible other object type
# BLENDER_OTHER_OBJECT_TYPES = {'CURVE', 'SURFACE', 'FONT', 'META'}
if 'OTHER' in object_types:
## Any object that is not in proposed list
ctx_objects += [obj for obj in ctx_objects if obj.type not in object_types]
return ctx_objects
def export_main(layout, operator, is_file_browser):
col = layout.column()
col.prop(operator, 'exposition')
col.prop(operator, 'use_active_camera')
# col.prop(operator, 'use_object_keys')
def export_panel_format(layout, operator, is_file_browser):
header, body = layout.panel("AE_export_format", default_closed=False)
header.label(text="Format")
if body:
col = body.column()
col.prop(operator, 'prefix')
## Format (language and file)
col.row().prop(operator, 'data_lang', expand=True)
if is_file_browser:
col.column().prop(operator, 'file_format')
def export_panel_include(layout, operator, is_file_browser):
header, body = layout.panel("AE_export_include", default_closed=False)
header.label(text="Include")
if body:
sublayout = body.column(heading="Limit to")
# if is_file_browser:
sublayout.prop(operator, "use_selection")
sublayout.prop(operator, "use_visible")
sublayout.prop(operator, "use_active_collection")
body.column().prop(operator, "object_types")
# body.prop(operator, "use_custom_props")
### --- Old functions
# Unused (old func that might not be usefull at all...)
def correct_shift(vec, cam):
@@ -246,24 +651,29 @@ class GPEXP_PT_extra_gprender_func(bpy.types.Panel):
col.operator("gp.export_keys_to_ae")
col.operator("gp.export_cam_keys_to_ae")
# def overcan_shift_fix_ui(self, context):
# layout = self.layout
# layout.operator("gp.fix_overscan_shift")
def export_ae_anim_menu(self, context):
row = self.layout.row(align=False)
row.operator('gp.export_anim_to_ae', text='After Effects Keyframe Data (.txt/.json)')
# row.operator('gp.export_anim_to_ae', text='', icon='COPYDOWN')
classes=(
GPEXP_OT_export_keys_to_ae,
GPEXP_OT_export_cam_keys_to_ae,
GPEXP_OT_fix_overscan_shift,
GPEXP_PT_extra_gprender_func
GPEXP_PT_extra_gprender_func,
GPEXP_OT_export_anim_to_ae,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.TOPBAR_MT_file_export.append(export_ae_anim_menu)
# if hasattr(bpy.types, 'RENDER_PT_overscan'):
# bpy.types.RENDER_PT_overscan.append(overcan_shift_fix_ui)
@@ -274,3 +684,4 @@ def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
bpy.types.TOPBAR_MT_file_export.remove(export_ae_anim_menu)
+89 -12
View File
@@ -239,13 +239,18 @@ class GPEXP_OT_reset_render_settings(bpy.types.Operator):
return {"FINISHED"}
class GPEXP_PG_selectable_prop(bpy.types.PropertyGroup):
node_name: bpy.props.StringProperty(name="Object Name")
node_name: bpy.props.StringProperty(name="Node Name")
node_label: bpy.props.StringProperty(name="Node Label")
name: bpy.props.StringProperty(name="Name or Path")
socket_name: bpy.props.StringProperty(name="Source socket Name") # Source socket name as reference
select: bpy.props.BoolProperty(name="Selected", default=True)
is_linked: bpy.props.BoolProperty(name="Linked", default=False)
# is_valid: bpy.props.BoolProperty(name="Valid", default=True)
## extra output naming options
name_from_node: bpy.props.StringProperty(name="Name From Node")
name_from_node_and_socket: bpy.props.StringProperty(name="Name From Node And Socket")
class GPEXP_OT_connect_selected_to_file_out(bpy.types.Operator):
bl_idname = "gp.connect_selected_to_file_out"
bl_label = "Connect Selected To File Output"
@@ -259,6 +264,24 @@ class GPEXP_OT_connect_selected_to_file_out(bpy.types.Operator):
name='Settings',
default=False)
## enum choice for naming: socket_name, node_name, node_and_socket_name,
name_type : bpy.props.EnumProperty(
name='Output Name From',
description='Choose the output name\
\nNode name use Label (Use node name when there is no Label)',
default='node_and_socket_name',
items=(
('node_and_socket_name', 'Node_Socket Name', 'Use the node name prefix and socket name', 0),
('socket_name', 'Socket Name', 'Use the socket name as output name', 1),
('node_name', 'Node Name', 'Use the node name as output name', 2),
)
)
# prefix_with_node_name : bpy.props.BoolProperty(
# name='Prefix With Node Name',
# description='Add the node name as prefix to the output name',
# default=False)
base_path : bpy.props.StringProperty(
name='Custom base path',
default='',
@@ -291,7 +314,6 @@ class GPEXP_OT_connect_selected_to_file_out(bpy.types.Operator):
),
)
color_depth : bpy.props.EnumProperty(
name='Color Depth',
default='16',
@@ -322,7 +344,28 @@ class GPEXP_OT_connect_selected_to_file_out(bpy.types.Operator):
continue
item = self.socket_collection.add()
item.node_name = n.name
item.socket_name = item.name = o.name
item.node_label = n.label.strip()
item.socket_name = o.name
## Set editable names
item.name = o.name
## Store other naming options (cleaned at exec with bpy.path.clean_name)
node_name = n.label.strip() if n.label.strip() else n.name
## Change node_name for render layers: scene_viewlayer_name
if n.type == 'R_LAYERS' and node_name != n.label: # skip if a label is set
node_name = f'{n.scene.name}_{n.layer}'
# node_name = f'{n.layer}'
item.name_from_node = node_name
if len(n.outputs) == 1:
## Only one output, just pick node name, no need to add socket name
item.name_from_node_and_socket = node_name
else:
item.name_from_node_and_socket = f'{node_name}_{o.name}'
## TODO: rename item.name according to tamplate pairs in preferences (to add later)
if o.is_linked:
item.is_linked = True
@@ -332,13 +375,15 @@ class GPEXP_OT_connect_selected_to_file_out(bpy.types.Operator):
def draw(self, context):
layout = self.layout
box = layout.box()
expand_icon = 'DISCLOSURE_TRI_DOWN' if self.show_custom_settings else 'DISCLOSURE_TRI_RIGHT'
layout.prop(self, 'show_custom_settings', emboss=False, icon=expand_icon)
box.prop(self, 'show_custom_settings', emboss=False, icon=expand_icon)
## Settings
if self.show_custom_settings:
layout.use_property_split = True
layout.prop(self, 'base_path')
col = layout.column()
box.use_property_split = True
box.row().prop(self, 'name_type', expand=False)
box.prop(self, 'base_path')
col = box.column()
col.prop(self, 'file_format')
col.prop(self, 'exr_codec')
col.row().prop(self, 'color_depth', expand=True)
@@ -351,7 +396,16 @@ class GPEXP_OT_connect_selected_to_file_out(bpy.types.Operator):
if item.node_name != current_node_name:
current_node_name = item.node_name
col.separator()
col.label(text=item.node_name, icon='NODE_SEL')
## Display node label + name or name only
if item.node_label:
display_node_name = f'{item.node_label} ({item.node_name})'
else:
display_node_name = item.node_name
## A bit dirty: For render layer node, show render layer-node name.
if display_node_name.startswith('Render Layer'):
rln = context.scene.node_tree.nodes.get(item.node_name)
display_node_name = f'{rln.scene.name}/{rln.layer} ({display_node_name})'
col.label(text=display_node_name, icon='NODE_SEL')
row = col.row()
if item.is_linked:
@@ -362,23 +416,46 @@ class GPEXP_OT_connect_selected_to_file_out(bpy.types.Operator):
display_name = item.socket_name
if 'crypto' in display_name.lower():
display_name = f'{display_name} (-> separate 32bit output node)'
display_name = f'{display_name} -> 32bit output node'
row.label(text=display_name)
row.label(text='', icon='RIGHTARROW')
if self.name_type == 'socket_name':
row.prop(item, 'name', text='')
elif self.name_type == 'node_name':
row.prop(item, 'name_from_node', text='')
elif self.name_type == 'node_and_socket_name':
row.prop(item, 'name_from_node_and_socket', text='')
def execute(self, context):
# Build exclude dict from selection
excludes = {}
remap_names = {}
## Old system
# if len(self.socket_collection):
# for item in self.socket_collection:
# if not item.select:
# # All deselected goes to exclude with {node_name: [socket1, ...]}
# excludes.setdefault(item.node_name, []).append(item.name)
# elif item.socket_name != item.name:
# remap_names[item.socket_name] = item.name
if len(self.socket_collection):
for item in self.socket_collection:
final_name = item.name
## change name if other options were used
if self.name_type == 'node_name':
final_name = item.name_from_node
elif self.name_type == 'node_and_socket_name':
final_name = item.name_from_node_and_socket
if not item.select:
# All deselected goes to exclude with {node_name: [socket1, ...]}
excludes.setdefault(item.node_name, []).append(item.name)
elif item.socket_name != item.name:
remap_names[item.socket_name] = item.name
excludes.setdefault(item.node_name, []).append(item.socket_name)
elif item.socket_name != final_name:
remap_names.setdefault(item.node_name, {})[item.socket_name] = final_name
# remap_names[item.socket_name] = final_name
## Handle default file format
file_ext = self.file_format
+52 -18
View File
@@ -161,6 +161,37 @@ class GPEXP_OT_export_infos_for_compo(bpy.types.Operator):
return {"FINISHED"}
class GPEXP_OT_restore_layers_state(bpy.types.Operator):
bl_idname = "gp.restore_layers_state"
bl_label = "Restore Layers State "
bl_description = "Restore original state of layers"
bl_options = {"REGISTER"} # , "UNDO"
def execute(self, context):
layers_info_path = Path(bpy.data.filepath).parent / 'render' / 'infos.json'
if not layers_info_path.exists():
self.report({"ERROR"}, 'No Info Found')
return {"CANCELLED"}
layers_info = json.loads(layers_info_path.read_text(encoding='utf-8'))
pool = [o for o in context.selected_objects if o.type == 'GPENCIL']
for gp in pool:
for layer_name, info in layers_info.items():
if gp.name == info['object']:
if not (layer := gp.data.layers.get(layer_name)):
return
if "opacity" in info:
layer.opacity = info['opacity']
if "blend_mode" in info:
layer.blend_mode = info['blend_mode']
return {"FINISHED"}
class GPEXP_OT_layers_state(bpy.types.Operator):
bl_idname = "gp.layers_state"
bl_label = "Set Layers State"
@@ -171,6 +202,9 @@ class GPEXP_OT_layers_state(bpy.types.Operator):
# description="Delete view layer that aren't used in the nodetree anymore",
# default=True)
no_popup : BoolProperty(name='No Popup',
default=False, description='To use for call in CLI or from other operators', options={'SKIP_SAVE'})
all_objects : BoolProperty(name='On All Object',
default=True, description='On All object, else use selected objects') # , options={'SKIP_SAVE'}
@@ -189,25 +223,24 @@ class GPEXP_OT_layers_state(bpy.types.Operator):
\nAffect all layers)') # , options={'SKIP_SAVE'}
opacity_exclude_list : StringProperty(name='Skip',
default='MA', description='Skip prefixes from this list when changing opacity\
default='MA, MASK, mask, MSK, msk', description='Skip prefixes from this list when changing opacity\
\nSeparate multiple value with a comma (ex: MAIN)') # , options={'SKIP_SAVE'}
hide_invisible_materials : BoolProperty(name='Hide Materials named invisible',
default=True, description='Hide material with name starting with "invisible"') # , options={'SKIP_SAVE'}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
# @classmethod
# def poll(cls, context):
# return context.object
def invoke(self, context, event):
## if no existing infos.json generated, call ops
l_infos = Path(bpy.data.filepath).parent / 'render' / 'infos.json'
if not l_infos.exists(): # only if infos not created
bpy.ops.gp.export_infos_for_compo('INVOKE_DEFAULT')
# return self.execute(context)
if self.no_popup:
return self.execute(context)
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
@@ -264,11 +297,15 @@ class GPEXP_OT_layers_state(bpy.types.Operator):
if l.opacity != 1:
# TODO Skip zeroed opacity ?
# check if there is an exclusion word
if any(x.strip() + '_' in l.info for x in self.opacity_exclude_list.strip(',').split(',') if x):
print(f'Skipped layer : {l.info}')
if l.opacity == 0:
## Skip layer with zero opacity
print(f'Skipped layer opacity reset (0 opacity) : {l.info}')
elif any(x.strip() + '_' in l.info for x in self.opacity_exclude_list.strip(',').split(',') if x):
# Skip layer if name has exclusion prefix
print(f'Skipped layer opacity reset (prefix in exclusion list) : {l.info}')
else:
## Set full opacity
full_opacity_state = '' if self.set_full_opacity else ' (check only)'
mess = f'{l.info} : opacity {l.opacity:.2f} >> 1.0{full_opacity_state}'
print(mess)
@@ -308,7 +345,7 @@ class GPEXP_OT_layers_state(bpy.types.Operator):
if changes:
changes.append('')
## disable multiframe editing on all GP (can cause artifacts on render)
## Disable multiframe editing on all GP (can cause artifacts on render)
gp_mu_edit_ct = 0
for gp in bpy.data.grease_pencils:
if gp.use_multiedit:
@@ -328,10 +365,12 @@ class GPEXP_OT_layers_state(bpy.types.Operator):
m.grease_pencil.hide = True
changes.append(f'{m.name} material hidden')
if not self.no_popup:
fn.show_message_box(_message=changes, _title="Layers Check Report", _icon='INFO')
return {"FINISHED"}
class GPEXP_OT_lower_layers_name(bpy.types.Operator):
bl_idname = "gp.lower_layers_name"
bl_label = "Normalize Layers Name"
@@ -606,12 +645,6 @@ class GPEXP_OT_select_layer_in_comp(bpy.types.Operator):
infos.append(mess)
continue
if not context.scene.view_layers.get(vl_name):
mess = f'/!\ {l.info}: view layer "{vl_name}" does not exists '
print(mess)
infos.append(mess)
continue
if not vl_name in used_vl:
mess = f'{l.info}: view layer "{vl_name}" not used in scene renderlayer nodes'
print(mess)
@@ -640,6 +673,7 @@ classes=(
GPEXP_OT_auto_number_object,
GPEXP_OT_lower_layers_name,
GPEXP_OT_export_infos_for_compo,
GPEXP_OT_restore_layers_state,
GPEXP_OT_layers_state,
GPEXP_OT_check_masks,
GPEXP_OT_select_layer_in_comp,
+2
View File
@@ -6,6 +6,8 @@ Organise export of gp layers through compositor output
`FILE_FORMAT` : Define file_format used for output. If not specified, use `OPEN_EXR_MULTILAYER` (set `OPEN_EXR` for separate sequences)
`GP_RENDER_MERGE_PATTERN` : Regex pattern for GP layer group merge. If not specified, pattern is `^([A-Z]{2})_` to match gp_toolbox two-capital-letter prefix system)
### Notable operator arguments for automatisation
+1 -1
View File
@@ -2,7 +2,7 @@ bl_info = {
"name": "GP Render",
"description": "Organise export of gp layers through compositor output",
"author": "Samuel Bernou",
"version": (1, 8, 1),
"version": (1, 8, 18),
"blender": (3, 0, 0),
"location": "View3D",
"warning": "",
+69 -23
View File
@@ -1,13 +1,14 @@
from typing import Coroutine
import bpy
import os
import re
import json
from mathutils import Vector
from pathlib import Path
from itertools import groupby
from math import isclose
from collections import defaultdict
from time import time
import json
from . constant import RD_SCENE_NAME
@@ -151,19 +152,18 @@ def copy_settings(obj_a, obj_b):
pass
def set_file_output_format(fo):
# 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'
fo.format.color_mode = 'RGBA'
fo.format.color_depth = '16'
fo.format.exr_codec = 'PIZ' # 'ZIP', 'RLE'
## PNG format
# fo.format.file_format = 'PNG'
# fo.format.color_mode = 'RGBA'
# fo.format.color_depth = '8'
# fo.format.compression = 15
env_file_format = json.loads(os.environ.get('GP_RENDER_FILE_FORMAT', '{}'))
if not env_file_format:
env_file_format = {
'file_format': 'OPEN_EXR_MULTILAYER',
'exr_codec': 'ZIP',
'color_depth': '16',
'color_mode': 'RGBA'
}
for k, v in env_file_format.items():
setattr(fo.format, k, v)
def set_scene_aa_settings(scene=None, aa=True):
'''aa == using native AA, else disable scene AA'''
@@ -300,6 +300,8 @@ def get_compo_scene(scene_name=None, create=True):
scn.node_tree.nodes.clear()
set_settings(scn)
scn.gp_render_settings['use_aa'] = True
# Set compo scene target in it's own property as well
scn.gp_render_settings.node_scene = scene_name
return scn
def get_render_scene(scene_name=None, create=True):
@@ -310,6 +312,7 @@ def get_render_scene(scene_name=None, create=True):
render_scn = bpy.data.scenes.get(scene_name)
if render_scn:
return render_scn
if not create:
return
@@ -589,8 +592,8 @@ def group_adjacent_layer_prefix_rlayer(ob, excluded_prefix=[], first_name=True):
:first_name: Keep the viewlayer of the bottom layer in group, else last
'''
from itertools import groupby
re_prefix = re.compile(r'^([A-Z]{2})_')
pattern = os.environ.get('GP_RENDER_MERGE_PATTERN', r'^([A-Z]{2})_')
re_prefix = re.compile(pattern, flags=re.IGNORECASE)
if isinstance(excluded_prefix, str):
excluded_prefix = [p.strip() for p in excluded_prefix.split(',')]
@@ -735,6 +738,34 @@ def clear_disconnected(fo):
print(f'Deleting unlinked fileout slot: {inp.name}')
fo.inputs.remove(inp)
def get_pairs(ng, fo):
'''return a list of tuple, pairs of connected socket indices
from nodegroup to fileout. ex: [(0, 3), (1, 0), (2, 1), (3, 2)]
'''
## Following version works well only if ng and fo have same number of sockets and all are connected.
# pairs = [(si, next((i for i, inp in enumerate(fo.inputs) if inp == o.links[0].to_socket), None))
# for si, o in enumerate(ng.outputs) if o.is_linked and o.links[0].to_node == fo]
# return pairs
## A bit more robust : Do not count the unlinked socket
pairs = []
str_ct = 0
for si, o in enumerate(ng.outputs):
if o.is_linked and o.links[0].to_node == fo:
dest_ct = 0
for j, inp in enumerate(fo.inputs):
if inp.is_linked and inp.links[0].from_node == ng:
if inp == o.links[0].to_socket:
pairs.append((str_ct, dest_ct))
dest_ct += 1
# not incremented on non linked socket
str_ct += 1
## not incremented on non-linked socket
return pairs
def reorder_fileout(fo, ng=None):
if not ng: # get connected nodegroup
for s in fo.inputs:
@@ -744,10 +775,18 @@ def reorder_fileout(fo, ng=None):
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))
## Order based on sockets indices
# for _ in range(len(ng.outputs)):
for _ in range(len(fo.inputs)):
# print(f'reorder iteration {_}')
pairs = get_pairs(ng, fo) # build index pairs
pair = next((p for p in pairs if p[0] is not None and p[1] is not None and p[0] != p[1]), None)
if pair is None:
## if pair is None, we have iterated over all pairs without index difference (ordered)
break
print(pair)
fo.inputs.move(pair[1], pair[0])
def reorganise_NG_nodegroup(ng):
'''refit node content to avoid overlap'''
@@ -1851,6 +1890,9 @@ def clean_mats_duplication(ob, skip_different_materials=True):
basemat = bpy.data.materials.get(match.group(1))
if not basemat:
continue
if not basemat.is_grease_pencil:
## Skip material with matching "stem" but not being GP material
continue
diff = different_gp_mat(mat, basemat)
if diff:
print(f'! {ob.name} : {diff}')
@@ -1900,6 +1942,9 @@ def connect_to_file_output(node_list, file_out=None, base_path='', excludes=None
excludes (dict, optionnal): List of output names to exclude {node_name: [outputs,]}.
Defaults toNone.
remap_names (dict, optionnal): List of output names to remap {node_name: {output_name: new_name}}.
frame (bpy.types.CompositorNode, optional): If given, create nodes into a frame.
Defaults to None.
@@ -1930,7 +1975,7 @@ def connect_to_file_output(node_list, file_out=None, base_path='', excludes=None
out_base = node.label
else:
out_base = node.name
bpy.path.clean_name(out_base)
out_base = bpy.path.clean_name(out_base)
out_name = f'OUT_{out_base}'
if outs:
@@ -1963,7 +2008,7 @@ def connect_to_file_output(node_list, file_out=None, base_path='', excludes=None
if next((l for l in o.links if recursive_node_connect_check(l, fo)), None):
continue
if remap_names and (custom_name := remap_names.get(o.name)):
if (socket_remaps := remap_names.get(node.name)) and (custom_name := socket_remaps.get(o.name)):
slot_name = bpy.path.clean_name(custom_name) # clean name ?
else:
slot_name = bpy.path.clean_name(o.name)
@@ -2026,7 +2071,8 @@ def connect_to_file_output(node_list, file_out=None, base_path='', excludes=None
if next((l for l in o.links if recursive_node_connect_check(l, fo)), None):
continue
if remap_names and (custom_name := remap_names.get(o.name)):
# if remap_names and (custom_name := remap_names.get(o.name)):
if (socket_remaps := remap_names.get(node.name)) and (custom_name := socket_remaps.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
+12 -5
View File
@@ -1,5 +1,5 @@
import bpy
from mathutils import Vector
from mathutils import Vector, Color
from . import fn
def add_rlayer(layer_name, scene=None, node_scene=None, location=None, color=None, node_name=None, width=400):
@@ -317,6 +317,12 @@ def connect_render_layer(rlayer, ng=None, out=None, frame=None,
return ng, out
def clamp_color_value(color, clamp_value=0.65) -> Color:
'''Return Color instance with clamped value component'''
color = Color(color)
color.v = min(color.v, clamp_value)
return color
def get_set_viewlayer_from_gp(ob, l, scene=None, node_scene=None,
base_path=None, file_slot=None, layer_slot=None):
'''setup ouptut from passed gp obj > layer
@@ -434,7 +440,7 @@ def get_set_viewlayer_from_gp(ob, l, scene=None, node_scene=None,
# use same color as layer
if fn.has_channel_color(l):
cp.use_custom_color = True
cp.color = l.channel_color
cp.color = clamp_color_value(l.channel_color)
connect_render_layer(cp, frame=frame,
base_path=base_path, file_slot=file_slot, layer_slot=layer_slot)
@@ -455,7 +461,7 @@ def get_set_viewlayer_from_gp(ob, l, scene=None, node_scene=None,
if fn.has_channel_color(l):
cp.use_custom_color = True
cp.color = l.channel_color
cp.color = clamp_color_value(l.channel_color)
frame = next((f for f in nodes if f.type == 'FRAME' and f.label == ob.name), None)
rl_nodes = frame_dic[frame.label] # get nodes from
@@ -518,9 +524,10 @@ def export_gp_objects(oblist, exclude_list=[], scene=None, node_scene=None, base
for l in ob.data.layers:
# if l.hide:
# continue
if l.hide or any(x + '_' in l.info for x in exclude_list): # exclude hided ?
if l.hide or l.opacity == 0 or any(x + '_' in l.info for x in exclude_list):
print(f'Exclude export: {ob.name} : {l.info}')
l.viewlayer_render = fn.get_view_layer('exclude', scene=scene).name # assign "exclude"
# Assign "exclude" layer
l.viewlayer_render = fn.get_view_layer('exclude', scene=scene).name
continue
get_set_viewlayer_from_gp(ob, l, scene=scene, node_scene=node_scene,
+5 -1
View File
@@ -274,7 +274,11 @@ class GPEXP_PT_gp_dopesheet_ui(Panel):
row.operator('gp.auto_number_object', icon='X', text='').delete = True
col.operator('gp.lower_layers_name', icon='SYNTAX_OFF', text='Rename Lowercase')
col.operator('gp.export_infos_for_compo', icon='FILE', text='Export Layers Infos') # Not really need, called in Check layers invoke
col.operator('gp.layers_state', icon='CHECKMARK', text='Check layers')
row = layout.row(align=True)
row.operator('gp.layers_state', icon='CHECKMARK', text='Check layers')
row.operator('gp.restore_layers_state', icon='DECORATE_OVERRIDE', text='')
col.operator('gp.check_masks', icon='MOD_MASK', text='Has Masks')
# row = layout.row()