From 7893d138acee0815c0733113b99f1f60bdb21f3b Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Wed, 20 Oct 2021 14:54:33 +0100 Subject: [PATCH 01/17] Added "Hubs" panel to Render with a utility function for lightmap preparation --- lightmaps.py | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++ operators.py | 12 ++++++++ panels.py | 15 ++++++++++ 3 files changed, 110 insertions(+) create mode 100644 lightmaps.py diff --git a/lightmaps.py b/lightmaps.py new file mode 100644 index 00000000..f11afa33 --- /dev/null +++ b/lightmaps.py @@ -0,0 +1,83 @@ +import bpy + +# Find and select the image texture associated with a MOZ_lightmap settings + +def selectImageTexture(lightmapNode, material): + # Search for the parent image texture + imageTexture = None + for inputs in lightmapNode.inputs: + for links in inputs.links: + imageTexture = links.from_node + if imageTexture: + # Unselect all nodes so there will be only one remaining selection + for n in material.node_tree.nodes: + n.select = False + imageTexture.select = True + print(f" - selected image texture '{imageTexture.name}' ({imageTexture.label})") + else: + print(" ! no image texture found") + return imageTexture + +# Find the UV map associated with an image texture + +def findUvMap(imageTexture): + # Search for the parent UV Map + for inputs in imageTexture.inputs: + for links in inputs.links: + # Is this node a MOZ lightmap node? + if links.from_node.bl_idname == "ShaderNodeUVMap": + return links.from_node.uv_map + else: + print(f" ! unexpected node type '{links.from_node.bl_idname}' instead of 'ShaderNodeUVMap'") + return None + +# Select the object that holds this mesh + +def selectObjectFromMesh(mesh): + for o in bpy.context.scene.objects: + if o.type == "MESH": + # if this mesh exists in the dict + if o.data.name == mesh.name: + o.select_set(True) + return + +# Select the UV input to the image texture for every mesh that uses the given material + +def selectUvMaps(imageTexture, material): + # Select the lightmap UVs on the associated mesh + uvMap = findUvMap(imageTexture) + if uvMap: + print(f" - found UV Map Node '{uvMap}'") + # Search for meshes that use this material (can't find a parent property so this will have to do) + for mesh in bpy.data.meshes: + if mesh.materials.find(material.name) != -1: + print(f" - found mesh '{mesh.name}' that uses this material") + selectObjectFromMesh(mesh) + if mesh.uv_layers.find(uvMap) != -1: + uvLayer = mesh.uv_layers[uvMap] + mesh.uv_layers.active = uvLayer + print(f" - UV layer '{uvMap}' is now active on '{mesh.name}'") + else: + print(f" ! failed to find UV layer '{uvMap}' in '{mesh.name}'") + else: + print(f" ! no UV map found") + +# Selects all MOZ lightmap related components ready for baking + +def selectLightmapComponents(): + # Deslect all objects to start with (only lightmapped objects should be selected) + bpy.ops.object.select_all(action='DESELECT') + # For every material + for material in bpy.data.materials: + if material.node_tree: + # For every node in the material graph + for shadernode in material.node_tree.nodes: + # Is this node a MOZ lightmap node? + if shadernode.bl_idname == "moz_lightmap.node": + print(f"found '{shadernode.name}' ({shadernode.label}) on material '{material.name}'") + # Select just the image texture node and activate it so it will be targetted by the bake + imageTexture = selectImageTexture(shadernode, material) + material.node_tree.nodes.active = imageTexture + if imageTexture: + selectUvMaps(imageTexture, material) + \ No newline at end of file diff --git a/operators.py b/operators.py index d808041a..8b0d5ed2 100644 --- a/operators.py +++ b/operators.py @@ -3,6 +3,7 @@ from bpy.types import Operator from . import components from functools import reduce +from . import lightmaps class AddHubsComponent(Operator): bl_idname = "wm.add_hubs_component" @@ -224,6 +225,15 @@ def execute(self, context): return {'FINISHED'} +class PrepareHubsLightmaps(Operator): + bl_idname = "wm.prepare_hubs_lightmaps" + bl_label = "Prepare Lightmaps" + bl_description = "Select all MOZ_lightmap input textures and the matching mesh UV layer" + + def execute(self, context): + lightmaps.selectLightmapComponents() + return {'FINISHED'} + def register(): bpy.utils.register_class(AddHubsComponent) bpy.utils.register_class(RemoveHubsComponent) @@ -232,6 +242,7 @@ def register(): bpy.utils.register_class(RemoveHubsComponentItem) bpy.utils.register_class(ReloadHubsConfig) bpy.utils.register_class(ResetHubsComponentNames) + bpy.utils.register_class(PrepareHubsLightmaps) def unregister(): bpy.utils.unregister_class(AddHubsComponent) @@ -241,3 +252,4 @@ def unregister(): bpy.utils.unregister_class(RemoveHubsComponentItem) bpy.utils.unregister_class(ReloadHubsConfig) bpy.utils.unregister_class(ResetHubsComponentNames) + bpy.utils.unregister_class(PrepareHubsLightmaps) diff --git a/panels.py b/panels.py index 58249f34..042f2489 100644 --- a/panels.py +++ b/panels.py @@ -3,6 +3,19 @@ from bpy.types import Panel from bpy.props import StringProperty from . import components +from . import operators + +class HubsRenderPanel(Panel): + bl_label = 'Hubs' + bl_idname = "RENDER_PT_hubs" + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = 'render' + + def draw(self, context): + layout = self.layout + row = layout.row() + row.operator(operators.PrepareHubsLightmaps.bl_idname) class HubsScenePanel(Panel): bl_label = 'Hubs' @@ -230,12 +243,14 @@ def draw_array_property(context, col, obj, target, path, property_name, property add_operator.path = property_path def register(): + bpy.utils.register_class(HubsRenderPanel) bpy.utils.register_class(HubsScenePanel) bpy.utils.register_class(HubsObjectPanel) bpy.utils.register_class(HubsMaterialPanel) bpy.utils.register_class(HubsBonePanel) def unregister(): + bpy.utils.unregister_class(HubsRenderPanel) bpy.utils.unregister_class(HubsScenePanel) bpy.utils.unregister_class(HubsObjectPanel) bpy.utils.unregister_class(HubsMaterialPanel) From 8612ec8bc20e15f789433cc61bae834503559025 Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Wed, 20 Oct 2021 15:41:00 +0100 Subject: [PATCH 02/17] Better error feedback to the user --- lightmaps.py | 19 +++++++++++-------- operators.py | 8 ++++++-- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/lightmaps.py b/lightmaps.py index f11afa33..29f33dcb 100644 --- a/lightmaps.py +++ b/lightmaps.py @@ -15,12 +15,12 @@ def selectImageTexture(lightmapNode, material): imageTexture.select = True print(f" - selected image texture '{imageTexture.name}' ({imageTexture.label})") else: - print(" ! no image texture found") + raise ValueError(f"No image texture found on material '{material.name}'") return imageTexture # Find the UV map associated with an image texture -def findUvMap(imageTexture): +def findUvMap(imageTexture, material): # Search for the parent UV Map for inputs in imageTexture.inputs: for links in inputs.links: @@ -28,7 +28,7 @@ def findUvMap(imageTexture): if links.from_node.bl_idname == "ShaderNodeUVMap": return links.from_node.uv_map else: - print(f" ! unexpected node type '{links.from_node.bl_idname}' instead of 'ShaderNodeUVMap'") + raise ValueError(f"Unexpected node type '{links.from_node.bl_idname}' instead of 'ShaderNodeUVMap' on material '{material.name}'") return None # Select the object that holds this mesh @@ -45,7 +45,7 @@ def selectObjectFromMesh(mesh): def selectUvMaps(imageTexture, material): # Select the lightmap UVs on the associated mesh - uvMap = findUvMap(imageTexture) + uvMap = findUvMap(imageTexture, material) if uvMap: print(f" - found UV Map Node '{uvMap}'") # Search for meshes that use this material (can't find a parent property so this will have to do) @@ -58,9 +58,9 @@ def selectUvMaps(imageTexture, material): mesh.uv_layers.active = uvLayer print(f" - UV layer '{uvMap}' is now active on '{mesh.name}'") else: - print(f" ! failed to find UV layer '{uvMap}' in '{mesh.name}'") + raise ValueError(f"Failed to find UV layer '{uvMap}' for mesh '{mesh.name}' using material '{material.name}'") else: - print(f" ! no UV map found") + raise ValueError(f"No UV map found for image texture '{imageTexture.name}' with image '{imageTexture.image.name}' in material '{material.name}'") # Selects all MOZ lightmap related components ready for baking @@ -75,9 +75,12 @@ def selectLightmapComponents(): # Is this node a MOZ lightmap node? if shadernode.bl_idname == "moz_lightmap.node": print(f"found '{shadernode.name}' ({shadernode.label}) on material '{material.name}'") - # Select just the image texture node and activate it so it will be targetted by the bake - imageTexture = selectImageTexture(shadernode, material) + # Select and activate just the image texture node so it will be targetted by the bake + imageTexture = selectImageTexture(shadernode, material) material.node_tree.nodes.active = imageTexture if imageTexture: + # Check image texture actually has an image + if imageTexture.image == None: + raise ValueError(f"No image found on image texture '{imageTexture.name}' ('{imageTexture.label}') in material '{material.name}'") selectUvMaps(imageTexture, material) \ No newline at end of file diff --git a/operators.py b/operators.py index 8b0d5ed2..c4744b84 100644 --- a/operators.py +++ b/operators.py @@ -228,10 +228,14 @@ def execute(self, context): class PrepareHubsLightmaps(Operator): bl_idname = "wm.prepare_hubs_lightmaps" bl_label = "Prepare Lightmaps" - bl_description = "Select all MOZ_lightmap input textures and the matching mesh UV layer" + bl_description = "Select all MOZ_lightmap input textures, the matching mesh UV layer, and the objects ready for baking" def execute(self, context): - lightmaps.selectLightmapComponents() + try: + lightmaps.selectLightmapComponents() + self.report({'INFO'}, "Lightmaps prepared and ready to bake") + except Exception as e: + self.report({'ERROR'}, str(e)) return {'FINISHED'} def register(): From 29eaac67a87b4c5e35def4bf55063f711d900c37 Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Wed, 20 Oct 2021 16:20:49 +0100 Subject: [PATCH 03/17] Fixed error when in EDIT mode --- lightmaps.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lightmaps.py b/lightmaps.py index 29f33dcb..a170e16a 100644 --- a/lightmaps.py +++ b/lightmaps.py @@ -64,8 +64,9 @@ def selectUvMaps(imageTexture, material): # Selects all MOZ lightmap related components ready for baking -def selectLightmapComponents(): - # Deslect all objects to start with (only lightmapped objects should be selected) +def selectLightmapComponents(): + # Deslect all objects to start with (can only be done in object mode) + bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.select_all(action='DESELECT') # For every material for material in bpy.data.materials: From 33ef09e33de096e3ab1b50d16283c15553db609e Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Thu, 21 Oct 2021 09:28:37 +0100 Subject: [PATCH 04/17] Fix selection where objects are hidden --- lightmaps.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lightmaps.py b/lightmaps.py index a170e16a..cf01d502 100644 --- a/lightmaps.py +++ b/lightmaps.py @@ -36,8 +36,9 @@ def findUvMap(imageTexture, material): def selectObjectFromMesh(mesh): for o in bpy.context.scene.objects: if o.type == "MESH": - # if this mesh exists in the dict if o.data.name == mesh.name: + # Objects cannot be selected if they are hidden + o.hide_set(False) o.select_set(True) return From 32402ad2bd57c8117f55a31a31cdbc430c09cbdc Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Thu, 21 Oct 2021 09:32:34 +0100 Subject: [PATCH 05/17] select_all would fail sometimes depending on the UI context. This method is more robust. --- lightmaps.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lightmaps.py b/lightmaps.py index cf01d502..ff29dd8f 100644 --- a/lightmaps.py +++ b/lightmaps.py @@ -66,9 +66,9 @@ def selectUvMaps(imageTexture, material): # Selects all MOZ lightmap related components ready for baking def selectLightmapComponents(): - # Deslect all objects to start with (can only be done in object mode) - bpy.ops.object.mode_set(mode='OBJECT') - bpy.ops.object.select_all(action='DESELECT') + # Deslect all objects to start with (bake objects will be selected) + for o in bpy.context.scene.objects: + o.select_set(False) # For every material for material in bpy.data.materials: if material.node_tree: From a7b4845845d4629d44beca7a3591bd4ba4d8dea8 Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Thu, 21 Oct 2021 12:09:38 +0100 Subject: [PATCH 06/17] Support for multiple different lightmap target images --- lightmaps.py | 67 ++++++++++++++++++++++++++++++++-------------------- operators.py | 6 +++-- panels.py | 10 +++++++- 3 files changed, 54 insertions(+), 29 deletions(-) diff --git a/lightmaps.py b/lightmaps.py index ff29dd8f..66a67d8f 100644 --- a/lightmaps.py +++ b/lightmaps.py @@ -1,22 +1,12 @@ import bpy # Find and select the image texture associated with a MOZ_lightmap settings - -def selectImageTexture(lightmapNode, material): - # Search for the parent image texture +def findImageTexture(lightmapNode): imageTexture = None for inputs in lightmapNode.inputs: for links in inputs.links: - imageTexture = links.from_node - if imageTexture: - # Unselect all nodes so there will be only one remaining selection - for n in material.node_tree.nodes: - n.select = False - imageTexture.select = True - print(f" - selected image texture '{imageTexture.name}' ({imageTexture.label})") - else: - raise ValueError(f"No image texture found on material '{material.name}'") - return imageTexture + return links.from_node + return None # Find the UV map associated with an image texture @@ -32,7 +22,6 @@ def findUvMap(imageTexture, material): return None # Select the object that holds this mesh - def selectObjectFromMesh(mesh): for o in bpy.context.scene.objects: if o.type == "MESH": @@ -40,32 +29,31 @@ def selectObjectFromMesh(mesh): # Objects cannot be selected if they are hidden o.hide_set(False) o.select_set(True) + print(f" --- selected object '{o.name}' because it uses mesh '{mesh.name}'") return # Select the UV input to the image texture for every mesh that uses the given material - def selectUvMaps(imageTexture, material): # Select the lightmap UVs on the associated mesh uvMap = findUvMap(imageTexture, material) if uvMap: - print(f" - found UV Map Node '{uvMap}'") + print(f" -- found UV Map Node '{uvMap}'") # Search for meshes that use this material (can't find a parent property so this will have to do) for mesh in bpy.data.meshes: if mesh.materials.find(material.name) != -1: - print(f" - found mesh '{mesh.name}' that uses this material") + print(f" -- found mesh '{mesh.name}' that uses this material") selectObjectFromMesh(mesh) if mesh.uv_layers.find(uvMap) != -1: uvLayer = mesh.uv_layers[uvMap] mesh.uv_layers.active = uvLayer - print(f" - UV layer '{uvMap}' is now active on '{mesh.name}'") + print(f" --- UV layer '{uvMap}' is now active on '{mesh.name}'") else: raise ValueError(f"Failed to find UV layer '{uvMap}' for mesh '{mesh.name}' using material '{material.name}'") else: raise ValueError(f"No UV map found for image texture '{imageTexture.name}' with image '{imageTexture.image.name}' in material '{material.name}'") # Selects all MOZ lightmap related components ready for baking - -def selectLightmapComponents(): +def selectLightmapComponents(targetName): # Deslect all objects to start with (bake objects will be selected) for o in bpy.context.scene.objects: o.select_set(False) @@ -76,13 +64,40 @@ def selectLightmapComponents(): for shadernode in material.node_tree.nodes: # Is this node a MOZ lightmap node? if shadernode.bl_idname == "moz_lightmap.node": - print(f"found '{shadernode.name}' ({shadernode.label}) on material '{material.name}'") - # Select and activate just the image texture node so it will be targetted by the bake - imageTexture = selectImageTexture(shadernode, material) - material.node_tree.nodes.active = imageTexture + print(f"found '{shadernode.name}' ({shadernode.label}) on material '{material.name}'") + imageTexture = findImageTexture(shadernode) if imageTexture: # Check image texture actually has an image if imageTexture.image == None: raise ValueError(f"No image found on image texture '{imageTexture.name}' ('{imageTexture.label}') in material '{material.name}'") - selectUvMaps(imageTexture, material) - \ No newline at end of file + # Is this lightmap texture image being targetted? + if targetName == "" or targetName == imageTexture.image.name: + # Unselect all nodes so there will be only one remaining selection + for n in material.node_tree.nodes: + n.select = False + # Select and activate the image texture node so it will be targetted by the bake + imageTexture.select = True + material.node_tree.nodes.active = imageTexture + print(f" - selected image texture '{imageTexture.name}' ({imageTexture.label})") + + selectUvMaps(imageTexture, material) + else: + print(f"Ignoring image texture '{imageTexture.name}' because it uses image '{imageTexture.image.name}' and the target is '{targetName}'") + else: + raise ValueError(f"No image texture found on material '{material.name}'") + +# List all the lightmap textures images +def listLightmapImages(): + result = set() + # For every material + for material in bpy.data.materials: + if material.node_tree: + # For every node in the material graph + for shadernode in material.node_tree.nodes: + # Is this node a MOZ lightmap node? + if shadernode.bl_idname == "moz_lightmap.node": + imageTexture = findImageTexture(shadernode) + if imageTexture: + if imageTexture.image: + result.add(imageTexture.image) + return result diff --git a/operators.py b/operators.py index c4744b84..792aba0d 100644 --- a/operators.py +++ b/operators.py @@ -227,12 +227,14 @@ def execute(self, context): class PrepareHubsLightmaps(Operator): bl_idname = "wm.prepare_hubs_lightmaps" - bl_label = "Prepare Lightmaps" + bl_label = "Prepare all Lightmaps" bl_description = "Select all MOZ_lightmap input textures, the matching mesh UV layer, and the objects ready for baking" + target: StringProperty(name="target") + def execute(self, context): try: - lightmaps.selectLightmapComponents() + lightmaps.selectLightmapComponents(self.target) self.report({'INFO'}, "Lightmaps prepared and ready to bake") except Exception as e: self.report({'ERROR'}, str(e)) diff --git a/panels.py b/panels.py index 042f2489..2ec102d7 100644 --- a/panels.py +++ b/panels.py @@ -4,6 +4,7 @@ from bpy.props import StringProperty from . import components from . import operators +from . import lightmaps class HubsRenderPanel(Panel): bl_label = 'Hubs' @@ -15,7 +16,14 @@ class HubsRenderPanel(Panel): def draw(self, context): layout = self.layout row = layout.row() - row.operator(operators.PrepareHubsLightmaps.bl_idname) + row.operator(operators.PrepareHubsLightmaps.bl_idname).target = "" + lightmapImages = lightmaps.listLightmapImages() + # Is the more than 1 lightmap texture? + if len(lightmapImages) > 1: + for lightmapImage in lightmapImages: + row = layout.row() + row.operator(operators.PrepareHubsLightmaps.bl_idname, text=f"Prepare '{lightmapImage.name}'").target = lightmapImage.name + class HubsScenePanel(Panel): bl_label = 'Hubs' From c7bd3c735bc8c1c7e81312543adcc365ff046464 Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Thu, 21 Oct 2021 14:21:36 +0100 Subject: [PATCH 07/17] Mesh face selection for UV packing --- lightmaps.py | 30 ++++++++++++++++++++++++++---- operators.py | 2 +- panels.py | 2 +- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/lightmaps.py b/lightmaps.py index 66a67d8f..ccb56756 100644 --- a/lightmaps.py +++ b/lightmaps.py @@ -1,4 +1,5 @@ import bpy +import bmesh # Find and select the image texture associated with a MOZ_lightmap settings def findImageTexture(lightmapNode): @@ -22,7 +23,7 @@ def findUvMap(imageTexture, material): return None # Select the object that holds this mesh -def selectObjectFromMesh(mesh): +def selectObjectFromMesh(mesh, material): for o in bpy.context.scene.objects: if o.type == "MESH": if o.data.name == mesh.name: @@ -30,7 +31,17 @@ def selectObjectFromMesh(mesh): o.hide_set(False) o.select_set(True) print(f" --- selected object '{o.name}' because it uses mesh '{mesh.name}'") - return + # Select the faces that have been assigned to this material (important for UV packing for lightmaps) + materialSlotIndex = o.material_slots.find(material.name) + if materialSlotIndex < 0: + raise ValueError(f"Failed to find a slot with material '{material.name}' in '{mesh.name}' attached to object '{o.name}'") + bm = bmesh.new() + bm.from_mesh(o.data) + for f in bm.faces: + if f.material_index == materialSlotIndex: + f.select = True + bm.to_mesh(o.data) + bm.free() # Select the UV input to the image texture for every mesh that uses the given material def selectUvMaps(imageTexture, material): @@ -42,7 +53,7 @@ def selectUvMaps(imageTexture, material): for mesh in bpy.data.meshes: if mesh.materials.find(material.name) != -1: print(f" -- found mesh '{mesh.name}' that uses this material") - selectObjectFromMesh(mesh) + selectObjectFromMesh(mesh, material) if mesh.uv_layers.find(uvMap) != -1: uvLayer = mesh.uv_layers[uvMap] mesh.uv_layers.active = uvLayer @@ -54,9 +65,20 @@ def selectUvMaps(imageTexture, material): # Selects all MOZ lightmap related components ready for baking def selectLightmapComponents(targetName): - # Deslect all objects to start with (bake objects will be selected) + # Force UI into OBJECT mode so scripts can manipulate meshes + bpy.ops.object.mode_set(mode='OBJECT') + # Deslect all objects to start with (bake objects will then be selected) for o in bpy.context.scene.objects: o.select_set(False) + # Deselect and show all mesh faces (targetted faces will then be selected) + if o.type == "MESH": + bm = bmesh.new() + bm.from_mesh(o.data) + for f in bm.faces: + f.hide = False + f.select = False + bm.to_mesh(o.data) + bm.free() # For every material for material in bpy.data.materials: if material.node_tree: diff --git a/operators.py b/operators.py index 792aba0d..1529215e 100644 --- a/operators.py +++ b/operators.py @@ -227,7 +227,7 @@ def execute(self, context): class PrepareHubsLightmaps(Operator): bl_idname = "wm.prepare_hubs_lightmaps" - bl_label = "Prepare all Lightmaps" + bl_label = "Prepare all lightmaps for baking" bl_description = "Select all MOZ_lightmap input textures, the matching mesh UV layer, and the objects ready for baking" target: StringProperty(name="target") diff --git a/panels.py b/panels.py index 2ec102d7..c7f3321d 100644 --- a/panels.py +++ b/panels.py @@ -22,7 +22,7 @@ def draw(self, context): if len(lightmapImages) > 1: for lightmapImage in lightmapImages: row = layout.row() - row.operator(operators.PrepareHubsLightmaps.bl_idname, text=f"Prepare '{lightmapImage.name}'").target = lightmapImage.name + row.operator(operators.PrepareHubsLightmaps.bl_idname, text=f"Prepare '{lightmapImage.name}' for packing").target = lightmapImage.name class HubsScenePanel(Panel): From 35ab016138919da7f16732303c3e7a97af2f3cdb Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Thu, 21 Oct 2021 15:22:21 +0100 Subject: [PATCH 08/17] Disable all lightmap image textures that are not targeted in an attempt to stop them being baked (it doesn't work for reasons unknown) --- lightmaps.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lightmaps.py b/lightmaps.py index ccb56756..5e79db3d 100644 --- a/lightmaps.py +++ b/lightmaps.py @@ -92,11 +92,12 @@ def selectLightmapComponents(targetName): # Check image texture actually has an image if imageTexture.image == None: raise ValueError(f"No image found on image texture '{imageTexture.name}' ('{imageTexture.label}') in material '{material.name}'") + # Deactivate and unselect all nodes + material.node_tree.nodes.active = None + for n in material.node_tree.nodes: + n.select = False # Is this lightmap texture image being targetted? if targetName == "" or targetName == imageTexture.image.name: - # Unselect all nodes so there will be only one remaining selection - for n in material.node_tree.nodes: - n.select = False # Select and activate the image texture node so it will be targetted by the bake imageTexture.select = True material.node_tree.nodes.active = imageTexture From f96943d93927d9901dfe4e27d807685adfadc24a Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Thu, 21 Oct 2021 16:40:11 +0100 Subject: [PATCH 09/17] Shader graph nodes can be active even if the UI doesn't show it and they will contribute to the bake if their parent object is selected --- lightmaps.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lightmaps.py b/lightmaps.py index 5e79db3d..5a270f0e 100644 --- a/lightmaps.py +++ b/lightmaps.py @@ -82,6 +82,11 @@ def selectLightmapComponents(targetName): # For every material for material in bpy.data.materials: if material.node_tree: + # Deactivate and unselect all nodes in the shader graph + # - they can be active even if the UI doesn't show it and they will be baked + material.node_tree.nodes.active = None + for n in material.node_tree.nodes: + n.select = False # For every node in the material graph for shadernode in material.node_tree.nodes: # Is this node a MOZ lightmap node? @@ -92,10 +97,6 @@ def selectLightmapComponents(targetName): # Check image texture actually has an image if imageTexture.image == None: raise ValueError(f"No image found on image texture '{imageTexture.name}' ('{imageTexture.label}') in material '{material.name}'") - # Deactivate and unselect all nodes - material.node_tree.nodes.active = None - for n in material.node_tree.nodes: - n.select = False # Is this lightmap texture image being targetted? if targetName == "" or targetName == imageTexture.image.name: # Select and activate the image texture node so it will be targetted by the bake @@ -105,7 +106,7 @@ def selectLightmapComponents(targetName): selectUvMaps(imageTexture, material) else: - print(f"Ignoring image texture '{imageTexture.name}' because it uses image '{imageTexture.image.name}' and the target is '{targetName}'") + print(f" - ignoring image texture '{imageTexture.name}' because it uses image '{imageTexture.image.name}' and the target is '{targetName}'") else: raise ValueError(f"No image texture found on material '{material.name}'") From 0c3d49a543805ba60a038055087d863251c64112 Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Fri, 22 Oct 2021 10:12:21 +0100 Subject: [PATCH 10/17] Refactor code slightly for clarity --- lightmaps.py | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/lightmaps.py b/lightmaps.py index 5a270f0e..1f75f722 100644 --- a/lightmaps.py +++ b/lightmaps.py @@ -22,26 +22,29 @@ def findUvMap(imageTexture, material): raise ValueError(f"Unexpected node type '{links.from_node.bl_idname}' instead of 'ShaderNodeUVMap' on material '{material.name}'") return None +# Selects all the faces of the mesh that have been assigned the given material (important for UV packing for lightmaps) +def selectMeshFacesFromMaterial(object, mesh, material): + materialSlotIndex = object.material_slots.find(material.name) + if materialSlotIndex < 0: + raise ValueError(f"Failed to find a slot with material '{material.name}' in '{mesh.name}' attached to object '{object.name}'") + bm = bmesh.new() + bm.from_mesh(object.data) + for f in bm.faces: + if f.material_index == materialSlotIndex: + f.select = True + bm.to_mesh(object.data) + bm.free() + # Select the object that holds this mesh def selectObjectFromMesh(mesh, material): - for o in bpy.context.scene.objects: - if o.type == "MESH": - if o.data.name == mesh.name: + for object in bpy.context.scene.objects: + if object.type == "MESH": + if object.data.name == mesh.name: # Objects cannot be selected if they are hidden - o.hide_set(False) - o.select_set(True) - print(f" --- selected object '{o.name}' because it uses mesh '{mesh.name}'") - # Select the faces that have been assigned to this material (important for UV packing for lightmaps) - materialSlotIndex = o.material_slots.find(material.name) - if materialSlotIndex < 0: - raise ValueError(f"Failed to find a slot with material '{material.name}' in '{mesh.name}' attached to object '{o.name}'") - bm = bmesh.new() - bm.from_mesh(o.data) - for f in bm.faces: - if f.material_index == materialSlotIndex: - f.select = True - bm.to_mesh(o.data) - bm.free() + object.hide_set(False) + object.select_set(True) + print(f" --- selected object '{object.name}' because it uses mesh '{mesh.name}'") + selectMeshFacesFromMaterial(object, mesh, material) # Select the UV input to the image texture for every mesh that uses the given material def selectUvMaps(imageTexture, material): From 856307cc27f9fc000a8862123667315912e0ea31 Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Fri, 22 Oct 2021 10:12:45 +0100 Subject: [PATCH 11/17] Sort lightmaps for clearer layout --- panels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panels.py b/panels.py index c7f3321d..092d41ae 100644 --- a/panels.py +++ b/panels.py @@ -17,7 +17,7 @@ def draw(self, context): layout = self.layout row = layout.row() row.operator(operators.PrepareHubsLightmaps.bl_idname).target = "" - lightmapImages = lightmaps.listLightmapImages() + lightmapImages = sorted(lightmaps.listLightmapImages(), key=lambda it:it.name) # Is the more than 1 lightmap texture? if len(lightmapImages) > 1: for lightmapImage in lightmapImages: From c303b94dc06bac2a6c0b96dac1b5e002257ccf2d Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Fri, 22 Oct 2021 10:29:44 +0100 Subject: [PATCH 12/17] Check for mixed materials to avoid image corruption --- lightmaps.py | 21 ++++++++++++++++++--- operators.py | 1 + 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/lightmaps.py b/lightmaps.py index 1f75f722..c3b8646e 100644 --- a/lightmaps.py +++ b/lightmaps.py @@ -66,8 +66,23 @@ def selectUvMaps(imageTexture, material): else: raise ValueError(f"No UV map found for image texture '{imageTexture.name}' with image '{imageTexture.image.name}' in material '{material.name}'") +# Check for selected objects with non-lightmapped materials. They might get baked and have corrupted image textures +def assertNoMixedMaterials(): + for object in bpy.context.scene.objects: + if object.select_get(): + hasLightmapNode = False + for materialSlot in object.material_slots: + material = materialSlot.material + for shadernode in material.node_tree.nodes: + if shadernode.bl_idname == "moz_lightmap.node": + hasLightmapNode = True + break + if not hasLightmapNode: + raise ValueError(f"Multi-material object '{object.name}' uses '{materialSlot.name}' with no lightmap. It will be corrupted by baking") + + # Selects all MOZ lightmap related components ready for baking -def selectLightmapComponents(targetName): +def selectLightmapComponents(target): # Force UI into OBJECT mode so scripts can manipulate meshes bpy.ops.object.mode_set(mode='OBJECT') # Deslect all objects to start with (bake objects will then be selected) @@ -101,7 +116,7 @@ def selectLightmapComponents(targetName): if imageTexture.image == None: raise ValueError(f"No image found on image texture '{imageTexture.name}' ('{imageTexture.label}') in material '{material.name}'") # Is this lightmap texture image being targetted? - if targetName == "" or targetName == imageTexture.image.name: + if target == "" or target == imageTexture.image.name: # Select and activate the image texture node so it will be targetted by the bake imageTexture.select = True material.node_tree.nodes.active = imageTexture @@ -109,7 +124,7 @@ def selectLightmapComponents(targetName): selectUvMaps(imageTexture, material) else: - print(f" - ignoring image texture '{imageTexture.name}' because it uses image '{imageTexture.image.name}' and the target is '{targetName}'") + print(f" - ignoring image texture '{imageTexture.name}' because it uses image '{imageTexture.image.name}' and the target is '{target}'") else: raise ValueError(f"No image texture found on material '{material.name}'") diff --git a/operators.py b/operators.py index 1529215e..e09f0f33 100644 --- a/operators.py +++ b/operators.py @@ -235,6 +235,7 @@ class PrepareHubsLightmaps(Operator): def execute(self, context): try: lightmaps.selectLightmapComponents(self.target) + lightmaps.assertNoMixedMaterials() self.report({'INFO'}, "Lightmaps prepared and ready to bake") except Exception as e: self.report({'ERROR'}, str(e)) From 8cbe9a9a02b18f999ed6fe27b4049d2399ecb59c Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Fri, 5 Nov 2021 10:29:20 +0000 Subject: [PATCH 13/17] Decoy textures for supporting mixed material scenes --- lightmaps.py | 64 +++++++++++++++++++++++++++++++++++++++------------- operators.py | 37 ++++++++++++++++++++++++++++-- panels.py | 8 +++++-- 3 files changed, 89 insertions(+), 20 deletions(-) diff --git a/lightmaps.py b/lightmaps.py index c3b8646e..1e309e7e 100644 --- a/lightmaps.py +++ b/lightmaps.py @@ -1,9 +1,11 @@ import bpy import bmesh +# Label for decoy texture +HUBS_DECOY_IMAGE_TEXTURE = "HUBS_DECOY_IMAGE_TEXTURE" + # Find and select the image texture associated with a MOZ_lightmap settings def findImageTexture(lightmapNode): - imageTexture = None for inputs in lightmapNode.inputs: for links in inputs.links: return links.from_node @@ -66,21 +68,6 @@ def selectUvMaps(imageTexture, material): else: raise ValueError(f"No UV map found for image texture '{imageTexture.name}' with image '{imageTexture.image.name}' in material '{material.name}'") -# Check for selected objects with non-lightmapped materials. They might get baked and have corrupted image textures -def assertNoMixedMaterials(): - for object in bpy.context.scene.objects: - if object.select_get(): - hasLightmapNode = False - for materialSlot in object.material_slots: - material = materialSlot.material - for shadernode in material.node_tree.nodes: - if shadernode.bl_idname == "moz_lightmap.node": - hasLightmapNode = True - break - if not hasLightmapNode: - raise ValueError(f"Multi-material object '{object.name}' uses '{materialSlot.name}' with no lightmap. It will be corrupted by baking") - - # Selects all MOZ lightmap related components ready for baking def selectLightmapComponents(target): # Force UI into OBJECT mode so scripts can manipulate meshes @@ -127,6 +114,11 @@ def selectLightmapComponents(target): print(f" - ignoring image texture '{imageTexture.name}' because it uses image '{imageTexture.image.name}' and the target is '{target}'") else: raise ValueError(f"No image texture found on material '{material.name}'") + # Is it a decoy node? + elif shadernode.bl_idname == "ShaderNodeTexImage" and shadernode.label == "HUBS_DECOY_IMAGE_TEXTURE": + # Select and activate the image texture node so it will be targetted by the bake + shadernode.select = True + material.node_tree.nodes.active = shadernode # List all the lightmap textures images def listLightmapImages(): @@ -143,3 +135,43 @@ def listLightmapImages(): if imageTexture.image: result.add(imageTexture.image) return result + +# Check for selected objects with non-lightmapped materials. They might get baked and have corrupted image textures +def assertSelectedObjectsAreSafeToBake(addDecoyRatherThanThrow): + decoyCount = 0 + for object in bpy.context.scene.objects: + if object.select_get(): + for materialSlot in object.material_slots: + material = materialSlot.material + hasLightmapNode = False + hasAtRiskImageTexture = False + hasDecoyImageTexture = False + for shadernode in material.node_tree.nodes: + if shadernode.bl_idname == "moz_lightmap.node": + hasLightmapNode = True + elif shadernode.bl_idname == "ShaderNodeTexImage": + if shadernode.label == HUBS_DECOY_IMAGE_TEXTURE: + hasDecoyImageTexture = True + else: + hasAtRiskImageTexture = True + if hasAtRiskImageTexture and not (hasLightmapNode or hasDecoyImageTexture): + if addDecoyRatherThanThrow: + decoy_image_texture = material.node_tree.nodes.new("ShaderNodeTexImage") + decoy_image_texture.label = HUBS_DECOY_IMAGE_TEXTURE + decoyCount += 1 + else: + raise ValueError(f"Multi-material object '{object.name}' uses '{materialSlot.name}' with no lightmap or decoy texture. It will be corrupted by baking") + return decoyCount + +def removeAllDecoyImageTextures(): + decoyCount = 0 + # For every material + for material in bpy.data.materials: + if material.node_tree: + # For every node in the material graph + for shadernode in material.node_tree.nodes: + # Is this a decoy image? + if shadernode.bl_idname == "ShaderNodeTexImage" and shadernode.label == HUBS_DECOY_IMAGE_TEXTURE: + material.node_tree.nodes.remove(shadernode) + decoyCount += 1 + return decoyCount diff --git a/operators.py b/operators.py index e09f0f33..9eeb4703 100644 --- a/operators.py +++ b/operators.py @@ -227,7 +227,7 @@ def execute(self, context): class PrepareHubsLightmaps(Operator): bl_idname = "wm.prepare_hubs_lightmaps" - bl_label = "Prepare all lightmaps for baking" + bl_label = "Select all lightmap elements for baking" bl_description = "Select all MOZ_lightmap input textures, the matching mesh UV layer, and the objects ready for baking" target: StringProperty(name="target") @@ -235,12 +235,41 @@ class PrepareHubsLightmaps(Operator): def execute(self, context): try: lightmaps.selectLightmapComponents(self.target) - lightmaps.assertNoMixedMaterials() + lightmaps.assertSelectedObjectsAreSafeToBake(False) self.report({'INFO'}, "Lightmaps prepared and ready to bake") except Exception as e: self.report({'ERROR'}, str(e)) return {'FINISHED'} + +class AddDecoyImageTextures(Operator): + bl_idname = "wm.add_decoy_image_textures" + bl_label = "Add to Selected" + bl_description = "Adds decoy image textures to the materials of selected meshes if they are required" + + def execute(self, context): + try: + decoyCount = lightmaps.assertSelectedObjectsAreSafeToBake(True) + self.report({'INFO'}, f"Added {decoyCount} decoy textures") + except Exception as e: + self.report({'ERROR'}, str(e)) + return {'FINISHED'} + +class RemoveDecoyImageTextures(Operator): + bl_idname = "wm.remove_decoy_image_textures" + bl_label = "Remove All" + bl_description = "Remove all decoy image textures from materials regardless of selection" + + def execute(self, context): + try: + decoyCount = lightmaps.removeAllDecoyImageTextures() + self.report({'INFO'}, f"Removed {decoyCount} decoy textures") + except Exception as e: + self.report({'ERROR'}, str(e)) + return {'FINISHED'} + + + def register(): bpy.utils.register_class(AddHubsComponent) bpy.utils.register_class(RemoveHubsComponent) @@ -250,6 +279,8 @@ def register(): bpy.utils.register_class(ReloadHubsConfig) bpy.utils.register_class(ResetHubsComponentNames) bpy.utils.register_class(PrepareHubsLightmaps) + bpy.utils.register_class(AddDecoyImageTextures) + bpy.utils.register_class(RemoveDecoyImageTextures) def unregister(): bpy.utils.unregister_class(AddHubsComponent) @@ -260,3 +291,5 @@ def unregister(): bpy.utils.unregister_class(ReloadHubsConfig) bpy.utils.unregister_class(ResetHubsComponentNames) bpy.utils.unregister_class(PrepareHubsLightmaps) + bpy.utils.unregister_class(AddDecoyImageTextures) + bpy.utils.unregister_class(RemoveDecoyImageTextures) diff --git a/panels.py b/panels.py index 092d41ae..ca3eee3b 100644 --- a/panels.py +++ b/panels.py @@ -18,11 +18,15 @@ def draw(self, context): row = layout.row() row.operator(operators.PrepareHubsLightmaps.bl_idname).target = "" lightmapImages = sorted(lightmaps.listLightmapImages(), key=lambda it:it.name) - # Is the more than 1 lightmap texture? + # Is their more than 1 lightmap texture? if len(lightmapImages) > 1: for lightmapImage in lightmapImages: row = layout.row() - row.operator(operators.PrepareHubsLightmaps.bl_idname, text=f"Prepare '{lightmapImage.name}' for packing").target = lightmapImage.name + row.operator(operators.PrepareHubsLightmaps.bl_idname, text=f"Select '{lightmapImage.name}' elements for packing").target = lightmapImage.name + row = layout.row() + row.label(text="Decoy Textures") + row.operator(operators.AddDecoyImageTextures.bl_idname) + row.operator(operators.RemoveDecoyImageTextures.bl_idname) class HubsScenePanel(Panel): From b545782a5343c325b1f786bed1566b71c9f9a1ad Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Fri, 5 Nov 2021 10:30:45 +0000 Subject: [PATCH 14/17] Exception catch when mode_set fails (context in Blender is mercurial) --- lightmaps.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lightmaps.py b/lightmaps.py index 1e309e7e..e811cc5a 100644 --- a/lightmaps.py +++ b/lightmaps.py @@ -71,7 +71,10 @@ def selectUvMaps(imageTexture, material): # Selects all MOZ lightmap related components ready for baking def selectLightmapComponents(target): # Force UI into OBJECT mode so scripts can manipulate meshes - bpy.ops.object.mode_set(mode='OBJECT') + try: + bpy.ops.object.mode_set(mode='OBJECT') + except Exception as e: + print(f"Failed to enter OBJECT mode (usually non-fatal): {str(e)}") # Deslect all objects to start with (bake objects will then be selected) for o in bpy.context.scene.objects: o.select_set(False) From b551a0dffd2449937991bbb591aa84b675456a00 Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Fri, 5 Nov 2021 10:31:12 +0000 Subject: [PATCH 15/17] Only show the button if there are any lightmaps to process --- panels.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/panels.py b/panels.py index ca3eee3b..6d9f9e48 100644 --- a/panels.py +++ b/panels.py @@ -16,8 +16,10 @@ class HubsRenderPanel(Panel): def draw(self, context): layout = self.layout row = layout.row() - row.operator(operators.PrepareHubsLightmaps.bl_idname).target = "" lightmapImages = sorted(lightmaps.listLightmapImages(), key=lambda it:it.name) + # Only show the panel if there are any lightmaps to prepare + if len(lightmapImages) > 0: + row.operator(operators.PrepareHubsLightmaps.bl_idname).target = "" # Is their more than 1 lightmap texture? if len(lightmapImages) > 1: for lightmapImage in lightmapImages: From 8324827fb052c1d117d04f67e96d2be4596cc929 Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Mon, 10 Jul 2023 16:24:59 +0100 Subject: [PATCH 16/17] Fixed merge with master --- .../io_hubs_addon/components/lightmaps.py | 0 addons/io_hubs_addon/components/operators.py | 50 +++ addons/io_hubs_addon/components/ui.py | 27 ++ operators.py | 295 ------------------ panels.py | 283 ----------------- 5 files changed, 77 insertions(+), 578 deletions(-) rename lightmaps.py => addons/io_hubs_addon/components/lightmaps.py (100%) delete mode 100644 operators.py delete mode 100644 panels.py diff --git a/lightmaps.py b/addons/io_hubs_addon/components/lightmaps.py similarity index 100% rename from lightmaps.py rename to addons/io_hubs_addon/components/lightmaps.py diff --git a/addons/io_hubs_addon/components/operators.py b/addons/io_hubs_addon/components/operators.py index f5d56f8b..b1367050 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 import os +from . import lightmaps class AddHubsComponent(Operator): @@ -624,6 +625,49 @@ def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} +class PrepareHubsLightmaps(Operator): + bl_idname = "wm.prepare_hubs_lightmaps" + bl_label = "Select all lightmap elements for baking" + bl_description = "Select all MOZ_lightmap input textures, the matching mesh UV layer, and the objects ready for baking" + + target: StringProperty(name="target") + + def execute(self, context): + try: + lightmaps.selectLightmapComponents(self.target) + lightmaps.assertSelectedObjectsAreSafeToBake(False) + self.report({'INFO'}, "Lightmaps prepared and ready to bake") + except Exception as e: + self.report({'ERROR'}, str(e)) + return {'FINISHED'} + + +class AddDecoyImageTextures(Operator): + bl_idname = "wm.add_decoy_image_textures" + bl_label = "Add to Selected" + bl_description = "Adds decoy image textures to the materials of selected meshes if they are required" + + def execute(self, context): + try: + decoyCount = lightmaps.assertSelectedObjectsAreSafeToBake(True) + self.report({'INFO'}, f"Added {decoyCount} decoy textures") + except Exception as e: + self.report({'ERROR'}, str(e)) + return {'FINISHED'} + +class RemoveDecoyImageTextures(Operator): + bl_idname = "wm.remove_decoy_image_textures" + bl_label = "Remove All" + bl_description = "Remove all decoy image textures from materials regardless of selection" + + def execute(self, context): + try: + decoyCount = lightmaps.removeAllDecoyImageTextures() + self.report({'INFO'}, f"Removed {decoyCount} decoy textures") + except Exception as e: + self.report({'ERROR'}, str(e)) + return {'FINISHED'} + def register(): bpy.utils.register_class(AddHubsComponent) @@ -636,6 +680,9 @@ def register(): bpy.utils.register_class(ViewReportInInfoEditor) bpy.utils.register_class(CopyHubsComponent) bpy.utils.register_class(OpenImage) + bpy.utils.register_class(PrepareHubsLightmaps) + bpy.utils.register_class(AddDecoyImageTextures) + bpy.utils.register_class(RemoveDecoyImageTextures) bpy.types.WindowManager.hubs_report_scroll_index = IntProperty(default=0, min=0) bpy.types.WindowManager.hubs_report_scroll_percentage = IntProperty( name="Scroll Position", default=0, min=0, max=100, subtype='PERCENTAGE') @@ -654,6 +701,9 @@ def unregister(): bpy.utils.unregister_class(ViewReportInInfoEditor) bpy.utils.unregister_class(CopyHubsComponent) bpy.utils.unregister_class(OpenImage) + bpy.utils.unregister_class(PrepareHubsLightmaps) + bpy.utils.unregister_class(AddDecoyImageTextures) + bpy.utils.unregister_class(RemoveDecoyImageTextures) 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 34e00ca5..cc9a5051 100644 --- a/addons/io_hubs_addon/components/ui.py +++ b/addons/io_hubs_addon/components/ui.py @@ -3,6 +3,8 @@ from .types import PanelType from .components_registry import get_component_by_name, get_components_registry from .utils import get_object_source, dash_to_title, is_linked +from . import operators +from . import lightmaps def draw_component_global(panel, context): @@ -178,6 +180,29 @@ class HubsBonePanel(bpy.types.Panel): def draw(self, context): draw_components_list(self, context) +class HubsRenderPanel(bpy.types.Panel): + bl_label = 'Hubs' + bl_idname = "RENDER_PT_hubs" + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = 'render' + + def draw(self, context): + layout = self.layout + row = layout.row() + lightmapImages = sorted(lightmaps.listLightmapImages(), key=lambda it:it.name) + # Only show the panel if there are any lightmaps to prepare + if len(lightmapImages) > 0: + row.operator(operators.PrepareHubsLightmaps.bl_idname).target = "" + # Is their more than 1 lightmap texture? + if len(lightmapImages) > 1: + for lightmapImage in lightmapImages: + row = layout.row() + row.operator(operators.PrepareHubsLightmaps.bl_idname, text=f"Select '{lightmapImage.name}' elements for packing").target = lightmapImage.name + row = layout.row() + row.label(text="Decoy Textures") + row.operator(operators.AddDecoyImageTextures.bl_idname) + row.operator(operators.RemoveDecoyImageTextures.bl_idname) class TooltipLabel(bpy.types.Operator): bl_idname = "ui.hubs_tooltip_label" @@ -213,6 +238,7 @@ def gizmo_display_popover_addition(self, context): def register(): + bpy.utils.register_class(HubsRenderPanel) bpy.utils.register_class(HubsObjectPanel) bpy.utils.register_class(HubsScenePanel) bpy.utils.register_class(HubsMaterialPanel) @@ -225,6 +251,7 @@ def register(): def unregister(): + bpy.utils.unregister_class(HubsRenderPanel) bpy.utils.unregister_class(HubsObjectPanel) bpy.utils.unregister_class(HubsScenePanel) bpy.utils.unregister_class(HubsMaterialPanel) diff --git a/operators.py b/operators.py deleted file mode 100644 index 9eeb4703..00000000 --- a/operators.py +++ /dev/null @@ -1,295 +0,0 @@ -import bpy -from bpy.props import StringProperty, BoolProperty, IntProperty, EnumProperty, CollectionProperty, PointerProperty -from bpy.types import Operator -from . import components -from functools import reduce -from . import lightmaps - -class AddHubsComponent(Operator): - bl_idname = "wm.add_hubs_component" - bl_label = "Add Hubs Component" - bl_property = "component_name" - - object_source: StringProperty(name="object_source") - component_name: StringProperty(name="component_name") - - def execute(self, context): - if self.component_name == '': - return - - obj = components.get_object_source(context, self.object_source) - - components.add_component( - obj, - self.component_name, - context.scene.hubs_settings.hubs_config, - context.scene.hubs_settings.registered_hubs_components - ) - - context.area.tag_redraw() - return {'FINISHED'} - - def invoke(self, context, event): - object_source = self.object_source - hubs_components = bpy.context.scene.hubs_settings.registered_hubs_components - - def sort_by_category(acc, v): - (component_name, component_class) = v - category = component_class.definition.get("category", "Misc") - acc[category] = acc.get(category, []) - acc[category].append(v) - return acc - - components_by_category = reduce(sort_by_category, hubs_components.items(), {}) - obj = components.get_object_source(context, object_source) - - def draw(self, context): - row = self.layout.row() - for category, cmps in components_by_category.items(): - column = row.column() - column.label(text=category) - for (component_name, component_class) in cmps: - component_display_name = components.dash_to_title(component_name) - if not components.is_object_source_component(object_source, component_class.definition): continue - - if components.has_component(obj, component_name): - column.label(text=component_display_name) - else: - op = column.operator(AddHubsComponent.bl_idname, text = component_display_name, icon='ADD') - op.component_name = component_name - op.object_source = object_source - - bpy.context.window_manager.popup_menu(draw) - - return {'RUNNING_MODAL'} - -class RemoveHubsComponent(Operator): - bl_idname = "wm.remove_hubs_component" - bl_label = "Remove Hubs Component" - - object_source: StringProperty(name="object_source") - component_name: StringProperty(name="component_name") - - def execute(self, context): - if self.component_name == '': - return - obj = components.get_object_source(context, self.object_source) - components.remove_component(obj, self.component_name) - context.area.tag_redraw() - return {'FINISHED'} - -class AddHubsComponentItem(Operator): - bl_idname = "wm.add_hubs_component_item" - bl_label = "Add a new item" - - path: StringProperty(name="path") - - def execute(self, context): - parts = self.path.split(".") - - cur_obj = context - - for part in parts: - try: - index = int(part) - cur_obj = cur_obj[index] - except: - cur_obj = getattr(cur_obj, part) - - cur_obj.add() - - context.area.tag_redraw() - - return{'FINISHED'} - -class CopyHubsComponent(Operator): - bl_idname = "wm.copy_hubs_component" - bl_label = "Copy component from active object" - - component_name: StringProperty(name="component_name") - - def execute(self, context): - src_obj = context.active_object - dest_objs = filter(lambda item: src_obj != item, context.selected_objects) - - hubs_settings = context.scene.hubs_settings - component_class = hubs_settings.registered_hubs_components[self.component_name] - component_class_name = component_class.__name__ - component_definition = hubs_settings.hubs_config['components'][self.component_name] - - if components.has_component(src_obj, self.component_name): - for dest_obj in dest_objs: - if components.has_component(dest_obj, self.component_name): - components.remove_component(dest_obj, self.component_name) - - components.add_component( - dest_obj, - self.component_name, - hubs_settings.hubs_config, - hubs_settings.registered_hubs_components - ) - - src_component = getattr(src_obj, component_class_name) - dest_component = getattr(dest_obj, component_class_name) - - self.copy_type(hubs_settings, src_component, dest_component, component_definition) - - return{'FINISHED'} - - - def copy_type(self, hubs_settings, src_obj, dest_obj, type_definition): - for property_name, property_definition in type_definition['properties'].items(): - self.copy_property(hubs_settings, src_obj, dest_obj, property_name, property_definition) - - def copy_property(self, hubs_settings, src_obj, dest_obj, property_name, property_definition): - property_type = property_definition['type'] - - if property_type == 'collections': - return - - registered_types = hubs_settings.hubs_config['types'] - is_custom_type = property_type in registered_types - - src_property = getattr(src_obj, property_name) - dest_property = getattr(dest_obj, property_name) - - if is_custom_type: - dest_obj[property_name] = self.copy_type(hubs_settings, src_property, dest_property, registered_types[property_type]) - elif property_type == 'array': - self.copy_array_property(hubs_settings, src_property, dest_property, property_definition) - else: - setattr(dest_obj, property_name, src_property) - - def copy_array_property(self, hubs_settings, src_arr, dest_arr, property_definition): - array_type = property_definition['arrayType'] - registered_types = hubs_settings.hubs_config['types'] - type_definition = registered_types[array_type] - - dest_arr.clear() - - for src_item in src_arr: - dest_item = dest_arr.add() - self.copy_type(hubs_settings, src_item, dest_item, type_definition) - - -class RemoveHubsComponentItem(Operator): - bl_idname = "wm.remove_hubs_component_item" - bl_label = "Remove an item" - - path: StringProperty(name="path") - - def execute(self, context): - parts = self.path.split(".") - - index = int(parts.pop()) - - cur_obj = context - - for part in parts: - try: - cur_index = int(part) - cur_obj = cur_obj[cur_index] - except: - cur_obj = getattr(cur_obj, part) - - cur_obj.remove(index) - - context.area.tag_redraw() - - return{'FINISHED'} - -class ReloadHubsConfig(Operator): - bl_idname = "wm.reload_hubs_config" - bl_label = "Reload Hubs Config" - - def execute(self, context): - context.scene.hubs_settings.reload_config() - context.area.tag_redraw() - return {'FINISHED'} - -class ResetHubsComponentNames(Operator): - bl_idname = "wm.reset_hubs_component_names" - bl_label = "Reset Selected Hubs Component Names and Ids" - - def execute(self, context): - for obj in context.selected_objects: - if components.has_component(obj, "kit-piece"): - kit_piece = obj.hubs_component_kit_piece - kit_piece.name = obj.name - kit_piece.id = obj.name - - if components.has_component(obj, "kit-alt-materials"): - alt_materials = obj.hubs_component_kit_alt_materials - alt_materials.name = obj.name - alt_materials.id = obj.name - - return {'FINISHED'} - -class PrepareHubsLightmaps(Operator): - bl_idname = "wm.prepare_hubs_lightmaps" - bl_label = "Select all lightmap elements for baking" - bl_description = "Select all MOZ_lightmap input textures, the matching mesh UV layer, and the objects ready for baking" - - target: StringProperty(name="target") - - def execute(self, context): - try: - lightmaps.selectLightmapComponents(self.target) - lightmaps.assertSelectedObjectsAreSafeToBake(False) - self.report({'INFO'}, "Lightmaps prepared and ready to bake") - except Exception as e: - self.report({'ERROR'}, str(e)) - return {'FINISHED'} - - -class AddDecoyImageTextures(Operator): - bl_idname = "wm.add_decoy_image_textures" - bl_label = "Add to Selected" - bl_description = "Adds decoy image textures to the materials of selected meshes if they are required" - - def execute(self, context): - try: - decoyCount = lightmaps.assertSelectedObjectsAreSafeToBake(True) - self.report({'INFO'}, f"Added {decoyCount} decoy textures") - except Exception as e: - self.report({'ERROR'}, str(e)) - return {'FINISHED'} - -class RemoveDecoyImageTextures(Operator): - bl_idname = "wm.remove_decoy_image_textures" - bl_label = "Remove All" - bl_description = "Remove all decoy image textures from materials regardless of selection" - - def execute(self, context): - try: - decoyCount = lightmaps.removeAllDecoyImageTextures() - self.report({'INFO'}, f"Removed {decoyCount} decoy textures") - except Exception as e: - self.report({'ERROR'}, str(e)) - return {'FINISHED'} - - - -def register(): - bpy.utils.register_class(AddHubsComponent) - bpy.utils.register_class(RemoveHubsComponent) - bpy.utils.register_class(CopyHubsComponent) - bpy.utils.register_class(AddHubsComponentItem) - bpy.utils.register_class(RemoveHubsComponentItem) - bpy.utils.register_class(ReloadHubsConfig) - bpy.utils.register_class(ResetHubsComponentNames) - bpy.utils.register_class(PrepareHubsLightmaps) - bpy.utils.register_class(AddDecoyImageTextures) - bpy.utils.register_class(RemoveDecoyImageTextures) - -def unregister(): - bpy.utils.unregister_class(AddHubsComponent) - bpy.utils.unregister_class(RemoveHubsComponent) - bpy.utils.unregister_class(CopyHubsComponent) - bpy.utils.unregister_class(AddHubsComponentItem) - bpy.utils.unregister_class(RemoveHubsComponentItem) - bpy.utils.unregister_class(ReloadHubsConfig) - bpy.utils.unregister_class(ResetHubsComponentNames) - bpy.utils.unregister_class(PrepareHubsLightmaps) - bpy.utils.unregister_class(AddDecoyImageTextures) - bpy.utils.unregister_class(RemoveDecoyImageTextures) diff --git a/panels.py b/panels.py deleted file mode 100644 index a99f4da2..00000000 --- a/panels.py +++ /dev/null @@ -1,283 +0,0 @@ -import re -import bpy -from bpy.types import Panel -from bpy.props import StringProperty -from . import components -from . import operators -from . import lightmaps - -class HubsRenderPanel(Panel): - bl_label = 'Hubs' - bl_idname = "RENDER_PT_hubs" - bl_space_type = 'PROPERTIES' - bl_region_type = 'WINDOW' - bl_context = 'render' - - def draw(self, context): - layout = self.layout - row = layout.row() - lightmapImages = sorted(lightmaps.listLightmapImages(), key=lambda it:it.name) - # Only show the panel if there are any lightmaps to prepare - if len(lightmapImages) > 0: - row.operator(operators.PrepareHubsLightmaps.bl_idname).target = "" - # Is their more than 1 lightmap texture? - if len(lightmapImages) > 1: - for lightmapImage in lightmapImages: - row = layout.row() - row.operator(operators.PrepareHubsLightmaps.bl_idname, text=f"Select '{lightmapImage.name}' elements for packing").target = lightmapImage.name - row = layout.row() - row.label(text="Decoy Textures") - row.operator(operators.AddDecoyImageTextures.bl_idname) - row.operator(operators.RemoveDecoyImageTextures.bl_idname) - - -class HubsScenePanel(Panel): - bl_label = 'Hubs' - bl_idname = "SCENE_PT_hubs" - bl_space_type = 'PROPERTIES' - bl_region_type = 'WINDOW' - bl_context = 'scene' - - def draw(self, context): - layout = self.layout - - row = layout.row() - row.prop(context.scene.hubs_settings, "config_path", text="Config File") - row.operator("wm.reload_hubs_config", text="", icon="FILE_REFRESH") - - draw_components_list(self, context) - -class HubsObjectPanel(Panel): - bl_label = "Hubs" - bl_idname = "OBJECT_PT_hubs" - bl_space_type = 'PROPERTIES' - bl_region_type = 'WINDOW' - bl_context = "object" - - def draw(self, context): - draw_components_list(self, context) - -class HubsMaterialPanel(Panel): - bl_label = 'Hubs' - bl_idname = "MATERIAL_PT_hubs" - bl_space_type = 'PROPERTIES' - bl_region_type = 'WINDOW' - bl_context = 'material' - - def draw(self, context): - draw_components_list(self, context) - -class HubsBonePanel(Panel): - bl_label = "Hubs" - bl_idname = "BONE_PT_hubs" - bl_space_type = 'PROPERTIES' - bl_region_type = 'WINDOW' - bl_context = "bone" - - def draw(self, context): - draw_components_list(self, context) - -class HubsGLTFExportPanel(bpy.types.Panel): - - bl_space_type = 'FILE_BROWSER' - bl_region_type = 'TOOL_PROPS' - bl_label = "Hubs Components" - bl_parent_id = "GLTF_PT_export_user_extensions" - bl_options = {'DEFAULT_CLOSED'} - - @classmethod - def poll(cls, context): - sfile = context.space_data - operator = sfile.active_operator - return operator.bl_idname == "EXPORT_SCENE_OT_gltf" - - def draw_header(self, context): - props = bpy.context.scene.HubsComponentsExtensionProperties - self.layout.prop(props, 'enabled', text="") - - def draw(self, context): - layout = self.layout - layout.use_property_split = True - layout.use_property_decorate = False # No animation. - - props = bpy.context.scene.HubsComponentsExtensionProperties - layout.active = props.enabled - - box = layout.box() - box.label(text="No options yet") - - - -def draw_components_list(panel, context): - layout = panel.layout - - obj = components.get_object_source(context, panel.bl_context) - - if obj is None: - layout.label(text="No object selected") - return - - hubs_settings = context.scene.hubs_settings - - if hubs_settings.hubs_config is None: - layout.label(text="No hubs config loaded") - return - - add_component_operator = layout.operator( - "wm.add_hubs_component", - text="Add Component", - icon="ADD" - ) - add_component_operator.object_source = panel.bl_context - - for component_item in obj.hubs_component_list.items: - row = layout.row() - draw_component(panel, context, obj, row, component_item) - - layout.separator() - -def draw_component(panel, context, obj, row, component_item): - hubs_settings = context.scene.hubs_settings - - component_name = component_item.name - component_definition = hubs_settings.hubs_config['components'].get(component_name) - if component_definition: - component_class = hubs_settings.registered_hubs_components[component_name] - component_class_name = component_class.__name__ - component = getattr(obj, component_class_name) - - has_properties = len(component_definition['properties']) > 0 - - col = row.box().column() - top_row = col.row() - if has_properties: - top_row.prop(component_item, "expanded", - icon="TRIA_DOWN" if component_item.expanded else "TRIA_RIGHT", - icon_only=True, emboss=False - ) - top_row.label(text=components.dash_to_title(component_name)) - - copy_component_operator = top_row.operator( - "wm.copy_hubs_component", - text="", - icon="PASTEDOWN" - ) - copy_component_operator.component_name = component_name - - remove_component_operator = top_row.operator( - "wm.remove_hubs_component", - text="", - icon="X" - ) - remove_component_operator.component_name = component_name - remove_component_operator.object_source = panel.bl_context - - if has_properties and component_item.expanded: - col.separator() - content_col = col.column() - - path = panel.bl_context + "." + component_class_name - - draw_type(context, content_col, obj, component, path, component_definition) - else: - col = row.box().column() - top_row = col.row() - top_row.label(text=f"Unknown component '{component_name}'",icon="ERROR") - remove_component_operator = top_row.operator( - "wm.remove_hubs_component", - text="", - icon="X" - ) - remove_component_operator.component_name = component_name - remove_component_operator.object_source = panel.bl_context - -def draw_type(context, col, obj, target, path, type_definition): - for property_name, property_definition in type_definition['properties'].items(): - draw_property(context, col, obj, target, path, property_name, property_definition) - -def draw_property(context, col, obj, target, path, property_name, property_definition): - property_type = property_definition['type'] - hubs_settings = context.scene.hubs_settings - registered_types = hubs_settings.hubs_config['types'] - is_custom_type = property_type in registered_types - - if property_type == 'collections': - draw_collections_property(context, col, obj, target, path, property_name, property_definition) - elif property_type == 'array': - draw_array_property(context, col, obj, target, path, property_name, property_definition) - elif is_custom_type: - type_row = col.row() - display_name = property_definition.get("label", components.camel_to_title(property_name)) - type_row.label(text=display_name) - draw_type(context, type_row.column(), obj, getattr(target, property_name), path + "." + property_name, registered_types[property_type]) - else: - col.prop(data=target, property=property_name) - -def draw_collections_property(_context, col, obj, _target, _path, property_name, property_definition): - collections_row = col.row() - collections_row.label(text=property_name) - - filtered_collection_names = [] - collection_prefix_regex = None - - if 'collectionPrefix' in property_definition: - collection_prefix = property_definition['collectionPrefix'] - collection_prefix_regex = re.compile( - r'^' + collection_prefix) - - for collection in obj.users_collection: - if collection_prefix_regex and collection_prefix_regex.match(collection.name): - new_name = collection_prefix_regex.sub( - "", collection.name) - filtered_collection_names.append(new_name) - elif not collection_prefix_regex: - filtered_collection_names.append(collection.name) - - collections_row.box().label(text=", ".join(filtered_collection_names)) - -def draw_array_property(context, col, obj, target, path, property_name, property_definition): - hubs_settings = context.scene.hubs_settings - registered_types = hubs_settings.hubs_config['types'] - array_type = property_definition['arrayType'] - item_definition = registered_types[array_type] - - array_value = getattr(target, property_name) - - property_path = path + "." + property_name - - if property_name != 'value': - col.label(text=property_name) - - for i, item in enumerate(array_value): - box_row = col.box().row() - box_col = box_row.column() - item_path = property_path + "." + str(i) - - draw_type(context, box_col, obj, item, item_path, item_definition) - - remove_operator = box_row.column().operator( - "wm.remove_hubs_component_item", - text="", - icon="X" - ) - remove_operator.path = item_path - - add_operator = col.operator( - "wm.add_hubs_component_item", - text="Add Item" - ) - add_operator.path = property_path - -def register(): - bpy.utils.register_class(HubsRenderPanel) - bpy.utils.register_class(HubsScenePanel) - bpy.utils.register_class(HubsObjectPanel) - bpy.utils.register_class(HubsMaterialPanel) - bpy.utils.register_class(HubsBonePanel) - -def unregister(): - bpy.utils.unregister_class(HubsRenderPanel) - bpy.utils.unregister_class(HubsScenePanel) - bpy.utils.unregister_class(HubsObjectPanel) - bpy.utils.unregister_class(HubsMaterialPanel) - bpy.utils.unregister_class(HubsBonePanel) From 11b9703c6201b1d11788b28afdf84e21be75e2a8 Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Mon, 10 Jul 2023 16:49:22 +0100 Subject: [PATCH 17/17] Incorporated linter suggestions --- addons/io_hubs_addon/components/lightmaps.py | 44 ++++++++++++-------- addons/io_hubs_addon/components/operators.py | 8 ++-- addons/io_hubs_addon/components/ui.py | 4 +- 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/addons/io_hubs_addon/components/lightmaps.py b/addons/io_hubs_addon/components/lightmaps.py index e811cc5a..42615b41 100644 --- a/addons/io_hubs_addon/components/lightmaps.py +++ b/addons/io_hubs_addon/components/lightmaps.py @@ -4,6 +4,7 @@ # Label for decoy texture HUBS_DECOY_IMAGE_TEXTURE = "HUBS_DECOY_IMAGE_TEXTURE" + # Find and select the image texture associated with a MOZ_lightmap settings def findImageTexture(lightmapNode): for inputs in lightmapNode.inputs: @@ -11,23 +12,24 @@ def findImageTexture(lightmapNode): return links.from_node return None -# Find the UV map associated with an image texture +# Find the UV map associated with an image texture def findUvMap(imageTexture, material): # Search for the parent UV Map for inputs in imageTexture.inputs: for links in inputs.links: # Is this node a MOZ lightmap node? - if links.from_node.bl_idname == "ShaderNodeUVMap": + if links.from_node.bl_idname == "ShaderNodeUVMap": return links.from_node.uv_map else: raise ValueError(f"Unexpected node type '{links.from_node.bl_idname}' instead of 'ShaderNodeUVMap' on material '{material.name}'") return None + # Selects all the faces of the mesh that have been assigned the given material (important for UV packing for lightmaps) def selectMeshFacesFromMaterial(object, mesh, material): materialSlotIndex = object.material_slots.find(material.name) - if materialSlotIndex < 0: + if materialSlotIndex < 0: raise ValueError(f"Failed to find a slot with material '{material.name}' in '{mesh.name}' attached to object '{object.name}'") bm = bmesh.new() bm.from_mesh(object.data) @@ -37,6 +39,7 @@ def selectMeshFacesFromMaterial(object, mesh, material): bm.to_mesh(object.data) bm.free() + # Select the object that holds this mesh def selectObjectFromMesh(mesh, material): for object in bpy.context.scene.objects: @@ -45,36 +48,38 @@ def selectObjectFromMesh(mesh, material): # Objects cannot be selected if they are hidden object.hide_set(False) object.select_set(True) - print(f" --- selected object '{object.name}' because it uses mesh '{mesh.name}'") + print(f" --- selected object '{object.name}' because it uses mesh '{mesh.name}'") selectMeshFacesFromMaterial(object, mesh, material) + # Select the UV input to the image texture for every mesh that uses the given material def selectUvMaps(imageTexture, material): # Select the lightmap UVs on the associated mesh uvMap = findUvMap(imageTexture, material) - if uvMap: + if uvMap: print(f" -- found UV Map Node '{uvMap}'") # Search for meshes that use this material (can't find a parent property so this will have to do) for mesh in bpy.data.meshes: if mesh.materials.find(material.name) != -1: - print(f" -- found mesh '{mesh.name}' that uses this material") - selectObjectFromMesh(mesh, material) + print(f" -- found mesh '{mesh.name}' that uses this material") + selectObjectFromMesh(mesh, material) if mesh.uv_layers.find(uvMap) != -1: uvLayer = mesh.uv_layers[uvMap] mesh.uv_layers.active = uvLayer - print(f" --- UV layer '{uvMap}' is now active on '{mesh.name}'") + print(f" --- UV layer '{uvMap}' is now active on '{mesh.name}'") else: raise ValueError(f"Failed to find UV layer '{uvMap}' for mesh '{mesh.name}' using material '{material.name}'") else: raise ValueError(f"No UV map found for image texture '{imageTexture.name}' with image '{imageTexture.image.name}' in material '{material.name}'") + # Selects all MOZ lightmap related components ready for baking -def selectLightmapComponents(target): +def selectLightmapComponents(target): # Force UI into OBJECT mode so scripts can manipulate meshes - try: - bpy.ops.object.mode_set(mode='OBJECT') + try: + bpy.ops.object.mode_set(mode='OBJECT') except Exception as e: - print(f"Failed to enter OBJECT mode (usually non-fatal): {str(e)}") + print(f"Failed to enter OBJECT mode (usually non-fatal): {str(e)}") # Deslect all objects to start with (bake objects will then be selected) for o in bpy.context.scene.objects: o.select_set(False) @@ -99,11 +104,11 @@ def selectLightmapComponents(target): for shadernode in material.node_tree.nodes: # Is this node a MOZ lightmap node? if shadernode.bl_idname == "moz_lightmap.node": - print(f"found '{shadernode.name}' ({shadernode.label}) on material '{material.name}'") - imageTexture = findImageTexture(shadernode) + print(f"found '{shadernode.name}' ({shadernode.label}) on material '{material.name}'") + imageTexture = findImageTexture(shadernode) if imageTexture: # Check image texture actually has an image - if imageTexture.image == None: + if imageTexture.image is None: raise ValueError(f"No image found on image texture '{imageTexture.name}' ('{imageTexture.label}') in material '{material.name}'") # Is this lightmap texture image being targetted? if target == "" or target == imageTexture.image.name: @@ -116,13 +121,14 @@ def selectLightmapComponents(target): else: print(f" - ignoring image texture '{imageTexture.name}' because it uses image '{imageTexture.image.name}' and the target is '{target}'") else: - raise ValueError(f"No image texture found on material '{material.name}'") + raise ValueError(f"No image texture found on material '{material.name}'") # Is it a decoy node? elif shadernode.bl_idname == "ShaderNodeTexImage" and shadernode.label == "HUBS_DECOY_IMAGE_TEXTURE": # Select and activate the image texture node so it will be targetted by the bake shadernode.select = True material.node_tree.nodes.active = shadernode - + + # List all the lightmap textures images def listLightmapImages(): result = set() @@ -139,6 +145,7 @@ def listLightmapImages(): result.add(imageTexture.image) return result + # Check for selected objects with non-lightmapped materials. They might get baked and have corrupted image textures def assertSelectedObjectsAreSafeToBake(addDecoyRatherThanThrow): decoyCount = 0 @@ -163,9 +170,10 @@ def assertSelectedObjectsAreSafeToBake(addDecoyRatherThanThrow): decoy_image_texture.label = HUBS_DECOY_IMAGE_TEXTURE decoyCount += 1 else: - raise ValueError(f"Multi-material object '{object.name}' uses '{materialSlot.name}' with no lightmap or decoy texture. It will be corrupted by baking") + raise ValueError(f"Multi-material object '{object.name}' uses '{materialSlot.name}' with no lightmap or decoy texture. It will be corrupted by baking") return decoyCount + def removeAllDecoyImageTextures(): decoyCount = 0 # For every material diff --git a/addons/io_hubs_addon/components/operators.py b/addons/io_hubs_addon/components/operators.py index b1367050..c8df20ba 100644 --- a/addons/io_hubs_addon/components/operators.py +++ b/addons/io_hubs_addon/components/operators.py @@ -625,6 +625,7 @@ def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} + class PrepareHubsLightmaps(Operator): bl_idname = "wm.prepare_hubs_lightmaps" bl_label = "Select all lightmap elements for baking" @@ -633,7 +634,7 @@ class PrepareHubsLightmaps(Operator): target: StringProperty(name="target") def execute(self, context): - try: + try: lightmaps.selectLightmapComponents(self.target) lightmaps.assertSelectedObjectsAreSafeToBake(False) self.report({'INFO'}, "Lightmaps prepared and ready to bake") @@ -648,20 +649,21 @@ class AddDecoyImageTextures(Operator): bl_description = "Adds decoy image textures to the materials of selected meshes if they are required" def execute(self, context): - try: + try: decoyCount = lightmaps.assertSelectedObjectsAreSafeToBake(True) self.report({'INFO'}, f"Added {decoyCount} decoy textures") except Exception as e: self.report({'ERROR'}, str(e)) return {'FINISHED'} + class RemoveDecoyImageTextures(Operator): bl_idname = "wm.remove_decoy_image_textures" bl_label = "Remove All" bl_description = "Remove all decoy image textures from materials regardless of selection" def execute(self, context): - try: + try: decoyCount = lightmaps.removeAllDecoyImageTextures() self.report({'INFO'}, f"Removed {decoyCount} decoy textures") except Exception as e: diff --git a/addons/io_hubs_addon/components/ui.py b/addons/io_hubs_addon/components/ui.py index cc9a5051..8f4d300d 100644 --- a/addons/io_hubs_addon/components/ui.py +++ b/addons/io_hubs_addon/components/ui.py @@ -180,6 +180,7 @@ class HubsBonePanel(bpy.types.Panel): def draw(self, context): draw_components_list(self, context) + class HubsRenderPanel(bpy.types.Panel): bl_label = 'Hubs' bl_idname = "RENDER_PT_hubs" @@ -190,7 +191,7 @@ class HubsRenderPanel(bpy.types.Panel): def draw(self, context): layout = self.layout row = layout.row() - lightmapImages = sorted(lightmaps.listLightmapImages(), key=lambda it:it.name) + lightmapImages = sorted(lightmaps.listLightmapImages(), key=lambda it: it.name) # Only show the panel if there are any lightmaps to prepare if len(lightmapImages) > 0: row.operator(operators.PrepareHubsLightmaps.bl_idname).target = "" @@ -204,6 +205,7 @@ def draw(self, context): row.operator(operators.AddDecoyImageTextures.bl_idname) row.operator(operators.RemoveDecoyImageTextures.bl_idname) + class TooltipLabel(bpy.types.Operator): bl_idname = "ui.hubs_tooltip_label" bl_label = "---"