From b3044398fc9181db2d2230f9f0f5cc1de7e9d297 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Apr 2023 23:43:47 +0200 Subject: [PATCH] Improve validation report + allow to select the invalid node --- openpype/hosts/houdini/api/action.py | 46 +++++++ .../publish/help/validate_vdb_output_node.xml | 25 ++-- .../publish/validate_vdb_output_node.py | 112 ++++++++++++------ 3 files changed, 135 insertions(+), 48 deletions(-) create mode 100644 openpype/hosts/houdini/api/action.py diff --git a/openpype/hosts/houdini/api/action.py b/openpype/hosts/houdini/api/action.py new file mode 100644 index 0000000000..27e8ce55bb --- /dev/null +++ b/openpype/hosts/houdini/api/action.py @@ -0,0 +1,46 @@ +import pyblish.api +import hou + +from openpype.pipeline.publish import get_errored_instances_from_context + + +class SelectInvalidAction(pyblish.api.Action): + """Select invalid nodes in Maya when plug-in failed. + + To retrieve the invalid nodes this assumes a static `get_invalid()` + method is available on the plugin. + + """ + label = "Select invalid" + on = "failed" # This action is only available on a failed plug-in + icon = "search" # Icon from Awesome Icon + + def process(self, context, plugin): + + errored_instances = get_errored_instances_from_context(context) + + # Apply pyblish.logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(errored_instances, plugin) + + # Get the invalid nodes for the plug-ins + self.log.info("Finding invalid nodes..") + invalid = list() + for instance in instances: + invalid_nodes = plugin.get_invalid(instance) + if invalid_nodes: + if isinstance(invalid_nodes, (list, tuple)): + invalid.extend(invalid_nodes) + else: + self.log.warning("Plug-in returned to be invalid, " + "but has no selectable nodes.") + + hou.clearAllSelected() + if invalid: + self.log.info("Selecting invalid nodes: {}".format( + ", ".join(node.path() for node in invalid) + )) + for node in invalid: + node.setSelected(True) + node.setCurrent(True) + else: + self.log.info("No invalid nodes found.") diff --git a/openpype/hosts/houdini/plugins/publish/help/validate_vdb_output_node.xml b/openpype/hosts/houdini/plugins/publish/help/validate_vdb_output_node.xml index 0f92560bf7..eb83bfffe3 100644 --- a/openpype/hosts/houdini/plugins/publish/help/validate_vdb_output_node.xml +++ b/openpype/hosts/houdini/plugins/publish/help/validate_vdb_output_node.xml @@ -1,21 +1,28 @@ -Scene setting +Invalid VDB -## Invalid input node +## Invalid VDB output + +All primitives of the output geometry must be VDBs, no other primitive +types are allowed. That means that regardless of the amount of VDBs in the +geometry it will have an equal amount of VDBs, points, primitives and +vertices since each VDB primitive is one point, one vertex and one VDB. + +This validation only checks the geometry on the first frame of the export +frame range. + -VDB input must have the same number of VDBs, points, primitives and vertices as output. -### __Detailed Info__ (optional) +### Detailed Info + +ROP node `{rop_path}` is set to export SOP path `{sop_path}`. + +{message} -A VDB is an inherited type of Prim, holds the following data: - - Primitives: 1 - - Points: 1 - - Vertices: 1 - - VDBs: 1 \ No newline at end of file diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py index b2b5c63799..3fa75e5822 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- import pyblish.api import hou + from openpype.pipeline import PublishXmlValidationError +from openpype.hosts.houdini.api.action import SelectInvalidAction def group_consecutive_numbers(nums): @@ -40,8 +42,13 @@ def group_consecutive_numbers(nums): class ValidateVDBOutputNode(pyblish.api.InstancePlugin): """Validate that the node connected to the output node is of type VDB. - Regardless of the amount of VDBs create the output will need to have an - equal amount of VDBs, points, primitives and vertices + All primitives of the output geometry must be VDBs, no other primitive + types are allowed. That means that regardless of the amount of VDBs in the + geometry it will have an equal amount of VDBs, points, primitives and + vertices since each VDB primitive is one point, one vertex and one VDB. + + This validation only checks the geometry on the first frame of the export + frame range for optimization purposes. A VDB is an inherited type of Prim, holds the following data: - Primitives: 1 @@ -55,64 +62,91 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin): families = ["vdbcache"] hosts = ["houdini"] label = "Validate Output Node (VDB)" + actions = [SelectInvalidAction] def process(self, instance): - invalid = self.get_invalid(instance) - if invalid: + invalid_nodes, message = self.get_invalid_with_message(instance) + if invalid_nodes: raise PublishXmlValidationError( self, - "Node connected to the output node is not of type VDB." + "Node connected to the output node is not of type VDB.", + formatting_data={ + "message": message, + "rop_path": instance.data.get("instance_node"), + "sop_path": instance.data.get("output_node") + } ) @classmethod - def get_invalid(cls, instance): + def get_invalid_with_message(cls, instance): node = instance.data.get("output_node") if node is None: - cls.log.error( + instance_node = instance.data.get("instance_node") + error = ( "SOP path is not correctly set on " - "ROP node '%s'." % instance.data.get("instance_node") + "ROP node `%s`." % instance_node ) - return [instance] + return [instance_node, error] frame = instance.data.get("frameStart", 0) + node.cook(force=True, frame_range=(frame, frame)) geometry = node.geometryAtFrame(frame) if geometry is None: # No geometry data on this node, maybe the node hasn't cooked? - cls.log.error( + error = ( "SOP node has no geometry data. " "Is it cooked? %s" % node.path() ) - return [node] + return [node, error] - prims = geometry.prims() - nr_of_prims = len(prims) - - # All primitives must be hou.VDB - invalid_prims = [] - for prim in prims: - if not isinstance(prim, hou.VDB): - invalid_prims.append(prim) - if invalid_prims: - # Log prim numbers as consecutive ranges so logging isn't very - # slow for large number of primitives - cls.log.error( - "Found non-VDB primitives for '{}', " - "primitive indices: {}".format( - node.path(), - ", ".join(group_consecutive_numbers( - prim.number() for prim in invalid_prims - )) - ) + num_prims = geometry.intrinsicValue("primitivecount") + num_points = geometry.intrinsicValue("pointcount") + if num_prims == 0 and num_points == 0: + # Since we are only checking the first frame it doesn't mean there + # won't be VDB prims in a few frames. As such we'll assume for now + # the user knows what he or she is doing + cls.log.warning( + "SOP node `{}` has no primitives on start frame {}. " + "Validation is skipped and it is assumed elsewhere in the " + "frame range VDB prims and only VDB prims will exist." + "".format(node.path(), int(frame)) ) - return [instance] + return [None, None] - nr_of_points = len(geometry.points()) - if nr_of_points != nr_of_prims: - cls.log.error("The number of primitives and points do not match") - return [instance] + num_vdb_prims = geometry.countPrimType(hou.primType.VDB) + cls.log.debug("Detected {} VDB primitives".format(num_vdb_prims)) + if num_prims != num_vdb_prims: + # There's at least one primitive that is not a VDB. + # Search them and report them to the artist. + prims = geometry.prims() + invalid_prims = [prim for prim in prims + if not isinstance(prim, hou.VDB)] + if invalid_prims: + # Log prim numbers as consecutive ranges so logging isn't very + # slow for large number of primitives + error = ( + "Found non-VDB primitives for `{}`. " + "Primitive indices {} are not VDB primitives.".format( + node.path(), + ", ".join(group_consecutive_numbers( + prim.number() for prim in invalid_prims + )) + ) + ) + return [node, error] - for prim in prims: - if prim.numVertices() != 1: - cls.log.error("Found primitive with more than 1 vertex!") - return [instance] + if num_points != num_vdb_prims: + # We have points unrelated to the VDB primitives. + error = ( + "The number of primitives and points do not match in '{}'. " + "This likely means you have unconnected points, which we do " + "not allow in the VDB output.".format(node.path())) + return [node, error] + + return [None, None] + + @classmethod + def get_invalid(cls, instance): + nodes, _ = cls.get_invalid_with_message(instance) + return nodes