Skip to content

validate_abc_primitive_to_detail

ValidateAbcPrimitiveToDetail

Bases: HoudiniInstancePlugin

Validate Alembic ROP Primitive to Detail attribute is consistent.

The Alembic ROP crashes Houdini whenever an attribute in the "Primitive to Detail" parameter exists on only a part of the primitives that belong to the same hierarchy path. Whenever it encounters inconsistent values, specifically where some are empty as opposed to others then Houdini crashes. (Tested in Houdini 17.5.229)

Source code in client/ayon_houdini/plugins/publish/validate_abc_primitive_to_detail.py
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
class ValidateAbcPrimitiveToDetail(plugin.HoudiniInstancePlugin):
    """Validate Alembic ROP Primitive to Detail attribute is consistent.

    The Alembic ROP crashes Houdini whenever an attribute in the "Primitive to
    Detail" parameter exists on only a part of the primitives that belong to
    the same hierarchy path. Whenever it encounters inconsistent values,
    specifically where some are empty as opposed to others then Houdini
    crashes. (Tested in Houdini 17.5.229)

    """

    order = pyblish.api.ValidatorOrder + 0.1
    families = ["abc"]
    label = "Validate Primitive to Detail (Abc)"

    def process(self, instance):
        invalid = self.get_invalid(instance)
        if invalid:
            raise PublishValidationError(
                "Primitives found with inconsistent primitive "
                "to detail attributes.",
                detail=(
                    "See log for more info."
                    f"Incorrect Rop(s)\n\n - {invalid[0].path()}"
                )
            )

    @classmethod
    def get_invalid(cls, instance):
        import hou  # noqa
        output_node = instance.data.get("output_node")
        rop_node = hou.node(instance.data["instance_node"])
        if output_node is None:
            cls.log.error(
                "SOP Output node in '%s' does not exist. "
                "Ensure a valid SOP output path is set." % rop_node.path()
            )

            return [rop_node]

        pattern = rop_node.parm("prim_to_detail_pattern").eval().strip()
        if not pattern:
            cls.log.debug(
                "Alembic ROP has no 'Primitive to Detail' pattern. "
                "Validation is ignored.."
            )
            return

        build_from_path = rop_node.parm("build_from_path").eval()
        if not build_from_path:
            cls.log.debug(
                "Alembic ROP has 'Build from Path' disabled. "
                "Validation is ignored.."
            )
            return

        path_attr = rop_node.parm("path_attrib").eval()
        if not path_attr:
            cls.log.error(
                "The Alembic ROP node has no Path Attribute"
                "value set, but 'Build Hierarchy from Attribute'"
                "is enabled."
            )
            return [rop_node]

        # Let's assume each attribute is explicitly named for now and has no
        # wildcards for Primitive to Detail. This simplifies the check.
        cls.log.debug("Checking Primitive to Detail pattern: %s" % pattern)
        cls.log.debug("Checking with path attribute: %s" % path_attr)

        if not hasattr(output_node, "geometry"):
            # In the case someone has explicitly set an Object
            # node instead of a SOP node in Geometry context
            # then for now we ignore - this allows us to also
            # export object transforms.
            cls.log.warning("No geometry output node found, skipping check..")
            return

        # Check if the primitive attribute exists
        frame = instance.data.get("frameStart", 0)
        geo = output_node.geometryAtFrame(frame)

        # If there are no primitives on the start frame then it might be
        # something that is emitted over time. As such we can't actually
        # validate whether the attributes exist, because they won't exist
        # yet. In that case, just warn the user and allow it.
        if len(geo.iterPrims()) == 0:
            cls.log.warning(
                "No primitives found on current frame. Validation"
                " for Primitive to Detail will be skipped."
            )
            return

        attrib = geo.findPrimAttrib(path_attr)
        if not attrib:
            cls.log.info(
                "Geometry Primitives are missing "
                "path attribute: `%s`" % path_attr
            )
            return [output_node]

        # Ensure at least a single string value is present
        if not attrib.strings():
            cls.log.info(
                "Primitive path attribute has no "
                "string values: %s" % path_attr
            )
            return [output_node]

        paths = None
        for attr in pattern.split(" "):
            if not attr.strip():
                # Ignore empty values
                continue

            # Check if the primitive attribute exists
            attrib = geo.findPrimAttrib(attr)
            if not attrib:
                # It is allowed to not have the attribute at all
                continue

            # The issue can only happen if at least one string attribute is
            # present. So we ignore cases with no values whatsoever.
            if not attrib.strings():
                continue

            check = defaultdict(set)
            values = geo.primStringAttribValues(attr)
            if paths is None:
                paths = geo.primStringAttribValues(path_attr)

            for path, value in zip(paths, values):
                check[path].add(value)

            for path, values in check.items():
                # Whenever a single path has multiple values for the
                # Primitive to Detail attribute then we consider it
                # inconsistent and invalidate the ROP node's content.
                if len(values) > 1:
                    cls.log.warning(
                        "Path has multiple values: %s (path: %s)"
                        % (list(values), path)
                    )
                    return [output_node]