Skip to content

validate_vdb_output_node

ValidateVDBOutputNode

Bases: HoudiniInstancePlugin

Validate that the node connected to the output node is of type VDB.

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 - Points: 1 - Vertices: 1 - VDBs: 1

Source code in client/ayon_houdini/plugins/publish/validate_vdb_output_node.py
 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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
class ValidateVDBOutputNode(plugin.HoudiniInstancePlugin):
    """Validate that the node connected to the output node is of type VDB.

    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
        - Points: 1
        - Vertices: 1
        - VDBs: 1

    """

    order = pyblish.api.ValidatorOrder + 0.1
    families = ["vdbcache"]
    label = "Validate Output Node (VDB)"
    actions = [SelectInvalidAction]

    def process(self, instance):
        invalid_nodes, message = self.get_invalid_with_message(instance)
        if invalid_nodes:

            # instance_node is str, but output_node is hou.Node so we convert
            output = instance.data.get("output_node")
            output_path = output.path() if output else None

            raise PublishXmlValidationError(
                self,
                "Invalid VDB content: {}".format(message),
                formatting_data={
                    "message": message,
                    "rop_path": instance.data.get("instance_node"),
                    "sop_path": output_path
                }
            )

    @classmethod
    def get_invalid_with_message(cls, instance):

        node = instance.data.get("output_node")
        if node is None:
            instance_node = instance.data.get("instance_node")
            error = (
                "SOP path is not correctly set on "
                "ROP node `{}`.".format(instance_node)
            )
            return [hou.node(instance_node), error]

        frame = instance.data.get("frameStart", 0)
        geometry = get_geometry_at_frame(node, frame)
        if geometry is None:
            # No geometry data on this node, maybe the node hasn't cooked?
            error = (
                "SOP node `{}` has no geometry data. "
                "Was it unable to cook?".format(node.path())
            )
            return [node, error]

        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 [None, None]

        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]

        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

get_geometry_at_frame(sop_node, frame, force=True)

Return geometry at frame but force a cooked value.

Source code in client/ayon_houdini/plugins/publish/validate_vdb_output_node.py
45
46
47
48
49
50
51
def get_geometry_at_frame(sop_node, frame, force=True):
    """Return geometry at frame but force a cooked value."""
    if not hasattr(sop_node, "geometry"):
        return
    with update_mode_context(hou.updateMode.AutoUpdate):
        sop_node.cook(force=force, frame_range=(frame, frame))
        return sop_node.geometryAtFrame(frame)

group_consecutive_numbers(nums)

Parameters:

Name Type Description Default
nums list

List of sorted integer numbers.

required

Yields:

Name Type Description
str

Group ranges as {start}-{end} if more than one number in the range else it yields {end}

Source code in client/ayon_houdini/plugins/publish/validate_vdb_output_node.py
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
def group_consecutive_numbers(nums):
    """
    Args:
        nums (list): List of sorted integer numbers.

    Yields:
        str: Group ranges as {start}-{end} if more than one number in the range
            else it yields {end}

    """
    start = None
    end = None

    def _result(a, b):
        if a == b:
            return "{}".format(a)
        else:
            return "{}-{}".format(a, b)

    for num in nums:
        if start is None:
            start = num
            end = num
        elif num == end + 1:
            end = num
        else:
            yield _result(start, end)
            start = num
            end = num
    if start is not None:
        yield _result(start, end)