From 69250cbd858f506659e76ac701bc3613af2924e7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 22 Aug 2024 01:52:38 +0200 Subject: [PATCH 1/3] Collect required data for local and farm render jobs to have a 'colorspace' value specified --- client/ayon_houdini/api/colorspace.py | 68 +++++++++---------- .../plugins/publish/collect_arnold_rop.py | 14 +--- .../plugins/publish/collect_karma_rop.py | 17 +---- .../publish/collect_local_render_instances.py | 48 ++++++++----- .../plugins/publish/collect_mantra_rop.py | 17 +---- .../plugins/publish/collect_redshift_rop.py | 17 +---- .../publish/collect_render_colorspace.py | 41 +++++++++++ .../plugins/publish/collect_usd_render.py | 20 +----- .../plugins/publish/collect_vray_rop.py | 17 +---- 9 files changed, 117 insertions(+), 142 deletions(-) create mode 100644 client/ayon_houdini/plugins/publish/collect_render_colorspace.py diff --git a/client/ayon_houdini/api/colorspace.py b/client/ayon_houdini/api/colorspace.py index ec6e4c2091..31c112c247 100644 --- a/client/ayon_houdini/api/colorspace.py +++ b/client/ayon_houdini/api/colorspace.py @@ -1,59 +1,55 @@ +from typing import List + import attr import hou from ayon_houdini.api.lib import get_color_management_preferences -from ayon_core.pipeline.colorspace import get_display_view_colorspace_name +from ayon_core.pipeline.colorspace import ( + get_display_view_colorspace_name, + get_ocio_config_colorspaces +) + @attr.s class LayerMetadata(object): """Data class for Render Layer metadata.""" - frameStart = attr.ib() - frameEnd = attr.ib() + products: "List[RenderProduct]" = attr.ib() @attr.s class RenderProduct(object): - """Getting Colorspace as - Specific Render Product Parameter for submitting - publish job. - - """ + """Specific Render Product Parameter for submitting.""" colorspace = attr.ib() # colorspace - view = attr.ib() productName = attr.ib(default=None) class ARenderProduct(object): + """This is the minimal data structure required to get + `ayon_core.pipeline.farm.pyblish_functions.create_instances_for_aov` to + work with deadline addon's job submissions.""" + # TODO: The exact data structure should actually be defined in core for all + # addons to align. + def __init__(self, aov_names: List[str]): + colorspace = get_scene_linear_colorspace() + products = [ + RenderProduct(colorspace=colorspace, productName=aov_name) + for aov_name in aov_names + ] + self.layer_data = LayerMetadata(products=products) - def __init__(self): - """Constructor.""" - # Initialize - self.layer_data = self._get_layer_data() - self.layer_data.products = self.get_colorspace_data() - - def _get_layer_data(self): - return LayerMetadata( - frameStart=int(hou.playbar.frameRange()[0]), - frameEnd=int(hou.playbar.frameRange()[1]), - ) - - def get_colorspace_data(self): - """To be implemented by renderer class. - This should return a list of RenderProducts. +def get_scene_linear_colorspace(): + """Return colorspace name for Houdini's OCIO config scene linear role. - Returns: - list: List of RenderProduct + By default, renderers in Houdini render output images in the scene linear + role colorspace. - """ - data = get_color_management_preferences() - colorspace_data = [ - RenderProduct( - colorspace=data["display"], - view=data["view"], - productName="" - ) - ] - return colorspace_data + Returns: + Optional[str]: The colorspace name for the 'scene_linear' role in + the OCIO config Houdini is currently set to. + """ + ocio_config_path = hou.Color.ocio_configPath() + colorspaces = get_ocio_config_colorspaces(ocio_config_path) + return colorspaces["roles"].get("scene_linear", {}).get("colorspace") def get_default_display_view_colorspace(): diff --git a/client/ayon_houdini/plugins/publish/collect_arnold_rop.py b/client/ayon_houdini/plugins/publish/collect_arnold_rop.py index 10c6d91d26..bebdb789de 100644 --- a/client/ayon_houdini/plugins/publish/collect_arnold_rop.py +++ b/client/ayon_houdini/plugins/publish/collect_arnold_rop.py @@ -4,11 +4,8 @@ import hou import pyblish.api -from ayon_houdini.api import colorspace, plugin -from ayon_houdini.api.lib import ( - get_color_management_preferences, - evalParmNoFrame -) +from ayon_houdini.api import plugin +from ayon_houdini.api.lib import evalParmNoFrame class CollectArnoldROPRenderProducts(plugin.HoudiniInstancePlugin): @@ -100,7 +97,6 @@ def process(self, instance): self.log.debug("Found render product: {}".format(product)) instance.data["files"] = list(render_products) - instance.data["renderProducts"] = colorspace.ARenderProduct() # For now by default do NOT try to publish the rendered output instance.data["publishJobState"] = "Suspended" @@ -110,12 +106,6 @@ def process(self, instance): instance.data["expectedFiles"] = list() instance.data["expectedFiles"].append(files_by_aov) - # update the colorspace data - colorspace_data = get_color_management_preferences() - instance.data["colorspaceConfig"] = colorspace_data["config"] - instance.data["colorspaceDisplay"] = colorspace_data["display"] - instance.data["colorspaceView"] = colorspace_data["view"] - def get_render_product_name(self, prefix, suffix): """Return the output filename using the AOV prefix and suffix""" diff --git a/client/ayon_houdini/plugins/publish/collect_karma_rop.py b/client/ayon_houdini/plugins/publish/collect_karma_rop.py index 60fec9d2e0..87b194ae8e 100644 --- a/client/ayon_houdini/plugins/publish/collect_karma_rop.py +++ b/client/ayon_houdini/plugins/publish/collect_karma_rop.py @@ -4,14 +4,8 @@ import hou import pyblish.api -from ayon_houdini.api.lib import ( - evalParmNoFrame, - get_color_management_preferences -) -from ayon_houdini.api import ( - colorspace, - plugin -) +from ayon_houdini.api.lib import evalParmNoFrame +from ayon_houdini.api import plugin class CollectKarmaROPRenderProducts(plugin.HoudiniInstancePlugin): @@ -63,7 +57,6 @@ def process(self, instance): filenames = list(render_products) instance.data["files"] = filenames - instance.data["renderProducts"] = colorspace.ARenderProduct() for product in render_products: self.log.debug("Found render product: %s" % product) @@ -72,12 +65,6 @@ def process(self, instance): instance.data["expectedFiles"] = list() instance.data["expectedFiles"].append(files_by_aov) - # update the colorspace data - colorspace_data = get_color_management_preferences() - instance.data["colorspaceConfig"] = colorspace_data["config"] - instance.data["colorspaceDisplay"] = colorspace_data["display"] - instance.data["colorspaceView"] = colorspace_data["view"] - def get_render_product_name(self, prefix, suffix): product_name = prefix if suffix: diff --git a/client/ayon_houdini/plugins/publish/collect_local_render_instances.py b/client/ayon_houdini/plugins/publish/collect_local_render_instances.py index 931a79535b..35c0e5afff 100644 --- a/client/ayon_houdini/plugins/publish/collect_local_render_instances.py +++ b/client/ayon_houdini/plugins/publish/collect_local_render_instances.py @@ -4,12 +4,15 @@ from ayon_core.pipeline.farm.patterning import match_aov_pattern from ayon_core.pipeline.publish import ( get_plugin_settings, - apply_plugin_settings_automatically + apply_plugin_settings_automatically, + ColormanagedPyblishPluginMixin ) from ayon_houdini.api import plugin +from ayon_houdini.api.colorspace import get_scene_linear_colorspace -class CollectLocalRenderInstances(plugin.HoudiniInstancePlugin): +class CollectLocalRenderInstances(plugin.HoudiniInstancePlugin, + ColormanagedPyblishPluginMixin): """Collect instances for local render. Agnostic Local Render Collector. @@ -49,9 +52,9 @@ def apply_settings(cls, project_settings): # get aov_filter from deadline settings cls.aov_filter = project_settings["deadline"]["publish"]["ProcessSubmittedJobOnFarm"]["aov_filter"] cls.aov_filter = { - item["name"]: item["value"] - for item in cls.aov_filter - } + item["name"]: item["value"] + for item in cls.aov_filter + } def process(self, instance): @@ -74,6 +77,14 @@ def process(self, instance): instance.data["productName"] ) + # NOTE: The assumption that the output image's colorspace is the + # scene linear role may be incorrect. Certain renderers, like + # Karma allow overriding explicitly the output colorspace of the + # image. Such override are currently not considered since these + # would need to be detected in a renderer-specific way and the + # majority of production scenarios these would not be overridden. + # TODO: Support renderer-specific explicit colorspace overrides + colorspace = get_scene_linear_colorspace() for aov_name, aov_filepaths in expectedFiles.items(): product_name = product_group @@ -108,6 +119,21 @@ def process(self, instance): if len(aov_filenames) == 1: aov_filenames = aov_filenames[0] + representation = { + "stagingDir": staging_dir, + "ext": ext, + "name": ext, + "tags": ["review"] if preview else [], + "files": aov_filenames, + "frameStart": instance.data["frameStartHandle"], + "frameEnd": instance.data["frameEndHandle"] + } + + # Set the colorspace for the representation + self.set_representation_colorspace(representation, + context, + colorspace=colorspace) + aov_instance.data.update({ # 'label': label, "task": instance.data["task"], @@ -120,17 +146,7 @@ def process(self, instance): "productGroup": product_group, "families": ["render.local.hou", "review"], "instance_node": instance.data["instance_node"], - "representations": [ - { - "stagingDir": staging_dir, - "ext": ext, - "name": ext, - "tags": ["review"] if preview else [], - "files": aov_filenames, - "frameStart": instance.data["frameStartHandle"], - "frameEnd": instance.data["frameEndHandle"] - } - ] + "representations": [representation] }) # Skip integrating original render instance. diff --git a/client/ayon_houdini/plugins/publish/collect_mantra_rop.py b/client/ayon_houdini/plugins/publish/collect_mantra_rop.py index f7feeee63b..f26768c3e9 100644 --- a/client/ayon_houdini/plugins/publish/collect_mantra_rop.py +++ b/client/ayon_houdini/plugins/publish/collect_mantra_rop.py @@ -4,14 +4,8 @@ import hou import pyblish.api -from ayon_houdini.api.lib import ( - evalParmNoFrame, - get_color_management_preferences -) -from ayon_houdini.api import ( - colorspace, - plugin -) +from ayon_houdini.api.lib import evalParmNoFrame +from ayon_houdini.api import plugin class CollectMantraROPRenderProducts(plugin.HoudiniInstancePlugin): @@ -108,7 +102,6 @@ def process(self, instance): filenames = list(render_products) instance.data["files"] = filenames - instance.data["renderProducts"] = colorspace.ARenderProduct() # For now by default do NOT try to publish the rendered output instance.data["publishJobState"] = "Suspended" @@ -118,12 +111,6 @@ def process(self, instance): instance.data["expectedFiles"] = list() instance.data["expectedFiles"].append(files_by_aov) - # update the colorspace data - colorspace_data = get_color_management_preferences() - instance.data["colorspaceConfig"] = colorspace_data["config"] - instance.data["colorspaceDisplay"] = colorspace_data["display"] - instance.data["colorspaceView"] = colorspace_data["view"] - def get_render_product_name(self, prefix, suffix): product_name = prefix if suffix: diff --git a/client/ayon_houdini/plugins/publish/collect_redshift_rop.py b/client/ayon_houdini/plugins/publish/collect_redshift_rop.py index 96cb6ebeaf..bd43805497 100644 --- a/client/ayon_houdini/plugins/publish/collect_redshift_rop.py +++ b/client/ayon_houdini/plugins/publish/collect_redshift_rop.py @@ -4,14 +4,8 @@ import hou import pyblish.api -from ayon_houdini.api.lib import ( - evalParmNoFrame, - get_color_management_preferences -) -from ayon_houdini.api import ( - colorspace, - plugin -) +from ayon_houdini.api.lib import evalParmNoFrame +from ayon_houdini.api import plugin class CollectRedshiftROPRenderProducts(plugin.HoudiniInstancePlugin): @@ -119,7 +113,6 @@ def process(self, instance): filenames = list(render_products) instance.data["files"] = filenames - instance.data["renderProducts"] = colorspace.ARenderProduct() # For now by default do NOT try to publish the rendered output instance.data["publishJobState"] = "Suspended" @@ -129,12 +122,6 @@ def process(self, instance): instance.data["expectedFiles"] = [] instance.data["expectedFiles"].append(files_by_aov) - # update the colorspace data - colorspace_data = get_color_management_preferences() - instance.data["colorspaceConfig"] = colorspace_data["config"] - instance.data["colorspaceDisplay"] = colorspace_data["display"] - instance.data["colorspaceView"] = colorspace_data["view"] - def get_render_product_name(self, prefix, suffix): """Return the output filename using the AOV prefix and suffix""" diff --git a/client/ayon_houdini/plugins/publish/collect_render_colorspace.py b/client/ayon_houdini/plugins/publish/collect_render_colorspace.py new file mode 100644 index 0000000000..1166243ce3 --- /dev/null +++ b/client/ayon_houdini/plugins/publish/collect_render_colorspace.py @@ -0,0 +1,41 @@ +from ayon_houdini.api import plugin, colorspace + +import pyblish.api + + +class CollectHoudiniRenderColorspace(plugin.HoudiniInstancePlugin): + """Collect Colorspace data for render output images. + + This currently assumes that all render products are in 'scene_linear' + colorspace role - which is the default behavior for renderers in Houdini. + """ + + label = "Collect Render Colorspace" + order = pyblish.api.CollectorOrder + 0.15 + families = ["mantra_rop", + "karma_rop", + "redshift_rop", + "arnold_rop", + "vray_rop", + "usdrender"] + + def process(self, instance): + # Set the required data for `ayon_core.pipeline.farm.pyblish_functions` + # functions used for farm publish job processing. + + # Define render products for `create_instances_for_aov` + # which uses it in `_create_instances_for_aov()` to match the render + # product's name to aovs to define the colorspace. + expected_files = instance.data["expectedFiles"] + aov_name = list(expected_files[0].keys()) + render_products_data = colorspace.ARenderProduct(aov_name) + instance.data["renderProducts"] = render_products_data + + # Required data for `create_instances_for_aov` + colorspace_data = colorspace.get_color_management_preferences() + instance.data["colorspaceConfig"] = colorspace_data["config"] + instance.data["colorspaceDisplay"] = colorspace_data["display"] + instance.data["colorspaceView"] = colorspace_data["view"] + + # Used in `create_skeleton_instance()` + instance.data["colorspace"] = colorspace.get_scene_linear_colorspace() diff --git a/client/ayon_houdini/plugins/publish/collect_usd_render.py b/client/ayon_houdini/plugins/publish/collect_usd_render.py index a6e7572a18..2934cce246 100644 --- a/client/ayon_houdini/plugins/publish/collect_usd_render.py +++ b/client/ayon_houdini/plugins/publish/collect_usd_render.py @@ -4,14 +4,8 @@ import hou import pyblish.api -from ayon_houdini.api import ( - colorspace, - plugin -) -from ayon_houdini.api.lib import ( - evalParmNoFrame, - get_color_management_preferences -) +from ayon_houdini.api import plugin +from ayon_houdini.api.lib import evalParmNoFrame class CollectUsdRender(plugin.HoudiniInstancePlugin): @@ -23,9 +17,6 @@ class CollectUsdRender(plugin.HoudiniInstancePlugin): Provides: instance -> ifdFile - instance -> colorspaceConfig - instance -> colorspaceDisplay - instance -> colorspaceView """ @@ -75,12 +66,5 @@ def replace_to_f(match): if "$F" not in export_file: instance.data["splitRenderFrameDependent"] = False - # update the colorspace data - colorspace_data = get_color_management_preferences() - instance.data["colorspaceConfig"] = colorspace_data["config"] - instance.data["colorspaceDisplay"] = colorspace_data["display"] - instance.data["colorspaceView"] = colorspace_data["view"] - # stub required data for Submit Publish Job publish plug-in instance.data["attachTo"] = [] - instance.data["renderProducts"] = colorspace.ARenderProduct() diff --git a/client/ayon_houdini/plugins/publish/collect_vray_rop.py b/client/ayon_houdini/plugins/publish/collect_vray_rop.py index 2f9c2bb18e..ed360b37a5 100644 --- a/client/ayon_houdini/plugins/publish/collect_vray_rop.py +++ b/client/ayon_houdini/plugins/publish/collect_vray_rop.py @@ -4,14 +4,8 @@ import hou import pyblish.api -from ayon_houdini.api.lib import ( - evalParmNoFrame, - get_color_management_preferences -) -from ayon_houdini.api import ( - colorspace, - plugin -) +from ayon_houdini.api.lib import evalParmNoFrame +from ayon_houdini.api import plugin class CollectVrayROPRenderProducts(plugin.HoudiniInstancePlugin): @@ -89,7 +83,6 @@ def process(self, instance): self.log.debug("Found render product: %s" % product) filenames = list(render_products) instance.data["files"] = filenames - instance.data["renderProducts"] = colorspace.ARenderProduct() # For now by default do NOT try to publish the rendered output instance.data["publishJobState"] = "Suspended" @@ -100,12 +93,6 @@ def process(self, instance): instance.data["expectedFiles"].append(files_by_aov) self.log.debug("expectedFiles:{}".format(files_by_aov)) - # update the colorspace data - colorspace_data = get_color_management_preferences() - instance.data["colorspaceConfig"] = colorspace_data["config"] - instance.data["colorspaceDisplay"] = colorspace_data["display"] - instance.data["colorspaceView"] = colorspace_data["view"] - def get_render_product_name(self, prefix, suffix=""): """Return the beauty output filename if render element enabled """ From 3541eb3c0b71938d278aaddf34e2982e8bc0a6d7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 22 Aug 2024 15:11:31 +0200 Subject: [PATCH 2/3] Force retrieving a default value for `view` if OCIO config is specified --- client/ayon_houdini/api/lib.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/client/ayon_houdini/api/lib.py b/client/ayon_houdini/api/lib.py index 3c2520250d..79935e40a3 100644 --- a/client/ayon_houdini/api/lib.py +++ b/client/ayon_houdini/api/lib.py @@ -579,12 +579,33 @@ def replace(match): def get_color_management_preferences(): """Get default OCIO preferences""" - return { + + preferences = { "config": hou.Color.ocio_configPath(), "display": hou.Color.ocio_defaultDisplay(), "view": hou.Color.ocio_defaultView() } + # Note: For whatever reason they are cases where `view` may be an empty + # string even though a valid default display is set where `PyOpenColorIO` + # does correctly return the values. + # Workaround to get the correct default view + if preferences["config"] and not preferences["view"]: + log.debug( + "Houdini `hou.Color.ocio_defaultView()` returned empty value." + " Falling back to `PyOpenColorIO` to get the default view.") + import PyOpenColorIO + config_path = preferences["config"] + config = PyOpenColorIO.Config.CreateFromFile(config_path) + display = config.getDefaultDisplay() + assert display == preferences["display"], \ + "Houdini default OCIO display must match config default display" + view = config.getDefaultView(display) + preferences["display"] = display + preferences["view"] = view + + return preferences + def get_obj_node_output(obj_node): """Find output node. From efdb4a4855693bb65f47ac6031157c2ba4b8831c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 22 Aug 2024 15:16:50 +0200 Subject: [PATCH 3/3] Do not hard error on Hou 19.5 or below --- client/ayon_houdini/api/lib.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/client/ayon_houdini/api/lib.py b/client/ayon_houdini/api/lib.py index 79935e40a3..dd89d2a922 100644 --- a/client/ayon_houdini/api/lib.py +++ b/client/ayon_houdini/api/lib.py @@ -594,7 +594,15 @@ def get_color_management_preferences(): log.debug( "Houdini `hou.Color.ocio_defaultView()` returned empty value." " Falling back to `PyOpenColorIO` to get the default view.") - import PyOpenColorIO + try: + import PyOpenColorIO + except ImportError: + log.warning( + "Unable to workaround empty return value of " + "`hou.Color.ocio_defaultView()` because `PyOpenColorIO` is " + "not available.") + return preferences + config_path = preferences["config"] config = PyOpenColorIO.Config.CreateFromFile(config_path) display = config.getDefaultDisplay()