Refactor to allow separate gp render scene and compositing scene

1.7.0

- fix: problem when removing render layers
- changed: node distribution refactor, allow separate compositing scene
  - Compositing scene (holding nodes) can be separated from render scene (holding GP objects and related viewlayers)
  - Default render named changed from `Render` to `RenderGP`
  - New properties in exposed Dopesheet N panel to manually set Render scene and Compo scene
  - Operator expose a `node_scene` parameter to separate where to send nodes
  - Switch scene button can have an extra button to go in compo scene if found
main
pullusb 2024-04-16 18:01:10 +02:00
parent fb810c1256
commit c6c9c7f56c
18 changed files with 538 additions and 263 deletions

4
.gitignore vendored
View File

@ -1,2 +1,4 @@
__pycache__ __pycache__
*.py[cod] *.py[cod]
pyrightconfig.json
*.vscode

View File

@ -14,6 +14,16 @@ Activate / deactivate layer opacity according to prefix
Activate / deactivate all masks using MA layers Activate / deactivate all masks using MA layers
--> -->
1.7.0
- fix: problem when removing render layers
- changed: node distribution refactor, allow separate compositing scene
- Compositing scene (holding nodes) can be separated from render scene (holding GP objects and related viewlayers)
- Default render named changed from `Render` to `RenderGP`
- New properties in exposed Dopesheet N panel to manually set Render scene and Compo scene
- Operator expose a `node_scene` parameter to separate where to send nodes
- Switch scene button can have an extra button to go in compo scene if found
1.6.4 1.6.4
- changed: remove poll checking gp active on some operators limiting use in script call - changed: remove poll checking gp active on some operators limiting use in script call

View File

@ -1,6 +1,40 @@
import bpy import bpy
from . import gen_vlayer, fn from . import gen_vlayer, fn
def add_layer_to_render(ob, node_scene=None):
'''Send GP object to render layer
return a tuple with report message'''
# ob = ob or bpy.context.object
layer = ob.data.layers.active
if not layer:
return ('ERROR', 'No active layer')
node_scene = fn.get_compo_scene(scene_name=node_scene, create=True)
ct = 0
# send scene ?
hidden = 0
for l in ob.data.layers:
if not l.select:
if not l.viewlayer_render:
# TODO : need to link, can raise error if object is not linked in Render scene yet
l.viewlayer_render = fn.get_view_layer('exclude').name
continue
gen_vlayer.get_set_viewlayer_from_gp(ob, l, node_scene=node_scene)
if l.hide:
hidden += 1
ct += 1
if hidden:
return ('WARNING', f'{hidden}/{ct} layers are hidden!')
else:
return ('INFO', f'{ct} layer(s) added')
class GPEXP_OT_add_layer_to_render(bpy.types.Operator): class GPEXP_OT_add_layer_to_render(bpy.types.Operator):
bl_idname = "gp.add_layer_to_render" bl_idname = "gp.add_layer_to_render"
bl_label = "Add Gp Layer as render nodes" bl_label = "Add Gp Layer as render nodes"
@ -12,73 +46,76 @@ class GPEXP_OT_add_layer_to_render(bpy.types.Operator):
return context.object and context.object.type == 'GPENCIL' return context.object and context.object.type == 'GPENCIL'
# mode : bpy.props.StringProperty(options={'SKIP_SAVE'}) # mode : bpy.props.StringProperty(options={'SKIP_SAVE'})
node_scene : bpy.props.StringProperty(default='',
description='Scene where to add nodes, Abort if not found', options={'SKIP_SAVE'})
def execute(self, context): def execute(self, context):
ob = context.object ret = add_layer_to_render(context.object, node_scene=self.node_scene)
layer = ob.data.layers.active if isinstance(ret, tuple):
if not layer: self.report({ret[0]}, ret[1])
self.report({'ERROR'}, 'No active layer') if ret[0] == 'ERROR':
return {"CANCELLED"} return {'CANCELLED'}
ct = 0
# send scene ?
hided = 0
for l in ob.data.layers:
if not l.select:
if not l.viewlayer_render:
# TODO : need to link, can raise error if object is not linked in Render scene yet
l.viewlayer_render = fn.get_view_layer('exclude').name
continue
gen_vlayer.get_set_viewlayer_from_gp(ob, l)
if l.hide:
hided += 1
ct += 1
if hided:
self.report({'WARNING'}, f'{hided}/{ct} layers are hided !')
else:
self.report({'INFO'}, f'{ct} layer(s) added to scene "Render"')
return {"FINISHED"} return {"FINISHED"}
def add_object_to_render(mode='ALL', scene='', node_scene=''):
context = bpy.context
if scene:
scn = fn.get_render_scene(scene)
else:
scn = fn.get_render_scene()
if node_scene:
node_scn = fn.get_compo_scene(scene_name=node_scene, create=True)
if not node_scn:
return ('ERROR', f'/!\ Node Scene "{node_scene}" not found ! Abort "Add object to Render" !')
else:
# if not passed add in render scene
node_scn = scn
excludes = [] # ['MA', 'IN'] # Get list dynamically
if mode == 'SELECTED':
gen_vlayer.export_gp_objects([o for o in context.selected_objects if o.type == 'GPENCIL'], exclude_list=excludes, scene=scn, node_scene=node_scn)
elif mode == 'ALL':
gen_vlayer.export_gp_objects([o for o in context.scene.objects if o.type == 'GPENCIL' and not o.hide_get() and fn.is_valid_name(o.name)], exclude_list=excludes, scene=scn, node_scene=node_scn)
## send operator with mode ALL or SELECTED to batch build ## send operator with mode ALL or SELECTED to batch build
class GPEXP_OT_add_objects_to_render(bpy.types.Operator): class GPEXP_OT_add_objects_to_render(bpy.types.Operator):
bl_idname = "gp.add_object_to_render" bl_idname = "gp.add_object_to_render"
bl_label = "Add all Gp Layer of active object as render nodes" bl_label = "Add all Gp Layer of active object as render nodes"
bl_description = "Setup GP object in render scene\ bl_description = "Setup GP object in render scene\
\nNote: 'send all' mode skip hided objects" \nNote: 'send all' mode skip hidden objects"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GPENCIL' return context.object and context.object.type == 'GPENCIL'
mode : bpy.props.StringProperty(default='ALL', options={'SKIP_SAVE'}) mode : bpy.props.EnumProperty(
items=(
('ALL', 'All', 'All objects', 0),
('SELECTED', 'Selected', 'Selected objects', 0),
),
default='ALL', options={'SKIP_SAVE'},
description='Choice to send all or only selected objects')
scene : bpy.props.StringProperty(default='',
description='Scene where to link object and create viewlayer (create if not exists)', options={'SKIP_SAVE'})
node_scene : bpy.props.StringProperty(default='',
description='Scene where to add nodes, Abort if not found', options={'SKIP_SAVE'})
def execute(self, context): def execute(self, context):
# create render scene ret = add_object_to_render(mode=self.mode, scene=self.scene, node_scene=self.node_scene)
if context.scene.name == 'Scene': if isinstance(ret, tuple):
scn = fn.get_render_scene() self.report({ret[0]}, ret[1])
else: if ret[0] == 'ERROR':
scn = context.scene return {'CANCELLED'}
excludes = [] # ['MA', 'IN'] # Get list dynamically
if self.mode == 'SELECTED':
gen_vlayer.export_gp_objects([o for o in context.selected_objects if o.type == 'GPENCIL'], exclude_list=excludes, scene=scn)
elif self.mode == 'ALL':
# scn = bpy.data.scenes.get('Scene')
# if not scn:
# self.report({'ERROR'}, 'Could not found default scene')
# return {"CANCELLED"}
gen_vlayer.export_gp_objects([o for o in context.scene.objects if o.type == 'GPENCIL' and not o.hide_get() and fn.is_valid_name(o.name)], exclude_list=excludes, scene=scn)
return {"FINISHED"} return {"FINISHED"}
class GPEXP_OT_split_to_scene(bpy.types.Operator): class GPEXP_OT_split_to_scene(bpy.types.Operator):
bl_idname = "gp.split_to_scene" bl_idname = "gp.split_to_scene"
bl_label = "Split Objects To Scene" bl_label = "Split Objects To Scene"

View File

@ -2,9 +2,10 @@ import bpy
import re import re
from pathlib import Path from pathlib import Path
from . import gen_vlayer, fn from . import gen_vlayer, fn
from bpy.props import (BoolProperty, StringProperty)
def batch_setup_render_scene(context=None, render_scn=None, preview=True): def batch_setup_render_scene(context=None, render_scn=None, node_scene=None, preview=True):
'''A series of setup actions for Render scene: '''A series of setup actions for Render scene:
- renumber fileout - renumber fileout
- Clean compo Tree - Clean compo Tree
@ -15,10 +16,12 @@ def batch_setup_render_scene(context=None, render_scn=None, preview=True):
if context is None: if context is None:
context = bpy.context context = bpy.context
if render_scn is None: if render_scn is None:
render_scn = bpy.data.scenes.get('Render') render_scn = fn.get_render_scene(create=False)
if not render_scn: if not render_scn:
print('"Render" scene not found in batch_setup_render_scene') print('Render scene not found in batch_setup_render_scene')
return return
if node_scene is None:
node_scene = render_scn
## Renumber File outputs ## Renumber File outputs
print('Renumber File outputs') print('Renumber File outputs')
@ -49,8 +52,7 @@ def batch_setup_render_scene(context=None, render_scn=None, preview=True):
## Clean compo Tree ## Clean compo Tree
print('Clean compo Tree') print('Clean compo Tree')
bpy.ops.gp.clean_compo_tree('EXEC_DEFAULT', use_render_scene=True) bpy.ops.gp.clean_compo_tree('EXEC_DEFAULT', scene=render_scn.name, node_scene=node_scene.name)
# bpy.ops.gp.clean_compo_tree('INVOKE_DEFAULT', use_render_scene=True)
## Trigger check file before finishing ? ## Trigger check file before finishing ?
# bpy.ops.gp.check_render_scene('INVOKE_DEFAULT') # bpy.ops.gp.check_render_scene('INVOKE_DEFAULT')
@ -63,55 +65,57 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator):
bl_description = "Trigger all operation to make build render scene with default settings" bl_description = "Trigger all operation to make build render scene with default settings"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
# @classmethod scene : StringProperty(name="Target Scene Name",
# def poll(cls, context): description="Render scene to send GP to on a named scene, abort if not exists (not exposed)",
# return context.object and context.object.type == 'GPENCIL' default='', options={'SKIP_SAVE'})
# timer : bpy.props.FloatProperty(default=0.1, options={'SKIP_SAVE'}) node_scene : StringProperty(name="Compositing Scene",
description="Name of the scene holding compositing nodes",
default='', options={'SKIP_SAVE'})
make_gp_single_user : bpy.props.BoolProperty(name='Set Single User Data', default=True, make_gp_single_user : BoolProperty(name='Set Single User Data', default=True,
description='Set single user on all objects GP data') description='Set single user on all objects GP data')
excluded_prefix : bpy.props.StringProperty( excluded_prefix : StringProperty(
name='Excluded Layer By Prefix', default='GP, RG, PO, MA', name='Excluded Layer By Prefix', default='GP, RG, PO, MA',
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 : bpy.props.BoolProperty(name='Clean Name And Visibility', default=True, clean_name_and_visibility : BoolProperty(name='Clean Name And Visibility', default=True,
description='Add object name to layer name when there is only prefix (ex: "CO_")\ description='Add object name to layer name when there is only prefix (ex: "CO_")\
\nEnable visibility for layer with prefix included in Prefix Filter') \nEnable visibility for layer with prefix included in Prefix Filter')
clean_material_duplication : bpy.props.BoolProperty(name='Clean Material Duplication', default=True, clean_material_duplication : BoolProperty(name='Clean Material Duplication', default=True,
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 : bpy.props.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. Set the other prefix and non-prefixed layer to exluded viewlayer')
set_layers_colors : bpy.props.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)')
trigger_rename_lowercase : bpy.props.BoolProperty(name='Trigger Rename Lowercase', default=True, trigger_rename_lowercase : BoolProperty(name='Trigger Rename Lowercase', default=True,
description='Rename all layer names lowercase') description='Rename all layer names lowercase')
trigger_renumber_by_distance : bpy.props.BoolProperty(name='Trigger Renumber By Distance', default=True, trigger_renumber_by_distance : BoolProperty(name='Trigger Renumber By Distance', default=True,
description='Renumber object accordind to distance from camera and In-Front value') description='Renumber object accordind to distance from camera and In-Front value')
export_layer_infos : bpy.props.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')
group_all_adjacent_layer_type : bpy.props.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')
change_to_gp_workspace : bpy.props.BoolProperty(name='Change To Gp Workspace', default=True, change_to_gp_workspace : BoolProperty(name='Change To Gp Workspace', default=True,
description='Switch to "GP Render" workspace shipped with addon') description='Switch to "GP Render" workspace shipped with addon')
batch_setup_render_scene : bpy.props.BoolProperty(name='Batch Setup Render Scene', default=True, batch_setup_render_scene : BoolProperty(name='Batch Setup Render Scene', default=True,
description='- Renumber fileoutputs\ description='- Renumber fileoutputs\
\n- Clean compo Tree\ \n- Clean compo Tree\
\n- Go to camera view in visible viewports\ \n- Go to camera view in visible viewports\
\n- Swap to bg cam' \n- Swap to bg cam'
) )
add_preview : bpy.props.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')
def invoke(self, context, event): def invoke(self, context, event):
@ -153,10 +157,21 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator):
prefix_to_render = [p.strip() for p in self.prefix_filter.split(',')] prefix_to_render = [p.strip() for p in self.prefix_filter.split(',')]
print('prefix_to_render: ', prefix_to_render) print('prefix_to_render: ', prefix_to_render)
render_scn = bpy.data.scenes.get('Render') render_scn = fn.get_render_scene(create=False)
if render_scn: if self.scene:
self.report({'ERROR'}, 'A "Render" scene already exists') render_scn = bpy.data.scenes.get(self.scene)
return {'CANCELLED'} if render_scn:
self.report({'ERROR'}, f'Abort, scene "{render_scn.name}" already exists')
return {'CANCELLED'}
if self.node_scene:
node_scene = fn.get_compo_scene(scene_name=self.node_scene, create=True) # create if not exists
## Set scene target in source scene
context.scene.gp_render_settings.node_scene = node_scene.name
else:
node_scene = fn.get_compo_scene(create=True)
if not node_scene:
node_scene = render_scn
all_gp_objects = [o for o in context.scene.objects if o.type == 'GPENCIL'] all_gp_objects = [o for o in context.scene.objects if o.type == 'GPENCIL']
@ -206,13 +221,11 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator):
if self.trigger_rename_lowercase: if self.trigger_rename_lowercase:
print('Trigger rename lowercase') print('Trigger rename lowercase')
bpy.ops.gp.lower_layers_name('EXEC_DEFAULT') bpy.ops.gp.lower_layers_name('EXEC_DEFAULT')
# bpy.ops.gp.lower_layers_name('INVOKE_DEFAULT')
## Trigger renumber by distance ## Trigger renumber by distance
if self.trigger_renumber_by_distance: if self.trigger_renumber_by_distance:
print('Trigger renumber by distance') print('Trigger renumber by distance')
bpy.ops.gp.auto_number_object('EXEC_DEFAULT') bpy.ops.gp.auto_number_object('EXEC_DEFAULT')
# bpy.ops.gp.auto_number_object('INVOKE_DEFAULT')
## Export layer infos ? (skip if json already exists) ## Export layer infos ? (skip if json already exists)
if self.export_layer_infos: if self.export_layer_infos:
@ -226,13 +239,13 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator):
o.data = o.data.copy() o.data = o.data.copy()
## Send all GP to render scene ## Send all GP to render scene
print('Send all GP to render scene') print('Send all GP to render scene (Create render scene if needed)')
# bpy.ops.gp.add_object_to_render(mode="ALL") # Ops to send all # bpy.ops.gp.add_object_to_render(mode="ALL") # Ops to send all
gen_vlayer.export_gp_objects(ob_list, exclude_list=self.excluded_prefix) # Create render scene OTF gen_vlayer.export_gp_objects(ob_list, exclude_list=self.excluded_prefix, scene=render_scn, node_scene=node_scene) # Create render scene OTF
## Switch to new Render Scene ## Switch to new Render Scene
print('Switch to new Render Scene') print('Switch to new Render Scene')
render_scn = bpy.data.scenes.get('Render') render_scn = fn.get_render_scene(create=False)
if not render_scn: if not render_scn:
self.report({'ERROR'}, 'No render scene found') self.report({'ERROR'}, 'No render scene found')
return {'CANCELLED'} return {'CANCELLED'}
@ -268,13 +281,13 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator):
# context.scene.update_tag() # context.scene.update_tag()
## Batch setup render scene ## Batch setup render scene
batch_setup_render_scene(render_scn=render_scn) batch_setup_render_scene(render_scn=render_scn, node_scene=node_scene)
## create preview ## create preview
if self.add_preview: if self.add_preview:
from .OP_merge_layers import merge_compositor_preview from .OP_merge_layers import merge_compositor_preview
merge_compositor_preview(scene=render_scn) merge_compositor_preview(scene=node_scene)
## No need for timer anymore ! ## No need for timer anymore !
# if batch_setup_render_scene: # if batch_setup_render_scene:
@ -295,7 +308,6 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator):
# bpy.ops.gp.check_render_scene('INVOKE_DEFAULT') # bpy.ops.gp.check_render_scene('INVOKE_DEFAULT')
## Note: After all these operation, a ctrl+Z might crash ## Note: After all these operation, a ctrl+Z might crash
print('\nDone.\n') print('\nDone.\n')
return {"FINISHED"} return {"FINISHED"}

View File

@ -18,7 +18,6 @@ def check_broken_modifier_target(pool=None, reports=None):
reports.append(f'Broken modifier target :{o.name} > {m.name} > {m.layer}') reports.append(f'Broken modifier target :{o.name} > {m.name} > {m.layer}')
# else: # else:
# print(f'Modifier target :{o.name} > {m.name} > ok') # print(f'Modifier target :{o.name} > {m.name} > ok')
return reports return reports
@ -72,10 +71,19 @@ def check_file_output_numbering(reports=None):
if not prenum.match(fo.base_path.split('/')[-1]): if not prenum.match(fo.base_path.split('/')[-1]):
reports.append(f'No object numbering : node {fo.name}') reports.append(f'No object numbering : node {fo.name}')
pct = 0 pct = 0
slots = fo.layer_slots if fo.format.file_format == 'OPEN_EXR_MULTILAYER' else fo.file_slots if fo.format.file_format == 'OPEN_EXR_MULTILAYER':
for fs in slots: ## multilayer use layer_slots > slot.name
if not prenum.match(fs.path.split('/')[0]): slots = fo.layer_slots
pct += 1 for fs in slots:
if not prenum.match(fs.name.split('/')[0]):
pct += 1
else:
## classic use file_slots > path
slots = fo.file_slots
for fs in slots:
if not prenum.match(fs.path.split('/')[0]):
pct += 1
if pct: if pct:
reports.append(f'{pct}/{len(slots)} slots not numbered: node {fo.name}') reports.append(f'{pct}/{len(slots)} slots not numbered: node {fo.name}')
@ -96,11 +104,11 @@ class GPEXP_OT_check_render_scene(bpy.types.Operator):
def invoke(self, context, event): def invoke(self, context, event):
return self.execute(context) 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):
layout = self.layout # layout = self.layout
# layout.prop(self, 'clear_unused_view_layers') # # layout.prop(self, 'clear_unused_view_layers')
def execute(self, context): def execute(self, context):
reports = [] reports = []

View File

@ -1,4 +1,5 @@
import bpy import bpy
from bpy.props import BoolProperty, StringProperty
from . import fn from . import fn
class GPEXP_OT_clean_compo_tree(bpy.types.Operator): class GPEXP_OT_clean_compo_tree(bpy.types.Operator):
@ -8,31 +9,35 @@ class GPEXP_OT_clean_compo_tree(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
# Internal prop (use when launching from python) # Internal prop (use when launching from python)
use_render_scene : bpy.props.BoolProperty(name="Use Render Scene", scene : StringProperty(name="Rener Scene",
description="Force the clean on scene named Render, abort if not exists (not exposed)", description="Scene to clear node from",
default=False, options={'SKIP_SAVE'}) default='', options={'SKIP_SAVE'})
clear_unused_view_layers : bpy.props.BoolProperty(name="Clear unused view layers", node_scene : StringProperty(name="Compositing Scene",
description="Scene containing nodes, using current scene when calling operator from UI to clean node",
default='', options={'SKIP_SAVE'})
clear_unused_view_layers : BoolProperty(name="Clear unused view layers",
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)
arrange_rl_nodes : bpy.props.BoolProperty(name="Arrange Render Node In Frames", arrange_rl_nodes : BoolProperty(name="Arrange Render Node In Frames",
description="Re-arrange Render Layer Nodes Y positions within each existing frames" , description="Re-arrange Render Layer Nodes Y positions within each existing frames" ,
default=True) default=True)
arrange_frames : bpy.props.BoolProperty(name="Arrange Frames", arrange_frames : BoolProperty(name="Arrange Frames",
description="Re-arrange all frames Y positions" , description="Re-arrange all frames Y positions" ,
default=True) default=True)
reorder_inputs : bpy.props.BoolProperty(name="Reorder I/O Sockets", reorder_inputs : BoolProperty(name="Reorder I/O Sockets",
description="Reorder inputs/outputs of all 'NG_' nodegroup and their connected file output", description="Reorder inputs/outputs of all 'NG_' nodegroup and their connected file output",
default=True) default=True)
clear_isolated_node_in_groups : bpy.props.BoolProperty(name="Clear Isolated Node In Groups", clear_isolated_node_in_groups : BoolProperty(name="Clear Isolated Node In Groups",
description="Clean content of 'NG_' nodegroup bpy deleting isolated nodes)", description="Clean content of 'NG_' nodegroup bpy deleting isolated nodes)",
default=True) default=True)
fo_clear_disconnected : bpy.props.BoolProperty(name="Remove Disconnected Export Inputs", fo_clear_disconnected : BoolProperty(name="Remove Disconnected Export Inputs",
description="Clear any disconnected intput of every 'file output' node", description="Clear any disconnected intput of every 'file output' node",
default=False) default=False)
@ -62,29 +67,39 @@ class GPEXP_OT_clean_compo_tree(bpy.types.Operator):
# box.prop(self, 'fo_clear_disconnected') # box.prop(self, 'fo_clear_disconnected')
def execute(self, context): def execute(self, context):
if self.use_render_scene:
render = bpy.data.scenes.get('Render') if self.scene:
if not render: scn = bpy.data.scenes.get(self.scene)
print('SKIP clean_compo_tree, No "Render" scene !') if not scn:
print(f'SKIP clean_compo_tree, No "{self.scene}" render scene found!')
return {"CANCELLED"} return {"CANCELLED"}
else: else:
render = context.scene scn = fn.get_render_scene(create=False)
if self.node_scene:
node_scene = bpy.data.scenes.get(self.node_scene)
if not scn:
print(f'SKIP clean_compo_tree, No "{self.node_scene}" compo scene found!')
return {"CANCELLED"}
else:
node_scene = context.scene
nodes = render.node_tree.nodes nodes = node_scene.node_tree.nodes
for n in nodes: for n in nodes:
n.update() n.update()
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']
for rl in reversed(render.view_layers): for rl in reversed(scn.view_layers):
if rl.name in used_rlayer_names or rl.name == 'View Layer': if rl.name in used_rlayer_names or rl.name == 'View Layer':
continue continue
render.view_layers.remove(rl) scn.view_layers.remove(rl)
if self.arrange_rl_nodes: if self.arrange_rl_nodes:
fn.rearrange_rlayers_in_frames(render.node_tree) fn.rearrange_rlayers_in_frames(node_scene.node_tree)
if self.arrange_frames: if self.arrange_frames:
fn.rearrange_frames(render.node_tree) fn.rearrange_frames(node_scene.node_tree)
if self.reorder_inputs: if self.reorder_inputs:
for n in nodes: for n in nodes:

View File

@ -1,5 +1,6 @@
import bpy import bpy
from . import fn
from .constant import RD_SCENE_NAME
class GPEXP_OT_clear_render_tree(bpy.types.Operator): class GPEXP_OT_clear_render_tree(bpy.types.Operator):
bl_idname = "gp.clear_render_tree" bl_idname = "gp.clear_render_tree"
@ -15,43 +16,47 @@ class GPEXP_OT_clear_render_tree(bpy.types.Operator):
def execute(self, context): def execute(self, context):
render = bpy.data.scenes.get('Render') render = fn.get_render_scene(create=False)
if not render: if not render:
print('SKIP, no Render scene') print(f'SKIP, no {RD_SCENE_NAME} scene')
return {"CANCELLED"} return {"CANCELLED"}
# clear all nodes in frames scn = context.scene
if render.use_nodes:
for i in range(len(render.node_tree.nodes))[::-1]:
# skip frames to delete later # Clear all nodes in frames
if render.node_tree.nodes[i].type == 'FRAME': if scn.use_nodes:
for i in range(len(scn.node_tree.nodes))[::-1]:
# Skip frames to delete later
if scn.node_tree.nodes[i].type == 'FRAME':
continue continue
# skip unparented nodes # Skip unparented nodes
if not render.node_tree.nodes[i].parent: if not scn.node_tree.nodes[i].parent:
continue continue
render.node_tree.nodes.remove(render.node_tree.nodes[i]) scn.node_tree.nodes.remove(scn.node_tree.nodes[i])
# delete all framesWorki # Delete all frames
if render.use_nodes: if scn.use_nodes:
for i in range(len(render.node_tree.nodes))[::-1]: for i in range(len(scn.node_tree.nodes))[::-1]:
if render.node_tree.nodes[i].type == 'FRAME': if scn.node_tree.nodes[i].type == 'FRAME':
render.node_tree.nodes.remove(render.node_tree.nodes[i]) scn.node_tree.nodes.remove(scn.node_tree.nodes[i])
# clear all view_layers # Clear all "NG_" nodegroups
for vl in reversed(render.view_layers):
if ' / ' in vl.name:
render.view_layers.remove(vl)
# clear all "NG_" nodegroups
for ng in reversed(bpy.data.node_groups): for ng in reversed(bpy.data.node_groups):
if ng.name.startswith('NG_'): if ng.name.startswith('NG_'):
ng.use_fake_user = False ng.use_fake_user = False
bpy.data.node_groups.remove(ng) bpy.data.node_groups.remove(ng)
if self.mode == 'COMPLETE': if self.mode == 'COMPLETE':
bpy.data.scenes.remove(render) bpy.data.scenes.remove(render)
return {"FINISHED"}
# Clear all view_layers
for vl in reversed(render.view_layers):
if ' / ' in vl.name:
render.view_layers.remove(vl)
return {"FINISHED"} return {"FINISHED"}

View File

@ -66,16 +66,17 @@ class GPEXP_OT_delete_render_layer(bpy.types.Operator):
def poll(cls, context): def poll(cls, context):
return True return True
# mode : bpy.props.StringProperty(default='NORMAL', options={'SKIP_SAVE'}) def execute(self, context):
## Compositing scene where nodes lives
compo_scene = context.scene
def execute(self, context): ## render scene where viewlayer lives
rd_scn = fn.get_render_scene(create=False)
rd_scn = bpy.data.scenes.get('Render')
if not rd_scn: if not rd_scn:
self.report({'ERROR'}, 'Viewlayers needs to be generated first!') self.report({'ERROR'}, 'No render scene found')
return {'CANCELLED'} return {'CANCELLED'}
nodes = rd_scn.node_tree.nodes nodes = compo_scene.node_tree.nodes
# list layers and viewlayers # list layers and viewlayers
# vls = [rd_scn.view_layers.get(l.viewlayer_render) for l in layers # vls = [rd_scn.view_layers.get(l.viewlayer_render) for l in layers
# if l.viewlayer_render and l.viewlayer_render != act.viewlayer_render and rd_scn.view_layers.get(l.viewlayer_render)] # if l.viewlayer_render and l.viewlayer_render != act.viewlayer_render and rd_scn.view_layers.get(l.viewlayer_render)]
@ -112,25 +113,44 @@ class GPEXP_OT_delete_render_layer(bpy.types.Operator):
inside_nodes = [] inside_nodes = []
ngroup = grp.node_tree ngroup = grp.node_tree
for i in range(len(grp.inputs))[::-1]:
if grp.inputs[i].name == sockin.name: if bpy.app.version < (4,0,0):
ngroup.inputs.remove(ngroup.inputs[i]) for i in range(len(grp.inputs))[::-1]:
if grp.inputs[i].name == sockin.name:
ngroup.inputs.remove(ngroup.inputs[i])
gp_in_socket = ngroup.nodes['Group Input'].outputs[i] gp_in_socket = ngroup.nodes['Group Input'].outputs[i]
for lnk in gp_in_socket.links: for lnk in gp_in_socket.links:
inside_nodes += fn.all_connected_forward(lnk.to_node) inside_nodes += fn.all_connected_forward(lnk.to_node)
list(set(inside_nodes)) list(set(inside_nodes))
break break
for i in range(len(grp.outputs))[::-1]: for i in range(len(grp.outputs))[::-1]:
if grp.outputs[i].name == sockout.name: if grp.outputs[i].name == sockout.name:
ngroup.outputs.remove(ngroup.outputs[i]) ngroup.outputs.remove(ngroup.outputs[i])
break break
else:
g_inputs = [s for s in ngroup.interface.items_tree if s.in_out == 'INPUT']
g_outputs = [s for s in ngroup.interface.items_tree if s.in_out == 'OUTPUT']
for i in range(len(grp.inputs))[::-1]:
if grp.inputs[i].name == sockin.name:
ngroup.interface.remove(g_inputs[i])
gp_in_socket = ngroup.nodes['Group Input'].outputs[i]
for lnk in gp_in_socket.links:
inside_nodes += fn.all_connected_forward(lnk.to_node)
list(set(inside_nodes))
break
for i in range(len(grp.outputs))[::-1]:
if grp.outputs[i].name == sockout.name:
ngroup.interface.remove(g_outputs[i])
break
for sub_n in reversed(inside_nodes): for sub_n in reversed(inside_nodes):
ngroup.nodes.remove(sub_n) ngroup.nodes.remove(sub_n)
# remove render_layer node # Remove render_layer node
rd_scn.node_tree.nodes.remove(n) nodes.remove(n)
return {"FINISHED"} return {"FINISHED"}

View File

@ -228,12 +228,12 @@ class GPEXP_OT_reset_render_settings(bpy.types.Operator):
use_native_aa = False use_native_aa = False
break break
if scn.use_aa != use_native_aa: if scn.gp_render_settings.use_aa != use_native_aa:
print(f'Scene {scn.name}: changed scene AA settings, native AA = {use_native_aa}') print(f'Scene {scn.name}: changed scene AA settings, native AA = {use_native_aa}')
fn.scene_aa(scene=scn, toggle=use_native_aa) fn.scene_aa(scene=scn, toggle=use_native_aa)
# set propertie on scn to reflect changes (without triggering update) # set propertie on scn to reflect changes (without triggering update)
scn['use_aa'] = use_native_aa scn.gp_render_settings['use_aa'] = use_native_aa
return {"FINISHED"} return {"FINISHED"}

View File

@ -9,6 +9,7 @@ from . import gen_vlayer
# --> need to delete/mute AA internal node # --> need to delete/mute AA internal node
def merge_layers(rlayers, obname=None, active=None, disconnect=True, color=None): def merge_layers(rlayers, obname=None, active=None, disconnect=True, color=None):
'''merge render layers, node_tree is found using first render layer (with id_data)'''
print(f'Merging {len(rlayers)} layers') print(f'Merging {len(rlayers)} layers')
print('->', [r.layer for r in rlayers]) print('->', [r.layer for r in rlayers])
@ -111,7 +112,7 @@ def merge_layers(rlayers, obname=None, active=None, disconnect=True, color=None)
# fn.clean_nodegroup_inputs(dg) # fn.clean_nodegroup_inputs(dg)
# # fn.clear_nodegroup_content_if_disconnected(dg.node_tree) # # fn.clear_nodegroup_content_if_disconnected(dg.node_tree)
bpy.context.scene.use_aa = False # trigger fn.scene_aa(toggle=False) bpy.context.scene.gp_render_settings.use_aa = False # trigger fn.scene_aa(toggle=False)
return ng, out return ng, out
@ -420,20 +421,20 @@ class GPEXP_OT_merge_selected_dopesheet_layers(bpy.types.Operator):
self.report({'ERROR'}, f'Should select multiple layers for merging') self.report({'ERROR'}, f'Should select multiple layers for merging')
return {"CANCELLED"} return {"CANCELLED"}
render = bpy.data.scenes.get('Render') rd_scene = fn.get_render_scene(create=False)
if render: if rd_scene:
nodes = render.node_tree.nodes nodes = rd_scene.node_tree.nodes
clean_ob_name = bpy.path.clean_name(ob.name) clean_ob_name = bpy.path.clean_name(ob.name)
rlayers = [] rlayers = []
for l in layers: for l in layers:
idname = f'{clean_ob_name} / {l.info}' idname = f'{clean_ob_name} / {l.info}'
rlayer = rl = None rlayer = rl = None
# check the render layer that have a parent frame # check the rd_scene layer that have a parent frame
if not render: if not rd_scene:
_vl, rl = gen_vlayer.get_set_viewlayer_from_gp(ob, l) _vl, rl = gen_vlayer.get_set_viewlayer_from_gp(ob, l)
render = bpy.data.scenes.get('Render') rd_scene = fn.get_render_scene(create=False)
nodes = render.node_tree.nodes nodes = rd_scene.node_tree.nodes
if not rl: if not rl:
rlayer = [n for n in nodes if n.type == 'R_LAYERS' and n.layer == idname and n.parent] rlayer = [n for n in nodes if n.type == 'R_LAYERS' and n.layer == idname and n.parent]
@ -467,16 +468,8 @@ class GPEXP_OT_merge_selected_viewlayer_nodes(bpy.types.Operator):
disconnect : bpy.props.BoolProperty(default=True, options={'SKIP_SAVE'}) disconnect : bpy.props.BoolProperty(default=True, options={'SKIP_SAVE'})
def execute(self, context): def execute(self, context):
if context.scene.name == 'Scene':
render = bpy.data.scenes.get('Render')
else:
render = context.scene
if not render: nodes = context.scene.node_tree.nodes
self.report({'ERROR'}, 'No render scene')
return {"CANCELLED"}
nodes = render.node_tree.nodes
selection = [n for n in nodes if n.select and n.type == 'R_LAYERS'] selection = [n for n in nodes if n.select and n.type == 'R_LAYERS']
if not nodes.active in selection: if not nodes.active in selection:
@ -490,8 +483,8 @@ class GPEXP_OT_merge_selected_viewlayer_nodes(bpy.types.Operator):
color = None color = None
if nodes.active.use_custom_color and nodes.active.color: if nodes.active.use_custom_color and nodes.active.color:
color = nodes.active.color color = nodes.active.color
merge_layers(selection, active=nodes.active, disconnect=self.disconnect, color=color)
merge_layers(selection, active=nodes.active, disconnect=self.disconnect, color=color)
return {"FINISHED"} return {"FINISHED"}
classes=( classes=(

View File

@ -1,5 +1,6 @@
import bpy import bpy
from .import fn from .import fn
from .constant import RD_SCENE_NAME
class GPEXP_OT_render_scene_switch(bpy.types.Operator): class GPEXP_OT_render_scene_switch(bpy.types.Operator):
bl_idname = "gp.render_scene_switch" bl_idname = "gp.render_scene_switch"
@ -11,7 +12,7 @@ class GPEXP_OT_render_scene_switch(bpy.types.Operator):
def poll(cls, context): def poll(cls, context):
return True return True
# mode : bpy.props.StringProperty(default='NORMAL', options={'SKIP_SAVE'}) scene : bpy.props.StringProperty(default='', options={'SKIP_SAVE'})
def execute(self, context): def execute(self, context):
scenes = bpy.data.scenes scenes = bpy.data.scenes
@ -19,7 +20,17 @@ class GPEXP_OT_render_scene_switch(bpy.types.Operator):
self.report({'ERROR'},'No other scene to go to') self.report({'ERROR'},'No other scene to go to')
return {"CANCELLED"} return {"CANCELLED"}
if context.scene.name == 'Render': ## Case where a scene name is passed
if self.scene:
scn = bpy.data.scenes.get(self.scene)
if not scn:
self.report({'ERROR'},f'No scene "{self.scene}"')
return {"CANCELLED"}
self.report({'INFO'},f'Switched to scene "{scn.name}"')
bpy.context.window.scene = scn
return {"FINISHED"}
if context.scene.name == RD_SCENE_NAME:
scn = scenes.get('Scene') scn = scenes.get('Scene')
if not scn: # get the next available scene if not scn: # get the next available scene
self.report({'WARNING'},'No scene named "Scene"') self.report({'WARNING'},'No scene named "Scene"')
@ -27,12 +38,11 @@ class GPEXP_OT_render_scene_switch(bpy.types.Operator):
scn = scenes[(slist.index(bpy.context.scene.name) + 1) % len(scenes)] scn = scenes[(slist.index(bpy.context.scene.name) + 1) % len(scenes)]
else: else:
scn = scenes.get('Render') scn = fn.get_render_scene(create=False)
if not scn: if not scn:
self.report({'ERROR'},'No "Render" scene yet') self.report({'ERROR'},f'No "{RD_SCENE_NAME}" scene yet')
return {"CANCELLED"} return {"CANCELLED"}
self.report({'INFO'},f'Switched to scene "{scn.name}"') self.report({'INFO'},f'Switched to scene "{scn.name}"')
bpy.context.window.scene = scn bpy.context.window.scene = scn
return {"FINISHED"} return {"FINISHED"}

View File

@ -562,6 +562,11 @@ class GPEXP_OT_select_layer_in_comp(bpy.types.Operator):
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GPENCIL' return context.object and context.object.type == 'GPENCIL'
def invoke(self, context, event):
self.additive = event.shift
return self.execute(context)
def execute(self, context): def execute(self, context):
gp = context.object.data gp = context.object.data
act = gp.layers.active act = gp.layers.active
@ -574,12 +579,19 @@ class GPEXP_OT_select_layer_in_comp(bpy.types.Operator):
self.report({'ERROR'}, 'No compo node-tree in active scene') self.report({'ERROR'}, 'No compo node-tree in active scene')
return {"CANCELLED"} return {"CANCELLED"}
nodes = context.scene.node_tree.nodes scn = context.scene
node_scene = fn.get_compo_scene(create=False) or scn
nodes = node_scene.node_tree.nodes
rl_nodes = [n for n in nodes if n.type == 'R_LAYERS'] rl_nodes = [n for n in nodes if n.type == 'R_LAYERS']
if not rl_nodes: if not rl_nodes:
self.report({'ERROR'}, 'No render layers nodes in active scene') self.report({'ERROR'}, 'No render layers nodes in active scene')
return {"CANCELLED"} return {"CANCELLED"}
# Deselect all nodes if shift is not pressed
if not self.additive:
for n in nodes:
n.select = False
used_vl = [n.layer for n in rl_nodes] used_vl = [n.layer for n in rl_nodes]
selected = [] selected = []
infos = [] infos = []
@ -619,6 +631,9 @@ class GPEXP_OT_select_layer_in_comp(bpy.types.Operator):
infos = infos + [f'-- Selected {len(selected)} nodes --'] + selected infos = infos + [f'-- Selected {len(selected)} nodes --'] + selected
fn.show_message_box(_message=infos, _title="Selected viewlayer in compo", _icon='INFO') fn.show_message_box(_message=infos, _title="Selected viewlayer in compo", _icon='INFO')
# Change viewed scene if not in current scene
if selected and scn != node_scene:
context.window.scene = node_scene
return {"FINISHED"} return {"FINISHED"}
classes=( classes=(

View File

@ -11,6 +11,7 @@ bl_info = {
"category": "Object" } "category": "Object" }
from . import properties
from . import OP_add_layer from . import OP_add_layer
from . import OP_merge_layers from . import OP_merge_layers
from . import OP_clear from . import OP_clear
@ -29,9 +30,10 @@ from . import OP_setup_layers
from . import OP_auto_build from . import OP_auto_build
from . import ui from . import ui
from .fn import scene_aa # from .fn import scene_aa
bl_modules = ( bl_modules = (
properties,
prefs, prefs,
OP_add_layer, OP_add_layer,
OP_clear, OP_clear,
@ -51,36 +53,18 @@ bl_modules = (
ui, ui,
) )
def update_scene_aa(context, scene):
scene_aa(toggle=bpy.context.scene.use_aa)
import bpy import bpy
def register(): def register():
# if bpy.app.background:
# return
for mod in bl_modules: for mod in bl_modules:
mod.register() mod.register()
# bpy.types.Scene.pgroup_name = bpy.props.PointerProperty(type = PROJ_PGT_settings)
bpy.types.Scene.use_aa = bpy.props.BoolProperty(
name='Use Native Anti Aliasing',
default=True,
description='\
Should be Off only if tree contains a merge_NG or alpha-over-combined renderlayers.\n\
Auto-set to Off when using node merge button\n\
Toggle: AA settings of and muting AA nested-nodegroup',
update=update_scene_aa)
def unregister(): def unregister():
# if bpy.app.background:
# return
for mod in reversed(bl_modules): for mod in reversed(bl_modules):
mod.unregister() mod.unregister()
del bpy.types.Scene.use_aa
if __name__ == "__main__": if __name__ == "__main__":
register() register()

1
constant.py Normal file
View File

@ -0,0 +1 @@
RD_SCENE_NAME = 'RenderGP'

91
fn.py
View File

@ -9,6 +9,7 @@ from collections import defaultdict
from time import time from time import time
import json import json
from . constant import RD_SCENE_NAME
### -- rules ### -- rules
@ -208,10 +209,20 @@ def scene_aa(scene=None, toggle=True):
if not scene: if not scene:
scene=bpy.context.scene scene=bpy.context.scene
# enable/disable native anti-alias on active scene # Enable/disable native anti-alias on active scene
set_scene_aa_settings(scene=scene, aa=toggle) set_scene_aa_settings(scene=scene, aa=toggle)
# mute/unmute AA nodegroups
for n in scene.node_tree.nodes: ## Set AA on scene where object and viewlayers exists
local_nodes = scene.node_tree.nodes
if (group_node := next((n for n in local_nodes if n.name.startswith('NG_')), None)):
# Get a viewlayer connected to a NG_ and check which scene is referred
if (rlayer := next((i.links[0].from_node for i in group_node.inputs if i.links and i.links[0].from_node.type == 'R_LAYERS'), None)):
if rlayer.scene and rlayer.scene != scene:
print(f'Set AA to {toggle} on scene "{rlayer.scene.name}"')
set_scene_aa_settings(scene=rlayer.scene, aa=toggle)
## Mute/Unmute AA nodegroups
for n in local_nodes:
if n.type == 'GROUP' and n.name.startswith('NG_'): if n.type == 'GROUP' and n.name.startswith('NG_'):
# n.mute = False # mute whole nodegroup ? # n.mute = False # mute whole nodegroup ?
for gn in n.node_tree.nodes: for gn in n.node_tree.nodes:
@ -250,17 +261,58 @@ def new_scene_from(name, src_scn=None, regen=True, crop=True, link_cam=True, lin
scn.use_nodes = True scn.use_nodes = True
return scn return scn
def get_render_scene(): def get_compo_scene(scene_name=None, create=True):
'''Get / Create a scene named Render''' '''Get / Create a dedicated compositing scene to link GP
render_scn = bpy.data.scenes.get('Render') use passed scene name, if no name is passed fall back to compo_scene propertie
if render_scn: return None if field is empty'''
return render_scn
scene_name = scene_name or bpy.context.scene.gp_render_settings.node_scene
if not scene_name:
# return None or render scene
return
scn = bpy.data.scenes.get(scene_name)
if scn:
return scn
if not create:
return
## -- Create render scene ## -- Create render scene
current = bpy.context.scene current = bpy.context.scene
## With data ## With data
render_scn = bpy.data.scenes.new('Render') scn = bpy.data.scenes.new(scene_name)
## copy original settings over to new scene
for attr in ['frame_start', 'frame_end', 'frame_current']: # , 'camera', 'world'
setattr(scn, attr, getattr(current, attr))
copy_settings(current.render, scn.render)
scn.use_nodes = True
## Clear node tree
scn.node_tree.nodes.clear()
set_settings(scn)
scn.gp_render_settings['use_aa'] = True
return scn
def get_render_scene(scene_name=None, create=True):
'''Get / Create a dedicated render scene to link GP'''
scene_name = scene_name or RD_SCENE_NAME
render_scn = bpy.data.scenes.get(scene_name)
if render_scn:
return render_scn
if not create:
return
## -- Create render scene
current = bpy.context.scene
## With data
render_scn = bpy.data.scenes.new(scene_name)
## copy original settings over to new scene ## copy original settings over to new scene
# copy_settings(current, render_scn) # BAD # copy_settings(current, render_scn) # BAD
@ -281,15 +333,16 @@ def get_render_scene():
# render_scn.node_tree.nodes.remove(n) # render_scn.node_tree.nodes.remove(n)
set_settings(render_scn) set_settings(render_scn)
render_scn['use_aa'] = True render_scn.gp_render_settings['use_aa'] = True
return render_scn return render_scn
def get_view_layer(name, scene=None): def get_view_layer(name, scene=None):
'''get viewlayer name '''get viewlayer name
return existing/created viewlayer return existing/created viewlayer
''' '''
if not scene:
scene = get_render_scene() scene = scene or get_render_scene()
### pass double letter prefix as suffix ### pass double letter prefix as suffix
## pass_name = re.sub(r'^([A-Z]{2})(_)(.*)', r'\3\2\1', 'name') ## pass_name = re.sub(r'^([A-Z]{2})(_)(.*)', r'\3\2\1', 'name')
## pass_name = f'{name}_{passe}' ## pass_name = f'{name}_{passe}'
@ -336,6 +389,9 @@ def get_frame_transform(f, node_tree=None):
# return real_loc(f), f.dimensions # return real_loc(f), f.dimensions
childs = [n for n in node_tree.nodes if n.parent == f] childs = [n for n in node_tree.nodes if n.parent == f]
if not childs:
return f.location, f.dimensions
# real_locs = [f.location + n.location for n in childs] # real_locs = [f.location + n.location for n in childs]
xs = [n.location.x for n in childs] + [n.location.x + n.dimensions.x for n in childs] xs = [n.location.x for n in childs] + [n.location.x + n.dimensions.x for n in childs]
@ -470,7 +526,7 @@ def remove_nodes_by_viewlayer(viewlayer_list, scene=None):
# Remove render_layer node # Remove render_layer node
scene.node_tree.nodes.remove(n) scene.node_tree.nodes.remove(n)
def merge_gplayer_viewlayers(ob=None, act=None, layers=None): def merge_gplayer_viewlayers(ob=None, act=None, layers=None, scene=None):
'''ob is not needed if active and layers are passed''' '''ob is not needed if active and layers are passed'''
if ob is None: if ob is None:
ob = bpy.context.object ob = bpy.context.object
@ -482,9 +538,8 @@ def merge_gplayer_viewlayers(ob=None, act=None, layers=None):
if act is None: if act is None:
return ({'ERROR'}, 'Active layer not found. Should be active layer on active object!') return ({'ERROR'}, 'Active layer not found. Should be active layer on active object!')
rd_scn = bpy.data.scenes.get('Render') rd_scn = scene or get_render_scene(create=False) # bpy.context.scene
if not rd_scn: node_scene = get_compo_scene(create=False) or rd_scn
return ({'ERROR'}, 'Viewlayers needs to be generated first!')
if not act.viewlayer_render: if not act.viewlayer_render:
return ({'ERROR'}, f'Active layer {act.info} has no viewlayer assigned') return ({'ERROR'}, f'Active layer {act.info} has no viewlayer assigned')
@ -495,8 +550,10 @@ def merge_gplayer_viewlayers(ob=None, act=None, layers=None):
# Remove duplication # Remove duplication
vls = list(set(vls)) vls = list(set(vls))
# Remove viewlayer related nodes # Remove viewlayer related nodes
remove_nodes_by_viewlayer(vls, rd_scn) # FIXME it's possible nodes are nodes searched in the right scene if launched from RenderGP
remove_nodes_by_viewlayer(vls, node_scene) # send compositing scene
# Assign view layer from active to selected # Assign view layer from active to selected
for l in layers: for l in layers:

View File

@ -2,19 +2,30 @@ import bpy
from mathutils import Vector from mathutils import Vector
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):
'''Create a render layer node
def add_rlayer(layer_name, scene=None, location=None, color=None, node_name=None, width=400): args:
'''create a render layer node layer_name (str): Name of the viewlayer
if node_name is not specified, use passed layer name scene (Scene): Scene holding the viewlayer
node_scene (Scene): scene where to add render layer
if None: fallback scene pointed by compo scene property
if compo scene property is empty: fallback to render scene.
location (Vector2): Location of the node
color (tuple): Color of the node
node_name (str): if not specified, use layer_name
width (int): width of the node
''' '''
if not node_name: if not node_name:
node_name = layer_name # 'RL_' + node_name = layer_name # 'RL_' +
if not scene: if not node_scene:
scene=bpy.context.scene node_scene=fn.get_compo_scene(create=False)
if not node_scene:
node_scene=fn.get_render_scene(create=False)
nodes = scene.node_tree.nodes nodes = node_scene.node_tree.nodes
comp = nodes.get(node_name) comp = nodes.get(node_name)
if comp: if comp:
@ -40,13 +51,11 @@ def add_rlayer(layer_name, scene=None, location=None, color=None, node_name=None
return comp return comp
def connect_render_layer(rlayer, ng=None, out=None, frame=None): def connect_render_layer(rlayer, ng=None, out=None, frame=None):
if bpy.context.scene.name == 'Scene':
scene = fn.get_render_scene()
else:
scene = bpy.context.scene
nodes = scene.node_tree.nodes node_tree = rlayer.id_data # get node_tree from rlayer
links = scene.node_tree.links
nodes = node_tree.nodes
links = node_tree.links
vl_name = rlayer.layer vl_name = rlayer.layer
if not vl_name or vl_name == 'View Layer': if not vl_name or vl_name == 'View Layer':
@ -88,7 +97,7 @@ def connect_render_layer(rlayer, ng=None, out=None, frame=None):
print(f'create nodegroup {ng_name}') print(f'create nodegroup {ng_name}')
ngroup = bpy.data.node_groups.new(ng_name, 'CompositorNodeTree') ngroup = bpy.data.node_groups.new(ng_name, 'CompositorNodeTree')
ng = fn.create_node('CompositorNodeGroup', tree=scene.node_tree, location=(fn.real_loc(rlayer)[0] + 600, fn.real_loc(rlayer)[1]), width=400) # (rlayer.location[0] + 600, rlayer.location[1]) ng = fn.create_node('CompositorNodeGroup', tree=node_tree, location=(fn.real_loc(rlayer)[0] + 600, fn.real_loc(rlayer)[1]), width=400) # (rlayer.location[0] + 600, rlayer.location[1])
if frame: if frame:
ng.parent= frame ng.parent= frame
ng.node_tree = ngroup ng.node_tree = ngroup
@ -163,8 +172,17 @@ def connect_render_layer(rlayer, ng=None, out=None, frame=None):
ngroup.links.new(ng_in.outputs[vl_name], aa.inputs[0]) # node_tree ngroup.links.new(ng_in.outputs[vl_name], aa.inputs[0]) # node_tree
ngroup.links.new(aa.outputs[0], ng_out.inputs[vl_name]) # node_tree ngroup.links.new(aa.outputs[0], ng_out.inputs[vl_name]) # node_tree
aa.mute = scene.use_aa # mute if native AA is used
## Get use_aa prop from render scene ?
# rd_scn = fn.get_render_scene(create=False)
# if rd_scn:
# aa.mute = rd_scn.gp_render_settings.use_aa # mute if native AA is used
scene = next((s for s in bpy.data.scenes if s.node_tree == node_tree), None)
if scene:
print(f'set AA from scene {scene.name}')
aa.mute = scene.gp_render_settings.use_aa # mute if native AA is used
else:
print('/!\ Scene could not be found to define if internal AA should be muted')
fn.reorganise_NG_nodegroup(ng) # decorative fn.reorganise_NG_nodegroup(ng) # decorative
@ -206,7 +224,7 @@ def connect_render_layer(rlayer, ng=None, out=None, frame=None):
out = nodes.get(out_name) out = nodes.get(out_name)
if not out: if not out:
# color = (0.2,0.3,0.5) # color = (0.2,0.3,0.5)
out = fn.create_node('CompositorNodeOutputFile', tree=scene.node_tree, location=(fn.real_loc(ng)[0]+500, fn.real_loc(ng)[1]+50), width=600) # =(ng.location[0]+600, ng.location[1]+50) out = fn.create_node('CompositorNodeOutputFile', tree=node_tree, location=(fn.real_loc(ng)[0]+500, fn.real_loc(ng)[1]+50), width=600) # =(ng.location[0]+600, ng.location[1]+50)
fn.set_file_output_format(out) fn.set_file_output_format(out)
out.name = out_name out.name = out_name
out.parent = frame out.parent = frame
@ -233,16 +251,20 @@ def connect_render_layer(rlayer, ng=None, out=None, frame=None):
return ng, out return ng, out
def get_set_viewlayer_from_gp(ob, l, scene=None): def get_set_viewlayer_from_gp(ob, l, scene=None, node_scene=None):
'''setup ouptut from passed gp obj > layer''' '''setup ouptut from passed gp obj > layer
if not scene: scene: scene to set viewlayer
if bpy.context.scene.name != 'Scene': node_scene: where to add compo node (use scene if not passed)'''
scene = bpy.context.scene
else:
scene = fn.get_render_scene()
# print('Set viewlayer Scene: ', scene.name) scene = scene or fn.get_render_scene()
node_tree = scene.node_tree node_scene = node_scene or fn.get_compo_scene() or scene
print('Viewlayer Scene: ', scene.name)
print('Compo Scene: ', node_scene.name)
## If not passed, identical to scene holding viewlayers
node_tree = node_scene.node_tree
nodes = node_tree.nodes nodes = node_tree.nodes
in_rds = scene.collection.all_objects.get(ob.name) in_rds = scene.collection.all_objects.get(ob.name)
@ -265,18 +287,26 @@ def get_set_viewlayer_from_gp(ob, l, scene=None):
if nob: if nob:
nob.select_set(True) nob.select_set(True)
# create viewlayer # Create viewlayer
vl_name = f'{ob.name} / {l.info}' vl_name = f'{ob.name} / {l.info}'
vl = fn.get_view_layer(vl_name, scene=scene) vl = fn.get_view_layer(vl_name, scene=scene)
vl_name = vl.name vl_name = vl.name
# affect layer to this vl
## To avoid potential error, transfer compo scene prop to renderscene.
scn = bpy.context.scene
if scn.gp_render_settings.node_scene and scn != scene:
if scn.gp_render_settings.node_scene != scene.gp_render_settings.node_scene:
print(f'Transfer compo scene target prop to render scene: "{scn.gp_render_settings.node_scene}"')
scene.gp_render_settings.node_scene = scn.gp_render_settings.node_scene
# Affect layer to this vl
l.viewlayer_render = vl_name l.viewlayer_render = vl_name
# check if already exists # Check if already exists
rlayer_list = [n for n in nodes if n.type == 'R_LAYERS' and n.layer == vl_name] rlayer_list = [n for n in nodes if n.type == 'R_LAYERS' and n.layer == vl_name]
# get frame object and their contents # Get frame object and their contents
# dict like : {objname : [layer_nodeA, layer_nodeB,...]} # dict model : {objname : [layer_nodeA, layer_nodeB,...]}
frame_dic = {f.label: [n for n in nodes if n.type == 'R_LAYERS' and n.parent and n.parent.name == f.name and '/' in n.layer] # n.layer != 'View Layer' frame_dic = {f.label: [n for n in nodes if n.type == 'R_LAYERS' and n.parent and n.parent.name == f.name and '/' in n.layer] # n.layer != 'View Layer'
for f in nodes if f.type == 'FRAME'} for f in nodes if f.type == 'FRAME'}
@ -323,7 +353,7 @@ def get_set_viewlayer_from_gp(ob, l, scene=None):
frame.label_size = 50 frame.label_size = 50
frame.location = (loc[0], loc[1] + 20) frame.location = (loc[0], loc[1] + 20)
cp = add_rlayer(vl_name, scene=scene, location=loc) cp = add_rlayer(vl_name, scene=scene, node_scene=node_scene, location=loc)
cp.parent = frame cp.parent = frame
# use same color as layer # use same color as layer
if fn.has_channel_color(l): if fn.has_channel_color(l):
@ -341,23 +371,24 @@ def get_set_viewlayer_from_gp(ob, l, scene=None):
print(f'\n {ob.name} -> {l.info} (connect to existing)') print(f'\n {ob.name} -> {l.info} (connect to existing)')
## object frame exists: get framing and insert ## object frame exists: get framing and insert
cp = add_rlayer(vl_name, scene=scene, location=(0,0)) cp = add_rlayer(vl_name, scene=scene, node_scene=node_scene, location=(0,0))
if cp.layer != vl_name: if cp.layer != vl_name:
print(f'problem with {cp}: {cp.layer} != {vl_name}') print(f'problem with {cp}: {cp.layer} != {vl_name}')
return return
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 = l.channel_color
frame = [f for f in nodes if f.type == 'FRAME' and f.label == ob.name][0] 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
if rl_nodes: if rl_nodes:
# get nodes order to insert # get nodes order to insert
rl_nodes.sort(key=lambda n: fn.real_loc(n).y, reverse=True) # descending rl_nodes.sort(key=lambda n: fn.real_loc(n).y, reverse=True) # descending
top_loc = fn.real_loc(rl_nodes[0]) top_loc = fn.real_loc(rl_nodes[0])
else: else:
top_loc = fn.get_frame_transform(frame[1], node_tree) - 60 print('!! gen_viewlayer: No Render layers nodes !!')
top_loc = fn.get_frame_transform(frame, node_tree) - 60
# cp.location = (top_loc[0], top_loc[1] + 100) # temp location to adjust x loc # cp.location = (top_loc[0], top_loc[1] + 100) # temp location to adjust x loc
@ -381,10 +412,9 @@ def get_set_viewlayer_from_gp(ob, l, scene=None):
# set x loc from first node in list (maybe use leftmost ?) # set x loc from first node in list (maybe use leftmost ?)
n.location = Vector((fn.real_loc(ref_node)[0], top_loc[1] - offset)) - n.parent.location n.location = Vector((fn.real_loc(ref_node)[0], top_loc[1] - offset)) - n.parent.location
offset += 180 offset += 180
n.update() n.update()
# reorder render layers nodes within frame
# reorder render layers nodes within frame
connect_render_layer(cp, frame=frame) connect_render_layer(cp, frame=frame)
# re-arrange all frames (since the offset probably overlapped) # re-arrange all frames (since the offset probably overlapped)
@ -392,7 +422,7 @@ def get_set_viewlayer_from_gp(ob, l, scene=None):
return vl, cp return vl, cp
def export_gp_objects(oblist, exclude_list=[], scene=None): def export_gp_objects(oblist, exclude_list=[], scene=None, node_scene=None):
# Skip layer containing element in exclude list # Skip layer containing element in exclude list
if not isinstance(oblist, list): if not isinstance(oblist, list):
oblist = [oblist] oblist = [oblist]
@ -410,4 +440,4 @@ def export_gp_objects(oblist, exclude_list=[], scene=None):
l.viewlayer_render = fn.get_view_layer('exclude', scene=scene).name # assign "exclude" l.viewlayer_render = fn.get_view_layer('exclude', scene=scene).name # assign "exclude"
continue continue
_vl, _cp = get_set_viewlayer_from_gp(ob, l, scene=scene) # scene=fn.get_render_scene()) _vl, _cp = get_set_viewlayer_from_gp(ob, l, scene=scene, node_scene=node_scene) # scene=fn.get_render_scene())

44
properties.py Normal file
View File

@ -0,0 +1,44 @@
import bpy
from .fn import scene_aa
def update_scene_aa(context, scene):
scene_aa(toggle=bpy.context.scene.gp_render_settings.use_aa)
class GPRENDER_PGT_settings(bpy.types.PropertyGroup) :
show_scene_setup :bpy.props.BoolProperty(
name='Show scene setup',
default=False,
description='Show scene setup, options to tweak render scene and compo scene')
render_scene : bpy.props.StringProperty(
name="Render Scene", description="Link object and create viewlayers in render scene, create if necessary",
default="", maxlen=0, options={'HIDDEN'})
node_scene : bpy.props.StringProperty(
name="Compo Scene", description="Add nodes in compositing scene, if empty, add in Render scene nodeTree",
default="", maxlen=0, options={'HIDDEN'})
use_aa : bpy.props.BoolProperty(
name='Use Native Anti Aliasing',
default=True,
description='\
Should be Off only if tree contains a merge_NG or alpha-over-combined renderlayers.\n\
Auto-set to Off when using node merge button\n\
Toggle: AA settings of and muting AA nested-nodegroup',
update=update_scene_aa)
classes=(
GPRENDER_PGT_settings,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.gp_render_settings = bpy.props.PointerProperty(type=GPRENDER_PGT_settings)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
del bpy.types.Scene.gp_render_settings

50
ui.py
View File

@ -2,6 +2,7 @@ import bpy
from bpy.types import Panel from bpy.types import Panel
from pathlib import Path from pathlib import Path
from .prefs import get_addon_prefs from .prefs import get_addon_prefs
from .constant import RD_SCENE_NAME
# from .preferences import get_addon_prefs # from .preferences import get_addon_prefs
# Node view panel # Node view panel
@ -16,7 +17,17 @@ class GPEXP_PT_gp_node_ui(Panel):
prefs = get_addon_prefs() prefs = get_addon_prefs()
advanced = prefs.advanced advanced = prefs.advanced
layout = self.layout layout = self.layout
layout.operator('gp.render_scene_switch', icon='SCENE_DATA', text='Switch Scene') row = layout.row(align=True)
row.operator('gp.render_scene_switch', icon='SCENE_DATA', text='Switch Scene')
## Check if there is another scene reference current scene in it's comp renderlayers
## If so, expose a button to go in (expensive check ?)
node_scn = next((s for s in bpy.data.scenes
if s != context.scene and s.use_nodes
and next((n for n in s.node_tree.nodes if n.type == 'R_LAYERS' and n.scene == context.scene), None)
),None)
if node_scn:
row.operator('gp.render_scene_switch', icon='NODETREE', text='Node Scene').scene = node_scn.name
scn = context.scene scn = context.scene
@ -166,7 +177,7 @@ class GPEXP_PT_gp_node_ui(Panel):
col.label(text='Post-Render:') col.label(text='Post-Render:')
col.operator('gp.renumber_files_on_disk', icon='FILE', text='Renumber Files On Disk') col.operator('gp.renumber_files_on_disk', icon='FILE', text='Renumber Files On Disk')
layout.prop(context.scene, 'use_aa', text='Use Native AA Settings') layout.prop(context.scene.gp_render_settings, 'use_aa', text='Use Native AA Settings')
layout.prop(prefs, 'advanced', text='Show Advanced Options') layout.prop(prefs, 'advanced', text='Show Advanced Options')
# layout.operator('gp.add_object_to_render', icon='RENDERLAYERS', text='Layer To Render').mode = 'ALL' # layout.operator('gp.add_object_to_render', icon='RENDERLAYERS', text='Layer To Render').mode = 'ALL'
# layout.operator('gp.add_object_to_render', icon='RENDERLAYERS', text='Layer To Render').mode = 'SELECTED' # layout.operator('gp.add_object_to_render', icon='RENDERLAYERS', text='Layer To Render').mode = 'SELECTED'
@ -186,8 +197,20 @@ class GPEXP_PT_gp_dopesheet_ui(Panel):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
settings = context.scene.gp_render_settings
icon = 'DISCLOSURE_TRI_DOWN' if settings.show_scene_setup else 'DISCLOSURE_TRI_RIGHT'
col = layout.column()
col.prop(settings, 'show_scene_setup', icon=icon, text='Scenes Setup', emboss=False )
if settings.show_scene_setup:
col.prop(settings, 'render_scene', icon='SCENE_DATA', placeholder=RD_SCENE_NAME)
nodetree_placeholder = settings.render_scene or RD_SCENE_NAME
col.prop(settings, 'node_scene', icon='NODETREE', placeholder=nodetree_placeholder)
op = layout.operator('gp_export.render_auto_build')
op.node_scene = settings.node_scene
layout.operator('gp_export.render_auto_build')
if context.object: if context.object:
layout.label(text=f'Object: {context.object.name}') layout.label(text=f'Object: {context.object.name}')
if context.object.data.users > 1: if context.object.data.users > 1:
@ -207,7 +230,9 @@ class GPEXP_PT_gp_dopesheet_ui(Panel):
txt = f'{len([l for l in context.object.data.layers if l.select])} Layer(s) To Render' txt = f'{len([l for l in context.object.data.layers if l.select])} Layer(s) To Render'
else: else:
txt = 'Layer To Render' txt = 'Layer To Render'
col.operator('gp.add_layer_to_render', icon='RENDERLAYERS', text=txt)
op = col.operator('gp.add_layer_to_render', icon='RENDERLAYERS', text=txt)
op.node_scene = settings.node_scene
# merge (only accessible if multiple layers selected) # merge (only accessible if multiple layers selected)
@ -232,8 +257,14 @@ class GPEXP_PT_gp_dopesheet_ui(Panel):
col = layout.column() col = layout.column()
col.label(text='Whole Objects:') col.label(text='Whole Objects:')
txt = f'{len([o for o in context.selected_objects if o.type == "GPENCIL" and o.select_get()])} Selected Object(s) To Render' txt = f'{len([o for o in context.selected_objects if o.type == "GPENCIL" and o.select_get()])} Selected Object(s) To Render'
col.operator('gp.add_object_to_render', icon='RENDERLAYERS', text=txt).mode='SELECTED'
col.operator('gp.add_object_to_render', icon='RENDERLAYERS', text='All Visible GP To Render').mode='ALL' op = col.operator('gp.add_object_to_render', icon='RENDERLAYERS', text=txt)
op.mode='SELECTED'
op.node_scene = settings.node_scene
op = col.operator('gp.add_object_to_render', icon='RENDERLAYERS', text='All Visible GP To Render')
op.mode='ALL'
op.node_scene = settings.node_scene
layout.separator() layout.separator()
col = layout.column() col = layout.column()
@ -357,9 +388,10 @@ def manager_ui(self, context):
## On objects ## On objects
# txt = 'Selected Object To Render' # txt = 'Selected Object To Render'
if context.scene.name != 'Render': # if context.scene.name != RD_SCENE_NAME:
txt = f'{len([o for o in context.selected_objects if o.type == "GPENCIL" and o.select_get()])} Selected Object(s) To Render' txt = f'{len([o for o in context.selected_objects if o.type == "GPENCIL" and o.select_get()])} Selected Object(s) To Render'
layout.operator('gp.add_object_to_render', icon='RENDERLAYERS', text=txt).mode='SELECTED' layout.operator('gp.add_object_to_render', icon='RENDERLAYERS', text=txt).mode='SELECTED'
layout.operator('gp.add_object_to_render', icon='RENDERLAYERS', text='All GP at once').mode='ALL' layout.operator('gp.add_object_to_render', icon='RENDERLAYERS', text='All GP at once').mode='ALL'