Compare commits
28
Commits
e97a86b624
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3904ffa024 | ||
|
|
0dae5157b1 | ||
|
|
d2a19ae9e3 | ||
|
|
cc1646081b | ||
|
|
26bcb56815 | ||
|
|
af2d8e1ad4 | ||
|
|
52e1f7ad0e | ||
|
|
9b76e380ae | ||
|
|
35a99ff6cd | ||
|
|
09a47413c3 | ||
|
|
19c43e6268 | ||
|
|
772af8cb3f | ||
|
|
bd7b022af8 | ||
|
|
c5d84ddb16 | ||
|
|
adbb813410 | ||
|
|
09eca87060 | ||
|
|
5c69bd0185 | ||
|
|
d6a102b044 | ||
|
|
e67adcb3f7 | ||
|
|
b0f62a732d | ||
|
|
8be44fa1e5 | ||
|
|
ea0561e14e | ||
|
|
2f8efed73b | ||
|
|
85a42939ca | ||
|
|
734714be09 | ||
|
|
747638eca9 | ||
|
|
1121c489a8 | ||
|
|
2e9b379144 |
@@ -13,6 +13,89 @@ if duplicate, need to "connect" with namespace ('_duprender') or something
|
|||||||
Activate / deactivate layer opacity according to prefix
|
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
|
1.8.2
|
||||||
|
|
||||||
- added: `Reset Layer State` (Check layer) in Autobuild
|
- added: `Reset Layer State` (Check layer) in Autobuild
|
||||||
|
|||||||
+30
-17
@@ -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,
|
||||||
@@ -129,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'})
|
||||||
@@ -194,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:
|
||||||
@@ -208,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')
|
||||||
@@ -255,7 +268,7 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator):
|
|||||||
|
|
||||||
## Reset layer states -> 100% opacity (except mask layer 'MA' by default), remove use light, set default blend mode, hide material named "invisible".
|
## 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:
|
if self.reset_layers_state:
|
||||||
bpy.ops.gp.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:
|
||||||
@@ -276,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()
|
||||||
@@ -329,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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||||
|
|||||||
+1
-1
@@ -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": "",
|
||||||
|
|||||||
@@ -152,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'''
|
||||||
@@ -301,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):
|
||||||
@@ -311,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
|
||||||
|
|
||||||
@@ -591,7 +593,7 @@ def group_adjacent_layer_prefix_rlayer(ob, excluded_prefix=[], first_name=True):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
pattern = os.environ.get('GP_RENDER_MERGE_PATTERN', r'^([A-Z]{2})_')
|
pattern = os.environ.get('GP_RENDER_MERGE_PATTERN', r'^([A-Z]{2})_')
|
||||||
re_prefix = re.compile(pattern)
|
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(',')]
|
||||||
@@ -736,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:
|
||||||
@@ -745,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'''
|
||||||
@@ -1852,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}')
|
||||||
@@ -1901,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.
|
||||||
|
|
||||||
@@ -1931,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:
|
||||||
@@ -1964,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)
|
||||||
@@ -2027,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
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user