Skip to content

plugin

Houdini specific AYON/Pyblish plugin definitions.

HoudiniContextPlugin

Bases: ContextPlugin

Base class for Houdini context publish plugins.

Source code in client/ayon_houdini/api/plugin.py
378
379
380
381
382
class HoudiniContextPlugin(pyblish.api.ContextPlugin):
    """Base class for Houdini context publish plugins."""

    hosts = ["houdini"]
    settings_category = SETTINGS_CATEGORY

HoudiniCreator

Bases: Creator, HoudiniCreatorBase

Base class for most of the Houdini creator plugins.

Source code in client/ayon_houdini/api/plugin.py
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
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
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
class HoudiniCreator(Creator, HoudiniCreatorBase):
    """Base class for most of the Houdini creator plugins."""
    selected_nodes = []
    settings_name = None
    add_publish_button = False

    settings_category = SETTINGS_CATEGORY

    def create(self, product_name, instance_data, pre_create_data):
        try:
            self.selected_nodes = []

            if pre_create_data.get("use_selection"):
                self.selected_nodes = hou.selectedNodes()

            # Get the node type and remove it from the data, not needed
            node_type = instance_data.pop("node_type", None)
            if node_type is None:
                node_type = "geometry"

            folder_path = instance_data["folderPath"]

            instance_node = self.create_instance_node(
                folder_path,
                product_name,
                "/out",
                node_type,
                pre_create_data
            )

            self.customize_node_look(instance_node)

            instance_data["instance_node"] = instance_node.path()
            instance_data["instance_id"] = instance_node.path()
            instance_data["families"] = self.get_publish_families()
            instance = CreatedInstance(
                self.product_type,
                product_name,
                instance_data,
                self)
            self._add_instance_to_context(instance)
            self.imprint(instance_node, instance.data_to_store())

            if self.add_publish_button:
                add_self_publish_button(instance_node)

            return instance

        except hou.Error as exc:
            raise CreatorError(f"Creator error: {exc}") from exc

    def lock_parameters(self, node, parameters):
        """Lock list of specified parameters on the node.

        Args:
            node (hou.Node): Houdini node to lock parameters on.
            parameters (list of str): List of parameter names.

        """
        for name in parameters:
            try:
                parm = node.parm(name)
                parm.lock(True)
            except AttributeError:
                self.log.debug("missing lock pattern {}".format(name))

    def collect_instances(self):
        # cache instances  if missing
        self.cache_instance_data(self.collection_shared_data)
        for instance in self.collection_shared_data[
                "houdini_cached_instances"].get(self.identifier, []):

            node_data = read(instance)

            # Node paths are always the full node path since that is unique
            # Because it's the node's path it's not written into attributes
            # but explicitly collected
            node_path = instance.path()
            node_data["instance_id"] = node_path
            node_data["instance_node"] = node_path
            node_data["families"] = self.get_publish_families()
            if "AYON_productName" in node_data:
                node_data["productName"] = node_data.pop("AYON_productName")

            created_instance = CreatedInstance.from_existing(
                node_data, self
            )
            self._add_instance_to_context(created_instance)

    def update_instances(self, update_list):
        for created_inst, changes in update_list:
            instance_node = hou.node(created_inst.get("instance_node"))
            new_values = {
                key: changes[key].new_value
                for key in changes.changed_keys
            }
            # Update parm templates and values
            self.imprint(
                instance_node,
                new_values,
                update=True
            )

    def imprint(self, node, values, update=False):
        # Never store instance node and instance id since that data comes
        # from the node's path
        if "productName" in values:
            values["AYON_productName"] = values.pop("productName")
        values.pop("instance_node", None)
        values.pop("instance_id", None)
        values.pop("families", None)
        imprint(node, values, update=update)

    def remove_instances(self, instances):
        """Remove specified instance from the scene.

        This is only removing `id` parameter so instance is no longer
        instance, because it might contain valuable data for artist.

        """
        for instance in instances:
            instance_node = hou.node(instance.data.get("instance_node"))
            if instance_node:
                instance_node.destroy()

            self._remove_instance_from_context(instance)

    def get_pre_create_attr_defs(self):
        return [
            BoolDef("use_selection", default=True, label="Use selection")
        ]

    @staticmethod
    def customize_node_look(
            node, color=None,
            shape="chevron_down"):
        """Set custom look for instance nodes.

        Args:
            node (hou.Node): Node to set look.
            color (hou.Color, Optional): Color of the node.
            shape (str, Optional): Shape name of the node.

        Returns:
            None

        """
        if not color:
            color = hou.Color((0.616, 0.871, 0.769))
        node.setUserData('nodeshape', shape)
        node.setColor(color)

    def get_publish_families(self):
        """Return families for the instances of this creator.

        Allow a Creator to define multiple families so that a creator can
        e.g. specify `usd` and `usdrop`.

        There is no need to override this method if you only have the
        primary family defined by the `product_type` property as that will
        always be set.

        Returns:
            List[str]: families for instances of this creator
        """
        return []

    def get_network_categories(self):
        """Return in which network view type this creator should show.

        The node type categories returned here will be used to define where
        the creator will show up in the TAB search for nodes in Houdini's
        Network View.

        This can be overridden in inherited classes to define where that
        particular Creator should be visible in the TAB search.

        Returns:
            list: List of houdini node type categories

        """
        return [hou.ropNodeTypeCategory()]

    def apply_settings(self, project_settings):
        """Method called on initialization of plugin to apply settings."""

        # Apply General Settings
        houdini_general_settings = project_settings["houdini"]["general"]
        self.add_publish_button = houdini_general_settings.get(
            "add_self_publish_button", False)

        # Apply Creator Settings
        settings_name = self.settings_name
        if settings_name is None:
            settings_name = self.__class__.__name__

        settings = project_settings["houdini"]["create"]
        settings = settings.get(settings_name)
        if settings is None:
            self.log.debug(
                "No settings found for {}".format(self.__class__.__name__)
            )
            return

        for key, value in settings.items():
            setattr(self, key, value)

apply_settings(project_settings)

Method called on initialization of plugin to apply settings.

Source code in client/ayon_houdini/api/plugin.py
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
def apply_settings(self, project_settings):
    """Method called on initialization of plugin to apply settings."""

    # Apply General Settings
    houdini_general_settings = project_settings["houdini"]["general"]
    self.add_publish_button = houdini_general_settings.get(
        "add_self_publish_button", False)

    # Apply Creator Settings
    settings_name = self.settings_name
    if settings_name is None:
        settings_name = self.__class__.__name__

    settings = project_settings["houdini"]["create"]
    settings = settings.get(settings_name)
    if settings is None:
        self.log.debug(
            "No settings found for {}".format(self.__class__.__name__)
        )
        return

    for key, value in settings.items():
        setattr(self, key, value)

customize_node_look(node, color=None, shape='chevron_down') staticmethod

Set custom look for instance nodes.

Parameters:

Name Type Description Default
node Node

Node to set look.

required
color (Color, Optional)

Color of the node.

None
shape (str, Optional)

Shape name of the node.

'chevron_down'

Returns:

Type Description

None

Source code in client/ayon_houdini/api/plugin.py
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
@staticmethod
def customize_node_look(
        node, color=None,
        shape="chevron_down"):
    """Set custom look for instance nodes.

    Args:
        node (hou.Node): Node to set look.
        color (hou.Color, Optional): Color of the node.
        shape (str, Optional): Shape name of the node.

    Returns:
        None

    """
    if not color:
        color = hou.Color((0.616, 0.871, 0.769))
    node.setUserData('nodeshape', shape)
    node.setColor(color)

get_network_categories()

Return in which network view type this creator should show.

The node type categories returned here will be used to define where the creator will show up in the TAB search for nodes in Houdini's Network View.

This can be overridden in inherited classes to define where that particular Creator should be visible in the TAB search.

Returns:

Name Type Description
list

List of houdini node type categories

Source code in client/ayon_houdini/api/plugin.py
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
def get_network_categories(self):
    """Return in which network view type this creator should show.

    The node type categories returned here will be used to define where
    the creator will show up in the TAB search for nodes in Houdini's
    Network View.

    This can be overridden in inherited classes to define where that
    particular Creator should be visible in the TAB search.

    Returns:
        list: List of houdini node type categories

    """
    return [hou.ropNodeTypeCategory()]

get_publish_families()

Return families for the instances of this creator.

Allow a Creator to define multiple families so that a creator can e.g. specify usd and usdrop.

There is no need to override this method if you only have the primary family defined by the product_type property as that will always be set.

Returns:

Type Description

List[str]: families for instances of this creator

Source code in client/ayon_houdini/api/plugin.py
254
255
256
257
258
259
260
261
262
263
264
265
266
267
def get_publish_families(self):
    """Return families for the instances of this creator.

    Allow a Creator to define multiple families so that a creator can
    e.g. specify `usd` and `usdrop`.

    There is no need to override this method if you only have the
    primary family defined by the `product_type` property as that will
    always be set.

    Returns:
        List[str]: families for instances of this creator
    """
    return []

lock_parameters(node, parameters)

Lock list of specified parameters on the node.

Parameters:

Name Type Description Default
node Node

Houdini node to lock parameters on.

required
parameters list of str

List of parameter names.

required
Source code in client/ayon_houdini/api/plugin.py
153
154
155
156
157
158
159
160
161
162
163
164
165
166
def lock_parameters(self, node, parameters):
    """Lock list of specified parameters on the node.

    Args:
        node (hou.Node): Houdini node to lock parameters on.
        parameters (list of str): List of parameter names.

    """
    for name in parameters:
        try:
            parm = node.parm(name)
            parm.lock(True)
        except AttributeError:
            self.log.debug("missing lock pattern {}".format(name))

remove_instances(instances)

Remove specified instance from the scene.

This is only removing id parameter so instance is no longer instance, because it might contain valuable data for artist.

Source code in client/ayon_houdini/api/plugin.py
215
216
217
218
219
220
221
222
223
224
225
226
227
def remove_instances(self, instances):
    """Remove specified instance from the scene.

    This is only removing `id` parameter so instance is no longer
    instance, because it might contain valuable data for artist.

    """
    for instance in instances:
        instance_node = hou.node(instance.data.get("instance_node"))
        if instance_node:
            instance_node.destroy()

        self._remove_instance_from_context(instance)

HoudiniCreatorBase

Bases: object

Source code in client/ayon_houdini/api/plugin.py
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
class HoudiniCreatorBase(object):
    @staticmethod
    def cache_instance_data(shared_data):
        """Cache instances for Creators to shared data.

        Create `houdini_cached_instances` key when needed in shared data and
        fill it with all collected instances from the scene under its
        respective creator identifiers.

        Create `houdini_cached_legacy_instance` key for any legacy instances
        detected in the scene as instances per family.

        Args:
            Dict[str, Any]: Shared data.

        """
        if shared_data.get("houdini_cached_instances") is None:
            cache = dict()
            cache_legacy = dict()

            nodes = []
            for id_type in [AYON_INSTANCE_ID, AVALON_INSTANCE_ID]:
                nodes.extend(lsattr("id", id_type))
            for node in nodes:

                creator_identifier_parm = node.parm("creator_identifier")
                if creator_identifier_parm:
                    # creator instance
                    creator_id = creator_identifier_parm.eval()
                    cache.setdefault(creator_id, []).append(node)

                else:
                    # legacy instance
                    family_parm = node.parm("family")
                    if not family_parm:
                        # must be a broken instance
                        continue

                    family = family_parm.eval()
                    cache_legacy.setdefault(family, []).append(node)

            shared_data["houdini_cached_instances"] = cache
            shared_data["houdini_cached_legacy_instance"] = cache_legacy

        return shared_data

    @staticmethod
    def create_instance_node(
        folder_path,
        node_name,
        parent,
        node_type="geometry",
        pre_create_data=None
    ):
        """Create node representing instance.

        Arguments:
            folder_path (str): Folder path.
            node_name (str): Name of the new node.
            parent (str): Name of the parent node.
            node_type (str, optional): Type of the node.
            pre_create_data (Optional[Dict]): Pre create data.

        Returns:
            hou.Node: Newly created instance node.

        """
        parent_node = hou.node(parent)
        instance_node = parent_node.createNode(
            node_type, node_name=node_name)
        instance_node.moveToGoodPosition()
        return instance_node

cache_instance_data(shared_data) staticmethod

Cache instances for Creators to shared data.

Create houdini_cached_instances key when needed in shared data and fill it with all collected instances from the scene under its respective creator identifiers.

Create houdini_cached_legacy_instance key for any legacy instances detected in the scene as instances per family.

Parameters:

Name Type Description Default
Dict[str, Any]

Shared data.

required
Source code in client/ayon_houdini/api/plugin.py
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
@staticmethod
def cache_instance_data(shared_data):
    """Cache instances for Creators to shared data.

    Create `houdini_cached_instances` key when needed in shared data and
    fill it with all collected instances from the scene under its
    respective creator identifiers.

    Create `houdini_cached_legacy_instance` key for any legacy instances
    detected in the scene as instances per family.

    Args:
        Dict[str, Any]: Shared data.

    """
    if shared_data.get("houdini_cached_instances") is None:
        cache = dict()
        cache_legacy = dict()

        nodes = []
        for id_type in [AYON_INSTANCE_ID, AVALON_INSTANCE_ID]:
            nodes.extend(lsattr("id", id_type))
        for node in nodes:

            creator_identifier_parm = node.parm("creator_identifier")
            if creator_identifier_parm:
                # creator instance
                creator_id = creator_identifier_parm.eval()
                cache.setdefault(creator_id, []).append(node)

            else:
                # legacy instance
                family_parm = node.parm("family")
                if not family_parm:
                    # must be a broken instance
                    continue

                family = family_parm.eval()
                cache_legacy.setdefault(family, []).append(node)

        shared_data["houdini_cached_instances"] = cache
        shared_data["houdini_cached_legacy_instance"] = cache_legacy

    return shared_data

create_instance_node(folder_path, node_name, parent, node_type='geometry', pre_create_data=None) staticmethod

Create node representing instance.

Parameters:

Name Type Description Default
folder_path str

Folder path.

required
node_name str

Name of the new node.

required
parent str

Name of the parent node.

required
node_type str

Type of the node.

'geometry'
pre_create_data Optional[Dict]

Pre create data.

None

Returns:

Type Description

hou.Node: Newly created instance node.

Source code in client/ayon_houdini/api/plugin.py
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
@staticmethod
def create_instance_node(
    folder_path,
    node_name,
    parent,
    node_type="geometry",
    pre_create_data=None
):
    """Create node representing instance.

    Arguments:
        folder_path (str): Folder path.
        node_name (str): Name of the new node.
        parent (str): Name of the parent node.
        node_type (str, optional): Type of the node.
        pre_create_data (Optional[Dict]): Pre create data.

    Returns:
        hou.Node: Newly created instance node.

    """
    parent_node = hou.node(parent)
    instance_node = parent_node.createNode(
        node_type, node_name=node_name)
    instance_node.moveToGoodPosition()
    return instance_node

HoudiniExtractorPlugin

Bases: Extractor

Base class for Houdini extract plugins.

Note

The HoudiniExtractorPlugin is a subclass of publish.Extractor, which in turn is a subclass of pyblish.api.InstancePlugin. Should there be a requirement to create an extractor that operates as a context plugin, it would be beneficial to incorporate the functionalities present in publish.Extractor.

Source code in client/ayon_houdini/api/plugin.py
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
class HoudiniExtractorPlugin(publish.Extractor):
    """Base class for Houdini extract plugins.

    Note:
        The `HoudiniExtractorPlugin` is a subclass of `publish.Extractor`,
            which in turn is a subclass of `pyblish.api.InstancePlugin`.
        Should there be a requirement to create an extractor that operates
            as a context plugin, it would be beneficial to incorporate
            the functionalities present in `publish.Extractor`.
    """

    hosts = ["houdini"]
    settings_category = SETTINGS_CATEGORY

    def render_rop(self, instance: pyblish.api.Instance):
        """Render the ROP node of the instance.

        If `instance.data["frames_to_fix"]` is set and is not empty it will
        be interpreted as a set of frames that will be rendered instead of the
        full rop nodes frame range.

        Only `instance.data["instance_node"]` is required.
        """
        # Log the start of the render
        rop_node = hou.node(instance.data["instance_node"])
        self.log.debug(f"Rendering {rop_node.path()}")

        frames_to_fix = clique.parse(instance.data.get("frames_to_fix", ""),
                                     "{ranges}")
        if len(set(frames_to_fix)) < 2:
            render_rop(rop_node)
            return

        # Render only frames to fix
        for frame_range in frames_to_fix.separate():
            frame_range = list(frame_range)
            first_frame = int(frame_range[0])
            last_frame = int(frame_range[-1])
            self.log.debug(
                f"Rendering frames to fix [{first_frame}, {last_frame}]"
            )
            # for step to be 1 since clique doesn't support steps.
            frame_range = (first_frame, last_frame, 1)
            render_rop(rop_node, frame_range=frame_range)

render_rop(instance)

Render the ROP node of the instance.

If instance.data["frames_to_fix"] is set and is not empty it will be interpreted as a set of frames that will be rendered instead of the full rop nodes frame range.

Only instance.data["instance_node"] is required.

Source code in client/ayon_houdini/api/plugin.py
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
def render_rop(self, instance: pyblish.api.Instance):
    """Render the ROP node of the instance.

    If `instance.data["frames_to_fix"]` is set and is not empty it will
    be interpreted as a set of frames that will be rendered instead of the
    full rop nodes frame range.

    Only `instance.data["instance_node"]` is required.
    """
    # Log the start of the render
    rop_node = hou.node(instance.data["instance_node"])
    self.log.debug(f"Rendering {rop_node.path()}")

    frames_to_fix = clique.parse(instance.data.get("frames_to_fix", ""),
                                 "{ranges}")
    if len(set(frames_to_fix)) < 2:
        render_rop(rop_node)
        return

    # Render only frames to fix
    for frame_range in frames_to_fix.separate():
        frame_range = list(frame_range)
        first_frame = int(frame_range[0])
        last_frame = int(frame_range[-1])
        self.log.debug(
            f"Rendering frames to fix [{first_frame}, {last_frame}]"
        )
        # for step to be 1 since clique doesn't support steps.
        frame_range = (first_frame, last_frame, 1)
        render_rop(rop_node, frame_range=frame_range)

HoudiniInstancePlugin

Bases: InstancePlugin

Base class for Houdini instance publish plugins.

Source code in client/ayon_houdini/api/plugin.py
371
372
373
374
375
class HoudiniInstancePlugin(pyblish.api.InstancePlugin):
    """Base class for Houdini instance publish plugins."""

    hosts = ["houdini"]
    settings_category = SETTINGS_CATEGORY

HoudiniLoader

Bases: LoaderPlugin

Base class for Houdini load plugins.

Source code in client/ayon_houdini/api/plugin.py
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
class HoudiniLoader(load.LoaderPlugin):
    """Base class for Houdini load plugins."""

    hosts = ["houdini"]
    settings_category = SETTINGS_CATEGORY
    use_ayon_entity_uri = False
    collapse_paths_to_root_vars = False

    @classmethod
    def apply_settings(cls, project_settings):
        # Prepare collapsible variable mapping using entries in `os.environ`
        # that are set to the project root paths
        cls.collapse_paths_to_root_vars: bool = (
            project_settings["houdini"]["load"]
            .get("collapse_path_to_project_root_vars", False)
        )

        super().apply_settings(project_settings)

    @classmethod
    def _get_collapsible_vars(cls) -> Dict[str, str]:
        """Return which variables keys may be collapsed to if path starts with
        the values."""
        collapsible_vars = {}
        for key, value in os.environ.items():
            if key.startswith("AYON_PROJECT_ROOT_"):
                if not value:
                    continue
                collapsible_vars[key] = value.replace("\\", "/")

        # Sort by length to ensure that the longest matching key is first
        # so that the nearest matching root is used
        return {
            key: value
            for key, value
            in sorted(collapsible_vars.items(),
                      key=lambda x: len(x[1]),
                      reverse=True)
        }

    @classmethod
    def filepath_from_context(cls, context):
        if cls.use_ayon_entity_uri:
            return get_ayon_entity_uri_from_representation_context(context)

        path = super().filepath_from_context(context)

        # Remap project roots to the collapsible path variables
        if cls.collapse_paths_to_root_vars:
            collapsible_vars = cls._get_collapsible_vars()
            if collapsible_vars:
                match_path = path.replace("\\", "/")
                for key, value in collapsible_vars.items():
                    if match_path.startswith(value):
                        # Replace start of string with the key
                        path = f"${key}" + path[len(value):]
                        break

        return path