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 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 1.8.1
- added: transfer timeline marker to render scene - 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') # print('Swap to bg cam')
# bpy.ops.gp.swap_render_cams() # bpy.ops.gp.swap_render_cams()
if render_scn.objects.get('bg_cam'): ## Switch to bg_cam (bad to set a camera by name)
render_scn.camera = render_scn.objects.get('bg_cam') # if render_scn.objects.get('bg_cam'):
fn.set_resolution_from_cam_prop(scene=render_scn) # 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!) ## Go to camera view in visible viewports (! Need timer + Already done in workspace script!)
# if not bpy.app.background: # 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') description='Set single user on all objects GP data')
excluded_prefix : StringProperty( 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)') description='Exclude layer to send to render by prefix (comma separated list)')
clean_name_and_visibility : BoolProperty(name='Clean Name And Visibility', default=True, 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') 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 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, set_layers_colors : BoolProperty(name='Set Layers Colors', default=True,
description='Set colors for on layers according to prefix (hadrcoded color set)') 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, export_layer_infos : BoolProperty(name='Export Layer Infos', default=True,
description='Export layers infos to a Json file') 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, 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') 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, add_preview : BoolProperty(name='Add Preview', default=True,
description='Create preview with stacked alpha over on render layers') 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 # File output templates
base_path : bpy.props.StringProperty(name='Base Path', default='', options={'SKIP_SAVE'}) base_path : bpy.props.StringProperty(name='Base Path', default='', options={'SKIP_SAVE'})
file_slot : bpy.props.StringProperty(name='File Slot', 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_rename_lowercase')
col.prop(self, 'trigger_renumber_by_distance') col.prop(self, 'trigger_renumber_by_distance')
col.prop(self, 'export_layer_infos') col.prop(self, 'export_layer_infos')
col.prop(self, 'reset_layers_state')
col.label(text='Send prefixed layer to render scene (except excluded)') col.label(text='Send prefixed layer to render scene (except excluded)')
col.prop(self, 'excluded_prefix', text='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: if not node_scene:
node_scene = render_scn 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: if self.clean_name_and_visibility:
for o in all_gp_objects: for o in all_gp_objects:
if o.hide_render: if o.hide_render:
@@ -204,10 +221,10 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator):
l.info = l.info + o.name.lower() l.info = l.info + o.name.lower()
## Make used prefix visible ?? (maybe some layer were intentionally hidden...) ## Make used prefix visible ?? (maybe some layer were intentionally hidden...)
if (res := re.search(r'^([A-Z]{2})_', l.info)): # 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: # 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') # print(f'{o.name} -> {l.info} : Switch visibility On')
l.hide = False # l.hide = False
if self.clean_material_duplication: if self.clean_material_duplication:
print('Clean material duplicates') 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)') print('Export layer infos (skip if json already exists)')
bpy.ops.gp.export_infos_for_compo('INVOKE_DEFAULT', skip_check=True) 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) ## Set GP object data to single user (Individual viewlayers)
if self.make_gp_single_user: if self.make_gp_single_user:
for o in ob_list: 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') self.report({'ERROR'}, 'No render scene found')
return {'CANCELLED'} 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 ## Group all adjacent layer type
if self.group_all_adjacent_layer_type: if self.group_all_adjacent_layer_type:
print('Group all adjacent layer type') print('Group all adjacent layer type')
for ob in ob_list: for ob in ob_list:
fn.group_adjacent_layer_prefix_rlayer(ob, excluded_prefix=self.excluded_prefix, first_name=True) 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 # bpy.ops.gp_export.render_scene_setup() # next render scene setup at once
## attempt to refresh scene ## attempt to refresh scene
# render_scn.node_tree.nodes.update() # 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) # batch_setup_render_scene(render_scn=render_scn)
## set at least one GP object active ## 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: if gp_ob:
context.view_layer.objects.active = 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 return reports
for fo in file_outs: 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}') reports.append(f'No object numbering : node {fo.name}')
pct = 0 pct = 0
if fo.format.file_format == 'OPEN_EXR_MULTILAYER': if fo.format.file_format == 'OPEN_EXR_MULTILAYER':
## multilayer use layer_slots > slot.name ## 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: if self.clear_unused_view_layers:
used_rlayer_names = [n.layer for n in nodes if n.type == 'R_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): 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 continue
scn.view_layers.remove(rl) scn.view_layers.remove(rl)
vl_number -= 1
if self.arrange_rl_nodes: if self.arrange_rl_nodes:
fn.rearrange_rlayers_in_frames(node_scene.node_tree) fn.rearrange_rlayers_in_frames(node_scene.node_tree)
+415 -4
View File
@@ -1,12 +1,24 @@
from encodings import utf_8 from encodings import utf_8
import bpy import bpy
import json
# import bpy_extras # import bpy_extras
from bpy_extras.object_utils import world_to_camera_view # as cam_space 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 mathutils import Vector
from pathlib import Path from pathlib import Path
import json
from . import fn 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(): def Export_AE_2d_position_json_data():
scn = bpy.context.scene 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)) 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...) # Unused (old func that might not be usefull at all...)
def correct_shift(vec, cam): 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_keys_to_ae")
col.operator("gp.export_cam_keys_to_ae") col.operator("gp.export_cam_keys_to_ae")
# def overcan_shift_fix_ui(self, context): # def overcan_shift_fix_ui(self, context):
# layout = self.layout # layout = self.layout
# layout.operator("gp.fix_overscan_shift") # 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=( classes=(
GPEXP_OT_export_keys_to_ae, GPEXP_OT_export_keys_to_ae,
GPEXP_OT_export_cam_keys_to_ae, GPEXP_OT_export_cam_keys_to_ae,
GPEXP_OT_fix_overscan_shift, GPEXP_OT_fix_overscan_shift,
GPEXP_PT_extra_gprender_func GPEXP_PT_extra_gprender_func,
GPEXP_OT_export_anim_to_ae,
) )
def register(): def register():
for cls in classes: for cls in classes:
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
bpy.types.TOPBAR_MT_file_export.append(export_ae_anim_menu)
# if hasattr(bpy.types, 'RENDER_PT_overscan'): # if hasattr(bpy.types, 'RENDER_PT_overscan'):
# bpy.types.RENDER_PT_overscan.append(overcan_shift_fix_ui) # bpy.types.RENDER_PT_overscan.append(overcan_shift_fix_ui)
@@ -274,3 +684,4 @@ def unregister():
for cls in reversed(classes): for cls in reversed(classes):
bpy.utils.unregister_class(cls) 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"} return {"FINISHED"}
class GPEXP_PG_selectable_prop(bpy.types.PropertyGroup): 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") name: bpy.props.StringProperty(name="Name or Path")
socket_name: bpy.props.StringProperty(name="Source socket Name") # Source socket name as reference socket_name: bpy.props.StringProperty(name="Source socket Name") # Source socket name as reference
select: bpy.props.BoolProperty(name="Selected", default=True) select: bpy.props.BoolProperty(name="Selected", default=True)
is_linked: bpy.props.BoolProperty(name="Linked", default=False) is_linked: bpy.props.BoolProperty(name="Linked", default=False)
# is_valid: bpy.props.BoolProperty(name="Valid", default=True) # 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): class GPEXP_OT_connect_selected_to_file_out(bpy.types.Operator):
bl_idname = "gp.connect_selected_to_file_out" bl_idname = "gp.connect_selected_to_file_out"
bl_label = "Connect Selected To File Output" bl_label = "Connect Selected To File Output"
@@ -259,6 +264,24 @@ class GPEXP_OT_connect_selected_to_file_out(bpy.types.Operator):
name='Settings', name='Settings',
default=False) 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( base_path : bpy.props.StringProperty(
name='Custom base path', name='Custom base path',
default='', default='',
@@ -291,7 +314,6 @@ class GPEXP_OT_connect_selected_to_file_out(bpy.types.Operator):
), ),
) )
color_depth : bpy.props.EnumProperty( color_depth : bpy.props.EnumProperty(
name='Color Depth', name='Color Depth',
default='16', default='16',
@@ -322,7 +344,28 @@ class GPEXP_OT_connect_selected_to_file_out(bpy.types.Operator):
continue continue
item = self.socket_collection.add() item = self.socket_collection.add()
item.node_name = n.name 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) ## TODO: rename item.name according to tamplate pairs in preferences (to add later)
if o.is_linked: if o.is_linked:
item.is_linked = True item.is_linked = True
@@ -332,13 +375,15 @@ class GPEXP_OT_connect_selected_to_file_out(bpy.types.Operator):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
box = layout.box()
expand_icon = 'DISCLOSURE_TRI_DOWN' if self.show_custom_settings else 'DISCLOSURE_TRI_RIGHT' 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 ## Settings
if self.show_custom_settings: if self.show_custom_settings:
layout.use_property_split = True box.use_property_split = True
layout.prop(self, 'base_path') box.row().prop(self, 'name_type', expand=False)
col = layout.column() box.prop(self, 'base_path')
col = box.column()
col.prop(self, 'file_format') col.prop(self, 'file_format')
col.prop(self, 'exr_codec') col.prop(self, 'exr_codec')
col.row().prop(self, 'color_depth', expand=True) 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: if item.node_name != current_node_name:
current_node_name = item.node_name current_node_name = item.node_name
col.separator() 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() row = col.row()
if item.is_linked: if item.is_linked:
@@ -362,23 +416,46 @@ class GPEXP_OT_connect_selected_to_file_out(bpy.types.Operator):
display_name = item.socket_name display_name = item.socket_name
if 'crypto' in display_name.lower(): 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=display_name)
row.label(text='', icon='RIGHTARROW') row.label(text='', icon='RIGHTARROW')
if self.name_type == 'socket_name':
row.prop(item, 'name', text='') 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): def execute(self, context):
# Build exclude dict from selection # Build exclude dict from selection
excludes = {} excludes = {}
remap_names = {} 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): if len(self.socket_collection):
for item in 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: if not item.select:
# All deselected goes to exclude with {node_name: [socket1, ...]} # All deselected goes to exclude with {node_name: [socket1, ...]}
excludes.setdefault(item.node_name, []).append(item.name) excludes.setdefault(item.node_name, []).append(item.socket_name)
elif item.socket_name != item.name: elif item.socket_name != final_name:
remap_names[item.socket_name] = item.name remap_names.setdefault(item.node_name, {})[item.socket_name] = final_name
# remap_names[item.socket_name] = final_name
## Handle default file format ## Handle default file format
file_ext = self.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"} 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): class GPEXP_OT_layers_state(bpy.types.Operator):
bl_idname = "gp.layers_state" bl_idname = "gp.layers_state"
bl_label = "Set 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", # description="Delete view layer that aren't used in the nodetree anymore",
# default=True) # 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', all_objects : BoolProperty(name='On All Object',
default=True, description='On All object, else use selected objects') # , options={'SKIP_SAVE'} 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'} \nAffect all layers)') # , options={'SKIP_SAVE'}
opacity_exclude_list : StringProperty(name='Skip', 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'} \nSeparate multiple value with a comma (ex: MAIN)') # , options={'SKIP_SAVE'}
hide_invisible_materials : BoolProperty(name='Hide Materials named invisible', hide_invisible_materials : BoolProperty(name='Hide Materials named invisible',
default=True, description='Hide material with name starting with "invisible"') # , options={'SKIP_SAVE'} default=True, description='Hide material with name starting with "invisible"') # , options={'SKIP_SAVE'}
# @classmethod
@classmethod # def poll(cls, context):
def poll(cls, context): # return context.object
return context.object and context.object.type == 'GPENCIL'
def invoke(self, context, event): def invoke(self, context, event):
## if no existing infos.json generated, call ops ## if no existing infos.json generated, call ops
l_infos = Path(bpy.data.filepath).parent / 'render' / 'infos.json' l_infos = Path(bpy.data.filepath).parent / 'render' / 'infos.json'
if not l_infos.exists(): # only if infos not created if not l_infos.exists(): # only if infos not created
bpy.ops.gp.export_infos_for_compo('INVOKE_DEFAULT') 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) return context.window_manager.invoke_props_dialog(self)
def draw(self, context): def draw(self, context):
@@ -264,11 +297,15 @@ class GPEXP_OT_layers_state(bpy.types.Operator):
if l.opacity != 1: if l.opacity != 1:
# TODO Skip zeroed opacity ? if l.opacity == 0:
# check if there is an exclusion word ## Skip layer with zero opacity
if any(x.strip() + '_' in l.info for x in self.opacity_exclude_list.strip(',').split(',') if x): print(f'Skipped layer opacity reset (0 opacity) : {l.info}')
print(f'Skipped layer : {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: else:
## Set full opacity
full_opacity_state = '' if self.set_full_opacity else ' (check only)' full_opacity_state = '' if self.set_full_opacity else ' (check only)'
mess = f'{l.info} : opacity {l.opacity:.2f} >> 1.0{full_opacity_state}' mess = f'{l.info} : opacity {l.opacity:.2f} >> 1.0{full_opacity_state}'
print(mess) print(mess)
@@ -308,7 +345,7 @@ class GPEXP_OT_layers_state(bpy.types.Operator):
if changes: if changes:
changes.append('') 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 gp_mu_edit_ct = 0
for gp in bpy.data.grease_pencils: for gp in bpy.data.grease_pencils:
if gp.use_multiedit: if gp.use_multiedit:
@@ -328,10 +365,12 @@ class GPEXP_OT_layers_state(bpy.types.Operator):
m.grease_pencil.hide = True m.grease_pencil.hide = True
changes.append(f'{m.name} material hidden') changes.append(f'{m.name} material hidden')
if not self.no_popup:
fn.show_message_box(_message=changes, _title="Layers Check Report", _icon='INFO') fn.show_message_box(_message=changes, _title="Layers Check Report", _icon='INFO')
return {"FINISHED"} return {"FINISHED"}
class GPEXP_OT_lower_layers_name(bpy.types.Operator): class GPEXP_OT_lower_layers_name(bpy.types.Operator):
bl_idname = "gp.lower_layers_name" bl_idname = "gp.lower_layers_name"
bl_label = "Normalize Layers Name" bl_label = "Normalize Layers Name"
@@ -606,12 +645,6 @@ class GPEXP_OT_select_layer_in_comp(bpy.types.Operator):
infos.append(mess) infos.append(mess)
continue 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: if not vl_name in used_vl:
mess = f'{l.info}: view layer "{vl_name}" not used in scene renderlayer nodes' mess = f'{l.info}: view layer "{vl_name}" not used in scene renderlayer nodes'
print(mess) print(mess)
@@ -640,6 +673,7 @@ classes=(
GPEXP_OT_auto_number_object, GPEXP_OT_auto_number_object,
GPEXP_OT_lower_layers_name, GPEXP_OT_lower_layers_name,
GPEXP_OT_export_infos_for_compo, GPEXP_OT_export_infos_for_compo,
GPEXP_OT_restore_layers_state,
GPEXP_OT_layers_state, GPEXP_OT_layers_state,
GPEXP_OT_check_masks, GPEXP_OT_check_masks,
GPEXP_OT_select_layer_in_comp, 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) `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 ### Notable operator arguments for automatisation
+1 -1
View File
@@ -2,7 +2,7 @@ bl_info = {
"name": "GP Render", "name": "GP Render",
"description": "Organise export of gp layers through compositor output", "description": "Organise export of gp layers through compositor output",
"author": "Samuel Bernou", "author": "Samuel Bernou",
"version": (1, 8, 1), "version": (1, 8, 18),
"blender": (3, 0, 0), "blender": (3, 0, 0),
"location": "View3D", "location": "View3D",
"warning": "", "warning": "",
+69 -23
View File
@@ -1,13 +1,14 @@
from typing import Coroutine
import bpy import bpy
import os import os
import re import re
import json
from mathutils import Vector from mathutils import Vector
from pathlib import Path from pathlib import Path
from itertools import groupby
from math import isclose from math import isclose
from collections import defaultdict from collections import defaultdict
from time import time from time import time
import json
from . constant import RD_SCENE_NAME from . constant import RD_SCENE_NAME
@@ -151,19 +152,18 @@ def copy_settings(obj_a, obj_b):
pass pass
def set_file_output_format(fo): 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 env_file_format = json.loads(os.environ.get('GP_RENDER_FILE_FORMAT', '{}'))
# fo.format.file_format = 'PNG' if not env_file_format:
# fo.format.color_mode = 'RGBA' env_file_format = {
# fo.format.color_depth = '8' 'file_format': 'OPEN_EXR_MULTILAYER',
# fo.format.compression = 15 '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): def set_scene_aa_settings(scene=None, aa=True):
'''aa == using native AA, else disable scene AA''' '''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() scn.node_tree.nodes.clear()
set_settings(scn) set_settings(scn)
scn.gp_render_settings['use_aa'] = True 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 return scn
def get_render_scene(scene_name=None, create=True): 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) render_scn = bpy.data.scenes.get(scene_name)
if render_scn: if render_scn:
return render_scn return render_scn
if not create: if not create:
return 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 :first_name: Keep the viewlayer of the bottom layer in group, else last
''' '''
from itertools import groupby pattern = os.environ.get('GP_RENDER_MERGE_PATTERN', r'^([A-Z]{2})_')
re_prefix = re.compile(r'^([A-Z]{2})_') re_prefix = re.compile(pattern, flags=re.IGNORECASE)
if isinstance(excluded_prefix, str): if isinstance(excluded_prefix, str):
excluded_prefix = [p.strip() for p in excluded_prefix.split(',')] 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}') print(f'Deleting unlinked fileout slot: {inp.name}')
fo.inputs.remove(inp) 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): def reorder_fileout(fo, ng=None):
if not ng: # get connected nodegroup if not ng: # get connected nodegroup
for s in fo.inputs: for s in fo.inputs:
@@ -744,10 +775,18 @@ def reorder_fileout(fo, ng=None):
if not ng: if not ng:
print(f'No nodegroup to refer to filter {fo.name}') print(f'No nodegroup to refer to filter {fo.name}')
return 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: ## Order based on sockets indices
all_outnames = [s.name for s in fo.inputs] # same as [fs.path for fs in fo.file_slots] # for _ in range(len(ng.outputs)):
fo.inputs.move(all_outnames.index(s_name), ordered.index(s_name)) 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): def reorganise_NG_nodegroup(ng):
'''refit node content to avoid overlap''' '''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)) basemat = bpy.data.materials.get(match.group(1))
if not basemat: if not basemat:
continue continue
if not basemat.is_grease_pencil:
## Skip material with matching "stem" but not being GP material
continue
diff = different_gp_mat(mat, basemat) diff = different_gp_mat(mat, basemat)
if diff: if diff:
print(f'! {ob.name} : {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,]}. excludes (dict, optionnal): List of output names to exclude {node_name: [outputs,]}.
Defaults toNone. 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. frame (bpy.types.CompositorNode, optional): If given, create nodes into a frame.
Defaults to None. 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 out_base = node.label
else: else:
out_base = node.name out_base = node.name
bpy.path.clean_name(out_base) out_base = bpy.path.clean_name(out_base)
out_name = f'OUT_{out_base}' out_name = f'OUT_{out_base}'
if outs: 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): if next((l for l in o.links if recursive_node_connect_check(l, fo)), None):
continue 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 ? slot_name = bpy.path.clean_name(custom_name) # clean name ?
else: else:
slot_name = bpy.path.clean_name(o.name) 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): if next((l for l in o.links if recursive_node_connect_check(l, fo)), None):
continue 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 ? slot_name = bpy.path.clean_name(custom_name) # clean name ?
else: else:
slot_name = bpy.path.clean_name(o.name) # directly use name in multi layer exr 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 import bpy
from mathutils import Vector from mathutils import Vector, Color
from . import fn from . import fn
def add_rlayer(layer_name, scene=None, node_scene=None, location=None, color=None, node_name=None, width=400): 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 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, def get_set_viewlayer_from_gp(ob, l, scene=None, node_scene=None,
base_path=None, file_slot=None, layer_slot=None): base_path=None, file_slot=None, layer_slot=None):
'''setup ouptut from passed gp obj > layer '''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 # use same color as layer
if fn.has_channel_color(l): if fn.has_channel_color(l):
cp.use_custom_color = True cp.use_custom_color = True
cp.color = l.channel_color cp.color = clamp_color_value(l.channel_color)
connect_render_layer(cp, frame=frame, connect_render_layer(cp, frame=frame,
base_path=base_path, file_slot=file_slot, layer_slot=layer_slot) 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): if fn.has_channel_color(l):
cp.use_custom_color = True 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) 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 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: for l in ob.data.layers:
# if l.hide: # if l.hide:
# continue # 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}') 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 continue
get_set_viewlayer_from_gp(ob, l, scene=scene, node_scene=node_scene, 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 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.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.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') col.operator('gp.check_masks', icon='MOD_MASK', text='Has Masks')
# row = layout.row() # row = layout.row()