From 206ffe6231e43fa5ae81af93a943646087a5a13e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 5 Dec 2024 18:29:00 +0800 Subject: [PATCH 01/14] implement the new extractor and options for exporting texture as single output --- .../plugins/create/create_textures.py | 9 +- .../publish/collect_textureset_images.py | 136 ++++++++++++++++-- .../publish/extract_map_as_single_output.py | 96 +++++++++++++ 3 files changed, 232 insertions(+), 9 deletions(-) create mode 100644 client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py diff --git a/client/ayon_substancepainter/plugins/create/create_textures.py b/client/ayon_substancepainter/plugins/create/create_textures.py index 5f1d4be..adf5cfd 100644 --- a/client/ayon_substancepainter/plugins/create/create_textures.py +++ b/client/ayon_substancepainter/plugins/create/create_textures.py @@ -53,7 +53,9 @@ def create(self, product_name, instance_data, pre_create_data): "exportPadding", "exportDilationDistance", "useCustomExportPreset", - "exportChannel" + "exportChannel", + "exportTextureSets", + "exportTextureSetsAsOneOutput" ]: if key in pre_create_data: creator_attributes[key] = pre_create_data[key] @@ -152,6 +154,11 @@ def get_instance_attr_defs(self): label="Review", tooltip="Mark as reviewable", default=True), + BoolDef("exportTextureSetsAsOneOutput", + label="Export Texture Sets As One Texture Output", + tooltip="Export multiple texture set(s) " + "as one Texture Output", + default=False), EnumDef("exportTextureSets", items=export_texture_set_enum, multiselection=True, diff --git a/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py b/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py index f1f9a0c..4d22d1c 100644 --- a/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py +++ b/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py @@ -4,6 +4,7 @@ import pyblish.api import ayon_api +from collections import defaultdict import substance_painter.textureset from ayon_core.pipeline import publish from ayon_substancepainter.api.lib import ( @@ -44,14 +45,29 @@ def process(self, instance): # Let's break the instance into multiple instances to integrate # a product per generated texture or texture UDIM sequence - for (texture_set_name, stack_name), template_maps in maps.items(): - self.log.info(f"Processing {texture_set_name}/{stack_name}") - for template, outputs in template_maps.items(): - self.log.info(f"Processing {template}") - self.create_image_instance(instance, template, outputs, - task_entity=task_entity, - texture_set_name=texture_set_name, - stack_name=stack_name) + creator_attr = instance.data["creator_attributes"] + if creator_attr.get("exportTextureSetsAsOneOutput", False): + texture_sets_by_map_identifier = defaultdict(list) + for (texture_set_name, stack_name), template_maps in maps.items(): + for template, outputs in template_maps.items(): + self.log.info(f"Processing {template}") + map_identifier = strip_template(template) + map_identifier = f"{map_identifier}" + texture_sets_by_map_identifier[map_identifier].extend(outputs) + for map_identifier, outputs in texture_sets_by_map_identifier.items(): + self.log.info(f"Processing {map_identifier}") + self.create_image_instance_by_map_filtering( + instance, outputs, task_entity, map_identifier) + + else: + for (texture_set_name, stack_name), template_maps in maps.items(): + self.log.info(f"Processing {texture_set_name}/{stack_name}") + for template, outputs in template_maps.items(): + self.log.info(f"Processing {template}") + self.create_image_instance(instance, template, outputs, + task_entity=task_entity, + texture_set_name=texture_set_name, + stack_name=stack_name) def create_image_instance(self, instance, template, outputs, task_entity, texture_set_name, stack_name): @@ -156,6 +172,110 @@ def create_image_instance(self, instance, template, outputs, image_instance.data["textureSetName"] = texture_set_name image_instance.data["textureStackName"] = stack_name + # Store color space with the instance + # Note: The extractor will assign it to the representation + colorspace = outputs[0].get("colorSpace") + if colorspace: + self.log.debug(f"{image_product_name} colorspace: {colorspace}") + image_instance.data["colorspace"] = colorspace + + # Store the instance in the original instance as a member + instance.append(image_instance) + + def create_image_instance_by_map_filtering(self, instance, outputs, + task_entity, map_identifier): + """Create a new instance per image based on map filtering. + + The new instances will be of product type `image`. + **Only used for exporting multiple texture sets as one texture output + + """ + + context = instance.context + first_filepath = outputs[0]["filepath"] + fnames = [os.path.basename(output["filepath"]) for output in outputs] + ext = os.path.splitext(first_filepath)[1] + assert ext.lstrip("."), f"No extension: {ext}" + # Function to remove textureSet from filepath + def remove_texture_set_token(filepath, texture_set): + return filepath.replace(texture_set, '') + + fnames_without_textureSet = [ + remove_texture_set_token(output["output"], output["textureSet"]) + for output in outputs + ] + + task_name = task_type = None + if task_entity: + task_name = task_entity["name"] + task_type = task_entity["taskType"] + + # TODO: The product type actually isn't 'texture' currently but + # for now this is only done so the product name starts with + # 'texture' + image_product_name = get_product_name( + context.data["projectName"], + task_name, + task_type, + context.data["hostName"], + product_type="texture", + variant=instance.data["variant"] + f".{map_identifier}", + project_settings=context.data["project_settings"] + ) + image_product_group_name = get_product_name( + context.data["projectName"], + task_name, + task_type, + context.data["hostName"], + product_type="texture", + variant=instance.data["variant"], + project_settings=context.data["project_settings"] + ) + + # Prepare representation + representation = { + "name": ext.lstrip("."), + "ext": ext.lstrip("."), + #TODO: strip the texture_sets. + "files": ( + fnames_without_textureSet + if len(fnames_without_textureSet) > 1 + else fnames_without_textureSet[0] + ), + } + + # Mark as UDIM explicitly if it has UDIM tiles. + if bool(outputs[0].get("udim")): + # The representation for a UDIM sequence should have a `udim` key + # that is a list of all udim tiles (str) like: ["1001", "1002"] + # strings. See CollectTextures plug-in and Integrators. + representation["udim"] = [output["udim"] for output in outputs] + + # Set up the representation for thumbnail generation + # TODO: Simplify this once thumbnail extraction is refactored + staging_dir = os.path.dirname(first_filepath) + representation["tags"] = ["review"] + representation["stagingDir"] = staging_dir + # Clone the instance + product_type = "image" + image_instance = context.create_instance(image_product_name) + image_instance[:] = instance[:] + image_instance.data.update(copy.deepcopy(dict(instance.data))) + image_instance.data["name"] = image_product_name + image_instance.data["label"] = image_product_name + image_instance.data["productName"] = image_product_name + image_instance.data["productType"] = product_type + image_instance.data["family"] = product_type + image_instance.data["families"] = [product_type, "textures"] + if instance.data["creator_attributes"].get("review"): + image_instance.data["families"].append("review") + image_instance.data["image_outputs"] = fnames + image_instance.data["representations"] = [representation] + + # Group the textures together in the loader + image_instance.data["productGroup"] = image_product_group_name + + # Store color space with the instance # Note: The extractor will assign it to the representation colorspace = outputs[0].get("colorSpace") diff --git a/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py b/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py new file mode 100644 index 0000000..17b4f7d --- /dev/null +++ b/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py @@ -0,0 +1,96 @@ +import clique +import os + +from ayon_core.pipeline import publish +from ayon_core.lib import ( + get_oiio_tool_args, + run_subprocess, +) + + +def convert_texture_maps_for_udim_export(staging_dir, image_outputs, has_udim=False): + if has_udim: + collections, remainder = clique.assemble(image_outputs, minimum_items=1) + return [ + os.path.join( + staging_dir, + collection.format(pattern="{head}{padding}{tail}") + ) + for collection in collections + ] + else: + return [ + os.path.join(staging_dir, output) for output in image_outputs + ] + + +def convert_texture_maps_as_single_output(staging_dir, source_image_outputs, + dest_image_outputs, has_udim=False, + log=None): + oiio_tool_args = get_oiio_tool_args("oiiotool") + + source_maps = convert_texture_maps_for_udim_export( + staging_dir, source_image_outputs, has_udim=has_udim) + dest_map = next(convert_texture_maps_for_udim_export( + staging_dir, dest_image_outputs, has_udim=has_udim + ), None) + + log.info(f"{source_maps} composited as {dest_map}") + oiio_cmd = oiio_tool_args + source_maps + [ + "--over", "-o", + dest_map + ] + + subprocess_args = " ".join(oiio_cmd) + + env = os.environ.copy() + env.pop("OCIO", None) + log.info(" ".join(subprocess_args)) + try: + run_subprocess(subprocess_args, env=env) + except Exception: + log.error("Texture maketx conversion failed", exc_info=True) + raise + + +class ExtractTexturesAsSingleOutput(publish.Extractor): + """Extract Texture As Single Output + + Combine the multliple texture sets into one single texture output. + + """ + + label = "Extract Texture Sets as Single Texture Output" + hosts = ["substancepainter"] + families = ["image"] + settings_category = "substancepainter" + + # Run directly after textures export + order = publish.Extractor.order - 0.099 + + def process(self, instance): + if "exportTextureSetsAsOneOutput" not in instance.data["creator_attributes"]: + self.log.debug( + "Skipping to export texture sets as single texture output.." + ) + return + + representations: "list[dict]" = instance.data["representations"] + + staging_dir = instance.data["stagingDir"] + source_image_outputs = instance.data["image_outputs"] + has_udim = False + dest_image_outputs = [] + for representation in list(representations): + dest_files = representation["files"] + is_sequence = isinstance(dest_files, (list, tuple)) + if not is_sequence: + dest_image_outputs = [dest_image_outputs] + if "udim" in representation: + has_udim = True + + convert_texture_maps_as_single_output( + staging_dir, source_image_outputs, + dest_image_outputs, has_udim=has_udim, + log=self.log + ) From b31c5e1e7f42372eff597d9230b90b5c00aabb09 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 6 Dec 2024 18:20:12 +0800 Subject: [PATCH 02/14] big roy's comment - code tweaks --- .../publish/collect_textureset_images.py | 140 ++++-------------- .../publish/extract_map_as_single_output.py | 44 ++++-- 2 files changed, 56 insertions(+), 128 deletions(-) diff --git a/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py b/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py index 4d22d1c..8d76d77 100644 --- a/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py +++ b/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py @@ -56,8 +56,10 @@ def process(self, instance): texture_sets_by_map_identifier[map_identifier].extend(outputs) for map_identifier, outputs in texture_sets_by_map_identifier.items(): self.log.info(f"Processing {map_identifier}") - self.create_image_instance_by_map_filtering( - instance, outputs, task_entity, map_identifier) + self.create_image_instance(instance, template, outputs, + task_entity=task_entity, + texture_set_name=texture_set_name, + stack_name=stack_name) else: for (texture_set_name, stack_name), template_maps in maps.items(): @@ -79,9 +81,22 @@ def create_image_instance(self, instance, template, outputs, context = instance.context first_filepath = outputs[0]["filepath"] + is_single_output = instance.data["creator_attributes"].get( + "exportTextureSetsAsOneOutput", False) fnames = [os.path.basename(output["filepath"]) for output in outputs] ext = os.path.splitext(first_filepath)[1] assert ext.lstrip("."), f"No extension: {ext}" + if is_single_output: + # Function to remove textureSet from filepath + def remove_texture_set_token(filepath, texture_set): + return filepath.replace(texture_set, "") + + fnames = [ + remove_texture_set_token( + output["output"], output["textureSet"] + ) + for output in outputs + ] always_include_texture_set_name = False # todo: make this configurable all_texture_sets = substance_painter.textureset.all_texture_sets() @@ -92,12 +107,13 @@ def create_image_instance(self, instance, template, outputs, # Define the suffix we want to give this particular texture # set and set up a remapped product naming for it. suffix = "" - if always_include_texture_set_name or len(all_texture_sets) > 1: - # More than one texture set, include texture set name - suffix += f".{texture_set_name}" - if texture_set.is_layered_material() and stack_name: - # More than one stack, include stack name - suffix += f".{stack_name}" + if not is_single_output: + if always_include_texture_set_name or len(all_texture_sets) > 1: + # More than one texture set, include texture set name + suffix += f".{texture_set_name}" + if texture_set.is_layered_material() and stack_name: + # More than one stack, include stack name + suffix += f".{stack_name}" # Always include the map identifier map_identifier = strip_template(template) @@ -162,6 +178,10 @@ def create_image_instance(self, instance, template, outputs, image_instance.data["families"] = [product_type, "textures"] if instance.data["creator_attributes"].get("review"): image_instance.data["families"].append("review") + if is_single_output: + image_instance.data["image_outputs"] = [ + os.path.basename(output["filepath"]) for output in outputs + ] image_instance.data["representations"] = [representation] @@ -172,110 +192,6 @@ def create_image_instance(self, instance, template, outputs, image_instance.data["textureSetName"] = texture_set_name image_instance.data["textureStackName"] = stack_name - # Store color space with the instance - # Note: The extractor will assign it to the representation - colorspace = outputs[0].get("colorSpace") - if colorspace: - self.log.debug(f"{image_product_name} colorspace: {colorspace}") - image_instance.data["colorspace"] = colorspace - - # Store the instance in the original instance as a member - instance.append(image_instance) - - def create_image_instance_by_map_filtering(self, instance, outputs, - task_entity, map_identifier): - """Create a new instance per image based on map filtering. - - The new instances will be of product type `image`. - **Only used for exporting multiple texture sets as one texture output - - """ - - context = instance.context - first_filepath = outputs[0]["filepath"] - fnames = [os.path.basename(output["filepath"]) for output in outputs] - ext = os.path.splitext(first_filepath)[1] - assert ext.lstrip("."), f"No extension: {ext}" - # Function to remove textureSet from filepath - def remove_texture_set_token(filepath, texture_set): - return filepath.replace(texture_set, '') - - fnames_without_textureSet = [ - remove_texture_set_token(output["output"], output["textureSet"]) - for output in outputs - ] - - task_name = task_type = None - if task_entity: - task_name = task_entity["name"] - task_type = task_entity["taskType"] - - # TODO: The product type actually isn't 'texture' currently but - # for now this is only done so the product name starts with - # 'texture' - image_product_name = get_product_name( - context.data["projectName"], - task_name, - task_type, - context.data["hostName"], - product_type="texture", - variant=instance.data["variant"] + f".{map_identifier}", - project_settings=context.data["project_settings"] - ) - image_product_group_name = get_product_name( - context.data["projectName"], - task_name, - task_type, - context.data["hostName"], - product_type="texture", - variant=instance.data["variant"], - project_settings=context.data["project_settings"] - ) - - # Prepare representation - representation = { - "name": ext.lstrip("."), - "ext": ext.lstrip("."), - #TODO: strip the texture_sets. - "files": ( - fnames_without_textureSet - if len(fnames_without_textureSet) > 1 - else fnames_without_textureSet[0] - ), - } - - # Mark as UDIM explicitly if it has UDIM tiles. - if bool(outputs[0].get("udim")): - # The representation for a UDIM sequence should have a `udim` key - # that is a list of all udim tiles (str) like: ["1001", "1002"] - # strings. See CollectTextures plug-in and Integrators. - representation["udim"] = [output["udim"] for output in outputs] - - # Set up the representation for thumbnail generation - # TODO: Simplify this once thumbnail extraction is refactored - staging_dir = os.path.dirname(first_filepath) - representation["tags"] = ["review"] - representation["stagingDir"] = staging_dir - # Clone the instance - product_type = "image" - image_instance = context.create_instance(image_product_name) - image_instance[:] = instance[:] - image_instance.data.update(copy.deepcopy(dict(instance.data))) - image_instance.data["name"] = image_product_name - image_instance.data["label"] = image_product_name - image_instance.data["productName"] = image_product_name - image_instance.data["productType"] = product_type - image_instance.data["family"] = product_type - image_instance.data["families"] = [product_type, "textures"] - if instance.data["creator_attributes"].get("review"): - image_instance.data["families"].append("review") - image_instance.data["image_outputs"] = fnames - image_instance.data["representations"] = [representation] - - # Group the textures together in the loader - image_instance.data["productGroup"] = image_product_group_name - - # Store color space with the instance # Note: The extractor will assign it to the representation colorspace = outputs[0].get("colorSpace") diff --git a/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py b/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py index 17b4f7d..21c532a 100644 --- a/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py +++ b/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py @@ -8,7 +8,18 @@ ) -def convert_texture_maps_for_udim_export(staging_dir, image_outputs, has_udim=False): +def get_texture_outputs(staging_dir, image_outputs, has_udim=False): + """Getting the expected texture output(s) with/without udim sequence + before merging them with oiio tools. + + Args: + staging_dir (str): staging dir + image_outputs (list): source image outputs + has_udim (bool, optional): Is with UDIM. Defaults to False. + + Returns: + list: Texture outputs which are used for merging. + """ if has_udim: collections, remainder = clique.assemble(image_outputs, minimum_items=1) return [ @@ -29,9 +40,9 @@ def convert_texture_maps_as_single_output(staging_dir, source_image_outputs, log=None): oiio_tool_args = get_oiio_tool_args("oiiotool") - source_maps = convert_texture_maps_for_udim_export( + source_maps = get_texture_outputs( staging_dir, source_image_outputs, has_udim=has_udim) - dest_map = next(convert_texture_maps_for_udim_export( + dest_map = next(get_texture_outputs( staging_dir, dest_image_outputs, has_udim=has_udim ), None) @@ -44,13 +55,11 @@ def convert_texture_maps_as_single_output(staging_dir, source_image_outputs, subprocess_args = " ".join(oiio_cmd) env = os.environ.copy() - env.pop("OCIO", None) - log.info(" ".join(subprocess_args)) + try: run_subprocess(subprocess_args, env=env) - except Exception: - log.error("Texture maketx conversion failed", exc_info=True) - raise + except Exception as exc: + raise RuntimeError("Flattening texture stack to single output image failed") from exc class ExtractTexturesAsSingleOutput(publish.Extractor): @@ -69,25 +78,28 @@ class ExtractTexturesAsSingleOutput(publish.Extractor): order = publish.Extractor.order - 0.099 def process(self, instance): - if "exportTextureSetsAsOneOutput" not in instance.data["creator_attributes"]: + if not instance.data.get("creator_attributes", {}).get( + "exportTextureSetsAsOneOutput", False): self.log.debug( "Skipping to export texture sets as single texture output.." ) return representations: "list[dict]" = instance.data["representations"] + repre = representations[0] staging_dir = instance.data["stagingDir"] source_image_outputs = instance.data["image_outputs"] has_udim = False dest_image_outputs = [] - for representation in list(representations): - dest_files = representation["files"] - is_sequence = isinstance(dest_files, (list, tuple)) - if not is_sequence: - dest_image_outputs = [dest_image_outputs] - if "udim" in representation: - has_udim = True + dest_files = repre["files"] + is_sequence = isinstance(dest_files, (list, tuple)) + if not is_sequence: + dest_image_outputs = [dest_image_outputs] + else: + dest_image_outputs = dest_files + if "udim" in repre: + has_udim = True convert_texture_maps_as_single_output( staging_dir, source_image_outputs, From 6a7a55b5176dc0eca26e4feb4396abcaf4c79116 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 6 Dec 2024 20:19:59 +0800 Subject: [PATCH 03/14] use paste argument for oiio command --- .../plugins/publish/extract_map_as_single_output.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py b/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py index 21c532a..4eaef60 100644 --- a/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py +++ b/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py @@ -48,7 +48,9 @@ def convert_texture_maps_as_single_output(staging_dir, source_image_outputs, log.info(f"{source_maps} composited as {dest_map}") oiio_cmd = oiio_tool_args + source_maps + [ - "--over", "-o", + "--paste:mergeroi=1", + "+0+0", + "-o", dest_map ] From 886ba8942a0b177d2761524581be88b751c64c94 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Sun, 8 Dec 2024 22:26:17 +0800 Subject: [PATCH 04/14] use 'flattentexturesets' for more concise naming --- .../plugins/create/create_textures.py | 4 +-- .../publish/collect_textureset_images.py | 33 +++++-------------- .../publish/extract_map_as_single_output.py | 2 +- 3 files changed, 11 insertions(+), 28 deletions(-) diff --git a/client/ayon_substancepainter/plugins/create/create_textures.py b/client/ayon_substancepainter/plugins/create/create_textures.py index adf5cfd..8b2dccb 100644 --- a/client/ayon_substancepainter/plugins/create/create_textures.py +++ b/client/ayon_substancepainter/plugins/create/create_textures.py @@ -55,7 +55,7 @@ def create(self, product_name, instance_data, pre_create_data): "useCustomExportPreset", "exportChannel", "exportTextureSets", - "exportTextureSetsAsOneOutput" + "flattenTextureSets" ]: if key in pre_create_data: creator_attributes[key] = pre_create_data[key] @@ -154,7 +154,7 @@ def get_instance_attr_defs(self): label="Review", tooltip="Mark as reviewable", default=True), - BoolDef("exportTextureSetsAsOneOutput", + BoolDef("flattenTextureSets", label="Export Texture Sets As One Texture Output", tooltip="Export multiple texture set(s) " "as one Texture Output", diff --git a/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py b/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py index 8d76d77..c252642 100644 --- a/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py +++ b/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py @@ -45,31 +45,14 @@ def process(self, instance): # Let's break the instance into multiple instances to integrate # a product per generated texture or texture UDIM sequence - creator_attr = instance.data["creator_attributes"] - if creator_attr.get("exportTextureSetsAsOneOutput", False): - texture_sets_by_map_identifier = defaultdict(list) - for (texture_set_name, stack_name), template_maps in maps.items(): - for template, outputs in template_maps.items(): - self.log.info(f"Processing {template}") - map_identifier = strip_template(template) - map_identifier = f"{map_identifier}" - texture_sets_by_map_identifier[map_identifier].extend(outputs) - for map_identifier, outputs in texture_sets_by_map_identifier.items(): - self.log.info(f"Processing {map_identifier}") + for (texture_set_name, stack_name), template_maps in maps.items(): + self.log.info(f"Processing {texture_set_name}/{stack_name}") + for template, outputs in template_maps.items(): + self.log.info(f"Processing {template}") self.create_image_instance(instance, template, outputs, - task_entity=task_entity, - texture_set_name=texture_set_name, - stack_name=stack_name) - - else: - for (texture_set_name, stack_name), template_maps in maps.items(): - self.log.info(f"Processing {texture_set_name}/{stack_name}") - for template, outputs in template_maps.items(): - self.log.info(f"Processing {template}") - self.create_image_instance(instance, template, outputs, - task_entity=task_entity, - texture_set_name=texture_set_name, - stack_name=stack_name) + task_entity=task_entity, + texture_set_name=texture_set_name, + stack_name=stack_name) def create_image_instance(self, instance, template, outputs, task_entity, texture_set_name, stack_name): @@ -82,7 +65,7 @@ def create_image_instance(self, instance, template, outputs, context = instance.context first_filepath = outputs[0]["filepath"] is_single_output = instance.data["creator_attributes"].get( - "exportTextureSetsAsOneOutput", False) + "flattenTextureSets", False) fnames = [os.path.basename(output["filepath"]) for output in outputs] ext = os.path.splitext(first_filepath)[1] assert ext.lstrip("."), f"No extension: {ext}" diff --git a/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py b/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py index 4eaef60..c4685fb 100644 --- a/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py +++ b/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py @@ -81,7 +81,7 @@ class ExtractTexturesAsSingleOutput(publish.Extractor): def process(self, instance): if not instance.data.get("creator_attributes", {}).get( - "exportTextureSetsAsOneOutput", False): + "flattenTextureSets", False): self.log.debug( "Skipping to export texture sets as single texture output.." ) From 7c669998d17cd98439b26bc0ff6df722d05281d7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Sun, 8 Dec 2024 22:26:34 +0800 Subject: [PATCH 05/14] use 'flattentexturesets' for more concise naming --- client/ayon_substancepainter/plugins/create/create_textures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_substancepainter/plugins/create/create_textures.py b/client/ayon_substancepainter/plugins/create/create_textures.py index 8b2dccb..027ee5a 100644 --- a/client/ayon_substancepainter/plugins/create/create_textures.py +++ b/client/ayon_substancepainter/plugins/create/create_textures.py @@ -155,7 +155,7 @@ def get_instance_attr_defs(self): tooltip="Mark as reviewable", default=True), BoolDef("flattenTextureSets", - label="Export Texture Sets As One Texture Output", + label="Flatten Texture Sets As One Texture Output", tooltip="Export multiple texture set(s) " "as one Texture Output", default=False), From fd50da00b2d0d91ee7e0963deb86d6d4c8a5bf95 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Sun, 8 Dec 2024 22:27:50 +0800 Subject: [PATCH 06/14] remove unused lib --- .../plugins/publish/collect_textureset_images.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py b/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py index c252642..5094708 100644 --- a/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py +++ b/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py @@ -4,7 +4,6 @@ import pyblish.api import ayon_api -from collections import defaultdict import substance_painter.textureset from ayon_core.pipeline import publish from ayon_substancepainter.api.lib import ( From 3cf3f77d3f1a7d7999fb717b454daca06f6cb9ed Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 6 Jan 2025 21:36:44 +0800 Subject: [PATCH 07/14] big roy's comment - use oiio-cmd for run-subprocess --- .../plugins/publish/extract_map_as_single_output.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py b/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py index c4685fb..fbfd868 100644 --- a/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py +++ b/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py @@ -54,12 +54,10 @@ def convert_texture_maps_as_single_output(staging_dir, source_image_outputs, dest_map ] - subprocess_args = " ".join(oiio_cmd) - env = os.environ.copy() try: - run_subprocess(subprocess_args, env=env) + run_subprocess(oiio_cmd, env=env) except Exception as exc: raise RuntimeError("Flattening texture stack to single output image failed") from exc From b67ce855ec9ac708773b6a54f7762ce40bfb1f89 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 18 Feb 2025 15:33:14 +0800 Subject: [PATCH 08/14] update argument --- .../plugins/publish/extract_map_as_single_output.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py b/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py index fbfd868..ed16ef9 100644 --- a/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py +++ b/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py @@ -48,8 +48,8 @@ def convert_texture_maps_as_single_output(staging_dir, source_image_outputs, log.info(f"{source_maps} composited as {dest_map}") oiio_cmd = oiio_tool_args + source_maps + [ - "--paste:mergeroi=1", - "+0+0", + "--mosaic", + "{}x1".format(len(source_maps)), "-o", dest_map ] From 455c0e5dbf4ba01da54bf25672ebb400c3495eff Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 18 Feb 2025 16:14:45 +0800 Subject: [PATCH 09/14] update oiio source maps argunment --- .../publish/collect_textureset_images.py | 28 +++++-------------- .../plugins/publish/extract_maketx.py | 1 - .../publish/extract_map_as_single_output.py | 2 +- 3 files changed, 8 insertions(+), 23 deletions(-) diff --git a/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py b/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py index 37ed0fc..8b76cc8 100644 --- a/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py +++ b/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py @@ -68,17 +68,6 @@ def create_image_instance(self, instance, template, outputs, fnames = [os.path.basename(output["filepath"]) for output in outputs] ext = os.path.splitext(first_filepath)[1] assert ext.lstrip("."), f"No extension: {ext}" - if is_single_output: - # Function to remove textureSet from filepath - def remove_texture_set_token(filepath, texture_set): - return filepath.replace(texture_set, "") - - fnames = [ - remove_texture_set_token( - output["output"], output["textureSet"] - ) - for output in outputs - ] always_include_texture_set_name = False # todo: make this configurable all_texture_sets = substance_painter.textureset.all_texture_sets() @@ -89,13 +78,12 @@ def remove_texture_set_token(filepath, texture_set): # Define the suffix we want to give this particular texture # set and set up a remapped product naming for it. suffix = "" - if not is_single_output: - if always_include_texture_set_name or len(all_texture_sets) > 1: - # More than one texture set, include texture set name - suffix += f".{texture_set_name}" - if texture_set.is_layered_material() and stack_name: - # More than one stack, include stack name - suffix += f".{stack_name}" + if always_include_texture_set_name or len(all_texture_sets) > 1: + # More than one texture set, include texture set name + suffix += f".{texture_set_name}" + if texture_set.is_layered_material() and stack_name: + # More than one stack, include stack name + suffix += f".{stack_name}" # Always include the map identifier map_identifier = strip_template(template) @@ -161,9 +149,7 @@ def remove_texture_set_token(filepath, texture_set): if instance.data["creator_attributes"].get("review"): image_instance.data["families"].append("review") if is_single_output: - image_instance.data["image_outputs"] = [ - os.path.basename(output["filepath"]) for output in outputs - ] + image_instance.data["image_outputs"] = fnames image_instance.data["representations"] = [representation] diff --git a/client/ayon_substancepainter/plugins/publish/extract_maketx.py b/client/ayon_substancepainter/plugins/publish/extract_maketx.py index a3d6add..7f42717 100644 --- a/client/ayon_substancepainter/plugins/publish/extract_maketx.py +++ b/client/ayon_substancepainter/plugins/publish/extract_maketx.py @@ -120,7 +120,6 @@ def process(self, instance): return representations: "list[dict]" = instance.data["representations"] - # If a tx representation is present we skip extraction if any(repre["name"] == "tx" for repre in representations): return diff --git a/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py b/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py index ed16ef9..b58af7f 100644 --- a/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py +++ b/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py @@ -20,7 +20,7 @@ def get_texture_outputs(staging_dir, image_outputs, has_udim=False): Returns: list: Texture outputs which are used for merging. """ - if has_udim: + if has_udim and len(image_outputs) > 1: collections, remainder = clique.assemble(image_outputs, minimum_items=1) return [ os.path.join( From 61332cacd57a63215f1edf8e8f011f68e9c0c2b5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 18 Feb 2025 16:18:14 +0800 Subject: [PATCH 10/14] extract single output before extract tx --- .../plugins/publish/extract_map_as_single_output.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py b/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py index b58af7f..556bbca 100644 --- a/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py +++ b/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py @@ -75,7 +75,7 @@ class ExtractTexturesAsSingleOutput(publish.Extractor): settings_category = "substancepainter" # Run directly after textures export - order = publish.Extractor.order - 0.099 + order = publish.Extractor.order - 0.098 def process(self, instance): if not instance.data.get("creator_attributes", {}).get( From 5c08ad81f114b84e9255a0d29cc0b91ff25cf866 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 18 Feb 2025 17:03:07 +0800 Subject: [PATCH 11/14] make sure oiiotool working to combine all textures as one output --- .../publish/collect_textureset_images.py | 16 +++++- .../publish/extract_map_as_single_output.py | 50 +++++++------------ 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py b/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py index 8b76cc8..facd35e 100644 --- a/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py +++ b/client/ayon_substancepainter/plugins/publish/collect_textureset_images.py @@ -149,7 +149,21 @@ def create_image_instance(self, instance, template, outputs, if instance.data["creator_attributes"].get("review"): image_instance.data["families"].append("review") if is_single_output: - image_instance.data["image_outputs"] = fnames + # Function to remove textureSet from filepath + def remove_texture_set_token(filepath, texture_set): + return filepath.replace(f".{texture_set}", "") + + single_fnames = { + remove_texture_set_token( + output["output"], output["udim"] + ) + for output in outputs + } + + image_instance.data["image_outputs"] = [ + os.path.basename(fname) for fname in single_fnames + ] + self.log.debug(f"image_outputs: {fnames}") image_instance.data["representations"] = [representation] diff --git a/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py b/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py index 556bbca..7e233a2 100644 --- a/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py +++ b/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py @@ -8,43 +8,32 @@ ) -def get_texture_outputs(staging_dir, image_outputs, has_udim=False): +def get_texture_outputs(staging_dir, image_outputs): """Getting the expected texture output(s) with/without udim sequence before merging them with oiio tools. Args: staging_dir (str): staging dir image_outputs (list): source image outputs - has_udim (bool, optional): Is with UDIM. Defaults to False. Returns: list: Texture outputs which are used for merging. """ - if has_udim and len(image_outputs) > 1: - collections, remainder = clique.assemble(image_outputs, minimum_items=1) - return [ - os.path.join( - staging_dir, - collection.format(pattern="{head}{padding}{tail}") - ) - for collection in collections - ] - else: - return [ - os.path.join(staging_dir, output) for output in image_outputs - ] + return [ + os.path.join(staging_dir, output) for output in image_outputs + ] def convert_texture_maps_as_single_output(staging_dir, source_image_outputs, - dest_image_outputs, has_udim=False, - log=None): + dest_image_outputs, log=None): oiio_tool_args = get_oiio_tool_args("oiiotool") source_maps = get_texture_outputs( - staging_dir, source_image_outputs, has_udim=has_udim) - dest_map = next(get_texture_outputs( - staging_dir, dest_image_outputs, has_udim=has_udim - ), None) + staging_dir, source_image_outputs) + dest_map = next( + (dest_texture for dest_texture in + get_texture_outputs( + staging_dir, dest_image_outputs)), None) log.info(f"{source_maps} composited as {dest_map}") oiio_cmd = oiio_tool_args + source_maps + [ @@ -75,7 +64,7 @@ class ExtractTexturesAsSingleOutput(publish.Extractor): settings_category = "substancepainter" # Run directly after textures export - order = publish.Extractor.order - 0.098 + order = publish.Extractor.order - 0.0991 def process(self, instance): if not instance.data.get("creator_attributes", {}).get( @@ -89,20 +78,17 @@ def process(self, instance): repre = representations[0] staging_dir = instance.data["stagingDir"] - source_image_outputs = instance.data["image_outputs"] + dest_image_outputs = instance.data["image_outputs"] has_udim = False - dest_image_outputs = [] - dest_files = repre["files"] - is_sequence = isinstance(dest_files, (list, tuple)) + source_image = repre["files"] + is_sequence = isinstance(source_image, (list, tuple)) if not is_sequence: - dest_image_outputs = [dest_image_outputs] + source_image_outputs = [source_image] else: - dest_image_outputs = dest_files - if "udim" in repre: - has_udim = True + source_image_outputs = source_image + repre["files"] = dest_image_outputs convert_texture_maps_as_single_output( staging_dir, source_image_outputs, - dest_image_outputs, has_udim=has_udim, - log=self.log + dest_image_outputs, log=self.log ) From a6a2123331497eb7f00ab7918433c4485fa3ca25 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 18 Feb 2025 17:37:36 +0800 Subject: [PATCH 12/14] make sure repre['files'] is string --- .../plugins/publish/extract_map_as_single_output.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py b/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py index 7e233a2..c0a2b48 100644 --- a/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py +++ b/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py @@ -79,14 +79,14 @@ def process(self, instance): staging_dir = instance.data["stagingDir"] dest_image_outputs = instance.data["image_outputs"] - has_udim = False source_image = repre["files"] is_sequence = isinstance(source_image, (list, tuple)) if not is_sequence: source_image_outputs = [source_image] else: source_image_outputs = source_image - repre["files"] = dest_image_outputs + repre["files"] = dest_image_outputs[0] + repre.pop("udim", None) convert_texture_maps_as_single_output( staging_dir, source_image_outputs, From bdf3838d457a2eabfb79d8dd9f27707ca0d95cc4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 18 Feb 2025 17:39:06 +0800 Subject: [PATCH 13/14] remove unused lib --- .../plugins/publish/extract_map_as_single_output.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py b/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py index c0a2b48..02fef74 100644 --- a/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py +++ b/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py @@ -1,4 +1,3 @@ -import clique import os from ayon_core.pipeline import publish From 4ccdc9f82b3c47364588f9c76e1a4e344a48df61 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 18 Feb 2025 17:39:44 +0800 Subject: [PATCH 14/14] update docstring --- .../plugins/publish/extract_map_as_single_output.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py b/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py index 02fef74..57a99dc 100644 --- a/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py +++ b/client/ayon_substancepainter/plugins/publish/extract_map_as_single_output.py @@ -8,8 +8,8 @@ def get_texture_outputs(staging_dir, image_outputs): - """Getting the expected texture output(s) with/without udim sequence - before merging them with oiio tools. + """Getting the expected texture output(s) before merging + them with oiio tools. Args: staging_dir (str): staging dir