From 6bd93217daf7a8c8dbe9ef4140357c1d8c34974e Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 14 Feb 2023 15:08:08 +0000 Subject: [PATCH 01/87] Publish cbid with ass files. --- .../publish/extract_arnold_scene_source.py | 24 +++++++ .../publish/validate_arnold_scene_source.py | 2 +- .../validate_arnold_scene_source_cbid.py | 72 +++++++++++++++++++ 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py diff --git a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py index 924ac58c40..bb27705d2c 100644 --- a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py @@ -139,6 +139,30 @@ class ExtractArnoldSceneSource(publish.Extractor): duplicate_nodes.append(duplicate_transform) delete_bin.append(duplicate_transform) + # Copy cbId from original to mtoa_constant. + attr_name = "mtoa_constant_cbId" + duplicate_shapes = cmds.listRelatives( + duplicate_transform, shapes=True + ) + original_shapes = cmds.listRelatives(node, shapes=True) + for duplicate_shape in duplicate_shapes: + duplicate_path = ( + duplicate_transform + "|" + duplicate_shape + ) + for original_shape in original_shapes: + original_path = node + "|" + original_shape + if duplicate_shape == original_shape: + cmds.addAttr( + duplicate_path, + longName=attr_name, + dataType="string" + ) + cmds.setAttr( + duplicate_path + "." + attr_name, + cmds.getAttr(original_path + ".cbId"), + type="string" + ) + with attribute_values(attribute_data): with maintained_selection(): self.log.info( diff --git a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py index 3b0ffd52d7..e9f6d218f9 100644 --- a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py @@ -37,7 +37,7 @@ class ValidateArnoldSceneSource(pyblish.api.InstancePlugin): nodes_by_name[node_split[-1]] = node for shape in cmds.listRelatives(node, shapes=True): - nodes_by_name[shape.split("|")[-1]] = shape + nodes_by_name[shape.split("|")[-1]] = node + "|" + shape return ungrouped_nodes, nodes_by_name, parents diff --git a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py new file mode 100644 index 0000000000..bb8ea88453 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py @@ -0,0 +1,72 @@ +import maya.cmds as cmds + +import pyblish.api +from openpype.pipeline.publish import ( + ValidateContentsOrder, PublishValidationError, RepairAction +) + + +class ValidateArnoldSceneSourceCbid(pyblish.api.InstancePlugin): + """Validate Arnold Scene Source Cbid. + + It is required for the proxy and content nodes to share the same cbid. + """ + + order = ValidateContentsOrder + hosts = ["maya"] + families = ["ass"] + label = "Validate Arnold Scene Source CBID" + actions = [RepairAction] + + @staticmethod + def _get_nodes_data(nodes): + nodes_by_name = {} + for node in nodes: + node_split = node.split("|") + nodes_by_name[node_split[-1]] = node + for shape in cmds.listRelatives(node, shapes=True): + nodes_by_name[shape.split("|")[-1]] = node + "|" + shape + + return nodes_by_name + + def get_invalid_couples(self, instance): + content_nodes_by_name = self._get_nodes_data( + instance.data["setMembers"] + ) + proxy_nodes_by_name = self._get_nodes_data( + instance.data.get("proxy", []) + ) + + invalid_couples = [] + for content_name, content_node in content_nodes_by_name.items(): + for proxy_name, proxy_node in proxy_nodes_by_name.items(): + if content_name == proxy_name: + content_value = cmds.getAttr(content_node + ".cbId") + proxy_value = cmds.getAttr(proxy_node + ".cbId") + if content_value != proxy_value: + invalid_couples.append((content_node, proxy_node)) + + return invalid_couples + + def process(self, instance): + # Proxy validation. + if not instance.data.get("proxy", []): + return + + # Validate for proxy nodes sharing the same cbId as content nodes. + invalid_couples = self.get_invalid_couples(instance) + if invalid_couples: + raise PublishValidationError( + "Found proxy nodes with mismatching cbid:\n{}".format( + invalid_couples + ) + ) + + @classmethod + def repair(cls, instance): + for content_node, proxy_node in cls.get_invalid_couples(cls, instance): + cmds.setAttr( + proxy_node + ".cbId", + cmds.getAttr(content_node + ".cbId"), + type="string" + ) From d4c001684c1f3319ecfe152a054fc84767c387db Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 15 Feb 2023 12:27:50 +0000 Subject: [PATCH 02/87] Working shader assignments --- openpype/tools/mayalookassigner/app.py | 23 +- .../tools/mayalookassigner/arnold_standin.py | 225 ++++++++++++++++++ openpype/tools/mayalookassigner/commands.py | 10 +- 3 files changed, 250 insertions(+), 8 deletions(-) create mode 100644 openpype/tools/mayalookassigner/arnold_standin.py diff --git a/openpype/tools/mayalookassigner/app.py b/openpype/tools/mayalookassigner/app.py index f9508657e5..e66f0d73e2 100644 --- a/openpype/tools/mayalookassigner/app.py +++ b/openpype/tools/mayalookassigner/app.py @@ -24,6 +24,7 @@ from .commands import ( remove_unused_looks ) from .vray_proxies import vrayproxy_assign_look +from . import arnold_standin module = sys.modules[__name__] module.window = None @@ -43,7 +44,7 @@ class MayaLookAssignerWindow(QtWidgets.QWidget): filename = get_workfile() self.setObjectName("lookManager") - self.setWindowTitle("Look Manager 1.3.0 - [{}]".format(filename)) + self.setWindowTitle("Look Manager 1.4.0 - [{}]".format(filename)) self.setWindowFlags(QtCore.Qt.Window) self.setParent(parent) @@ -240,18 +241,26 @@ class MayaLookAssignerWindow(QtWidgets.QWidget): )) nodes = item["nodes"] + # Assign Vray Proxy look. if cmds.pluginInfo('vrayformaya', query=True, loaded=True): self.echo("Getting vray proxy nodes ...") vray_proxies = set(cmds.ls(type="VRayProxy", long=True)) - if vray_proxies: - for vp in vray_proxies: - if vp in nodes: - vrayproxy_assign_look(vp, subset_name) + for vp in vray_proxies: + if vp in nodes: + vrayproxy_assign_look(vp, subset_name) - nodes = list(set(item["nodes"]).difference(vray_proxies)) + nodes = list(set(item["nodes"]).difference(vray_proxies)) - # Assign look + # Assign Arnold Standin look. + arnold_standins = set(cmds.ls(type="aiStandIn", long=True)) + for standin in arnold_standins: + if standin in nodes: + arnold_standin.assign_look(standin, subset_name) + + nodes = list(set(item["nodes"]).difference(arnold_standins)) + + # Assign look if nodes: assign_look_by_version(nodes, version_id=version["_id"]) diff --git a/openpype/tools/mayalookassigner/arnold_standin.py b/openpype/tools/mayalookassigner/arnold_standin.py new file mode 100644 index 0000000000..6b5ab10720 --- /dev/null +++ b/openpype/tools/mayalookassigner/arnold_standin.py @@ -0,0 +1,225 @@ +import os +import re +from collections import defaultdict +import json +import logging + +from maya import cmds + +from openpype.pipeline import ( + legacy_io, + get_representation_path, + registered_host, + discover_loader_plugins, + loaders_from_representation, + load_container +) +from openpype.client import ( + get_representation_by_name, + get_last_version_by_subset_name +) +from openpype.hosts.maya.api import lib + + +log = logging.getLogger(__name__) + + +def get_cbid_by_node(path): + """Get cbid from Arnold Scene Source. + + Args: + path (string): Path to Arnold Scene Source. + + Returns: + (dict): Dictionary with node full name/path and CBID. + """ + import arnold + results = {} + + arnold.AiBegin() + + arnold.AiMsgSetConsoleFlags(arnold.AI_LOG_ALL) + + arnold.AiSceneLoad(None, path, None) + + # Iterate over all shader nodes + iter = arnold.AiUniverseGetNodeIterator(arnold.AI_NODE_SHAPE) + while not arnold.AiNodeIteratorFinished(iter): + node = arnold.AiNodeIteratorGetNext(iter) + if arnold.AiNodeIs(node, "polymesh"): + node_name = arnold.AiNodeGetName(node) + try: + results[arnold.AiNodeGetStr(node, "cbId")].append(node_name) + except KeyError: + results[arnold.AiNodeGetStr(node, "cbId")] = [node_name] + + arnold.AiNodeIteratorDestroy(iter) + arnold.AiEnd() + + return results + + +def get_standin_path(node): + path = cmds.getAttr(node + ".dso") + + # Account for frame extension. + basename = os.path.basename(path) + current_frame = 1 + pattern = "(#+)" + matches = re.findall(pattern, basename) + if matches: + substring = "%{}d".format(str(len(matches[0])).zfill(2)) + path = path.replace(matches[0], substring) + path = path % current_frame + + return path + + +def assign_look(standin, subset): + log.info("Assigning {} to {}.".format(subset, standin)) + + nodes_by_id = get_cbid_by_node(get_standin_path(standin)) + + # Group by asset id so we run over the look per asset + node_ids_by_asset_id = defaultdict(set) + for node_id in nodes_by_id: + asset_id = node_id.split(":", 1)[0] + node_ids_by_asset_id[asset_id].add(node_id) + + project_name = legacy_io.active_project() + for asset_id, node_ids in node_ids_by_asset_id.items(): + + # Get latest look version + version = get_last_version_by_subset_name( + project_name, + subset_name=subset, + asset_id=asset_id, + fields=["_id"] + ) + if not version: + log.info("Didn't find last version for subset name {}".format( + subset + )) + continue + + # Relationships. + json_representation = get_representation_by_name( + project_name, representation_name="json", version_id=version["_id"] + ) + + # Load relationships + shader_relation = get_representation_path(json_representation) + with open(shader_relation, "r") as f: + relationships = json.load(f) + + # Load look. + # Get representations of shader file and relationships + look_representation = get_representation_by_name( + project_name, representation_name="ma", version_id=version["_id"] + ) + + # See if representation is already loaded, if so reuse it. + host = registered_host() + representation_id = str(look_representation['_id']) + for container in host.ls(): + if (container['loader'] == "LookLoader" and + container['representation'] == representation_id): + log.info("Reusing loaded look ...") + container_node = container['objectName'] + break + else: + log.info("Using look for the first time ...") + + # Load file + all_loaders = discover_loader_plugins() + loaders = loaders_from_representation( + all_loaders, representation_id + ) + loader = next( + (i for i in loaders if i.__name__ == "LookLoader"), None) + if loader is None: + raise RuntimeError("Could not find LookLoader, this is a bug") + + # Reference the look file + with lib.maintained_selection(): + container_node = load_container(loader, look_representation) + + # Get container members + shader_nodes = lib.get_container_members(container_node) + + # Get only the node ids and paths related to this asset + # And get the shader edits the look supplies + asset_nodes_by_id = { + node_id: nodes_by_id[node_id] for node_id in node_ids + } + edits = list( + lib.iter_shader_edits( + relationships, shader_nodes, asset_nodes_by_id + ) + ) + + # Create assignments + assignments = {} + for edit in edits: + if edit["action"] == "assign": + nodes = edit["nodes"] + shader = edit["shader"] + if not cmds.ls(shader, type="shadingEngine"): + log.info("Skipping non-shader: %s" % shader) + continue + + inputs = cmds.listConnections( + shader + ".surfaceShader", source=True) + if not inputs: + log.info("Shading engine missing material: %s" % shader) + + # Strip off component assignments + for i, node in enumerate(nodes): + if "." in node: + log.warning( + ("Converting face assignment to full object " + "assignment. This conversion can be lossy: " + "{}").format(node)) + nodes[i] = node.split(".")[0] + + material = inputs[0] + assignments[material] = nodes + + # Assign shader + # Clear all current shader assignments + plug = standin + ".operators" + num = cmds.getAttr(plug, size=True) + for i in reversed(range(num)): + cmds.removeMultiInstance("{}[{}]".format(plug, i), b=True) + + # Create new assignment overrides + index = 0 + for material, paths in assignments.items(): + for path in paths: + operator = cmds.createNode("aiSetParameter") + cmds.setAttr(operator + ".selection", path, type="string") + operator_assignments = { + "shader": { + "value": material, + "index": 0, + "enabled": True + } + } + for assignee, data in operator_assignments.items(): + cmds.setAttr( + "{}.assignment[{}]".format(operator, data["index"]), + "{}='{}'".format(assignee, data["value"]), + type="string" + ) + cmds.setAttr( + "{}.enableAssignment[{}]".format( + operator, data["index"] + ), + data["enabled"] + ) + + cmds.connectAttr( + operator + ".out", "{}[{}]".format(plug, index) + ) + + index += 1 diff --git a/openpype/tools/mayalookassigner/commands.py b/openpype/tools/mayalookassigner/commands.py index 2e7a51efde..69fcc77bce 100644 --- a/openpype/tools/mayalookassigner/commands.py +++ b/openpype/tools/mayalookassigner/commands.py @@ -1,6 +1,7 @@ from collections import defaultdict import logging import os +import re import maya.cmds as cmds @@ -13,6 +14,7 @@ from openpype.pipeline import ( from openpype.hosts.maya.api import lib from .vray_proxies import get_alembic_ids_cache +from . import arnold_standin log = logging.getLogger(__name__) @@ -107,6 +109,7 @@ def create_asset_id_hash(nodes): """ node_id_hash = defaultdict(list) for node in nodes: + shapes = cmds.ls(cmds.listRelatives(node, shapes=True), long=True) # iterate over content of reference node if cmds.nodeType(node) == "reference": ref_hashes = create_asset_id_hash( @@ -122,7 +125,12 @@ def create_asset_id_hash(nodes): pid = k.split(":")[0] if node not in node_id_hash[pid]: node_id_hash[pid].append(node) - + elif shapes and cmds.nodeType(shapes[0]) == "aiStandIn": + path = arnold_standin.get_standin_path(shapes[0]) + for id, _ in arnold_standin.get_cbid_by_node(path).items(): + pid = id.split(":")[0] + if shapes[0] not in node_id_hash[pid]: + node_id_hash[pid].append(shapes[0]) else: value = lib.get_id(node) if value is None: From fafd55cfb1cbcf797ce5a8fa747f2efbc4497236 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 16 Feb 2023 08:12:26 +0000 Subject: [PATCH 03/87] Better support for namespace when publishing ASS --- .../publish/extract_arnold_scene_source.py | 15 ++++++++++++--- .../publish/validate_arnold_scene_source.py | 5 +++-- .../publish/validate_arnold_scene_source_cbid.py | 5 +++-- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py index bb27705d2c..8c9d90e2e6 100644 --- a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py @@ -113,14 +113,22 @@ class ExtractArnoldSceneSource(publish.Extractor): instance.data["representations"].append(representation) def _extract(self, nodes, attribute_data, kwargs): - self.log.info("Writing: " + kwargs["filename"]) + self.log.info( + "Writing {} with:\n{}".format(kwargs["filename"], kwargs) + ) filenames = [] # Duplicating nodes so they are direct children of the world. This # makes the hierarchy of any exported ass file the same. with delete_after() as delete_bin: duplicate_nodes = [] for node in nodes: + parent = cmds.ls( + cmds.listRelatives(node, parent=True)[0], long=True + )[0] duplicate_transform = cmds.duplicate(node)[0] + duplicate_transform = "{}|{}".format( + parent, duplicate_transform + ) # Discard the children. shapes = cmds.listRelatives(duplicate_transform, shapes=True) @@ -133,8 +141,9 @@ class ExtractArnoldSceneSource(publish.Extractor): duplicate_transform, world=True )[0] - cmds.rename(duplicate_transform, node.split("|")[-1]) - duplicate_transform = "|" + node.split("|")[-1] + basename = node.split("|")[-1].split(":")[-1] + cmds.rename(duplicate_transform, basename) + duplicate_transform = "|" + basename duplicate_nodes.append(duplicate_transform) delete_bin.append(duplicate_transform) diff --git a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py index e9f6d218f9..84240e63e6 100644 --- a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py @@ -35,9 +35,10 @@ class ValidateArnoldSceneSource(pyblish.api.InstancePlugin): if parent: parents.append(parent) - nodes_by_name[node_split[-1]] = node + nodes_by_name[node_split[-1].split(":")[-1]] = node for shape in cmds.listRelatives(node, shapes=True): - nodes_by_name[shape.split("|")[-1]] = node + "|" + shape + basename = shape.split("|")[-1].split(":")[-1] + nodes_by_name[basename] = node + "|" + shape return ungrouped_nodes, nodes_by_name, parents diff --git a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py index bb8ea88453..056cc94edf 100644 --- a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py +++ b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py @@ -23,9 +23,10 @@ class ValidateArnoldSceneSourceCbid(pyblish.api.InstancePlugin): nodes_by_name = {} for node in nodes: node_split = node.split("|") - nodes_by_name[node_split[-1]] = node + nodes_by_name[node_split[-1].split(":")[-1]] = node for shape in cmds.listRelatives(node, shapes=True): - nodes_by_name[shape.split("|")[-1]] = node + "|" + shape + basename = shape.split("|")[-1].split(":")[-1] + nodes_by_name[basename] = node + "|" + shape return nodes_by_name From cbb04773335311c6af2d9311ad045ae12e42d778 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 16 Feb 2023 08:12:37 +0000 Subject: [PATCH 04/87] Fix loading ASS --- openpype/hosts/maya/plugins/load/load_arnold_standin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_arnold_standin.py b/openpype/hosts/maya/plugins/load/load_arnold_standin.py index 66e8b69639..5a216b930b 100644 --- a/openpype/hosts/maya/plugins/load/load_arnold_standin.py +++ b/openpype/hosts/maya/plugins/load/load_arnold_standin.py @@ -179,7 +179,7 @@ class ArnoldStandinLoader(load.LoaderPlugin): # If no proxy exists, the string operator wont replace anything. cmds.setAttr( string_replace_operator + ".match", - "resources/" + proxy_basename, + proxy_basename, type="string" ) cmds.setAttr( From cf4fd979cb6fd3431a365267661a3720ccb1f436 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 16 Feb 2023 08:13:29 +0000 Subject: [PATCH 05/87] Support attribute assignments --- .../tools/mayalookassigner/arnold_standin.py | 85 ++++++++++++------- 1 file changed, 52 insertions(+), 33 deletions(-) diff --git a/openpype/tools/mayalookassigner/arnold_standin.py b/openpype/tools/mayalookassigner/arnold_standin.py index 6b5ab10720..3290944df9 100644 --- a/openpype/tools/mayalookassigner/arnold_standin.py +++ b/openpype/tools/mayalookassigner/arnold_standin.py @@ -24,6 +24,12 @@ from openpype.hosts.maya.api import lib log = logging.getLogger(__name__) +ATTRIBUTE_MAPPING = { + "aiSubdivType": "subdiv_type", + "aiSubdivIterations": "subdiv_iterations" +} + + def get_cbid_by_node(path): """Get cbid from Arnold Scene Source. @@ -146,6 +152,7 @@ def assign_look(standin, subset): # Get container members shader_nodes = lib.get_container_members(container_node) + namespace = shader_nodes[0].split(":")[0] # Get only the node ids and paths related to this asset # And get the shader edits the look supplies @@ -159,31 +166,49 @@ def assign_look(standin, subset): ) # Create assignments - assignments = {} + node_assignments = {} for edit in edits: + for node in edit["nodes"]: + if node not in node_assignments: + node_assignments[node] = [] + if edit["action"] == "assign": - nodes = edit["nodes"] - shader = edit["shader"] - if not cmds.ls(shader, type="shadingEngine"): - log.info("Skipping non-shader: %s" % shader) + if not cmds.ls(edit["shader"], type="shadingEngine"): + log.info("Skipping non-shader: %s" % edit["shader"]) continue inputs = cmds.listConnections( - shader + ".surfaceShader", source=True) + edit["shader"] + ".surfaceShader", source=True) if not inputs: - log.info("Shading engine missing material: %s" % shader) + log.info( + "Shading engine missing material: %s" % edit["shader"] + ) # Strip off component assignments - for i, node in enumerate(nodes): + for i, node in enumerate(edit["nodes"]): if "." in node: log.warning( ("Converting face assignment to full object " "assignment. This conversion can be lossy: " "{}").format(node)) - nodes[i] = node.split(".")[0] + edit["nodes"][i] = node.split(".")[0] - material = inputs[0] - assignments[material] = nodes + assignment = "shader='{}'".format(inputs[0]) + for node in edit["nodes"]: + node_assignments[node].append(assignment) + + if edit["action"] == "setattr": + for attr, value in edit["attributes"].items(): + if attr not in ATTRIBUTE_MAPPING: + log.warning( + "Skipping setting attribute {} on {} because it is" + " not recognized.".format(attr, edit["nodes"]) + ) + continue + + assignment = "{}={}".format(ATTRIBUTE_MAPPING[attr], value) + for node in edit["nodes"]: + node_assignments[node].append(assignment) # Assign shader # Clear all current shader assignments @@ -194,32 +219,26 @@ def assign_look(standin, subset): # Create new assignment overrides index = 0 - for material, paths in assignments.items(): - for path in paths: + for node, assignments in node_assignments.items(): + if not assignments: + continue + + with lib.maintained_selection(): operator = cmds.createNode("aiSetParameter") - cmds.setAttr(operator + ".selection", path, type="string") - operator_assignments = { - "shader": { - "value": material, - "index": 0, - "enabled": True - } - } - for assignee, data in operator_assignments.items(): - cmds.setAttr( - "{}.assignment[{}]".format(operator, data["index"]), - "{}='{}'".format(assignee, data["value"]), - type="string" - ) - cmds.setAttr( - "{}.enableAssignment[{}]".format( - operator, data["index"] - ), - data["enabled"] - ) + operator = cmds.rename(operator, namespace + ":" + operator) + + cmds.setAttr(operator + ".selection", node, type="string") + for i, assignment in enumerate(assignments): + cmds.setAttr( + "{}.assignment[{}]".format(operator, i), + assignment, + type="string" + ) cmds.connectAttr( operator + ".out", "{}[{}]".format(plug, index) ) index += 1 + + cmds.sets(operator, edit=True, addElement=container_node[0]) From 2cb2ba00b788b1423d86098bf99d50b6f27ccc3b Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 16 Feb 2023 08:30:57 +0000 Subject: [PATCH 06/87] Fix collecting and assigning enum values --- openpype/hosts/maya/plugins/publish/collect_look.py | 2 +- openpype/tools/mayalookassigner/arnold_standin.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index b01160a1c0..287ddc228b 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -556,7 +556,7 @@ class CollectLook(pyblish.api.InstancePlugin): continue if cmds.getAttr(attribute, type=True) == "message": continue - node_attributes[attr] = cmds.getAttr(attribute) + node_attributes[attr] = cmds.getAttr(attribute, asString=True) # Only include if there are any properties we care about if not node_attributes: continue diff --git a/openpype/tools/mayalookassigner/arnold_standin.py b/openpype/tools/mayalookassigner/arnold_standin.py index 3290944df9..392fe32148 100644 --- a/openpype/tools/mayalookassigner/arnold_standin.py +++ b/openpype/tools/mayalookassigner/arnold_standin.py @@ -206,7 +206,11 @@ def assign_look(standin, subset): ) continue + if isinstance(value, str): + value = "'{}'".format(value) + assignment = "{}={}".format(ATTRIBUTE_MAPPING[attr], value) + for node in edit["nodes"]: node_assignments[node].append(assignment) From 0e70a9559bd1238a586ce80bf178ee29ce0313f1 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 16 Feb 2023 09:03:55 +0000 Subject: [PATCH 07/87] Support displacement shader --- .../tools/mayalookassigner/arnold_standin.py | 56 ++++++++++++------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/openpype/tools/mayalookassigner/arnold_standin.py b/openpype/tools/mayalookassigner/arnold_standin.py index 392fe32148..0ba1526cda 100644 --- a/openpype/tools/mayalookassigner/arnold_standin.py +++ b/openpype/tools/mayalookassigner/arnold_standin.py @@ -81,6 +81,31 @@ def get_standin_path(node): return path +def shading_engine_assignments(shading_engine, attr, nodes, assignments): + shader_inputs = cmds.listConnections( + shading_engine + "." + attr, source=True) + if not shader_inputs: + log.info( + "Shading engine \"{}\" missing input \"{}\"".format( + shading_engine, attr + ) + ) + + # Strip off component assignments + for i, node in enumerate(nodes): + if "." in node: + log.warning( + ("Converting face assignment to full object " + "assignment. This conversion can be lossy: " + "{}").format(node)) + nodes[i] = node.split(".")[0] + + shader_type = "shader" if attr == "surfaceShader" else "disp_map" + assignment = "{}='{}'".format(shader_type, shader_inputs[0]) + for node in nodes: + assignments[node].append(assignment) + + def assign_look(standin, subset): log.info("Assigning {} to {}.".format(subset, standin)) @@ -177,25 +202,18 @@ def assign_look(standin, subset): log.info("Skipping non-shader: %s" % edit["shader"]) continue - inputs = cmds.listConnections( - edit["shader"] + ".surfaceShader", source=True) - if not inputs: - log.info( - "Shading engine missing material: %s" % edit["shader"] - ) - - # Strip off component assignments - for i, node in enumerate(edit["nodes"]): - if "." in node: - log.warning( - ("Converting face assignment to full object " - "assignment. This conversion can be lossy: " - "{}").format(node)) - edit["nodes"][i] = node.split(".")[0] - - assignment = "shader='{}'".format(inputs[0]) - for node in edit["nodes"]: - node_assignments[node].append(assignment) + shading_engine_assignments( + edit["shader"], + "surfaceShader", + edit["nodes"], + node_assignments + ) + shading_engine_assignments( + edit["shader"], + "displacementShader", + edit["nodes"], + node_assignments + ) if edit["action"] == "setattr": for attr, value in edit["attributes"].items(): From d13b74cb29b41d8fb94a7b6e3fbdd0146619ade9 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 16 Feb 2023 10:47:51 +0000 Subject: [PATCH 08/87] Support more parameters. --- .../tools/mayalookassigner/arnold_standin.py | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/openpype/tools/mayalookassigner/arnold_standin.py b/openpype/tools/mayalookassigner/arnold_standin.py index 0ba1526cda..b7568ec45f 100644 --- a/openpype/tools/mayalookassigner/arnold_standin.py +++ b/openpype/tools/mayalookassigner/arnold_standin.py @@ -25,11 +25,49 @@ log = logging.getLogger(__name__) ATTRIBUTE_MAPPING = { + "primaryVisibility": "visibility", # Camera + "castsShadows": "visibility", # Shadow + "receiveShadows": "receive_shadows", + "aiSelfShadows": "self_shadows", + "aiOpaque": "opaque", + "aiMatte": "matte", + "aiVisibleInDiffuseTransmission": "visibility", + "aiVisibleInSpecularTransmission": "visibility", + "aiVisibleInVolume": "visibility", + "aiVisibleInDiffuseReflection": "visibility", + "aiVisibleInSpecularReflection": "visibility", + "aiSubdivUvSmoothing": "subdiv_uv_smoothing", + "aiDispHeight": "disp_height", + "aiDispPadding": "disp_padding", + "aiDispZeroValue": "disp_zero_value", + "aiStepSize": "step_size", + "aiVolumePadding": "volume_padding", "aiSubdivType": "subdiv_type", "aiSubdivIterations": "subdiv_iterations" } +def calculate_visibility_mask(attributes): + # https://arnoldsupport.com/2018/11/21/backdoor-setting-visibility/ + mapping = { + "primaryVisibility": 1, # Camera + "castsShadows": 2, # Shadow + "aiVisibleInDiffuseTransmission": 4, + "aiVisibleInSpecularTransmission": 8, + "aiVisibleInVolume": 16, + "aiVisibleInDiffuseReflection": 32, + "aiVisibleInSpecularReflection": 64 + } + mask = 255 + for attr, value in mapping.items(): + if attributes.get(attr, True): + continue + + mask -= value + + return mask + + def get_cbid_by_node(path): """Get cbid from Arnold Scene Source. @@ -216,6 +254,7 @@ def assign_look(standin, subset): ) if edit["action"] == "setattr": + visibility = False for attr, value in edit["attributes"].items(): if attr not in ATTRIBUTE_MAPPING: log.warning( @@ -227,11 +266,37 @@ def assign_look(standin, subset): if isinstance(value, str): value = "'{}'".format(value) + if ATTRIBUTE_MAPPING[attr] == "visibility": + visibility = True + continue + assignment = "{}={}".format(ATTRIBUTE_MAPPING[attr], value) for node in edit["nodes"]: node_assignments[node].append(assignment) + if visibility: + # https://arnoldsupport.com/2018/11/21/backdoor-setting-visibility/ + mapping = { + "primaryVisibility": 1, # Camera + "castsShadows": 2, # Shadow + "aiVisibleInDiffuseTransmission": 4, + "aiVisibleInSpecularTransmission": 8, + "aiVisibleInVolume": 16, + "aiVisibleInDiffuseReflection": 32, + "aiVisibleInSpecularReflection": 64 + } + mask = 255 + for attr, value in mapping.items(): + if edit["attributes"].get(attr, True): + continue + mask -= value + + assignment = "visibility={}".format(mask) + + for node in edit["nodes"]: + node_assignments[node].append(assignment) + # Assign shader # Clear all current shader assignments plug = standin + ".operators" From 70b607bf15de2bbc35b7fff26ed710cd0f46c443 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 16 Feb 2023 11:05:57 +0000 Subject: [PATCH 09/87] Refactor to share code between vray and arnold --- .../tools/mayalookassigner/arnold_standin.py | 68 ++------------- openpype/tools/mayalookassigner/lib.py | 87 +++++++++++++++++++ 2 files changed, 95 insertions(+), 60 deletions(-) create mode 100644 openpype/tools/mayalookassigner/lib.py diff --git a/openpype/tools/mayalookassigner/arnold_standin.py b/openpype/tools/mayalookassigner/arnold_standin.py index b7568ec45f..7d3b7e59b3 100644 --- a/openpype/tools/mayalookassigner/arnold_standin.py +++ b/openpype/tools/mayalookassigner/arnold_standin.py @@ -1,24 +1,14 @@ import os import re from collections import defaultdict -import json import logging from maya import cmds -from openpype.pipeline import ( - legacy_io, - get_representation_path, - registered_host, - discover_loader_plugins, - loaders_from_representation, - load_container -) -from openpype.client import ( - get_representation_by_name, - get_last_version_by_subset_name -) -from openpype.hosts.maya.api import lib +from openpype.pipeline import legacy_io +from openpype.client import get_last_version_by_subset_name +from openpype.hosts.maya import api +from . import lib log = logging.getLogger(__name__) @@ -171,50 +161,8 @@ def assign_look(standin, subset): )) continue - # Relationships. - json_representation = get_representation_by_name( - project_name, representation_name="json", version_id=version["_id"] - ) - - # Load relationships - shader_relation = get_representation_path(json_representation) - with open(shader_relation, "r") as f: - relationships = json.load(f) - - # Load look. - # Get representations of shader file and relationships - look_representation = get_representation_by_name( - project_name, representation_name="ma", version_id=version["_id"] - ) - - # See if representation is already loaded, if so reuse it. - host = registered_host() - representation_id = str(look_representation['_id']) - for container in host.ls(): - if (container['loader'] == "LookLoader" and - container['representation'] == representation_id): - log.info("Reusing loaded look ...") - container_node = container['objectName'] - break - else: - log.info("Using look for the first time ...") - - # Load file - all_loaders = discover_loader_plugins() - loaders = loaders_from_representation( - all_loaders, representation_id - ) - loader = next( - (i for i in loaders if i.__name__ == "LookLoader"), None) - if loader is None: - raise RuntimeError("Could not find LookLoader, this is a bug") - - # Reference the look file - with lib.maintained_selection(): - container_node = load_container(loader, look_representation) - - # Get container members - shader_nodes = lib.get_container_members(container_node) + relationships = lib.get_look_relationships(version["_id"]) + shader_nodes, container_node = lib.load_look(version["_id"]) namespace = shader_nodes[0].split(":")[0] # Get only the node ids and paths related to this asset @@ -223,7 +171,7 @@ def assign_look(standin, subset): node_id: nodes_by_id[node_id] for node_id in node_ids } edits = list( - lib.iter_shader_edits( + api.lib.iter_shader_edits( relationships, shader_nodes, asset_nodes_by_id ) ) @@ -310,7 +258,7 @@ def assign_look(standin, subset): if not assignments: continue - with lib.maintained_selection(): + with api.lib.maintained_selection(): operator = cmds.createNode("aiSetParameter") operator = cmds.rename(operator, namespace + ":" + operator) diff --git a/openpype/tools/mayalookassigner/lib.py b/openpype/tools/mayalookassigner/lib.py new file mode 100644 index 0000000000..5594c53c33 --- /dev/null +++ b/openpype/tools/mayalookassigner/lib.py @@ -0,0 +1,87 @@ +import json +import logging + +from openpype.pipeline import ( + legacy_io, + get_representation_path, + registered_host, + discover_loader_plugins, + loaders_from_representation, + load_container +) +from openpype.client import get_representation_by_name +from openpype.hosts.maya.api import lib + + +log = logging.getLogger(__name__) + + +def get_look_relationships(version_id): + # type: (str) -> dict + """Get relations for the look. + + Args: + version_id (str): Parent version Id. + + Returns: + dict: Dictionary of relations. + """ + + project_name = legacy_io.active_project() + json_representation = get_representation_by_name( + project_name, representation_name="json", version_id=version_id + ) + + # Load relationships + shader_relation = get_representation_path(json_representation) + with open(shader_relation, "r") as f: + relationships = json.load(f) + + return relationships + + +def load_look(version_id): + # type: (str) -> list + """Load look from version. + + Get look from version and invoke Loader for it. + + Args: + version_id (str): Version ID + + Returns: + list of shader nodes. + + """ + + project_name = legacy_io.active_project() + # Get representations of shader file and relationships + look_representation = get_representation_by_name( + project_name, representation_name="ma", version_id=version_id + ) + + # See if representation is already loaded, if so reuse it. + host = registered_host() + representation_id = str(look_representation['_id']) + for container in host.ls(): + if (container['loader'] == "LookLoader" and + container['representation'] == representation_id): + log.info("Reusing loaded look ...") + container_node = container['objectName'] + break + else: + log.info("Using look for the first time ...") + + # Load file + all_loaders = discover_loader_plugins() + loaders = loaders_from_representation(all_loaders, representation_id) + loader = next( + (i for i in loaders if i.__name__ == "LookLoader"), None) + if loader is None: + raise RuntimeError("Could not find LookLoader, this is a bug") + + # Reference the look file + with lib.maintained_selection(): + container_node = load_container(loader, look_representation) + + return lib.get_container_members(container_node), container_node From c426e34761828bfb7540c041fc2b0398ab10acb4 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 16 Feb 2023 11:08:25 +0000 Subject: [PATCH 10/87] Missing refactor code --- .../tools/mayalookassigner/vray_proxies.py | 97 ++----------------- 1 file changed, 7 insertions(+), 90 deletions(-) diff --git a/openpype/tools/mayalookassigner/vray_proxies.py b/openpype/tools/mayalookassigner/vray_proxies.py index 889396e555..6ee618f37a 100644 --- a/openpype/tools/mayalookassigner/vray_proxies.py +++ b/openpype/tools/mayalookassigner/vray_proxies.py @@ -3,26 +3,16 @@ import os from collections import defaultdict import logging -import json import six import alembic.Abc from maya import cmds -from openpype.client import ( - get_representation_by_name, - get_last_version_by_subset_name, -) -from openpype.pipeline import ( - legacy_io, - load_container, - loaders_from_representation, - discover_loader_plugins, - get_representation_path, - registered_host, -) -from openpype.hosts.maya.api import lib +from openpype.client import get_last_version_by_subset_name +from openpype.pipeline import legacy_io +from openpype.hosts.maya import api +from . import lib log = logging.getLogger(__name__) @@ -149,79 +139,6 @@ def assign_vrayproxy_shaders(vrayproxy, assignments): index += 1 -def get_look_relationships(version_id): - # type: (str) -> dict - """Get relations for the look. - - Args: - version_id (str): Parent version Id. - - Returns: - dict: Dictionary of relations. - """ - - project_name = legacy_io.active_project() - json_representation = get_representation_by_name( - project_name, representation_name="json", version_id=version_id - ) - - # Load relationships - shader_relation = get_representation_path(json_representation) - with open(shader_relation, "r") as f: - relationships = json.load(f) - - return relationships - - -def load_look(version_id): - # type: (str) -> list - """Load look from version. - - Get look from version and invoke Loader for it. - - Args: - version_id (str): Version ID - - Returns: - list of shader nodes. - - """ - - project_name = legacy_io.active_project() - # Get representations of shader file and relationships - look_representation = get_representation_by_name( - project_name, representation_name="ma", version_id=version_id - ) - - # See if representation is already loaded, if so reuse it. - host = registered_host() - representation_id = str(look_representation['_id']) - for container in host.ls(): - if (container['loader'] == "LookLoader" and - container['representation'] == representation_id): - log.info("Reusing loaded look ...") - container_node = container['objectName'] - break - else: - log.info("Using look for the first time ...") - - # Load file - all_loaders = discover_loader_plugins() - loaders = loaders_from_representation(all_loaders, representation_id) - loader = next( - (i for i in loaders if i.__name__ == "LookLoader"), None) - if loader is None: - raise RuntimeError("Could not find LookLoader, this is a bug") - - # Reference the look file - with lib.maintained_selection(): - container_node = load_container(loader, look_representation) - - # Get container members - shader_nodes = lib.get_container_members(container_node) - return shader_nodes - - def vrayproxy_assign_look(vrayproxy, subset="lookDefault"): # type: (str, str) -> None """Assign look to vray proxy. @@ -263,8 +180,8 @@ def vrayproxy_assign_look(vrayproxy, subset="lookDefault"): )) continue - relationships = get_look_relationships(version["_id"]) - shadernodes = load_look(version["_id"]) + relationships = lib.get_look_relationships(version["_id"]) + shadernodes, _ = lib.load_look(version["_id"]) # Get only the node ids and paths related to this asset # And get the shader edits the look supplies @@ -272,7 +189,7 @@ def vrayproxy_assign_look(vrayproxy, subset="lookDefault"): node_id: nodes_by_id[node_id] for node_id in node_ids } edits = list( - lib.iter_shader_edits( + api.lib.iter_shader_edits( relationships, shadernodes, asset_nodes_by_id)) # Create assignments From 017d85d74b897a85d422e8b6770e4b789ad040e9 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 16 Feb 2023 11:11:29 +0000 Subject: [PATCH 11/87] Hound --- openpype/tools/mayalookassigner/commands.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/tools/mayalookassigner/commands.py b/openpype/tools/mayalookassigner/commands.py index 69fcc77bce..c22a8c9211 100644 --- a/openpype/tools/mayalookassigner/commands.py +++ b/openpype/tools/mayalookassigner/commands.py @@ -1,7 +1,6 @@ from collections import defaultdict import logging import os -import re import maya.cmds as cmds From d770547c292b2f286d5ba32ee851c336c4a4fe4a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 1 Mar 2023 12:21:07 +0000 Subject: [PATCH 12/87] Get all descendents in collector. - refactor common code to lib. --- openpype/hosts/maya/api/lib.py | 41 +++++++++++++++++- .../publish/collect_arnold_scene_source.py | 11 +++-- .../maya/plugins/publish/collect_instances.py | 42 +------------------ .../publish/validate_arnold_scene_source.py | 3 -- .../validate_arnold_scene_source_cbid.py | 3 -- 5 files changed, 48 insertions(+), 52 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 4324d321dc..7e8bb5439f 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -4,7 +4,6 @@ import os import sys import platform import uuid -import math import re import json @@ -3562,3 +3561,43 @@ def get_color_management_output_transform(): if preferences["output_transform_enabled"]: colorspace = preferences["output_transform"] return colorspace + + +def get_all_children(nodes): + """Return all children of `nodes` including each instanced child. + Using maya.cmds.listRelatives(allDescendents=True) includes only the first + instance. As such, this function acts as an optimal replacement with a + focus on a fast query. + + """ + + sel = OpenMaya.MSelectionList() + traversed = set() + iterator = OpenMaya.MItDag(OpenMaya.MItDag.kDepthFirst) + for node in nodes: + + if node in traversed: + # Ignore if already processed as a child + # before + continue + + sel.clear() + sel.add(node) + dag = sel.getDagPath(0) + + iterator.reset(dag) + # ignore self + iterator.next() # noqa: B305 + while not iterator.isDone(): + + path = iterator.fullPathName() + + if path in traversed: + iterator.prune() + iterator.next() # noqa: B305 + continue + + traversed.add(path) + iterator.next() # noqa: B305 + + return list(traversed) diff --git a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py index 0415808b7a..fd4993d09e 100644 --- a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py @@ -1,6 +1,7 @@ from maya import cmds import pyblish.api +from openpype.hosts.maya.api.lib import get_all_children class CollectArnoldSceneSource(pyblish.api.InstancePlugin): @@ -21,11 +22,13 @@ class CollectArnoldSceneSource(pyblish.api.InstancePlugin): self.log.warning("Skipped empty instance: \"%s\" " % objset) continue if objset.endswith("content_SET"): - instance.data["setMembers"] = cmds.ls(members, long=True) - self.log.debug("content members: {}".format(members)) + set_members = get_all_children(cmds.ls(members, long=True)) + instance.data["setMembers"] = set_members + self.log.debug("content members: {}".format(set_members)) elif objset.endswith("proxy_SET"): - instance.data["proxy"] = cmds.ls(members, long=True) - self.log.debug("proxy members: {}".format(members)) + set_members = get_all_children(cmds.ls(members, long=True)) + instance.data["proxy"] = set_members + self.log.debug("proxy members: {}".format(set_members)) # Use camera in object set if present else default to render globals # camera. diff --git a/openpype/hosts/maya/plugins/publish/collect_instances.py b/openpype/hosts/maya/plugins/publish/collect_instances.py index 6c6819f0a2..6bf0756323 100644 --- a/openpype/hosts/maya/plugins/publish/collect_instances.py +++ b/openpype/hosts/maya/plugins/publish/collect_instances.py @@ -1,48 +1,8 @@ from maya import cmds -import maya.api.OpenMaya as om import pyblish.api import json - - -def get_all_children(nodes): - """Return all children of `nodes` including each instanced child. - Using maya.cmds.listRelatives(allDescendents=True) includes only the first - instance. As such, this function acts as an optimal replacement with a - focus on a fast query. - - """ - - sel = om.MSelectionList() - traversed = set() - iterator = om.MItDag(om.MItDag.kDepthFirst) - for node in nodes: - - if node in traversed: - # Ignore if already processed as a child - # before - continue - - sel.clear() - sel.add(node) - dag = sel.getDagPath(0) - - iterator.reset(dag) - # ignore self - iterator.next() # noqa: B305 - while not iterator.isDone(): - - path = iterator.fullPathName() - - if path in traversed: - iterator.prune() - iterator.next() # noqa: B305 - continue - - traversed.add(path) - iterator.next() # noqa: B305 - - return list(traversed) +from openpype.hosts.maya.api.lib import get_all_children class CollectInstances(pyblish.api.ContextPlugin): diff --git a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py index 84240e63e6..e582560e12 100644 --- a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py @@ -36,9 +36,6 @@ class ValidateArnoldSceneSource(pyblish.api.InstancePlugin): parents.append(parent) nodes_by_name[node_split[-1].split(":")[-1]] = node - for shape in cmds.listRelatives(node, shapes=True): - basename = shape.split("|")[-1].split(":")[-1] - nodes_by_name[basename] = node + "|" + shape return ungrouped_nodes, nodes_by_name, parents diff --git a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py index 056cc94edf..5d0ef79838 100644 --- a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py +++ b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py @@ -24,9 +24,6 @@ class ValidateArnoldSceneSourceCbid(pyblish.api.InstancePlugin): for node in nodes: node_split = node.split("|") nodes_by_name[node_split[-1].split(":")[-1]] = node - for shape in cmds.listRelatives(node, shapes=True): - basename = shape.split("|")[-1].split(":")[-1] - nodes_by_name[basename] = node + "|" + shape return nodes_by_name From 6b67508d405b18013502396d32fbf2cba4f41ec6 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 16 Mar 2023 17:03:11 +0000 Subject: [PATCH 13/87] Update openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py Co-authored-by: Roy Nieterau --- .../hosts/maya/plugins/publish/extract_arnold_scene_source.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py index 8c9d90e2e6..54b8a005b2 100644 --- a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py @@ -122,9 +122,7 @@ class ExtractArnoldSceneSource(publish.Extractor): with delete_after() as delete_bin: duplicate_nodes = [] for node in nodes: - parent = cmds.ls( - cmds.listRelatives(node, parent=True)[0], long=True - )[0] + parent = cmds.listRelatives(node, parent=True, fullPath=True)[0] duplicate_transform = cmds.duplicate(node)[0] duplicate_transform = "{}|{}".format( parent, duplicate_transform From b443b52d82fc5ea8246cb92e5484a31e73296016 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 16 Mar 2023 17:03:51 +0000 Subject: [PATCH 14/87] Update openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py Co-authored-by: Roy Nieterau --- .../hosts/maya/plugins/publish/extract_arnold_scene_source.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py index 54b8a005b2..f672c5a94b 100644 --- a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py @@ -140,8 +140,7 @@ class ExtractArnoldSceneSource(publish.Extractor): )[0] basename = node.split("|")[-1].split(":")[-1] - cmds.rename(duplicate_transform, basename) - duplicate_transform = "|" + basename + duplicate_transform = cmds.rename(duplicate_transform, basename) duplicate_nodes.append(duplicate_transform) delete_bin.append(duplicate_transform) From eaac0cb2e783e259de904c67278e60cb14e0ab88 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 16 Mar 2023 17:04:12 +0000 Subject: [PATCH 15/87] Update openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py Co-authored-by: Roy Nieterau --- .../hosts/maya/plugins/publish/extract_arnold_scene_source.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py index f672c5a94b..4de0f49de1 100644 --- a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py @@ -148,9 +148,9 @@ class ExtractArnoldSceneSource(publish.Extractor): # Copy cbId from original to mtoa_constant. attr_name = "mtoa_constant_cbId" duplicate_shapes = cmds.listRelatives( - duplicate_transform, shapes=True + duplicate_transform, shapes=True, fullPath=True ) - original_shapes = cmds.listRelatives(node, shapes=True) + original_shapes = cmds.listRelatives(node, shapes=True, fullPath=True) for duplicate_shape in duplicate_shapes: duplicate_path = ( duplicate_transform + "|" + duplicate_shape From 60de2b537fec0bb53c475854a015839b1f0836e8 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 16 Mar 2023 17:09:42 +0000 Subject: [PATCH 16/87] Update openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py Co-authored-by: Roy Nieterau --- .../publish/validate_arnold_scene_source_cbid.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py index 5d0ef79838..87c47978c8 100644 --- a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py +++ b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py @@ -37,12 +37,16 @@ class ValidateArnoldSceneSourceCbid(pyblish.api.InstancePlugin): invalid_couples = [] for content_name, content_node in content_nodes_by_name.items(): - for proxy_name, proxy_node in proxy_nodes_by_name.items(): - if content_name == proxy_name: - content_value = cmds.getAttr(content_node + ".cbId") - proxy_value = cmds.getAttr(proxy_node + ".cbId") - if content_value != proxy_value: - invalid_couples.append((content_node, proxy_node)) + proxy_node = proxy_nodes_by_name.get(content_name, None) + + if not proxy_node: + self.log.debug("Content node '{}' has no matching proxy node.".format(content_node)) + continue + + content_id = lib.get_id(content_node) + proxy_id = lib.get_id(proxy_node) + if content_id != proxy_id: + invalid_couples.append((content_node, proxy_node)) return invalid_couples From e599dcda0d5cee6f91239201dda6a2febb509888 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 16 Mar 2023 17:11:54 +0000 Subject: [PATCH 17/87] Update openpype/hosts/maya/tools/mayalookassigner/commands.py Co-authored-by: Roy Nieterau --- openpype/hosts/maya/tools/mayalookassigner/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/tools/mayalookassigner/commands.py b/openpype/hosts/maya/tools/mayalookassigner/commands.py index c22a8c9211..d7061f12e1 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/commands.py +++ b/openpype/hosts/maya/tools/mayalookassigner/commands.py @@ -108,7 +108,7 @@ def create_asset_id_hash(nodes): """ node_id_hash = defaultdict(list) for node in nodes: - shapes = cmds.ls(cmds.listRelatives(node, shapes=True), long=True) + shapes = cmds.listRelatives(node, shapes=True, fullPath=True) # iterate over content of reference node if cmds.nodeType(node) == "reference": ref_hashes = create_asset_id_hash( From d22e0bb6fa27b719b34efc4b14adf1919457aa10 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 16 Mar 2023 17:29:08 +0000 Subject: [PATCH 18/87] setMembers > contentMembers --- .../maya/plugins/publish/collect_arnold_scene_source.py | 4 ++-- .../maya/plugins/publish/extract_arnold_scene_source.py | 2 +- .../maya/plugins/publish/validate_arnold_scene_source.py | 6 +++--- .../plugins/publish/validate_arnold_scene_source_cbid.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py index fd4993d09e..ab15d0419f 100644 --- a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py @@ -23,7 +23,7 @@ class CollectArnoldSceneSource(pyblish.api.InstancePlugin): continue if objset.endswith("content_SET"): set_members = get_all_children(cmds.ls(members, long=True)) - instance.data["setMembers"] = set_members + instance.data["contentMembers"] = set_members self.log.debug("content members: {}".format(set_members)) elif objset.endswith("proxy_SET"): set_members = get_all_children(cmds.ls(members, long=True)) @@ -35,7 +35,7 @@ class CollectArnoldSceneSource(pyblish.api.InstancePlugin): cameras = cmds.ls(type="camera", long=True) renderable = [c for c in cameras if cmds.getAttr("%s.renderable" % c)] camera = renderable[0] - for node in instance.data["setMembers"]: + for node in instance.data["contentMembers"]: camera_shapes = cmds.listRelatives( node, shapes=True, type="camera" ) diff --git a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py index 4de0f49de1..0325c2518e 100644 --- a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py @@ -72,7 +72,7 @@ class ExtractArnoldSceneSource(publish.Extractor): } filenames = self._extract( - instance.data["setMembers"], attribute_data, kwargs + instance.data["contentMembers"], attribute_data, kwargs ) if "representations" not in instance.data: diff --git a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py index e582560e12..2a7eabe285 100644 --- a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py @@ -43,7 +43,7 @@ class ValidateArnoldSceneSource(pyblish.api.InstancePlugin): ungrouped_nodes = [] nodes, content_nodes_by_name, content_parents = self._get_nodes_data( - instance.data["setMembers"] + instance.data["contentMembers"] ) ungrouped_nodes.extend(nodes) @@ -64,11 +64,11 @@ class ValidateArnoldSceneSource(pyblish.api.InstancePlugin): return # Validate for content and proxy nodes amount being the same. - if len(instance.data["setMembers"]) != len(instance.data["proxy"]): + if len(instance.data["contentMembers"]) != len(instance.data["proxy"]): raise PublishValidationError( "Amount of content nodes ({}) and proxy nodes ({}) needs to " "be the same.".format( - len(instance.data["setMembers"]), + len(instance.data["contentMembers"]), len(instance.data["proxy"]) ) ) diff --git a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py index 87c47978c8..0cc2b482e4 100644 --- a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py +++ b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py @@ -29,7 +29,7 @@ class ValidateArnoldSceneSourceCbid(pyblish.api.InstancePlugin): def get_invalid_couples(self, instance): content_nodes_by_name = self._get_nodes_data( - instance.data["setMembers"] + instance.data["contentMembers"] ) proxy_nodes_by_name = self._get_nodes_data( instance.data.get("proxy", []) From 1e3862ca7a9aacc6d8e7d27602919a48146a813e Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 16 Mar 2023 17:40:57 +0000 Subject: [PATCH 19/87] Copy cbid from duplicated nodes. --- .../publish/extract_arnold_scene_source.py | 36 ++++--------------- 1 file changed, 7 insertions(+), 29 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py index 0325c2518e..1678f97627 100644 --- a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py @@ -4,9 +4,7 @@ from maya import cmds import arnold from openpype.pipeline import publish -from openpype.hosts.maya.api.lib import ( - maintained_selection, attribute_values, delete_after -) +from openpype.hosts.maya.api import lib class ExtractArnoldSceneSource(publish.Extractor): @@ -119,7 +117,7 @@ class ExtractArnoldSceneSource(publish.Extractor): filenames = [] # Duplicating nodes so they are direct children of the world. This # makes the hierarchy of any exported ass file the same. - with delete_after() as delete_bin: + with lib.delete_after() as delete_bin: duplicate_nodes = [] for node in nodes: parent = cmds.listRelatives(node, parent=True, fullPath=True)[0] @@ -145,32 +143,12 @@ class ExtractArnoldSceneSource(publish.Extractor): duplicate_nodes.append(duplicate_transform) delete_bin.append(duplicate_transform) - # Copy cbId from original to mtoa_constant. - attr_name = "mtoa_constant_cbId" - duplicate_shapes = cmds.listRelatives( - duplicate_transform, shapes=True, fullPath=True - ) - original_shapes = cmds.listRelatives(node, shapes=True, fullPath=True) - for duplicate_shape in duplicate_shapes: - duplicate_path = ( - duplicate_transform + "|" + duplicate_shape - ) - for original_shape in original_shapes: - original_path = node + "|" + original_shape - if duplicate_shape == original_shape: - cmds.addAttr( - duplicate_path, - longName=attr_name, - dataType="string" - ) - cmds.setAttr( - duplicate_path + "." + attr_name, - cmds.getAttr(original_path + ".cbId"), - type="string" - ) + # Copy cbId to mtoa_constant. + for node in duplicate_nodes: + lib.set_attribute("mtoa_constant_cbId", lib.get_id(node)) - with attribute_values(attribute_data): - with maintained_selection(): + with lib.attribute_values(attribute_data): + with lib.maintained_selection(): self.log.info( "Writing: {}".format(duplicate_nodes) ) From caf101bd7d461e9616904c17ec19e1540d18187b Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 17 Mar 2023 07:26:57 +0000 Subject: [PATCH 20/87] rsplit instead of split --- .../maya/plugins/publish/validate_arnold_scene_source_cbid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py index 0cc2b482e4..457c3b7d52 100644 --- a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py +++ b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py @@ -22,8 +22,8 @@ class ValidateArnoldSceneSourceCbid(pyblish.api.InstancePlugin): def _get_nodes_data(nodes): nodes_by_name = {} for node in nodes: - node_split = node.split("|") - nodes_by_name[node_split[-1].split(":")[-1]] = node + node_name = node.rsplit("|", 1)[-1].rsplit(":", 1)[-1] + nodes_by_name[node_name] = node return nodes_by_name From baf60646730e34d8d9965472b8923d71144e1d30 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 17 Mar 2023 07:39:08 +0000 Subject: [PATCH 21/87] Validate against same named nodes in different hierarchies. --- .../publish/validate_arnold_scene_source.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py index 2a7eabe285..8f443f6963 100644 --- a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py @@ -26,6 +26,7 @@ class ValidateArnoldSceneSource(pyblish.api.InstancePlugin): ungrouped_nodes = [] nodes_by_name = {} parents = [] + same_named_nodes = [] for node in nodes: node_split = node.split("|") if len(node_split) == 2: @@ -35,7 +36,21 @@ class ValidateArnoldSceneSource(pyblish.api.InstancePlugin): if parent: parents.append(parent) - nodes_by_name[node_split[-1].split(":")[-1]] = node + node_name = node.rsplit("|", 1)[-1].rsplit(":", 1)[-1] + + # Check for same same nodes, which can happen in different + # hierarchies. + if node_name in nodes_by_name: + same_named_nodes.append((node, nodes_by_name[node_name])) + + nodes_by_name[node_name] = node + + if same_named_nodes: + raise PublishValidationError( + "Found nodes with the same name:\n{}".format( + "\n".join(["{}".format(n) for n in same_named_nodes]) + ) + ) return ungrouped_nodes, nodes_by_name, parents From 1048f58db3c72b2e061f0f6091143e210d13b3c0 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 17 Mar 2023 07:43:11 +0000 Subject: [PATCH 22/87] Use set_id and get_id --- .../plugins/publish/validate_arnold_scene_source_cbid.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py index 457c3b7d52..ad174404dc 100644 --- a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py +++ b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py @@ -67,8 +67,4 @@ class ValidateArnoldSceneSourceCbid(pyblish.api.InstancePlugin): @classmethod def repair(cls, instance): for content_node, proxy_node in cls.get_invalid_couples(cls, instance): - cmds.setAttr( - proxy_node + ".cbId", - cmds.getAttr(content_node + ".cbId"), - type="string" - ) + lib.set_id(proxy_node, lib.get_id(content_node), overwrite=False) From 214b76796c70aa1bb6608bb4f8510fcaba0e015f Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 17 Mar 2023 07:47:19 +0000 Subject: [PATCH 23/87] Failsafe for mtoa not loaded and user feedback. --- .../hosts/maya/tools/mayalookassigner/app.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/tools/mayalookassigner/app.py b/openpype/hosts/maya/tools/mayalookassigner/app.py index e66f0d73e2..2a8775fff6 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/app.py +++ b/openpype/hosts/maya/tools/mayalookassigner/app.py @@ -251,12 +251,23 @@ class MayaLookAssignerWindow(QtWidgets.QWidget): vrayproxy_assign_look(vp, subset_name) nodes = list(set(item["nodes"]).difference(vray_proxies)) + else: + self.echo( + "Could not assign to VRayProxy because vrayformaya plugin " + "is not loaded." + ) # Assign Arnold Standin look. - arnold_standins = set(cmds.ls(type="aiStandIn", long=True)) - for standin in arnold_standins: - if standin in nodes: - arnold_standin.assign_look(standin, subset_name) + if cmds.pluginInfo("mtoa", query=True, loaded=True): + arnold_standins = set(cmds.ls(type="aiStandIn", long=True)) + for standin in arnold_standins: + if standin in nodes: + arnold_standin.assign_look(standin, subset_name) + else: + self.echo( + "Could not assign to aiStandIn because mtoa plugin is not " + "loaded." + ) nodes = list(set(item["nodes"]).difference(arnold_standins)) From 14bd9b86b91876cb3be5d1c9d713b7a92931da80 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Fri, 17 Mar 2023 07:48:17 +0000 Subject: [PATCH 24/87] Update openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py Co-authored-by: Roy Nieterau --- .../tools/mayalookassigner/arnold_standin.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py b/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py index 7d3b7e59b3..d4f93aeca6 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py +++ b/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py @@ -189,16 +189,16 @@ def assign_look(standin, subset): continue shading_engine_assignments( - edit["shader"], - "surfaceShader", - edit["nodes"], - node_assignments + shading_engine=edit["shader"], + attr="surfaceShader", + nodes=edit["nodes"], + assignments=node_assignments ) shading_engine_assignments( - edit["shader"], - "displacementShader", - edit["nodes"], - node_assignments + shading_engine=edit["shader"], + attr="displacementShader", + nodes=edit["nodes"], + assignments=node_assignments ) if edit["action"] == "setattr": From 54af7a82d5142d3f52575d7e86be3a827c450404 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 17 Mar 2023 07:49:48 +0000 Subject: [PATCH 25/87] attr > attribute --- .../maya/tools/mayalookassigner/arnold_standin.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py b/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py index d4f93aeca6..4f85d15108 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py +++ b/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py @@ -109,13 +109,13 @@ def get_standin_path(node): return path -def shading_engine_assignments(shading_engine, attr, nodes, assignments): +def shading_engine_assignments(shading_engine, attribute, nodes, assignments): shader_inputs = cmds.listConnections( - shading_engine + "." + attr, source=True) + shading_engine + "." + attribute, source=True) if not shader_inputs: log.info( "Shading engine \"{}\" missing input \"{}\"".format( - shading_engine, attr + shading_engine, attribute ) ) @@ -128,7 +128,7 @@ def shading_engine_assignments(shading_engine, attr, nodes, assignments): "{}").format(node)) nodes[i] = node.split(".")[0] - shader_type = "shader" if attr == "surfaceShader" else "disp_map" + shader_type = "shader" if attribute == "surfaceShader" else "disp_map" assignment = "{}='{}'".format(shader_type, shader_inputs[0]) for node in nodes: assignments[node].append(assignment) @@ -190,13 +190,13 @@ def assign_look(standin, subset): shading_engine_assignments( shading_engine=edit["shader"], - attr="surfaceShader", + attribute="surfaceShader", nodes=edit["nodes"], assignments=node_assignments ) shading_engine_assignments( shading_engine=edit["shader"], - attr="displacementShader", + attribute="displacementShader", nodes=edit["nodes"], assignments=node_assignments ) From 6d1fd474a396f64b03ef999fda2b7761274ea317 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 17 Mar 2023 07:53:24 +0000 Subject: [PATCH 26/87] Use calculate_visibility_mask --- .../tools/mayalookassigner/arnold_standin.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py b/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py index 4f85d15108..6d7be80060 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py +++ b/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py @@ -224,22 +224,7 @@ def assign_look(standin, subset): node_assignments[node].append(assignment) if visibility: - # https://arnoldsupport.com/2018/11/21/backdoor-setting-visibility/ - mapping = { - "primaryVisibility": 1, # Camera - "castsShadows": 2, # Shadow - "aiVisibleInDiffuseTransmission": 4, - "aiVisibleInSpecularTransmission": 8, - "aiVisibleInVolume": 16, - "aiVisibleInDiffuseReflection": 32, - "aiVisibleInSpecularReflection": 64 - } - mask = 255 - for attr, value in mapping.items(): - if edit["attributes"].get(attr, True): - continue - mask -= value - + mask = calculate_visibility_mask(edit["attributes"]) assignment = "visibility={}".format(mask) for node in edit["nodes"]: From e420883189ad06b8bcc76107fab0399e3e1f8465 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 17 Mar 2023 07:55:20 +0000 Subject: [PATCH 27/87] Hound --- .../maya/plugins/publish/extract_arnold_scene_source.py | 8 ++++++-- .../plugins/publish/validate_arnold_scene_source_cbid.py | 9 ++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py index 1678f97627..c2523d4d12 100644 --- a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py @@ -120,7 +120,9 @@ class ExtractArnoldSceneSource(publish.Extractor): with lib.delete_after() as delete_bin: duplicate_nodes = [] for node in nodes: - parent = cmds.listRelatives(node, parent=True, fullPath=True)[0] + parent = cmds.listRelatives( + node, parent=True, fullPath=True + )[0] duplicate_transform = cmds.duplicate(node)[0] duplicate_transform = "{}|{}".format( parent, duplicate_transform @@ -138,7 +140,9 @@ class ExtractArnoldSceneSource(publish.Extractor): )[0] basename = node.split("|")[-1].split(":")[-1] - duplicate_transform = cmds.rename(duplicate_transform, basename) + duplicate_transform = cmds.rename( + duplicate_transform, basename + ) duplicate_nodes.append(duplicate_transform) delete_bin.append(duplicate_transform) diff --git a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py index ad174404dc..dafad2f40a 100644 --- a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py +++ b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py @@ -1,6 +1,5 @@ -import maya.cmds as cmds - import pyblish.api +from openpype.hosts.maya.api import lib from openpype.pipeline.publish import ( ValidateContentsOrder, PublishValidationError, RepairAction ) @@ -40,7 +39,11 @@ class ValidateArnoldSceneSourceCbid(pyblish.api.InstancePlugin): proxy_node = proxy_nodes_by_name.get(content_name, None) if not proxy_node: - self.log.debug("Content node '{}' has no matching proxy node.".format(content_node)) + self.log.debug( + "Content node '{}' has no matching proxy node.".format( + content_node + ) + ) continue content_id = lib.get_id(content_node) From 0e8f251c5cbb58f4f67e18f8af8d9a7f06d8c0d6 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 17 Mar 2023 07:55:27 +0000 Subject: [PATCH 28/87] Hound --- .../hosts/maya/plugins/publish/validate_arnold_scene_source.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py index 8f443f6963..d09a8610c4 100644 --- a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py @@ -1,5 +1,3 @@ -import maya.cmds as cmds - import pyblish.api from openpype.pipeline.publish import ( ValidateContentsOrder, PublishValidationError From 7cd7b2daf97f8f9776888f40d7123db84a006377 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 17 Mar 2023 09:03:35 +0000 Subject: [PATCH 29/87] Fix extractor --- .../publish/extract_arnold_scene_source.py | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py index c2523d4d12..7348c2db4d 100644 --- a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py @@ -120,6 +120,10 @@ class ExtractArnoldSceneSource(publish.Extractor): with lib.delete_after() as delete_bin: duplicate_nodes = [] for node in nodes: + # Only interested in transforms: + if cmds.nodeType(node) != "transform": + continue + parent = cmds.listRelatives( node, parent=True, fullPath=True )[0] @@ -128,13 +132,6 @@ class ExtractArnoldSceneSource(publish.Extractor): parent, duplicate_transform ) - # Discard the children. - shapes = cmds.listRelatives(duplicate_transform, shapes=True) - children = cmds.listRelatives( - duplicate_transform, children=True - ) - cmds.delete(set(children) - set(shapes)) - duplicate_transform = cmds.parent( duplicate_transform, world=True )[0] @@ -144,12 +141,22 @@ class ExtractArnoldSceneSource(publish.Extractor): duplicate_transform, basename ) + # Discard the children. + shapes = cmds.listRelatives( + duplicate_transform, shapes=True, fullPath=True + ) + children = cmds.listRelatives( + duplicate_transform, children=True, fullPath=True + ) + cmds.delete(set(children) - set(shapes)) + duplicate_nodes.append(duplicate_transform) + duplicate_nodes.extend(shapes) delete_bin.append(duplicate_transform) # Copy cbId to mtoa_constant. for node in duplicate_nodes: - lib.set_attribute("mtoa_constant_cbId", lib.get_id(node)) + lib.set_attribute("mtoa_constant_cbId", lib.get_id(node), node) with lib.attribute_values(attribute_data): with lib.maintained_selection(): From 0b67d9c7581510ee271aa698a11000874680581d Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 17 Mar 2023 12:07:43 +0000 Subject: [PATCH 30/87] Initial working GPU extractor --- .../maya/plugins/publish/extract_gpu_cache.py | 45 +++++++++++++++++++ openpype/plugins/publish/integrate.py | 2 +- .../plugins/publish/integrate_hero_version.py | 20 ++++++++- 3 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 openpype/hosts/maya/plugins/publish/extract_gpu_cache.py diff --git a/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py b/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py new file mode 100644 index 0000000000..0e69f6dc57 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py @@ -0,0 +1,45 @@ +import os + +from maya import cmds + +from openpype.pipeline import publish +from openpype.hosts.maya.api.lib import ( + maintained_selection, attribute_values, delete_after +) + + +class ExtractGPUCache(publish.Extractor): + """Extract the content of the instance to an CPU cache file.""" + + label = "CPU Cache" + hosts = ["maya"] + families = ["model"] + + def process(self, instance): + staging_dir = self.staging_dir(instance) + filename = "{}.abc".format(instance.name) + + # Write out GPU cache file. + cmds.gpuCache( + instance[:], + directory=staging_dir, + fileName=filename, + saveMultipleFiles=False + ) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + "name": "gpu_cache", + "ext": "abc", + "files": filename, + "stagingDir": staging_dir, + "data": {"heroSuffix": "gpu_cache"} + } + + instance.data["representations"].append(representation) + + self.log.info( + "Extracted instance {} to: {}".format(instance.name, staging_dir) + ) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index b117006871..f8fb6041b3 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -398,7 +398,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): self.log.debug("{}".format(op_session.to_data())) op_session.commit() - # Backwards compatibility + # Backwards compatibility used in hero integration. # todo: can we avoid the need to store this? instance.data["published_representations"] = { p["representation"]["_id"]: p for p in prepared_representations diff --git a/openpype/plugins/publish/integrate_hero_version.py b/openpype/plugins/publish/integrate_hero_version.py index e796f7b376..4d7d0accad 100644 --- a/openpype/plugins/publish/integrate_hero_version.py +++ b/openpype/plugins/publish/integrate_hero_version.py @@ -18,7 +18,7 @@ from openpype.client.operations import ( prepare_hero_version_update_data, prepare_representation_update_data, ) -from openpype.lib import create_hard_link +from openpype.lib import create_hard_link, StringTemplate from openpype.pipeline import ( schema ) @@ -306,6 +306,23 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): anatomy_filled = anatomy.format(anatomy_data) template_filled = anatomy_filled[template_key]["path"] + # For representations that have the same extension, an + # additional suffix can be available to make the destination + # filename different. + hero_suffix = repre_info["representation"]["data"].get( + "heroSuffix" + ) + if hero_suffix: + fill_data = copy.deepcopy(template_filled.used_values) + template_filled.template = template_filled.replace( + "." + fill_data["ext"], + "_{}.{}".format(hero_suffix, fill_data["ext"]) + ) + template_filled = StringTemplate( + template_filled.template + ).format(fill_data) + + # Prepare new repre repre_data = { "path": str(template_filled), "template": hero_template @@ -316,7 +333,6 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): if value is not None: repre_context[key] = value - # Prepare new repre repre = copy.deepcopy(repre_info["representation"]) repre["parent"] = new_hero_version["_id"] repre["context"] = repre_context From cf6242292e6586c7f8c7162337d30cb8a559c4cf Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 17 Mar 2023 13:10:44 +0100 Subject: [PATCH 31/87] Fix #4648: Fix Collect Render for V-Ray, Redshift and Renderman - Not entirely sure this is the correct colorspace solution for all renderers. But at least this hotfixes the collecting. --- openpype/hosts/maya/api/lib_renderproducts.py | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index a54256c59a..ed85b1619a 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -857,6 +857,7 @@ class RenderProductsVray(ARenderProducts): if default_ext in {"exr (multichannel)", "exr (deep)"}: default_ext = "exr" + colorspace = lib.get_color_management_output_transform() products = [] # add beauty as default when not disabled @@ -868,7 +869,7 @@ class RenderProductsVray(ARenderProducts): productName="", ext=default_ext, camera=camera, - colorspace=lib.get_color_management_output_transform(), + colorspace=colorspace, multipart=self.multipart ) ) @@ -882,6 +883,7 @@ class RenderProductsVray(ARenderProducts): productName="Alpha", ext=default_ext, camera=camera, + colorspace=colorspace, multipart=self.multipart ) ) @@ -917,7 +919,8 @@ class RenderProductsVray(ARenderProducts): product = RenderProduct(productName=name, ext=default_ext, aov=aov, - camera=camera) + camera=camera, + colorspace=colorspace) products.append(product) # Continue as we've processed this special case AOV continue @@ -929,7 +932,7 @@ class RenderProductsVray(ARenderProducts): ext=default_ext, aov=aov, camera=camera, - colorspace=lib.get_color_management_output_transform() + colorspace=colorspace ) products.append(product) @@ -1130,6 +1133,7 @@ class RenderProductsRedshift(ARenderProducts): products = [] light_groups_enabled = False has_beauty_aov = False + colorspace = lib.get_color_management_output_transform() for aov in aovs: enabled = self._get_attr(aov, "enabled") if not enabled: @@ -1173,7 +1177,8 @@ class RenderProductsRedshift(ARenderProducts): ext=ext, multipart=False, camera=camera, - driver=aov) + driver=aov, + colorspace=colorspace) products.append(product) if light_groups: @@ -1188,7 +1193,8 @@ class RenderProductsRedshift(ARenderProducts): ext=ext, multipart=False, camera=camera, - driver=aov) + driver=aov, + colorspace=colorspace) products.append(product) # When a Beauty AOV is added manually, it will be rendered as @@ -1204,7 +1210,8 @@ class RenderProductsRedshift(ARenderProducts): RenderProduct(productName=beauty_name, ext=ext, multipart=self.multipart, - camera=camera)) + camera=camera, + colorspace=colorspace)) return products @@ -1236,6 +1243,8 @@ class RenderProductsRenderman(ARenderProducts): """ from rfm2.api.displays import get_displays # noqa + colorspace = lib.get_color_management_output_transform() + cameras = [ self.sanitize_camera_name(c) for c in self.get_renderable_cameras() @@ -1302,7 +1311,8 @@ class RenderProductsRenderman(ARenderProducts): productName=aov_name, ext=extensions, camera=camera, - multipart=True + multipart=True, + colorspace=colorspace ) if has_cryptomatte and matte_enabled: @@ -1311,7 +1321,8 @@ class RenderProductsRenderman(ARenderProducts): aov=cryptomatte_aov, ext=extensions, camera=camera, - multipart=True + multipart=True, + colorspace=colorspace ) else: # this code should handle the case where no multipart From c05a3b3b1d8777861114e8ec88732f87f28f6c60 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 17 Mar 2023 12:17:58 +0000 Subject: [PATCH 32/87] Hound and BigRoy feedback --- openpype/hosts/maya/plugins/publish/extract_gpu_cache.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py b/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py index 0e69f6dc57..413561e409 100644 --- a/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py +++ b/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py @@ -1,17 +1,12 @@ -import os - from maya import cmds from openpype.pipeline import publish -from openpype.hosts.maya.api.lib import ( - maintained_selection, attribute_values, delete_after -) class ExtractGPUCache(publish.Extractor): """Extract the content of the instance to an CPU cache file.""" - label = "CPU Cache" + label = "GPU Cache" hosts = ["maya"] families = ["model"] From a6805987ca5d2ddaea8c1d97fa98e5a964a0b540 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 17 Mar 2023 14:58:28 +0000 Subject: [PATCH 33/87] Ensure gpu and alembic do not overwrite when integrating. --- openpype/hosts/maya/plugins/publish/extract_gpu_cache.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py b/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py index 413561e409..e77890b534 100644 --- a/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py +++ b/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py @@ -30,7 +30,8 @@ class ExtractGPUCache(publish.Extractor): "ext": "abc", "files": filename, "stagingDir": staging_dir, - "data": {"heroSuffix": "gpu_cache"} + "data": {"heroSuffix": "gpu_cache"}, + "outputName": "gpu_cache" } instance.data["representations"].append(representation) From b63b02687011d1771f6165754e280e062e0ff763 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 17 Mar 2023 15:38:44 +0000 Subject: [PATCH 34/87] Filter to transforms with shapes only. --- .../maya/plugins/publish/extract_arnold_scene_source.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py index 7348c2db4d..63e6ff0f36 100644 --- a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py @@ -124,6 +124,13 @@ class ExtractArnoldSceneSource(publish.Extractor): if cmds.nodeType(node) != "transform": continue + # Only interested in transforms with shapes. + shapes = cmds.listRelatives( + node, shapes=True, fullPath=True + ) or [] + if not shapes: + continue + parent = cmds.listRelatives( node, parent=True, fullPath=True )[0] From 785c352407a2953a05bc958555e236a3771f4e09 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 17 Mar 2023 17:41:33 +0000 Subject: [PATCH 35/87] Adjust loader for extractor. --- openpype/hosts/maya/plugins/load/load_gpucache.py | 2 +- openpype/hosts/maya/plugins/publish/extract_gpu_cache.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_gpucache.py b/openpype/hosts/maya/plugins/load/load_gpucache.py index 07e5734f43..b7ca7292f5 100644 --- a/openpype/hosts/maya/plugins/load/load_gpucache.py +++ b/openpype/hosts/maya/plugins/load/load_gpucache.py @@ -11,7 +11,7 @@ class GpuCacheLoader(load.LoaderPlugin): """Load Alembic as gpuCache""" families = ["model", "animation", "proxyAbc", "pointcache"] - representations = ["abc"] + representations = ["gpu_cache"] label = "Import Gpu Cache" order = -5 diff --git a/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py b/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py index e77890b534..db84833722 100644 --- a/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py +++ b/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py @@ -8,7 +8,7 @@ class ExtractGPUCache(publish.Extractor): label = "GPU Cache" hosts = ["maya"] - families = ["model"] + families = ["model", "animation", "pointcache"] def process(self, instance): staging_dir = self.staging_dir(instance) From 0fe0e8c02cbafc56b202c828d5010e3d1b665d56 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sat, 18 Mar 2023 10:03:28 +0000 Subject: [PATCH 36/87] Settings for GPU cache extractor --- .../maya/plugins/publish/extract_gpu_cache.py | 33 +++++++++-- .../defaults/project_settings/maya.json | 15 +++++ .../schemas/schema_maya_publish.json | 59 +++++++++++++++++++ 3 files changed, 102 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py b/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py index db84833722..544a2d376a 100644 --- a/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py +++ b/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py @@ -1,3 +1,5 @@ +import json + from maya import cmds from openpype.pipeline import publish @@ -9,18 +11,39 @@ class ExtractGPUCache(publish.Extractor): label = "GPU Cache" hosts = ["maya"] families = ["model", "animation", "pointcache"] + simulationRate = 1.0 + sampleMultiplier = 1 + optimize = True + optimizationThreshold = 40000 + optimizeAnimationsForMotionBlur = True + writeMaterials = True + useBaseTessellation = True def process(self, instance): staging_dir = self.staging_dir(instance) filename = "{}.abc".format(instance.name) # Write out GPU cache file. - cmds.gpuCache( - instance[:], - directory=staging_dir, - fileName=filename, - saveMultipleFiles=False + kwargs = { + "directory": staging_dir, + "fileName": filename, + "saveMultipleFiles": False, + "simulationRate": self.simulationRate, + "sampleMultiplier": self.sampleMultiplier, + "optimize": self.optimize, + "optimizationThreshold": self.optimizationThreshold, + "optimizeAnimationsForMotionBlur": ( + self.optimizeAnimationsForMotionBlur + ), + "writeMaterials": self.writeMaterials, + "useBaseTessellation": self.useBaseTessellation + } + self.log.debug( + "Extract {} with:\n{}".format( + instance[:], json.dumps(kwargs, indent=4, sort_keys=True) + ) ) + cmds.gpuCache(instance[:], **kwargs) if "representations" not in instance.data: instance.data["representations"] = [] diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 63ba4542f3..801a04d144 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -923,6 +923,21 @@ "enabled": true, "active": true, "ogsfx_path": "/maya2glTF/PBR/shaders/glTF_PBR.ogsfx" + }, + "ExtractGPUCache": { + "enabled": true, + "families": [ + "model", + "animation", + "pointcache" + ], + "simulationRate": 0.0, + "sampleMultiplier": 0, + "optimize": true, + "optimizationThreshold": 40000, + "optimizeAnimationsForMotionBlur": true, + "writeMaterials": true, + "useBaseTessellation": true } }, "load": { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 3484f42f6b..03a447b05e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -992,6 +992,65 @@ "label": "GLSL Shader Directory" } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "ExtractGPUCache", + "label": "Extract GPU Cache", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "key": "simulationRate", + "label": "Evaluate Every", + "type": "number", + "decimal": 4, + "minimum": 1 + }, + { + "key": "sampleMultiplier", + "label": "Save Every", + "type": "number", + "minimum": 1 + }, + { + "key": "optimize", + "label": "Optimize Hierarchy", + "type": "boolean" + }, + { + "key": "optimizationThreshold", + "label": "Optimization Threshold", + "type": "number", + "minimum": 1 + }, + { + "key": "optimizeAnimationsForMotionBlur", + "label": "Optimize Animations For Motion Blur", + "type": "boolean" + }, + { + "key": "writeMaterials", + "label": "Write Materials", + "type": "boolean" + }, + { + "key": "useBaseTessellation", + "label": "User Base Tesselation", + "type": "boolean" + } + ] } ] } From 69ff474801b4f7cb8aa463cac6119b868a2669b9 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sat, 18 Mar 2023 10:26:59 +0000 Subject: [PATCH 37/87] Remove hero suffix logic. --- .../maya/plugins/publish/extract_gpu_cache.py | 1 - .../plugins/publish/integrate_hero_version.py | 16 ---------------- 2 files changed, 17 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py b/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py index 544a2d376a..6e8eaf57ce 100644 --- a/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py +++ b/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py @@ -53,7 +53,6 @@ class ExtractGPUCache(publish.Extractor): "ext": "abc", "files": filename, "stagingDir": staging_dir, - "data": {"heroSuffix": "gpu_cache"}, "outputName": "gpu_cache" } diff --git a/openpype/plugins/publish/integrate_hero_version.py b/openpype/plugins/publish/integrate_hero_version.py index 4d7d0accad..6e233dd5a9 100644 --- a/openpype/plugins/publish/integrate_hero_version.py +++ b/openpype/plugins/publish/integrate_hero_version.py @@ -306,22 +306,6 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): anatomy_filled = anatomy.format(anatomy_data) template_filled = anatomy_filled[template_key]["path"] - # For representations that have the same extension, an - # additional suffix can be available to make the destination - # filename different. - hero_suffix = repre_info["representation"]["data"].get( - "heroSuffix" - ) - if hero_suffix: - fill_data = copy.deepcopy(template_filled.used_values) - template_filled.template = template_filled.replace( - "." + fill_data["ext"], - "_{}.{}".format(hero_suffix, fill_data["ext"]) - ) - template_filled = StringTemplate( - template_filled.template - ).format(fill_data) - # Prepare new repre repre_data = { "path": str(template_filled), From 6490c1e387b04fe13ad6d754fa0a8d18e695ad89 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sat, 18 Mar 2023 10:27:49 +0000 Subject: [PATCH 38/87] Hound --- openpype/plugins/publish/integrate_hero_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_hero_version.py b/openpype/plugins/publish/integrate_hero_version.py index 6e233dd5a9..7adb2b66ec 100644 --- a/openpype/plugins/publish/integrate_hero_version.py +++ b/openpype/plugins/publish/integrate_hero_version.py @@ -18,7 +18,7 @@ from openpype.client.operations import ( prepare_hero_version_update_data, prepare_representation_update_data, ) -from openpype.lib import create_hard_link, StringTemplate +from openpype.lib import create_hard_link from openpype.pipeline import ( schema ) From 03c6fab3ea0ed2a979e06b29eef6914bfa440d76 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sat, 18 Mar 2023 10:29:01 +0000 Subject: [PATCH 39/87] Remove hero edits. --- openpype/plugins/publish/integrate_hero_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_hero_version.py b/openpype/plugins/publish/integrate_hero_version.py index 7adb2b66ec..e796f7b376 100644 --- a/openpype/plugins/publish/integrate_hero_version.py +++ b/openpype/plugins/publish/integrate_hero_version.py @@ -306,7 +306,6 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): anatomy_filled = anatomy.format(anatomy_data) template_filled = anatomy_filled[template_key]["path"] - # Prepare new repre repre_data = { "path": str(template_filled), "template": hero_template @@ -317,6 +316,7 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): if value is not None: repre_context[key] = value + # Prepare new repre repre = copy.deepcopy(repre_info["representation"]) repre["parent"] = new_hero_version["_id"] repre["context"] = repre_context From b3e8fb2c175eb8de1a43b88550b8659a95c9966d Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sat, 18 Mar 2023 10:40:30 +0000 Subject: [PATCH 40/87] Ensure unique extraction. --- openpype/hosts/maya/plugins/publish/extract_gpu_cache.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py b/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py index 6e8eaf57ce..b51242fa50 100644 --- a/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py +++ b/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py @@ -21,7 +21,7 @@ class ExtractGPUCache(publish.Extractor): def process(self, instance): staging_dir = self.staging_dir(instance) - filename = "{}.abc".format(instance.name) + filename = "{}_gpu_cache".format(instance.name) # Write out GPU cache file. kwargs = { @@ -51,9 +51,9 @@ class ExtractGPUCache(publish.Extractor): representation = { "name": "gpu_cache", "ext": "abc", - "files": filename, + "files": filename + ".abc", "stagingDir": staging_dir, - "outputName": "gpu_cache" + #"outputName": "gpu_cache" } instance.data["representations"].append(representation) From 12c54e22d6ebaa09f21621606f9dd1d96bd73aa7 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sat, 18 Mar 2023 10:40:47 +0000 Subject: [PATCH 41/87] Ensure unique integration --- openpype/hosts/maya/plugins/publish/extract_gpu_cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py b/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py index b51242fa50..91efab38ed 100644 --- a/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py +++ b/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py @@ -53,7 +53,7 @@ class ExtractGPUCache(publish.Extractor): "ext": "abc", "files": filename + ".abc", "stagingDir": staging_dir, - #"outputName": "gpu_cache" + "outputName": "gpu_cache" } instance.data["representations"].append(representation) From f38bb352444caf23a624fffe26614161abb75a73 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Mon, 20 Mar 2023 09:20:52 +0000 Subject: [PATCH 42/87] Update openpype/hosts/maya/plugins/publish/extract_gpu_cache.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fabià Serra Arrizabalaga --- openpype/hosts/maya/plugins/publish/extract_gpu_cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py b/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py index 91efab38ed..965122822c 100644 --- a/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py +++ b/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py @@ -6,7 +6,7 @@ from openpype.pipeline import publish class ExtractGPUCache(publish.Extractor): - """Extract the content of the instance to an CPU cache file.""" + """Extract the content of the instance to a GPU cache file.""" label = "GPU Cache" hosts = ["maya"] From 81c6b91633de5b9973ac70a8b515636a5b532a17 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Mon, 20 Mar 2023 17:24:03 +0000 Subject: [PATCH 43/87] Update openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py Co-authored-by: Roy Nieterau --- .../maya/plugins/publish/extract_arnold_scene_source.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py index 63e6ff0f36..f7d059cdb6 100644 --- a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py @@ -139,9 +139,11 @@ class ExtractArnoldSceneSource(publish.Extractor): parent, duplicate_transform ) - duplicate_transform = cmds.parent( - duplicate_transform, world=True - )[0] + + if cmds.listRelatives(duplicate_transform, parent=True): + duplicate_transform = cmds.parent( + duplicate_transform, world=True + )[0] basename = node.split("|")[-1].split(":")[-1] duplicate_transform = cmds.rename( From b22fbf58dc4d93a21c98507bfe9fb2a60a5b8a75 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Mon, 20 Mar 2023 17:34:13 +0000 Subject: [PATCH 44/87] Update openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py Co-authored-by: Roy Nieterau --- .../hosts/maya/plugins/publish/extract_arnold_scene_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py index f7d059cdb6..225b8dbb28 100644 --- a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py @@ -150,7 +150,7 @@ class ExtractArnoldSceneSource(publish.Extractor): duplicate_transform, basename ) - # Discard the children. + # Discard children nodes that are not shapes shapes = cmds.listRelatives( duplicate_transform, shapes=True, fullPath=True ) From c2c963c703900e5587828cb756e83a28da940ed9 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 20 Mar 2023 17:49:26 +0000 Subject: [PATCH 45/87] Improve same named nodes error message. --- .../publish/validate_arnold_scene_source.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py index d09a8610c4..a0e2e84a00 100644 --- a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py @@ -24,7 +24,7 @@ class ValidateArnoldSceneSource(pyblish.api.InstancePlugin): ungrouped_nodes = [] nodes_by_name = {} parents = [] - same_named_nodes = [] + same_named_nodes = {} for node in nodes: node_split = node.split("|") if len(node_split) == 2: @@ -39,16 +39,21 @@ class ValidateArnoldSceneSource(pyblish.api.InstancePlugin): # Check for same same nodes, which can happen in different # hierarchies. if node_name in nodes_by_name: - same_named_nodes.append((node, nodes_by_name[node_name])) + try: + same_named_nodes[node_name].append(node) + except KeyError: + same_named_nodes[node_name] = [ + nodes_by_name[node_name], node + ] nodes_by_name[node_name] = node if same_named_nodes: - raise PublishValidationError( - "Found nodes with the same name:\n{}".format( - "\n".join(["{}".format(n) for n in same_named_nodes]) - ) - ) + message = "Found nodes with the same name:" + for name, nodes in same_named_nodes.items(): + message += "\n\n\"{}\":\n{}".format(name, "\n".join(nodes)) + + raise PublishValidationError(message) return ungrouped_nodes, nodes_by_name, parents From 5511b479ab84fd5dbeba822f213fa1a117cd8c10 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 20 Mar 2023 17:50:40 +0000 Subject: [PATCH 46/87] _get_nodes_data > _get_nodes_by_name --- .../maya/plugins/publish/validate_arnold_scene_source.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py index a0e2e84a00..7055dc145e 100644 --- a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source.py @@ -20,7 +20,7 @@ class ValidateArnoldSceneSource(pyblish.api.InstancePlugin): families = ["ass"] label = "Validate Arnold Scene Source" - def _get_nodes_data(self, nodes): + def _get_nodes_by_name(self, nodes): ungrouped_nodes = [] nodes_by_name = {} parents = [] @@ -60,12 +60,12 @@ class ValidateArnoldSceneSource(pyblish.api.InstancePlugin): def process(self, instance): ungrouped_nodes = [] - nodes, content_nodes_by_name, content_parents = self._get_nodes_data( - instance.data["contentMembers"] + nodes, content_nodes_by_name, content_parents = ( + self._get_nodes_by_name(instance.data["contentMembers"]) ) ungrouped_nodes.extend(nodes) - nodes, proxy_nodes_by_name, proxy_parents = self._get_nodes_data( + nodes, proxy_nodes_by_name, proxy_parents = self._get_nodes_by_name( instance.data.get("proxy", []) ) ungrouped_nodes.extend(nodes) From cd5493edea38ddf4680c137b0a07ab6eca389b93 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 20 Mar 2023 17:54:15 +0000 Subject: [PATCH 47/87] _get_nodes_data > _get_nodes_by_name --- .../plugins/publish/validate_arnold_scene_source_cbid.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py index dafad2f40a..6a4799f73f 100644 --- a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py +++ b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py @@ -18,7 +18,7 @@ class ValidateArnoldSceneSourceCbid(pyblish.api.InstancePlugin): actions = [RepairAction] @staticmethod - def _get_nodes_data(nodes): + def _get_nodes_by_name(nodes): nodes_by_name = {} for node in nodes: node_name = node.rsplit("|", 1)[-1].rsplit(":", 1)[-1] @@ -27,10 +27,10 @@ class ValidateArnoldSceneSourceCbid(pyblish.api.InstancePlugin): return nodes_by_name def get_invalid_couples(self, instance): - content_nodes_by_name = self._get_nodes_data( + content_nodes_by_name = self._get_nodes_by_name( instance.data["contentMembers"] ) - proxy_nodes_by_name = self._get_nodes_data( + proxy_nodes_by_name = self._get_nodes_by_name( instance.data.get("proxy", []) ) From 12c85bc46cdd4e5621c3305a01e608ea9dc0e7a9 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 20 Mar 2023 17:55:09 +0000 Subject: [PATCH 48/87] Make get_invalid_couples class method --- .../plugins/publish/validate_arnold_scene_source_cbid.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py index 6a4799f73f..e27723e104 100644 --- a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py +++ b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py @@ -26,11 +26,12 @@ class ValidateArnoldSceneSourceCbid(pyblish.api.InstancePlugin): return nodes_by_name - def get_invalid_couples(self, instance): - content_nodes_by_name = self._get_nodes_by_name( + @classmethod + def get_invalid_couples(cls, instance): + content_nodes_by_name = cls._get_nodes_by_name( instance.data["contentMembers"] ) - proxy_nodes_by_name = self._get_nodes_by_name( + proxy_nodes_by_name = cls._get_nodes_by_name( instance.data.get("proxy", []) ) @@ -39,7 +40,7 @@ class ValidateArnoldSceneSourceCbid(pyblish.api.InstancePlugin): proxy_node = proxy_nodes_by_name.get(content_name, None) if not proxy_node: - self.log.debug( + cls.log.debug( "Content node '{}' has no matching proxy node.".format( content_node ) From b7e87dc1e193c056750c65900b21fb8093da1da9 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 20 Mar 2023 18:04:32 +0000 Subject: [PATCH 49/87] Hound --- .../hosts/maya/plugins/publish/extract_arnold_scene_source.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py index 225b8dbb28..8344f02894 100644 --- a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py @@ -139,7 +139,6 @@ class ExtractArnoldSceneSource(publish.Extractor): parent, duplicate_transform ) - if cmds.listRelatives(duplicate_transform, parent=True): duplicate_transform = cmds.parent( duplicate_transform, world=True From 2246fa15abeae14607ada49e189bc88c3280f3f0 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 21 Mar 2023 07:56:19 +0000 Subject: [PATCH 50/87] Documentation for settings --- website/docs/admin_hosts_maya.md | 37 ++++++++++++++++--- website/docs/assets/maya-admin_gpu_cache.png | Bin 0 -> 20248 bytes 2 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 website/docs/assets/maya-admin_gpu_cache.png diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index ae0cf76f53..d38b911062 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -6,7 +6,7 @@ sidebar_label: Maya ## Publish Plugins -### Render Settings Validator +### Render Settings Validator `ValidateRenderSettings` @@ -51,7 +51,7 @@ just one instance of this node type but if that is not so, validator will go thr instances and check the value there. Node type for **VRay** settings is `VRaySettingsNode`, for **Renderman** it is `rmanGlobals`, for **Redshift** it is `RedshiftOptions`. -### Model Name Validator +### Model Name Validator `ValidateRenderSettings` @@ -95,7 +95,7 @@ You can set various aspects of scene submission to farm with per-project setting - **Optional** will mark sumission plugin optional - **Active** will enable/disable plugin - - **Tile Assembler Plugin** will set what should be used to assemble tiles on Deadline. Either **Open Image IO** will be used + - **Tile Assembler Plugin** will set what should be used to assemble tiles on Deadline. Either **Open Image IO** will be used or Deadlines **Draft Tile Assembler**. - **Use Published scene** enable to render from published scene instead of scene in work area. Rendering from published files is much safer. - **Use Asset dependencies** will mark job pending on farm until asset dependencies are fulfilled - for example Deadline will wait for scene file to be synced to cloud, etc. @@ -107,6 +107,35 @@ or Deadlines **Draft Tile Assembler**. This is useful to fix some specific renderer glitches and advanced hacking of Maya Scene files. `Patch name` is label for patch for easier orientation. `Patch regex` is regex used to find line in file, after `Patch line` string is inserted. Note that you need to add line ending. +### Extract GPU Cache + +![Maya GPU Cache](assets/maya-admin_gpu_cache.png) + +- **Evaluate Every** Specifies which samples are saved during cache creation. For example, a value of 2 specifies that only every other sample specified by the Evaluate every # frame(s) option is saved to your Alembic file. + +- **Save Every** Specifies how often samples are taken during file creation. By default, one sample of your object's transformations is taken every frame and saved to the Alembic file. + + For example, a value of 2 caches the transformations of the current object at every other frame of the Cache Time Range. + +- **Optimize Hierarchy** When on, nodes and objects in a selected hierarchy are consolidated to maximize the performance of the cache file during playback. +- **Optimization Threshold** (Available only when Optimize Hierarchy is on.) Specifies the maximum number of vertices contained in a single draw primitive. The default value of 40000 may be ideal for most Maya supported graphics cards. When set to the default value, after optimization, each object in the GPU cache file(s) will have no more than 40000 vertices. This value can be set higher depending on the memory available on your system graphics card. + +- **Optimize Animations for Motion Blur** When on, objects with animated transform nodes display with motion blur when the cache is played back in Viewport 2.0 render mode. See Viewport 2.0 options. + + Maya first determines if the GPU cache includes animation data. If the GPU cache is static and does not contain animation data, Maya does not optimize the GPU cache for motion blur. + +:::note Motion Blur does not support Cached Playback. +::: + +- **Write Materials** When on, Maya exports the Lambert and Phong materials from source geometry to the GPU Cache file. These materials display when the GPU-cached file is played back in Viewport 2.0. + + GPU-cached objects support all the high-quality lighting and shading effects provide by the Viewport 2.0 rendering mode. See Viewport 2.0 options. + +:::note Lambert and Phong materials do not display on GPU-cached files when they are played back in scene view's High Quality Rendering or Default Quality Rendering modes. +::: + +- **Use Base Tessellation** Exports geometry with base tessellation and no smoothing applied. If this setting is turned off, the extractor will export geometry with the current Smooth Mesh Preview setting applied. + ## Custom Menu You can add your custom tools menu into Maya by extending definitions in **Maya -> Scripts Menu Definition**. ![Custom menu definition](assets/maya-admin_scriptsmenu.png) @@ -169,5 +198,3 @@ Fill in the necessary fields (the optional fields are regex filters) - Build your workfile ![maya build template](assets/maya-build_workfile_from_template.png) - - diff --git a/website/docs/assets/maya-admin_gpu_cache.png b/website/docs/assets/maya-admin_gpu_cache.png new file mode 100644 index 0000000000000000000000000000000000000000..8b07b06c1e26c91ce42a77b34182abd0c5db975e GIT binary patch literal 20248 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 Hr-UW|ar0GU literal 0 HcmV?d00001 From b40289c9b5df00ae2222d70fce6a5b8a1e37498d Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 21 Mar 2023 09:17:20 +0000 Subject: [PATCH 51/87] simulationRate > step, sampleMultiplier > stepSave --- openpype/hosts/maya/plugins/publish/extract_gpu_cache.py | 8 ++++---- openpype/settings/defaults/project_settings/maya.json | 4 ++-- .../projects_schema/schemas/schema_maya_publish.json | 8 ++++---- website/docs/admin_hosts_maya.md | 6 +++--- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py b/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py index 965122822c..f92bb9c67f 100644 --- a/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py +++ b/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py @@ -11,8 +11,8 @@ class ExtractGPUCache(publish.Extractor): label = "GPU Cache" hosts = ["maya"] families = ["model", "animation", "pointcache"] - simulationRate = 1.0 - sampleMultiplier = 1 + step = 1.0 + stepSave = 1 optimize = True optimizationThreshold = 40000 optimizeAnimationsForMotionBlur = True @@ -28,8 +28,8 @@ class ExtractGPUCache(publish.Extractor): "directory": staging_dir, "fileName": filename, "saveMultipleFiles": False, - "simulationRate": self.simulationRate, - "sampleMultiplier": self.sampleMultiplier, + "step": self.step, + "stepSave": self.stepSave, "optimize": self.optimize, "optimizationThreshold": self.optimizationThreshold, "optimizeAnimationsForMotionBlur": ( diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 8fec46ccf2..b06a97dce3 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -932,8 +932,8 @@ "animation", "pointcache" ], - "simulationRate": 0.0, - "sampleMultiplier": 0, + "step": 0.0, + "stepSave": 0, "optimize": true, "optimizationThreshold": 40000, "optimizeAnimationsForMotionBlur": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 24a4805656..09b235fa0e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -1016,15 +1016,15 @@ "object_type": "text" }, { - "key": "simulationRate", - "label": "Evaluate Every", + "key": "step", + "label": "Step", "type": "number", "decimal": 4, "minimum": 1 }, { - "key": "sampleMultiplier", - "label": "Save Every", + "key": "stepSave", + "label": "Step Save", "type": "number", "minimum": 1 }, diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index 3cfd28b20a..e07bb7d669 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -110,12 +110,12 @@ This is useful to fix some specific renderer glitches and advanced hacking of Ma ![Maya GPU Cache](assets/maya-admin_gpu_cache.png) -- **Evaluate Every** Specifies which samples are saved during cache creation. For example, a value of 2 specifies that only every other sample specified by the Evaluate every # frame(s) option is saved to your Alembic file. - -- **Save Every** Specifies how often samples are taken during file creation. By default, one sample of your object's transformations is taken every frame and saved to the Alembic file. +- **Step** Specifies how often samples are taken during file creation. By default, one sample of your object's transformations is taken every frame and saved to the Alembic file. For example, a value of 2 caches the transformations of the current object at every other frame of the Cache Time Range. +- **Step Save** Specifies which samples are saved during cache creation. For example, a value of 2 specifies that only every other sample specified by the Step # frame(s) option is saved to your Alembic file. + - **Optimize Hierarchy** When on, nodes and objects in a selected hierarchy are consolidated to maximize the performance of the cache file during playback. - **Optimization Threshold** (Available only when Optimize Hierarchy is on.) Specifies the maximum number of vertices contained in a single draw primitive. The default value of 40000 may be ideal for most Maya supported graphics cards. When set to the default value, after optimization, each object in the GPU cache file(s) will have no more than 40000 vertices. This value can be set higher depending on the memory available on your system graphics card. From e083a18dda71214eb33c5b1f3872cc324f7ff915 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 21 Mar 2023 09:19:54 +0000 Subject: [PATCH 52/87] Default values for step and stepSave. --- openpype/settings/defaults/project_settings/maya.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index b06a97dce3..7757f201ad 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -932,8 +932,8 @@ "animation", "pointcache" ], - "step": 0.0, - "stepSave": 0, + "step": 1.0, + "stepSave": 1, "optimize": true, "optimizationThreshold": 40000, "optimizeAnimationsForMotionBlur": true, From 7e7f6ced16d0e867ed2f6a67c05012c5020c0c52 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Tue, 21 Mar 2023 10:54:07 +0000 Subject: [PATCH 53/87] Update openpype/hosts/maya/plugins/publish/extract_gpu_cache.py --- openpype/hosts/maya/plugins/publish/extract_gpu_cache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py b/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py index f92bb9c67f..deee456982 100644 --- a/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py +++ b/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py @@ -28,8 +28,8 @@ class ExtractGPUCache(publish.Extractor): "directory": staging_dir, "fileName": filename, "saveMultipleFiles": False, - "step": self.step, - "stepSave": self.stepSave, + "simulationRate": self.step, + "sampleMultiplier": self.stepSave, "optimize": self.optimize, "optimizationThreshold": self.optimizationThreshold, "optimizeAnimationsForMotionBlur": ( From 9f6bc9459a5288bfa55e7b78a4cb8c49e0ae681c Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 21 Mar 2023 18:03:48 +0000 Subject: [PATCH 54/87] Revert alembic representation exclusion. --- openpype/hosts/maya/plugins/load/load_gpucache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_gpucache.py b/openpype/hosts/maya/plugins/load/load_gpucache.py index b7ca7292f5..c447311454 100644 --- a/openpype/hosts/maya/plugins/load/load_gpucache.py +++ b/openpype/hosts/maya/plugins/load/load_gpucache.py @@ -11,7 +11,7 @@ class GpuCacheLoader(load.LoaderPlugin): """Load Alembic as gpuCache""" families = ["model", "animation", "proxyAbc", "pointcache"] - representations = ["gpu_cache"] + representations = ["abc", "gpu_cache"] label = "Import Gpu Cache" order = -5 From 2e8ba574e258384d3ee3b630b4af4e31a25878db Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 21 Mar 2023 18:04:10 +0000 Subject: [PATCH 55/87] Appropriate label --- openpype/hosts/maya/plugins/load/load_gpucache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_gpucache.py b/openpype/hosts/maya/plugins/load/load_gpucache.py index c447311454..f342679791 100644 --- a/openpype/hosts/maya/plugins/load/load_gpucache.py +++ b/openpype/hosts/maya/plugins/load/load_gpucache.py @@ -13,7 +13,7 @@ class GpuCacheLoader(load.LoaderPlugin): families = ["model", "animation", "proxyAbc", "pointcache"] representations = ["abc", "gpu_cache"] - label = "Import Gpu Cache" + label = "Load Gpu Cache" order = -5 icon = "code-fork" color = "orange" From e516a75ce3649be96ab9a53abca42e4d7c417e35 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 21 Mar 2023 18:04:16 +0000 Subject: [PATCH 56/87] Code cosmetics --- .../hosts/maya/plugins/load/load_gpucache.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_gpucache.py b/openpype/hosts/maya/plugins/load/load_gpucache.py index f342679791..794b21eb5d 100644 --- a/openpype/hosts/maya/plugins/load/load_gpucache.py +++ b/openpype/hosts/maya/plugins/load/load_gpucache.py @@ -1,5 +1,9 @@ import os +import maya.cmds as cmds + +from openpype.hosts.maya.api.pipeline import containerise +from openpype.hosts.maya.api.lib import unique_namespace from openpype.pipeline import ( load, get_representation_path @@ -20,10 +24,6 @@ class GpuCacheLoader(load.LoaderPlugin): def load(self, context, name, namespace, data): - import maya.cmds as cmds - from openpype.hosts.maya.api.pipeline import containerise - from openpype.hosts.maya.api.lib import unique_namespace - asset = context['asset']['name'] namespace = namespace or unique_namespace( asset + "_", @@ -42,10 +42,9 @@ class GpuCacheLoader(load.LoaderPlugin): c = colors.get('model') if c is not None: cmds.setAttr(root + ".useOutlinerColor", 1) - cmds.setAttr(root + ".outlinerColor", - (float(c[0])/255), - (float(c[1])/255), - (float(c[2])/255) + cmds.setAttr( + root + ".outlinerColor", + (float(c[0]) / 255), (float(c[1]) / 255), (float(c[2]) / 255) ) # Create transform with shape @@ -74,9 +73,6 @@ class GpuCacheLoader(load.LoaderPlugin): loader=self.__class__.__name__) def update(self, container, representation): - - import maya.cmds as cmds - path = get_representation_path(representation) # Update the cache @@ -96,7 +92,6 @@ class GpuCacheLoader(load.LoaderPlugin): self.update(container, representation) def remove(self, container): - import maya.cmds as cmds members = cmds.sets(container['objectName'], query=True) cmds.lockNode(members, lock=False) cmds.delete([container['objectName']] + members) From 525db92e4aaf083d6c92b08edde1ed84c0ed1710 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 23 Mar 2023 15:43:19 +0000 Subject: [PATCH 57/87] Update openpype/settings/defaults/project_settings/maya.json --- openpype/settings/defaults/project_settings/maya.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 7757f201ad..7b3d54f869 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -926,7 +926,7 @@ "ogsfx_path": "/maya2glTF/PBR/shaders/glTF_PBR.ogsfx" }, "ExtractGPUCache": { - "enabled": true, + "enabled": false, "families": [ "model", "animation", From be3251d007c2dd151078af62527889d2cb89d851 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 27 Mar 2023 16:32:34 +0200 Subject: [PATCH 58/87] Cleanup Collect Review code --- .../maya/plugins/publish/collect_review.py | 102 ++++++++++-------- 1 file changed, 56 insertions(+), 46 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 36affe852b..713bda25ba 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -4,7 +4,7 @@ import pymel.core as pm import pyblish.api from openpype.client import get_subset_by_name -from openpype.pipeline import legacy_io +from openpype.pipeline import legacy_io, KnownPublishError class CollectReview(pyblish.api.InstancePlugin): @@ -15,7 +15,6 @@ class CollectReview(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.3 label = 'Collect Review Data' families = ["review"] - legacy = True def process(self, instance): @@ -35,57 +34,68 @@ class CollectReview(pyblish.api.InstancePlugin): self.log.debug('members: {}'.format(members)) # validate required settings - assert len(cameras) == 1, "Not a single camera found in extraction" + if len(cameras) == 0: + raise KnownPublishError("Not camera found in review " + "instance: {}".format(instance)) + elif len(cameras) > 2: + raise KnownPublishError( + "Only a single camera is allowed for a review instance but " + "more than one camera found in review instance: {}. " + "Cameras found: {}".format(instance, ", ".join(cameras))) + camera = cameras[0] self.log.debug('camera: {}'.format(camera)) - objectset = instance.context.data['objectsets'] + context = instance.context + objectset = context.data['objectsets'] - reviewable_subset = None - reviewable_subset = list(set(members) & set(objectset)) - if reviewable_subset: - assert len(reviewable_subset) <= 1, "Multiple subsets for review" - self.log.debug('subset for review: {}'.format(reviewable_subset)) + reviewable_subsets = list(set(members) & set(objectset)) + if reviewable_subsets: + if len(reviewable_subsets) > 1: + raise KnownPublishError( + "Multiple attached subsets for review are not supported. " + "Attached: {}".format(", ".join(reviewable_subsets)) + ) - i = 0 - for inst in instance.context: + reviewable_subset = reviewable_subsets[0] + self.log.debug( + "Subset attached to review: {}".format(reviewable_subset) + ) - self.log.debug('filtering {}'.format(inst)) - data = instance.context[i].data + # Find the relevant publishing instance in the current context + reviewable_inst = next(inst for inst in context + if inst.name == reviewable_subset) + data = reviewable_inst.data - if inst.name != reviewable_subset[0]: - self.log.debug('subset name does not match {}'.format( - reviewable_subset[0])) - i += 1 - continue + self.log.debug( + 'Adding review family to {}'.format(reviewable_subset) + ) + if data.get('families'): + data['families'].append('review') + else: + data['families'] = ['review'] + + data['review_camera'] = camera + data['frameStartFtrack'] = instance.data["frameStartHandle"] + data['frameEndFtrack'] = instance.data["frameEndHandle"] + data['frameStartHandle'] = instance.data["frameStartHandle"] + data['frameEndHandle'] = instance.data["frameEndHandle"] + data["frameStart"] = instance.data["frameStart"] + data["frameEnd"] = instance.data["frameEnd"] + data['handles'] = instance.data.get('handles', None) + data['step'] = instance.data['step'] + data['fps'] = instance.data['fps'] + data['review_width'] = instance.data['review_width'] + data['review_height'] = instance.data['review_height'] + data["isolate"] = instance.data["isolate"] + data["panZoom"] = instance.data.get("panZoom", False) + data["panel"] = instance.data["panel"] + + # The review instance must be active + cmds.setAttr(str(instance) + '.active', 1) + + instance.data['remove'] = True - if data.get('families'): - data['families'].append('review') - else: - data['families'] = ['review'] - self.log.debug('adding review family to {}'.format( - reviewable_subset)) - data['review_camera'] = camera - # data["publish"] = False - data['frameStartFtrack'] = instance.data["frameStartHandle"] - data['frameEndFtrack'] = instance.data["frameEndHandle"] - data['frameStartHandle'] = instance.data["frameStartHandle"] - data['frameEndHandle'] = instance.data["frameEndHandle"] - data["frameStart"] = instance.data["frameStart"] - data["frameEnd"] = instance.data["frameEnd"] - data['handles'] = instance.data.get('handles', None) - data['step'] = instance.data['step'] - data['fps'] = instance.data['fps'] - data['review_width'] = instance.data['review_width'] - data['review_height'] = instance.data['review_height'] - data["isolate"] = instance.data["isolate"] - data["panZoom"] = instance.data.get("panZoom", False) - data["panel"] = instance.data["panel"] - cmds.setAttr(str(instance) + '.active', 1) - self.log.debug('data {}'.format(instance.context[i].data)) - instance.context[i].data.update(data) - instance.data['remove'] = True - self.log.debug('isntance data {}'.format(instance.data)) else: legacy_subset_name = task + 'Review' asset_doc = instance.context.data['assetEntity'] @@ -107,7 +117,7 @@ class CollectReview(pyblish.api.InstancePlugin): instance.data["frameEndHandle"] # make ftrack publishable - instance.data["families"] = ['ftrack'] + instance.data.setdefault("families", []).append(['ftrack']) cmds.setAttr(str(instance) + '.active', 1) From 63851a6bd00fb1b52d255e5c7295cfc5c6c51b50 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 27 Mar 2023 16:36:54 +0200 Subject: [PATCH 59/87] Fix typo --- openpype/hosts/maya/plugins/publish/collect_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 713bda25ba..47071631a2 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -117,7 +117,7 @@ class CollectReview(pyblish.api.InstancePlugin): instance.data["frameEndHandle"] # make ftrack publishable - instance.data.setdefault("families", []).append(['ftrack']) + instance.data.setdefault("families", []).append('ftrack') cmds.setAttr(str(instance) + '.active', 1) From 2d92deae1694fb7761032518b672b647890d5b5e Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 29 Mar 2023 11:31:49 +0100 Subject: [PATCH 60/87] Fixes --- openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py b/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py index 6d7be80060..449eacb40f 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py +++ b/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py @@ -118,6 +118,7 @@ def shading_engine_assignments(shading_engine, attribute, nodes, assignments): shading_engine, attribute ) ) + return # Strip off component assignments for i, node in enumerate(nodes): @@ -261,4 +262,4 @@ def assign_look(standin, subset): index += 1 - cmds.sets(operator, edit=True, addElement=container_node[0]) + cmds.sets(operator, edit=True, addElement=container_node) From 57bbf946fda65a0e0b6ce3fce04de0f0f8dabf16 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 29 Mar 2023 11:31:55 +0100 Subject: [PATCH 61/87] Code cosmetics --- .../hosts/maya/tools/mayalookassigner/arnold_standin.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py b/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py index 449eacb40f..dfffbc5961 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py +++ b/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py @@ -111,7 +111,8 @@ def get_standin_path(node): def shading_engine_assignments(shading_engine, attribute, nodes, assignments): shader_inputs = cmds.listConnections( - shading_engine + "." + attribute, source=True) + shading_engine + "." + attribute, source=True + ) if not shader_inputs: log.info( "Shading engine \"{}\" missing input \"{}\"".format( @@ -124,9 +125,9 @@ def shading_engine_assignments(shading_engine, attribute, nodes, assignments): for i, node in enumerate(nodes): if "." in node: log.warning( - ("Converting face assignment to full object " - "assignment. This conversion can be lossy: " - "{}").format(node)) + "Converting face assignment to full object assignment. This " + "conversion can be lossy: {}".format(node) + ) nodes[i] = node.split(".")[0] shader_type = "shader" if attribute == "surfaceShader" else "disp_map" From 73b369a32d26e2c01d3140b9074d1fd83402a772 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 29 Mar 2023 15:37:36 +0100 Subject: [PATCH 62/87] Update openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py Co-authored-by: Roy Nieterau --- .../hosts/maya/plugins/publish/extract_arnold_scene_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py index 8344f02894..ece53edc23 100644 --- a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py @@ -144,7 +144,7 @@ class ExtractArnoldSceneSource(publish.Extractor): duplicate_transform, world=True )[0] - basename = node.split("|")[-1].split(":")[-1] + basename = node.rsplit("|", 1)[-1].rsplit(":", 1)[-1] duplicate_transform = cmds.rename( duplicate_transform, basename ) From dea30d2f689a330488c9a5e0a91a7a2d769dbf89 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 29 Mar 2023 16:06:27 +0100 Subject: [PATCH 63/87] BigRoy feedback --- .../publish/collect_arnold_scene_source.py | 7 ++++--- .../publish/extract_arnold_scene_source.py | 8 +------- .../tools/mayalookassigner/arnold_standin.py | 18 ++++++++---------- .../maya/tools/mayalookassigner/commands.py | 12 +++++------- .../tools/mayalookassigner/vray_proxies.py | 8 +++++--- 5 files changed, 23 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py index ab15d0419f..0845f653b1 100644 --- a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py @@ -22,9 +22,10 @@ class CollectArnoldSceneSource(pyblish.api.InstancePlugin): self.log.warning("Skipped empty instance: \"%s\" " % objset) continue if objset.endswith("content_SET"): - set_members = get_all_children(cmds.ls(members, long=True)) - instance.data["contentMembers"] = set_members - self.log.debug("content members: {}".format(set_members)) + members = cmds.ls(members, long=True) + children = get_all_children(members) + instance.data["contentMembers"] = children + self.log.debug("content members: {}".format(children)) elif objset.endswith("proxy_SET"): set_members = get_all_children(cmds.ls(members, long=True)) instance.data["proxy"] = set_members diff --git a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py index 8344f02894..ce5dc27bbd 100644 --- a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py @@ -126,18 +126,12 @@ class ExtractArnoldSceneSource(publish.Extractor): # Only interested in transforms with shapes. shapes = cmds.listRelatives( - node, shapes=True, fullPath=True + node, shapes=True, noIntermediate=True ) or [] if not shapes: continue - parent = cmds.listRelatives( - node, parent=True, fullPath=True - )[0] duplicate_transform = cmds.duplicate(node)[0] - duplicate_transform = "{}|{}".format( - parent, duplicate_transform - ) if cmds.listRelatives(duplicate_transform, parent=True): duplicate_transform = cmds.parent( diff --git a/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py b/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py index dfffbc5961..771b256614 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py +++ b/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py @@ -58,17 +58,17 @@ def calculate_visibility_mask(attributes): return mask -def get_cbid_by_node(path): - """Get cbid from Arnold Scene Source. +def get_id_by_node(path): + """Get node id from Arnold Scene Source. Args: path (string): Path to Arnold Scene Source. Returns: - (dict): Dictionary with node full name/path and CBID. + (dict): Dictionary with node full name/path and id. """ import arnold - results = {} + results = defaultdict(list) arnold.AiBegin() @@ -82,10 +82,7 @@ def get_cbid_by_node(path): node = arnold.AiNodeIteratorGetNext(iter) if arnold.AiNodeIs(node, "polymesh"): node_name = arnold.AiNodeGetName(node) - try: - results[arnold.AiNodeGetStr(node, "cbId")].append(node_name) - except KeyError: - results[arnold.AiNodeGetStr(node, "cbId")] = [node_name] + results[arnold.AiNodeGetStr(node, "cbId")].append(node_name) arnold.AiNodeIteratorDestroy(iter) arnold.AiEnd() @@ -139,7 +136,7 @@ def shading_engine_assignments(shading_engine, attribute, nodes, assignments): def assign_look(standin, subset): log.info("Assigning {} to {}.".format(subset, standin)) - nodes_by_id = get_cbid_by_node(get_standin_path(standin)) + nodes_by_id = get_id_by_node(get_standin_path(standin)) # Group by asset id so we run over the look per asset node_ids_by_asset_id = defaultdict(set) @@ -164,7 +161,8 @@ def assign_look(standin, subset): continue relationships = lib.get_look_relationships(version["_id"]) - shader_nodes, container_node = lib.load_look(version["_id"]) + shader_nodes, container_nodes = lib.load_look(version["_id"]) + container_node = container_nodes[0] namespace = shader_nodes[0].split(":")[0] # Get only the node ids and paths related to this asset diff --git a/openpype/hosts/maya/tools/mayalookassigner/commands.py b/openpype/hosts/maya/tools/mayalookassigner/commands.py index d7061f12e1..d78e31111d 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/commands.py +++ b/openpype/hosts/maya/tools/mayalookassigner/commands.py @@ -121,15 +121,13 @@ def create_asset_id_hash(nodes): path = cmds.getAttr("{}.fileName".format(node)) ids = get_alembic_ids_cache(path) for k, _ in ids.items(): - pid = k.split(":")[0] - if node not in node_id_hash[pid]: - node_id_hash[pid].append(node) + id = k.split(":")[0] + node_id_hash[id].append(node) elif shapes and cmds.nodeType(shapes[0]) == "aiStandIn": path = arnold_standin.get_standin_path(shapes[0]) - for id, _ in arnold_standin.get_cbid_by_node(path).items(): - pid = id.split(":")[0] - if shapes[0] not in node_id_hash[pid]: - node_id_hash[pid].append(shapes[0]) + for id, _ in arnold_standin.get_id_by_node(path).items(): + id = id.split(":")[0] + node_id_hash[id].append(shapes[0]) else: value = lib.get_id(node) if value is None: diff --git a/openpype/hosts/maya/tools/mayalookassigner/vray_proxies.py b/openpype/hosts/maya/tools/mayalookassigner/vray_proxies.py index 6ee618f37a..1d2ec5fd87 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/vray_proxies.py +++ b/openpype/hosts/maya/tools/mayalookassigner/vray_proxies.py @@ -11,7 +11,7 @@ from maya import cmds from openpype.client import get_last_version_by_subset_name from openpype.pipeline import legacy_io -from openpype.hosts.maya import api +import openpype.hosts.maya.lib as maya_lib from . import lib @@ -189,8 +189,10 @@ def vrayproxy_assign_look(vrayproxy, subset="lookDefault"): node_id: nodes_by_id[node_id] for node_id in node_ids } edits = list( - api.lib.iter_shader_edits( - relationships, shadernodes, asset_nodes_by_id)) + maya_lib.iter_shader_edits( + relationships, shadernodes, asset_nodes_by_id + ) + ) # Create assignments assignments = {} From 9a71cfec41577ce7c766a99d41c3cfc5c24b5956 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 29 Mar 2023 17:57:12 +0200 Subject: [PATCH 64/87] Allow passing an empty environment explicitly like `env = {}` --- openpype/lib/execute.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index 6f9a095285..c01aad734f 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -198,7 +198,7 @@ def run_openpype_process(*args, **kwargs): args = get_openpype_execute_args(*args) env = kwargs.pop("env", None) # Keep env untouched if are passed and not empty - if not env: + if env is None: # Skip envs that can affect OpenPype process # - fill more if you find more env = clean_envs_for_openpype_process(os.environ) From 391b7450dd98bc748253c39630fc45c023ca7be9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 29 Mar 2023 17:58:22 +0200 Subject: [PATCH 65/87] Don't pass empty `env` since it didn't do that prior to this either --- openpype/pipeline/colorspace.py | 3 +-- openpype/plugins/publish/extract_burnin.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 2085e2d37f..4cea92a4e4 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -218,8 +218,7 @@ def get_data_subprocess(config_path, data_type): log.info("Executing: {}".format(" ".join(args))) process_kwargs = { - "logger": log, - "env": {} + "logger": log } run_openpype_process(*args, **process_kwargs) diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index 95575444b2..ed39bd9354 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -336,8 +336,7 @@ class ExtractBurnin(publish.Extractor): # Run burnin script process_kwargs = { - "logger": self.log, - "env": {} + "logger": self.log } run_openpype_process(*args, **process_kwargs) From 168a49dba66b1dca067b0909300290487ae94e49 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 29 Mar 2023 18:00:03 +0200 Subject: [PATCH 66/87] Remove both `PYTHONPATH` and `PYTHONHOME` - Fixes an issue with Houdini Py3.7 conflict with OpenPype Py3.9 because Houdini sets `PYTHONHOME` --- openpype/lib/execute.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index c01aad734f..0c5ae2df8a 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -170,11 +170,13 @@ def clean_envs_for_openpype_process(env=None): """ if env is None: env = os.environ - return { - key: value - for key, value in env.items() - if key not in ("PYTHONPATH",) - } + + # Exclude some environment variables from a copy of the environment + env = env.copy() + for key in ["PYTHONPATH", "PYTHONHOME"]: + env.pop(key, None) + + return env def run_openpype_process(*args, **kwargs): From 0d8a8af3711d6e696778ef8af611859886beebd6 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 29 Mar 2023 17:26:25 +0100 Subject: [PATCH 67/87] Use lib.get_all_children --- .../maya/tools/mayalookassigner/commands.py | 24 +------------------ 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/openpype/hosts/maya/tools/mayalookassigner/commands.py b/openpype/hosts/maya/tools/mayalookassigner/commands.py index d78e31111d..3fd367e860 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/commands.py +++ b/openpype/hosts/maya/tools/mayalookassigner/commands.py @@ -45,33 +45,11 @@ def get_namespace_from_node(node): return parts[0] if len(parts) > 1 else u":" -def list_descendents(nodes): - """Include full descendant hierarchy of given nodes. - - This is a workaround to cmds.listRelatives(allDescendents=True) because - this way correctly keeps children instance paths (see Maya documentation) - - This fixes LKD-26: assignments not working as expected on instanced shapes. - - Return: - list: List of children descendents of nodes - - """ - result = [] - while True: - nodes = cmds.listRelatives(nodes, - fullPath=True) - if nodes: - result.extend(nodes) - else: - return result - - def get_selected_nodes(): """Get information from current selection""" selection = cmds.ls(selection=True, long=True) - hierarchy = list_descendents(selection) + hierarchy = lib.get_all_children(selection) return list(set(selection + hierarchy)) From 32a30127a37da7ee0ac8aee6d4a69265b70c110e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 29 Mar 2023 23:06:21 +0200 Subject: [PATCH 68/87] Fix playblasting in Maya 2020 with override viewport options #4730 --- openpype/vendor/python/common/capture.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/vendor/python/common/capture.py b/openpype/vendor/python/common/capture.py index 09a42d84d1..e99ea5438d 100644 --- a/openpype/vendor/python/common/capture.py +++ b/openpype/vendor/python/common/capture.py @@ -733,6 +733,14 @@ def _applied_viewport_options(options, panel): options = dict(ViewportOptions, **(options or {})) + # BUGFIX Maya 2020 some keys in viewport options dict may not be unicode + # This is a local OpenPype edit to capture.py for issue #4730 + # TODO: Remove when dropping Maya 2020 compatibility + if int(cmds.about(version=True)) <= 2020: + options = { + str(key): value for key, value in options.items() + } + # separate the plugin display filter options since they need to # be set differently (see #55) plugins = cmds.pluginDisplayFilter(query=True, listFilters=True) From 0d7b42957f56e637a32ee489cd56588ddc7f7627 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 30 Mar 2023 07:08:24 +0100 Subject: [PATCH 69/87] Add shape to container when loading. --- openpype/hosts/maya/plugins/load/load_arnold_standin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_arnold_standin.py b/openpype/hosts/maya/plugins/load/load_arnold_standin.py index 3097ba21aa..a527844f7b 100644 --- a/openpype/hosts/maya/plugins/load/load_arnold_standin.py +++ b/openpype/hosts/maya/plugins/load/load_arnold_standin.py @@ -84,7 +84,7 @@ class ArnoldStandinLoader(load.LoaderPlugin): sequence = is_sequence(os.listdir(os.path.dirname(self.fname))) cmds.setAttr(standin_shape + ".useFrameExtension", sequence) - nodes = [root, standin] + nodes = [root, standin, standin_shape] if operator is not None: nodes.append(operator) self[:] = nodes From 4889ffdd0b4f1f62ffb87418d5b903064e013bb2 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 30 Mar 2023 07:08:44 +0100 Subject: [PATCH 70/87] Dont query shapes when creating id hashes. --- openpype/hosts/maya/tools/mayalookassigner/commands.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/tools/mayalookassigner/commands.py b/openpype/hosts/maya/tools/mayalookassigner/commands.py index 3fd367e860..a71cb361a4 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/commands.py +++ b/openpype/hosts/maya/tools/mayalookassigner/commands.py @@ -86,7 +86,6 @@ def create_asset_id_hash(nodes): """ node_id_hash = defaultdict(list) for node in nodes: - shapes = cmds.listRelatives(node, shapes=True, fullPath=True) # iterate over content of reference node if cmds.nodeType(node) == "reference": ref_hashes = create_asset_id_hash( @@ -101,11 +100,11 @@ def create_asset_id_hash(nodes): for k, _ in ids.items(): id = k.split(":")[0] node_id_hash[id].append(node) - elif shapes and cmds.nodeType(shapes[0]) == "aiStandIn": - path = arnold_standin.get_standin_path(shapes[0]) + elif cmds.nodeType(node) == "aiStandIn": + path = arnold_standin.get_standin_path(node) for id, _ in arnold_standin.get_id_by_node(path).items(): id = id.split(":")[0] - node_id_hash[id].append(shapes[0]) + node_id_hash[id].append(node) else: value = lib.get_id(node) if value is None: From 35a5d5f172a5a04fe96d4e568ecd37f301f485f4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 30 Mar 2023 19:43:33 +0200 Subject: [PATCH 71/87] Revert for backwards compatibility of client code Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/lib/execute.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index 0c5ae2df8a..7735eb141b 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -200,7 +200,7 @@ def run_openpype_process(*args, **kwargs): args = get_openpype_execute_args(*args) env = kwargs.pop("env", None) # Keep env untouched if are passed and not empty - if env is None: + if not env: # Skip envs that can affect OpenPype process # - fill more if you find more env = clean_envs_for_openpype_process(os.environ) From 82c1dcc8f1a3c18fc51012f254d8ec7a18544d41 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 30 Mar 2023 22:32:12 +0200 Subject: [PATCH 72/87] Maya `capture.py` allow to explicitly pass `pluginObjects`. This way we can pass e.g. `gpuCacheDisplayFilter` value preset setting for a plug-in even when it might not exist as an argument. --- .../settings/defaults/project_settings/maya.json | 6 ++++-- .../schemas/schema_maya_capture.json | 11 ++++++----- openpype/vendor/python/common/capture.py | 16 ++++++++++++++-- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index e914eb29f9..4b2b794a36 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -867,7 +867,6 @@ "dynamics": false, "fluids": false, "follicles": false, - "gpuCacheDisplayFilter": false, "greasePencils": false, "grid": false, "hairSystems": true, @@ -894,7 +893,10 @@ "polymeshes": true, "strokes": false, "subdivSurfaces": false, - "textures": false + "textures": false, + "pluginObjects": { + "gpuCacheDisplayFilter": false + } }, "Camera Options": { "displayGateMask": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json index 416e530db2..3f63f08158 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json @@ -422,11 +422,6 @@ "key": "follicles", "label": "Follicles" }, - { - "type": "boolean", - "key": "gpuCacheDisplayFilter", - "label": "GPU Cache" - }, { "type": "boolean", "key": "greasePencils", @@ -561,6 +556,12 @@ "type": "boolean", "key": "textures", "label": "Texture Placements" + }, + { + "type": "dict-modifiable", + "key": "pluginObjects", + "label": "Plugin Objects", + "object_type": "boolean" } ] }, diff --git a/openpype/vendor/python/common/capture.py b/openpype/vendor/python/common/capture.py index e99ea5438d..2263640d75 100644 --- a/openpype/vendor/python/common/capture.py +++ b/openpype/vendor/python/common/capture.py @@ -732,6 +732,7 @@ def _applied_viewport_options(options, panel): """Context manager for applying `options` to `panel`""" options = dict(ViewportOptions, **(options or {})) + plugin_options = options.pop("pluginObjects", {}) # BUGFIX Maya 2020 some keys in viewport options dict may not be unicode # This is a local OpenPype edit to capture.py for issue #4730 @@ -740,11 +741,14 @@ def _applied_viewport_options(options, panel): options = { str(key): value for key, value in options.items() } + plugin_options = { + str(key): value for key, value in plugin_options.items() + } + # Backwards compatibility for `pluginObjects` flattened into `options` # separate the plugin display filter options since they need to # be set differently (see #55) - plugins = cmds.pluginDisplayFilter(query=True, listFilters=True) - plugin_options = dict() + plugins = set(cmds.pluginDisplayFilter(query=True, listFilters=True)) for plugin in plugins: if plugin in options: plugin_options[plugin] = options.pop(plugin) @@ -754,6 +758,14 @@ def _applied_viewport_options(options, panel): cmds.modelEditor(panel, edit=True, **options) except TypeError as e: logger.error("Cannot apply options {}".format(e)) + # Try to set as much as possible of the state by setting them one by + # one. This way we can also report the failing key values explicitly. + for key, value in options.items(): + try: + cmds.modelEditor(panel, edit=True, **{key: value}) + except TypeError: + logger.error("Failing to apply option '{}': {}".format(key, + value)) # plugin display filter options for plugin, state in plugin_options.items(): From b7b839e94333cbd47a6ef4b35d52d407df025af3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 31 Mar 2023 00:08:32 +0200 Subject: [PATCH 73/87] Merge remote-tracking branch 'upstream/develop' into refactor/maya_collect_review_cleanup # Conflicts: # openpype/hosts/maya/plugins/publish/collect_review.py --- .github/pr-branch-labeler.yml | 15 + .github/pr-glob-labeler.yml | 102 ++ .github/workflows/project_actions.yml | 56 +- CHANGELOG.md | 926 ++++++++++++++++++ openpype/hosts/hiero/api/lib.py | 6 +- .../plugins/publish/collect_current_file.py | 5 +- openpype/hosts/max/api/lib.py | 54 +- openpype/hosts/max/api/menu.py | 9 + .../max/plugins/publish/collect_render.py | 2 +- openpype/hosts/maya/api/lib.py | 4 +- .../hosts/maya/plugins/load/load_reference.py | 106 +- openpype/lib/execute.py | 7 + .../deadline/plugins/publish/collect_pools.py | 47 +- .../plugins/publish/submit_max_deadline.py | 72 +- .../publish/validate_deadline_pools.py | 6 +- .../plugins/publish/integrate_kitsu_note.py | 35 +- openpype/plugins/publish/extract_burnin.py | 3 + openpype/scripts/otio_burnin.py | 119 ++- .../defaults/project_settings/deadline.json | 5 +- .../defaults/project_settings/global.json | 23 + .../defaults/project_settings/kitsu.json | 5 +- .../defaults/project_settings/maya.json | 3 +- .../system_settings/applications.json | 2 +- .../schema_project_deadline.json | 17 +- .../projects_schema/schema_project_kitsu.json | 76 +- .../schemas/schema_maya_capture.json | 14 +- .../project_manager/project_manager/view.py | 4 +- openpype/version.py | 2 +- pyproject.toml | 2 +- .../assets/integrate_kitsu_note_settings.png | Bin 30524 -> 48874 bytes website/docs/module_kitsu.md | 7 +- website/docs/pype2/admin_presets_plugins.md | 2 + 32 files changed, 1634 insertions(+), 102 deletions(-) create mode 100644 .github/pr-branch-labeler.yml create mode 100644 .github/pr-glob-labeler.yml diff --git a/.github/pr-branch-labeler.yml b/.github/pr-branch-labeler.yml new file mode 100644 index 0000000000..ca82051006 --- /dev/null +++ b/.github/pr-branch-labeler.yml @@ -0,0 +1,15 @@ +# Apply label "feature" if head matches "feature/*" +'type: feature': + head: "feature/*" + +# Apply label "feature" if head matches "feature/*" +'type: enhancement': + head: "enhancement/*" + +# Apply label "bugfix" if head matches one of "bugfix/*" or "hotfix/*" +'type: bug': + head: ["bugfix/*", "hotfix/*"] + +# Apply label "release" if base matches "release/*" +'Bump Minor': + base: "release/next-minor" \ No newline at end of file diff --git a/.github/pr-glob-labeler.yml b/.github/pr-glob-labeler.yml new file mode 100644 index 0000000000..286e7768b5 --- /dev/null +++ b/.github/pr-glob-labeler.yml @@ -0,0 +1,102 @@ +# Add type: unittest label if any changes in tests folders +'type: unittest': +- '*/*tests*/**/*' + +# any changes in documentation structure +'type: documentation': +- '*/**/*website*/**/*' +- '*/**/*docs*/**/*' + +# hosts triage +'host: Nuke': +- '*/**/*nuke*' +- '*/**/*nuke*/**/*' + +'host: Photoshop': +- '*/**/*photoshop*' +- '*/**/*photoshop*/**/*' + +'host: Harmony': +- '*/**/*harmony*' +- '*/**/*harmony*/**/*' + +'host: UE': +- '*/**/*unreal*' +- '*/**/*unreal*/**/*' + +'host: Houdini': +- '*/**/*houdini*' +- '*/**/*houdini*/**/*' + +'host: Maya': +- '*/**/*maya*' +- '*/**/*maya*/**/*' + +'host: Resolve': +- '*/**/*resolve*' +- '*/**/*resolve*/**/*' + +'host: Blender': +- '*/**/*blender*' +- '*/**/*blender*/**/*' + +'host: Hiero': +- '*/**/*hiero*' +- '*/**/*hiero*/**/*' + +'host: Fusion': +- '*/**/*fusion*' +- '*/**/*fusion*/**/*' + +'host: Flame': +- '*/**/*flame*' +- '*/**/*flame*/**/*' + +'host: TrayPublisher': +- '*/**/*traypublisher*' +- '*/**/*traypublisher*/**/*' + +'host: 3dsmax': +- '*/**/*max*' +- '*/**/*max*/**/*' + +'host: TV Paint': +- '*/**/*tvpaint*' +- '*/**/*tvpaint*/**/*' + +'host: CelAction': +- '*/**/*celaction*' +- '*/**/*celaction*/**/*' + +'host: After Effects': +- '*/**/*aftereffects*' +- '*/**/*aftereffects*/**/*' + +'host: Substance Painter': +- '*/**/*substancepainter*' +- '*/**/*substancepainter*/**/*' + +# modules triage +'module: Deadline': +- '*/**/*deadline*' +- '*/**/*deadline*/**/*' + +'module: RoyalRender': +- '*/**/*royalrender*' +- '*/**/*royalrender*/**/*' + +'module: Sitesync': +- '*/**/*sync_server*' +- '*/**/*sync_server*/**/*' + +'module: Ftrack': +- '*/**/*ftrack*' +- '*/**/*ftrack*/**/*' + +'module: Shotgrid': +- '*/**/*shotgrid*' +- '*/**/*shotgrid*/**/*' + +'module: Kitsu': +- '*/**/*kitsu*' +- '*/**/*kitsu*/**/*' diff --git a/.github/workflows/project_actions.yml b/.github/workflows/project_actions.yml index ca94f3ae77..1e1a1441f7 100644 --- a/.github/workflows/project_actions.yml +++ b/.github/workflows/project_actions.yml @@ -1,8 +1,8 @@ name: project-actions on: - pull_request: - types: [review_requested] + pull_request_target: + types: [opened, synchronize, assigned, review_requested] pull_request_review: types: [submitted] @@ -10,7 +10,7 @@ jobs: pr_review_requested: name: pr_review_requested runs-on: ubuntu-latest - if: github.event_name == 'pull_request' && github.event.action == 'review_requested' + if: github.event_name == 'pull_request_review' && github.event.review.state == 'changes_requested' steps: - name: Move PR to 'Change Requested' uses: leonsteinhaeuser/project-beta-automations@v2.1.0 @@ -20,3 +20,53 @@ jobs: project_id: 11 resource_node_id: ${{ github.event.pull_request.node_id }} status_value: Change Requested + + size-label: + name: pr_size_label + runs-on: ubuntu-latest + if: | + ${{(github.event_name == 'pull_request' && github.event.action == 'assigned') + || (github.event_name == 'pull_request' && github.event.action == 'opened')}} + + steps: + - name: Add size label + uses: "pascalgn/size-label-action@v0.4.3" + env: + GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}" + IGNORED: ".gitignore\n*.md\n*.json" + with: + sizes: > + { + "0": "XS", + "100": "S", + "500": "M", + "1000": "L", + "1500": "XL", + "2500": "XXL" + } + + label_prs_branch: + name: pr_branch_label + runs-on: ubuntu-latest + if: | + ${{(github.event_name == 'pull_request' && github.event.action == 'assigned') + || (github.event_name == 'pull_request' && github.event.action == 'opened')}} + steps: + - name: Label PRs - Branch name detection + uses: ffittschen/pr-branch-labeler@v1 + with: + repo-token: ${{ secrets.YNPUT_BOT_TOKEN }} + + label_prs_globe: + name: pr_globe_label + runs-on: ubuntu-latest + if: | + ${{(github.event_name == 'pull_request' && github.event.action == 'assigned') + || (github.event_name == 'pull_request' && github.event.action == 'opened')}} + steps: + - name: Label PRs - Globe detection + uses: actions/labeler@v4.0.3 + with: + repo-token: ${{ secrets.YNPUT_BOT_TOKEN }} + configuration-path: ".github/pr-glob-labeler.yml" + sync-labels: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 145c2e2c1a..4e22b783c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,931 @@ # Changelog +## [3.15.3](https://github.com/ynput/OpenPype/tree/3.15.3) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.2...3.15.3) + +### **🆕 New features** + + +
+Blender: Extract Review #3616 + +Added Review to Blender. + +This implementation is based on #3508 but made compatible for the current implementation of OpenPype for Blender. + + +___ + +
+ + +
+Data Exchanges: Point Cloud for 3dsMax #4532 + +Publish PRT format with tyFlow in 3dsmax + +Publish PRT format with tyFlow in 3dsmax and possibly set up loader to load the format too. +- [x] creator +- [x] extractor +- [x] validator +- [x] loader + + +___ + +
+ + +
+Global: persistent staging directory for renders #4583 + +Allows configure if staging directory (`stagingDir`) should be persistent with use of profiles. + +With this feature, users can specify a transient data folder path based on presets, which can be used during the creation and publishing stages. In some cases, these DCCs automatically add a rendering path during the creation stage, which is then used in publishing.One of the key advantages of this feature is that it allows users to take advantage of faster storages for rendering, which can help improve workflow efficiency. Additionally, this feature allows users to keep their rendered data persistent, and use their own infrastructure for regular cleaning.However, it should be noted that some productions may want to use this feature without persistency. Furthermore, there may be a need for retargeting the rendering folder to faster storages, which is also not supported at the moment.It is studio responsibility to clean up obsolete folders with data.Location of the folder is configured in `project_anatomy/templates/others`. ('transient' key is expected, with 'folder' key, could be more templates)Which family/task type/subset is applicable is configured in:`project_settings/global/tools/publish/transient_dir_profiles` + + +___ + +
+ + +
+Kitsu custom comment template #4599 + +Kitsu allows to write markdown in its comment field. This can be something very powerful to deliver dynamic comments with the help the data from the instance.This feature is defaults to off so the admin have to manually set up the comment field the way they want.I have added a basic example on how the comment can look like as the comment-fields default value.To this I want to add some documentation also but that's on its way when the code itself looks good for the reviewers. + + +___ + +
+ + +
+MaxScene Family #4615 + +Introduction of the Max Scene Family + + +___ + +
+ +### **🚀 Enhancements** + + +
+Maya: Multiple values on single render attribute - OP-4131 #4631 + +When validating render attributes, this adds support for multiple values. When repairing first value in list is used. + + +___ + +
+ + +
+Maya: enable 2D Pan/Zoom for playblasts - OP-5213 #4687 + +Setting for enabling 2D Pan/Zoom on reviews. + + +___ + +
+ + +
+Copy existing or generate new Fusion profile on prelaunch #4572 + +Fusion preferences will be copied to the predefined `~/.openpype/hosts/fusion/prefs` folder (or any other folder set in system settings) on launch. + +The idea is to create a copy of existing Fusion profile, adding an OpenPype menu to the Fusion instance.By default the copy setting is turned off, so no file copying is performed. Instead the clean Fusion profile is created by Fusion in the predefined folder. The default locaion is set to `~/.openpype/hosts/fusion/prefs`, to better comply with the other os platforms. After creating the default profile, some modifications are applied: +- forced Python3 +- forced English interface +- setup Openpype specific path maps.If the `copy_prefs` checkbox is toggled, a copy of existing Fusion profile folder will be placed in the mentioned location. Then they are altered the same way as described above. The operation is run only once, on the first launch, unless the `force_sync [Resync profile on each launch]` is toggled.English interface is forced because the `FUSION16_PROFILE_DIR` environment variable is not read otherwise (seems to be a Fusion bug). + + +___ + +
+ + +
+Houdini: Create button open new publisher's "create" tab #4601 + +During a talk with @maxpareschi he mentioned that the new publisher in Houdini felt super confusing due to "Create" going to the older creator but now being completely empty and the publish button directly went to the publish tab.This resolves that by fixing the Create button to now open the new publisher but on the Create tab.Also made publish button enforce going to the "publish" tab for consistency in usage.@antirotor I think changing the Create button's callback was just missed in this commit or was there a specific reason to not change that around yet? + + +___ + +
+ + +
+Clockify: refresh and fix the integration #4607 + +Due to recent API changes, Clockify requires `user_id` to operate with the timers. I updated this part and currently it is a WIP for making it fully functional. Most functions, such as start and stop timer, and projects sync are currently working. For the rate limiting task new dependency is added: https://pypi.org/project/ratelimiter/ + + +___ + +
+ + +
+Fusion publish existing frames #4611 + +This PR adds the function to publish existing frames instead of having to re-render all of them for each new publish.I have split the render_locally plugin so the review-part is its own plugin now.I also change the saver-creator-plugin's label from Saver to Render (saver) as I intend to add a Prerender creator like in Nuke. + + +___ + +
+ + +
+Resolution settings referenced from DB record for 3dsMax #4652 + +- Add Callback for setting the resolution according to DB after the new scene is created. +- Add a new Action into openpype menu which allows the user to reset the resolution in 3dsMax + + +___ + +
+ + +
+3dsmax: render instance settings in Publish tab #4658 + +Allows user preset the pools, group and use_published settings in Render Creator in the Max Hosts.User can set the settings before or after creating instance in the new publisher + + +___ + +
+ + +
+scene length setting referenced from DB record for 3dsMax #4665 + +Setting the timeline length based on DB record in 3dsMax Hosts + + +___ + +
+ + +
+Publisher: Windows reduce command window pop-ups during Publishing #4672 + +Reduce the command line pop-ups that show on Windows during publishing. + + +___ + +
+ + +
+Publisher: Explicit save #4676 + +Publisher have explicit button to save changes, so reset can happen without saving any changes. Save still happens automatically when publishing is started or on publisher window close. But a popup is shown if context of host has changed. Important context was enhanced by workfile path (if host integration supports it) so workfile changes are captured too. In that case a dialog with confirmation is shown to user. All callbacks that may require save of context were moved to main window to be able handle dialog show at one place. Save changes now returns success so the rest of logic is skipped -> publishing won't start, when save of instances fails.Save and reset buttons have shortcuts (Ctrl + s and Ctrls + r). + + +___ + +
+ + +
+CelAction: conditional workfile parameters from settings #4677 + +Since some productions were requesting excluding some workfile parameters from publishing submission, we needed to move them to settings so those could be altered per project. + + +___ + +
+ + +
+Improve logging of used app + tool envs on application launch #4682 + +Improve logging of what apps + tool environments got loaded for an application launch. + + +___ + +
+ + +
+Fix name and docstring for Create Workdir Extra Folders prelaunch hook #4683 + +Fix class name and docstring for Create Workdir Extra Folders prelaunch hookThe class name and docstring were originally copied from another plug-in and didn't match the plug-in logic.This also fixes potentially seeing this twice in your logs. Before:After:Where it was actually running both this prelaunch hook and the actual `AddLastWorkfileToLaunchArgs` plugin. + + +___ + +
+ + +
+Application launch context: Include app group name in logger #4684 + +Clarify in logs better what app group the ApplicationLaunchContext belongs to and what application is being launched.Before:After: + + +___ + +
+ + +
+increment workfile version 3dsmax #4685 + +increment workfile version in 3dsmax as if in blender and maya hosts. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Maya: Fix getting non-active model panel. #2968 + +When capturing multiple cameras with image planes that have file sequences playing, only the active (first) camera will play through the file sequence. + + +___ + +
+ + +
+Maya: Fix broken review publishing. #4549 + +Resolves #4547 + + +___ + +
+ + +
+Maya: Avoid error on right click in Loader if `mtoa` is not loaded #4616 + +Fix an error on right clicking in the Loader when `mtoa` is not a loaded plug-in.Additionally if `mtoa` isn't loaded the loader will now load the plug-in before trying to create the arnold standin. + + +___ + +
+ + +
+Maya: Fix extract look colorspace detection #4618 + +Fix the logic which guesses the colorspace using `arnold` python library. +- Previously it'd error if `mtoa` was not available on path so it still required `mtoa` to be available. +- The guessing colorspace logic doesn't actually require `mtoa` to be loaded, but just the `arnold` python library to be available. This changes the logic so it doesn't require the `mtoa` plugin to get loaded to guess the colorspace. +- The if/else branch was likely not doing what was intended `cmds.loadPlugin("mtoa", quiet=True)` returns None if the plug-in was already loaded. So this would only ever be true if it ends up loading the `mtoa` plugin the first time. +```python +# Tested in Maya 2022.1 +print(cmds.loadPlugin("mtoa", quiet=True)) +# ['mtoa'] +print(cmds.loadPlugin("mtoa", quiet=True)) +# None +``` + + +___ + +
+ + +
+Maya: Maya Playblast Options overrides - OP-3847 #4634 + +When publishing a review in Maya, the extractor would fail due to wrong (long) panel name. + + +___ + +
+ + +
+Bugfix/op 2834 fix extract playblast #4701 + +Paragraphs contain detailed information on the changes made to the product or service, providing an in-depth description of the updates and enhancements. They can be used to explain the reasoning behind the changes, or to highlight the importance of the new features. Paragraphs can often include links to further information or support documentation. + + +___ + +
+ + +
+Bugfix/op 2834 fix extract playblast #4704 + +Paragraphs contain detailed information on the changes made to the product or service, providing an in-depth description of the updates and enhancements. They can be used to explain the reasoning behind the changes, or to highlight the importance of the new features. Paragraphs can often include links to further information or support documentation. + + +___ + +
+ + +
+Maya: bug fix for passing zoom settings if review is attached to subset #4716 + +Fix for attaching review to subset with pan/zoom option. + + +___ + +
+ + +
+Maya: tile assembly fail in draft - OP-4820 #4416 + +Tile assembly in Deadline was broken. + +Initial bug report revealed other areas of the tile assembly that needed fixing. + + +___ + +
+ + +
+Maya: Yeti Validate Rig Input - OP-3454 #4554 + +Fix Yeti Validate Rig Input + +Existing workflow was broken due to this #3297. + + +___ + +
+ + +
+Scene inventory: Fix code errors when "not found" entries are found #4594 + +Whenever a "NOT FOUND" entry is present a lot of errors happened in the Scene Inventory: +- It started spamming a lot of errors for the VersionDelegate since it had no numeric version (no version at all).Error reported on Discord: +```python +Traceback (most recent call last): + File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\utils\delegates.py", line 65, in paint + text = self.displayText( + File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\utils\delegates.py", line 33, in displayText + assert isinstance(value, numbers.Integral), ( +AssertionError: Version is not integer. "None" +``` +- Right click menu would error on NOT FOUND entries, and thus not show. With this PR it will now _disregard_ not found items for "Set version" and "Remove" but still allow actions.This PR resolves those. + + +___ + +
+ + +
+Kitsu: Sync OP with zou, make sure value-data is int or float #4596 + +Currently the data zou pulls is a string and not a value causing some bugs in the pipe where a value is expected (like `Set frame range` in Fusion). + + + +This PR makes sure each value is set with int() or float() so these bugs can't happen later on. + + + +_(A request to cgwire has also bin sent to allow force values only for some metadata columns, but currently the user can enter what ever they want in there)_ + + +___ + +
+ + +
+Max: fix the bug of removing an instance #4617 + +fix the bug of removing an instance in 3dsMax + + +___ + +
+ + +
+Global | Nuke: fixing farm publishing workflow #4623 + +After Nuke had adopted new publisher with new creators new issues were introduced. Those issues were addressed with this PR. Those are for example broken reviewable video files publishing if published via farm. Also fixed local publishing. + + +___ + +
+ + +
+Ftrack: Ftrack additional families filtering #4633 + +Ftrack family collector makes sure the subset family is also in instance families for additional families filtering. + + +___ + +
+ + +
+Ftrack: Hierarchical <> Non-Hierarchical attributes sync fix #4635 + +Sync between hierarchical and non-hierarchical attributes should be fixed and work as expected. Action should sync the values as expected and event handler should do it too and only on newly created entities. + + +___ + +
+ + +
+bugfix for 3dsmax publishing error #4637 + +fix the bug of failing publishing job in 3dsMax + + +___ + +
+ + +
+General: Use right validation for ffmpeg executable #4640 + +Use ffmpeg exec validation for ffmpeg executables instead of oiio exec validation. The validation is used as last possible source of ffmpeg from `PATH` environment variables, which is an edge case but can cause issues. + + +___ + +
+ + +
+3dsmax: opening last workfile #4644 + +Supports opening last saved workfile in 3dsmax host. + + +___ + +
+ + +
+Fixed a bug where a QThread in the splash screen could be destroyed before finishing execution #4647 + +This should fix the occasional behavior of the QThread being destroyed before even its worker returns from the `run()` function.After quiting, it should wait for the QThread object to properly close itself. + + +___ + +
+ + +
+General: Use right plugin class for Collect Comment #4653 + +Collect Comment plugin is instance plugin so should inherit from `InstancePlugin` instead of `ContextPlugin`. + + +___ + +
+ + +
+Global: add tags field to thumbnail representation #4660 + +Thumbnail representation might be missing tags field. + + +___ + +
+ + +
+Integrator: Enforce unique destination transfers, disallow overwrites in queued transfers #4662 + +Fix #4656 by enforcing unique destination transfers in the Integrator. It's now disallowed to a destination in the file transaction queue with a new source path during the publish. + + +___ + +
+ + +
+Hiero: Creator with correct workfile numeric padding input #4666 + +Creator was showing 99 in workfile input for long time, even if users set default value to 1001 in studio settings. This has been fixed now. + + +___ + +
+ + +
+Nuke: Nukenodes family instance without frame range #4669 + +No need to add frame range data into `nukenodes` (backdrop) family publishes - since those are timeless. + + +___ + +
+ + +
+TVPaint: Optional Validation plugins can be de/activated by user #4674 + +Added `OptionalPyblishPluginMixin` to TVpaint plugins that can be optional. + + +___ + +
+ + +
+Kitsu: Slightly less strict with instance data #4678 + +- Allow to take task name from context if asset doesn't have any. Fixes an issue with Photoshop's review instance not having `task` in data. +- Allow to match "review" against both `instance.data["family"]` and `instance.data["families"]` because some instances don't have the primary family in families, e.g. in Photoshop and TVPaint. +- Do not error on Integrate Kitsu Review whenever for whatever reason Integrate Kitsu Note did not created a comment but just log the message that it was unable to connect a review. + + +___ + +
+ + +
+Publisher: Fix reset shortcut sequence #4694 + +Fix bug created in https://github.com/ynput/OpenPype/pull/4676 where key sequence is checked using unsupported method. The check was changed to convert event into `QKeySequence` object which can be compared to prepared sequence. + + +___ + +
+ + +
+Refactor _capture #4702 + +Paragraphs contain detailed information on the changes made to the product or service, providing an in-depth description of the updates and enhancements. They can be used to explain the reasoning behind the changes, or to highlight the importance of the new features. Paragraphs can often include links to further information or support documentation. + + +___ + +
+ + +
+Hiero: correct container colors if UpToDate #4708 + +Colors on loaded containers are now correctly identifying real state of version. `Red` for out of date and `green` for up to date. + + +___ + +
+ +### **🔀 Refactored code** + + +
+Look Assigner: Move Look Assigner tool since it's Maya only #4604 + +Fix #4357: Move Look Assigner tool to maya since it's Maya only + + +___ + +
+ + +
+Maya: Remove unused functions from Extract Look #4671 + +Remove unused functions from Maya Extract Look plug-in + + +___ + +
+ + +
+Extract Review code refactor #3930 + +Trying to reduce complexity of Extract Review plug-in +- Re-use profile filtering from lib +- Remove "combination families" additional filtering which supposedly was from OP v2 +- Simplify 'formatting' for filling gaps +- Use `legacy_io.Session` over `os.environ` + + +___ + +
+ + +
+Maya: Replace last usages of Qt module #4610 + +Replace last usage of `Qt` module with `qtpy`. This change is needed for `PySide6` support. All changes happened in Maya loader plugins. + + +___ + +
+ + +
+Update tests and documentation for `ColormanagedPyblishPluginMixin` #4612 + +Refactor `ExtractorColormanaged` to `ColormanagedPyblishPluginMixin` in tests and documentation. + + +___ + +
+ + +
+Improve logging of used app + tool envs on application launch (minor tweak) #4686 + +Use `app.full_name` for change done in #4682 + + +___ + +
+ +### **📃 Documentation** + + +
+Docs/add architecture document #4344 + +Add `ARCHITECTURE.md` document. + +his document attemps to give a quick overview of the project to help onboarding, it's not an extensive documentation but more of a elevator pitch one-line descriptions of files/directories and what the attempt to do. + + +___ + +
+ + +
+Documentation: Tweak grammar and fix some typos #4613 + +This resolves some grammar and typos in the documentation.Also fixes the extension of some images in after effects docs which used uppercase extension even though files were lowercase extension. + + +___ + +
+ + +
+Docs: Fix some minor grammar/typos #4680 + +Typo/grammar fixes in documentation. + + +___ + +
+ +### **Merged pull requests** + + +
+Maya: Implement image file node loader #4313 + +Implements a loader for loading texture image into a `file` node in Maya. + +Similar to Maya's hypershade creation of textures on load you have the option to choose for three modes of creating: +- Texture +- Projection +- StencilThese should match what Maya generates if you create those in Maya. +- [x] Load and manage file nodes +- [x] Apply color spaces after #4195 +- [x] Support for _either_ UDIM or image sequence - currently it seems to always load sequences as UDIM automatically. +- [ ] Add support for animation sequences of UDIM textures using the `..exr` path format? + + +___ + +
+ + +
+Maya Look Assigner: Don't rely on containers for get all assets #4600 + +This resolves #4044 by not actually relying on containers in the scene but instead just rely on finding nodes with `cbId` attributes. As such, imported nodes would also be found and a shader can be assigned (similar to when using get from selection).**Please take into consideration the potential downsides below**Potential downsides would be: +- IF an already loaded look has any dagNodes, say a 3D Projection node - then that will also show up as a loaded asset where previously nodes from loaded looks were ignored. +- If any dag nodes were created locally - they would have gotten `cbId` attributes on scene save and thus the current asset would almost always show? + + +___ + +
+ + +
+Maya: Unify menu labels for "Set Frame Range" and "Set Resolution" #4605 + +Fix #4109: Unify menu labels for "Set Frame Range" and "Set Resolution"This also tweaks it in Houdini from Reset Frame Range to Set Frame Range. + + +___ + +
+ + +
+Resolve missing OPENPYPE_MONGO in deadline global job preload #4484 + +In the GlobalJobPreLoad plugin, we propose to replace the SpawnProcess by a sub-process and to pass the environment variables in the parameters, since the SpawnProcess under Centos Linux does not pass the environment variables. + +In the GlobalJobPreLoad plugin, the Deadline SpawnProcess is used to start the OpenPype process. The problem is that the SpawnProcess does not pass environment variables, including OPENPYPE_MONGO, to the process when it is under Centos7 linux, and the process gets stuck. We propose to replace it by a subprocess and to pass the variable in the parameters. + + +___ + +
+ + +
+Tests: Added setup_only to tests #4591 + +Allows to download test zip, unzip and restore DB in preparation for new test. + + +___ + +
+ + +
+Maya: Arnold don't reset maya timeline frame range on render creation (or setting render settings) #4603 + +Fix #4429: Do not reset fps or playback timeline on applying or creating render settings + + +___ + +
+ + +
+Bump @sideway/formula from 3.0.0 to 3.0.1 in /website #4609 + +Bumps [@sideway/formula](https://github.com/sideway/formula) from 3.0.0 to 3.0.1. +
+Commits + +
+
+Maintainer changes +

This version was pushed to npm by marsup, a new releaser for @​sideway/formula since your current version.

+
+
+ + +[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@sideway/formula&package-manager=npm_and_yarn&previous-version=3.0.0&new-version=3.0.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) + +Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. + +[//]: # (dependabot-automerge-start) +[//]: # (dependabot-automerge-end) + +--- + +
+Dependabot commands and options +
+ +You can trigger Dependabot actions by commenting on this PR: +- `@dependabot rebase` will rebase this PR +- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it +- `@dependabot merge` will merge this PR after your CI passes on it +- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it +- `@dependabot cancel merge` will cancel a previously requested merge and block automerging +- `@dependabot reopen` will reopen this PR if it is closed +- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually +- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) +- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) +- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) +- `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language +- `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language +- `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language +- `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language + +You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/ynput/OpenPype/network/alerts). + +
+___ + +
+ + +
+Update artist_hosts_maya_arnold.md #4626 + +Correct Arnold docs. +___ + +
+ + +
+Maya: Add "Include Parent Hierarchy" option in animation creator plugin #4645 + +Add an option in Project Settings > Maya > Creator Plugins > Create Animation to include (or not) parent hierarchy. This is to avoid artists to check manually the option for all create animation. + + +___ + +
+ + +
+General: Filter available applications #4667 + +Added option to filter applications that don't have valid executable available in settings in launcher and ftrack actions. This option can be disabled in new settings category `Applications`. The filtering is by default disabled. + + +___ + +
+ + +
+3dsmax: make sure that startup script executes #4695 + +Fixing reliability of OpenPype startup in 3dsmax. + + +___ + +
+ + +
+Project Manager: Change minimum frame start/end to '0' #4719 + +Project manager can have frame start/end set to `0`. + + +___ + +
+ + + +## [3.15.2](https://github.com/ynput/OpenPype/tree/3.15.2) [Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.1...3.15.2) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index bbd1edc14a..0d4368529f 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -1221,7 +1221,7 @@ def set_track_color(track_item, color): def check_inventory_versions(track_items=None): """ - Actual version color idetifier of Loaded containers + Actual version color identifier of Loaded containers Check all track items and filter only Loader nodes for its version. It will get all versions from database @@ -1249,10 +1249,10 @@ def check_inventory_versions(track_items=None): project_name = legacy_io.active_project() filter_result = filter_containers(containers, project_name) for container in filter_result.latest: - set_track_color(container["_item"], clip_color) + set_track_color(container["_item"], clip_color_last) for container in filter_result.outdated: - set_track_color(container["_item"], clip_color_last) + set_track_color(container["_item"], clip_color) def selection_changed_timeline(event): diff --git a/openpype/hosts/houdini/plugins/publish/collect_current_file.py b/openpype/hosts/houdini/plugins/publish/collect_current_file.py index 9cca07fdc7..caf679f98b 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_current_file.py +++ b/openpype/hosts/houdini/plugins/publish/collect_current_file.py @@ -1,7 +1,6 @@ import os import hou -from openpype.pipeline import legacy_io import pyblish.api @@ -11,7 +10,7 @@ class CollectHoudiniCurrentFile(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder - 0.01 label = "Houdini Current File" hosts = ["houdini"] - family = ["workfile"] + families = ["workfile"] def process(self, instance): """Inject the current working file""" @@ -21,7 +20,7 @@ class CollectHoudiniCurrentFile(pyblish.api.InstancePlugin): # By default, Houdini will even point a new scene to a path. # However if the file is not saved at all and does not exist, # we assume the user never set it. - filepath = "" + current_file = "" elif os.path.basename(current_file) == "untitled.hip": # Due to even a new file being called 'untitled.hip' we are unable diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 39657a2525..ac7d75db08 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -182,7 +182,6 @@ def reset_scene_resolution(): scene resolution can be overwritten by an asset if the asset.data contains any information regarding scene resolution . - Returns: None """ @@ -200,6 +199,59 @@ def reset_scene_resolution(): set_scene_resolution(width, height) +def get_frame_range() -> dict: + """Get the current assets frame range and handles. + + Returns: + dict: with frame start, frame end, handle start, handle end. + """ + # Set frame start/end + asset = get_current_project_asset() + frame_start = asset["data"].get("frameStart") + frame_end = asset["data"].get("frameEnd") + # Backwards compatibility + if frame_start is None or frame_end is None: + frame_start = asset["data"].get("edit_in") + frame_end = asset["data"].get("edit_out") + if frame_start is None or frame_end is None: + return + handles = asset["data"].get("handles") or 0 + handle_start = asset["data"].get("handleStart") + if handle_start is None: + handle_start = handles + handle_end = asset["data"].get("handleEnd") + if handle_end is None: + handle_end = handles + return { + "frameStart": frame_start, + "frameEnd": frame_end, + "handleStart": handle_start, + "handleEnd": handle_end + } + + +def reset_frame_range(fps: bool = True): + """Set frame range to current asset. + This is part of 3dsmax documentation: + + animationRange: A System Global variable which lets you get and + set an Interval value that defines the start and end frames + of the Active Time Segment. + frameRate: A System Global variable which lets you get + and set an Integer value that defines the current + scene frame rate in frames-per-second. + """ + if fps: + data_fps = get_current_project(fields=["data.fps"]) + fps_number = float(data_fps["data"]["fps"]) + rt.frameRate = fps_number + frame_range = get_frame_range() + frame_start = frame_range["frameStart"] - int(frame_range["handleStart"]) + frame_end = frame_range["frameEnd"] + int(frame_range["handleEnd"]) + frange_cmd = f"animationRange = interval {frame_start} {frame_end}" + rt.execute(frange_cmd) + + def set_context_setting(): """Apply the project settings from the project definition diff --git a/openpype/hosts/max/api/menu.py b/openpype/hosts/max/api/menu.py index 1f18972394..066cc90039 100644 --- a/openpype/hosts/max/api/menu.py +++ b/openpype/hosts/max/api/menu.py @@ -6,6 +6,7 @@ from pymxs import runtime as rt from openpype.tools.utils import host_tools from openpype.hosts.max.api import lib + class OpenPypeMenu(object): """Object representing OpenPype menu. @@ -114,6 +115,10 @@ class OpenPypeMenu(object): res_action.triggered.connect(self.resolution_callback) openpype_menu.addAction(res_action) + frame_action = QtWidgets.QAction("Set Frame Range", openpype_menu) + frame_action.triggered.connect(self.frame_range_callback) + openpype_menu.addAction(frame_action) + return openpype_menu def load_callback(self): @@ -139,3 +144,7 @@ class OpenPypeMenu(object): def resolution_callback(self): """Callback to reset scene resolution""" return lib.reset_scene_resolution() + + def frame_range_callback(self): + """Callback to reset frame range""" + return lib.reset_frame_range() diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 7c9e311c2f..63e4108c84 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -61,7 +61,7 @@ class CollectRender(pyblish.api.InstancePlugin): "plugin": "3dsmax", "frameStart": context.data['frameStart'], "frameEnd": context.data['frameEnd'], - "version": version_int + "version": version_int, } self.log.info("data: {0}".format(data)) instance.data.update(data) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index aa1e501578..1a62e7dbc3 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2478,8 +2478,8 @@ def load_capture_preset(data=None): float(value[2]) / 255 ] disp_options[key] = value - else: - disp_options['displayGradient'] = True + elif key == "displayGradient": + disp_options[key] = value options['display_options'] = disp_options diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index d93702a16d..82c15ab899 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -1,4 +1,6 @@ import os +import difflib +import contextlib from maya import cmds from openpype.settings import get_project_settings @@ -8,7 +10,82 @@ from openpype.pipeline.create import ( get_legacy_creator_by_name, ) import openpype.hosts.maya.api.plugin -from openpype.hosts.maya.api.lib import maintained_selection +from openpype.hosts.maya.api.lib import ( + maintained_selection, + get_container_members +) + + +@contextlib.contextmanager +def preserve_modelpanel_cameras(container, log=None): + """Preserve camera members of container in the modelPanels. + + This is used to ensure a camera remains in the modelPanels after updating + to a new version. + + """ + + # Get the modelPanels that used the old camera + members = get_container_members(container) + old_cameras = set(cmds.ls(members, type="camera", long=True)) + if not old_cameras: + # No need to manage anything + yield + return + + panel_cameras = {} + for panel in cmds.getPanel(type="modelPanel"): + cam = cmds.ls(cmds.modelPanel(panel, query=True, camera=True), + long=True) + + # Often but not always maya returns the transform from the + # modelPanel as opposed to the camera shape, so we convert it + # to explicitly be the camera shape + if cmds.nodeType(cam) != "camera": + cam = cmds.listRelatives(cam, + children=True, + fullPath=True, + type="camera")[0] + if cam in old_cameras: + panel_cameras[panel] = cam + + if not panel_cameras: + # No need to manage anything + yield + return + + try: + yield + finally: + new_members = get_container_members(container) + new_cameras = set(cmds.ls(new_members, type="camera", long=True)) + if not new_cameras: + return + + for panel, cam_name in panel_cameras.items(): + new_camera = None + if cam_name in new_cameras: + new_camera = cam_name + elif len(new_cameras) == 1: + new_camera = next(iter(new_cameras)) + else: + # Multiple cameras in the updated container but not an exact + # match detected by name. Find the closest match + matches = difflib.get_close_matches(word=cam_name, + possibilities=new_cameras, + n=1) + if matches: + new_camera = matches[0] # best match + if log: + log.info("Camera in '{}' restored with " + "closest match camera: {} (before: {})" + .format(panel, new_camera, cam_name)) + + if not new_camera: + # Unable to find the camera to re-apply in the modelpanel + continue + + cmds.modelPanel(panel, edit=True, camera=new_camera) class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): @@ -68,6 +145,9 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): new_nodes = (list(set(nodes) - set(shapes))) + # if there are cameras, try to lock their transforms + self._lock_camera_transforms(new_nodes) + current_namespace = pm.namespaceInfo(currentNamespace=True) if current_namespace != ":": @@ -136,6 +216,15 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): def switch(self, container, representation): self.update(container, representation) + def update(self, container, representation): + with preserve_modelpanel_cameras(container, log=self.log): + super(ReferenceLoader, self).update(container, representation) + + # We also want to lock camera transforms on any new cameras in the + # reference or for a camera which might have changed names. + members = get_container_members(container) + self._lock_camera_transforms(members) + def _post_process_rig(self, name, namespace, context, options): output = next((node for node in self if @@ -168,3 +257,18 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): options={"useSelection": True}, data={"dependencies": dependency} ) + + def _lock_camera_transforms(self, nodes): + cameras = cmds.ls(nodes, type="camera") + if not cameras: + return + + # Check the Maya version, lockTransform has been introduced since + # Maya 2016.5 Ext 2 + version = int(cmds.about(version=True)) + if version >= 2016: + for camera in cameras: + cmds.camera(camera, edit=True, lockTransform=True) + else: + self.log.warning("This version of Maya does not support locking of" + " transforms of cameras.") diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index 7a929a0ade..6f9a095285 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -8,6 +8,8 @@ import tempfile from .log import Logger from .vendor_bin_utils import find_executable +from .openpype_version import is_running_from_build + # MSDN process creation flag (Windows only) CREATE_NO_WINDOW = 0x08000000 @@ -200,6 +202,11 @@ def run_openpype_process(*args, **kwargs): # Skip envs that can affect OpenPype process # - fill more if you find more env = clean_envs_for_openpype_process(os.environ) + + # Only keep OpenPype version if we are running from build. + if not is_running_from_build(): + env.pop("OPENPYPE_VERSION", None) + return run_subprocess(args, env=env, **kwargs) diff --git a/openpype/modules/deadline/plugins/publish/collect_pools.py b/openpype/modules/deadline/plugins/publish/collect_pools.py index 48130848d5..e221eb00ea 100644 --- a/openpype/modules/deadline/plugins/publish/collect_pools.py +++ b/openpype/modules/deadline/plugins/publish/collect_pools.py @@ -3,21 +3,60 @@ """ import pyblish.api +from openpype.lib import TextDef +from openpype.pipeline.publish import OpenPypePyblishPluginMixin -class CollectDeadlinePools(pyblish.api.InstancePlugin): +class CollectDeadlinePools(pyblish.api.InstancePlugin, + OpenPypePyblishPluginMixin): """Collect pools from instance if present, from Setting otherwise.""" order = pyblish.api.CollectorOrder + 0.420 label = "Collect Deadline Pools" - families = ["rendering", "render.farm", "renderFarm", "renderlayer"] + families = ["rendering", + "render.farm", + "renderFarm", + "renderlayer", + "maxrender"] primary_pool = None secondary_pool = None + @classmethod + def apply_settings(cls, project_settings, system_settings): + # deadline.publish.CollectDeadlinePools + settings = project_settings["deadline"]["publish"]["CollectDeadlinePools"] # noqa + cls.primary_pool = settings.get("primary_pool", None) + cls.secondary_pool = settings.get("secondary_pool", None) + def process(self, instance): + + attr_values = self.get_attr_values_from_data(instance.data) if not instance.data.get("primaryPool"): - instance.data["primaryPool"] = self.primary_pool or "none" + instance.data["primaryPool"] = ( + attr_values.get("primaryPool") or self.primary_pool or "none" + ) if not instance.data.get("secondaryPool"): - instance.data["secondaryPool"] = self.secondary_pool or "none" + instance.data["secondaryPool"] = ( + attr_values.get("secondaryPool") or self.secondary_pool or "none" # noqa + ) + + @classmethod + def get_attribute_defs(cls): + # TODO: Preferably this would be an enum for the user + # but the Deadline server URL can be dynamic and + # can be set per render instance. Since get_attribute_defs + # can't be dynamic unfortunately EnumDef isn't possible (yet?) + # pool_names = self.deadline_module.get_deadline_pools(deadline_url, + # self.log) + # secondary_pool_names = ["-"] + pool_names + + return [ + TextDef("primaryPool", + label="Primary Pool", + default=cls.primary_pool), + TextDef("secondaryPool", + label="Secondary Pool", + default=cls.secondary_pool) + ] diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 417a03de74..c728b6b9c7 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -3,7 +3,15 @@ import getpass import copy import attr -from openpype.pipeline import legacy_io +from openpype.lib import ( + TextDef, + BoolDef, + NumberDef, +) +from openpype.pipeline import ( + legacy_io, + OpenPypePyblishPluginMixin +) from openpype.settings import get_project_settings from openpype.hosts.max.api.lib import ( get_current_renderer, @@ -22,7 +30,8 @@ class MaxPluginInfo(object): IgnoreInputs = attr.ib(default=True) -class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): +class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, + OpenPypePyblishPluginMixin): label = "Submit Render to Deadline" hosts = ["max"] @@ -31,14 +40,22 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): use_published = True priority = 50 - tile_priority = 50 chunk_size = 1 jobInfo = {} pluginInfo = {} group = None - deadline_pool = None - deadline_pool_secondary = None - framePerTask = 1 + + @classmethod + def apply_settings(cls, project_settings, system_settings): + settings = project_settings["deadline"]["publish"]["MaxSubmitDeadline"] # noqa + + # Take some defaults from settings + cls.use_published = settings.get("use_published", + cls.use_published) + cls.priority = settings.get("priority", + cls.priority) + cls.chuck_size = settings.get("chunk_size", cls.chunk_size) + cls.group = settings.get("group", cls.group) def get_job_info(self): job_info = DeadlineJobInfo(Plugin="3dsmax") @@ -49,11 +66,11 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): instance = self._instance context = instance.context - # Always use the original work file name for the Job name even when # rendering is done from the published Work File. The original work # file name is clearer because it can also have subversion strings, # etc. which are stripped for the published file. + src_filepath = context.data["currentFile"] src_filename = os.path.basename(src_filepath) @@ -71,13 +88,13 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): job_info.Pool = instance.data.get("primaryPool") job_info.SecondaryPool = instance.data.get("secondaryPool") - job_info.ChunkSize = instance.data.get("chunkSize", 1) - job_info.Comment = context.data.get("comment") - job_info.Priority = instance.data.get("priority", self.priority) - job_info.FramesPerTask = instance.data.get("framesPerTask", 1) - if self.group: - job_info.Group = self.group + attr_values = self.get_attr_values_from_data(instance.data) + + job_info.ChunkSize = attr_values.get("chunkSize", 1) + job_info.Comment = context.data.get("comment") + job_info.Priority = attr_values.get("priority", self.priority) + job_info.Group = attr_values.get("group", self.group) # Add options from RenderGlobals render_globals = instance.data.get("renderGlobals", {}) @@ -216,3 +233,32 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): plugin_info.update(plugin_data) return job_info, plugin_info + + @classmethod + def get_attribute_defs(cls): + defs = super(MaxSubmitDeadline, cls).get_attribute_defs() + defs.extend([ + BoolDef("use_published", + default=cls.use_published, + label="Use Published Scene"), + + NumberDef("priority", + minimum=1, + maximum=250, + decimals=0, + default=cls.priority, + label="Priority"), + + NumberDef("chunkSize", + minimum=1, + maximum=50, + decimals=0, + default=cls.chunk_size, + label="Frame Per Task"), + + TextDef("group", + default=cls.group, + label="Group Name"), + ]) + + return defs diff --git a/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py b/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py index 05afa5080d..7c8ab62d4d 100644 --- a/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py +++ b/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py @@ -17,7 +17,11 @@ class ValidateDeadlinePools(OptionalPyblishPluginMixin, label = "Validate Deadline Pools" order = pyblish.api.ValidatorOrder - families = ["rendering", "render.farm", "renderFarm", "renderlayer"] + families = ["rendering", + "render.farm", + "renderFarm", + "renderlayer", + "maxrender"] optional = True def process(self, instance): diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index 6be14b3bdf..f8e56377bb 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -14,7 +14,10 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): # status settings set_status_note = False note_status_shortname = "wfa" - status_conditions = list() + status_change_conditions = { + "status_conditions": [], + "family_requirements": [], + } # comment settings custom_comment_template = { @@ -55,7 +58,7 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): continue kitsu_task = instance.data.get("kitsu_task") - if kitsu_task is None: + if not kitsu_task: continue # Get note status, by default uses the task status for the note @@ -65,13 +68,39 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): # Check if any status condition is not met allow_status_change = True - for status_cond in self.status_conditions: + for status_cond in self.status_change_conditions[ + "status_conditions" + ]: condition = status_cond["condition"] == "equal" match = status_cond["short_name"].upper() == shortname if match and not condition or condition and not match: allow_status_change = False break + if allow_status_change: + # Get families + families = { + instance.data.get("family") + for instance in context + if instance.data.get("publish") + } + + # Check if any family requirement is met + for family_requirement in self.status_change_conditions[ + "family_requirements" + ]: + condition = family_requirement["condition"] == "equal" + + for family in families: + match = family_requirement["family"].lower() == family + if match and not condition or condition and not match: + allow_status_change = False + break + + if allow_status_change: + break + + # Set note status if self.set_status_note and allow_status_change: kitsu_status = gazu.task.get_task_status_by_short_name( self.note_status_shortname diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index 44ba4a5025..95575444b2 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -252,6 +252,9 @@ class ExtractBurnin(publish.Extractor): # Add context data burnin_data. burnin_data["custom"] = custom_data + # Add data members. + burnin_data.update(instance.data.get("burninDataMembers", {})) + # Add source camera name to burnin data camera_name = repre.get("camera_name") if camera_name: diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index ef449f4f74..d0a4266941 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -4,8 +4,10 @@ import re import subprocess import platform import json -import opentimelineio_contrib.adapters.ffmpeg_burnins as ffmpeg_burnins +import tempfile +from string import Formatter +import opentimelineio_contrib.adapters.ffmpeg_burnins as ffmpeg_burnins from openpype.lib import ( get_ffmpeg_tool_path, get_ffmpeg_codec_args, @@ -23,7 +25,7 @@ FFMPEG = ( ).format(ffmpeg_path) DRAWTEXT = ( - "drawtext=fontfile='%(font)s':text=\\'%(text)s\\':" + "drawtext@'%(label)s'=fontfile='%(font)s':text=\\'%(text)s\\':" "x=%(x)s:y=%(y)s:fontcolor=%(color)s@%(opacity).1f:fontsize=%(size)d" ) TIMECODE = ( @@ -39,6 +41,45 @@ TIMECODE_KEY = "{timecode}" SOURCE_TIMECODE_KEY = "{source_timecode}" +def convert_list_to_command(list_to_convert, fps, label=""): + """Convert a list of values to a drawtext command file for ffmpeg `sendcmd` + + The list of values is expected to have a value per frame. If the video + file ends up being longer than the amount of samples per frame than the + last value will be held. + + Args: + list_to_convert (list): List of values per frame. + fps (float or int): The expected frame per seconds of the output file. + label (str): Label for the drawtext, if specific drawtext filter is + required + + Returns: + str: Filepath to the temporary drawtext command file. + + """ + + with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: + for i, value in enumerate(list_to_convert): + seconds = i / fps + + # Escape special character + value = str(value).replace(":", "\\:") + + filter = "drawtext" + if label: + filter += "@" + label + + line = ( + "{start} {filter} reinit text='{value}';" + "\n".format(start=seconds, filter=filter, value=value) + ) + + f.write(line) + f.flush() + return f.name + + def _get_ffprobe_data(source): """Reimplemented from otio burnins to be able use full path to ffprobe :param str source: source media file @@ -144,7 +185,13 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins): self.options_init.update(options_init) def add_text( - self, text, align, frame_start=None, frame_end=None, options=None + self, + text, + align, + frame_start=None, + frame_end=None, + options=None, + cmd="" ): """ Adding static text to a filter. @@ -165,7 +212,13 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins): if frame_end is not None: options["frame_end"] = frame_end - self._add_burnin(text, align, options, DRAWTEXT) + draw_text = DRAWTEXT + if cmd: + draw_text = "{}, {}".format(cmd, DRAWTEXT) + + options["label"] = align + + self._add_burnin(text, align, options, draw_text) def add_timecode( self, align, frame_start=None, frame_end=None, frame_start_tc=None, @@ -408,11 +461,13 @@ def burnins_from_data( True by default. Presets must be set separately. Should be dict with 2 keys: - - "options" - sets look of burnins - colors, opacity,...(more info: ModifiedBurnins doc) + - "options" - sets look of burnins - colors, opacity,... + (more info: ModifiedBurnins doc) - *OPTIONAL* default values are used when not included - "burnins" - contains dictionary with burnins settings - *OPTIONAL* burnins won't be added (easier is not to use this) - - each key of "burnins" represents Alignment, there are 6 possibilities: + - each key of "burnins" represents Alignment, + there are 6 possibilities: TOP_LEFT TOP_CENTERED TOP_RIGHT BOTTOM_LEFT BOTTOM_CENTERED BOTTOM_RIGHT - value must be string with text you want to burn-in @@ -491,13 +546,14 @@ def burnins_from_data( if source_timecode is not None: data[SOURCE_TIMECODE_KEY[1:-1]] = SOURCE_TIMECODE_KEY + clean_up_paths = [] for align_text, value in burnin_values.items(): if not value: continue - if isinstance(value, (dict, list, tuple)): + if isinstance(value, dict): raise TypeError(( - "Expected string or number type." + "Expected string, number or list type." " Got: {} - \"{}\"" " (Make sure you have new burnin presets)." ).format(str(type(value)), str(value))) @@ -533,8 +589,48 @@ def burnins_from_data( print("Source does not have set timecode value.") value = value.replace(SOURCE_TIMECODE_KEY, MISSING_KEY_VALUE) - key_pattern = re.compile(r"(\{.*?[^{0]*\})") + # Convert lists. + cmd = "" + text = None + keys = [i[1] for i in Formatter().parse(value) if i[1] is not None] + list_to_convert = [] + # Warn about nested dictionary support for lists. Ei. we dont support + # it. + if "[" in "".join(keys): + print( + "We dont support converting nested dictionaries to lists," + " so skipping {}".format(value) + ) + else: + for key in keys: + data_value = data[key] + + # Multiple lists are not supported. + if isinstance(data_value, list) and list_to_convert: + raise ValueError( + "Found multiple lists to convert, which is not " + "supported: {}".format(value) + ) + + if isinstance(data_value, list): + print("Found list to convert: {}".format(data_value)) + for v in data_value: + data[key] = v + list_to_convert.append(value.format(**data)) + + if list_to_convert: + value = list_to_convert[0] + path = convert_list_to_command( + list_to_convert, data["fps"], label=align + ) + cmd = "sendcmd=f='{}'".format(path) + cmd = cmd.replace("\\", "/") + cmd = cmd.replace(":", "\\:") + clean_up_paths.append(path) + + # Failsafe for missing keys. + key_pattern = re.compile(r"(\{.*?[^{0]*\})") missing_keys = [] for group in key_pattern.findall(value): try: @@ -568,7 +664,8 @@ def burnins_from_data( continue text = value.format(**data) - burnin.add_text(text, align, frame_start, frame_end) + + burnin.add_text(text, align, frame_start, frame_end, cmd=cmd) ffmpeg_args = [] if codec_data: @@ -599,6 +696,8 @@ def burnins_from_data( burnin.render( output_path, args=ffmpeg_args_str, overwrite=overwrite, **data ) + for path in clean_up_paths: + os.remove(path) if __name__ == "__main__": diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 0cbd323299..fdd70f1a44 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -43,10 +43,7 @@ "use_published": true, "priority": 50, "chunk_size": 10, - "group": "none", - "deadline_pool": "", - "deadline_pool_secondary": "", - "framePerTask": 1 + "group": "none" }, "NukeSubmitDeadline": { "enabled": true, diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index aba840da78..30e56300d1 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -249,6 +249,29 @@ } } } + }, + { + "families": [], + "hosts": [ + "maya" + ], + "task_types": [], + "task_names": [], + "subsets": [], + "burnins": { + "maya_burnin": { + "TOP_LEFT": "{yy}-{mm}-{dd}", + "TOP_CENTERED": "{focalLength:.2f} mm", + "TOP_RIGHT": "{anatomy[version]}", + "BOTTOM_LEFT": "{username}", + "BOTTOM_CENTERED": "{asset}", + "BOTTOM_RIGHT": "{frame_start}-{current_frame}-{frame_end}", + "filter": { + "families": [], + "tags": [] + } + } + } } ] }, diff --git a/openpype/settings/defaults/project_settings/kitsu.json b/openpype/settings/defaults/project_settings/kitsu.json index 0638450595..59a36d8b97 100644 --- a/openpype/settings/defaults/project_settings/kitsu.json +++ b/openpype/settings/defaults/project_settings/kitsu.json @@ -8,7 +8,10 @@ "IntegrateKitsuNote": { "set_status_note": false, "note_status_shortname": "wfa", - "status_conditions": [], + "status_change_conditions": { + "status_conditions": [], + "family_requirements": [] + }, "custom_comment_template": { "enabled": false, "comment_template": "{comment}\n\n| | |\n|--|--|\n| version| `{version}` |\n| family | `{family}` |\n| name | `{name}` |" diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index e914eb29f9..fda053e6e6 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -795,6 +795,7 @@ "quality": 95 }, "Display Options": { + "override_display": true, "background": [ 125, 125, @@ -813,7 +814,7 @@ 125, 255 ], - "override_display": true + "displayGradient": true }, "Generic": { "isolate_view": true, diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 5fd9b926fb..eb3a88ce66 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -133,7 +133,7 @@ "linux": [] }, "arguments": { - "windows": [], + "windows": ["-U MAXScript {OPENPYPE_ROOT}\\openpype\\hosts\\max\\startup\\startup.ms"], "darwin": [], "linux": [] }, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index 9906939cc7..d8b5e4dc1f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -239,27 +239,12 @@ { "type": "number", "key": "chunk_size", - "label": "Chunk Size" + "label": "Frame per Task" }, { "type": "text", "key": "group", "label": "Group Name" - }, - { - "type": "text", - "key": "deadline_pool", - "label": "Deadline pool" - }, - { - "type": "text", - "key": "deadline_pool_secondary", - "label": "Deadline pool (secondary)" - }, - { - "type": "number", - "key": "framePerTask", - "label": "Frame Per Task" } ] }, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json index ee309f63a7..8aeed00542 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json @@ -54,30 +54,62 @@ "label": "Note shortname" }, { - "type": "list", - "key": "status_conditions", - "label": "Status conditions", - "object_type": { - "type": "dict", - "key": "conditions_dict", - "children": [ - { - "type": "enum", - "key": "condition", - "label": "Condition", - "enum_items": [ - {"equal": "Equal"}, - {"not_equal": "Not equal"} + "type": "dict", + "collapsible": true, + "key": "status_change_conditions", + "label": "Status change conditions", + "children": [ + { + "type": "list", + "key": "status_conditions", + "label": "Status conditions", + "object_type": { + "type": "dict", + "key": "condition_dict", + "children": [ + { + "type": "enum", + "key": "condition", + "label": "Condition", + "enum_items": [ + {"equal": "Equal"}, + {"not_equal": "Not equal"} + ] + }, + { + "type": "text", + "key": "short_name", + "label": "Short name" + } ] - }, - { - "type": "text", - "key": "short_name", - "label": "Short name" } - ] - }, - "label": "Status shortname" + }, + { + "type": "list", + "key": "family_requirements", + "label": "Family requirements", + "object_type": { + "type": "dict", + "key": "requirement_dict", + "children": [ + { + "type": "enum", + "key": "condition", + "label": "Condition", + "enum_items": [ + {"equal": "Equal"}, + {"not_equal": "Not equal"} + ] + }, + { + "type": "text", + "key": "family", + "label": "Family" + } + ] + } + } + ] }, { "type": "dict", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json index dec5a5cdc2..a4a986bad8 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json @@ -19,12 +19,12 @@ { "type": "text", "key": "compression", - "label": "Compression type" + "label": "Encoding" }, { "type": "text", "key": "format", - "label": "Data format" + "label": "Format" }, { "type": "number", @@ -48,7 +48,11 @@ "type": "label", "label": "Display Options" }, - + { + "type": "boolean", + "key": "override_display", + "label": "Override display options" + }, { "type": "color", "key": "background", @@ -66,8 +70,8 @@ }, { "type": "boolean", - "key": "override_display", - "label": "Override display options" + "key": "displayGradient", + "label": "Display background gradient" } ] }, diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index b35491c5b2..2cf11b702d 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -72,8 +72,8 @@ class HierarchyView(QtWidgets.QTreeView): column_delegate_defs = { "name": NameDef(), "type": TypeDef(), - "frameStart": NumberDef(1), - "frameEnd": NumberDef(1), + "frameStart": NumberDef(0), + "frameEnd": NumberDef(0), "fps": NumberDef(1, decimals=3, step=1), "resolutionWidth": NumberDef(0), "resolutionHeight": NumberDef(0), diff --git a/openpype/version.py b/openpype/version.py index bc5ea7fe7c..4d6ee5590e 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.3-nightly.4" +__version__ = "3.15.4-nightly.1" diff --git a/pyproject.toml b/pyproject.toml index 02370a4f10..42ce5aa32c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.15.2" # OpenPype +version = "3.15.3" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" diff --git a/website/docs/assets/integrate_kitsu_note_settings.png b/website/docs/assets/integrate_kitsu_note_settings.png index 127e79ab8063dae0408f90a82d6bb4b5e233ed47..9c59f0bf5cc9a748de1a8b66b248f539ee8014b5 100644 GIT binary patch literal 48874 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 diff --git a/website/docs/module_kitsu.md b/website/docs/module_kitsu.md index 23898fba2e..05cff87fcc 100644 --- a/website/docs/module_kitsu.md +++ b/website/docs/module_kitsu.md @@ -43,10 +43,11 @@ Task status can be automatically set during publish thanks to `Integrate Kitsu N `Admin -> Studio Settings -> Project Settings -> Kitsu -> Integrate Kitsu Note`. -There are three settings available: -- `Set status on note` -> turns on and off this integrator. +There are four settings available: +- `Set status on note` -> Turns on and off this integrator. - `Note shortname` -> Which status shortname should be set automatically (Case sensitive). -- `Status conditions` -> Conditions that need to be met for kitsu status to be changed. You can add as many conditions as you like. There are two fields to each conditions: `Condition` (Whether current status should be equal or not equal to the condition status) and `Short name` (Kitsu Shortname of the condition status). +- `Status change conditions - Status conditions` -> Conditions that need to be met for kitsu status to be changed. You can add as many conditions as you like. There are two fields to each conditions: `Condition` (Whether current status should be equal or not equal to the condition status) and `Short name` (Kitsu Shortname of the condition status). +- `Status change conditions - Family requirements` -> With this option you can add requirements to which families must be pushed or not in order to have the task status set by this integrator. There are two fields for each requirements: `Condition` (Same as the above) and `Family` (name of the family concerned by this requirement). For instance, adding one item set to `Not equal` and `workfile`, would mean the task status would change if a subset from another family than workfile is published (workfile can still be included), but not if you publish the workfile subset only. ![Integrate Kitsu Note project settings](assets/integrate_kitsu_note_settings.png) diff --git a/website/docs/pype2/admin_presets_plugins.md b/website/docs/pype2/admin_presets_plugins.md index e589f7d14b..44c2a28dec 100644 --- a/website/docs/pype2/admin_presets_plugins.md +++ b/website/docs/pype2/admin_presets_plugins.md @@ -293,6 +293,7 @@ If source representation has suffix **"h264"** and burnin suffix is **"client"** - It is allowed to use [Anatomy templates](admin_config#anatomy) themselves in burnins if they can be filled with available data. - Additional keys in burnins: + | Burnin key | Description | | --- | --- | | frame_start | First frame number. | @@ -303,6 +304,7 @@ If source representation has suffix **"h264"** and burnin suffix is **"client"** | resolution_height | Resolution height. | | fps | Fps of an output. | | timecode | Timecode by frame start and fps. | + | focalLength | **Only available in Maya**

Camera focal length per frame. Use syntax `{focalLength:.2f}` for decimal truncating. Eg. `35.234985` with `{focalLength:.2f}` would produce `35.23`, whereas `{focalLength:.0f}` would produce `35`. | :::warning `timecode` is specific key that can be **only at the end of content**. (`"BOTTOM_RIGHT": "TC: {timecode}"`) From 10fdeb5243eafd09badd0d7ad868ee13bc8c0d99 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 31 Mar 2023 11:23:12 +0100 Subject: [PATCH 74/87] Use json sidecar instead of querying ass file for IDs. --- .../publish/extract_arnold_scene_source.py | 27 +++++++-- .../tools/mayalookassigner/arnold_standin.py | 57 +++++-------------- .../maya/tools/mayalookassigner/commands.py | 3 +- .../hosts/maya/tools/mayalookassigner/lib.py | 2 +- 4 files changed, 37 insertions(+), 52 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py index e12268282c..2ac9d4c6a2 100644 --- a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py @@ -1,4 +1,6 @@ import os +from collections import defaultdict +import json from maya import cmds import arnold @@ -17,8 +19,7 @@ class ExtractArnoldSceneSource(publish.Extractor): def process(self, instance): staging_dir = self.staging_dir(instance) - filename = "{}.ass".format(instance.name) - file_path = os.path.join(staging_dir, filename) + file_path = os.path.join(staging_dir, "{}.ass".format(instance.name)) # Mask mask = arnold.AI_NODE_ALL @@ -69,7 +70,7 @@ class ExtractArnoldSceneSource(publish.Extractor): "mask": mask } - filenames = self._extract( + filenames, nodes_by_id = self._extract( instance.data["contentMembers"], attribute_data, kwargs ) @@ -86,6 +87,19 @@ class ExtractArnoldSceneSource(publish.Extractor): instance.data["representations"].append(representation) + json_path = os.path.join(staging_dir, "{}.json".format(instance.name)) + with open(json_path, "w") as f: + json.dump(nodes_by_id, f) + + representation = { + "name": "json", + "ext": "json", + "files": os.path.basename(json_path), + "stagingDir": staging_dir + } + + instance.data["representations"].append(representation) + self.log.info( "Extracted instance {} to: {}".format(instance.name, staging_dir) ) @@ -95,7 +109,7 @@ class ExtractArnoldSceneSource(publish.Extractor): return kwargs["filename"] = file_path.replace(".ass", "_proxy.ass") - filenames = self._extract( + filenames, _ = self._extract( instance.data["proxy"], attribute_data, kwargs ) @@ -115,6 +129,7 @@ class ExtractArnoldSceneSource(publish.Extractor): "Writing {} with:\n{}".format(kwargs["filename"], kwargs) ) filenames = [] + nodes_by_id = defaultdict(list) # Duplicating nodes so they are direct children of the world. This # makes the hierarchy of any exported ass file the same. with lib.delete_after() as delete_bin: @@ -158,7 +173,7 @@ class ExtractArnoldSceneSource(publish.Extractor): # Copy cbId to mtoa_constant. for node in duplicate_nodes: - lib.set_attribute("mtoa_constant_cbId", lib.get_id(node), node) + nodes_by_id[lib.get_id(node)].append(node.replace("|", "/")) with lib.attribute_values(attribute_data): with lib.maintained_selection(): @@ -178,4 +193,4 @@ class ExtractArnoldSceneSource(publish.Extractor): self.log.info("Exported: {}".format(filenames)) - return filenames + return filenames, nodes_by_id diff --git a/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py b/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py index 771b256614..08e7746eda 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py +++ b/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py @@ -1,5 +1,5 @@ import os -import re +import json from collections import defaultdict import logging @@ -58,52 +58,24 @@ def calculate_visibility_mask(attributes): return mask -def get_id_by_node(path): - """Get node id from Arnold Scene Source. +def get_nodes_by_id(standin): + """Get node id from aiStandIn via json sidecar. Args: - path (string): Path to Arnold Scene Source. + standin (string): aiStandIn node. Returns: (dict): Dictionary with node full name/path and id. """ - import arnold - results = defaultdict(list) + path = cmds.getAttr(standin + ".dso") + json_path = None + for f in os.listdir(os.path.dirname(path)): + if f.endswith(".json"): + json_path = os.path.join(os.path.dirname(path), f) + break - arnold.AiBegin() - - arnold.AiMsgSetConsoleFlags(arnold.AI_LOG_ALL) - - arnold.AiSceneLoad(None, path, None) - - # Iterate over all shader nodes - iter = arnold.AiUniverseGetNodeIterator(arnold.AI_NODE_SHAPE) - while not arnold.AiNodeIteratorFinished(iter): - node = arnold.AiNodeIteratorGetNext(iter) - if arnold.AiNodeIs(node, "polymesh"): - node_name = arnold.AiNodeGetName(node) - results[arnold.AiNodeGetStr(node, "cbId")].append(node_name) - - arnold.AiNodeIteratorDestroy(iter) - arnold.AiEnd() - - return results - - -def get_standin_path(node): - path = cmds.getAttr(node + ".dso") - - # Account for frame extension. - basename = os.path.basename(path) - current_frame = 1 - pattern = "(#+)" - matches = re.findall(pattern, basename) - if matches: - substring = "%{}d".format(str(len(matches[0])).zfill(2)) - path = path.replace(matches[0], substring) - path = path % current_frame - - return path + with open(json_path, "r") as f: + return json.load(f) def shading_engine_assignments(shading_engine, attribute, nodes, assignments): @@ -136,7 +108,7 @@ def shading_engine_assignments(shading_engine, attribute, nodes, assignments): def assign_look(standin, subset): log.info("Assigning {} to {}.".format(subset, standin)) - nodes_by_id = get_id_by_node(get_standin_path(standin)) + nodes_by_id = get_nodes_by_id(standin) # Group by asset id so we run over the look per asset node_ids_by_asset_id = defaultdict(set) @@ -161,8 +133,7 @@ def assign_look(standin, subset): continue relationships = lib.get_look_relationships(version["_id"]) - shader_nodes, container_nodes = lib.load_look(version["_id"]) - container_node = container_nodes[0] + shader_nodes, container_node = lib.load_look(version["_id"]) namespace = shader_nodes[0].split(":")[0] # Get only the node ids and paths related to this asset diff --git a/openpype/hosts/maya/tools/mayalookassigner/commands.py b/openpype/hosts/maya/tools/mayalookassigner/commands.py index 3e070c2efe..c5e6c973cf 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/commands.py +++ b/openpype/hosts/maya/tools/mayalookassigner/commands.py @@ -87,8 +87,7 @@ def create_asset_id_hash(nodes): id = k.split(":")[0] node_id_hash[id].append(node) elif cmds.nodeType(node) == "aiStandIn": - path = arnold_standin.get_standin_path(node) - for id, _ in arnold_standin.get_id_by_node(path).items(): + for id, _ in arnold_standin.get_nodes_by_id(node).items(): id = id.split(":")[0] node_id_hash[id].append(node) else: diff --git a/openpype/hosts/maya/tools/mayalookassigner/lib.py b/openpype/hosts/maya/tools/mayalookassigner/lib.py index 5594c53c33..fddaf6112d 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/lib.py +++ b/openpype/hosts/maya/tools/mayalookassigner/lib.py @@ -82,6 +82,6 @@ def load_look(version_id): # Reference the look file with lib.maintained_selection(): - container_node = load_container(loader, look_representation) + container_node = load_container(loader, look_representation)[0] return lib.get_container_members(container_node), container_node From 5b0a9772f0d9fb51fc109038c3bcf9d5008b3f88 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 31 Mar 2023 12:45:40 +0200 Subject: [PATCH 75/87] Update openpype/vendor/python/common/capture.py --- openpype/vendor/python/common/capture.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/vendor/python/common/capture.py b/openpype/vendor/python/common/capture.py index 2263640d75..224699f916 100644 --- a/openpype/vendor/python/common/capture.py +++ b/openpype/vendor/python/common/capture.py @@ -757,7 +757,6 @@ def _applied_viewport_options(options, panel): try: cmds.modelEditor(panel, edit=True, **options) except TypeError as e: - logger.error("Cannot apply options {}".format(e)) # Try to set as much as possible of the state by setting them one by # one. This way we can also report the failing key values explicitly. for key, value in options.items(): From 6d4e01dad5d0cedda46ba72f6aecb290b51a4e32 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 31 Mar 2023 12:55:11 +0200 Subject: [PATCH 76/87] Update openpype/hosts/maya/plugins/publish/collect_review.py Co-authored-by: Toke Jepsen --- openpype/hosts/maya/plugins/publish/collect_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 92c3aeab75..153f7315ec 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -36,7 +36,7 @@ class CollectReview(pyblish.api.InstancePlugin): # validate required settings if len(cameras) == 0: - raise KnownPublishError("Not camera found in review " + raise KnownPublishError("No camera found in review " "instance: {}".format(instance)) elif len(cameras) > 2: raise KnownPublishError( From 26d1c8df556facf9c03b5bc5eef9b5fb3f6a05b8 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Fri, 31 Mar 2023 17:25:19 +0100 Subject: [PATCH 77/87] Update openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py Co-authored-by: Roy Nieterau --- .../hosts/maya/plugins/publish/extract_arnold_scene_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py index 2ac9d4c6a2..83295683db 100644 --- a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py @@ -142,7 +142,7 @@ class ExtractArnoldSceneSource(publish.Extractor): # Only interested in transforms with shapes. shapes = cmds.listRelatives( node, shapes=True, noIntermediate=True - ) or [] + ) if not shapes: continue From 90113168cfc129953ab3924d0bd91fbc7d646130 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 31 Mar 2023 17:53:32 +0100 Subject: [PATCH 78/87] Doc string for shading_engine_assignments --- .../hosts/maya/tools/mayalookassigner/arnold_standin.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py b/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py index 08e7746eda..64fab2fa27 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py +++ b/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py @@ -79,6 +79,14 @@ def get_nodes_by_id(standin): def shading_engine_assignments(shading_engine, attribute, nodes, assignments): + """Full assignments with shader or disp_map. + + Args: + shading_engine (string): Shading engine for material. + attribute (string): "surfaceShader" or "displacementShader" + nodes: (list): Nodes paths relative to aiStandIn. + assignments (dict): Assignments by nodes. + """ shader_inputs = cmds.listConnections( shading_engine + "." + attribute, source=True ) From 89f9003c91dd324c58857b124bd2237ef956203f Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Fri, 31 Mar 2023 19:31:54 +0100 Subject: [PATCH 79/87] Update openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py --- .../hosts/maya/plugins/publish/extract_arnold_scene_source.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py index 83295683db..7f063f1558 100644 --- a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py @@ -173,6 +173,7 @@ class ExtractArnoldSceneSource(publish.Extractor): # Copy cbId to mtoa_constant. for node in duplicate_nodes: + # Converting Maya hierarchy separator "|" to Arnold separator "/". nodes_by_id[lib.get_id(node)].append(node.replace("|", "/")) with lib.attribute_values(attribute_data): From e244778b3a399645b60f82f20ac48010467c6309 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 31 Mar 2023 19:41:22 +0100 Subject: [PATCH 80/87] Log --- openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py b/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py index 64fab2fa27..7eeeb72553 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py +++ b/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py @@ -74,6 +74,10 @@ def get_nodes_by_id(standin): json_path = os.path.join(os.path.dirname(path), f) break + if not json_path: + log.warning("Could not find json file for {}.".format(standin)) + return {} + with open(json_path, "r") as f: return json.load(f) From b17f5218fe7c02885e8a163fe42ed7c2c031f815 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 31 Mar 2023 19:42:50 +0100 Subject: [PATCH 81/87] Hound --- .../hosts/maya/plugins/publish/extract_arnold_scene_source.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py index 7f063f1558..14bcc71da6 100644 --- a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py @@ -173,7 +173,8 @@ class ExtractArnoldSceneSource(publish.Extractor): # Copy cbId to mtoa_constant. for node in duplicate_nodes: - # Converting Maya hierarchy separator "|" to Arnold separator "/". + # Converting Maya hierarchy separator "|" to Arnold + # separator "/". nodes_by_id[lib.get_id(node)].append(node.replace("|", "/")) with lib.attribute_values(attribute_data): From 007b2a6853707e6659fd9ce4e2685a34bec1e1e4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 3 Apr 2023 12:04:34 +0200 Subject: [PATCH 82/87] Documentation: Add Extract Burnin documentation (#4765) * Add Extract Burnin documentation * Add links / fix links * Simplify documentation * Remove mention of houdini --- .../settings_project_global.md | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 821585ae21..2de9038f3f 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -194,6 +194,74 @@ A profile may generate multiple outputs from a single input. Each output must de - Nuke extractor settings path: `project_settings/nuke/publish/ExtractReviewDataMov/outputs/baking/add_custom_tags` - Filtering by input length. Input may be video, sequence or single image. It is possible that `.mp4` should be created only when input is video or sequence and to create review `.png` when input is single frame. In some cases the output should be created even if it's single frame or multi frame input. + +### Extract Burnin + +Plugin is responsible for adding burnins into review representations. + +Burnins are text values painted on top of input and may be surrounded with box in 6 available positions `Top Left`, `Top Center`, `Top Right`, `Bottom Left`, `Bottom Center`, `Bottom Right`. + +![presets_plugins_extract_burnin](../assets/presets_plugins_extract_burnin_01.png) + +The Extract Burnin plugin creates new representations based on plugin presets, representations in instance and whether the reviewable matches the profile filter. +A burnin can also be directly linked by name in the output definitions of the [Extract Review plug-in settings](#extract-review) so _can_ be triggered without a matching profile. + +#### Burnin formatting options (`options`) + +The formatting options define the font style for the burnin texts. +The X and Y offset define the margin around texts and (background) boxes. + +#### Burnin profiles (`profiles`) + +Plugin process is skipped if `profiles` are not set at all. Profiles contain list of profile items. Each burnin profile may specify filters for **hosts**, **tasks** and **families**. Filters work the same way as described in [Profile Filters](#profile-filters). + +#### Profile burnins + +A burnin profile may set multiple burnin outputs from one input. The burnin's name represents the unique **filename suffix** to avoid overriding files with same name. + +| Key | Description | Type | Example | +| --- | --- | --- | --- | +| **Top Left** | Top left corner content. | str | "{dd}.{mm}.{yyyy}" | +| **Top Centered** | Top center content. | str | "v{version:0>3}" | +| **Top Right** | Top right corner content. | str | "Static text" | +| **Bottom Left** | Bottom left corner content. | str | "{asset}" | +| **Bottom Centered** | Bottom center content. | str | "{username}" | +| **Bottom Right** | Bottom right corner content. | str | "{frame_start}-{current_frame}-{frame_end}" | + +Each burnin profile can be configured with additional family filtering and can +add additional tags to the burnin representation, these can be configured under +the profile's **Additional filtering** section. + +:::note Filename suffix +The filename suffix is appended to filename of the source representation. For +example, if the source representation has suffix **"h264"** and the burnin +suffix is **"client"** then the final suffix is **"h264_client"**. +::: + +**Available keys in burnin content** + +- It is possible to use same keys as in [Anatomy](admin_settings_project_anatomy.md#available-template-keys). +- It is allowed to use Anatomy templates themselves in burnins if they can be filled with available data. + +- Additional keys in burnins: + + | Burnin key | Description | + | --- | --- | + | frame_start | First frame number. | + | frame_end | Last frame number. | + | current_frame | Frame number for each frame. | + | duration | Count number of frames. | + | resolution_width | Resolution width. | + | resolution_height | Resolution height. | + | fps | Fps of an output. | + | timecode | Timecode by frame start and fps. | + | focalLength | **Only available in Maya**

Camera focal length per frame. Use syntax `{focalLength:.2f}` for decimal truncating. Eg. `35.234985` with `{focalLength:.2f}` would produce `35.23`, whereas `{focalLength:.0f}` would produce `35`. | + +:::warning +`timecode` is a specific key that can be **only at the end of content**. (`"BOTTOM_RIGHT": "TC: {timecode}"`) +::: + + ### IntegrateAssetNew Saves information for all published subsets into DB, published assets are available for other hosts, tools and tasks after. From 34528b6eeb26c6d463b3e9a7b0afbd3937866d99 Mon Sep 17 00:00:00 2001 From: Thomas Fricard <51854004+friquette@users.noreply.github.com> Date: Mon, 3 Apr 2023 12:11:53 +0200 Subject: [PATCH 83/87] Deactivate closed Kitsu projects on OP (#4619) * update openpype projects status from kitsu settings * fix linting errors * get all projects and verify the project status for each project * optimizing project status name verification * sync only if project exists and is active in OP * remove noqa Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * break from all_status for loop and add comment for non sync projects --------- Co-authored-by: jeremy.oblet Co-authored-by: Thomas Fricard Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../modules/kitsu/utils/update_op_with_zou.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 1f38648dfa..4f4f0810bc 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -329,6 +329,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: "code": project_code, "fps": float(project["fps"]), "zou_id": project["id"], + "active": project['project_status_name'] != "Closed", } ) @@ -379,7 +380,7 @@ def sync_all_projects( # Iterate projects dbcon = AvalonMongoDB() dbcon.install() - all_projects = gazu.project.all_open_projects() + all_projects = gazu.project.all_projects() for project in all_projects: if ignore_projects and project["name"] in ignore_projects: continue @@ -404,7 +405,21 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): if not project: project = gazu.project.get_project_by_name(project["name"]) - log.info("Synchronizing {}...".format(project["name"])) + # Get all statuses for projects from Kitsu + all_status = gazu.project.all_project_status() + for status in all_status: + if project['project_status_id'] == status['id']: + project['project_status_name'] = status['name'] + break + + # Do not sync closed kitsu project that is not found in openpype + if ( + project['project_status_name'] == "Closed" + and not get_project(project['name']) + ): + return + + log.info(f"Synchronizing {project['name']}...") # Get all assets from zou all_assets = gazu.asset.all_assets_for_project(project) @@ -429,6 +444,9 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): log.info("Project created: {}".format(project_name)) bulk_writes.append(write_project_to_op(project, dbcon)) + if project['project_status_name'] == "Closed": + return + # Try to find project document if not project_dict: project_dict = get_project(project_name) From e218fb4b6d0d24581c35d5a935058bfbecc918b9 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Mon, 3 Apr 2023 12:58:28 +0100 Subject: [PATCH 84/87] Update openpype/hosts/maya/plugins/publish/extract_gpu_cache.py Co-authored-by: Roy Nieterau --- openpype/hosts/maya/plugins/publish/extract_gpu_cache.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py b/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py index deee456982..422f5ad019 100644 --- a/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py +++ b/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py @@ -20,6 +20,8 @@ class ExtractGPUCache(publish.Extractor): useBaseTessellation = True def process(self, instance): + cmds.loadPlugin("gpuCache", quiet=True) + staging_dir = self.staging_dir(instance) filename = "{}_gpu_cache".format(instance.name) From 12084d1f9ed9aaf91f66ae9cd856c02c843729e1 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Mon, 3 Apr 2023 14:09:51 +0200 Subject: [PATCH 85/87] Maya: Validate Render Settings. (#4735) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixes error message when using attribute validation * BigRoy feedback * Failsafe for empty values. --------- Co-authored-by: Jakub Ježek --- .../publish/validate_rendersettings.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py index 53f340cd2c..ebf7b3138d 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py +++ b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py @@ -275,15 +275,6 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): # go through definitions and test if such node.attribute exists. # if so, compare its value from the one required. for attribute, data in cls.get_nodes(instance, renderer).items(): - # Validate the settings has values. - if not data["values"]: - cls.log.error( - "Settings for {}.{} is missing values.".format( - node, attribute - ) - ) - continue - for node in data["nodes"]: try: render_value = cmds.getAttr( @@ -316,6 +307,15 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): ) result = {} for attr, values in OrderedDict(validation_settings).items(): + values = [convert_to_int_or_float(v) for v in values if v] + + # Validate the settings has values. + if not values: + cls.log.error( + "Settings for {} is missing values.".format(attr) + ) + continue + cls.log.debug("{}: {}".format(attr, values)) if "." not in attr: cls.log.warning( @@ -324,8 +324,6 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): ) continue - values = [convert_to_int_or_float(v) for v in values] - node_type, attribute_name = attr.split(".", 1) # first get node of that type From 4bdf5b79b8ceb0bdaacaed97786163620e6e710c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 3 Apr 2023 17:44:21 +0200 Subject: [PATCH 86/87] Improve speed of Collect Custom Staging Directory --- .../plugins/publish/collect_custom_staging_dir.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/openpype/plugins/publish/collect_custom_staging_dir.py b/openpype/plugins/publish/collect_custom_staging_dir.py index 72ab0fe34d..b749b251c0 100644 --- a/openpype/plugins/publish/collect_custom_staging_dir.py +++ b/openpype/plugins/publish/collect_custom_staging_dir.py @@ -42,16 +42,17 @@ class CollectCustomStagingDir(pyblish.api.InstancePlugin): subset_name = instance.data["subset"] host_name = instance.context.data["hostName"] project_name = instance.context.data["projectName"] - + project_settings = instance.context.data["project_settings"] anatomy = instance.context.data["anatomy"] - anatomy_data = copy.deepcopy(instance.data["anatomyData"]) - task = anatomy_data.get("task", {}) + task = instance.data["anatomyData"].get("task", {}) transient_tml, is_persistent = get_custom_staging_dir_info( project_name, host_name, family, task.get("name"), - task.get("type"), subset_name, anatomy=anatomy, log=self.log) - result_str = "Not adding" + task.get("type"), subset_name, project_settings=project_settings, + anatomy=anatomy, log=self.log) + if transient_tml: + anatomy_data = copy.deepcopy(instance.data["anatomyData"]) anatomy_data["root"] = anatomy.roots scene_name = instance.context.data.get("currentFile") if scene_name: @@ -61,6 +62,8 @@ class CollectCustomStagingDir(pyblish.api.InstancePlugin): instance.data["stagingDir_persistent"] = is_persistent result_str = "Adding '{}' as".format(transient_dir) + else: + result_str = "Not adding" self.log.info("{} custom staging dir for instance with '{}'".format( result_str, family From 177cce233cb88bb7a51a5195204e7a57f3d2e2f8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 3 Apr 2023 18:01:33 +0200 Subject: [PATCH 87/87] Workfiles: keep Browse always enabled (#4766) * OP-5494 - keep Browse always enable din Workfiles Browse might make sense even if there are no workfiles present, better in that case it makes the most sense (eg. I want to locate workfile from outside). * use last existing directory of workfile root instead of goint to cwd * use lstring instead of index cut --------- Co-authored-by: Jakub Trllo --- openpype/tools/workfiles/files_widget.py | 22 ++++++++++++++++------ openpype/tools/workfiles/save_as_dialog.py | 6 +++--- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/openpype/tools/workfiles/files_widget.py b/openpype/tools/workfiles/files_widget.py index 18be746d49..2f338cf516 100644 --- a/openpype/tools/workfiles/files_widget.py +++ b/openpype/tools/workfiles/files_widget.py @@ -379,7 +379,7 @@ class FilesWidget(QtWidgets.QWidget): # Disable/Enable buttons based on available files in model has_valid_items = self._workarea_files_model.has_valid_items() - self._btn_browse.setEnabled(has_valid_items) + self._btn_browse.setEnabled(True) self._btn_open.setEnabled(has_valid_items) if self._publish_context_select_mode: @@ -617,14 +617,24 @@ class FilesWidget(QtWidgets.QWidget): ext_filter = "Work File (*{0})".format( " *".join(self._get_host_extensions()) ) + dir_key = "directory" + if qtpy.API in ("pyside", "pyside2", "pyside6"): + dir_key = "dir" + + workfile_root = self._workfiles_root + # Find existing directory of workfile root + # - Qt will use 'cwd' instead, if path does not exist, which may lead + # to igniter directory + while workfile_root: + if os.path.exists(workfile_root): + break + workfile_root = os.path.dirname(workfile_root) + kwargs = { "caption": "Work Files", - "filter": ext_filter + "filter": ext_filter, + dir_key: workfile_root } - if qtpy.API in ("pyside", "pyside2", "pyside6"): - kwargs["dir"] = self._workfiles_root - else: - kwargs["directory"] = self._workfiles_root work_file = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0] if work_file: diff --git a/openpype/tools/workfiles/save_as_dialog.py b/openpype/tools/workfiles/save_as_dialog.py index de21deee42..aa881e7946 100644 --- a/openpype/tools/workfiles/save_as_dialog.py +++ b/openpype/tools/workfiles/save_as_dialog.py @@ -51,7 +51,7 @@ class CommentMatcher(object): # Create a regex group for extensions extensions = registered_host().file_extensions() any_extension = "(?:{})".format( - "|".join(re.escape(ext[1:]) for ext in extensions) + "|".join(re.escape(ext.lstrip(".")) for ext in extensions) ) # Use placeholders that will never be in the filename @@ -373,7 +373,7 @@ class SaveAsDialog(QtWidgets.QDialog): if not data["comment"]: data.pop("comment", None) - data["ext"] = data["ext"][1:] + data["ext"] = data["ext"].lstrip(".") anatomy_filled = self.anatomy.format(data) return anatomy_filled[self.template_key]["file"] @@ -413,7 +413,7 @@ class SaveAsDialog(QtWidgets.QDialog): if not data["comment"]: data.pop("comment", None) - data["ext"] = data["ext"][1:] + data["ext"] = data["ext"].lstrip(".") version = get_last_workfile_with_version( self.root, template, data, extensions