From 4af02443b8b5e77a2cf3f8c632cebe4ba317bc59 Mon Sep 17 00:00:00 2001 From: Gottfried Hofmann Date: Mon, 22 Apr 2024 15:47:27 +0200 Subject: [PATCH 01/17] Operator to automatically bake lightmaps for selected objects. --- addons/io_hubs_addon/components/operators.py | 159 ++++++++++++++++++- addons/io_hubs_addon/components/ui.py | 14 ++ 2 files changed, 172 insertions(+), 1 deletion(-) diff --git a/addons/io_hubs_addon/components/operators.py b/addons/io_hubs_addon/components/operators.py index 2907e0cb..64ccb227 100644 --- a/addons/io_hubs_addon/components/operators.py +++ b/addons/io_hubs_addon/components/operators.py @@ -1,5 +1,5 @@ import bpy -from bpy.props import StringProperty, IntProperty, BoolProperty, CollectionProperty +from bpy.props import StringProperty, IntProperty, BoolProperty, CollectionProperty, FloatProperty from bpy.types import Operator, PropertyGroup from functools import reduce @@ -670,6 +670,161 @@ def invoke(self, context, event): return {'RUNNING_MODAL'} +class BakeLightmaps(Operator): + bl_idname = "object.bake_lightmaps" + bl_label = "Bake Lightmaps" + bl_description = "Bake lightmaps of selected objects using the Cycles render engine and pack them into the .blend." + bl_options = {'REGISTER', 'UNDO'} + + default_intensity: FloatProperty(name = "Lightmaps Intensity", default = 3.14, description="Multiplier for hubs on how to interpert the brightness of the image. Set this to 1.0 if you have set up the lightmaps manually and use a non-HDR format like png or jpg.") + resolution: IntProperty(name = "Lightmaps Resolution", default = 2048, description="The pixel resoltion of the resulting lightmap.") + samples: IntProperty(name = "Max Samples", default = 1024, description="The number of samples to use for baking. Higher values reduce noise but take longer.") + + def execute(self, context): + # Check selected objects + selected_objects = bpy.context.selected_objects + # Filter mesh objects + mesh_objs = [ob for ob in selected_objects if ob.type == 'MESH'] + + # set up UV layer structure. The first layer has to be UV0, the second one UV1 for the lightmap. + for obj in mesh_objs: + obj_uv_layers = obj.data.uv_layers + # Check whether there are any UV layers and if not, create the two that are required. + if len(obj_uv_layers) == 0: + obj_uv_layers.new(name='UV0') + obj_uv_layers.new(name='UV1') + # The first layer is usually used for regular texturing so don't touch it. + # elif obj_uv_layers[0].name != 'UV0': + # # Rename the first UV layer to "UV0" + # obj_uv_layers[0].name = 'UV0' + + # In case there is only one UV layer create a second one named "UV1" for the lightmap. + if len(obj_uv_layers) == 1: + obj_uv_layers.new(name='UV1') + # Check if object has a second UV layer. If it is named "UV1", assume it is used for the lightmap. + # Otherwise add a new UV layer "UV1" and place it second in the slot list. + elif obj_uv_layers[1].name != 'UV1': + print("The second UV layer in hubs should be named UV1 and is reserved for the lightmap, all the layers >1 are ignored.") + obj_uv_layers.new(name='UV1') + # The new layer is the last in the list, swap it for position 1 + obj_uv_layers[1], obj_uv_layers[-1] = obj_uv_layers[-1], obj_uv_layers[1] + + # The layer for the lightmap needs to be the active one before lightmap packing + obj_uv_layers.active = obj_uv_layers['UV1'] + + # run UV lightmap packing on all selected objects + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + # TODO: We need to warn the user at some place like the README that the uv_layer[1] gets completely overwritten if it is called 'UV1' + bpy.ops.uv.lightmap_pack() + bpy.ops.object.mode_set(mode='OBJECT') + + # Gather all materials on the selected objects + materials = [] + for obj in mesh_objs: + if len(obj.material_slots) >= 1: + # TODO: Make more efficient + for slot in obj.material_slots: + if slot.material not in materials: + materials.append(slot.material) + else: + # an object without materials should not be selected when running the bake operator + print("Object " + obj.name + " does not have material slots, removing from selection") + obj.select_set(False) + # Check for the required nodes and set them up if not present + lightmap_texture_nodes = [] + for mat in materials: + mat_nodes = mat.node_tree.nodes + lightmap_nodes = [node for node in mat_nodes if node.bl_idname=='moz_lightmap.node'] + if len(lightmap_nodes) > 1: + print("Too many lightmap nodes in node tree of material", mat.name) + elif len(lightmap_nodes) < 1: + lightmap_texture_nodes.append(self.setup_moz_lightmap_nodes(mat.node_tree)) + else: + # TODO: Check wether all nodes are set up correctly, for now assume they are + lightmap_nodes[0].intensity = self.default_intensity + # the image texture node needs to be the active one for baking, it is connected to the lightmap node so get it from there + lightmap_texture_node = lightmap_nodes[0].inputs[0].links[0].from_node + mat.node_tree.nodes.active = lightmap_texture_node + lightmap_texture_nodes.append(lightmap_texture_node) + + # Baking has to happen in Cycles, it is not supported in EEVEE yet + render_engine_tmp = context.scene.render.engine + context.scene.render.engine = 'CYCLES' + samples_tmp = context.scene.cycles.samples + context.scene.cycles.samples = self.samples + # Baking needs to happen without the color pass because we only want the direct and indirect light contributions + bake_settings = context.scene.render.bake + bake_settings.use_pass_direct = True + bake_settings.use_pass_indirect = True + bake_settings.use_pass_color = False + # The should be small because otherwise it could overwrite UV islands + bake_settings.margin = 1 + # Not sure whether this has any influence + bake_settings.image_settings.file_format = 'HDR' + context.scene.render.image_settings.file_format = 'HDR' + bpy.ops.object.bake(type='DIFFUSE') + # After baking is done, return everything back to normal + context.scene.cycles.samples = samples_tmp + context.scene.render.engine = render_engine_tmp + # Pack all newly created or updated images + for node in lightmap_texture_nodes: + file_path = bpy.path.abspath(f"{bpy.app.tempdir}/{node.image.name}.hdr") + # node.image.save_render(file_path) + node.image.filepath_raw = file_path + node.image.file_format = 'HDR' + node.image.save() + node.image.pack() + # Update the filepath so it unpacks nicely for the user. + #TODO: Mechanism taken from reflection_probe.py line 300-306, de-duplicate + new_filepath = f"//{node.image.name}.hdr" + node.image.packed_files[0].filepath = new_filepath + node.image.filepath_raw = new_filepath + node.image.filepath = new_filepath + + # Remove file from temporary directory to de-clutter the system. Especially on windows the temporary directory is rarely purged. + if os.path.exists(file_path): + os.remove(file_path) + + return {'FINISHED'} + + def invoke(self, context, event): + if bpy.data.is_saved == False: + # + def draw(self, context): + self.layout.label( + text="You ned to save the .blend file before running this operator.") + bpy.context.window_manager.popup_menu( + draw, title="File not saved.", icon='ERROR') + return {'CANCELLED'} + # needed to get the dialoge with the intensity + return context.window_manager.invoke_props_dialog(self) + + def setup_moz_lightmap_nodes(self, node_tree): + ''' Returns the lightmap texture node of the newly created setup ''' + mat_nodes = node_tree.nodes + # This function gets called when no lightmap node is present + lightmap_node = mat_nodes.new(type="moz_lightmap.node") + lightmap_node.intensity = self.default_intensity + + lightmap_texture_node = mat_nodes.new(type="ShaderNodeTexImage") + lightmap_texture_node.location[0] -= 300 + + img = bpy.data.images.new('LightMap', self.resolution, self.resolution, alpha=False, float_buffer=True) + lightmap_texture_node.image = img + + UVmap_node = mat_nodes.new(type="ShaderNodeUVMap") + UVmap_node.uv_map = "UV1" + UVmap_node.location[0] -= 500 + + node_tree.links.new(UVmap_node.outputs['UV'], lightmap_texture_node.inputs['Vector']) + node_tree.links.new(lightmap_texture_node.outputs['Color'], lightmap_node.inputs['Lightmap']) + + # the image texture node needs to be the active one for baking + node_tree.nodes.active = lightmap_texture_node + + return lightmap_texture_node + def register(): bpy.utils.register_class(AddHubsComponent) bpy.utils.register_class(RemoveHubsComponent) @@ -681,6 +836,7 @@ def register(): bpy.utils.register_class(ViewReportInInfoEditor) bpy.utils.register_class(CopyHubsComponent) bpy.utils.register_class(OpenImage) + bpy.utils.register_class(BakeLightmaps) bpy.types.WindowManager.hubs_report_scroll_index = IntProperty( default=0, min=0) bpy.types.WindowManager.hubs_report_scroll_percentage = IntProperty( @@ -700,6 +856,7 @@ def unregister(): bpy.utils.unregister_class(ViewReportInInfoEditor) bpy.utils.unregister_class(CopyHubsComponent) bpy.utils.unregister_class(OpenImage) + bpy.utils.unregister_class(BakeLightmaps) del bpy.types.WindowManager.hubs_report_scroll_index del bpy.types.WindowManager.hubs_report_scroll_percentage del bpy.types.WindowManager.hubs_report_last_title diff --git a/addons/io_hubs_addon/components/ui.py b/addons/io_hubs_addon/components/ui.py index 9dbb41fd..ed1098bd 100644 --- a/addons/io_hubs_addon/components/ui.py +++ b/addons/io_hubs_addon/components/ui.py @@ -141,6 +141,18 @@ class HubsObjectPanel(bpy.types.Panel): def draw(self, context): draw_components_list(self, context) +class HubsObjectLightmapPanel(bpy.types.Panel): + bl_label = "Hubs Lightmap Baker" + bl_idname = "OBJECT_PT_hubs_lightmap_baker" + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = "object" + + def draw(self, context): + layout = self.layout + row = layout.row() + row.operator("object.bake_lightmaps", text="Bake Lightmaps of selected objects") + class HUBS_PT_ToolsPanel(bpy.types.Panel): bl_idname = "HUBS_PT_ToolsPanel" @@ -230,6 +242,7 @@ def register(): bpy.utils.register_class(HubsBonePanel) bpy.utils.register_class(TooltipLabel) bpy.utils.register_class(HUBS_PT_ToolsPanel) + bpy.utils.register_class(HubsObjectLightmapPanel) bpy.types.TOPBAR_MT_window.append(window_menu_addition) bpy.types.VIEW3D_MT_object.append(object_menu_addition) @@ -243,6 +256,7 @@ def unregister(): bpy.utils.unregister_class(HubsBonePanel) bpy.utils.unregister_class(TooltipLabel) bpy.utils.unregister_class(HUBS_PT_ToolsPanel) + bpy.utils.unregister_class(HubsObjectLightmapPanel) bpy.types.TOPBAR_MT_window.remove(window_menu_addition) bpy.types.VIEW3D_MT_object.remove(object_menu_addition) From 567b121731ba9acd81337a399445ea60eb86a8aa Mon Sep 17 00:00:00 2001 From: Gottfried Hofmann Date: Wed, 8 May 2024 16:41:42 +0200 Subject: [PATCH 02/17] Use smart UV project instead of lightmap pack --- addons/io_hubs_addon/components/operators.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/addons/io_hubs_addon/components/operators.py b/addons/io_hubs_addon/components/operators.py index 64ccb227..38b8c6d5 100644 --- a/addons/io_hubs_addon/components/operators.py +++ b/addons/io_hubs_addon/components/operators.py @@ -716,8 +716,11 @@ def execute(self, context): bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') # TODO: We need to warn the user at some place like the README that the uv_layer[1] gets completely overwritten if it is called 'UV1' - bpy.ops.uv.lightmap_pack() + # bpy.ops.uv.lightmap_pack() + bpy.ops.uv.smart_project() bpy.ops.object.mode_set(mode='OBJECT') + # Update the view layer so all parts take notice of the changed UV layout + bpy.context.view_layer.update() # Gather all materials on the selected objects materials = [] @@ -759,7 +762,7 @@ def execute(self, context): bake_settings.use_pass_indirect = True bake_settings.use_pass_color = False # The should be small because otherwise it could overwrite UV islands - bake_settings.margin = 1 + bake_settings.margin = 0 # Not sure whether this has any influence bake_settings.image_settings.file_format = 'HDR' context.scene.render.image_settings.file_format = 'HDR' @@ -812,6 +815,7 @@ def setup_moz_lightmap_nodes(self, node_tree): img = bpy.data.images.new('LightMap', self.resolution, self.resolution, alpha=False, float_buffer=True) lightmap_texture_node.image = img + lightmap_texture_node.image.colorspace_settings.name = 'Linear' UVmap_node = mat_nodes.new(type="ShaderNodeUVMap") UVmap_node.uv_map = "UV1" From a6bad910a8bd611e150e9ab2f887776e15c19e17 Mon Sep 17 00:00:00 2001 From: Gottfried Hofmann Date: Fri, 17 May 2024 16:36:55 +0200 Subject: [PATCH 03/17] Use Smart UV Project instead of Lightmap Pack --- addons/io_hubs_addon/components/operators.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/addons/io_hubs_addon/components/operators.py b/addons/io_hubs_addon/components/operators.py index 38b8c6d5..76d618e8 100644 --- a/addons/io_hubs_addon/components/operators.py +++ b/addons/io_hubs_addon/components/operators.py @@ -683,8 +683,15 @@ class BakeLightmaps(Operator): def execute(self, context): # Check selected objects selected_objects = bpy.context.selected_objects - # Filter mesh objects - mesh_objs = [ob for ob in selected_objects if ob.type == 'MESH'] + + # filter mesh objects and others + mesh_objs, other_objs = [], [] + for ob in selected_objects: + (mesh_objs if ob.type == 'MESH' else other_objs).append(ob) + + for ob in other_objs: + # Remove non-mesh objects from selection to ensure baking will work + ob.select_set(False) # set up UV layer structure. The first layer has to be UV0, the second one UV1 for the lightmap. for obj in mesh_objs: @@ -762,7 +769,7 @@ def execute(self, context): bake_settings.use_pass_indirect = True bake_settings.use_pass_color = False # The should be small because otherwise it could overwrite UV islands - bake_settings.margin = 0 + bake_settings.margin = 2 # Not sure whether this has any influence bake_settings.image_settings.file_format = 'HDR' context.scene.render.image_settings.file_format = 'HDR' From deda117ea74445bd23fd9ee6fa6fd5b4eebbe2dd Mon Sep 17 00:00:00 2001 From: Gottfried Hofmann Date: Tue, 30 Jul 2024 18:30:02 +0200 Subject: [PATCH 04/17] Fix liniting issues --- addons/io_hubs_addon/components/operators.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/addons/io_hubs_addon/components/operators.py b/addons/io_hubs_addon/components/operators.py index 76d618e8..d89598da 100644 --- a/addons/io_hubs_addon/components/operators.py +++ b/addons/io_hubs_addon/components/operators.py @@ -676,9 +676,15 @@ class BakeLightmaps(Operator): bl_description = "Bake lightmaps of selected objects using the Cycles render engine and pack them into the .blend." bl_options = {'REGISTER', 'UNDO'} - default_intensity: FloatProperty(name = "Lightmaps Intensity", default = 3.14, description="Multiplier for hubs on how to interpert the brightness of the image. Set this to 1.0 if you have set up the lightmaps manually and use a non-HDR format like png or jpg.") - resolution: IntProperty(name = "Lightmaps Resolution", default = 2048, description="The pixel resoltion of the resulting lightmap.") - samples: IntProperty(name = "Max Samples", default = 1024, description="The number of samples to use for baking. Higher values reduce noise but take longer.") + default_intensity: FloatProperty(name="Lightmaps Intensity", + default=3.14, + description="Multiplier for hubs on how to interpert the brightness of the image. Set this to 1.0 if you have set up the lightmaps manually and use a non-HDR format like png or jpg.") + resolution: IntProperty(name="Lightmaps Resolution", + default=2048, + description="The pixel resoltion of the resulting lightmap.") + samples: IntProperty(name="Max Samples", + default=1024, + description="The number of samples to use for baking. Higher values reduce noise but take longer.") def execute(self, context): # Check selected objects From 6a69cfeda551f3be63279ec5645c036a5166e044 Mon Sep 17 00:00:00 2001 From: Gottfried Hofmann Date: Tue, 30 Jul 2024 18:41:03 +0200 Subject: [PATCH 05/17] Fix liniting issues --- addons/io_hubs_addon/components/operators.py | 30 +++++++++----------- addons/io_hubs_addon/components/ui.py | 1 + 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/addons/io_hubs_addon/components/operators.py b/addons/io_hubs_addon/components/operators.py index d89598da..28564cc0 100644 --- a/addons/io_hubs_addon/components/operators.py +++ b/addons/io_hubs_addon/components/operators.py @@ -676,20 +676,20 @@ class BakeLightmaps(Operator): bl_description = "Bake lightmaps of selected objects using the Cycles render engine and pack them into the .blend." bl_options = {'REGISTER', 'UNDO'} - default_intensity: FloatProperty(name="Lightmaps Intensity", - default=3.14, + default_intensity: FloatProperty(name="Lightmaps Intensity", + default=3.14, description="Multiplier for hubs on how to interpert the brightness of the image. Set this to 1.0 if you have set up the lightmaps manually and use a non-HDR format like png or jpg.") - resolution: IntProperty(name="Lightmaps Resolution", - default=2048, + resolution: IntProperty(name="Lightmaps Resolution", + default=2048, description="The pixel resoltion of the resulting lightmap.") - samples: IntProperty(name="Max Samples", - default=1024, + samples: IntProperty(name="Max Samples", + default=1024, description="The number of samples to use for baking. Higher values reduce noise but take longer.") def execute(self, context): # Check selected objects selected_objects = bpy.context.selected_objects - + # filter mesh objects and others mesh_objs, other_objs = [], [] for ob in selected_objects: @@ -706,10 +706,6 @@ def execute(self, context): if len(obj_uv_layers) == 0: obj_uv_layers.new(name='UV0') obj_uv_layers.new(name='UV1') - # The first layer is usually used for regular texturing so don't touch it. - # elif obj_uv_layers[0].name != 'UV0': - # # Rename the first UV layer to "UV0" - # obj_uv_layers[0].name = 'UV0' # In case there is only one UV layer create a second one named "UV1" for the lightmap. if len(obj_uv_layers) == 1: @@ -751,7 +747,7 @@ def execute(self, context): lightmap_texture_nodes = [] for mat in materials: mat_nodes = mat.node_tree.nodes - lightmap_nodes = [node for node in mat_nodes if node.bl_idname=='moz_lightmap.node'] + lightmap_nodes = [node for node in mat_nodes if node.bl_idname == 'moz_lightmap.node'] if len(lightmap_nodes) > 1: print("Too many lightmap nodes in node tree of material", mat.name) elif len(lightmap_nodes) < 1: @@ -792,7 +788,7 @@ def execute(self, context): node.image.save() node.image.pack() # Update the filepath so it unpacks nicely for the user. - #TODO: Mechanism taken from reflection_probe.py line 300-306, de-duplicate + # TODO: Mechanism taken from reflection_probe.py line 300-306, de-duplicate new_filepath = f"//{node.image.name}.hdr" node.image.packed_files[0].filepath = new_filepath node.image.filepath_raw = new_filepath @@ -803,10 +799,9 @@ def execute(self, context): os.remove(file_path) return {'FINISHED'} - + def invoke(self, context, event): - if bpy.data.is_saved == False: - # + if bpy.data.is_saved is False: def draw(self, context): self.layout.label( text="You ned to save the .blend file before running this operator.") @@ -815,7 +810,7 @@ def draw(self, context): return {'CANCELLED'} # needed to get the dialoge with the intensity return context.window_manager.invoke_props_dialog(self) - + def setup_moz_lightmap_nodes(self, node_tree): ''' Returns the lightmap texture node of the newly created setup ''' mat_nodes = node_tree.nodes @@ -842,6 +837,7 @@ def setup_moz_lightmap_nodes(self, node_tree): return lightmap_texture_node + def register(): bpy.utils.register_class(AddHubsComponent) bpy.utils.register_class(RemoveHubsComponent) diff --git a/addons/io_hubs_addon/components/ui.py b/addons/io_hubs_addon/components/ui.py index ed1098bd..f8b82036 100644 --- a/addons/io_hubs_addon/components/ui.py +++ b/addons/io_hubs_addon/components/ui.py @@ -141,6 +141,7 @@ class HubsObjectPanel(bpy.types.Panel): def draw(self, context): draw_components_list(self, context) + class HubsObjectLightmapPanel(bpy.types.Panel): bl_label = "Hubs Lightmap Baker" bl_idname = "OBJECT_PT_hubs_lightmap_baker" From c0c68ead2e1fbbaa2310c50b027efdfe51693039 Mon Sep 17 00:00:00 2001 From: Gottfried Hofmann Date: Tue, 3 Sep 2024 17:53:19 +0200 Subject: [PATCH 06/17] Update addons/io_hubs_addon/components/operators.py At some point in the Blender 4 series linear was separated out into a bunch of different versions, Linear Rec.709 is what we have used when importing HDRs and seems to be the new default. This code should make it work for all Blender versions Co-authored-by: Exairnous --- addons/io_hubs_addon/components/operators.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/addons/io_hubs_addon/components/operators.py b/addons/io_hubs_addon/components/operators.py index 28564cc0..97078f2c 100644 --- a/addons/io_hubs_addon/components/operators.py +++ b/addons/io_hubs_addon/components/operators.py @@ -823,7 +823,10 @@ def setup_moz_lightmap_nodes(self, node_tree): img = bpy.data.images.new('LightMap', self.resolution, self.resolution, alpha=False, float_buffer=True) lightmap_texture_node.image = img - lightmap_texture_node.image.colorspace_settings.name = 'Linear' + if bpy.app.version < (4, 0, 0): + lightmap_texture_node.image.colorspace_settings.name = "Linear" + else: + lightmap_texture_node.image.colorspace_settings.name = "Linear Rec.709" UVmap_node = mat_nodes.new(type="ShaderNodeUVMap") UVmap_node.uv_map = "UV1" From 4f068084ec873678c5247825057dc327d3772b6b Mon Sep 17 00:00:00 2001 From: Gottfried Hofmann Date: Tue, 3 Sep 2024 18:00:24 +0200 Subject: [PATCH 07/17] Add custom name in consts.py for the Lightmap UV Layer --- addons/io_hubs_addon/components/consts.py | 2 ++ addons/io_hubs_addon/components/operators.py | 21 ++++++++++---------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/addons/io_hubs_addon/components/consts.py b/addons/io_hubs_addon/components/consts.py index 63f23995..797a094c 100644 --- a/addons/io_hubs_addon/components/consts.py +++ b/addons/io_hubs_addon/components/consts.py @@ -1,5 +1,7 @@ from math import pi +LIGHTMAP_LAYER_NAME = "Lightmap" + DISTANCE_MODELS = [("inverse", "Inverse drop off (inverse)", "Volume will decrease inversely with distance"), ("linear", "Linear drop off (linear)", diff --git a/addons/io_hubs_addon/components/operators.py b/addons/io_hubs_addon/components/operators.py index 97078f2c..ff61d247 100644 --- a/addons/io_hubs_addon/components/operators.py +++ b/addons/io_hubs_addon/components/operators.py @@ -11,6 +11,7 @@ from .gizmos import update_gizmos from .utils import is_linked, redraw_component_ui from ..icons import get_hubs_icons +from .consts import LIGHTMAP_LAYER_NAME import os @@ -699,27 +700,27 @@ def execute(self, context): # Remove non-mesh objects from selection to ensure baking will work ob.select_set(False) - # set up UV layer structure. The first layer has to be UV0, the second one UV1 for the lightmap. + # set up UV layer structure. The first layer has to be UV0, the second one LIGHTMAP_LAYER_NAME for the lightmap. for obj in mesh_objs: obj_uv_layers = obj.data.uv_layers # Check whether there are any UV layers and if not, create the two that are required. if len(obj_uv_layers) == 0: obj_uv_layers.new(name='UV0') - obj_uv_layers.new(name='UV1') + obj_uv_layers.new(name=LIGHTMAP_LAYER_NAME) - # In case there is only one UV layer create a second one named "UV1" for the lightmap. + # In case there is only one UV layer create a second one named LIGHTMAP_LAYER_NAME for the lightmap. if len(obj_uv_layers) == 1: - obj_uv_layers.new(name='UV1') - # Check if object has a second UV layer. If it is named "UV1", assume it is used for the lightmap. - # Otherwise add a new UV layer "UV1" and place it second in the slot list. - elif obj_uv_layers[1].name != 'UV1': - print("The second UV layer in hubs should be named UV1 and is reserved for the lightmap, all the layers >1 are ignored.") - obj_uv_layers.new(name='UV1') + obj_uv_layers.new(name=LIGHTMAP_LAYER_NAME) + # Check if object has a second UV layer. If it is named LIGHTMAP_LAYER_NAME, assume it is used for the lightmap. + # Otherwise add a new UV layer LIGHTMAP_LAYER_NAME and place it second in the slot list. + elif obj_uv_layers[1].name != LIGHTMAP_LAYER_NAME: + print("The second UV layer in hubs should be named " + LIGHTMAP_LAYER_NAME + " and is reserved for the lightmap, all the layers >1 are ignored.") + obj_uv_layers.new(name=LIGHTMAP_LAYER_NAME) # The new layer is the last in the list, swap it for position 1 obj_uv_layers[1], obj_uv_layers[-1] = obj_uv_layers[-1], obj_uv_layers[1] # The layer for the lightmap needs to be the active one before lightmap packing - obj_uv_layers.active = obj_uv_layers['UV1'] + obj_uv_layers.active = obj_uv_layers[LIGHTMAP_LAYER_NAME] # run UV lightmap packing on all selected objects bpy.ops.object.mode_set(mode='EDIT') From 021d1babf3cbe2b5f7fa80be1b49630507e5747f Mon Sep 17 00:00:00 2001 From: Gottfried Hofmann Date: Tue, 3 Sep 2024 18:10:44 +0200 Subject: [PATCH 08/17] Return to old settings (render engine, bake etc.) after bake is finished. --- addons/io_hubs_addon/components/operators.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/addons/io_hubs_addon/components/operators.py b/addons/io_hubs_addon/components/operators.py index ff61d247..61ccab21 100644 --- a/addons/io_hubs_addon/components/operators.py +++ b/addons/io_hubs_addon/components/operators.py @@ -767,6 +767,7 @@ def execute(self, context): samples_tmp = context.scene.cycles.samples context.scene.cycles.samples = self.samples # Baking needs to happen without the color pass because we only want the direct and indirect light contributions + bake_settings_before = context.scene.render.bake.copy() bake_settings = context.scene.render.bake bake_settings.use_pass_direct = True bake_settings.use_pass_indirect = True @@ -799,6 +800,11 @@ def execute(self, context): if os.path.exists(file_path): os.remove(file_path) + # return to old settings + bake_settings = bake_settings_before + context.scene.cycles.samples = samples_tmp + context.scene.render.engine = render_engine_tmp + return {'FINISHED'} def invoke(self, context, event): From d5cfa46fad768389ef4a3a846c1a107f3c32ee14 Mon Sep 17 00:00:00 2001 From: Gottfried Hofmann Date: Tue, 3 Sep 2024 18:19:24 +0200 Subject: [PATCH 09/17] Remove check whether file is saved, not needed anymore with packed images. --- addons/io_hubs_addon/components/operators.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/addons/io_hubs_addon/components/operators.py b/addons/io_hubs_addon/components/operators.py index 61ccab21..94d6ac8e 100644 --- a/addons/io_hubs_addon/components/operators.py +++ b/addons/io_hubs_addon/components/operators.py @@ -767,7 +767,7 @@ def execute(self, context): samples_tmp = context.scene.cycles.samples context.scene.cycles.samples = self.samples # Baking needs to happen without the color pass because we only want the direct and indirect light contributions - bake_settings_before = context.scene.render.bake.copy() + bake_settings_before = context.scene.render.bake bake_settings = context.scene.render.bake bake_settings.use_pass_direct = True bake_settings.use_pass_indirect = True @@ -808,13 +808,6 @@ def execute(self, context): return {'FINISHED'} def invoke(self, context, event): - if bpy.data.is_saved is False: - def draw(self, context): - self.layout.label( - text="You ned to save the .blend file before running this operator.") - bpy.context.window_manager.popup_menu( - draw, title="File not saved.", icon='ERROR') - return {'CANCELLED'} # needed to get the dialoge with the intensity return context.window_manager.invoke_props_dialog(self) From 5ca8cd1a5cc199db4f663f7408719bcf723a902f Mon Sep 17 00:00:00 2001 From: Gottfried Hofmann Date: Tue, 3 Sep 2024 18:26:27 +0200 Subject: [PATCH 10/17] Add warning to readme that the second UV layer could be overwritten by the auto baker. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 3ee8c102..fbc26aa2 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,10 @@ Note that for use in Hubs, you currently **MUST** use the second UV set, as Thre ![setting bake UV](https://user-images.githubusercontent.com/130735/83697782-b9e96b00-a5b4-11ea-986b-6690c69d8a3f.png) +# Automatically baking Lightmaps + +To automatically create the node-setup needed to bake lightmaps and run baking on one step, select all objects you want to bake lightmaps for and got to `Object Properties > Hubs Lightmap Baker` and click on `Bake Lightmaps of selected objects`. **WARNING**: If a second UV layer is present on an object but it does not have a material with a `MOZ_lightmap` node, the UV layer will be overwritten! + # Exporting This addon works in conjunction with the official glTF add-on, so exporting is done through it. Select "File > Export > glTF 2.0" and then ensure "Hubs Components" is enabled under "Extensions". From 8582edadad457fc777536361f84b87a1aeb075c2 Mon Sep 17 00:00:00 2001 From: Gottfried Hofmann Date: Tue, 3 Sep 2024 18:27:47 +0200 Subject: [PATCH 11/17] Adjust name of lightmap uv layer to be the same as in the readme. --- addons/io_hubs_addon/components/consts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/io_hubs_addon/components/consts.py b/addons/io_hubs_addon/components/consts.py index 797a094c..1de4eca9 100644 --- a/addons/io_hubs_addon/components/consts.py +++ b/addons/io_hubs_addon/components/consts.py @@ -1,6 +1,6 @@ from math import pi -LIGHTMAP_LAYER_NAME = "Lightmap" +LIGHTMAP_LAYER_NAME = "LightmapUV" DISTANCE_MODELS = [("inverse", "Inverse drop off (inverse)", "Volume will decrease inversely with distance"), From 3d03bea965fadbc1d00a1306a4f9a4c747425687 Mon Sep 17 00:00:00 2001 From: Gottfried Hofmann Date: Wed, 4 Sep 2024 17:17:41 +0200 Subject: [PATCH 12/17] Suggestion: Create object groups with the same material for unwrapping, uses the space better for multiple objects and materials but is prone to overlaps in some corner cases. --- addons/io_hubs_addon/components/consts.py | 1 + addons/io_hubs_addon/components/operators.py | 76 ++++++++++++++------ 2 files changed, 55 insertions(+), 22 deletions(-) diff --git a/addons/io_hubs_addon/components/consts.py b/addons/io_hubs_addon/components/consts.py index 1de4eca9..1c101452 100644 --- a/addons/io_hubs_addon/components/consts.py +++ b/addons/io_hubs_addon/components/consts.py @@ -1,6 +1,7 @@ from math import pi LIGHTMAP_LAYER_NAME = "LightmapUV" +LIGHTMAP_UV_ISLAND_MARGIN = 0.01 DISTANCE_MODELS = [("inverse", "Inverse drop off (inverse)", "Volume will decrease inversely with distance"), diff --git a/addons/io_hubs_addon/components/operators.py b/addons/io_hubs_addon/components/operators.py index 94d6ac8e..c1dd77dd 100644 --- a/addons/io_hubs_addon/components/operators.py +++ b/addons/io_hubs_addon/components/operators.py @@ -11,7 +11,7 @@ from .gizmos import update_gizmos from .utils import is_linked, redraw_component_ui from ..icons import get_hubs_icons -from .consts import LIGHTMAP_LAYER_NAME +from .consts import LIGHTMAP_LAYER_NAME, LIGHTMAP_UV_ISLAND_MARGIN import os @@ -679,27 +679,15 @@ class BakeLightmaps(Operator): default_intensity: FloatProperty(name="Lightmaps Intensity", default=3.14, - description="Multiplier for hubs on how to interpert the brightness of the image. Set this to 1.0 if you have set up the lightmaps manually and use a non-HDR format like png or jpg.") + description="Multiplier for hubs on how to interpret the brightness of the image. Set this to 1.0 if you have set up the lightmaps manually and use a non-HDR format like png or jpg.") resolution: IntProperty(name="Lightmaps Resolution", default=2048, - description="The pixel resoltion of the resulting lightmap.") + description="The pixel resolution of the resulting lightmap.") samples: IntProperty(name="Max Samples", default=1024, description="The number of samples to use for baking. Higher values reduce noise but take longer.") - def execute(self, context): - # Check selected objects - selected_objects = bpy.context.selected_objects - - # filter mesh objects and others - mesh_objs, other_objs = [], [] - for ob in selected_objects: - (mesh_objs if ob.type == 'MESH' else other_objs).append(ob) - - for ob in other_objs: - # Remove non-mesh objects from selection to ensure baking will work - ob.select_set(False) - + def create_uv_layouts(self, context, mesh_objs): # set up UV layer structure. The first layer has to be UV0, the second one LIGHTMAP_LAYER_NAME for the lightmap. for obj in mesh_objs: obj_uv_layers = obj.data.uv_layers @@ -721,32 +709,73 @@ def execute(self, context): # The layer for the lightmap needs to be the active one before lightmap packing obj_uv_layers.active = obj_uv_layers[LIGHTMAP_LAYER_NAME] + # Set the object as selected in object mode + obj.select_set(True) # run UV lightmap packing on all selected objects bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') # TODO: We need to warn the user at some place like the README that the uv_layer[1] gets completely overwritten if it is called 'UV1' # bpy.ops.uv.lightmap_pack() - bpy.ops.uv.smart_project() + bpy.ops.uv.smart_project(island_margin=LIGHTMAP_UV_ISLAND_MARGIN) bpy.ops.object.mode_set(mode='OBJECT') + # Deselct the objects again to return without changing the scene + for obj in mesh_objs: + obj.select_set(False) # Update the view layer so all parts take notice of the changed UV layout bpy.context.view_layer.update() + return{'FINISHED'} + + def execute(self, context): + # Check selected objects + selected_objects = bpy.context.selected_objects + + # filter mesh objects and others + mesh_objs, other_objs = [], [] + for ob in selected_objects: + (mesh_objs if ob.type == 'MESH' else other_objs).append(ob) + # Remove all objects from selection so we can easily re-select subgroups later + ob.select_set(False) + + # for ob in other_objs: + # Remove non-mesh objects from selection to ensure baking will work + # ob.select_set(False) + # Gather all materials on the selected objects - materials = [] + # materials = [] + # Dictionary that stores which object has which materials so we can group them later + material_object_associations = {} for obj in mesh_objs: if len(obj.material_slots) >= 1: # TODO: Make more efficient for slot in obj.material_slots: - if slot.material not in materials: - materials.append(slot.material) + if slot.material is not None: + mat = slot.material + if mat not in material_object_associations: + # materials.append(mat) + material_object_associations[mat] = [] + material_object_associations[mat].append(obj) else: # an object without materials should not be selected when running the bake operator - print("Object " + obj.name + " does not have material slots, removing from selection") + print("Object " + obj.name + " does not have material slots, removing from set") obj.select_set(False) + mesh_objs.remove(obj) + + print(material_object_associations.items()) + # Set up the UV layer structure and auto-unwrap optimized for lightmaps + visited_objects = set() + for mat, obj_list in material_object_associations.items(): + for ob in visited_objects: + if ob in obj_list: + obj_list.remove(ob) + self.create_uv_layouts(context, obj_list) + for ob in obj_list: + visited_objects.add(ob) + # Check for the required nodes and set them up if not present lightmap_texture_nodes = [] - for mat in materials: + for mat in material_object_associations.keys(): mat_nodes = mat.node_tree.nodes lightmap_nodes = [node for node in mat_nodes if node.bl_idname == 'moz_lightmap.node'] if len(lightmap_nodes) > 1: @@ -761,6 +790,9 @@ def execute(self, context): mat.node_tree.nodes.active = lightmap_texture_node lightmap_texture_nodes.append(lightmap_texture_node) + # Re-select all the objects that need baking before running the operator + for ob in mesh_objs: + ob.select_set(True) # Baking has to happen in Cycles, it is not supported in EEVEE yet render_engine_tmp = context.scene.render.engine context.scene.render.engine = 'CYCLES' From 020d77d72808e4aec57574fb41b53cd6628ea438 Mon Sep 17 00:00:00 2001 From: Gottfried Hofmann Date: Wed, 4 Sep 2024 17:21:00 +0200 Subject: [PATCH 13/17] Fix linting issues --- addons/io_hubs_addon/components/operators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/io_hubs_addon/components/operators.py b/addons/io_hubs_addon/components/operators.py index c1dd77dd..0543808f 100644 --- a/addons/io_hubs_addon/components/operators.py +++ b/addons/io_hubs_addon/components/operators.py @@ -709,7 +709,7 @@ def create_uv_layouts(self, context, mesh_objs): # The layer for the lightmap needs to be the active one before lightmap packing obj_uv_layers.active = obj_uv_layers[LIGHTMAP_LAYER_NAME] - # Set the object as selected in object mode + # Set the object as selected in object mode obj.select_set(True) # run UV lightmap packing on all selected objects @@ -726,7 +726,7 @@ def create_uv_layouts(self, context, mesh_objs): bpy.context.view_layer.update() return{'FINISHED'} - + def execute(self, context): # Check selected objects selected_objects = bpy.context.selected_objects From 7e455ef10b55f5ac958ad8cb4c6b3e6d7d55474f Mon Sep 17 00:00:00 2001 From: Gottfried Hofmann Date: Wed, 4 Sep 2024 17:29:06 +0200 Subject: [PATCH 14/17] Remove TODO --- addons/io_hubs_addon/components/operators.py | 1 - 1 file changed, 1 deletion(-) diff --git a/addons/io_hubs_addon/components/operators.py b/addons/io_hubs_addon/components/operators.py index 0543808f..77c84c8e 100644 --- a/addons/io_hubs_addon/components/operators.py +++ b/addons/io_hubs_addon/components/operators.py @@ -715,7 +715,6 @@ def create_uv_layouts(self, context, mesh_objs): # run UV lightmap packing on all selected objects bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') - # TODO: We need to warn the user at some place like the README that the uv_layer[1] gets completely overwritten if it is called 'UV1' # bpy.ops.uv.lightmap_pack() bpy.ops.uv.smart_project(island_margin=LIGHTMAP_UV_ISLAND_MARGIN) bpy.ops.object.mode_set(mode='OBJECT') From cf994da9eb5c9ceb199c1e052e2b25d1872290f7 Mon Sep 17 00:00:00 2001 From: Gottfried Hofmann Date: Tue, 8 Oct 2024 11:55:05 +0200 Subject: [PATCH 15/17] Several fixes thanks to @vincentfretin --- .vscode/settings.json | 35 ++++++++++---------- addons/io_hubs_addon/components/operators.py | 14 ++++---- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 722b82c4..1c703b5b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,18 +1,19 @@ -{ - "python.autoComplete.extraPaths": [ - "./fake_bpy_modules_3.3-20221006" - ], - "python.formatting.provider": "autopep8", - "python.formatting.autopep8Args": [ - "--exclude=models", "--max-line-length", "120", "--experimental" - ], - "python.analysis.extraPaths": [ - "./fake_bpy_modules_3.3-20221006" - ], - "editor.defaultFormatter": "ms-python.autopep8", - "editor.formatOnSave": true, - "editor.formatOnSaveMode": "file", - "autopep8.args": [ - "--exclude=models", "--max-line-length", "120", "--experimental" - ], +{ + "python.autoComplete.extraPaths": [ + "./fake_bpy_modules_3.3-20221006" + ], + "python.formatting.provider": "autopep8", + "python.formatting.autopep8Args": [ + "--exclude=models", "--max-line-length", "120", "--experimental" + ], + "python.analysis.extraPaths": [ + "./fake_bpy_modules_3.3-20221006" + ], + "editor.defaultFormatter": "ms-python.autopep8", + "editor.formatOnSave": true, + "editor.formatOnSaveMode": "file", + "autopep8.args": [ + "--exclude=models", "--max-line-length", "120", "--experimental" + ], + "cmake.configureOnOpen": false, } \ No newline at end of file diff --git a/addons/io_hubs_addon/components/operators.py b/addons/io_hubs_addon/components/operators.py index 77c84c8e..8634447c 100644 --- a/addons/io_hubs_addon/components/operators.py +++ b/addons/io_hubs_addon/components/operators.py @@ -718,13 +718,13 @@ def create_uv_layouts(self, context, mesh_objs): # bpy.ops.uv.lightmap_pack() bpy.ops.uv.smart_project(island_margin=LIGHTMAP_UV_ISLAND_MARGIN) bpy.ops.object.mode_set(mode='OBJECT') - # Deselct the objects again to return without changing the scene + # Deselect the objects again to return without changing the scene for obj in mesh_objs: obj.select_set(False) # Update the view layer so all parts take notice of the changed UV layout bpy.context.view_layer.update() - return{'FINISHED'} + return {'FINISHED'} def execute(self, context): # Check selected objects @@ -745,20 +745,20 @@ def execute(self, context): # materials = [] # Dictionary that stores which object has which materials so we can group them later material_object_associations = {} - for obj in mesh_objs: + # Iterate over a copy of mesh_objs because we are modifying it further down + for obj in list(mesh_objs): if len(obj.material_slots) >= 1: # TODO: Make more efficient for slot in obj.material_slots: if slot.material is not None: mat = slot.material if mat not in material_object_associations: - # materials.append(mat) material_object_associations[mat] = [] material_object_associations[mat].append(obj) else: # an object without materials should not be selected when running the bake operator - print("Object " + obj.name + " does not have material slots, removing from set") - obj.select_set(False) + print("Object " + obj.name + " does not have material slots, removing from list of objects that will be unwrapped.") + # obj.select_set(False) mesh_objs.remove(obj) print(material_object_associations.items()) @@ -839,7 +839,7 @@ def execute(self, context): return {'FINISHED'} def invoke(self, context, event): - # needed to get the dialoge with the intensity + # needed to get the dialog with the intensity return context.window_manager.invoke_props_dialog(self) def setup_moz_lightmap_nodes(self, node_tree): From a91a5b43f363b1558e9f551c34f62f05eb42f991 Mon Sep 17 00:00:00 2001 From: Gottfried Hofmann Date: Tue, 8 Oct 2024 14:10:26 +0200 Subject: [PATCH 16/17] Fix: Create selection sets materialwise to omit overwriting already baked areas. --- addons/io_hubs_addon/components/operators.py | 28 ++++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/addons/io_hubs_addon/components/operators.py b/addons/io_hubs_addon/components/operators.py index 8634447c..e5125a6e 100644 --- a/addons/io_hubs_addon/components/operators.py +++ b/addons/io_hubs_addon/components/operators.py @@ -688,16 +688,21 @@ class BakeLightmaps(Operator): description="The number of samples to use for baking. Higher values reduce noise but take longer.") def create_uv_layouts(self, context, mesh_objs): + # Sometimes the list can be empty, in that case do not unwrap but return directly + if len(mesh_objs) == 0: + return {'FINISHED'} # set up UV layer structure. The first layer has to be UV0, the second one LIGHTMAP_LAYER_NAME for the lightmap. for obj in mesh_objs: obj_uv_layers = obj.data.uv_layers # Check whether there are any UV layers and if not, create the two that are required. if len(obj_uv_layers) == 0: + print("Creating completely new UV layer setup for object " + str(obj.name)) obj_uv_layers.new(name='UV0') obj_uv_layers.new(name=LIGHTMAP_LAYER_NAME) # In case there is only one UV layer create a second one named LIGHTMAP_LAYER_NAME for the lightmap. if len(obj_uv_layers) == 1: + print("Creating lightmap UV layer for object " + str(obj.name)) obj_uv_layers.new(name=LIGHTMAP_LAYER_NAME) # Check if object has a second UV layer. If it is named LIGHTMAP_LAYER_NAME, assume it is used for the lightmap. # Otherwise add a new UV layer LIGHTMAP_LAYER_NAME and place it second in the slot list. @@ -756,21 +761,22 @@ def execute(self, context): material_object_associations[mat] = [] material_object_associations[mat].append(obj) else: - # an object without materials should not be selected when running the bake operator print("Object " + obj.name + " does not have material slots, removing from list of objects that will be unwrapped.") - # obj.select_set(False) mesh_objs.remove(obj) - print(material_object_associations.items()) # Set up the UV layer structure and auto-unwrap optimized for lightmaps - visited_objects = set() + visited_materials = set() for mat, obj_list in material_object_associations.items(): - for ob in visited_objects: - if ob in obj_list: - obj_list.remove(ob) - self.create_uv_layouts(context, obj_list) - for ob in obj_list: - visited_objects.add(ob) + objs_to_uv_unwrap = [] + if mat not in visited_materials: + # Several objects can share the same material so bundle them all + for ob in obj_list: + for slot in ob.material_slots: + if slot.material is not None: + objs_to_uv_unwrap.extend(material_object_associations[slot.material]) + visited_materials.add(slot.material) + print("Objects to UV unwrap: " + str(objs_to_uv_unwrap)) + self.create_uv_layouts(context, objs_to_uv_unwrap) # Check for the required nodes and set them up if not present lightmap_texture_nodes = [] @@ -860,7 +866,7 @@ def setup_moz_lightmap_nodes(self, node_tree): lightmap_texture_node.image.colorspace_settings.name = "Linear Rec.709" UVmap_node = mat_nodes.new(type="ShaderNodeUVMap") - UVmap_node.uv_map = "UV1" + UVmap_node.uv_map = LIGHTMAP_LAYER_NAME UVmap_node.location[0] -= 500 node_tree.links.new(UVmap_node.outputs['UV'], lightmap_texture_node.inputs['Vector']) From 9a3f6f459a6d288108cdebaf9fcbdfefb5fa92b9 Mon Sep 17 00:00:00 2001 From: Gottfried Hofmann Date: Tue, 8 Oct 2024 14:12:23 +0200 Subject: [PATCH 17/17] Microfix: Remove TODO --- addons/io_hubs_addon/components/operators.py | 1 - 1 file changed, 1 deletion(-) diff --git a/addons/io_hubs_addon/components/operators.py b/addons/io_hubs_addon/components/operators.py index e5125a6e..3ee87e41 100644 --- a/addons/io_hubs_addon/components/operators.py +++ b/addons/io_hubs_addon/components/operators.py @@ -753,7 +753,6 @@ def execute(self, context): # Iterate over a copy of mesh_objs because we are modifying it further down for obj in list(mesh_objs): if len(obj.material_slots) >= 1: - # TODO: Make more efficient for slot in obj.material_slots: if slot.material is not None: mat = slot.material