From e930709d27c2cafcf3ec4e9812b6feb963b6c873 Mon Sep 17 00:00:00 2001 From: CGDJay Date: Sat, 22 Jun 2024 14:03:18 +0100 Subject: [PATCH 1/5] Created Pack channel operator including the ability to delete extra maps and to pack on export (Tested in blender version 4.1) --- operators/operators.py | 177 +++++++++++++++++++++++++++++++++++++---- preferences.py | 55 +++++++------ ui.py | 77 +++++++++--------- 3 files changed, 233 insertions(+), 76 deletions(-) diff --git a/operators/operators.py b/operators/operators.py index d5ec41b..5e07e6b 100644 --- a/operators/operators.py +++ b/operators/operators.py @@ -1,10 +1,15 @@ import os import time +import numpy + + + + import bpy import blf from bpy.types import SpaceView3D, Event, Context, Operator, UILayout -from bpy.props import StringProperty +from bpy.props import StringProperty, BoolProperty from ..constants import Global, Error from ..utils.render import get_rendered_objects @@ -151,6 +156,7 @@ def export(context: Context, suffix: str, path: str = None) -> str: return path def execute(self, context: Context): + report_value, report_string = \ bad_setup_check(context, active_export=True) if report_value: @@ -227,6 +233,12 @@ def execute(self, context: Context): ) context.window_manager.progress_end() + + #run the pack maps operator on export if the setting is enabled + if gd.use_pack_maps==True: + bpy.ops.grab_doc.pack_maps(remove_maps=False) + + return {'FINISHED'} @@ -588,23 +600,157 @@ def draw(self, _context: Context): # TODO: CHANNEL PACKING ################################################ +# original code sourced from : +# https://blender.stackexchange.com/questions/274712/how-to-channel-pack-texture-in-python + + +def pack_image_channels(pack_order,PackName): + dst_array = None + has_alpha = False + + # Build the packed pixel array + for pack_item in pack_order: + image = pack_item[0] + # Initialize arrays on the first iteration + if dst_array is None: + w, h = image.size + src_array = numpy.empty(w * h * 4, dtype=numpy.float32) + dst_array = numpy.ones(w * h * 4, dtype=numpy.float32) + assert image.size[:] == (w, h), "Images must be same size" + + # Fetch pixels from the source image and copy channels + image.pixels.foreach_get(src_array) + for src_chan, dst_chan in pack_item[1:]: + if dst_chan == 3: + has_alpha = True + dst_array[dst_chan::4] = src_array[src_chan::4] + + # Create image from the packed pixels + dst_image = bpy.data.images.new(PackName, w, h, alpha=has_alpha) + dst_image.pixels.foreach_set(dst_array) + + return dst_image + +def Return_Channel_Path(context ,Channel): + gd = context.scene.gd + if Channel == 'none': + return ("") + if Channel == 'normals': + return ((os.path.join(gd.export_path,gd.export_name+'_'+gd.occlusion[0].suffix+'.png'))) + if Channel == 'curvature': + return((os.path.join(gd.export_path,gd.export_name+'_'+gd.curvature[0].suffix+'.png'))) + if Channel == 'occlusion': + return((os.path.join(gd.export_path,gd.export_name+'_'+gd.occlusion[0].suffix+'.png'))) + if Channel == 'height': + return((os.path.join(gd.export_path,gd.export_name+'_'+gd.height[0].suffix+'.png'))) + if Channel == 'id': + return((os.path.join(gd.export_path,gd.export_name+'_'+gd.id[0].suffix+'.png'))) + if Channel == 'alpha': + return((os.path.join(gd.export_path,gd.export_name+'_'+gd.alpha[0].suffix+'.png'))) + if Channel == 'color': + return((os.path.join(gd.export_path,gd.export_name+'_'+gd.color[0].suffix+'.png'))) + if Channel == 'emissive': + return((os.path.join(gd.export_path,gd.export_name+'_'+gd.emissive[0].suffix+'.png'))) + if Channel == 'roughness': + return((os.path.join(gd.export_path,gd.export_name+'_'+gd.roughness[0].suffix+'.png'))) + if Channel == 'metallic': + return((os.path.join(gd.export_path,gd.export_name+'_'+gd.metallic[0].suffix+'.png'))) + return False + + +class GRABDOC_OT_pack_maps(Operator): + """Pack bake maps into single texture""" + bl_idname = "grab_doc.pack_maps" + bl_label = "Pack Maps" + bl_options = {'REGISTER', 'UNDO'} + + remove_maps : BoolProperty (name='Remove packed maps' ,default=False) + + # Poll should check if all required images are correctly present in the export path + @classmethod + def poll(cls, context: Context) -> bool: + gd = context.scene.gd + + RChannel=gd.channel_R + GChannel=gd.channel_G + BChannel=gd.channel_B + AChannel=gd.channel_A + + r=(Return_Channel_Path(context,RChannel)) + g=(Return_Channel_Path(context,GChannel)) + b=(Return_Channel_Path(context,BChannel)) + a=(Return_Channel_Path(context,AChannel)) + + if gd.channel_A == 'none': + + if os.path.exists(r) & os.path.exists(g) & os.path.exists(b)==True: + return True + else: + return False + else: + if os.path.exists(r) & os.path.exists(g) & os.path.exists(b) & os.path.exists(a)==True: + return True + else: + return False + + + def execute(self, context): -#class GRABDOC_OT_map_pack_info(OpInfo, Operator): -# """Information about Map Packing and current limitations""" -# bl_idname = "grab_doc.map_pack_info" -# bl_label = "MAP PACKING INFORMATION" -# bl_options = {'INTERNAL'}# + gd = context.scene.gd + Name = f"{gd.export_name}" + + # Will require a property for the suffix + PackName= (Name+"_"+gd.pack_name) + Path= gd.export_path + + #Loads all images into blender to avoid using a seperate package to conver to np array + ImageR= bpy.data.images.load(Return_Channel_Path(context,gd.channel_R)) + ImageG=bpy.data.images.load(Return_Channel_Path(context,gd.channel_G)) + ImageB=bpy.data.images.load(Return_Channel_Path(context,gd.channel_B)) + if gd.channel_A != 'none': + ImageA=bpy.data.images.load(Return_Channel_Path(context,gd.channel_A)) + + + if gd.channel_A == 'none': + pack_order = [ + (ImageR, (0, 0)) + ,(ImageG, (0, 1)) + ,(ImageB, (0, 2)) + ] + + else: + pack_order = [ + (ImageR, (0, 0)) + ,(ImageG, (0, 1)) + ,(ImageB, (0, 2)) + ,(ImageA, (0, 3)) + ] + + dst_image=pack_image_channels(pack_order,PackName) + + dst_image.filepath_raw = Path+"//"+PackName+'.'+gd.format + dst_image.file_format = gd.format + dst_image.save() + + + #option to delete the extra maps through the operator panel + if self.remove_maps==True: + if gd.channel_A != 'none': + os.remove(Return_Channel_Path(context,gd.channel_R)) + os.remove(Return_Channel_Path(context,gd.channel_G)) + os.remove(Return_Channel_Path(context,gd.channel_B)) + os.remove(Return_Channel_Path(context,gd.channel_A)) + else: + os.remove(Return_Channel_Path(context,gd.channel_R)) + os.remove(Return_Channel_Path(context,gd.channel_G)) + os.remove(Return_Channel_Path(context,gd.channel_B)) + + #reset value as this would be best a manual opt in on a final pack to prevent re exporting + self.remove_maps=False + + return {'FINISHED'} -# def invoke(self, context: Context, _event: Event): -# return context.window_manager.invoke_props_dialog(self, width=500) -# def draw(self, _context: Context): -# col = self.layout.column(align=True) -# for line in Global.PACK_MAPS_WARNING.split('\n')[1:][:-1]: -# col.label(text=line) -# -# def execute(self, context: Context): -# return {'FINISHED'} ################################################ @@ -626,6 +772,7 @@ def draw(self, _context: Context): GRABDOC_OT_export_current_preview, GRABDOC_OT_config_maps, #GRABDOC_OT_map_pack_info + GRABDOC_OT_pack_maps ) def register(): diff --git a/preferences.py b/preferences.py index eacc178..6f1bf5a 100644 --- a/preferences.py +++ b/preferences.py @@ -1,3 +1,4 @@ + import os import bpy @@ -390,30 +391,36 @@ def update_scale(self, context: Context): # TODO: # - Implement core functionality # - Add all properties to presets - #use_pack_maps: BoolProperty( - # name='Enable Packing on Export', - # default=False - #) - #channel_R: EnumProperty( - # items=MAP_TYPES, - # default="occlusion", - # name='R' - #) - #channel_G: EnumProperty( - # items=MAP_TYPES, - # default="roughness", - # name='G' - #) - #channel_B: EnumProperty( - # items=MAP_TYPES, - # default="metallic", - # name='B' - #) - #channel_A: EnumProperty( - # items=MAP_TYPES, - # default="alpha", - # name='A' - #) + use_pack_maps: BoolProperty( + name='Enable Packing on Export', + default=False + ) + + pack_name : StringProperty ( + name= 'Pack map name', + default= 'AORM') + + channel_R: EnumProperty( + items=MAP_TYPES, + default="occlusion", + name='R' + ) + channel_G: EnumProperty( + items=MAP_TYPES, + default="roughness", + name='G' + ) + channel_B: EnumProperty( + items=MAP_TYPES, + default="metallic", + name='B' + ) + channel_A: EnumProperty( + items=MAP_TYPES, + default="none", + name='A' + ) + ################################## diff --git a/ui.py b/ui.py index dd5fc6f..095782f 100644 --- a/ui.py +++ b/ui.py @@ -277,41 +277,42 @@ def draw(self, context: Context): baker.draw(context, layout) -#class GRABDOC_PT_pack_maps(PanelInfo, Panel): -# bl_label = 'Pack Maps' -# bl_parent_id = "GRABDOC_PT_grabdoc" - -# @classmethod -# def poll(cls, context: Context) -> bool: -# return proper_scene_setup() and not context.scene.gd.preview_state - -# def draw_header_preset(self, _context: Context): -# self.layout.operator( -# "grab_doc.map_pack_info", -# emboss=False, -# text="", -# icon="HELP" -# ) - -# def draw_header(self, context: Context): -# gd = context.scene.gd -# -# row = self.layout.row(align=True) -# row.prop(gd, '_use_pack_maps', text='') -# row.separator(factor=.5) - -# def draw(self, context: Context): -# gd = context.scene.gd - -# layout = self.layout -# layout.use_property_split = True -# layout.use_property_decorate = False - -# col = layout.column(align=True) -# col.prop(gd, '_channel_R') -# col.prop(gd, '_channel_G') -# col.prop(gd, 'channel_B') -# col.prop(gd, 'channel_A') +class GRABDOC_PT_pack_maps(PanelInfo, Panel): + bl_label = 'Pack Maps' + bl_parent_id = "GRABDOC_PT_grabdoc" + + @classmethod + def poll(cls, context: Context) -> bool: + return proper_scene_setup() and not context.scene.gd.preview_state + + def draw_header_preset(self, _context: Context): + self.layout.operator( + "grab_doc.pack_maps", + ) + + def draw_header(self, context: Context): + gd = context.scene.gd + + row = self.layout.row(align=True) + # row.prop(gd, '_use_pack_maps', text='') + row.separator(factor=.5) + + def draw(self, context: Context): + gd = context.scene.gd + + + + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + + col = layout.column(align=True) + col.prop(gd, 'use_pack_maps', text="Pack on export") + col.prop(gd, 'channel_R', text="channel R") + col.prop(gd, 'channel_G', text="channel G") + col.prop(gd, 'channel_B', text="channel B") + col.prop(gd, 'channel_A', text="channel A") + col.prop(gd, 'pack_name', text="Suffix") ################################################ @@ -367,6 +368,7 @@ class GRABDOC_PT_occlusion(BakerPanel, PanelInfo, Panel): ID = Global.OCCLUSION_ID NAME = Global.OCCLUSION_NAME + class GRABDOC_PT_height(BakerPanel, PanelInfo, Panel): ID = Global.HEIGHT_ID NAME = Global.HEIGHT_NAME @@ -411,6 +413,7 @@ class GRABDOC_PT_metallic(BakerPanel, PanelInfo, Panel): GRABDOC_PT_grabdoc, GRABDOC_PT_export, GRABDOC_PT_view_edit_maps, + GRABDOC_PT_pack_maps, GRABDOC_PT_normals, GRABDOC_PT_curvature, GRABDOC_PT_occlusion, @@ -420,9 +423,9 @@ class GRABDOC_PT_metallic(BakerPanel, PanelInfo, Panel): GRABDOC_PT_color, GRABDOC_PT_emissive, GRABDOC_PT_roughness, - GRABDOC_PT_metallic + GRABDOC_PT_metallic ) -# GRABDOC_PT_pack_maps + def register(): for cls in classes: From ffab8385f47ee9c59fb374cf0565cc28b4c15fff Mon Sep 17 00:00:00 2001 From: CGDJay Date: Sat, 22 Jun 2024 14:21:22 +0100 Subject: [PATCH 2/5] Fixed file format issue --- operators/operators.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/operators/operators.py b/operators/operators.py index 5e07e6b..cd2e583 100644 --- a/operators/operators.py +++ b/operators/operators.py @@ -636,25 +636,25 @@ def Return_Channel_Path(context ,Channel): if Channel == 'none': return ("") if Channel == 'normals': - return ((os.path.join(gd.export_path,gd.export_name+'_'+gd.occlusion[0].suffix+'.png'))) + return ((os.path.join(gd.export_path,gd.export_name+'_'+gd.occlusion[0].suffix+get_format()))) if Channel == 'curvature': - return((os.path.join(gd.export_path,gd.export_name+'_'+gd.curvature[0].suffix+'.png'))) + return((os.path.join(gd.export_path,gd.export_name+'_'+gd.curvature[0].suffix+get_format()))) if Channel == 'occlusion': - return((os.path.join(gd.export_path,gd.export_name+'_'+gd.occlusion[0].suffix+'.png'))) + return((os.path.join(gd.export_path,gd.export_name+'_'+gd.occlusion[0].suffix+get_format()))) if Channel == 'height': - return((os.path.join(gd.export_path,gd.export_name+'_'+gd.height[0].suffix+'.png'))) + return((os.path.join(gd.export_path,gd.export_name+'_'+gd.height[0].suffix+get_format()))) if Channel == 'id': - return((os.path.join(gd.export_path,gd.export_name+'_'+gd.id[0].suffix+'.png'))) + return((os.path.join(gd.export_path,gd.export_name+'_'+gd.id[0].suffix+get_format()))) if Channel == 'alpha': - return((os.path.join(gd.export_path,gd.export_name+'_'+gd.alpha[0].suffix+'.png'))) + return((os.path.join(gd.export_path,gd.export_name+'_'+gd.alpha[0].suffix+get_format()))) if Channel == 'color': - return((os.path.join(gd.export_path,gd.export_name+'_'+gd.color[0].suffix+'.png'))) + return((os.path.join(gd.export_path,gd.export_name+'_'+gd.color[0].suffix+get_format()))) if Channel == 'emissive': - return((os.path.join(gd.export_path,gd.export_name+'_'+gd.emissive[0].suffix+'.png'))) + return((os.path.join(gd.export_path,gd.export_name+'_'+gd.emissive[0].suffix+get_format()))) if Channel == 'roughness': - return((os.path.join(gd.export_path,gd.export_name+'_'+gd.roughness[0].suffix+'.png'))) + return((os.path.join(gd.export_path,gd.export_name+'_'+gd.roughness[0].suffix+get_format()))) if Channel == 'metallic': - return((os.path.join(gd.export_path,gd.export_name+'_'+gd.metallic[0].suffix+'.png'))) + return((os.path.join(gd.export_path,gd.export_name+'_'+gd.metallic[0].suffix+get_format()))) return False @@ -728,7 +728,7 @@ def execute(self, context): dst_image=pack_image_channels(pack_order,PackName) - dst_image.filepath_raw = Path+"//"+PackName+'.'+gd.format + dst_image.filepath_raw = Path+"//"+PackName+get_format() dst_image.file_format = gd.format dst_image.save() From 16da7c72b6b321148dc5c72eeee7b970e84c85b9 Mon Sep 17 00:00:00 2001 From: CGDJay Date: Sat, 22 Jun 2024 15:09:17 +0100 Subject: [PATCH 3/5] Added presets and removing duplicate images --- operators/operators.py | 17 +++++++++++++---- preferences.py | 7 +++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/operators/operators.py b/operators/operators.py index cd2e583..d304f2c 100644 --- a/operators/operators.py +++ b/operators/operators.py @@ -666,7 +666,7 @@ class GRABDOC_OT_pack_maps(Operator): remove_maps : BoolProperty (name='Remove packed maps' ,default=False) - # Poll should check if all required images are correctly present in the export path + # Poll to check if all required images are correctly present in the export path @classmethod def poll(cls, context: Context) -> bool: gd = context.scene.gd @@ -676,13 +676,14 @@ def poll(cls, context: Context) -> bool: BChannel=gd.channel_B AChannel=gd.channel_A + r=(Return_Channel_Path(context,RChannel)) g=(Return_Channel_Path(context,GChannel)) b=(Return_Channel_Path(context,BChannel)) a=(Return_Channel_Path(context,AChannel)) + #Alpha requires a edge case as it should be the only option that uses the "none" setting if gd.channel_A == 'none': - if os.path.exists(r) & os.path.exists(g) & os.path.exists(b)==True: return True else: @@ -699,11 +700,10 @@ def execute(self, context): gd = context.scene.gd Name = f"{gd.export_name}" - # Will require a property for the suffix PackName= (Name+"_"+gd.pack_name) Path= gd.export_path - #Loads all images into blender to avoid using a seperate package to conver to np array + #Loads all images into blender to avoid using a seperate python module to convert to np array ImageR= bpy.data.images.load(Return_Channel_Path(context,gd.channel_R)) ImageG=bpy.data.images.load(Return_Channel_Path(context,gd.channel_G)) ImageB=bpy.data.images.load(Return_Channel_Path(context,gd.channel_B)) @@ -733,6 +733,15 @@ def execute(self, context): dst_image.save() + #Remove images from blend file to keep it clean + + bpy.data.images.remove(ImageR) + bpy.data.images.remove(ImageG) + bpy.data.images.remove(ImageB) + if gd.channel_A != 'none': + bpy.data.images.remove(ImageA) + bpy.data.images.remove(dst_image) + #option to delete the extra maps through the operator panel if self.remove_maps==True: if gd.channel_A != 'none': diff --git a/preferences.py b/preferences.py index 6f1bf5a..04c1d30 100644 --- a/preferences.py +++ b/preferences.py @@ -115,6 +115,13 @@ class GRABDOC_OT_add_preset(AddPresetBase, Operator): "gd.emissive", "gd.roughness", "gd.metallic", + + "gd.use_pack_maps", + "gd.pack_name", + "gd.channel_R", + "gd.channel_G", + "gd.channel_B", + "gd.channel_A" ] # Where to store the preset From 4720160f23b660cab83d47e44e2f0c9fc30aae29 Mon Sep 17 00:00:00 2001 From: CGDJay Date: Sat, 22 Jun 2024 20:32:01 +0100 Subject: [PATCH 4/5] Initial investigation into adding material AO into the AO bake pass (intended use for foliage atlases and 3d concept art) --- utils/node.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/utils/node.py b/utils/node.py index 700e1e2..642289d 100644 --- a/utils/node.py +++ b/utils/node.py @@ -191,9 +191,23 @@ def node_init() -> None: generate_shader_interface(tree, inputs) # Create nodes + tree.interface.new_socket(name="Normal",description="some_color_input",in_out ="INPUT", socket_type="NodeSocketColor") + + + group_input = tree.nodes.new('NodeGroupInput') + group_input.name = "Group Input" + group_input.location = (-1000,0) + + bpy.data.node_groups["GD_Ambient Occlusion"].interface.items_tree[1].default_value = (0.5, 0.5, 1, 1) + + group_output = tree.nodes.new('NodeGroupOutput') group_output.name = "Group Output" + normal_map = tree.nodes.new('ShaderNodeNormalMap') + normal_map.name= "Normal Map" + normal_map.location = (-800,0) + ao = tree.nodes.new('ShaderNodeAmbientOcclusion') ao.name = "Ambient Occlusion" ao.samples = 32 @@ -210,6 +224,9 @@ def node_init() -> None: # Link nodes links = tree.links + links.new(normal_map.inputs["Color"],group_input.outputs["Normal"]) + links.new(ao.inputs["Normal"],normal_map.outputs["Normal"]) + links.new(gamma.inputs["Color"], ao.outputs["Color"]) links.new(emission.inputs["Color"], gamma.outputs["Color"]) links.new(group_output.inputs["Shader"], emission.outputs["Emission"]) From ee0d3b06e6f63e9b5f8475a34408653d942f56a0 Mon Sep 17 00:00:00 2001 From: CGDJay Date: Sat, 22 Jun 2024 21:20:09 +0100 Subject: [PATCH 5/5] Removed "none" option from R G B enum packing settings to prevent error --- preferences.py | 6 +++--- utils/node.py | 12 +++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/preferences.py b/preferences.py index 04c1d30..7a581dc 100644 --- a/preferences.py +++ b/preferences.py @@ -408,17 +408,17 @@ def update_scale(self, context: Context): default= 'AORM') channel_R: EnumProperty( - items=MAP_TYPES, + items=MAP_TYPES[1:], default="occlusion", name='R' ) channel_G: EnumProperty( - items=MAP_TYPES, + items=MAP_TYPES[1:], default="roughness", name='G' ) channel_B: EnumProperty( - items=MAP_TYPES, + items=MAP_TYPES[1:], default="metallic", name='B' ) diff --git a/utils/node.py b/utils/node.py index 642289d..c8111af 100644 --- a/utils/node.py +++ b/utils/node.py @@ -189,16 +189,18 @@ def node_init() -> None: # Create sockets generate_shader_interface(tree, inputs) + tree.interface.new_socket( + name='Normal', + socket_type='NodeSocketVector', + in_out='INPUT' + ) - # Create nodes - tree.interface.new_socket(name="Normal",description="some_color_input",in_out ="INPUT", socket_type="NodeSocketColor") - - + # Create nodes group_input = tree.nodes.new('NodeGroupInput') group_input.name = "Group Input" group_input.location = (-1000,0) - bpy.data.node_groups["GD_Ambient Occlusion"].interface.items_tree[1].default_value = (0.5, 0.5, 1, 1) + bpy.data.node_groups["GD_Ambient Occlusion"].interface.items_tree[1].default_value = (0.5, 0.5, 1) group_output = tree.nodes.new('NodeGroupOutput')