Skip to content

validate_resolution

JumpToEditorNodeAction

Bases: Action

Select the editor nodes related to the USD attributes.

If a "Render Settings" node in the current Houdini scene defined the Render Settings primitive or changed the resolution attribute this would select the LOP node that set that attribute.

It does so by using the HoudiniPrimEditorNodes custom data on the USD object that Houdini stores when editing a USD attribute.

Source code in client/ayon_houdini/plugins/publish/validate_resolution.py
 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
class JumpToEditorNodeAction(pyblish.api.Action):
    """Select the editor nodes related to the USD attributes.

    If a "Render Settings" node in the current Houdini scene defined the
    Render Settings primitive or changed the resolution attribute this would
    select the LOP node that set that attribute.

    It does so by using the `HoudiniPrimEditorNodes` custom data on the USD
    object that Houdini stores when editing a USD attribute.
    """
    label = "Jump to Editor Node"
    on = "failed"  # This action is only available on a failed plug-in
    icon = "search"  # Icon from Awesome Icon

    get_invalid_objects_fn = "get_invalid_resolution"

    def process(self, context, plugin):
        errored_instances = get_errored_instances_from_context(context,
                                                               plugin=plugin)

        # Get the invalid nodes for the plug-ins
        self.log.info("Finding invalid nodes..")
        objects: "list[Usd.Object]" = list()
        for instance in errored_instances:

            get_invalid = getattr(plugin, self.get_invalid_objects_fn)
            invalid_objects = get_invalid(instance)
            if invalid_objects:
                if isinstance(invalid_objects, (list, tuple)):
                    objects.extend(invalid_objects)
                else:
                    self.log.warning("Plug-in returned to be invalid, "
                                     "but has no selectable nodes.")

        if not objects:
            self.log.info("No invalid objects found.")

        nodes: "list[hou.Node]" = []
        for obj in objects:
            lop_editor_nodes = self.get_lop_editor_node(obj)
            if lop_editor_nodes:
                # Get the last entry because it is the last node in the graph
                # that edited attribute or prim. For that node find the first
                # editable node so that we do not select inside e.g. a locked
                # HDA.
                editable_node = self.get_editable_node(lop_editor_nodes[-1])
                nodes.append(editable_node)

        hou.clearAllSelected()
        if nodes:
            self.log.info("Selecting invalid nodes: {}".format(
                ", ".join(node.path() for node in nodes)
            ))
            for node in nodes:
                node.setSelected(True)
                node.setCurrent(True)
        else:
            self.log.info("No invalid nodes found.")

    def get_lop_editor_node(self, obj: Usd.Object):
        """Return Houdini LOP Editor node from a USD object.

        If the object is a USD attribute but has no editor nodes, it will
        try to find the editor nodes from the parent prim.

        Arguments:
            obj (Usd.Object): USD object

        Returns:
            list[hou.Node]: Houdini LOP Editor nodes, if set in the custom
                data of the object.

        """
        key = "HoudiniPrimEditorNodes"
        editor_nodes = obj.GetCustomDataByKey(key)
        if not editor_nodes and isinstance(obj, Usd.Attribute):
            prim = obj.GetPrim()
            editor_nodes = prim.GetCustomDataByKey(key)

        if not editor_nodes:
            return []
        return [hou.nodeBySessionId(node) for node in editor_nodes]

    def get_editable_node(self, node: hou.Node):
        """Return the node or nearest parent that is editable.

        If the node is inside a locked HDA and it's not editable, then go up
        to the first parent that is editable.

        Returns:
            hou.Node: The node itself or the first parent that is editable.
        """
        while node.isInsideLockedHDA():
            # Allow editable node inside HDA
            if node.isEditableInsideLockedHDA():
                return node
            node = node.parent()
        return node

get_editable_node(node)

Return the node or nearest parent that is editable.

If the node is inside a locked HDA and it's not editable, then go up to the first parent that is editable.

Returns:

Type Description

hou.Node: The node itself or the first parent that is editable.

Source code in client/ayon_houdini/plugins/publish/validate_resolution.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
def get_editable_node(self, node: hou.Node):
    """Return the node or nearest parent that is editable.

    If the node is inside a locked HDA and it's not editable, then go up
    to the first parent that is editable.

    Returns:
        hou.Node: The node itself or the first parent that is editable.
    """
    while node.isInsideLockedHDA():
        # Allow editable node inside HDA
        if node.isEditableInsideLockedHDA():
            return node
        node = node.parent()
    return node

get_lop_editor_node(obj)

Return Houdini LOP Editor node from a USD object.

If the object is a USD attribute but has no editor nodes, it will try to find the editor nodes from the parent prim.

Parameters:

Name Type Description Default
obj Object

USD object

required

Returns:

Type Description

list[hou.Node]: Houdini LOP Editor nodes, if set in the custom data of the object.

Source code in client/ayon_houdini/plugins/publish/validate_resolution.py
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def get_lop_editor_node(self, obj: Usd.Object):
    """Return Houdini LOP Editor node from a USD object.

    If the object is a USD attribute but has no editor nodes, it will
    try to find the editor nodes from the parent prim.

    Arguments:
        obj (Usd.Object): USD object

    Returns:
        list[hou.Node]: Houdini LOP Editor nodes, if set in the custom
            data of the object.

    """
    key = "HoudiniPrimEditorNodes"
    editor_nodes = obj.GetCustomDataByKey(key)
    if not editor_nodes and isinstance(obj, Usd.Attribute):
        prim = obj.GetPrim()
        editor_nodes = prim.GetCustomDataByKey(key)

    if not editor_nodes:
        return []
    return [hou.nodeBySessionId(node) for node in editor_nodes]

ValidateRenderResolution

Bases: HoudiniInstancePlugin, OptionalPyblishPluginMixin

Validate the render resolution setting aligned with DB

Source code in client/ayon_houdini/plugins/publish/validate_resolution.py
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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
class ValidateRenderResolution(plugin.HoudiniInstancePlugin,
                               OptionalPyblishPluginMixin):
    """Validate the render resolution setting aligned with DB"""

    order = pyblish.api.ValidatorOrder
    families = ["usdrender"]
    label = "Validate Render Resolution"
    actions = [SelectROPAction, JumpToEditorNodeAction]
    optional = True

    def process(self, instance):
        if not self.is_active(instance.data):
            return

        invalid = self.get_invalid_resolution(instance)
        if invalid:
            raise PublishValidationError(
                "Render resolution does not match the entity's resolution for "
                "the current context. See log for details.",
                description=self.get_description()
            )

    @classmethod
    def get_invalid_resolution(cls, instance):
        # Get render resolution and pixel aspect ratio from USD stage
        rop_node = hou.node(instance.data["instance_node"])
        lop_node: hou.LopNode = get_usd_rop_loppath(rop_node)
        if not lop_node:
            cls.log.debug(
                f"No LOP node found for ROP node: {rop_node.path()}")
            return

        stage: Usd.Stage = lop_node.stage()
        render_settings: UsdRender.Settings = (
            get_usd_render_rop_rendersettings(rop_node, stage, logger=cls.log))
        if not render_settings:
            cls.log.debug(
                f"No render settings found for LOP node: {lop_node.path()}")
            return

        invalid = []

        # Each render product can have different resolution set if explicitly
        # overridden. If not set, it will use the resolution from the render
        # settings.
        sample_time = Usd.TimeCode.EarliestTime()

        # Get all resolution and pixel aspect attributes to validate
        resolution_attributes = [render_settings.GetResolutionAttr()]
        pixel_aspect_attributes = [render_settings.GetPixelAspectRatioAttr()]
        for product in cls.iter_render_products(render_settings, stage):
            resolution_attr = product.GetResolutionAttr()
            if resolution_attr.HasAuthoredValue():
                resolution_attributes.append(resolution_attr)

            pixel_aspect_attr = product.GetPixelAspectRatioAttr()
            if pixel_aspect_attr.HasAuthoredValue():
                pixel_aspect_attributes.append(pixel_aspect_attr)

        # Validate resolution and pixel aspect ratio
        width, height, pixel_aspect = cls.get_expected_resolution(instance)
        for resolution_attr in resolution_attributes:
            current_width, current_height = resolution_attr.Get(sample_time)
            if current_width != width or current_height != height:
                cls.log.error(
                    f"{resolution_attr.GetPath()}: "
                    f"{current_width}x{current_height} "
                    f"does not match context resolution {width}x{height}"
                )
                invalid.append(resolution_attr)

        for pixel_aspect_attr in pixel_aspect_attributes:
            current_pixel_aspect = pixel_aspect_attr.Get(sample_time)
            if current_pixel_aspect != pixel_aspect:
                cls.log.error(
                    f"{pixel_aspect_attr.GetPath()}: "
                    f"{current_pixel_aspect} does not "
                    f"match context pixel aspect {pixel_aspect}")
                invalid.append(pixel_aspect_attr)

        return invalid

    @classmethod
    def get_expected_resolution(cls, instance):
        """Return the expected resolution and pixel aspect ratio for the
        instance based on the task entity or folder entity."""

        entity = instance.data.get("taskEntity")
        if not entity:
            entity = instance.data["folderEntity"]

        attributes = entity["attrib"]
        width = attributes["resolutionWidth"]
        height = attributes["resolutionHeight"]
        pixel_aspect = attributes["pixelAspect"]
        return int(width), int(height), float(pixel_aspect)

    @classmethod
    def iter_render_products(cls, render_settings, stage):
        """Iterate over all render products in the USD render settings"""
        for product_path in render_settings.GetProductsRel().GetTargets():
            prim = stage.GetPrimAtPath(product_path)
            if not prim.IsValid():
                cls.log.debug(
                    f"Render product path is not a valid prim: {product_path}")
                return

            if prim.IsA(UsdRender.Product):
                yield UsdRender.Product(prim)

    @staticmethod
    def get_description():
        return inspect.cleandoc("""
            ### Render Resolution does not match context

            The render resolution or pixel aspect ratio does not match the
            resolution configured in the project database. Please ensure the
            render resolution is set correctly.

            #### USD Render Settings

            In most cases the render resolution is defined via the Render
            Settings prim in USD, however each Render Product is capable
            of authoring its own override. The logs will report the exact
            attribute path for the mismatching resolution or aspect ratio.
        """)

get_expected_resolution(instance) classmethod

Return the expected resolution and pixel aspect ratio for the instance based on the task entity or folder entity.

Source code in client/ayon_houdini/plugins/publish/validate_resolution.py
203
204
205
206
207
208
209
210
211
212
213
214
215
216
@classmethod
def get_expected_resolution(cls, instance):
    """Return the expected resolution and pixel aspect ratio for the
    instance based on the task entity or folder entity."""

    entity = instance.data.get("taskEntity")
    if not entity:
        entity = instance.data["folderEntity"]

    attributes = entity["attrib"]
    width = attributes["resolutionWidth"]
    height = attributes["resolutionHeight"]
    pixel_aspect = attributes["pixelAspect"]
    return int(width), int(height), float(pixel_aspect)

iter_render_products(render_settings, stage) classmethod

Iterate over all render products in the USD render settings

Source code in client/ayon_houdini/plugins/publish/validate_resolution.py
218
219
220
221
222
223
224
225
226
227
228
229
@classmethod
def iter_render_products(cls, render_settings, stage):
    """Iterate over all render products in the USD render settings"""
    for product_path in render_settings.GetProductsRel().GetTargets():
        prim = stage.GetPrimAtPath(product_path)
        if not prim.IsValid():
            cls.log.debug(
                f"Render product path is not a valid prim: {product_path}")
            return

        if prim.IsA(UsdRender.Product):
            yield UsdRender.Product(prim)