├── docs
├── index.md
├── license.md
├── img
│ ├── favicon.ico
│ └── ay-symbol-blackw-full.png
└── css
│ └── custom.css
├── README.md
├── client
└── ayon_houdini
│ ├── startup
│ ├── otls
│ │ ├── ayon_lop_import.hda
│ │ │ ├── houdini.hdalibrary
│ │ │ ├── ayon_8_8Lop_1lop__import_8_81.0
│ │ │ │ ├── MessageNodes
│ │ │ │ ├── Contents.dir
│ │ │ │ │ ├── Sections.list
│ │ │ │ │ ├── .OPdummydefs
│ │ │ │ │ ├── .OPfallbacks
│ │ │ │ │ ├── Contents.houdini_versions
│ │ │ │ │ ├── Contents.modtimes
│ │ │ │ │ └── Contents.createtimes
│ │ │ │ ├── IconImage
│ │ │ │ ├── OnCreated
│ │ │ │ ├── AYON__icon.png
│ │ │ │ ├── InternalFileOptions
│ │ │ │ ├── OnNameChanged
│ │ │ │ ├── PythonModule
│ │ │ │ ├── TypePropertiesOptions
│ │ │ │ ├── CreateScript
│ │ │ │ ├── Sections.list
│ │ │ │ ├── OnLoaded
│ │ │ │ ├── Tools.shelf
│ │ │ │ ├── Help
│ │ │ │ └── ExtraFileOptions
│ │ │ ├── Sections.list
│ │ │ └── INDEX__SECTION
│ │ ├── ayon.generic_loader.hda
│ │ │ ├── houdini.hdalibrary
│ │ │ ├── ayon_8_8Lop_1generic__loader_8_81.0
│ │ │ │ ├── Help
│ │ │ │ ├── Contents.dir
│ │ │ │ │ ├── Sections.list
│ │ │ │ │ ├── Contents.createtimes
│ │ │ │ │ ├── Contents.modtimes
│ │ │ │ │ └── Contents.houdini_versions
│ │ │ │ ├── OnDeleted
│ │ │ │ ├── IconImage
│ │ │ │ ├── InternalFileOptions
│ │ │ │ ├── OnNameChanged
│ │ │ │ ├── PythonModule
│ │ │ │ ├── TypePropertiesOptions
│ │ │ │ ├── OnLoaded
│ │ │ │ ├── Sections.list
│ │ │ │ ├── CreateScript
│ │ │ │ ├── OnCreated
│ │ │ │ ├── Tools.shelf
│ │ │ │ └── ExtraFileOptions
│ │ │ ├── ayon_8_8Sop_1generic__loader_8_81.0
│ │ │ │ ├── Help
│ │ │ │ ├── Contents.dir
│ │ │ │ │ ├── Sections.list
│ │ │ │ │ ├── Contents.createtimes
│ │ │ │ │ ├── Contents.modtimes
│ │ │ │ │ └── Contents.houdini_versions
│ │ │ │ ├── OnDeleted
│ │ │ │ ├── IconImage
│ │ │ │ ├── InternalFileOptions
│ │ │ │ ├── OnNameChanged
│ │ │ │ ├── PythonModule
│ │ │ │ ├── TypePropertiesOptions
│ │ │ │ ├── OnLoaded
│ │ │ │ ├── Sections.list
│ │ │ │ ├── CreateScript
│ │ │ │ ├── OnCreated
│ │ │ │ ├── Tools.shelf
│ │ │ │ └── ExtraFileOptions
│ │ │ ├── ayon_8_8Object_1generic__loader_8_81.0
│ │ │ │ ├── Help
│ │ │ │ ├── Contents.dir
│ │ │ │ │ ├── Sections.list
│ │ │ │ │ ├── Contents.modtimes
│ │ │ │ │ ├── Contents.createtimes
│ │ │ │ │ └── Contents.mime
│ │ │ │ ├── OnDeleted
│ │ │ │ ├── IconImage
│ │ │ │ ├── InternalFileOptions
│ │ │ │ ├── OnNameChanged
│ │ │ │ ├── PythonModule
│ │ │ │ ├── TypePropertiesOptions
│ │ │ │ ├── OnLoaded
│ │ │ │ ├── Sections.list
│ │ │ │ ├── CreateScript
│ │ │ │ ├── OnCreated
│ │ │ │ ├── Tools.shelf
│ │ │ │ └── ExtraFileOptions
│ │ │ ├── Sections.list
│ │ │ └── INDEX__SECTION
│ │ ├── ayon_lop_load_shot.hda
│ │ │ ├── houdini.hdalibrary
│ │ │ ├── ayon_8_8Lop_1load__shot_8_81.0
│ │ │ │ ├── MessageNodes
│ │ │ │ ├── Contents.dir
│ │ │ │ │ ├── Sections.list
│ │ │ │ │ ├── .OPdummydefs
│ │ │ │ │ ├── .OPfallbacks
│ │ │ │ │ ├── Contents.houdini_versions
│ │ │ │ │ ├── Contents.modtimes
│ │ │ │ │ └── Contents.createtimes
│ │ │ │ ├── IconImage
│ │ │ │ ├── OnDeleted
│ │ │ │ ├── AYON__icon.png
│ │ │ │ ├── InternalFileOptions
│ │ │ │ ├── OnNameChanged
│ │ │ │ ├── PythonModule
│ │ │ │ ├── Help
│ │ │ │ ├── TypePropertiesOptions
│ │ │ │ ├── CreateScript
│ │ │ │ ├── OnLoaded
│ │ │ │ ├── Sections.list
│ │ │ │ ├── OnCreated
│ │ │ │ ├── Tools.shelf
│ │ │ │ └── ExtraFileOptions
│ │ │ ├── Sections.list
│ │ │ └── INDEX__SECTION
│ │ ├── ayon_lop_mute_layers.hda
│ │ │ ├── houdini.hdalibrary
│ │ │ ├── ayon_8_8Lop_1mute__layers_8_81.0
│ │ │ │ ├── Contents.dir
│ │ │ │ │ ├── Sections.list
│ │ │ │ │ ├── Contents.modtimes
│ │ │ │ │ ├── Contents.createtimes
│ │ │ │ │ └── Contents.houdini_versions
│ │ │ │ ├── IconImage
│ │ │ │ ├── AYON__icon.png
│ │ │ │ ├── InternalFileOptions
│ │ │ │ ├── Sections.list
│ │ │ │ ├── TypePropertiesOptions
│ │ │ │ ├── CreateScript
│ │ │ │ ├── ExtraFileOptions
│ │ │ │ ├── Help
│ │ │ │ └── Tools.shelf
│ │ │ ├── Sections.list
│ │ │ └── INDEX__SECTION
│ │ └── README.md
│ ├── python2.7libs
│ │ └── pythonrc.py
│ ├── python3.7libs
│ │ └── pythonrc.py
│ ├── python3.9libs
│ │ └── pythonrc.py
│ ├── python3.10libs
│ │ └── pythonrc.py
│ ├── python3.11libs
│ │ └── pythonrc.py
│ ├── PARMmenu.xml
│ ├── OPmenu.xml
│ └── husdplugins
│ │ └── outputprocessors
│ │ └── remap_to_publish.py
│ ├── version.py
│ ├── __init__.py
│ ├── plugins
│ ├── publish
│ │ ├── collect_workscene_fps.py
│ │ ├── collect_staticmesh_type.py
│ │ ├── help
│ │ │ └── validate_vdb_output_node.xml
│ │ ├── collect_reviewable_instances.py
│ │ ├── collect_renderlayer.py
│ │ ├── collect_current_file.py
│ │ ├── validate_usd_render_product_names.py
│ │ ├── collect_workfile.py
│ │ ├── collect_rop_frame_range.py
│ │ ├── validate_mkpaths_toggled.py
│ │ ├── validate_alembic_face_sets.py
│ │ ├── validate_houdini_license_category.py
│ │ ├── save_scene.py
│ │ ├── validate_bypass.py
│ │ ├── collect_farm_instances.py
│ │ ├── validate_file_extension.py
│ │ ├── validate_frame_token.py
│ │ ├── validate_animation_settings.py
│ │ ├── validate_export_is_a_single_frame.py
│ │ ├── validate_mesh_is_static.py
│ │ ├── validate_wait_for_render.py
│ │ ├── extract_active_view_thumbnail.py
│ │ ├── validate_alembic_input_node.py
│ │ ├── validate_camera_rop.py
│ │ ├── validate_render_products.py
│ │ ├── collect_render_colorspace.py
│ │ ├── collect_frames.py
│ │ ├── validate_usd_output_node.py
│ │ ├── collect_usd_render.py
│ │ ├── validate_no_errors.py
│ │ ├── collect_cache_farm.py
│ │ ├── collect_output_node.py
│ │ ├── validate_scene_review.py
│ │ ├── extract_hda.py
│ │ ├── validate_cop_output_node.py
│ │ ├── extract_render.py
│ │ ├── increment_current_file.py
│ │ └── collect_karma_rop.py
│ ├── inventory
│ │ ├── set_camera_resolution.py
│ │ ├── show_parameters.py
│ │ └── select_containers.py
│ ├── load
│ │ ├── show_usdview.py
│ │ ├── load_shot_lop.py
│ │ ├── load_asset_lop.py
│ │ ├── actions.py
│ │ ├── load_usd_layer.py
│ │ ├── load_alembic_archive.py
│ │ └── load_usd_sop.py
│ ├── workfile_build
│ │ └── create_placeholder.py
│ └── create
│ │ ├── create_usd_componentbuilder.py
│ │ ├── create_composite.py
│ │ └── create_copernicus.py
│ ├── api
│ ├── __init__.py
│ └── colorspace.py
│ ├── hooks
│ ├── set_paths.py
│ └── set_default_display_and_view.py
│ └── addon.py
├── mkdocs_requirements.txt
├── server
├── settings
│ ├── __init__.py
│ ├── templated_workfile_build.py
│ ├── conversion.py
│ ├── general.py
│ ├── main.py
│ ├── shelves.py
│ └── imageio.py
└── __init__.py
├── package.py
├── .github
└── workflows
│ ├── validate_pr_labels.yml
│ ├── deploy_mkdocs.yml
│ ├── pr_linting.yml
│ ├── upload_to_ynput_cloud.yml
│ ├── release_trigger.yml
│ └── assign_pr_to_project.yml
├── mkdocs.yml
└── ruff.toml
/docs/index.md:
--------------------------------------------------------------------------------
1 | --8<-- "README.md"
2 |
--------------------------------------------------------------------------------
/docs/license.md:
--------------------------------------------------------------------------------
1 | --8<-- "LICENSE"
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Houdini addon
2 | Houdini integration for AYON.
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_import.hda/houdini.hdalibrary:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/houdini.hdalibrary:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_load_shot.hda/houdini.hdalibrary:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_mute_layers.hda/houdini.hdalibrary:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Lop_1generic__loader_8_81.0/Help:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Sop_1generic__loader_8_81.0/Help:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ynput/ayon-houdini/HEAD/docs/img/favicon.ico
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Object_1generic__loader_8_81.0/Help:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/img/ay-symbol-blackw-full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ynput/ayon-houdini/HEAD/docs/img/ay-symbol-blackw-full.png
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_import.hda/ayon_8_8Lop_1lop__import_8_81.0/MessageNodes:
--------------------------------------------------------------------------------
1 | warn_no_representation_set reference
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_load_shot.hda/ayon_8_8Lop_1load__shot_8_81.0/MessageNodes:
--------------------------------------------------------------------------------
1 | warn_no_representation_set sublayer
--------------------------------------------------------------------------------
/client/ayon_houdini/version.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Package declaring AYON addon 'houdini' version."""
3 | __version__ = "0.9.4+dev"
4 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_import.hda/ayon_8_8Lop_1lop__import_8_81.0/Contents.dir/Sections.list:
--------------------------------------------------------------------------------
1 | ""
2 | Contents.mime Contents.mime
3 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_load_shot.hda/ayon_8_8Lop_1load__shot_8_81.0/Contents.dir/Sections.list:
--------------------------------------------------------------------------------
1 | ""
2 | Contents.mime Contents.mime
3 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Lop_1generic__loader_8_81.0/Contents.dir/Sections.list:
--------------------------------------------------------------------------------
1 | ""
2 | Contents.mime Contents.mime
3 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Sop_1generic__loader_8_81.0/Contents.dir/Sections.list:
--------------------------------------------------------------------------------
1 | ""
2 | Contents.mime Contents.mime
3 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_mute_layers.hda/ayon_8_8Lop_1mute__layers_8_81.0/Contents.dir/Sections.list:
--------------------------------------------------------------------------------
1 | ""
2 | Contents.mime Contents.mime
3 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Object_1generic__loader_8_81.0/Contents.dir/Sections.list:
--------------------------------------------------------------------------------
1 | ""
2 | Contents.mime Contents.mime
3 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Object_1generic__loader_8_81.0/Contents.dir/Contents.modtimes:
--------------------------------------------------------------------------------
1 | {
2 | "hdaroot.def":1734981838
3 | }
4 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Object_1generic__loader_8_81.0/Contents.dir/Contents.createtimes:
--------------------------------------------------------------------------------
1 | {
2 | "hdaroot.def":1734981795
3 | }
4 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Lop_1generic__loader_8_81.0/Contents.dir/Contents.createtimes:
--------------------------------------------------------------------------------
1 | {
2 | "hdaroot/output0.def":1734470573,
3 | "hdaroot.def":1734980606
4 | }
5 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Lop_1generic__loader_8_81.0/Contents.dir/Contents.modtimes:
--------------------------------------------------------------------------------
1 | {
2 | "hdaroot/output0.def":1734470586,
3 | "hdaroot.def":1734981657
4 | }
5 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Sop_1generic__loader_8_81.0/Contents.dir/Contents.createtimes:
--------------------------------------------------------------------------------
1 | {
2 | "hdaroot/output0.def":1728474331,
3 | "hdaroot.def":1734981830
4 | }
5 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Sop_1generic__loader_8_81.0/Contents.dir/Contents.modtimes:
--------------------------------------------------------------------------------
1 | {
2 | "hdaroot/output0.def":1728474347,
3 | "hdaroot.def":1734981871
4 | }
5 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_import.hda/Sections.list:
--------------------------------------------------------------------------------
1 | ""
2 | INDEX__SECTION INDEX_SECTION
3 | houdini.hdalibrary houdini.hdalibrary
4 | ayon_8_8Lop_1lop__import_8_81.0 ayon::Lop/lop_import::1.0
5 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_load_shot.hda/Sections.list:
--------------------------------------------------------------------------------
1 | ""
2 | INDEX__SECTION INDEX_SECTION
3 | houdini.hdalibrary houdini.hdalibrary
4 | ayon_8_8Lop_1load__shot_8_81.0 ayon::Lop/load_shot::1.0
5 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_mute_layers.hda/Sections.list:
--------------------------------------------------------------------------------
1 | ""
2 | INDEX__SECTION INDEX_SECTION
3 | houdini.hdalibrary houdini.hdalibrary
4 | ayon_8_8Lop_1mute__layers_8_81.0 ayon::Lop/mute_layers::1.0
5 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Lop_1generic__loader_8_81.0/Contents.dir/Contents.houdini_versions:
--------------------------------------------------------------------------------
1 | {
2 | "values":["20.5.370"
3 | ],
4 | "indexes":{
5 | "hdaroot/output0.userdata":0
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Sop_1generic__loader_8_81.0/Contents.dir/Contents.houdini_versions:
--------------------------------------------------------------------------------
1 | {
2 | "values":["20.5.370"
3 | ],
4 | "indexes":{
5 | "hdaroot/output0.userdata":0
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_import.hda/ayon_8_8Lop_1lop__import_8_81.0/IconImage:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ynput/ayon-houdini/HEAD/client/ayon_houdini/startup/otls/ayon_lop_import.hda/ayon_8_8Lop_1lop__import_8_81.0/IconImage
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_import.hda/ayon_8_8Lop_1lop__import_8_81.0/OnCreated:
--------------------------------------------------------------------------------
1 | from ayon_houdini.api.lib import remove_all_thumbnails
2 |
3 |
4 | # Clear thumbnails
5 | node = kwargs["node"]
6 | remove_all_thumbnails(node)
7 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_load_shot.hda/ayon_8_8Lop_1load__shot_8_81.0/IconImage:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ynput/ayon-houdini/HEAD/client/ayon_houdini/startup/otls/ayon_lop_load_shot.hda/ayon_8_8Lop_1load__shot_8_81.0/IconImage
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_load_shot.hda/ayon_8_8Lop_1load__shot_8_81.0/OnDeleted:
--------------------------------------------------------------------------------
1 | from ayon_houdini.api.lib import remove_all_thumbnails
2 |
3 |
4 | # Clear thumbnails
5 | node = kwargs["node"]
6 | remove_all_thumbnails(node)
7 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Lop_1generic__loader_8_81.0/OnDeleted:
--------------------------------------------------------------------------------
1 | from ayon_houdini.api.lib import remove_all_thumbnails
2 |
3 |
4 | # Clear thumbnails
5 | node = kwargs["node"]
6 | remove_all_thumbnails(node)
7 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Sop_1generic__loader_8_81.0/OnDeleted:
--------------------------------------------------------------------------------
1 | from ayon_houdini.api.lib import remove_all_thumbnails
2 |
3 |
4 | # Clear thumbnails
5 | node = kwargs["node"]
6 | remove_all_thumbnails(node)
7 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_import.hda/ayon_8_8Lop_1lop__import_8_81.0/AYON__icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ynput/ayon-houdini/HEAD/client/ayon_houdini/startup/otls/ayon_lop_import.hda/ayon_8_8Lop_1lop__import_8_81.0/AYON__icon.png
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Lop_1generic__loader_8_81.0/IconImage:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ynput/ayon-houdini/HEAD/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Lop_1generic__loader_8_81.0/IconImage
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Object_1generic__loader_8_81.0/OnDeleted:
--------------------------------------------------------------------------------
1 | from ayon_houdini.api.lib import remove_all_thumbnails
2 |
3 |
4 | # Clear thumbnails
5 | node = kwargs["node"]
6 | remove_all_thumbnails(node)
7 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Sop_1generic__loader_8_81.0/IconImage:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ynput/ayon-houdini/HEAD/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Sop_1generic__loader_8_81.0/IconImage
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_load_shot.hda/ayon_8_8Lop_1load__shot_8_81.0/AYON__icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ynput/ayon-houdini/HEAD/client/ayon_houdini/startup/otls/ayon_lop_load_shot.hda/ayon_8_8Lop_1load__shot_8_81.0/AYON__icon.png
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_mute_layers.hda/ayon_8_8Lop_1mute__layers_8_81.0/IconImage:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ynput/ayon-houdini/HEAD/client/ayon_houdini/startup/otls/ayon_lop_mute_layers.hda/ayon_8_8Lop_1mute__layers_8_81.0/IconImage
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Object_1generic__loader_8_81.0/IconImage:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ynput/ayon-houdini/HEAD/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Object_1generic__loader_8_81.0/IconImage
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_import.hda/ayon_8_8Lop_1lop__import_8_81.0/InternalFileOptions:
--------------------------------------------------------------------------------
1 | {
2 | "nodeconntype":{
3 | "type":"bool",
4 | "value":false
5 | },
6 | "nodeparmtype":{
7 | "type":"bool",
8 | "value":false
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_mute_layers.hda/ayon_8_8Lop_1mute__layers_8_81.0/AYON__icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ynput/ayon-houdini/HEAD/client/ayon_houdini/startup/otls/ayon_lop_mute_layers.hda/ayon_8_8Lop_1mute__layers_8_81.0/AYON__icon.png
--------------------------------------------------------------------------------
/client/ayon_houdini/__init__.py:
--------------------------------------------------------------------------------
1 | from .version import __version__
2 | from .addon import (
3 | HoudiniAddon,
4 | HOUDINI_HOST_DIR,
5 | )
6 |
7 |
8 | __all__ = (
9 | "__version__",
10 |
11 | "HoudiniAddon",
12 | "HOUDINI_HOST_DIR",
13 | )
14 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_load_shot.hda/ayon_8_8Lop_1load__shot_8_81.0/InternalFileOptions:
--------------------------------------------------------------------------------
1 | {
2 | "nodeconntype":{
3 | "type":"bool",
4 | "value":false
5 | },
6 | "nodeparmtype":{
7 | "type":"bool",
8 | "value":false
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Lop_1generic__loader_8_81.0/InternalFileOptions:
--------------------------------------------------------------------------------
1 | {
2 | "nodeconntype":{
3 | "type":"bool",
4 | "value":false
5 | },
6 | "nodeparmtype":{
7 | "type":"bool",
8 | "value":false
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Sop_1generic__loader_8_81.0/InternalFileOptions:
--------------------------------------------------------------------------------
1 | {
2 | "nodeconntype":{
3 | "type":"bool",
4 | "value":false
5 | },
6 | "nodeparmtype":{
7 | "type":"bool",
8 | "value":false
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_import.hda/ayon_8_8Lop_1lop__import_8_81.0/Contents.dir/.OPdummydefs:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ynput/ayon-houdini/HEAD/client/ayon_houdini/startup/otls/ayon_lop_import.hda/ayon_8_8Lop_1lop__import_8_81.0/Contents.dir/.OPdummydefs
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_load_shot.hda/ayon_8_8Lop_1load__shot_8_81.0/Contents.dir/.OPdummydefs:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ynput/ayon-houdini/HEAD/client/ayon_houdini/startup/otls/ayon_lop_load_shot.hda/ayon_8_8Lop_1load__shot_8_81.0/Contents.dir/.OPdummydefs
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_mute_layers.hda/ayon_8_8Lop_1mute__layers_8_81.0/InternalFileOptions:
--------------------------------------------------------------------------------
1 | {
2 | "nodeconntype":{
3 | "type":"bool",
4 | "value":false
5 | },
6 | "nodeparmtype":{
7 | "type":"bool",
8 | "value":false
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Object_1generic__loader_8_81.0/InternalFileOptions:
--------------------------------------------------------------------------------
1 | {
2 | "nodeconntype":{
3 | "type":"bool",
4 | "value":false
5 | },
6 | "nodeparmtype":{
7 | "type":"bool",
8 | "value":false
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_import.hda/ayon_8_8Lop_1lop__import_8_81.0/Contents.dir/.OPfallbacks:
--------------------------------------------------------------------------------
1 | ayon::Lop/generic_loader::1.0 E:/Ynput/ayon-houdini/client/ayon_houdini/startup/otls/ayon.generic_loader.hda
2 | ayon::Lop/generic_loader::1.0 otls/ayon.generic_loader.hda
3 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_load_shot.hda/ayon_8_8Lop_1load__shot_8_81.0/Contents.dir/.OPfallbacks:
--------------------------------------------------------------------------------
1 | ayon::Lop/generic_loader::1.0 E:/Ynput/ayon-houdini/client/ayon_houdini/startup/otls/ayon.generic_loader.hda
2 | ayon::Lop/generic_loader::1.0 otls/ayon.generic_loader.hda
3 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_mute_layers.hda/ayon_8_8Lop_1mute__layers_8_81.0/Contents.dir/Contents.modtimes:
--------------------------------------------------------------------------------
1 | {
2 | "hdaroot/mute_layers.def":1722285992,
3 | "hdaroot/get_layers.def":1722286006,
4 | "hdaroot/output0.def":1722285928,
5 | "hdaroot.def":1722286039
6 | }
7 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_mute_layers.hda/ayon_8_8Lop_1mute__layers_8_81.0/Contents.dir/Contents.createtimes:
--------------------------------------------------------------------------------
1 | {
2 | "hdaroot/mute_layers.def":1708351767,
3 | "hdaroot/get_layers.def":1708351772,
4 | "hdaroot/output0.def":1708352522,
5 | "hdaroot.def":1722285625
6 | }
7 |
--------------------------------------------------------------------------------
/mkdocs_requirements.txt:
--------------------------------------------------------------------------------
1 | mkdocs-material >= 9.6.7
2 | mkdocs-autoapi >= 0.4.0
3 | mkdocstrings-python >= 1.16.2
4 | mkdocs-minify-plugin >= 0.8.0
5 | markdown-checklist >= 0.4.4
6 | mdx-gh-links >= 0.4
7 | pymdown-extensions >= 10.14.3
8 | mike >= 2.1.3
9 | mkdocstrings-shell >= 1.0.2
10 |
--------------------------------------------------------------------------------
/server/settings/__init__.py:
--------------------------------------------------------------------------------
1 | from .main import (
2 | HoudiniSettings,
3 | DEFAULT_VALUES,
4 | )
5 | from .conversion import convert_settings_overrides
6 |
7 |
8 | __all__ = (
9 | "HoudiniSettings",
10 | "DEFAULT_VALUES",
11 |
12 | "convert_settings_overrides",
13 | )
14 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_import.hda/ayon_8_8Lop_1lop__import_8_81.0/OnNameChanged:
--------------------------------------------------------------------------------
1 | from ayon_houdini.api.hda_utils import (
2 | keep_background_images_linked
3 | )
4 |
5 |
6 | node = kwargs["node"]
7 | old_name = kwargs["old_name"]
8 | keep_background_images_linked(node, old_name)
9 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_load_shot.hda/ayon_8_8Lop_1load__shot_8_81.0/OnNameChanged:
--------------------------------------------------------------------------------
1 | from ayon_houdini.api.hda_utils import (
2 | keep_background_images_linked
3 | )
4 |
5 |
6 | node = kwargs["node"]
7 | old_name = kwargs["old_name"]
8 | keep_background_images_linked(node, old_name)
9 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Lop_1generic__loader_8_81.0/OnNameChanged:
--------------------------------------------------------------------------------
1 | from ayon_houdini.api.hda_utils import (
2 | keep_background_images_linked
3 | )
4 |
5 |
6 | node = kwargs["node"]
7 | old_name = kwargs["old_name"]
8 | keep_background_images_linked(node, old_name)
9 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Object_1generic__loader_8_81.0/OnNameChanged:
--------------------------------------------------------------------------------
1 | from ayon_houdini.api.hda_utils import (
2 | keep_background_images_linked
3 | )
4 |
5 |
6 | node = kwargs["node"]
7 | old_name = kwargs["old_name"]
8 | keep_background_images_linked(node, old_name)
9 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Sop_1generic__loader_8_81.0/OnNameChanged:
--------------------------------------------------------------------------------
1 | from ayon_houdini.api.hda_utils import (
2 | keep_background_images_linked
3 | )
4 |
5 |
6 | node = kwargs["node"]
7 | old_name = kwargs["old_name"]
8 | keep_background_images_linked(node, old_name)
9 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_mute_layers.hda/ayon_8_8Lop_1mute__layers_8_81.0/Contents.dir/Contents.houdini_versions:
--------------------------------------------------------------------------------
1 | {
2 | "values":["20.0.724"
3 | ],
4 | "indexes":{
5 | "hdaroot/mute_layers.userdata":0,
6 | "hdaroot/get_layers.userdata":0,
7 | "hdaroot/output0.userdata":0
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_import.hda/ayon_8_8Lop_1lop__import_8_81.0/Contents.dir/Contents.houdini_versions:
--------------------------------------------------------------------------------
1 | {
2 | "values":["20.5.370"
3 | ],
4 | "indexes":{
5 | "hdaroot/output0.userdata":0,
6 | "hdaroot/reference.userdata":0,
7 | "hdaroot/warn_no_representation_set.userdata":0
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_import.hda/ayon_8_8Lop_1lop__import_8_81.0/PythonModule:
--------------------------------------------------------------------------------
1 | from ayon_houdini.api.hda_utils import (
2 | on_thumbnail_show_changed,
3 | on_thumbnail_size_changed,
4 | update_thumbnail,
5 | setup_flag_changed_callback,
6 | ensure_loader_expression_parm_defaults,
7 | )
8 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_load_shot.hda/ayon_8_8Lop_1load__shot_8_81.0/Contents.dir/Contents.houdini_versions:
--------------------------------------------------------------------------------
1 | {
2 | "values":["20.5.370"
3 | ],
4 | "indexes":{
5 | "hdaroot/output0.userdata":0,
6 | "hdaroot/sublayer.userdata":0,
7 | "hdaroot/warn_no_representation_set.userdata":0
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_load_shot.hda/ayon_8_8Lop_1load__shot_8_81.0/PythonModule:
--------------------------------------------------------------------------------
1 | from ayon_houdini.api.hda_utils import (
2 | on_thumbnail_show_changed,
3 | on_thumbnail_size_changed,
4 | update_thumbnail,
5 | setup_flag_changed_callback,
6 | ensure_loader_expression_parm_defaults
7 | )
8 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/python2.7libs/pythonrc.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """AYON startup script."""
3 | from ayon_core.pipeline import install_host
4 | from ayon_houdini.api import HoudiniHost
5 |
6 |
7 | def main():
8 | print("Installing AYON ...")
9 | install_host(HoudiniHost())
10 |
11 |
12 | main()
13 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/python3.7libs/pythonrc.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """AYON startup script."""
3 | from ayon_core.pipeline import install_host
4 | from ayon_houdini.api import HoudiniHost
5 |
6 |
7 | def main():
8 | print("Installing AYON ...")
9 | install_host(HoudiniHost())
10 |
11 |
12 | main()
13 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/python3.9libs/pythonrc.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """AYON startup script."""
3 | from ayon_core.pipeline import install_host
4 | from ayon_houdini.api import HoudiniHost
5 |
6 |
7 | def main():
8 | print("Installing AYON ...")
9 | install_host(HoudiniHost())
10 |
11 |
12 | main()
13 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_import.hda/ayon_8_8Lop_1lop__import_8_81.0/Contents.dir/Contents.modtimes:
--------------------------------------------------------------------------------
1 | {
2 | "hdaroot/warn_no_representation_set.def":1734980176,
3 | "hdaroot/output0.def":1734617342,
4 | "hdaroot.def":1740008861,
5 | "hdaroot/generic_loader.def":1734981661,
6 | "hdaroot/reference.def":1740008026
7 | }
8 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_load_shot.hda/ayon_8_8Lop_1load__shot_8_81.0/Contents.dir/Contents.modtimes:
--------------------------------------------------------------------------------
1 | {
2 | "hdaroot/sublayer.def":1740008062,
3 | "hdaroot/warn_no_representation_set.def":1729552277,
4 | "hdaroot/output0.def":1729552203,
5 | "hdaroot.def":1740008070,
6 | "hdaroot/generic_loader.def":1734981661
7 | }
8 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/python3.10libs/pythonrc.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """AYON startup script."""
3 | from ayon_core.pipeline import install_host
4 | from ayon_houdini.api import HoudiniHost
5 |
6 |
7 | def main():
8 | print("Installing AYON ...")
9 | install_host(HoudiniHost())
10 |
11 |
12 | main()
13 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/python3.11libs/pythonrc.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """AYON startup script."""
3 | from ayon_core.pipeline import install_host
4 | from ayon_houdini.api import HoudiniHost
5 |
6 |
7 | def main():
8 | print("Installing AYON ...")
9 | install_host(HoudiniHost())
10 |
11 |
12 | main()
13 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_import.hda/ayon_8_8Lop_1lop__import_8_81.0/Contents.dir/Contents.createtimes:
--------------------------------------------------------------------------------
1 | {
2 | "hdaroot/warn_no_representation_set.def":1708980551,
3 | "hdaroot/output0.def":1698215383,
4 | "hdaroot.def":1740007734,
5 | "hdaroot/generic_loader.def":1734471755,
6 | "hdaroot/reference.def":1698150558
7 | }
8 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_load_shot.hda/ayon_8_8Lop_1load__shot_8_81.0/Contents.dir/Contents.createtimes:
--------------------------------------------------------------------------------
1 | {
2 | "hdaroot/sublayer.def":1720045839,
3 | "hdaroot/warn_no_representation_set.def":1708980551,
4 | "hdaroot/output0.def":1698215383,
5 | "hdaroot.def":1740007739,
6 | "hdaroot/generic_loader.def":1734531150
7 | }
8 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/Sections.list:
--------------------------------------------------------------------------------
1 | ""
2 | INDEX__SECTION INDEX_SECTION
3 | houdini.hdalibrary houdini.hdalibrary
4 | ayon_8_8Object_1generic__loader_8_81.0 ayon::Object/generic_loader::1.0
5 | ayon_8_8Sop_1generic__loader_8_81.0 ayon::Sop/generic_loader::1.0
6 | ayon_8_8Lop_1generic__loader_8_81.0 ayon::Lop/generic_loader::1.0
7 |
--------------------------------------------------------------------------------
/package.py:
--------------------------------------------------------------------------------
1 | name = "houdini"
2 | title = "Houdini"
3 | version = "0.9.4+dev"
4 | app_host_name = "houdini"
5 | client_dir = "ayon_houdini"
6 | project_can_override_addon_version = True
7 |
8 | ayon_server_version = ">=1.1.2"
9 | ayon_required_addons = {
10 | "core": ">=1.6.11",
11 | }
12 | ayon_compatible_addons = {
13 | "deadline": ">=0.5.20",
14 | }
15 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_load_shot.hda/ayon_8_8Lop_1load__shot_8_81.0/Help:
--------------------------------------------------------------------------------
1 | = AYON Load Shot =
2 |
3 | #icon: opdef:/ayon::Lop/load_shot::1.0?AYON_icon.png
4 |
5 | """Sublayers a USD file, usually a shot."""
6 |
7 | == Overview ==
8 |
9 | *Sublayers* a USD file into the current stage, by default targeting the root layer stack.
10 |
11 | @related
12 |
13 | * [Node:lop/sublayer]
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_mute_layers.hda/ayon_8_8Lop_1mute__layers_8_81.0/Sections.list:
--------------------------------------------------------------------------------
1 | ""
2 | DialogScript DialogScript
3 | CreateScript CreateScript
4 | InternalFileOptions InternalFileOptions
5 | Contents.gz Contents.gz
6 | TypePropertiesOptions TypePropertiesOptions
7 | Help Help
8 | Tools.shelf Tools.shelf
9 | IconImage IconImage
10 | ExtraFileOptions ExtraFileOptions
11 | AYON__icon.png AYON_icon.png
12 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_import.hda/ayon_8_8Lop_1lop__import_8_81.0/TypePropertiesOptions:
--------------------------------------------------------------------------------
1 | CheckExternal := 1;
2 | ContentsCompressionType := 1;
3 | ForbidOutsideParms := 1;
4 | GzipContents := 1;
5 | LockContents := 1;
6 | MakeDefault := 1;
7 | ParmsFromVfl := 0;
8 | PrefixDroppedParmLabel := 0;
9 | PrefixDroppedParmName := 0;
10 | SaveCachedCode := 0;
11 | SaveIcon := 1;
12 | SaveSpareParms := 0;
13 | UnlockOnCreate := 0;
14 | UseDSParms := 1;
15 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_load_shot.hda/ayon_8_8Lop_1load__shot_8_81.0/TypePropertiesOptions:
--------------------------------------------------------------------------------
1 | CheckExternal := 1;
2 | ContentsCompressionType := 1;
3 | ForbidOutsideParms := 1;
4 | GzipContents := 1;
5 | LockContents := 1;
6 | MakeDefault := 1;
7 | ParmsFromVfl := 0;
8 | PrefixDroppedParmLabel := 0;
9 | PrefixDroppedParmName := 0;
10 | SaveCachedCode := 0;
11 | SaveIcon := 1;
12 | SaveSpareParms := 0;
13 | UnlockOnCreate := 0;
14 | UseDSParms := 1;
15 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Lop_1generic__loader_8_81.0/PythonModule:
--------------------------------------------------------------------------------
1 | from ayon_houdini.api.hda_utils import (
2 | on_thumbnail_show_changed,
3 | on_thumbnail_size_changed,
4 | update_thumbnail,
5 | setup_flag_changed_callback,
6 | get_available_versions_with_labels,
7 | get_available_representations_with_labels,
8 | select_product_name,
9 | set_to_latest_version,
10 | expression_clear_cache
11 | )
12 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Lop_1generic__loader_8_81.0/TypePropertiesOptions:
--------------------------------------------------------------------------------
1 | CheckExternal := 1;
2 | ContentsCompressionType := 1;
3 | ForbidOutsideParms := 1;
4 | GzipContents := 1;
5 | LockContents := 1;
6 | MakeDefault := 1;
7 | ParmsFromVfl := 0;
8 | PrefixDroppedParmLabel := 0;
9 | PrefixDroppedParmName := 0;
10 | SaveCachedCode := 0;
11 | SaveIcon := 1;
12 | SaveSpareParms := 0;
13 | UnlockOnCreate := 0;
14 | UseDSParms := 1;
15 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Sop_1generic__loader_8_81.0/PythonModule:
--------------------------------------------------------------------------------
1 | from ayon_houdini.api.hda_utils import (
2 | on_thumbnail_show_changed,
3 | on_thumbnail_size_changed,
4 | update_thumbnail,
5 | setup_flag_changed_callback,
6 | get_available_versions_with_labels,
7 | get_available_representations_with_labels,
8 | select_product_name,
9 | set_to_latest_version,
10 | expression_clear_cache
11 | )
12 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Sop_1generic__loader_8_81.0/TypePropertiesOptions:
--------------------------------------------------------------------------------
1 | CheckExternal := 1;
2 | ContentsCompressionType := 1;
3 | ForbidOutsideParms := 1;
4 | GzipContents := 1;
5 | LockContents := 1;
6 | MakeDefault := 1;
7 | ParmsFromVfl := 0;
8 | PrefixDroppedParmLabel := 0;
9 | PrefixDroppedParmName := 0;
10 | SaveCachedCode := 0;
11 | SaveIcon := 1;
12 | SaveSpareParms := 0;
13 | UnlockOnCreate := 0;
14 | UseDSParms := 1;
15 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_mute_layers.hda/ayon_8_8Lop_1mute__layers_8_81.0/TypePropertiesOptions:
--------------------------------------------------------------------------------
1 | CheckExternal := 1;
2 | ContentsCompressionType := 1;
3 | ForbidOutsideParms := 1;
4 | GzipContents := 1;
5 | LockContents := 1;
6 | MakeDefault := 1;
7 | ParmsFromVfl := 0;
8 | PrefixDroppedParmLabel := 0;
9 | PrefixDroppedParmName := 0;
10 | SaveCachedCode := 0;
11 | SaveIcon := 1;
12 | SaveSpareParms := 0;
13 | UnlockOnCreate := 0;
14 | UseDSParms := 1;
15 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Object_1generic__loader_8_81.0/PythonModule:
--------------------------------------------------------------------------------
1 | from ayon_houdini.api.hda_utils import (
2 | on_thumbnail_show_changed,
3 | on_thumbnail_size_changed,
4 | update_thumbnail,
5 | setup_flag_changed_callback,
6 | get_available_versions_with_labels,
7 | get_available_representations_with_labels,
8 | select_product_name,
9 | set_to_latest_version,
10 | expression_clear_cache
11 | )
12 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Object_1generic__loader_8_81.0/TypePropertiesOptions:
--------------------------------------------------------------------------------
1 | CheckExternal := 1;
2 | ContentsCompressionType := 1;
3 | ForbidOutsideParms := 1;
4 | GzipContents := 1;
5 | LockContents := 1;
6 | MakeDefault := 1;
7 | ParmsFromVfl := 0;
8 | PrefixDroppedParmLabel := 0;
9 | PrefixDroppedParmName := 0;
10 | SaveCachedCode := 0;
11 | SaveIcon := 1;
12 | SaveSpareParms := 0;
13 | UnlockOnCreate := 0;
14 | UseDSParms := 1;
15 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_mute_layers.hda/ayon_8_8Lop_1mute__layers_8_81.0/CreateScript:
--------------------------------------------------------------------------------
1 | # Automatically generated script
2 | \set noalias = 1
3 | #
4 | # Creation script for ayon::mute_layers::1.0 operator
5 | #
6 |
7 | if ( "$arg1" == "" ) then
8 | echo This script is intended as a creation script
9 | exit
10 | endif
11 |
12 | # Node $arg1 (ayon::Lop/mute_layers::1.0)
13 | opexprlanguage -s hscript $arg1
14 | opuserdata -n '___Version___' -v '' $arg1
15 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/collect_workscene_fps.py:
--------------------------------------------------------------------------------
1 | import hou
2 | import pyblish.api
3 | from ayon_houdini.api import plugin
4 |
5 |
6 | class CollectWorksceneFPS(plugin.HoudiniContextPlugin):
7 | """Get the FPS of the work scene."""
8 |
9 | label = "Workscene FPS"
10 | order = pyblish.api.CollectorOrder
11 |
12 | def process(self, context):
13 | fps = hou.fps()
14 | self.log.info("Workscene FPS: %s" % fps)
15 | context.data.update({"fps": fps})
16 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_import.hda/INDEX__SECTION:
--------------------------------------------------------------------------------
1 | Operator: ayon::lop_import::1.0
2 | Label: AYON Load Asset
3 | Path: oplib:/ayon::Lop/lop_import::1.0?ayon::Lop/lop_import::1.0
4 | Icon: opdef:/ayon::Lop/lop_import::1.0?IconImage
5 | Table: Lop
6 | License:
7 | Extra:
8 | User:
9 | Inputs: 0 to 1
10 | Subnet: true
11 | Python: false
12 | Empty: false
13 | Modified: Thu Feb 20 01:40:21 2025
14 |
15 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_load_shot.hda/INDEX__SECTION:
--------------------------------------------------------------------------------
1 | Operator: ayon::load_shot::1.0
2 | Label: AYON Load Shot
3 | Path: oplib:/ayon::Lop/load_shot::1.0?ayon::Lop/load_shot::1.0
4 | Icon: opdef:/ayon::Lop/load_shot::1.0?IconImage
5 | Table: Lop
6 | License:
7 | Extra:
8 | User:
9 | Inputs: 0 to 1
10 | Subnet: true
11 | Python: false
12 | Empty: false
13 | Modified: Thu Feb 20 01:34:18 2025
14 |
15 |
--------------------------------------------------------------------------------
/docs/css/custom.css:
--------------------------------------------------------------------------------
1 | [data-md-color-scheme="slate"] {
2 | /* simple slate overrides */
3 | --md-primary-fg-color: hsl(155, 49%, 50%);
4 | --md-accent-fg-color: rgb(93, 200, 156);
5 | --md-typeset-a-color: hsl(155, 49%, 45%) !important;
6 | }
7 | [data-md-color-scheme="default"] {
8 | /* simple default overrides */
9 | --md-primary-fg-color: hsl(155, 49%, 50%);
10 | --md-accent-fg-color: rgb(93, 200, 156);
11 | --md-typeset-a-color: hsl(155, 49%, 45%) !important;
12 | }
13 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_mute_layers.hda/INDEX__SECTION:
--------------------------------------------------------------------------------
1 | Operator: ayon::mute_layers::1.0
2 | Label: AYON Mute Layers
3 | Path: oplib:/ayon::Lop/mute_layers::1.0?ayon::Lop/mute_layers::1.0
4 | Icon: opdef:/ayon::Lop/mute_layers::1.0?IconImage
5 | Table: Lop
6 | License:
7 | Extra:
8 | User:
9 | Inputs: 1 to 1
10 | Subnet: true
11 | Python: false
12 | Empty: false
13 | Modified: Mon Jul 29 22:47:28 2024
14 |
15 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Lop_1generic__loader_8_81.0/OnLoaded:
--------------------------------------------------------------------------------
1 | node = kwargs["node"]
2 | hda_module = node.hdaModule()
3 | hda_module.setup_flag_changed_callback(node)
4 |
5 |
6 | # Duplicate callback
7 | def on_duplicate():
8 | """Duplicate thumbnail on node duplicate"""
9 | if node.evalParm("show_thumbnail") and node.evalParm("representation"):
10 | hda_module.update_thumbnail(node)
11 |
12 |
13 | if not hou.hipFile.isLoadingHipFile():
14 | on_duplicate()
15 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Sop_1generic__loader_8_81.0/OnLoaded:
--------------------------------------------------------------------------------
1 | node = kwargs["node"]
2 | hda_module = node.hdaModule()
3 | hda_module.setup_flag_changed_callback(node)
4 |
5 |
6 | # Duplicate callback
7 | def on_duplicate():
8 | """Duplicate thumbnail on node duplicate"""
9 | if node.evalParm("show_thumbnail") and node.evalParm("representation"):
10 | hda_module.update_thumbnail(node)
11 |
12 |
13 | if not hou.hipFile.isLoadingHipFile():
14 | on_duplicate()
15 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_import.hda/ayon_8_8Lop_1lop__import_8_81.0/CreateScript:
--------------------------------------------------------------------------------
1 | # Automatically generated script
2 | \set noalias = 1
3 | #
4 | # Creation script for ayon::lop_import::1.0 operator
5 | #
6 |
7 | if ( "$arg1" == "" ) then
8 | echo This script is intended as a creation script
9 | exit
10 | endif
11 |
12 | # Node $arg1 (ayon::Lop/lop_import::1.0)
13 | opexprlanguage -s hscript $arg1
14 | opuserdata -n '___Version___' -v '' $arg1
15 | opuserdata -n 'wirestyle' -v 'rounded' $arg1
16 |
--------------------------------------------------------------------------------
/client/ayon_houdini/api/__init__.py:
--------------------------------------------------------------------------------
1 | from .pipeline import (
2 | HoudiniHost,
3 | ls,
4 | containerise
5 | )
6 |
7 | from .lib import (
8 | lsattr,
9 | lsattrs,
10 | read,
11 |
12 | maintained_selection
13 | )
14 |
15 | import hou
16 | hou.logging.createSource("AYON")
17 |
18 | __all__ = [
19 | "HoudiniHost",
20 |
21 | "ls",
22 | "containerise",
23 |
24 | # Utility functions
25 | "lsattr",
26 | "lsattrs",
27 | "read",
28 |
29 | "maintained_selection"
30 | ]
31 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Object_1generic__loader_8_81.0/OnLoaded:
--------------------------------------------------------------------------------
1 | node = kwargs["node"]
2 | hda_module = node.hdaModule()
3 | hda_module.setup_flag_changed_callback(node)
4 |
5 |
6 | # Duplicate callback
7 | def on_duplicate():
8 | """Duplicate thumbnail on node duplicate"""
9 | if node.evalParm("show_thumbnail") and node.evalParm("representation"):
10 | hda_module.update_thumbnail(node)
11 |
12 |
13 | if not hou.hipFile.isLoadingHipFile():
14 | on_duplicate()
15 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Lop_1generic__loader_8_81.0/Sections.list:
--------------------------------------------------------------------------------
1 | ""
2 | DialogScript DialogScript
3 | CreateScript CreateScript
4 | InternalFileOptions InternalFileOptions
5 | Contents.gz Contents.gz
6 | TypePropertiesOptions TypePropertiesOptions
7 | Tools.shelf Tools.shelf
8 | Help Help
9 | IconImage IconImage
10 | PythonModule PythonModule
11 | OnCreated OnCreated
12 | OnLoaded OnLoaded
13 | OnDeleted OnDeleted
14 | OnNameChanged OnNameChanged
15 | ExtraFileOptions ExtraFileOptions
16 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Object_1generic__loader_8_81.0/Sections.list:
--------------------------------------------------------------------------------
1 | ""
2 | DialogScript DialogScript
3 | CreateScript CreateScript
4 | InternalFileOptions InternalFileOptions
5 | Contents.gz Contents.gz
6 | TypePropertiesOptions TypePropertiesOptions
7 | Tools.shelf Tools.shelf
8 | Help Help
9 | IconImage IconImage
10 | PythonModule PythonModule
11 | OnCreated OnCreated
12 | OnLoaded OnLoaded
13 | OnDeleted OnDeleted
14 | OnNameChanged OnNameChanged
15 | ExtraFileOptions ExtraFileOptions
16 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Sop_1generic__loader_8_81.0/Sections.list:
--------------------------------------------------------------------------------
1 | ""
2 | DialogScript DialogScript
3 | CreateScript CreateScript
4 | InternalFileOptions InternalFileOptions
5 | Contents.gz Contents.gz
6 | TypePropertiesOptions TypePropertiesOptions
7 | Tools.shelf Tools.shelf
8 | Help Help
9 | IconImage IconImage
10 | PythonModule PythonModule
11 | OnCreated OnCreated
12 | OnLoaded OnLoaded
13 | OnDeleted OnDeleted
14 | OnNameChanged OnNameChanged
15 | ExtraFileOptions ExtraFileOptions
16 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Lop_1generic__loader_8_81.0/CreateScript:
--------------------------------------------------------------------------------
1 | # Automatically generated script
2 | \set noalias = 1
3 | #
4 | # Creation script for ayon::generic_loader::1.0 operator
5 | #
6 |
7 | if ( "$arg1" == "" ) then
8 | echo This script is intended as a creation script
9 | exit
10 | endif
11 |
12 | # Node $arg1 (ayon::Lop/generic_loader::1.0)
13 | opexprlanguage -s hscript $arg1
14 | opuserdata -n '___Version___' -v '' $arg1
15 | opuserdata -n 'nodeshape' -v 'null' $arg1
16 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Sop_1generic__loader_8_81.0/CreateScript:
--------------------------------------------------------------------------------
1 | # Automatically generated script
2 | \set noalias = 1
3 | #
4 | # Creation script for ayon::generic_loader::1.0 operator
5 | #
6 |
7 | if ( "$arg1" == "" ) then
8 | echo This script is intended as a creation script
9 | exit
10 | endif
11 |
12 | # Node $arg1 (ayon::Sop/generic_loader::1.0)
13 | opexprlanguage -s hscript $arg1
14 | opuserdata -n '___Version___' -v '' $arg1
15 | opuserdata -n 'nodeshape' -v 'null' $arg1
16 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Object_1generic__loader_8_81.0/CreateScript:
--------------------------------------------------------------------------------
1 | # Automatically generated script
2 | \set noalias = 1
3 | #
4 | # Creation script for ayon::generic_loader::1.0 operator
5 | #
6 |
7 | if ( "$arg1" == "" ) then
8 | echo This script is intended as a creation script
9 | exit
10 | endif
11 |
12 | # Node $arg1 (ayon::Object/generic_loader::1.0)
13 | opexprlanguage -s hscript $arg1
14 | opuserdata -n '___Version___' -v '' $arg1
15 | opuserdata -n 'nodeshape' -v 'null' $arg1
16 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_mute_layers.hda/ayon_8_8Lop_1mute__layers_8_81.0/ExtraFileOptions:
--------------------------------------------------------------------------------
1 | {
2 | "AYON_icon.png/Cursor":{
3 | "type":"intarray",
4 | "value":[0,0]
5 | },
6 | "AYON_icon.png/IsExpr":{
7 | "type":"bool",
8 | "value":false
9 | },
10 | "AYON_icon.png/IsPython":{
11 | "type":"bool",
12 | "value":false
13 | },
14 | "AYON_icon.png/IsScript":{
15 | "type":"bool",
16 | "value":false
17 | },
18 | "AYON_icon.png/Source":{
19 | "type":"string",
20 | "value":"AYON_icon.png"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/.github/workflows/validate_pr_labels.yml:
--------------------------------------------------------------------------------
1 | name: 🔎 Validate PR Labels
2 | on:
3 | pull_request:
4 | types:
5 | - opened
6 | - edited
7 | - labeled
8 | - unlabeled
9 |
10 | jobs:
11 | validate-type-label:
12 | uses: ynput/ops-repo-automation/.github/workflows/validate_pr_labels.yml@main
13 | with:
14 | repo: "${{ github.repository }}"
15 | pull_request_number: ${{ github.event.pull_request.number }}
16 | query_prefix: "type: "
17 | secrets:
18 | token: ${{ secrets.YNPUT_BOT_TOKEN }}
19 |
--------------------------------------------------------------------------------
/.github/workflows/deploy_mkdocs.yml:
--------------------------------------------------------------------------------
1 | name: Deploy MkDocs
2 |
3 | on:
4 | push:
5 | tags:
6 | - "*"
7 | workflow_dispatch:
8 |
9 | jobs:
10 | build-mk-docs:
11 | # FIXME: Update @develop to @main after `ops-repo-automation` release.
12 | uses: ynput/ops-repo-automation/.github/workflows/deploy_mkdocs.yml@develop
13 | with:
14 | repo: ${{ github.repository }}
15 | secrets:
16 | YNPUT_BOT_TOKEN: ${{ secrets.YNPUT_BOT_TOKEN }}
17 | CI_USER: ${{ secrets.CI_USER }}
18 | CI_EMAIL: ${{ secrets.CI_EMAIL }}
19 |
--------------------------------------------------------------------------------
/.github/workflows/pr_linting.yml:
--------------------------------------------------------------------------------
1 | name: 📇 Code Linting
2 |
3 | on:
4 | push:
5 | branches: [ develop ]
6 | pull_request:
7 | branches: [ develop ]
8 |
9 | workflow_dispatch:
10 |
11 | concurrency:
12 | group: ${{ github.workflow }}-${{ github.event.pull_request.number}}
13 | cancel-in-progress: true
14 |
15 | permissions:
16 | contents: read
17 | pull-requests: write
18 |
19 | jobs:
20 | linting:
21 | runs-on: ubuntu-latest
22 | steps:
23 | - uses: actions/checkout@v4
24 | - uses: chartboost/ruff-action@v1
25 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_import.hda/ayon_8_8Lop_1lop__import_8_81.0/Sections.list:
--------------------------------------------------------------------------------
1 | ""
2 | DialogScript DialogScript
3 | CreateScript CreateScript
4 | InternalFileOptions InternalFileOptions
5 | Contents.gz Contents.gz
6 | TypePropertiesOptions TypePropertiesOptions
7 | Tools.shelf Tools.shelf
8 | Help Help
9 | IconImage IconImage
10 | MessageNodes MessageNodes
11 | PythonModule PythonModule
12 | OnCreated OnCreated
13 | OnNameChanged OnNameChanged
14 | OnLoaded OnLoaded
15 | ExtraFileOptions ExtraFileOptions
16 | AYON__icon.png AYON_icon.png
17 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_load_shot.hda/ayon_8_8Lop_1load__shot_8_81.0/CreateScript:
--------------------------------------------------------------------------------
1 | # Automatically generated script
2 | \set noalias = 1
3 | #
4 | # Creation script for ayon::load_shot::1.0 operator
5 | #
6 |
7 | if ( "$arg1" == "" ) then
8 | echo This script is intended as a creation script
9 | exit
10 | endif
11 |
12 | # Node $arg1 (ayon::Lop/load_shot::1.0)
13 | opexprlanguage -s hscript $arg1
14 | opuserdata -n '___Version___' -v '' $arg1
15 | opuserdata -n 'nodeshape' -v 'bulge_down' $arg1
16 | opuserdata -n 'wirestyle' -v 'rounded' $arg1
17 |
--------------------------------------------------------------------------------
/.github/workflows/upload_to_ynput_cloud.yml:
--------------------------------------------------------------------------------
1 | name: 📤 Upload to Ynput Cloud
2 |
3 | on:
4 | workflow_dispatch:
5 | release:
6 | types: [published]
7 |
8 | jobs:
9 | call-upload-to-ynput-cloud:
10 | uses: ynput/ops-repo-automation/.github/workflows/upload_to_ynput_cloud.yml@main
11 | secrets:
12 | CI_EMAIL: ${{ secrets.CI_EMAIL }}
13 | CI_USER: ${{ secrets.CI_USER }}
14 | YNPUT_BOT_TOKEN: ${{ secrets.YNPUT_BOT_TOKEN }}
15 | YNPUT_CLOUD_URL: ${{ secrets.YNPUT_CLOUD_URL }}
16 | YNPUT_CLOUD_TOKEN: ${{ secrets.YNPUT_CLOUD_TOKEN }}
17 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_import.hda/ayon_8_8Lop_1lop__import_8_81.0/OnLoaded:
--------------------------------------------------------------------------------
1 | node = kwargs["node"]
2 | hda_module = node.hdaModule()
3 | hda_module.setup_flag_changed_callback(node)
4 |
5 |
6 | # Duplicate callback
7 | def on_duplicate():
8 | """Duplicate thumbnail on node duplicate"""
9 | if node.evalParm("show_thumbnail") and node.evalParm("representation"):
10 | hda_module.update_thumbnail(node)
11 |
12 |
13 | if not hou.hipFile.isLoadingHipFile():
14 | on_duplicate()
15 | else:
16 | hda_module.ensure_loader_expression_parm_defaults(node)
17 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_load_shot.hda/ayon_8_8Lop_1load__shot_8_81.0/OnLoaded:
--------------------------------------------------------------------------------
1 | node = kwargs["node"]
2 | hda_module = node.hdaModule()
3 | hda_module.setup_flag_changed_callback(node)
4 |
5 |
6 | # Duplicate callback
7 | def on_duplicate():
8 | """Duplicate thumbnail on node duplicate"""
9 | if node.evalParm("show_thumbnail") and node.evalParm("representation"):
10 | hda_module.update_thumbnail(node)
11 |
12 |
13 | if not hou.hipFile.isLoadingHipFile():
14 | on_duplicate()
15 | else:
16 | hda_module.ensure_loader_expression_parm_defaults(node)
17 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_load_shot.hda/ayon_8_8Lop_1load__shot_8_81.0/Sections.list:
--------------------------------------------------------------------------------
1 | ""
2 | DialogScript DialogScript
3 | CreateScript CreateScript
4 | InternalFileOptions InternalFileOptions
5 | Contents.gz Contents.gz
6 | TypePropertiesOptions TypePropertiesOptions
7 | Tools.shelf Tools.shelf
8 | Help Help
9 | IconImage IconImage
10 | MessageNodes MessageNodes
11 | PythonModule PythonModule
12 | OnDeleted OnDeleted
13 | OnNameChanged OnNameChanged
14 | OnLoaded OnLoaded
15 | OnCreated OnCreated
16 | ExtraFileOptions ExtraFileOptions
17 | AYON__icon.png AYON_icon.png
18 |
--------------------------------------------------------------------------------
/client/ayon_houdini/hooks/set_paths.py:
--------------------------------------------------------------------------------
1 | from ayon_applications import PreLaunchHook, LaunchTypes
2 |
3 |
4 | class SetPath(PreLaunchHook):
5 | """Set current dir to workdir.
6 |
7 | Hook `GlobalHostDataHook` must be executed before this hook.
8 | """
9 | app_groups = {"houdini"}
10 | launch_types = {LaunchTypes.local}
11 |
12 | def execute(self):
13 | workdir = self.launch_context.env.get("AYON_WORKDIR", "")
14 | if not workdir:
15 | self.log.warning("BUG: Workdir is not filled.")
16 | return
17 |
18 | self.launch_context.kwargs["cwd"] = workdir
19 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_mute_layers.hda/ayon_8_8Lop_1mute__layers_8_81.0/Help:
--------------------------------------------------------------------------------
1 | = AYON Mute Layers =
2 |
3 | #icon: opdef:/ayon::Lop/mute_layers::1.0?AYON_icon.png
4 |
5 | """Mute USD layers by wildcard path matching."""
6 |
7 | == Overview ==
8 |
9 | Sets used USD layers in the stage to be Muted downstream.
10 |
11 | For example using `*usdAsset_look*` this can ensure to mute all look related layers, or `*usdShot_lighting*` to do the same for lighting.
12 |
13 | The `pattern` parameter has a drop-down to easily set or disable often used presets.
14 |
15 | @parameters
16 |
17 |
18 | @related
19 |
20 | * [Node:lop/configurestage]
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_load_shot.hda/ayon_8_8Lop_1load__shot_8_81.0/OnCreated:
--------------------------------------------------------------------------------
1 | node = kwargs["node"]
2 | hda_module = node.hdaModule()
3 | hda_module.setup_flag_changed_callback(node)
4 |
5 | node.parm("file").lock(True)
6 |
7 | # Get attribute defaults from settings
8 | # TODO: Clean this up and re-use more from HDA utils lib
9 | from ayon_core.settings import get_current_project_settings
10 | settings = get_current_project_settings()
11 | load_settings = settings["houdini"].get("load", {}).get("LOPLoadShotLoader", {})
12 | use_ayon_entity_uri = load_settings.get("use_ayon_entity_uri", False)
13 | node.parm("use_ayon_entity_uri").set(use_ayon_entity_uri)
14 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Lop_1generic__loader_8_81.0/OnCreated:
--------------------------------------------------------------------------------
1 | node = kwargs["node"]
2 | hda_module = node.hdaModule()
3 | hda_module.setup_flag_changed_callback(node)
4 |
5 | node.parm("file").lock(True)
6 |
7 | # Get attribute defaults from settings
8 | # TODO: Clean this up and re-use more from HDA utils lib
9 | from ayon_core.settings import get_current_project_settings
10 | settings = get_current_project_settings()
11 | load_settings = settings["houdini"].get("load", {}).get("GenericLoader", {})
12 | use_ayon_entity_uri = load_settings.get("use_ayon_entity_uri", False)
13 | node.parm("use_ayon_entity_uri").set(use_ayon_entity_uri)
14 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Object_1generic__loader_8_81.0/OnCreated:
--------------------------------------------------------------------------------
1 | node = kwargs["node"]
2 | hda_module = node.hdaModule()
3 | hda_module.setup_flag_changed_callback(node)
4 |
5 | node.parm("file").lock(True)
6 |
7 | # Get attribute defaults from settings
8 | # TODO: Clean this up and re-use more from HDA utils lib
9 | from ayon_core.settings import get_current_project_settings
10 | settings = get_current_project_settings()
11 | load_settings = settings["houdini"].get("load", {}).get("GenericLoader", {})
12 | use_ayon_entity_uri = load_settings.get("use_ayon_entity_uri", False)
13 | node.parm("use_ayon_entity_uri").set(use_ayon_entity_uri)
14 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Sop_1generic__loader_8_81.0/OnCreated:
--------------------------------------------------------------------------------
1 | node = kwargs["node"]
2 | hda_module = node.hdaModule()
3 | hda_module.setup_flag_changed_callback(node)
4 |
5 | node.parm("file").lock(True)
6 |
7 | # Get attribute defaults from settings
8 | # TODO: Clean this up and re-use more from HDA utils lib
9 | from ayon_core.settings import get_current_project_settings
10 | settings = get_current_project_settings()
11 | load_settings = settings["houdini"].get("load", {}).get("GenericLoader", {})
12 | use_ayon_entity_uri = load_settings.get("use_ayon_entity_uri", False)
13 | node.parm("use_ayon_entity_uri").set(use_ayon_entity_uri)
14 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/collect_staticmesh_type.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Collector for staticMesh types. """
3 |
4 | import pyblish.api
5 | from ayon_houdini.api import plugin
6 |
7 |
8 | class CollectStaticMeshType(plugin.HoudiniInstancePlugin):
9 | """Collect data type for fbx instance."""
10 |
11 | families = ["staticMesh"]
12 | label = "Collect type of staticMesh"
13 |
14 | order = pyblish.api.CollectorOrder
15 |
16 | def process(self, instance):
17 |
18 | if instance.data["creator_identifier"] == "io.openpype.creators.houdini.staticmesh.fbx": # noqa: E501
19 | # Marking this instance as FBX triggers the FBX extractor.
20 | instance.data["families"] += ["fbx"]
21 |
--------------------------------------------------------------------------------
/.github/workflows/release_trigger.yml:
--------------------------------------------------------------------------------
1 | name: 🚀 Release Trigger
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | draft:
7 | type: boolean
8 | description: "Create Release Draft"
9 | required: false
10 | default: false
11 | release_overwrite:
12 | type: string
13 | description: "Set Version Release Tag"
14 | required: false
15 |
16 | jobs:
17 | call-release-trigger:
18 | uses: ynput/ops-repo-automation/.github/workflows/release_trigger.yml@main
19 | with:
20 | draft: ${{ inputs.draft }}
21 | release_overwrite: ${{ inputs.release_overwrite }}
22 | secrets:
23 | token: ${{ secrets.YNPUT_BOT_TOKEN }}
24 | email: ${{ secrets.CI_EMAIL }}
25 | user: ${{ secrets.CI_USER }}
26 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/PARMmenu.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
18 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/help/validate_vdb_output_node.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Invalid VDB
5 |
6 | ## Invalid VDB output
7 |
8 | All primitives of the output geometry must be VDBs, no other primitive
9 | types are allowed. That means that regardless of the amount of VDBs in the
10 | geometry it will have an equal amount of VDBs, points, primitives and
11 | vertices since each VDB primitive is one point, one vertex and one VDB.
12 |
13 | This validation only checks the geometry on the first frame of the export
14 | frame range.
15 |
16 |
17 |
18 |
19 |
20 | ### Detailed Info
21 |
22 | ROP node `{rop_path}` is set to export SOP path `{sop_path}`.
23 |
24 | {message}
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/collect_reviewable_instances.py:
--------------------------------------------------------------------------------
1 | import pyblish.api
2 | from ayon_houdini.api import plugin
3 |
4 |
5 | class CollectReviewableInstances(plugin.HoudiniInstancePlugin):
6 | """Collect Reviewable Instances.
7 |
8 | Basically, all instances of the specified families
9 | with creator_attribure["review"]
10 | """
11 |
12 | order = pyblish.api.CollectorOrder
13 | label = "Collect Reviewable Instances"
14 | families = ["mantra_rop",
15 | "karma_rop",
16 | "redshift_rop",
17 | "arnold_rop",
18 | "vray_rop",
19 | "usdrender"]
20 |
21 | def process(self, instance):
22 | creator_attribute = instance.data["creator_attributes"]
23 |
24 | instance.data["review"] = creator_attribute.get("review", False)
25 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Object_1generic__loader_8_81.0/Tools.shelf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 | $HDA_TABLE_AND_NAME
11 | OBJ
12 |
13 | AYON
14 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/server/__init__.py:
--------------------------------------------------------------------------------
1 | from typing import Type, Any
2 |
3 | from ayon_server.addons import BaseServerAddon
4 |
5 | from .settings import (
6 | HoudiniSettings,
7 | DEFAULT_VALUES,
8 | convert_settings_overrides,
9 | )
10 |
11 |
12 | class Houdini(BaseServerAddon):
13 | settings_model: Type[HoudiniSettings] = HoudiniSettings
14 |
15 | async def get_default_settings(self):
16 | settings_model_cls = self.get_settings_model()
17 | return settings_model_cls(**DEFAULT_VALUES)
18 |
19 | async def convert_settings_overrides(
20 | self,
21 | source_version: str,
22 | overrides: dict[str, Any],
23 | ) -> dict[str, Any]:
24 | convert_settings_overrides(source_version, overrides)
25 | # Use super conversion
26 | return await super().convert_settings_overrides(
27 | source_version, overrides)
28 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/inventory/set_camera_resolution.py:
--------------------------------------------------------------------------------
1 | from ayon_core.pipeline import InventoryAction
2 | from ayon_houdini.api.lib import (
3 | get_camera_from_container,
4 | set_camera_resolution
5 | )
6 | from ayon_core.pipeline.context_tools import get_current_task_entity
7 |
8 |
9 | class SetCameraResolution(InventoryAction):
10 |
11 | label = "Set Camera Resolution"
12 | icon = "desktop"
13 | color = "orange"
14 |
15 | @staticmethod
16 | def is_compatible(container):
17 | return (
18 | container.get("loader") == "CameraLoader"
19 | )
20 |
21 | def process(self, containers):
22 | task_entity = get_current_task_entity()
23 | for container in containers:
24 | node = container["node"]
25 | camera = get_camera_from_container(node)
26 | set_camera_resolution(camera, task_entity)
27 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_import.hda/ayon_8_8Lop_1lop__import_8_81.0/Tools.shelf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 | LOP
10 |
11 |
12 | $HDA_TABLE_AND_NAME
13 |
14 | AYON
15 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_load_shot.hda/ayon_8_8Lop_1load__shot_8_81.0/Tools.shelf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 | LOP
10 |
11 |
12 | $HDA_TABLE_AND_NAME
13 |
14 | AYON
15 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Lop_1generic__loader_8_81.0/Tools.shelf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 | LOP
10 |
11 |
12 | $HDA_TABLE_AND_NAME
13 |
14 | AYON
15 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Sop_1generic__loader_8_81.0/Tools.shelf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 | SOP
10 |
11 |
12 | $HDA_TABLE_AND_NAME
13 |
14 | AYON
15 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/inventory/show_parameters.py:
--------------------------------------------------------------------------------
1 | from ayon_core.pipeline import InventoryAction
2 | from ayon_houdini.api.lib import show_node_parmeditor
3 |
4 | import hou
5 |
6 |
7 | class ShowParametersAction(InventoryAction):
8 | """Show node parameters in a pop-up parameter window."""
9 | label = "Show parameters"
10 | icon = "pencil-square-o"
11 | color = "#888888"
12 | order = 100
13 |
14 | @staticmethod
15 | def is_compatible(container) -> bool:
16 | object_name: str = container.get("objectName")
17 | if not object_name:
18 | return False
19 |
20 | node = hou.node(object_name)
21 | if not node:
22 | return False
23 |
24 | return True
25 |
26 | def process(self, containers):
27 | for container in containers:
28 | node = hou.node(container["objectName"])
29 | show_node_parmeditor(node)
30 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_mute_layers.hda/ayon_8_8Lop_1mute__layers_8_81.0/Tools.shelf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 | LOP
11 |
12 |
13 | $HDA_TABLE_AND_NAME
14 |
15 | AYON
16 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/OPmenu.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
28 |
29 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/collect_renderlayer.py:
--------------------------------------------------------------------------------
1 | """Collect Render layer name from ROP.
2 |
3 | This simple collector will take name of the ROP node and set it as the render
4 | layer name for the instance.
5 |
6 | This aligns with the behavior of Maya and possibly others, even though there
7 | is nothing like render layer explicitly in Houdini.
8 |
9 | """
10 | import hou
11 | import pyblish.api
12 | from ayon_houdini.api import plugin
13 |
14 |
15 | class CollectRendelayerFromROP(plugin.HoudiniInstancePlugin):
16 | label = "Collect Render layer name from ROP"
17 | order = pyblish.api.CollectorOrder - 0.499
18 | families = ["mantra_rop",
19 | "karma_rop",
20 | "redshift_rop",
21 | "arnold_rop",
22 | "vray_rop",
23 | "usdrender"]
24 |
25 | def process(self, instance):
26 | rop = hou.node(instance.data.get("instance_node"))
27 | instance.data["renderlayer"] = rop.name()
28 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/inventory/select_containers.py:
--------------------------------------------------------------------------------
1 | from ayon_core.pipeline import InventoryAction
2 |
3 | import hou
4 |
5 |
6 | class SelectInScene(InventoryAction):
7 | """Select nodes in the scene from selected containers in scene inventory"""
8 |
9 | label = "Select in scene"
10 | icon = "search"
11 | color = "#888888"
12 | order = 99
13 |
14 | @staticmethod
15 | def is_compatible(container) -> bool:
16 | object_name: str = container.get("objectName")
17 | if not object_name:
18 | return False
19 |
20 | node = hou.node(object_name)
21 | if not node:
22 | return False
23 |
24 | return True
25 |
26 | def process(self, containers):
27 | nodes = [hou.node(container["objectName"]) for container in containers]
28 | if not nodes:
29 | return
30 |
31 | hou.clearAllSelected()
32 | for node in nodes:
33 | node.setSelected(True)
34 |
35 | # Set last as current
36 | nodes[-1].setCurrent(True)
37 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/collect_current_file.py:
--------------------------------------------------------------------------------
1 | import os
2 | import hou
3 |
4 | import pyblish.api
5 | from ayon_houdini.api import plugin
6 |
7 |
8 | class CollectHoudiniCurrentFile(plugin.HoudiniContextPlugin):
9 | """Inject the current working file into context"""
10 |
11 | order = pyblish.api.CollectorOrder - 0.1
12 | label = "Houdini Current File"
13 |
14 | def process(self, context):
15 | """Inject the current working file"""
16 |
17 | current_file = hou.hipFile.path()
18 | if (
19 | hou.hipFile.isNewFile()
20 | or not os.path.exists(current_file)
21 | ):
22 | # By default, Houdini will even point a new scene to a path.
23 | # However if the file is not saved at all and does not exist,
24 | # we assume the user never set it.
25 | self.log.warning("Houdini workfile is unsaved.")
26 | current_file = ""
27 |
28 | context.data["currentFile"] = current_file
29 | self.log.info('Current workfile path: {}'.format(current_file))
30 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/README.md:
--------------------------------------------------------------------------------
1 | ## How to create expanded HDAs
2 |
3 | In order to be able to see the contents of HDAs and store them in version control (i.e. git) is really useful to expand them using the `hotl` binary that ships with Houdini (https://www.sidefx.com/docs/houdini/ref/utils/hotl.html), that way we can do small tweaks to the HDAs without needing to use Houdini and we can version control the changes done over time.
4 |
5 | ### How to
6 |
7 | We run the following command, delete the original hda file and then rename the directory to match the hdafile name.
8 | ```
9 | hotl -t directory hdafile
10 | ```
11 |
12 | Example:
13 | ```
14 | hotl -t ayon_lop_import ayon_lop_import.hda
15 | rm ayon_lop_import.hda
16 | mv ayon_lop_import ayon_lop_import.hda
17 | ```
18 |
19 | ### Where to find `hotl`
20 |
21 | The `hotl` command ships with any of the Houdini install binaries, like for example:
22 | ```
23 | ./houdini/20.5.320/bin/hotl
24 | ```
25 |
26 | Or using the `terminal` from AYON launcher in Houdini context. You should be
27 | able to call just:
28 | ```
29 | hotl
30 | ```
31 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/validate_usd_render_product_names.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 |
4 | import pyblish.api
5 | from ayon_core.pipeline import PublishValidationError
6 |
7 | from ayon_houdini.api import plugin
8 |
9 |
10 | class ValidateUSDRenderProductNames(plugin.HoudiniInstancePlugin):
11 | """Validate USD Render Product names are correctly set absolute paths."""
12 |
13 | order = pyblish.api.ValidatorOrder
14 | families = ["usdrender"]
15 | label = "Validate USD Render Product Names"
16 | optional = True
17 |
18 | def process(self, instance):
19 |
20 | invalid = []
21 | for filepath in instance.data.get("files", []):
22 |
23 | if not filepath:
24 | invalid.append("Detected empty output filepath.")
25 |
26 | if not os.path.isabs(filepath):
27 | invalid.append(
28 | "Output file path is not absolute path: %s" % filepath
29 | )
30 |
31 | if invalid:
32 | for message in invalid:
33 | self.log.error(message)
34 | raise PublishValidationError(
35 | "USD Render Paths are invalid.", title=self.label)
36 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/collect_workfile.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pyblish.api
4 | from ayon_houdini.api import plugin
5 |
6 | class CollectWorkfile(plugin.HoudiniInstancePlugin):
7 | """Inject workfile representation into instance"""
8 |
9 | order = pyblish.api.CollectorOrder - 0.01
10 | label = "Houdini Workfile Data"
11 | families = ["workfile"]
12 |
13 | def process(self, instance):
14 |
15 | current_file = instance.context.data["currentFile"]
16 | folder, file = os.path.split(current_file)
17 | filename, ext = os.path.splitext(file)
18 |
19 | instance.data.update({
20 | "setMembers": [current_file],
21 | "frameStart": instance.context.data['frameStart'],
22 | "frameEnd": instance.context.data['frameEnd'],
23 | "handleStart": instance.context.data['handleStart'],
24 | "handleEnd": instance.context.data['handleEnd']
25 | })
26 |
27 | instance.data['representations'] = [{
28 | 'name': ext.lstrip("."),
29 | 'ext': ext.lstrip("."),
30 | 'files': file,
31 | "stagingDir": folder,
32 | }]
33 |
34 | self.log.debug('Collected workfile instance: {}'.format(file))
35 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/collect_rop_frame_range.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Collector plugin for frames data on ROP instances."""
3 | import hou # noqa
4 | import pyblish.api
5 | from ayon_houdini.api import lib, plugin
6 |
7 |
8 | class CollectRopFrameRange(plugin.HoudiniInstancePlugin):
9 | """Collect all frames which would be saved from the ROP nodes"""
10 |
11 | order = pyblish.api.CollectorOrder
12 | label = "Collect RopNode Frame Range"
13 |
14 | def process(self, instance):
15 |
16 | node_path = instance.data.get("instance_node")
17 | if node_path is None:
18 | # Instance without instance node like a workfile instance
19 | self.log.debug(
20 | "No instance node found for instance: {}".format(instance)
21 | )
22 | return
23 |
24 | ropnode = hou.node(node_path)
25 | frame_data = lib.get_frame_data(
26 | ropnode, self.log
27 | )
28 |
29 | if not frame_data:
30 | return
31 |
32 | # Log debug message about the collected frame range
33 | self.log.debug(
34 | "Collected frame_data: {}".format(frame_data)
35 | )
36 |
37 | instance.data.update(frame_data)
38 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/validate_mkpaths_toggled.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import pyblish.api
3 |
4 | from ayon_core.pipeline import PublishValidationError
5 |
6 | from ayon_houdini.api import plugin
7 |
8 |
9 | class ValidateIntermediateDirectoriesChecked(plugin.HoudiniInstancePlugin):
10 | """Validate Create Intermediate Directories is enabled on ROP node."""
11 |
12 | order = pyblish.api.ValidatorOrder
13 | families = ["pointcache", "camera", "vdbcache", "model"]
14 | label = "Create Intermediate Directories Checked"
15 |
16 | def process(self, instance):
17 |
18 | invalid = self.get_invalid(instance)
19 | if invalid:
20 | nodes = "\n".join(f"- {node.path()}" for node in invalid)
21 | raise PublishValidationError(
22 | ("Found ROP node with Create Intermediate "
23 | "Directories turned off:\n {}".format(nodes)),
24 | title=self.label)
25 |
26 | @classmethod
27 | def get_invalid(cls, instance):
28 |
29 | result = []
30 |
31 | for node in instance[:]:
32 | if node.parm("mkpath").eval() != 1:
33 | cls.log.error("Invalid settings found on `%s`" % node.path())
34 | result.append(node)
35 |
36 | return result
37 |
--------------------------------------------------------------------------------
/client/ayon_houdini/addon.py:
--------------------------------------------------------------------------------
1 | import os
2 | from ayon_core.addon import AYONAddon, IHostAddon
3 |
4 | from .version import __version__
5 |
6 | HOUDINI_HOST_DIR = os.path.dirname(os.path.abspath(__file__))
7 |
8 |
9 | class HoudiniAddon(AYONAddon, IHostAddon):
10 | name = "houdini"
11 | version = __version__
12 | host_name = "houdini"
13 |
14 | def add_implementation_envs(self, env, _app):
15 | # Add requirements to HOUDINI_PATH
16 | startup_path = os.path.join(HOUDINI_HOST_DIR, "startup")
17 | new_houdini_path = [startup_path]
18 |
19 | old_houdini_path = env.get("HOUDINI_PATH") or ""
20 | for path in old_houdini_path.split(os.pathsep):
21 | if not path:
22 | continue
23 |
24 | norm_path = os.path.normpath(path)
25 | if norm_path not in new_houdini_path:
26 | new_houdini_path.append(norm_path)
27 |
28 | # Add & (ampersand), it represents "the standard Houdini Path contents"
29 | new_houdini_path.append("&")
30 | env["HOUDINI_PATH"] = os.pathsep.join(new_houdini_path)
31 |
32 | def get_launch_hook_paths(self, app):
33 | if app.host_name != self.host_name:
34 | return []
35 | return [
36 | os.path.join(HOUDINI_HOST_DIR, "hooks")
37 | ]
38 |
39 | def get_workfile_extensions(self):
40 | return [".hip", ".hiplc", ".hipnc"]
41 |
--------------------------------------------------------------------------------
/server/settings/templated_workfile_build.py:
--------------------------------------------------------------------------------
1 | from ayon_server.settings import (
2 | BaseSettingsModel,
3 | SettingsField,
4 | task_types_enum,
5 | folder_types_enum,
6 | )
7 |
8 |
9 | class TemplatedWorkfileProfileModel(BaseSettingsModel):
10 | task_types: list[str] = SettingsField(
11 | default_factory=list,
12 | title="Task types",
13 | enum_resolver=task_types_enum
14 | )
15 | task_names: list[str] = SettingsField(
16 | default_factory=list,
17 | title="Task names"
18 | )
19 | folder_types: list[str] = SettingsField(
20 | default_factory=list,
21 | title="Folder Types",
22 | enum_resolver=folder_types_enum
23 | )
24 | folder_paths: list[str] = SettingsField(
25 | default_factory=list,
26 | title="Folder paths"
27 | )
28 | path: str = SettingsField(
29 | title="Path to template",
30 | section="Template Configuration",
31 | )
32 | keep_placeholder: bool = SettingsField(
33 | False,
34 | title="Keep placeholders")
35 | create_first_version: bool = SettingsField(
36 | True,
37 | title="Create first version"
38 | )
39 |
40 |
41 | class TemplatedWorkfileBuildModel(BaseSettingsModel):
42 | """Settings for templated workfile builder."""
43 | profiles: list[TemplatedWorkfileProfileModel] = SettingsField(
44 | default_factory=list
45 | )
46 |
--------------------------------------------------------------------------------
/server/settings/conversion.py:
--------------------------------------------------------------------------------
1 | import semver
2 | from typing import Any
3 |
4 |
5 | def parse_version(version):
6 | try:
7 | return semver.VersionInfo.parse(version)
8 | except ValueError:
9 | return None
10 |
11 |
12 | def _convert_validate_subset_name(overrides: dict[str, Any]) -> None:
13 | # Convert old "ValidateSubsetName" to new "ValidateProductName"
14 | if "publish" not in overrides:
15 | return
16 |
17 | publish_overrides = overrides["publish"]
18 | if (
19 | "ValidateSubsetName" in publish_overrides
20 | and "ValidateProductName" not in publish_overrides
21 | ):
22 | publish_overrides["ValidateProductName"] = publish_overrides.pop(
23 | "ValidateSubsetName"
24 | )
25 |
26 | def _enable_create_render_rops_use_render_product_type(
27 | overrides: dict[str, Any]
28 | ) -> None:
29 | # Enforce render creators `render_rops_use_render_product_type` to True
30 | # to remain backwards compatible with older versions
31 | create = overrides.setdefault("create", {})
32 | create["render_rops_use_legacy_product_type"] = True
33 |
34 |
35 | def convert_settings_overrides(
36 | source_version: str,
37 | overrides: dict[str, Any],
38 | ) -> dict[str, Any]:
39 | _convert_validate_subset_name(overrides)
40 |
41 | if parse_version(source_version) < (0, 5, 1):
42 | _enable_create_render_rops_use_render_product_type(overrides)
43 |
44 | return overrides
45 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/validate_alembic_face_sets.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import hou
3 | import pyblish.api
4 | from ayon_houdini.api import plugin
5 |
6 |
7 | class ValidateAlembicROPFaceSets(plugin.HoudiniInstancePlugin):
8 | """Validate Face Sets are disabled for extraction to pointcache.
9 |
10 | When groups are saved as Face Sets with the Alembic these show up
11 | as shadingEngine connections in Maya - however, with animated groups
12 | these connections in Maya won't work as expected, it won't update per
13 | frame. Additionally, it can break shader assignments in some cases
14 | where it requires to first break this connection to allow a shader to
15 | be assigned.
16 |
17 | It is allowed to include Face Sets, so only an issue is logged to
18 | identify that it could introduce issues down the pipeline.
19 |
20 | """
21 |
22 | order = pyblish.api.ValidatorOrder + 0.1
23 | families = ["abc"]
24 | label = "Validate Alembic ROP Face Sets"
25 |
26 | def process(self, instance):
27 |
28 | rop = hou.node(instance.data["instance_node"])
29 | facesets = rop.parm("facesets").eval()
30 |
31 | # 0 = No Face Sets
32 | # 1 = Save Non-Empty Groups as Face Sets
33 | # 2 = Save All Groups As Face Sets
34 | if facesets != 0:
35 | self.log.warning(
36 | "Alembic ROP saves 'Face Sets' for Geometry. "
37 | "Are you sure you want this?"
38 | )
39 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/INDEX__SECTION:
--------------------------------------------------------------------------------
1 | Operator: ayon::generic_loader::1.0
2 | Label: AYON Generic Loader
3 | Path: oplib:/ayon::Object/generic_loader::1.0?ayon::Object/generic_loader::1.0
4 | Icon: opdef:/ayon::Object/generic_loader::1.0?IconImage
5 | Table: Object
6 | License:
7 | Extra:
8 | User:
9 | Inputs: 0 to 0
10 | Subnet: true
11 | Python: false
12 | Empty: false
13 | Modified: Mon Dec 23 21:23:36 2024
14 |
15 | Operator: ayon::generic_loader::1.0
16 | Label: AYON Generic Loader
17 | Path: oplib:/ayon::Sop/generic_loader::1.0?ayon::Sop/generic_loader::1.0
18 | Icon: opdef:/ayon::Sop/generic_loader::1.0?IconImage
19 | Table: Sop
20 | License:
21 | Extra: inputcolors='0 ' outputcolors='1 "RGB 0.700195 0.700195 0.700195" '
22 | User:
23 | Inputs: 0 to 0
24 | Subnet: true
25 | Python: false
26 | Empty: false
27 | Modified: Mon Dec 23 21:24:01 2024
28 |
29 | Operator: ayon::generic_loader::1.0
30 | Label: AYON Generic Loader
31 | Path: oplib:/ayon::Lop/generic_loader::1.0?ayon::Lop/generic_loader::1.0
32 | Icon: opdef:/ayon::Lop/generic_loader::1.0?IconImage
33 | Table: Lop
34 | License:
35 | Extra:
36 | User:
37 | Inputs: 0 to 0
38 | Subnet: true
39 | Python: false
40 | Empty: false
41 | Modified: Mon Dec 23 21:20:34 2024
42 |
43 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/validate_houdini_license_category.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import hou
3 |
4 | import pyblish.api
5 | from ayon_core.pipeline import PublishValidationError
6 |
7 | from ayon_houdini.api import plugin
8 |
9 |
10 | class ValidateHoudiniNotApprenticeLicense(plugin.HoudiniInstancePlugin):
11 | """Validate the Houdini instance runs a non Apprentice license.
12 |
13 | USD ROPs:
14 | When extracting USD files from an apprentice Houdini license,
15 | the resulting files will get "scrambled" with a license protection
16 | and get a special .usdnc suffix.
17 |
18 | This currently breaks the product/representation pipeline so we
19 | disallow any publish with apprentice license.
20 |
21 | Alembic ROPs:
22 | Houdini Apprentice does not export Alembic.
23 | """
24 |
25 | order = pyblish.api.ValidatorOrder
26 | families = ["usdrop", "abc", "fbx", "camera"]
27 | label = "Houdini Apprentice License"
28 |
29 | def process(self, instance):
30 |
31 | if hou.isApprentice():
32 | # Find which family or product type was matched with the plug-in
33 | families = {instance.data["productType"]}
34 | families.update(instance.data.get("families", []))
35 | disallowed_families = families.intersection(self.families)
36 | families = " ".join(sorted(disallowed_families)).title()
37 |
38 | raise PublishValidationError(
39 | "{} publishing requires a non apprentice license."
40 | .format(families),
41 | title=self.label)
42 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/save_scene.py:
--------------------------------------------------------------------------------
1 | import inspect
2 | import pyblish.api
3 |
4 | from ayon_core.pipeline import registered_host, PublishError
5 |
6 | from ayon_houdini.api import plugin
7 |
8 |
9 | class SaveCurrentScene(plugin.HoudiniContextPlugin):
10 | """Save current scene"""
11 |
12 | label = "Save current file"
13 | order = pyblish.api.ExtractorOrder - 0.49
14 |
15 | def process(self, context):
16 |
17 | # Filename must not have changed since collecting
18 | host = registered_host()
19 | current_file = host.get_current_workfile()
20 | if context.data['currentFile'] != current_file:
21 | raise PublishError(
22 | f"Collected filename '{context.data['currentFile']}' differs"
23 | f" from current scene name '{current_file}'.",
24 | description=self.get_error_description()
25 | )
26 | if host.workfile_has_unsaved_changes():
27 | self.log.info("Saving current file: {}".format(current_file))
28 | host.save_workfile(current_file)
29 | else:
30 | self.log.debug("No unsaved changes, skipping file save..")
31 |
32 |
33 | def get_error_description(self):
34 | return inspect.cleandoc(
35 | """### Scene File Name Changed During Publishing
36 | This error occurs when you validate the scene and then save it as
37 | a new file manually, or if you open a new file and continue
38 | publishing.
39 |
40 | Please reset the publisher and publish without changing
41 | the scene file midway.
42 | """
43 | )
44 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/validate_bypass.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import hou
3 |
4 | import pyblish.api
5 | from ayon_core.pipeline import PublishValidationError
6 |
7 | from ayon_houdini.api import plugin
8 |
9 |
10 | class ValidateBypassed(plugin.HoudiniInstancePlugin):
11 | """Validate all primitives build hierarchy from attribute when enabled.
12 |
13 | The name of the attribute must exist on the prims and have the same name
14 | as Build Hierarchy from Attribute's `Path Attribute` value on the Alembic
15 | ROP node whenever Build Hierarchy from Attribute is enabled.
16 |
17 | """
18 |
19 | order = pyblish.api.ValidatorOrder - 0.1
20 | families = ["*"]
21 | label = "Validate ROP Bypass"
22 |
23 | def process(self, instance):
24 |
25 | if not instance.data.get("instance_node"):
26 | # Ignore instances without an instance node
27 | # e.g. in memory bootstrap instances
28 | self.log.debug(
29 | "Skipping instance without instance node: {}".format(instance)
30 | )
31 | return
32 |
33 | invalid = self.get_invalid(instance)
34 | if invalid:
35 | rop = invalid[0]
36 | raise PublishValidationError(
37 | ("ROP node {} is set to bypass, publishing cannot "
38 | "continue.".format(rop.path())),
39 | title=self.label
40 | )
41 |
42 | @classmethod
43 | def get_invalid(cls, instance):
44 |
45 | rop = hou.node(instance.data["instance_node"])
46 | if hasattr(rop, "isBypassed") and rop.isBypassed():
47 | return [rop]
48 |
--------------------------------------------------------------------------------
/server/settings/general.py:
--------------------------------------------------------------------------------
1 | from ayon_server.settings import BaseSettingsModel, SettingsField
2 |
3 |
4 | class HoudiniVarModel(BaseSettingsModel):
5 | _layout = "expanded"
6 | var: str = SettingsField("", title="Var")
7 | value: str = SettingsField("", title="Value")
8 | is_directory: bool = SettingsField(False, title="Treat as directory")
9 |
10 |
11 | class UpdateHoudiniVarcontextModel(BaseSettingsModel):
12 | """Sync vars with context changes.
13 |
14 | If a value is treated as a directory on update
15 | it will be ensured the folder exists.
16 | """
17 |
18 | enabled: bool = SettingsField(title="Enabled")
19 | # TODO this was dynamic dictionary '{var: path}'
20 | houdini_vars: list[HoudiniVarModel] = SettingsField(
21 | default_factory=list,
22 | title="Houdini Vars"
23 | )
24 |
25 |
26 | class GeneralSettingsModel(BaseSettingsModel):
27 | add_self_publish_button: bool = SettingsField(
28 | False,
29 | title="Add Self Publish Button"
30 | )
31 | update_houdini_var_context: UpdateHoudiniVarcontextModel = SettingsField(
32 | default_factory=UpdateHoudiniVarcontextModel,
33 | title="Update Houdini Vars on context change"
34 | )
35 |
36 |
37 | DEFAULT_GENERAL_SETTINGS = {
38 | "add_self_publish_button": False,
39 | "update_houdini_var_context": {
40 | "enabled": True,
41 | "houdini_vars": [
42 | {
43 | "var": "JOB",
44 | "value": "{root[work]}/{project[name]}/{hierarchy}/{folder[name]}/work/{task[name]}", # noqa
45 | "is_directory": True
46 | }
47 | ]
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/collect_farm_instances.py:
--------------------------------------------------------------------------------
1 | import pyblish.api
2 | from ayon_houdini.api import plugin
3 |
4 |
5 | class CollectFarmInstances(plugin.HoudiniInstancePlugin):
6 | """Collect instances for farm render."""
7 |
8 | order = pyblish.api.CollectorOrder - 0.49
9 | families = ["mantra_rop",
10 | "karma_rop",
11 | "redshift_rop",
12 | "arnold_rop",
13 | "vray_rop",
14 | "usdrender",
15 | "ass","pointcache", "redshiftproxy",
16 | "vdbcache", "model", "staticMesh",
17 | "rop.opengl", "usdrop", "camera"]
18 |
19 | targets = ["local", "remote"]
20 | label = "Collect farm instances"
21 |
22 | def process(self, instance):
23 |
24 | creator_attribute = instance.data["creator_attributes"]
25 |
26 | # Collect Render Target
27 | if creator_attribute.get("render_target") not in {
28 | "farm_split",
29 | "farm",
30 | "local_export_farm_render",
31 | }:
32 | instance.data["farm"] = False
33 | instance.data["splitRender"] = False
34 | try:
35 | instance.data["families"].remove("publish.hou")
36 | except ValueError:
37 | pass
38 | self.log.debug("Render on farm is disabled. "
39 | "Skipping farm collecting.")
40 | return
41 |
42 | instance.data["farm"] = True
43 | instance.data["splitRender"] = (
44 | creator_attribute.get("render_target")
45 | in {"farm_split", "local_export_farm_render"}
46 | )
47 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: ayon-houdini
2 | repo_url: https://github.com/ynput/ayon-houdini
3 |
4 | nav:
5 | - Home: index.md
6 | - License: license.md
7 |
8 | theme:
9 | name: material
10 | palette:
11 | - media: "(prefers-color-scheme: dark)"
12 | scheme: slate
13 | toggle:
14 | icon: material/weather-sunny
15 | name: Switch to light mode
16 | - media: "(prefers-color-scheme: light)"
17 | scheme: default
18 | toggle:
19 | icon: material/weather-night
20 | name: Switch to dark mode
21 | logo: img/ay-symbol-blackw-full.png
22 | favicon: img/favicon.ico
23 | features:
24 | - navigation.sections
25 | - navigation.path
26 | - navigation.prune
27 |
28 | extra:
29 | version:
30 | provider: mike
31 |
32 | extra_css: [css/custom.css]
33 |
34 | markdown_extensions:
35 | - mdx_gh_links
36 | - pymdownx.snippets
37 |
38 | plugins:
39 | - search
40 | - offline
41 | - mkdocs-autoapi:
42 | autoapi_dir: ./
43 | autoapi_add_nav_entry: Reference
44 | autoapi_ignore:
45 | - .*
46 | - docs/**/*
47 | - tests/**/*
48 | - tools/**/*
49 | - stubs/**/* # mocha fix
50 | - ./**/pythonrc.py # houdini fix
51 | - .*/**/*
52 | - ./*.py
53 | - mkdocstrings:
54 | handlers:
55 | python:
56 | paths:
57 | - ./
58 | - client/*
59 | - server/*
60 | - services/*
61 | - minify:
62 | minify_html: true
63 | minify_js: true
64 | minify_css: true
65 | htmlmin_opts:
66 | remove_comments: true
67 | cache_safe: true
68 | - mike
69 |
70 | hooks:
71 | - mkdocs_hooks.py
72 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/load/show_usdview.py:
--------------------------------------------------------------------------------
1 | import os
2 | import platform
3 | import subprocess
4 | import hou
5 |
6 | from ayon_core.lib.vendor_bin_utils import find_executable
7 | from ayon_houdini.api import plugin
8 |
9 |
10 | class ShowInUsdview(plugin.HoudiniLoader):
11 | """Open USD file in usdview"""
12 |
13 | label = "Show in usdview"
14 | representations = {"*"}
15 | product_types = {"*"}
16 | extensions = {"usd", "usda", "usdlc", "usdnc", "abc"}
17 | order = 15
18 |
19 | icon = "code-fork"
20 | color = "white"
21 |
22 | def load(self, context, name=None, namespace=None, data=None):
23 | from pathlib import Path
24 |
25 | if platform.system() == "Windows":
26 | if hou.applicationVersion()[0] >= 20:
27 | executable = "usdview.cmd"
28 | else:
29 | executable = "usdview.bat"
30 | else:
31 | executable = "usdview"
32 |
33 | usdview = find_executable(executable)
34 | if not usdview:
35 | raise RuntimeError("Unable to find usdview")
36 |
37 | # For some reason Windows can return the path like:
38 | # C:/PROGRA~1/SIDEEF~1/HOUDIN~1.435/bin/usdview
39 | # convert to resolved path so `subprocess` can take it
40 | usdview = str(Path(usdview).resolve().as_posix())
41 |
42 | filepath = self.filepath_from_context(context)
43 | filepath = os.path.normpath(filepath)
44 | filepath = filepath.replace("\\", "/")
45 |
46 | if not os.path.exists(filepath):
47 | self.log.error("File does not exist: %s" % filepath)
48 | return
49 |
50 | self.log.info("Start houdini variant of usdview...")
51 |
52 | subprocess.Popen([usdview, filepath, "--renderer", "GL"])
53 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/validate_file_extension.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 | import hou
4 |
5 | import pyblish.api
6 | from ayon_core.pipeline import PublishValidationError
7 |
8 | from ayon_houdini.api import lib, plugin
9 |
10 |
11 | class ValidateFileExtension(plugin.HoudiniInstancePlugin):
12 | """Validate the output file extension fits the output product type.
13 |
14 | File extensions:
15 | - Pointcache must be .abc
16 | - Camera must be .abc
17 | - VDB must be .vdb
18 |
19 | """
20 |
21 | order = pyblish.api.ValidatorOrder
22 | families = ["camera", "vdbcache"]
23 | label = "Output File Extension"
24 |
25 | product_type_extensions = {
26 | "camera": ".abc",
27 | "vdbcache": ".vdb",
28 | }
29 |
30 | def process(self, instance):
31 |
32 | invalid = self.get_invalid(instance)
33 | if invalid:
34 | raise PublishValidationError(
35 | f"ROP node has incorrect file extension: {invalid[0].path()}",
36 | title=self.label
37 | )
38 |
39 | @classmethod
40 | def get_invalid(cls, instance):
41 | # Get expected extension
42 | product_type = instance.data.get("productType")
43 | extension = cls.product_type_extensions.get(product_type, None)
44 | if extension is None:
45 | raise PublishValidationError(
46 | "Unsupported product type: {}".format(product_type),
47 | title=cls.label)
48 |
49 | # Perform extension check
50 | node = hou.node(instance.data["instance_node"])
51 | output = lib.get_output_parameter(node).eval()
52 | _, output_extension = os.path.splitext(output)
53 | if output_extension != extension:
54 | return [node]
55 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/validate_frame_token.py:
--------------------------------------------------------------------------------
1 | import hou
2 |
3 | import pyblish.api
4 |
5 | from ayon_core.pipeline import PublishValidationError
6 | from ayon_houdini.api import lib, plugin
7 |
8 |
9 | class ValidateFrameToken(plugin.HoudiniInstancePlugin):
10 | """Validate if the unexpanded string contains the frame ('$F') token.
11 |
12 | This validator will *only* check the output parameter of the node if
13 | the Valid Frame Range is not set to 'Render Current Frame'
14 |
15 | Rules:
16 | If you render out a frame range it is mandatory to have the
17 | frame token - '$F4' or similar - to ensure that each frame gets
18 | written. If this is not the case you will override the same file
19 | every time a frame is written out.
20 |
21 | Examples:
22 | Good: 'my_vbd_cache.$F4.vdb'
23 | Bad: 'my_vbd_cache.vdb'
24 |
25 | """
26 |
27 | order = pyblish.api.ValidatorOrder
28 | label = "Validate Frame Token"
29 | families = ["vdbcache"]
30 |
31 | def process(self, instance):
32 |
33 | invalid = self.get_invalid(instance)
34 | if invalid:
35 | raise PublishValidationError(
36 | f"Output settings do no match for '{invalid[0].path()}'"
37 | )
38 |
39 | @classmethod
40 | def get_invalid(cls, instance):
41 |
42 | node = hou.node(instance.data["instance_node"])
43 | # Check trange parm, 0 means Render Current Frame
44 | frame_range = node.evalParm("trange")
45 | if frame_range == 0:
46 | return
47 |
48 | output_parm = lib.get_output_parameter(node)
49 | unexpanded_str = output_parm.unexpandedString()
50 |
51 | if "$F" not in unexpanded_str:
52 | cls.log.error("No frame token found in '%s'" % node.path())
53 | return [node]
54 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_import.hda/ayon_8_8Lop_1lop__import_8_81.0/Help:
--------------------------------------------------------------------------------
1 | = AYON Load Asset =
2 |
3 | #icon: opdef:/ayon::Lop/lop_import::1.0?AYON_icon.png
4 |
5 | """References an AYON USD product, usually an asset or a shot."""
6 |
7 | == Overview ==
8 |
9 | *References* or *Payloads* an AYON USD product into the current USD layer.
10 |
11 | This node allows you to select which AYON USD product to load. It does this by utilizing a [Node:lop/reference] to load the specified AYON USD product.
12 |
13 | @parameters
14 | ~~~ Choose Product ~~~
15 | Project:
16 | The name of the AYON project from which to load products.
17 | Folder Path:
18 | The path to the AYON folder (entity) within the project's hierarchy.
19 | Product :
20 | The name of the AYON product you want to load.
21 | :note:
22 | Products can have multiple representations.
23 | Version:
24 | The specific version of the product you want to load.
25 | Representation:
26 | The name of the representation, which refers to the file format (e.g., USD, USDA, ABC).
27 | :note:
28 | we use the representation name to query the file path from AYON.
29 | Refresh:
30 | Click to refresh and retry applying the product load parameters to load the correct file
31 | Reload Files:
32 | Click to reload the contents of all files imported by this node. This also clears the cache of file wilrdcard pattern expansions.
33 | File:
34 | The file path that will be loaded. This is locked by default as it is typically generated by the node.
35 | :tip:
36 | It's locked by default as it should be computed by the node.
37 | Primitive Root:
38 | The referenced prim will be overlayed onto this prim, and the referenced prim’s descendants will become this prim’s descendants. If this prim doesn’t exist, the node will create it.
39 |
40 | @related
41 |
42 | * [Node:lop/reference]
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/validate_animation_settings.py:
--------------------------------------------------------------------------------
1 | import hou
2 |
3 | import pyblish.api
4 | from ayon_core.pipeline.publish import PublishValidationError
5 |
6 | from ayon_houdini.api import lib, plugin
7 |
8 |
9 | class ValidateAnimationSettings(plugin.HoudiniInstancePlugin):
10 | """Validate if the unexpanded string contains the frame ('$F') token
11 |
12 | This validator will only check the output parameter of the node if
13 | the Valid Frame Range is not set to 'Render Current Frame'
14 |
15 | Rules:
16 | If you render out a frame range it is mandatory to have the
17 | frame token - '$F4' or similar - to ensure that each frame gets
18 | written. If this is not the case you will override the same file
19 | every time a frame is written out.
20 |
21 | Examples:
22 | Good: 'my_vbd_cache.$F4.vdb'
23 | Bad: 'my_vbd_cache.vdb'
24 |
25 | """
26 |
27 | order = pyblish.api.ValidatorOrder
28 | label = "Validate Frame Settings"
29 | families = ["vdbcache"]
30 |
31 | def process(self, instance):
32 |
33 | invalid = self.get_invalid(instance)
34 | if invalid:
35 | raise PublishValidationError(
36 | f"Output settings do no match for '{invalid[0].path()}'"
37 | )
38 |
39 | @classmethod
40 | def get_invalid(cls, instance):
41 |
42 | node = hou.node(instance.data["instance_node"])
43 | # Check trange parm, 0 means Render Current Frame
44 | frame_range = node.evalParm("trange")
45 | if frame_range == 0:
46 | return
47 |
48 | output_parm = lib.get_output_parameter(node)
49 | unexpanded_str = output_parm.unexpandedString()
50 |
51 | if "$F" not in unexpanded_str:
52 | cls.log.error("No frame token found in '%s'" % node.path())
53 | return [node]
54 |
--------------------------------------------------------------------------------
/.github/workflows/assign_pr_to_project.yml:
--------------------------------------------------------------------------------
1 | name: 🔸Auto assign pr
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | pr_number:
6 | type: string
7 | description: "Run workflow for this PR number"
8 | required: true
9 | project_id:
10 | type: string
11 | description: "Github Project Number"
12 | required: true
13 | default: "16"
14 | pull_request:
15 | types:
16 | - opened
17 |
18 | env:
19 | GH_TOKEN: ${{ github.token }}
20 |
21 | jobs:
22 | get-pr-repo:
23 | runs-on: ubuntu-latest
24 | outputs:
25 | pr_repo_name: ${{ steps.get-repo-name.outputs.repo_name || github.event.pull_request.head.repo.full_name }}
26 |
27 | # INFO `github.event.pull_request.head.repo.full_name` is not available on manual triggered (dispatched) runs
28 | steps:
29 | - name: Get PR repo name
30 | if: ${{ github.event_name == 'workflow_dispatch' }}
31 | id: get-repo-name
32 | run: |
33 | repo_name=$(gh pr view ${{ inputs.pr_number }} --json headRepository,headRepositoryOwner --repo ${{ github.repository }} | jq -r '.headRepositoryOwner.login + "/" + .headRepository.name')
34 | echo "repo_name=$repo_name" >> $GITHUB_OUTPUT
35 |
36 | auto-assign-pr:
37 | needs:
38 | - get-pr-repo
39 | if: ${{ needs.get-pr-repo.outputs.pr_repo_name == github.repository }}
40 | uses: ynput/ops-repo-automation/.github/workflows/pr_to_project.yml@main
41 | with:
42 | repo: "${{ github.repository }}"
43 | project_id: ${{ inputs.project_id != '' && fromJSON(inputs.project_id) || 16 }}
44 | pull_request_number: ${{ github.event.pull_request.number || fromJSON(inputs.pr_number) }}
45 | secrets:
46 | # INFO fallback to default `github.token` is required for PRs from forks
47 | # INFO organization secrets won't be available to forks
48 | token: ${{ secrets.YNPUT_BOT_TOKEN || github.token}}
49 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/workfile_build/create_placeholder.py:
--------------------------------------------------------------------------------
1 | from ayon_core.pipeline.workfile.workfile_template_builder import (
2 | CreatePlaceholderItem,
3 | PlaceholderCreateMixin,
4 | )
5 | from ayon_core.pipeline import registered_host
6 | from ayon_core.pipeline.create import CreateContext
7 |
8 | from ayon_houdini.api.workfile_template_builder import (
9 | HoudiniPlaceholderPlugin
10 | )
11 |
12 |
13 | class HoudiniPlaceholderCreatePlugin(
14 | HoudiniPlaceholderPlugin, PlaceholderCreateMixin
15 | ):
16 | """Workfile template plugin to create "create placeholders".
17 |
18 | "create placeholders" will be replaced by publish instances.
19 |
20 | TODO:
21 | Support imprint & read precreate data to instances.
22 | """
23 |
24 | identifier = "ayon.create.placeholder"
25 | label = "Houdini Create"
26 |
27 | def populate_placeholder(self, placeholder):
28 | self.populate_create_placeholder(placeholder)
29 |
30 | def repopulate_placeholder(self, placeholder):
31 | self.populate_create_placeholder(placeholder)
32 |
33 | def get_placeholder_options(self, options=None):
34 | return self.get_create_plugin_options(options)
35 |
36 | def get_placeholder_node_name(self, placeholder_data):
37 | create_context = CreateContext(registered_host())
38 | creator = create_context.creators.get(placeholder_data["creator"])
39 | product_type = creator.product_type
40 | node_name = "{}_{}".format(
41 | self.identifier.replace(".", "_"),
42 | product_type
43 | )
44 |
45 | return node_name
46 |
47 | def collect_placeholders(self):
48 | output = []
49 | create_placeholders = self.collect_scene_placeholders()
50 |
51 | for node in create_placeholders:
52 | placeholder_data = self._read(node)
53 | output.append(
54 | CreatePlaceholderItem(node.path(), placeholder_data, self)
55 | )
56 |
57 | return output
58 |
--------------------------------------------------------------------------------
/ruff.toml:
--------------------------------------------------------------------------------
1 | # Exclude a variety of commonly ignored directories.
2 | exclude = [
3 | ".bzr",
4 | ".direnv",
5 | ".eggs",
6 | ".git",
7 | ".git-rewrite",
8 | ".hg",
9 | ".ipynb_checkpoints",
10 | ".mypy_cache",
11 | ".nox",
12 | ".pants.d",
13 | ".pyenv",
14 | ".pytest_cache",
15 | ".pytype",
16 | ".ruff_cache",
17 | ".svn",
18 | ".tox",
19 | ".venv",
20 | ".vscode",
21 | "__pypackages__",
22 | "_build",
23 | "buck-out",
24 | "build",
25 | "dist",
26 | "node_modules",
27 | "site-packages",
28 | "venv",
29 | ]
30 |
31 | # Same as Black.
32 | line-length = 79
33 | indent-width = 4
34 |
35 | [lint]
36 | # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
37 | # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
38 | # McCabe complexity (`C901`) by default.
39 | select = ["E", "F", "W"]
40 | ignore = []
41 |
42 | # Allow fix for all enabled rules (when `--fix`) is provided.
43 | fixable = ["ALL"]
44 | unfixable = []
45 |
46 | # Allow unused variables when underscore-prefixed.
47 | dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
48 |
49 | [format]
50 | # Like Black, use double quotes for strings.
51 | quote-style = "double"
52 |
53 | # Like Black, indent with spaces, rather than tabs.
54 | indent-style = "space"
55 |
56 | # Like Black, respect magic trailing commas.
57 | skip-magic-trailing-comma = false
58 |
59 | # Like Black, automatically detect the appropriate line ending.
60 | line-ending = "auto"
61 |
62 | # Enable auto-formatting of code examples in docstrings. Markdown,
63 | # reStructuredText code/literal blocks and doctests are all supported.
64 | #
65 | # This is currently disabled by default, but it is planned for this
66 | # to be opt-out in the future.
67 | docstring-code-format = false
68 |
69 | # Set the line length limit used when formatting code snippets in
70 | # docstrings.
71 | #
72 | # This only has an effect when the `docstring-code-format` setting is
73 | # enabled.
74 | docstring-code-line-length = "dynamic"
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/validate_export_is_a_single_frame.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Validator for checking that export is a single frame."""
3 | import hou
4 |
5 | from ayon_core.pipeline import (
6 | PublishValidationError,
7 | OptionalPyblishPluginMixin
8 | )
9 | from ayon_core.pipeline.publish import ValidateContentsOrder
10 | from ayon_houdini.api.action import SelectInvalidAction
11 | from ayon_houdini.api import plugin
12 |
13 |
14 | class ValidateSingleFrame(plugin.HoudiniInstancePlugin,
15 | OptionalPyblishPluginMixin):
16 | """Validate Export is a Single Frame.
17 |
18 | It checks if rop node is exporting one frame.
19 | This is mainly for Model product type.
20 | """
21 |
22 | families = ["model"]
23 | label = "Validate Single Frame"
24 | order = ValidateContentsOrder + 0.1
25 | actions = [SelectInvalidAction]
26 |
27 | def process(self, instance):
28 | if not self.is_active(instance.data):
29 | return
30 |
31 | invalid = self.get_invalid(instance)
32 | if invalid:
33 | raise PublishValidationError(
34 | "See log for details. "
35 | "Invalid ROP node: {0}".format(invalid[0].path())
36 | )
37 |
38 | @classmethod
39 | def get_invalid(cls, instance):
40 |
41 | frame_start = instance.data.get("frameStartHandle")
42 | frame_end = instance.data.get("frameEndHandle")
43 |
44 | # This happens if instance node has no 'trange' parameter.
45 | if frame_start is None or frame_end is None:
46 | cls.log.debug(
47 | "No frame data, skipping check.."
48 | )
49 | return
50 |
51 | if frame_start != frame_end:
52 | rop = hou.node(instance.data["instance_node"])
53 | cls.log.error(
54 | "Invalid frame range on '%s'."
55 | "You should use the same frame number for 'f1' "
56 | "and 'f2' parameters.",
57 | rop.path()
58 | )
59 | return [rop]
60 |
--------------------------------------------------------------------------------
/server/settings/main.py:
--------------------------------------------------------------------------------
1 | from ayon_server.settings import BaseSettingsModel, SettingsField
2 | from .general import (
3 | GeneralSettingsModel,
4 | DEFAULT_GENERAL_SETTINGS
5 | )
6 | from .imageio import (
7 | HoudiniImageIOModel,
8 | DEFAULT_IMAGEIO_SETTINGS
9 | )
10 | from .shelves import ShelvesModel
11 | from .create import (
12 | CreatePluginsModel,
13 | DEFAULT_HOUDINI_CREATE_SETTINGS
14 | )
15 | from .publish import (
16 | PublishPluginsModel,
17 | DEFAULT_HOUDINI_PUBLISH_SETTINGS,
18 | )
19 | from .load import (
20 | LoadPluginsModel,
21 | )
22 | from .templated_workfile_build import (
23 | TemplatedWorkfileBuildModel
24 | )
25 |
26 |
27 | class HoudiniSettings(BaseSettingsModel):
28 | general: GeneralSettingsModel = SettingsField(
29 | default_factory=GeneralSettingsModel,
30 | title="General"
31 | )
32 | imageio: HoudiniImageIOModel = SettingsField(
33 | default_factory=HoudiniImageIOModel,
34 | title="Color Management (ImageIO)"
35 | )
36 | shelves: list[ShelvesModel] = SettingsField(
37 | default_factory=list,
38 | title="Shelves Manager",
39 | )
40 | create: CreatePluginsModel = SettingsField(
41 | default_factory=CreatePluginsModel,
42 | title="Creator Plugins",
43 | )
44 | publish: PublishPluginsModel = SettingsField(
45 | default_factory=PublishPluginsModel,
46 | title="Publish Plugins",
47 | )
48 | load: LoadPluginsModel = SettingsField(
49 | default_factory=LoadPluginsModel,
50 | title="Loader Plugins",
51 | )
52 | templated_workfile_build: TemplatedWorkfileBuildModel = SettingsField(
53 | title="Templated Workfile Build",
54 | default_factory=TemplatedWorkfileBuildModel
55 | )
56 |
57 |
58 | DEFAULT_VALUES = {
59 | "general": DEFAULT_GENERAL_SETTINGS,
60 | "imageio": DEFAULT_IMAGEIO_SETTINGS,
61 | "shelves": [],
62 | "create": DEFAULT_HOUDINI_CREATE_SETTINGS,
63 | "publish": DEFAULT_HOUDINI_PUBLISH_SETTINGS,
64 | "templated_workfile_build": {
65 | "profiles": []
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Object_1generic__loader_8_81.0/Contents.dir/Contents.mime:
--------------------------------------------------------------------------------
1 | MIME-Version: 1.0
2 | Content-Type: multipart/mixed; boundary="HOUDINIMIMEBOUNDARY0xD3ADD339-0x00000F49-0x56B122C9-0x00000001HOUDINIMIMEBOUNDARY"
3 |
4 | --HOUDINIMIMEBOUNDARY0xD3ADD339-0x00000F49-0x56B122C9-0x00000001HOUDINIMIMEBOUNDARY
5 | Content-Disposition: attachment; filename="node_type"
6 | Content-Type: text/plain
7 |
8 | Object
9 |
10 | --HOUDINIMIMEBOUNDARY0xD3ADD339-0x00000F49-0x56B122C9-0x00000001HOUDINIMIMEBOUNDARY
11 | Content-Disposition: attachment; filename="hdaroot.init"
12 | Content-Type: text/plain
13 |
14 | type = ayon::generic_loader::1.0
15 | matchesdef = 0
16 |
17 | --HOUDINIMIMEBOUNDARY0xD3ADD339-0x00000F49-0x56B122C9-0x00000001HOUDINIMIMEBOUNDARY
18 | Content-Disposition: attachment; filename="hdaroot.def"
19 | Content-Type: text/plain
20 |
21 | objflags objflags = origin off
22 | pretransform UT_DMatrix4 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1
23 | comment ""
24 | position -2.4962 -1.66745
25 | connectornextid 0
26 | flags = lock off model off template off footprint off xray off bypass off display on render off highlight off unload off savedata off compress on colordefault on exposed on selectable on
27 | outputsNamed3
28 | {
29 | }
30 | inputsNamed3
31 | {
32 | }
33 | inputs
34 | {
35 | }
36 | stat
37 | {
38 | create -1
39 | modify -1
40 | author Mustafa_Taher@Major-Kalawy
41 | access 0777
42 | }
43 | color UT_Color RGB 0.8 0.8 0.8
44 | delscript ""
45 | exprlanguage hscript
46 | end
47 |
48 | --HOUDINIMIMEBOUNDARY0xD3ADD339-0x00000F49-0x56B122C9-0x00000001HOUDINIMIMEBOUNDARY
49 | Content-Disposition: attachment; filename="hdaroot.userdata"
50 | Content-Type: text/plain
51 |
52 | {
53 | "___Version___":{
54 | "type":"string",
55 | "value":""
56 | },
57 | "nodeshape":{
58 | "type":"string",
59 | "value":"null"
60 | }
61 | }
62 |
63 | --HOUDINIMIMEBOUNDARY0xD3ADD339-0x00000F49-0x56B122C9-0x00000001HOUDINIMIMEBOUNDARY
64 | Content-Disposition: attachment; filename="hdaroot.net"
65 | Content-Type: text/plain
66 |
67 | 1
68 |
69 | --HOUDINIMIMEBOUNDARY0xD3ADD339-0x00000F49-0x56B122C9-0x00000001HOUDINIMIMEBOUNDARY--
70 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/load/load_shot_lop.py:
--------------------------------------------------------------------------------
1 | from ayon_core.pipeline import load
2 | from ayon_houdini.api.lib import find_active_network
3 | from ayon_houdini.api import hda_utils
4 |
5 | import hou
6 |
7 |
8 | class LOPLoadShotLoader(load.LoaderPlugin):
9 | """Load sublayer into Solaris using AYON Load Shot LOP"""
10 |
11 | product_types = {"*"}
12 | label = "Load Shot (LOPs)"
13 | representations = ["usd", "abc", "usda", "usdc"]
14 | order = -10
15 | icon = "code-fork"
16 | color = "orange"
17 |
18 | def load(self, context, name=None, namespace=None, data=None):
19 |
20 | # Define node name
21 | namespace = namespace if namespace else context["folder"]["name"]
22 | node_name = "{}_{}".format(namespace, name) if namespace else name
23 |
24 | # Create node
25 | network = find_active_network(
26 | category=hou.lopNodeTypeCategory(),
27 | default="/stage"
28 | )
29 | node = network.createNode("ayon::load_shot", node_name=node_name)
30 | node.moveToGoodPosition()
31 |
32 | hda_utils.set_node_representation_from_context(node, context)
33 |
34 | nodes = [node]
35 | self[:] = nodes
36 |
37 | return node
38 |
39 | def update(self, container, context):
40 | node = container["node"]
41 | hda_utils.set_node_representation_from_context(node, context)
42 |
43 | def remove(self, container):
44 | node = container["node"]
45 | node.destroy()
46 |
47 | def switch(self, container, context):
48 | self.update(container, context)
49 |
50 | def create_load_placeholder_node(
51 | self, node_name: str, placeholder_data: dict
52 | ) -> hou.Node:
53 | """Define how to create a placeholder node for this loader for the
54 | Workfile Template Builder system."""
55 | # Create node
56 | network = find_active_network(
57 | category=hou.lopNodeTypeCategory(),
58 | default="/stage"
59 | )
60 | node = network.createNode("null", node_name=node_name)
61 | node.moveToGoodPosition()
62 | return node
63 |
64 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/load/load_asset_lop.py:
--------------------------------------------------------------------------------
1 | from ayon_core.pipeline import load
2 | from ayon_houdini.api.lib import find_active_network
3 | from ayon_houdini.api import hda_utils
4 |
5 | import hou
6 |
7 |
8 | class LOPLoadAssetLoader(load.LoaderPlugin):
9 | """Load reference/payload into Solaris using AYON `lop_import` LOP"""
10 |
11 | product_types = {"*"}
12 | label = "Load Asset (LOPs)"
13 | representations = ["usd", "abc", "usda", "usdc"]
14 | order = -10
15 | icon = "code-fork"
16 | color = "orange"
17 |
18 | def load(self, context, name=None, namespace=None, data=None):
19 |
20 | # Define node name
21 | namespace = namespace if namespace else context["folder"]["name"]
22 | node_name = "{}_{}".format(namespace, name) if namespace else name
23 |
24 | # Create node
25 | network = find_active_network(
26 | category=hou.lopNodeTypeCategory(),
27 | default="/stage"
28 | )
29 | node = network.createNode("ayon::lop_import", node_name=node_name)
30 | node.moveToGoodPosition()
31 |
32 | hda_utils.set_node_representation_from_context(node, context)
33 |
34 | nodes = [node]
35 | self[:] = nodes
36 |
37 | return node
38 |
39 | def update(self, container, context):
40 | node = container["node"]
41 | hda_utils.set_node_representation_from_context(node, context)
42 |
43 | def remove(self, container):
44 | node = container["node"]
45 | node.destroy()
46 |
47 | def switch(self, container, context):
48 | self.update(container, context)
49 |
50 | def create_load_placeholder_node(
51 | self, node_name: str, placeholder_data: dict
52 | ) -> hou.Node:
53 | """Define how to create a placeholder node for this loader for the
54 | Workfile Template Builder system."""
55 | # Create node
56 | network = find_active_network(
57 | category=hou.lopNodeTypeCategory(),
58 | default="/stage"
59 | )
60 | node = network.createNode("null", node_name=node_name)
61 | node.moveToGoodPosition()
62 | return node
63 |
64 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Lop_1generic__loader_8_81.0/ExtraFileOptions:
--------------------------------------------------------------------------------
1 | {
2 | "OnCreated/Cursor":{
3 | "type":"intarray",
4 | "value":[14,1]
5 | },
6 | "OnCreated/IsExpr":{
7 | "type":"bool",
8 | "value":false
9 | },
10 | "OnCreated/IsPython":{
11 | "type":"bool",
12 | "value":true
13 | },
14 | "OnCreated/IsScript":{
15 | "type":"bool",
16 | "value":true
17 | },
18 | "OnCreated/Source":{
19 | "type":"string",
20 | "value":""
21 | },
22 | "OnDeleted/Cursor":{
23 | "type":"intarray",
24 | "value":[7,1]
25 | },
26 | "OnDeleted/IsExpr":{
27 | "type":"bool",
28 | "value":false
29 | },
30 | "OnDeleted/IsPython":{
31 | "type":"bool",
32 | "value":true
33 | },
34 | "OnDeleted/IsScript":{
35 | "type":"bool",
36 | "value":true
37 | },
38 | "OnDeleted/Source":{
39 | "type":"string",
40 | "value":""
41 | },
42 | "OnLoaded/Cursor":{
43 | "type":"intarray",
44 | "value":[5,1]
45 | },
46 | "OnLoaded/IsExpr":{
47 | "type":"bool",
48 | "value":false
49 | },
50 | "OnLoaded/IsPython":{
51 | "type":"bool",
52 | "value":true
53 | },
54 | "OnLoaded/IsScript":{
55 | "type":"bool",
56 | "value":true
57 | },
58 | "OnLoaded/Source":{
59 | "type":"string",
60 | "value":""
61 | },
62 | "OnNameChanged/Cursor":{
63 | "type":"intarray",
64 | "value":[9,1]
65 | },
66 | "OnNameChanged/IsExpr":{
67 | "type":"bool",
68 | "value":false
69 | },
70 | "OnNameChanged/IsPython":{
71 | "type":"bool",
72 | "value":true
73 | },
74 | "OnNameChanged/IsScript":{
75 | "type":"bool",
76 | "value":true
77 | },
78 | "OnNameChanged/Source":{
79 | "type":"string",
80 | "value":""
81 | },
82 | "PythonModule/Cursor":{
83 | "type":"intarray",
84 | "value":[7,35]
85 | },
86 | "PythonModule/IsExpr":{
87 | "type":"bool",
88 | "value":false
89 | },
90 | "PythonModule/IsPython":{
91 | "type":"bool",
92 | "value":true
93 | },
94 | "PythonModule/IsScript":{
95 | "type":"bool",
96 | "value":true
97 | },
98 | "PythonModule/Source":{
99 | "type":"string",
100 | "value":""
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Object_1generic__loader_8_81.0/ExtraFileOptions:
--------------------------------------------------------------------------------
1 | {
2 | "OnCreated/Cursor":{
3 | "type":"intarray",
4 | "value":[14,1]
5 | },
6 | "OnCreated/IsExpr":{
7 | "type":"bool",
8 | "value":false
9 | },
10 | "OnCreated/IsPython":{
11 | "type":"bool",
12 | "value":true
13 | },
14 | "OnCreated/IsScript":{
15 | "type":"bool",
16 | "value":true
17 | },
18 | "OnCreated/Source":{
19 | "type":"string",
20 | "value":""
21 | },
22 | "OnDeleted/Cursor":{
23 | "type":"intarray",
24 | "value":[2,1]
25 | },
26 | "OnDeleted/IsExpr":{
27 | "type":"bool",
28 | "value":false
29 | },
30 | "OnDeleted/IsPython":{
31 | "type":"bool",
32 | "value":true
33 | },
34 | "OnDeleted/IsScript":{
35 | "type":"bool",
36 | "value":true
37 | },
38 | "OnDeleted/Source":{
39 | "type":"string",
40 | "value":""
41 | },
42 | "OnLoaded/Cursor":{
43 | "type":"intarray",
44 | "value":[11,1]
45 | },
46 | "OnLoaded/IsExpr":{
47 | "type":"bool",
48 | "value":false
49 | },
50 | "OnLoaded/IsPython":{
51 | "type":"bool",
52 | "value":true
53 | },
54 | "OnLoaded/IsScript":{
55 | "type":"bool",
56 | "value":true
57 | },
58 | "OnLoaded/Source":{
59 | "type":"string",
60 | "value":""
61 | },
62 | "OnNameChanged/Cursor":{
63 | "type":"intarray",
64 | "value":[9,1]
65 | },
66 | "OnNameChanged/IsExpr":{
67 | "type":"bool",
68 | "value":false
69 | },
70 | "OnNameChanged/IsPython":{
71 | "type":"bool",
72 | "value":true
73 | },
74 | "OnNameChanged/IsScript":{
75 | "type":"bool",
76 | "value":true
77 | },
78 | "OnNameChanged/Source":{
79 | "type":"string",
80 | "value":""
81 | },
82 | "PythonModule/Cursor":{
83 | "type":"intarray",
84 | "value":[10,1]
85 | },
86 | "PythonModule/IsExpr":{
87 | "type":"bool",
88 | "value":false
89 | },
90 | "PythonModule/IsPython":{
91 | "type":"bool",
92 | "value":true
93 | },
94 | "PythonModule/IsScript":{
95 | "type":"bool",
96 | "value":true
97 | },
98 | "PythonModule/Source":{
99 | "type":"string",
100 | "value":""
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon.generic_loader.hda/ayon_8_8Sop_1generic__loader_8_81.0/ExtraFileOptions:
--------------------------------------------------------------------------------
1 | {
2 | "OnCreated/Cursor":{
3 | "type":"intarray",
4 | "value":[11,71]
5 | },
6 | "OnCreated/IsExpr":{
7 | "type":"bool",
8 | "value":false
9 | },
10 | "OnCreated/IsPython":{
11 | "type":"bool",
12 | "value":true
13 | },
14 | "OnCreated/IsScript":{
15 | "type":"bool",
16 | "value":true
17 | },
18 | "OnCreated/Source":{
19 | "type":"string",
20 | "value":""
21 | },
22 | "OnDeleted/Cursor":{
23 | "type":"intarray",
24 | "value":[7,1]
25 | },
26 | "OnDeleted/IsExpr":{
27 | "type":"bool",
28 | "value":false
29 | },
30 | "OnDeleted/IsPython":{
31 | "type":"bool",
32 | "value":true
33 | },
34 | "OnDeleted/IsScript":{
35 | "type":"bool",
36 | "value":true
37 | },
38 | "OnDeleted/Source":{
39 | "type":"string",
40 | "value":""
41 | },
42 | "OnLoaded/Cursor":{
43 | "type":"intarray",
44 | "value":[15,1]
45 | },
46 | "OnLoaded/IsExpr":{
47 | "type":"bool",
48 | "value":false
49 | },
50 | "OnLoaded/IsPython":{
51 | "type":"bool",
52 | "value":true
53 | },
54 | "OnLoaded/IsScript":{
55 | "type":"bool",
56 | "value":true
57 | },
58 | "OnLoaded/Source":{
59 | "type":"string",
60 | "value":""
61 | },
62 | "OnNameChanged/Cursor":{
63 | "type":"intarray",
64 | "value":[9,1]
65 | },
66 | "OnNameChanged/IsExpr":{
67 | "type":"bool",
68 | "value":false
69 | },
70 | "OnNameChanged/IsPython":{
71 | "type":"bool",
72 | "value":true
73 | },
74 | "OnNameChanged/IsScript":{
75 | "type":"bool",
76 | "value":true
77 | },
78 | "OnNameChanged/Source":{
79 | "type":"string",
80 | "value":""
81 | },
82 | "PythonModule/Cursor":{
83 | "type":"intarray",
84 | "value":[11,1]
85 | },
86 | "PythonModule/IsExpr":{
87 | "type":"bool",
88 | "value":false
89 | },
90 | "PythonModule/IsPython":{
91 | "type":"bool",
92 | "value":true
93 | },
94 | "PythonModule/IsScript":{
95 | "type":"bool",
96 | "value":true
97 | },
98 | "PythonModule/Source":{
99 | "type":"string",
100 | "value":""
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/validate_mesh_is_static.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Validator for correct naming of Static Meshes."""
3 | from ayon_core.pipeline import (
4 | PublishValidationError,
5 | OptionalPyblishPluginMixin
6 | )
7 | from ayon_core.pipeline.publish import ValidateContentsOrder
8 |
9 | from ayon_houdini.api import plugin
10 | from ayon_houdini.api.action import SelectInvalidAction
11 | from ayon_houdini.api.lib import get_output_children
12 |
13 |
14 | class ValidateMeshIsStatic(plugin.HoudiniInstancePlugin,
15 | OptionalPyblishPluginMixin):
16 | """Validate mesh is static.
17 |
18 | It checks if output node is time dependent.
19 | this avoids getting different output from ROP node when extracted
20 | from a different frame than the first frame.
21 | (Might be overly restrictive though)
22 | """
23 |
24 | families = ["staticMesh",
25 | "model"]
26 | label = "Validate Mesh is Static"
27 | order = ValidateContentsOrder + 0.1
28 | actions = [SelectInvalidAction]
29 |
30 | def process(self, instance):
31 | if not self.is_active(instance.data):
32 | return
33 |
34 | invalid = self.get_invalid(instance)
35 | if invalid:
36 | nodes = [n.path() for n in invalid]
37 | raise PublishValidationError(
38 | "See log for details. "
39 | "Invalid nodes: {0}".format(nodes)
40 | )
41 |
42 | @classmethod
43 | def get_invalid(cls, instance):
44 |
45 | invalid = []
46 |
47 | output_node = instance.data.get("output_node")
48 | if output_node is None:
49 | cls.log.debug(
50 | "No Output Node, skipping check.."
51 | )
52 | return
53 |
54 | all_outputs = get_output_children(output_node)
55 |
56 | for output in all_outputs:
57 | if output.isTimeDependent():
58 | invalid.append(output)
59 | cls.log.error(
60 | "Output node '%s' is time dependent.",
61 | output.path()
62 | )
63 |
64 | return invalid
65 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_import.hda/ayon_8_8Lop_1lop__import_8_81.0/ExtraFileOptions:
--------------------------------------------------------------------------------
1 | {
2 | "AYON_icon.png/Cursor":{
3 | "type":"intarray",
4 | "value":[0,0]
5 | },
6 | "AYON_icon.png/IsExpr":{
7 | "type":"bool",
8 | "value":false
9 | },
10 | "AYON_icon.png/IsPython":{
11 | "type":"bool",
12 | "value":false
13 | },
14 | "AYON_icon.png/IsScript":{
15 | "type":"bool",
16 | "value":false
17 | },
18 | "AYON_icon.png/Source":{
19 | "type":"string",
20 | "value":"C:/Users/Maqina-05/Desktop/AYON_icon.png"
21 | },
22 | "OnCreated/Cursor":{
23 | "type":"intarray",
24 | "value":[1,15]
25 | },
26 | "OnCreated/IsExpr":{
27 | "type":"bool",
28 | "value":false
29 | },
30 | "OnCreated/IsPython":{
31 | "type":"bool",
32 | "value":true
33 | },
34 | "OnCreated/IsScript":{
35 | "type":"bool",
36 | "value":true
37 | },
38 | "OnCreated/Source":{
39 | "type":"string",
40 | "value":""
41 | },
42 | "OnLoaded/Cursor":{
43 | "type":"intarray",
44 | "value":[3,27]
45 | },
46 | "OnLoaded/IsExpr":{
47 | "type":"bool",
48 | "value":false
49 | },
50 | "OnLoaded/IsPython":{
51 | "type":"bool",
52 | "value":true
53 | },
54 | "OnLoaded/IsScript":{
55 | "type":"bool",
56 | "value":true
57 | },
58 | "OnLoaded/Source":{
59 | "type":"string",
60 | "value":""
61 | },
62 | "OnNameChanged/Cursor":{
63 | "type":"intarray",
64 | "value":[1,15]
65 | },
66 | "OnNameChanged/IsExpr":{
67 | "type":"bool",
68 | "value":false
69 | },
70 | "OnNameChanged/IsPython":{
71 | "type":"bool",
72 | "value":true
73 | },
74 | "OnNameChanged/IsScript":{
75 | "type":"bool",
76 | "value":true
77 | },
78 | "OnNameChanged/Source":{
79 | "type":"string",
80 | "value":""
81 | },
82 | "PythonModule/Cursor":{
83 | "type":"intarray",
84 | "value":[5,1]
85 | },
86 | "PythonModule/IsExpr":{
87 | "type":"bool",
88 | "value":false
89 | },
90 | "PythonModule/IsPython":{
91 | "type":"bool",
92 | "value":true
93 | },
94 | "PythonModule/IsScript":{
95 | "type":"bool",
96 | "value":true
97 | },
98 | "PythonModule/Source":{
99 | "type":"string",
100 | "value":""
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/server/settings/shelves.py:
--------------------------------------------------------------------------------
1 | from ayon_server.settings import (
2 | BaseSettingsModel,
3 | SettingsField,
4 | MultiplatformPathModel
5 | )
6 |
7 |
8 | class ShelfToolsModel(BaseSettingsModel):
9 | """Name and Script Path are mandatory."""
10 | label: str = SettingsField(title="Name")
11 | script: str = SettingsField(title="Script Path")
12 | icon: str = SettingsField("", title="Icon Path")
13 | help: str = SettingsField("", title="Help text")
14 |
15 |
16 | class ShelfDefinitionModel(BaseSettingsModel):
17 | _layout = "expanded"
18 | shelf_name: str = SettingsField(title="Shelf name")
19 | tools_list: list[ShelfToolsModel] = SettingsField(
20 | default_factory=list,
21 | title="Shelf Tools"
22 | )
23 |
24 |
25 | class AddShelfFileModel(BaseSettingsModel):
26 | shelf_set_source_path: MultiplatformPathModel = SettingsField(
27 | default_factory=MultiplatformPathModel,
28 | title="Shelf Set Path"
29 | )
30 |
31 |
32 | class AddSetAndDefinitionsModel(BaseSettingsModel):
33 | shelf_set_name: str = SettingsField("", title="Shelf Set Name")
34 | shelf_definition: list[ShelfDefinitionModel] = SettingsField(
35 | default_factory=list,
36 | title="Shelves Definitions"
37 | )
38 |
39 |
40 | def shelves_enum_options():
41 | return [
42 | {
43 | "value": "add_shelf_file",
44 | "label": "Add a .shelf file"
45 | },
46 | {
47 | "value": "add_set_and_definitions",
48 | "label": "Add Shelf Set Name and Shelves Definitions"
49 | }
50 | ]
51 |
52 |
53 | class ShelvesModel(BaseSettingsModel):
54 | options: str = SettingsField(
55 | title="Options",
56 | description="Switch between shelves manager options",
57 | enum_resolver=shelves_enum_options,
58 | conditional_enum=True
59 | )
60 | add_shelf_file: AddShelfFileModel = SettingsField(
61 | title="Add a .shelf file",
62 | default_factory=AddShelfFileModel
63 | )
64 | add_set_and_definitions: AddSetAndDefinitionsModel = SettingsField(
65 | title="Add Shelf Set Name and Shelves Definitions",
66 | default_factory=AddSetAndDefinitionsModel
67 | )
68 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/validate_wait_for_render.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import hou
3 |
4 | import pyblish.api
5 | from ayon_core.pipeline import PublishValidationError
6 | from ayon_core.pipeline.publish import RepairAction
7 |
8 | from ayon_houdini.api import plugin
9 |
10 |
11 | class ValidateWaitForRender(plugin.HoudiniInstancePlugin):
12 | """Validate `WaitForRendertoComplete` is enabled.
13 |
14 | Disabling `WaitForRendertoComplete` cause the local render to fail
15 | as the publish execution continues while the render may not be
16 | finished yet.
17 | """
18 |
19 | order = pyblish.api.ValidatorOrder
20 | families = ["usdrender"]
21 | label = "Validate Wait For Render to Complete"
22 | actions = [RepairAction]
23 |
24 | def process(self, instance):
25 | if not instance.data.get("instance_node"):
26 | # Ignore instances without an instance node
27 | # e.g. in memory bootstrap instances
28 | self.log.debug(
29 | f"Skipping instance without instance node: {instance}"
30 | )
31 | return
32 |
33 | if instance.data["creator_attributes"].get("render_target") != "local":
34 | # This validator should work only with local render target.
35 | self.log.debug(
36 | "Skipping Validator, Render target"
37 | " is not 'Local machine rendering'"
38 | )
39 | return
40 |
41 | invalid = self.get_invalid(instance)
42 | if invalid:
43 | rop = invalid[0]
44 | raise PublishValidationError(
45 | f"ROP node '{rop.path()}' has 'Wait For Render"
46 | " to Complete' parm disabled.Please, enable it.",
47 | title=self.label
48 | )
49 |
50 | @classmethod
51 | def get_invalid(cls, instance):
52 | rop = hou.node(instance.data["instance_node"])
53 | if not rop.evalParm("soho_foreground"):
54 | return [rop]
55 |
56 | @classmethod
57 | def repair(cls, instance):
58 | """Enable WaitForRendertoComplete. """
59 |
60 | rop = hou.node(instance.data["instance_node"])
61 | rop.parm("soho_foreground").set(True)
62 |
63 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/extract_active_view_thumbnail.py:
--------------------------------------------------------------------------------
1 | import tempfile
2 | import pyblish.api
3 |
4 | from ayon_core.pipeline import OptionalPyblishPluginMixin
5 | from ayon_houdini.api import lib, plugin
6 | from ayon_houdini.api.pipeline import IS_HEADLESS
7 |
8 |
9 | class ExtractActiveViewThumbnail(plugin.HoudiniExtractorPlugin,
10 | OptionalPyblishPluginMixin):
11 | """Set instance thumbnail to a screengrab of current active viewport.
12 |
13 | This makes it so that if an instance does not have a thumbnail set yet that
14 | it will get a thumbnail of the currently active view at the time of
15 | publishing as a fallback.
16 |
17 | """
18 | order = pyblish.api.ExtractorOrder + 0.49
19 | label = "Extract Active View Thumbnail"
20 | families = ["workfile"]
21 |
22 | def process(self, instance):
23 | if not self.is_active(instance.data):
24 | return
25 |
26 | if IS_HEADLESS:
27 | self.log.debug(
28 | "Skip extraction of active view thumbnail, due to being in"
29 | "headless mode."
30 | )
31 | return
32 |
33 | thumbnail = instance.data.get("thumbnailPath")
34 | if thumbnail:
35 | # A thumbnail was already set for this instance
36 | return
37 |
38 | view_thumbnail = self.get_view_thumbnail(instance)
39 | if not view_thumbnail:
40 | return
41 | self.log.debug("Setting instance thumbnail path to: {}"
42 | .format(view_thumbnail)
43 | )
44 | instance.data["thumbnailPath"] = view_thumbnail
45 |
46 | def get_view_thumbnail(self, instance):
47 |
48 | sceneview = lib.get_scene_viewer()
49 | if sceneview is None:
50 | self.log.debug("Skipping Extract Active View Thumbnail"
51 | " because no scene view was detected.")
52 | return
53 |
54 | with tempfile.NamedTemporaryFile(
55 | "w", suffix=".jpg", delete=False
56 | ) as tmp:
57 | thumbnail_path = tmp.name
58 | lib.sceneview_snapshot(sceneview, thumbnail_path)
59 |
60 | instance.context.data["cleanupFullPaths"].append(thumbnail_path)
61 | return thumbnail_path
62 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/validate_alembic_input_node.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import hou
3 | import pyblish.api
4 | from ayon_core.pipeline import PublishValidationError
5 |
6 | from ayon_houdini.api import plugin
7 |
8 |
9 | class ValidateAlembicInputNode(plugin.HoudiniInstancePlugin):
10 | """Validate that the node connected to the output is correct.
11 |
12 | The connected node cannot be of the following types for Alembic:
13 | - VDB
14 | - Volume
15 |
16 | """
17 |
18 | order = pyblish.api.ValidatorOrder + 0.1
19 | families = ["abc"]
20 | label = "Validate Input Node (Abc)"
21 |
22 | def process(self, instance):
23 | invalid = self.get_invalid(instance)
24 | if invalid:
25 | raise PublishValidationError(
26 | ("Primitive types found that are not supported "
27 | "for Alembic output."),
28 | title=self.label
29 | )
30 |
31 | @classmethod
32 | def get_invalid(cls, instance):
33 |
34 | invalid_prim_types = ["VDB", "Volume"]
35 | output_node = instance.data.get("output_node")
36 |
37 | if output_node is None:
38 | node = hou.node(instance.data["instance_node"])
39 | cls.log.error(
40 | "SOP Output node in '%s' does not exist. "
41 | "Ensure a valid SOP output path is set." % node.path()
42 | )
43 |
44 | return [node]
45 |
46 | if not hasattr(output_node, "geometry"):
47 | # In the case someone has explicitly set an Object
48 | # node instead of a SOP node in Geometry context
49 | # then for now we ignore - this allows us to also
50 | # export object transforms.
51 | cls.log.warning("No geometry output node found, skipping check..")
52 | return
53 |
54 | frame = instance.data.get("frameStart", 0)
55 | geo = output_node.geometryAtFrame(frame)
56 |
57 | invalid = False
58 | for prim_type in invalid_prim_types:
59 | if geo.countPrimType(prim_type) > 0:
60 | cls.log.error(
61 | "Found a primitive which is of type '%s' !" % prim_type
62 | )
63 | invalid = True
64 |
65 | if invalid:
66 | return [output_node]
67 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/validate_camera_rop.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Validator plugin for Houdini Camera ROP settings."""
3 | import pyblish.api
4 | from ayon_core.pipeline import PublishValidationError
5 |
6 | from ayon_houdini.api import plugin
7 |
8 |
9 | class ValidateCameraROP(plugin.HoudiniInstancePlugin):
10 | """Validate Camera ROP settings."""
11 |
12 | order = pyblish.api.ValidatorOrder
13 | families = ["camera"]
14 | label = "Camera ROP"
15 |
16 | def process(self, instance):
17 |
18 | import hou
19 |
20 | node = hou.node(instance.data.get("instance_node"))
21 | if node.parm("use_sop_path").eval():
22 | raise PublishValidationError(
23 | ("Alembic ROP for Camera export should not be "
24 | "set to 'Use Sop Path'. Please disable."),
25 | title=self.label
26 | )
27 |
28 | # Get the root and objects parameter of the Alembic ROP node
29 | root = node.parm("root").eval()
30 | objects = node.parm("objects").eval()
31 | errors = []
32 | if not root:
33 | errors.append("Root parameter must be set on Alembic ROP")
34 | if not root.startswith("/"):
35 | errors.append("Root parameter must start with slash /")
36 | if not objects:
37 | errors.append("Objects parameter must be set on Alembic ROP")
38 | if len(objects.split(" ")) != 1:
39 | errors.append("Must have only a single object.")
40 |
41 | if errors:
42 | for error in errors:
43 | self.log.error(error)
44 | raise PublishValidationError(
45 | "Some checks failed, see validator log.",
46 | title=self.label)
47 |
48 | # Check if the object exists and is a camera
49 | path = root + "/" + objects
50 | camera = hou.node(path)
51 |
52 | if not camera:
53 | raise PublishValidationError(
54 | "Camera path does not exist: %s" % path,
55 | title=self.label)
56 |
57 | if camera.type().name() != "cam":
58 | raise PublishValidationError(
59 | ("Object set in Alembic ROP is not a camera: "
60 | "{} (type: {})").format(camera, camera.type().name()),
61 | title=self.label)
62 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/husdplugins/outputprocessors/remap_to_publish.py:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 |
4 | import hou
5 | from husd.outputprocessor import OutputProcessor
6 |
7 |
8 | _COMPATIBILITY_PLACEHOLDER = object()
9 |
10 |
11 | class AYONRemapPaths(OutputProcessor):
12 | """Remap paths based on a mapping dict on rop node."""
13 |
14 | def __init__(self):
15 | self._mapping = dict()
16 |
17 | @staticmethod
18 | def name():
19 | return "ayon_remap_paths"
20 |
21 | @staticmethod
22 | def displayName():
23 | return "AYON Remap Paths"
24 |
25 | @staticmethod
26 | def hidden():
27 | return True
28 |
29 | @staticmethod
30 | def parameters():
31 | group = hou.ParmTemplateGroup()
32 |
33 | parm_template = hou.StringParmTemplate(
34 | "ayon_remap_paths_remap_json",
35 | "Remapping dict (json)",
36 | default_value="{}",
37 | num_components=1,
38 | string_type=hou.stringParmType.Regular,
39 | )
40 | group.append(parm_template)
41 |
42 | return group.asDialogScript()
43 |
44 | def beginSave(self,
45 | config_node,
46 | config_overrides,
47 | lop_node,
48 | t,
49 | # Added in Houdini 20.5.182
50 | stage_variables=_COMPATIBILITY_PLACEHOLDER):
51 |
52 | args = [config_node, config_overrides, lop_node, t]
53 | if stage_variables is not _COMPATIBILITY_PLACEHOLDER:
54 | args.append(stage_variables)
55 | super(AYONRemapPaths, self).beginSave(*args)
56 |
57 | value = config_node.evalParm("ayon_remap_paths_remap_json")
58 | mapping = json.loads(value)
59 | assert isinstance(self._mapping, dict)
60 |
61 | # Ensure all keys are normalized paths so the lookup can be done
62 | # correctly
63 | mapping = {
64 | os.path.normpath(key): value for key, value in mapping.items()
65 | }
66 | self._mapping = mapping
67 |
68 | def processReferencePath(self,
69 | asset_path,
70 | referencing_layer_path,
71 | asset_is_layer):
72 | return self._mapping.get(os.path.normpath(asset_path), asset_path)
73 |
74 |
75 | def usdOutputProcessor():
76 | return AYONRemapPaths
77 |
--------------------------------------------------------------------------------
/client/ayon_houdini/api/colorspace.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | import attr
4 | import hou
5 | from ayon_houdini.api.lib import get_color_management_preferences
6 | from ayon_core.pipeline.colorspace import (
7 | get_display_view_colorspace_name,
8 | get_ocio_config_colorspaces
9 | )
10 |
11 |
12 | @attr.s
13 | class LayerMetadata(object):
14 | """Data class for Render Layer metadata."""
15 | products: "List[RenderProduct]" = attr.ib()
16 |
17 |
18 | @attr.s
19 | class RenderProduct(object):
20 | """Specific Render Product Parameter for submitting."""
21 | colorspace = attr.ib() # colorspace
22 | productName = attr.ib(default=None)
23 |
24 |
25 | class ARenderProduct(object):
26 | """This is the minimal data structure required to get
27 | `ayon_core.pipeline.farm.pyblish_functions.create_instances_for_aov` to
28 | work with deadline addon's job submissions."""
29 | # TODO: The exact data structure should actually be defined in core for all
30 | # addons to align.
31 | def __init__(self, aov_names: List[str]):
32 | colorspace = get_scene_linear_colorspace()
33 | products = [
34 | RenderProduct(colorspace=colorspace, productName=aov_name)
35 | for aov_name in aov_names
36 | ]
37 | self.layer_data = LayerMetadata(products=products)
38 |
39 |
40 | def get_scene_linear_colorspace():
41 | """Return colorspace name for Houdini's OCIO config scene linear role.
42 |
43 | By default, renderers in Houdini render output images in the scene linear
44 | role colorspace.
45 |
46 | Returns:
47 | Optional[str]: The colorspace name for the 'scene_linear' role in
48 | the OCIO config Houdini is currently set to.
49 | """
50 | ocio_config_path = hou.Color.ocio_configPath()
51 | colorspaces = get_ocio_config_colorspaces(ocio_config_path)
52 | return colorspaces["roles"].get("scene_linear", {}).get("colorspace")
53 |
54 |
55 | def get_default_display_view_colorspace() -> str:
56 | """Returns the colorspace attribute of the default (display, view) pair.
57 |
58 | It's used for 'ociocolorspace' parm in OpenGL Node."""
59 |
60 | prefs = get_color_management_preferences()
61 | colorspace = get_display_view_colorspace_name(
62 | config_path=prefs["config"],
63 | display=prefs["display"],
64 | view=prefs["view"]
65 | )
66 | return colorspace
67 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/validate_render_products.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import inspect
3 | import hou
4 | import pyblish.api
5 |
6 | from ayon_core.pipeline import PublishValidationError
7 |
8 | from ayon_houdini.api.action import SelectROPAction
9 | from ayon_houdini.api import plugin
10 |
11 |
12 | class ValidateUsdRenderProducts(plugin.HoudiniInstancePlugin):
13 | """Validate at least one render product is present"""
14 |
15 | order = pyblish.api.ValidatorOrder
16 | families = ["usdrender"]
17 | hosts = ["houdini"]
18 | label = "Validate Render Products"
19 | actions = [SelectROPAction]
20 |
21 | def get_description(self):
22 | return inspect.cleandoc(
23 | """### No Render Products
24 |
25 | The render submission specified no Render Product outputs and
26 | as such would not generate any rendered files.
27 |
28 | This is usually the case if no Render Settings or Render
29 | Products were created.
30 |
31 | Make sure to create the Render Settings
32 | relevant to the renderer you want to use.
33 |
34 | """
35 | )
36 |
37 | def process(self, instance):
38 |
39 | node_path = instance.data["instance_node"]
40 | if not instance.data.get("output_node"):
41 |
42 | # Report LOP path parm for better logs
43 | lop_path_parm = hou.node(node_path).parm("loppath")
44 | if lop_path_parm:
45 | value = lop_path_parm.evalAsString()
46 | self.log.warning(
47 | f"ROP node 'loppath' parm is set to: '{value}'")
48 |
49 | raise PublishValidationError(
50 | f"No valid LOP path configured on ROP "
51 | f"'{node_path}'.",
52 | title="Invalid LOP path")
53 |
54 | if not instance.data.get("files", []):
55 | node = hou.node(node_path)
56 | rendersettings_path = (
57 | node.evalParm("rendersettings") or "/Render/rendersettings"
58 | )
59 | raise PublishValidationError(
60 | message=(
61 | "No Render Products found in Render Settings "
62 | "for '{}' at '{}'".format(node_path, rendersettings_path)
63 | ),
64 | description=self.get_description(),
65 | title=self.label
66 | )
67 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/load/actions.py:
--------------------------------------------------------------------------------
1 | """A module containing generic loader actions that will display in the Loader.
2 |
3 | """
4 |
5 | from ayon_houdini.api import plugin
6 |
7 |
8 | class SetFrameRangeLoader(plugin.HoudiniLoader):
9 | """Set frame range excluding pre- and post-handles"""
10 |
11 | product_types = {
12 | "animation",
13 | "camera",
14 | "pointcache",
15 | "vdbcache",
16 | "usd",
17 | }
18 | representations = {"abc", "vdb", "usd"}
19 |
20 | label = "Set frame range"
21 | order = 11
22 | icon = "clock-o"
23 | color = "white"
24 |
25 | def load(self, context, name, namespace, data):
26 |
27 | import hou
28 |
29 | version_attributes = context["version"]["attrib"]
30 |
31 | start = version_attributes.get("frameStart")
32 | end = version_attributes.get("frameEnd")
33 |
34 | if start is None or end is None:
35 | print(
36 | "Skipping setting frame range because start or "
37 | "end frame data is missing.."
38 | )
39 | return
40 |
41 | hou.playbar.setFrameRange(start, end)
42 | hou.playbar.setPlaybackRange(start, end)
43 |
44 |
45 | class SetFrameRangeWithHandlesLoader(plugin.HoudiniLoader):
46 | """Set frame range including pre- and post-handles"""
47 |
48 | product_types = {
49 | "animation",
50 | "camera",
51 | "pointcache",
52 | "vdbcache",
53 | "usd",
54 | }
55 | representations = {"abc", "vdb", "usd"}
56 |
57 | label = "Set frame range (with handles)"
58 | order = 12
59 | icon = "clock-o"
60 | color = "white"
61 |
62 | def load(self, context, name, namespace, data):
63 |
64 | import hou
65 |
66 | version_attributes = context["version"]["attrib"]
67 |
68 | start = version_attributes.get("frameStart")
69 | end = version_attributes.get("frameEnd")
70 |
71 | if start is None or end is None:
72 | print(
73 | "Skipping setting frame range because start or "
74 | "end frame data is missing.."
75 | )
76 | return
77 |
78 | # Include handles
79 | start -= version_attributes.get("handleStart", 0)
80 | end += version_attributes.get("handleEnd", 0)
81 |
82 | hou.playbar.setFrameRange(start, end)
83 | hou.playbar.setPlaybackRange(start, end)
84 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/collect_render_colorspace.py:
--------------------------------------------------------------------------------
1 | from ayon_houdini.api import plugin, colorspace
2 |
3 | import pyblish.api
4 |
5 |
6 | class CollectHoudiniRenderColorspace(plugin.HoudiniInstancePlugin):
7 | """Collect Colorspace data for render output images.
8 |
9 | This currently assumes that all render products are in 'scene_linear'
10 | colorspace role - which is the default behavior for renderers in Houdini.
11 | """
12 |
13 | label = "Collect Render Colorspace"
14 | order = pyblish.api.CollectorOrder + 0.15
15 | families = ["mantra_rop",
16 | "karma_rop",
17 | "redshift_rop",
18 | "arnold_rop",
19 | "vray_rop",
20 | "usdrender"]
21 |
22 | def process(self, instance):
23 | # Set the required data for `ayon_core.pipeline.farm.pyblish_functions`
24 | # functions used for farm publish job processing.
25 |
26 | # Define render products for `create_instances_for_aov`
27 | # which uses it in `_create_instances_for_aov()` to match the render
28 | # product's name to aovs to define the colorspace.
29 | expected_files = instance.data.get("expectedFiles")
30 | if not expected_files:
31 | self.log.debug("No expected files found. "
32 | "Skipping collecting of render colorspace.")
33 | return
34 | aov_name = list(expected_files[0].keys())
35 | render_products_data = colorspace.ARenderProduct(aov_name)
36 | instance.data["renderProducts"] = render_products_data
37 |
38 | # Required data for `create_instances_for_aov`
39 | colorspace_data = colorspace.get_color_management_preferences()
40 | instance.data["colorspaceConfig"] = colorspace_data["config"]
41 | instance.data["colorspaceDisplay"] = colorspace_data["display"]
42 | instance.data["colorspaceView"] = colorspace_data["view"]
43 |
44 | # Used in `create_skeleton_instance()`
45 | instance.data["colorspace"] = colorspace.get_scene_linear_colorspace()
46 |
47 | self.log.debug(
48 | "Collected OCIO color information:\n"
49 | f" - Config: {instance.data['colorspaceConfig']}\n"
50 | f" - Colorspace: {instance.data['colorspace']}\n"
51 | f" - Scene Display: {instance.data['colorspaceDisplay']}\n"
52 | f" - Scene View: {instance.data['colorspaceView']}\n"
53 | )
54 |
--------------------------------------------------------------------------------
/client/ayon_houdini/hooks/set_default_display_and_view.py:
--------------------------------------------------------------------------------
1 | from ayon_applications import PreLaunchHook, LaunchTypes
2 |
3 |
4 | class SetDefaultDisplayView(PreLaunchHook):
5 | """Set default view and default display for houdini via OpenColorIO.
6 |
7 | Houdini's defaultDisplay and defaultView are set by
8 | setting 'OCIO_ACTIVE_DISPLAYS' and 'OCIO_ACTIVE_VIEWS'
9 | environment variables respectively.
10 |
11 | More info: https://www.sidefx.com/docs/houdini/io/ocio.html#set-up
12 | """
13 |
14 | app_groups = {"houdini"}
15 | launch_types = {LaunchTypes.local}
16 |
17 | def execute(self):
18 |
19 | OCIO = self.launch_context.env.get("OCIO")
20 |
21 | # This is a cheap way to skip this hook if either global color
22 | # management or houdini color management was disabled because the
23 | # OCIO var would be set by the global OCIOEnvHook
24 | if not OCIO:
25 | return
26 |
27 | # workfile settings added in '0.2.13'
28 | houdini_color_settings = \
29 | self.data["project_settings"]["houdini"]["imageio"].get("workfile")
30 |
31 | if not houdini_color_settings:
32 | self.log.info("Hook 'SetDefaultDisplayView' requires Houdini "
33 | "addon version >= '0.2.13'")
34 | return
35 |
36 | if not houdini_color_settings["enabled"]:
37 | self.log.info(
38 | "Houdini workfile color management is disabled."
39 | )
40 | return
41 |
42 | # 'OCIO_ACTIVE_DISPLAYS', 'OCIO_ACTIVE_VIEWS' are checked
43 | # as Admins can add them in Ayon env vars or Ayon tools.
44 |
45 | default_display = houdini_color_settings["default_display"]
46 | if default_display:
47 | # get 'OCIO_ACTIVE_DISPLAYS' value if exists.
48 | self._set_context_env("OCIO_ACTIVE_DISPLAYS", default_display)
49 |
50 | default_view = houdini_color_settings["default_view"]
51 | if default_view:
52 | # get 'OCIO_ACTIVE_VIEWS' value if exists.
53 | self._set_context_env("OCIO_ACTIVE_VIEWS", default_view)
54 |
55 | def _set_context_env(self, env_var, default_value):
56 | env_value = self.launch_context.env.get(env_var, "")
57 | new_value = ":".join(
58 | key for key in [default_value, env_value] if key
59 | )
60 | self.log.info(
61 | "Setting {} environment to: {}"
62 | .format(env_var, new_value)
63 | )
64 | self.launch_context.env[env_var] = new_value
65 |
--------------------------------------------------------------------------------
/client/ayon_houdini/startup/otls/ayon_lop_load_shot.hda/ayon_8_8Lop_1load__shot_8_81.0/ExtraFileOptions:
--------------------------------------------------------------------------------
1 | {
2 | "AYON_icon.png/Cursor":{
3 | "type":"intarray",
4 | "value":[0,0]
5 | },
6 | "AYON_icon.png/IsExpr":{
7 | "type":"bool",
8 | "value":false
9 | },
10 | "AYON_icon.png/IsPython":{
11 | "type":"bool",
12 | "value":false
13 | },
14 | "AYON_icon.png/IsScript":{
15 | "type":"bool",
16 | "value":false
17 | },
18 | "AYON_icon.png/Source":{
19 | "type":"string",
20 | "value":"AYON_icon.png"
21 | },
22 | "OnCreated/Cursor":{
23 | "type":"intarray",
24 | "value":[1,1]
25 | },
26 | "OnCreated/IsExpr":{
27 | "type":"bool",
28 | "value":false
29 | },
30 | "OnCreated/IsPython":{
31 | "type":"bool",
32 | "value":true
33 | },
34 | "OnCreated/IsScript":{
35 | "type":"bool",
36 | "value":true
37 | },
38 | "OnCreated/Source":{
39 | "type":"string",
40 | "value":""
41 | },
42 | "OnDeleted/Cursor":{
43 | "type":"intarray",
44 | "value":[1,15]
45 | },
46 | "OnDeleted/IsExpr":{
47 | "type":"bool",
48 | "value":false
49 | },
50 | "OnDeleted/IsPython":{
51 | "type":"bool",
52 | "value":true
53 | },
54 | "OnDeleted/IsScript":{
55 | "type":"bool",
56 | "value":true
57 | },
58 | "OnDeleted/Source":{
59 | "type":"string",
60 | "value":""
61 | },
62 | "OnLoaded/Cursor":{
63 | "type":"intarray",
64 | "value":[9,76]
65 | },
66 | "OnLoaded/IsExpr":{
67 | "type":"bool",
68 | "value":false
69 | },
70 | "OnLoaded/IsPython":{
71 | "type":"bool",
72 | "value":true
73 | },
74 | "OnLoaded/IsScript":{
75 | "type":"bool",
76 | "value":true
77 | },
78 | "OnLoaded/Source":{
79 | "type":"string",
80 | "value":""
81 | },
82 | "OnNameChanged/Cursor":{
83 | "type":"intarray",
84 | "value":[1,15]
85 | },
86 | "OnNameChanged/IsExpr":{
87 | "type":"bool",
88 | "value":false
89 | },
90 | "OnNameChanged/IsPython":{
91 | "type":"bool",
92 | "value":true
93 | },
94 | "OnNameChanged/IsScript":{
95 | "type":"bool",
96 | "value":true
97 | },
98 | "OnNameChanged/Source":{
99 | "type":"string",
100 | "value":""
101 | },
102 | "PythonModule/Cursor":{
103 | "type":"intarray",
104 | "value":[8,1]
105 | },
106 | "PythonModule/IsExpr":{
107 | "type":"bool",
108 | "value":false
109 | },
110 | "PythonModule/IsPython":{
111 | "type":"bool",
112 | "value":true
113 | },
114 | "PythonModule/IsScript":{
115 | "type":"bool",
116 | "value":true
117 | },
118 | "PythonModule/Source":{
119 | "type":"string",
120 | "value":""
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/collect_frames.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Collector plugin for frames data on ROP instances."""
3 | import os
4 | import hou # noqa
5 | import clique
6 | import pyblish.api
7 | from ayon_houdini.api import lib, plugin
8 |
9 |
10 | class CollectFrames(plugin.HoudiniInstancePlugin):
11 | """Collect all frames which would be saved from the ROP nodes"""
12 |
13 | # This specific order value is used so that
14 | # this plugin runs after CollectRopFrameRange
15 | order = pyblish.api.CollectorOrder + 0.1
16 | label = "Collect Frames"
17 | families = ["camera", "vdbcache", "imagesequence", "ass",
18 | "redshiftproxy", "review", "pointcache", "fbx",
19 | "model", "bgeo", "image_rop"]
20 |
21 | def process(self, instance):
22 |
23 | # CollectRopFrameRange computes `start_frame` and `end_frame`
24 | # depending on the trange value.
25 | start_frame = instance.data["frameStartHandle"]
26 | end_frame = instance.data["frameEndHandle"]
27 |
28 | # Evaluate the file name at the first frame.
29 | ropnode = hou.node(instance.data["instance_node"])
30 | output_parm = lib.get_output_parameter(ropnode)
31 | output = output_parm.evalAtFrame(start_frame)
32 | file_name = os.path.basename(output)
33 |
34 | # todo: `frames` currently conflicts with "explicit frames" for a
35 | # for a custom frame list. So this should be refactored.
36 |
37 | instance.data.update({
38 | "frames": file_name, # Set frames to the file name by default.
39 | "stagingDir": os.path.dirname(output)
40 | })
41 |
42 | # Skip unnecessary logic if start and end frames are equal.
43 | if start_frame == end_frame:
44 | return
45 |
46 | # Create collection using frame pattern.
47 | # e.g. 'pointcacheBgeoCache_AB010.1001.bgeo'
48 | # will be
49 | frame_collection, _ = clique.assemble(
50 | [file_name],
51 | patterns=[clique.PATTERNS["frames"]],
52 | minimum_items=1
53 | )
54 |
55 | # Return as no frame pattern detected.
56 | if not frame_collection:
57 | return
58 |
59 | # It's always expected to be one collection.
60 | frame_collection = frame_collection[0]
61 | frame_collection.indexes.clear()
62 | frame_collection.indexes.update(
63 | list(range(start_frame, end_frame + 1))
64 | )
65 | instance.data["frames"] = list(frame_collection)
66 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/validate_usd_output_node.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import inspect
3 |
4 | import pyblish.api
5 |
6 | from ayon_core.pipeline import PublishValidationError
7 | from ayon_houdini.api.action import SelectROPAction
8 | from ayon_houdini.api import plugin
9 |
10 |
11 | class ValidateUSDOutputNode(plugin.HoudiniInstancePlugin):
12 | """Validate the instance USD LOPs Output Node.
13 |
14 | This will ensure:
15 | - The LOP Path is set.
16 | - The LOP Path refers to an existing object.
17 | - The LOP Path node is a LOP node.
18 |
19 | """
20 |
21 | # Validate early so that this error reports higher than others to the user
22 | # so that if another invalidation is due to the output node being invalid
23 | # the user will likely first focus on this first issue
24 | order = pyblish.api.ValidatorOrder - 0.4
25 | families = ["usdrop"]
26 | label = "Validate Output Node (USD)"
27 | actions = [SelectROPAction]
28 |
29 | def process(self, instance):
30 |
31 | invalid = self.get_invalid(instance)
32 | if invalid:
33 | node_path = invalid[0].path()
34 | raise PublishValidationError(
35 | f"Output node '{node_path}' has no valid LOP path set.",
36 | title=self.label,
37 | description=self.get_description()
38 | )
39 |
40 | @classmethod
41 | def get_invalid(cls, instance):
42 |
43 | import hou
44 |
45 | output_node = instance.data.get("output_node")
46 |
47 | if output_node is None:
48 | node = hou.node(instance.data.get("instance_node"))
49 | cls.log.error(
50 | "USD node '%s' configured LOP path does not exist. "
51 | "Ensure a valid LOP path is set." % node.path()
52 | )
53 |
54 | return [node]
55 |
56 | # Output node must be a Sop node.
57 | if not isinstance(output_node, hou.LopNode):
58 | cls.log.error(
59 | "Output node %s is not a LOP node. "
60 | "LOP Path must point to a LOP node, "
61 | "instead found category type: %s"
62 | % (output_node.path(), output_node.type().category().name())
63 | )
64 | return [output_node]
65 |
66 | def get_description(self):
67 | return inspect.cleandoc(
68 | """### USD ROP has invalid LOP path
69 |
70 | The USD ROP node has no or an invalid LOP path set to be exported.
71 | Make sure to correctly configure what you want to export for the
72 | publish.
73 | """
74 | )
75 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/collect_usd_render.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 |
4 | import hou
5 | import pyblish.api
6 |
7 | from ayon_houdini.api import plugin
8 | from ayon_houdini.api.lib import evalParmNoFrame
9 |
10 |
11 | class CollectUsdRender(plugin.HoudiniInstancePlugin):
12 | """Collect publishing data for USD Render ROP.
13 |
14 | If `rendercommand` parm is disabled (and thus no rendering triggers by the
15 | usd render rop) it is assumed to be a "Split Render" job where the farm
16 | will get an additional render job after the USD file is extracted.
17 |
18 | Provides:
19 | instance -> ifdFile
20 |
21 | """
22 |
23 | label = "Collect USD Render Rop"
24 | order = pyblish.api.CollectorOrder
25 | hosts = ["houdini"]
26 | families = ["usdrender"]
27 |
28 | def process(self, instance):
29 |
30 | rop = hou.node(instance.data.get("instance_node"))
31 |
32 | if instance.data["splitRender"]:
33 | # USD file output
34 | lop_output = evalParmNoFrame(
35 | rop, "lopoutput", pad_character="#"
36 | )
37 |
38 | # The file is usually relative to the Output Processor's 'Save to
39 | # Directory' which forces all USD files to end up in that directory
40 | # TODO: It is possible for a user to disable this
41 | # TODO: When enabled I think only the basename of the `lopoutput`
42 | # parm is preserved, any parent folders defined are likely ignored
43 | folder = evalParmNoFrame(
44 | rop, "savetodirectory_directory", pad_character="#"
45 | )
46 |
47 | export_file = os.path.join(folder, lop_output)
48 |
49 | # Substitute any # characters in the name back to their $F4
50 | # equivalent
51 | def replace_to_f(match):
52 | number = len(match.group(0))
53 | if number <= 1:
54 | number = "" # make it just $F not $F1 or $F0
55 | return "$F{}".format(number)
56 |
57 | export_file = re.sub("#+", replace_to_f, export_file)
58 | self.log.debug(
59 | "Found export file: {}".format(export_file)
60 | )
61 | instance.data["ifdFile"] = export_file
62 |
63 | # The render job is not frame dependent but fully dependent on
64 | # the job having been completed, since the extracted file is a
65 | # single file.
66 | if "$F" not in export_file:
67 | instance.data["splitRenderFrameDependent"] = False
68 |
69 | # stub required data for Submit Publish Job publish plug-in
70 | instance.data["attachTo"] = []
71 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/validate_no_errors.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import hou
3 |
4 | import pyblish.api
5 | from ayon_core.pipeline import PublishValidationError
6 |
7 | from ayon_houdini.api import plugin
8 |
9 |
10 | def cook_in_range(node, start, end):
11 | current = hou.intFrame()
12 | if start >= current >= end:
13 | # Allow cooking current frame since we're in frame range
14 | node.cook(force=False)
15 | else:
16 | node.cook(force=False, frame_range=(start, start))
17 |
18 |
19 | def get_errors(node):
20 | """Get cooking errors.
21 |
22 | If node already has errors check whether it needs to recook
23 | If so, then recook first to see if that solves it.
24 |
25 | """
26 | if node.errors() and node.needsToCook():
27 | node.cook()
28 |
29 | return node.errors()
30 |
31 |
32 | class ValidateNoErrors(plugin.HoudiniInstancePlugin):
33 | """Validate the Instance has no current cooking errors."""
34 |
35 | order = pyblish.api.ValidatorOrder
36 | label = "Validate no errors"
37 |
38 | def process(self, instance):
39 |
40 | if not instance.data.get("instance_node"):
41 | self.log.debug(
42 | "Skipping 'Validate no errors' because instance "
43 | "has no instance node: {}".format(instance)
44 | )
45 | return
46 |
47 | validate_nodes = []
48 |
49 | if len(instance) > 0:
50 | validate_nodes.append(hou.node(instance.data.get("instance_node")))
51 | output_node = instance.data.get("output_node")
52 | if output_node:
53 | validate_nodes.append(output_node)
54 |
55 | for node in validate_nodes:
56 | self.log.debug("Validating for errors: %s" % node.path())
57 | errors = get_errors(node)
58 |
59 | if errors:
60 | # If there are current errors, then try an unforced cook
61 | # to see whether the error will disappear.
62 | self.log.debug(
63 | "Recooking to revalidate error "
64 | "is up to date for: %s" % node.path()
65 | )
66 | current_frame = hou.intFrame()
67 | start = instance.data.get("frameStart", current_frame)
68 | end = instance.data.get("frameEnd", current_frame)
69 | cook_in_range(node, start=start, end=end)
70 |
71 | # Check for errors again after the forced recook
72 | errors = get_errors(node)
73 | if errors:
74 | self.log.error(errors)
75 | raise PublishValidationError(
76 | "Node has errors: {}".format(node.path()),
77 | title=self.label)
78 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/collect_cache_farm.py:
--------------------------------------------------------------------------------
1 | import os
2 | import hou
3 | import pyblish.api
4 | from ayon_houdini.api import (
5 | lib,
6 | plugin
7 | )
8 |
9 |
10 | class CollectFarmCacheFamily(plugin.HoudiniInstancePlugin):
11 | """Collect publish.hou family for caching on farm as early as possible."""
12 | order = pyblish.api.CollectorOrder - 0.45
13 | families = ["ass", "pointcache", "redshiftproxy",
14 | "vdbcache", "model", "staticMesh",
15 | "rop.opengl", "usdrop", "camera"]
16 | targets = ["local", "remote"]
17 | label = "Collect Data for Cache"
18 |
19 | def process(self, instance):
20 |
21 | if not instance.data["farm"]:
22 | self.log.debug("Caching on farm is disabled. "
23 | "Skipping farm collecting.")
24 | return
25 | instance.data["families"].append("publish.hou")
26 |
27 |
28 | class CollectDataforCache(plugin.HoudiniInstancePlugin):
29 | """Collect data for caching to Deadline."""
30 |
31 | # Run after Collect Frames
32 | order = pyblish.api.CollectorOrder + 0.11
33 | families = ["publish.hou"]
34 | targets = ["local", "remote"]
35 | label = "Collect Data for Cache"
36 |
37 | def process(self, instance):
38 | # Why do we need this particular collector to collect the expected
39 | # output files from a ROP node. Don't we have a dedicated collector
40 | # for that yet?
41 | # Answer: No, we don't have a generic expected file collector.
42 | # Because different product types needs different logic.
43 | # e.g. check CollectMantraROPRenderProducts
44 | # and CollectKarmaROPRenderProducts
45 | # Collect expected files
46 | ropnode = hou.node(instance.data["instance_node"])
47 | output_parm = lib.get_output_parameter(ropnode)
48 | expected_filepath = output_parm.eval()
49 |
50 | files = instance.data.setdefault("files", list())
51 | frames = instance.data.get("frames", "")
52 | if isinstance(frames, str):
53 | # single file
54 | files.append(expected_filepath)
55 | else:
56 | # list of files
57 | staging_dir, _ = os.path.split(expected_filepath)
58 | files.extend("{}/{}".format(staging_dir, f) for f in frames)
59 |
60 | expected_files = instance.data.setdefault("expectedFiles", list())
61 | expected_files.append({"cache": files})
62 | self.log.debug(f"Caching on farm expected files: {expected_files}")
63 |
64 | instance.data.update({
65 | # used in HoudiniCacheSubmitDeadline in ayon-deadline
66 | "plugin": "Houdini",
67 | "publish": True
68 | })
69 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/collect_output_node.py:
--------------------------------------------------------------------------------
1 | import pyblish.api
2 | from ayon_core.pipeline.publish import KnownPublishError
3 | from ayon_houdini.api import plugin
4 |
5 |
6 | class CollectOutputSOPPath(plugin.HoudiniInstancePlugin):
7 | """Collect the out node's SOP/COP Path value."""
8 |
9 | order = pyblish.api.CollectorOrder - 0.45
10 | families = [
11 | "pointcache",
12 | "camera",
13 | "vdbcache",
14 | "imagesequence",
15 | "redshiftproxy",
16 | "staticMesh",
17 | "model",
18 | "usdrender",
19 | "usdrop"
20 | ]
21 |
22 | label = "Collect Output Node Path"
23 |
24 | def process(self, instance):
25 |
26 | import hou
27 |
28 | node = hou.node(instance.data["instance_node"])
29 |
30 | # Get sop path
31 | node_type = node.type().name()
32 | if node_type == "geometry":
33 | out_node = node.parm("soppath").evalAsNode()
34 |
35 | elif node_type == "alembic":
36 |
37 | # Alembic can switch between using SOP Path or object
38 | if node.parm("use_sop_path").eval():
39 | out_node = node.parm("sop_path").evalAsNode()
40 | else:
41 | root = node.parm("root").eval()
42 | objects = node.parm("objects").eval()
43 | path = root + "/" + objects
44 | out_node = hou.node(path)
45 |
46 | elif node_type == "comp":
47 | out_node = node.parm("coppath").evalAsNode()
48 |
49 | elif node_type == "usd" or node_type == "usdrender":
50 | out_node = node.parm("loppath").evalAsNode()
51 |
52 | elif node_type == "usd_rop" or node_type == "usdrender_rop":
53 | # Inside Solaris e.g. /stage (not in ROP context)
54 | # When incoming connection is present it takes it directly
55 | inputs = node.inputs()
56 | if inputs:
57 | out_node = inputs[0]
58 | else:
59 | out_node = node.parm("loppath").evalAsNode()
60 |
61 | elif node_type == "Redshift_Proxy_Output":
62 | out_node = node.parm("RS_archive_sopPath").evalAsNode()
63 |
64 | elif node_type == "filmboxfbx":
65 | out_node = node.parm("startnode").evalAsNode()
66 |
67 | else:
68 | raise KnownPublishError(
69 | f"ROP node type '{node_type}' is not supported"
70 | f" for product type '{instance.data['product_type']}'"
71 | )
72 |
73 | if not out_node:
74 | self.log.warning("No output node collected.")
75 | return
76 |
77 | self.log.debug("Output node: %s" % out_node.path())
78 | instance.data["output_node"] = out_node
79 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/validate_scene_review.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import hou
3 |
4 | import pyblish.api
5 | from ayon_core.pipeline import PublishValidationError
6 |
7 | from ayon_houdini.api import plugin
8 |
9 |
10 | class ValidateSceneReview(plugin.HoudiniInstancePlugin):
11 | """Validator Some Scene Settings before publishing the review
12 | 1. Scene Path
13 | 2. Resolution
14 | """
15 |
16 | order = pyblish.api.ValidatorOrder
17 | families = ["rop.opengl"]
18 | label = "Scene Setting for review"
19 |
20 | def process(self, instance):
21 |
22 | report = []
23 | instance_node = hou.node(instance.data.get("instance_node"))
24 |
25 | invalid = self.get_invalid_scene_path(instance_node)
26 | if invalid:
27 | report.append(invalid)
28 |
29 | invalid = self.get_invalid_camera_path(instance_node)
30 | if invalid:
31 | report.append(invalid)
32 |
33 | invalid = self.get_invalid_resolution(instance_node)
34 | if invalid:
35 | report.extend(invalid)
36 |
37 | if report:
38 | raise PublishValidationError(
39 | "\n\n".join(report),
40 | title=self.label)
41 |
42 | def get_invalid_scene_path(self, rop_node):
43 | scene_path_parm = rop_node.parm("scenepath")
44 | scene_path_node = scene_path_parm.evalAsNode()
45 | if not scene_path_node:
46 | path = scene_path_parm.evalAsString()
47 | return "Scene path does not exist: '{}'".format(path)
48 |
49 | def get_invalid_camera_path(self, rop_node):
50 | camera_path_parm = rop_node.parm("camera")
51 | camera_node = camera_path_parm.evalAsNode()
52 | path = camera_path_parm.evalAsString()
53 | if not camera_node:
54 | return "Camera path does not exist: '{}'".format(path)
55 | type_name = camera_node.type().name()
56 | if type_name not in {"cam", "lopimportcam"}:
57 | return "Camera path is not a camera: '{}' (type: {})".format(
58 | path, type_name
59 | )
60 |
61 | def get_invalid_resolution(self, rop_node):
62 |
63 | # The resolution setting is only used when Override Camera Resolution
64 | # is enabled. So we skip validation if it is disabled.
65 | override = rop_node.parm("tres").eval()
66 | if not override:
67 | return
68 |
69 | invalid = []
70 | res_width = rop_node.parm("res1").eval()
71 | res_height = rop_node.parm("res2").eval()
72 | if res_width == 0:
73 | invalid.append("Override Resolution width is set to zero.")
74 | if res_height == 0:
75 | invalid.append("Override Resolution height is set to zero")
76 |
77 | return invalid
78 |
--------------------------------------------------------------------------------
/server/settings/imageio.py:
--------------------------------------------------------------------------------
1 | from pydantic import validator
2 | from ayon_server.settings import BaseSettingsModel, SettingsField
3 | from ayon_server.settings.validators import ensure_unique_names
4 |
5 |
6 | class ImageIOFileRuleModel(BaseSettingsModel):
7 | name: str = SettingsField("", title="Rule name")
8 | pattern: str = SettingsField("", title="Regex pattern")
9 | colorspace: str = SettingsField("", title="Colorspace name")
10 | ext: str = SettingsField("", title="File extension")
11 |
12 |
13 | class ImageIOFileRulesModel(BaseSettingsModel):
14 | activate_host_rules: bool = SettingsField(False)
15 | rules: list[ImageIOFileRuleModel] = SettingsField(
16 | default_factory=list,
17 | title="Rules"
18 | )
19 |
20 | @validator("rules")
21 | def validate_unique_outputs(cls, value):
22 | ensure_unique_names(value)
23 | return value
24 |
25 |
26 | class WorkfileImageIOModel(BaseSettingsModel):
27 | """Workfile settings help.
28 |
29 | Empty values will be skipped, allowing any existing env vars to
30 | pass through as defined.
31 |
32 | Note: The render space in Houdini is
33 | always set to the 'scene_linear' role."""
34 |
35 | enabled: bool = SettingsField(False, title="Enabled")
36 | default_display: str = SettingsField(
37 | title="Default active displays",
38 | description="It behaves like the 'OCIO_ACTIVE_DISPLAYS' env var,"
39 | " Colon-separated list of displays, e.g ACES:P3"
40 | )
41 | default_view: str = SettingsField(
42 | title="Default active views",
43 | description="It behaves like the 'OCIO_ACTIVE_VIEWS' env var,"
44 | " Colon-separated list of views, e.g sRGB:DCDM"
45 | )
46 | review_color_space: str = SettingsField(
47 | title="Review colorspace",
48 | description="It exposes OCIO Colorspace parameter in opengl nodes."
49 | "if left empty, Ayon will figure out the default "
50 | "colorspace using your default display and default view."
51 | )
52 |
53 |
54 | class HoudiniImageIOModel(BaseSettingsModel):
55 | activate_host_color_management: bool = SettingsField(
56 | True, title="Enable Color Management"
57 | )
58 | file_rules: ImageIOFileRulesModel = SettingsField(
59 | default_factory=ImageIOFileRulesModel,
60 | title="File Rules"
61 | )
62 | workfile: WorkfileImageIOModel = SettingsField(
63 | default_factory=WorkfileImageIOModel,
64 | title="Workfile"
65 | )
66 |
67 |
68 | DEFAULT_IMAGEIO_SETTINGS = {
69 | "activate_host_color_management": True,
70 | "file_rules": {
71 | "activate_host_rules": False,
72 | "rules": []
73 | },
74 | "workfile": {
75 | "enabled": False,
76 | "default_display": "ACES",
77 | "default_view": "sRGB",
78 | "review_color_space": ""
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/create/create_usd_componentbuilder.py:
--------------------------------------------------------------------------------
1 | import inspect
2 |
3 | from ayon_houdini.api import plugin
4 | from ayon_core.pipeline import CreatedInstance, CreatorError
5 |
6 | import hou
7 |
8 |
9 | class CreateUSDComponentBuilder(plugin.HoudiniCreator):
10 | identifier = "io.ayon.creators.houdini.componentbuilder"
11 | label = "USD Component Builder LOPs"
12 | product_type = "usd"
13 | product_base_type = "usd"
14 | icon = "cubes"
15 | description = "Create USD from Component Builder LOPs"
16 |
17 | def get_detail_description(self):
18 | return inspect.cleandoc("""
19 | Creates a USD publish from a Component Output LOP that is part of
20 | a solaris component builder network.
21 |
22 | The created USD will contain the component builder LOPs and all
23 | its dependencies inside the single product.
24 |
25 | To use it, select a Component Output LOP and click "Create" for
26 | this creator. It will generate an instance for each selected
27 | Component Output LOP.
28 | """)
29 |
30 | def create(self, product_name, instance_data, pre_create_data):
31 | nodes = hou.selectedNodes()
32 | builders = [
33 | node for node in nodes
34 | if node.type().nameWithCategory() == "Lop/componentoutput"
35 | ]
36 | for builder in builders:
37 | self.create_for_instance_node(product_name, instance_data, builder)
38 |
39 | def create_for_instance_node(
40 | self, product_name, instance_data, instance_node):
41 |
42 | try:
43 | self.customize_node_look(instance_node)
44 | instance_data["instance_node"] = instance_node.path()
45 | instance_data["instance_id"] = instance_node.path()
46 | instance_data["families"] = self.get_publish_families()
47 | instance = CreatedInstance(
48 | product_type=self.product_type,
49 | product_name=product_name,
50 | data=instance_data,
51 | creator=self,
52 | )
53 | self._add_instance_to_context(instance)
54 | self.imprint(instance_node, instance.data_to_store())
55 | except hou.Error as er:
56 | raise CreatorError("Creator error: {}".format(er)) from er
57 |
58 | # Lock any parameters in this list
59 | to_lock = [
60 | # Lock some AYON attributes
61 | "productType",
62 | "productBaseType",
63 | "id",
64 | ]
65 | self.lock_parameters(instance_node, to_lock)
66 |
67 | def get_network_categories(self):
68 | # Do not expose via tab menu because currently it does not create any
69 | # node, but only 'imprints' on an existing node.
70 | return []
71 |
72 | def get_publish_families(self):
73 | return ["usd", "componentbuilder"]
74 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/extract_hda.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 | import contextlib
4 |
5 | import hou
6 | import pyblish.api
7 |
8 | from ayon_core.pipeline import PublishError
9 | from ayon_houdini.api import plugin
10 |
11 |
12 | @contextlib.contextmanager
13 | def revert_original_parm_template_group(node: "hou.OpNode"):
14 | """Restore parm template group after the context"""
15 | parm_group = node.parmTemplateGroup()
16 | try:
17 | yield
18 | finally:
19 | # Set the original
20 | node.setParmTemplateGroup(parm_group)
21 |
22 |
23 | class ExtractHDA(plugin.HoudiniExtractorPlugin):
24 |
25 | order = pyblish.api.ExtractorOrder
26 | label = "Extract HDA"
27 | families = ["hda"]
28 |
29 | def process(self, instance):
30 |
31 | hda_node = hou.node(instance.data.get("instance_node"))
32 | hda_def = hda_node.type().definition()
33 | hda_options = hda_def.options()
34 | hda_options.setSaveInitialParmsAndContents(True)
35 |
36 | next_version = instance.data["anatomyData"]["version"]
37 | self.log.info("setting version: {}".format(next_version))
38 | hda_def.setVersion(str(next_version))
39 | hda_def.setOptions(hda_options)
40 |
41 | with revert_original_parm_template_group(hda_node):
42 | # Remove our own custom parameters so that if the HDA definition
43 | # has "Save Spare Parameters" enabled, we don't save our custom
44 | # attributes
45 | # Get our custom `Extra` AYON parameters
46 | parm_group = hda_node.parmTemplateGroup()
47 | # The name 'Extra' is a hard coded name in AYON.
48 | parm_folder = parm_group.findFolder("Extra")
49 | if not parm_folder:
50 | raise PublishError(
51 | "Extra AYON parm folder does not exist"
52 | f" on {hda_node.path()}"
53 | "\n\nPlease select the node and create an"
54 | " HDA product from the publisher UI."
55 | )
56 |
57 | # Remove `Extra` AYON parameters
58 | parm_group.remove(parm_folder.name())
59 | hda_node.setParmTemplateGroup(parm_group)
60 |
61 | # Save the HDA file
62 | hda_def.save(hda_def.libraryFilePath(), hda_node, hda_options)
63 |
64 | if "representations" not in instance.data:
65 | instance.data["representations"] = []
66 |
67 | file = os.path.basename(hda_def.libraryFilePath())
68 | staging_dir = os.path.dirname(hda_def.libraryFilePath())
69 | self.log.info("Using HDA from {}".format(hda_def.libraryFilePath()))
70 |
71 | representation = {
72 | 'name': 'hda',
73 | 'ext': 'hda',
74 | 'files': file,
75 | "stagingDir": staging_dir,
76 | }
77 | instance.data["representations"].append(representation)
78 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/validate_cop_output_node.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import hou
3 |
4 | import pyblish.api
5 | from ayon_core.pipeline import PublishValidationError
6 |
7 | from ayon_houdini.api import plugin
8 |
9 |
10 | class ValidateCopOutputNode(plugin.HoudiniInstancePlugin):
11 | """Validate the instance COP2 Output Node.
12 |
13 | This will ensure:
14 | - The COP2 Path is set.
15 | - The COP2 Path refers to an existing object.
16 | - The COP2 Path node is a COP node.
17 | """
18 |
19 | order = pyblish.api.ValidatorOrder
20 | families = ["imagesequence"]
21 | label = "Validate COP2 Output Node"
22 |
23 | def process(self, instance):
24 |
25 | invalid = self.get_invalid(instance)
26 | if invalid:
27 | raise PublishValidationError(
28 | "Output node '{}' is incorrect. "
29 | "See plug-in log for details.".format(invalid[0].path()),
30 | title=self.label,
31 | description=(
32 | "### Invalid COP2 output node\n\n"
33 | "The output node path for the instance must be set to a "
34 | "valid COP2 node path.\n\nSee the log for more details."
35 | )
36 | )
37 |
38 | @classmethod
39 | def get_invalid(cls, instance):
40 | output_node = instance.data.get("output_node")
41 |
42 | if not output_node:
43 | node = hou.node(instance.data.get("instance_node"))
44 | cls.log.error(
45 | "COP2 Output node in '%s' does not exist. "
46 | "Ensure a valid COP2 output path is set." % node.path()
47 | )
48 |
49 | return [node]
50 |
51 | # Output node must be a COP2 node.
52 | version = hou.applicationVersion()
53 | if version >= (20, 5, 0):
54 | # In Houdini 20.5 and later, COP2 nodes are used and `hou.CopNode`
55 | # started referring to Copernicus nodes instead.
56 | class_type = hou.Cop2Node
57 | else:
58 | class_type = hou.CopNode
59 |
60 | node_category_name = output_node.type().category().name()
61 | if not isinstance(output_node, class_type):
62 | cls.log.error(
63 | "Output node %s is not a COP2 node. "
64 | "COP2 Path must point to a COP2 node, "
65 | "instead found category type: %s",
66 | output_node.path(), node_category_name
67 | )
68 | return [output_node]
69 |
70 | # For the sake of completeness also assert the category type
71 | # is Cop2 to avoid potential edge case scenarios even though
72 | # the isinstance check above should be stricter than this category
73 | if node_category_name != "Cop2":
74 | cls.log.error(
75 | "Output node %s is not of category Cop2.", output_node.path()
76 | )
77 | return [output_node]
78 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/extract_render.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pyblish.api
3 |
4 | from ayon_core.pipeline import PublishError
5 | from ayon_houdini.api import plugin
6 | from ayon_houdini.api.lib import format_as_collections
7 |
8 |
9 | class ExtractRender(plugin.HoudiniExtractorPlugin):
10 |
11 | order = pyblish.api.ExtractorOrder
12 | label = "Extract Render"
13 | families = ["mantra_rop",
14 | "karma_rop",
15 | "redshift_rop",
16 | "arnold_rop",
17 | "vray_rop",
18 | "usdrender"]
19 |
20 | def process(self, instance):
21 | creator_attribute = instance.data["creator_attributes"]
22 |
23 | do_local_render = (
24 | creator_attribute.get("render_target")
25 | in {"local", "local_export_farm_render"}
26 | )
27 |
28 | if instance.data.get("farm") and not do_local_render:
29 | self.log.debug(
30 | "Render should be processed on farm, skipping local render."
31 | )
32 | return
33 |
34 | if do_local_render:
35 | # FIXME Render the entire frame range if any of the AOVs does
36 | # not have a previously rendered version. This situation breaks
37 | # the publishing.
38 | # because There will be missing frames as ROP nodes typically
39 | # cannot render different frame ranges for each AOV; they always
40 | # use the same frame range for all AOVs.
41 | self.render_rop(instance)
42 |
43 | if (
44 | creator_attribute.get("render_target")
45 | == "local_export_farm_render"
46 | ):
47 | return
48 |
49 | # `ExpectedFiles` is a list that includes one dict.
50 | expected_files = instance.data["expectedFiles"][0]
51 | # Each key in that dict is a list of files.
52 | # Combine lists of files into one big list.
53 | all_frames = []
54 | for value in expected_files.values():
55 | if isinstance(value, str):
56 | all_frames.append(value)
57 | elif isinstance(value, list):
58 | all_frames.extend(value)
59 | # Check missing frames.
60 | # Frames won't exist if user cancels the render.
61 | missing_frames = [
62 | frame
63 | for frame in all_frames
64 | if not os.path.exists(frame)
65 | ]
66 |
67 | if missing_frames:
68 | # Combine collections for simpler logs of missing files
69 | missing_frames = format_as_collections(missing_frames)
70 | missing_frames = "\n ".join(
71 | f"- {sequence}" for sequence in missing_frames
72 | )
73 | raise PublishError(
74 | "Failed to complete render extraction.\n"
75 | "Please render any missing output files.",
76 | detail=f"Missing output files: \n {missing_frames}"
77 | )
78 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/create/create_composite.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Creator plugin for creating composite sequences."""
3 | from ayon_houdini.api import plugin
4 | from ayon_core.pipeline import CreatorError
5 | from ayon_core.lib import EnumDef
6 |
7 | import hou
8 |
9 |
10 | class CreateCompositeSequence(plugin.HoudiniCreator):
11 | """Composite ROP to Image Sequence"""
12 |
13 | identifier = "io.openpype.creators.houdini.imagesequence"
14 | label = "Composite (COP2)"
15 | description = "Render legacy COP2 ROP to image sequence"
16 | product_type = "imagesequence"
17 | product_base_type = "imagesequence"
18 | icon = "fa5.eye"
19 |
20 | ext = ".exr"
21 |
22 | # Default render target
23 | render_target = "local"
24 |
25 | def get_publish_families(self):
26 | return ["imagesequence", "publish.hou"]
27 |
28 | def create(self, product_name, instance_data, pre_create_data):
29 | import hou # noqa
30 |
31 | instance_data.update({"node_type": "comp"})
32 |
33 | instance = super(CreateCompositeSequence, self).create(
34 | product_name,
35 | instance_data,
36 | pre_create_data)
37 |
38 | instance_node = hou.node(instance.get("instance_node"))
39 | parms = {
40 | "trange": 1,
41 | }
42 |
43 | if self.selected_nodes:
44 | if len(self.selected_nodes) > 1:
45 | raise CreatorError("More than one item selected.")
46 | path = self.selected_nodes[0].path()
47 | parms["coppath"] = path
48 |
49 | instance_node.setParms(parms)
50 |
51 | # Manually set f1 & f2 to $FSTART and $FEND respectively
52 | # to match other Houdini nodes default.
53 | instance_node.parm("f1").setExpression("$FSTART")
54 | instance_node.parm("f2").setExpression("$FEND")
55 |
56 | # Lock any parameters in this list
57 | to_lock = ["prim_to_detail_pattern"]
58 | self.lock_parameters(instance_node, to_lock)
59 |
60 | def set_node_staging_dir(
61 | self, node, staging_dir, instance, pre_create_data):
62 | node.parm("copoutput").set(f"{staging_dir}/$OS.$F4{self.ext}")
63 |
64 | def get_network_categories(self):
65 | return [
66 | hou.ropNodeTypeCategory(),
67 | hou.cop2NodeTypeCategory()
68 | ]
69 |
70 | def get_instance_attr_defs(self):
71 | render_target_items = {
72 | "local": "Local machine rendering",
73 | "local_no_render": "Use existing frames (local)",
74 | "farm": "Farm Rendering",
75 | }
76 |
77 | return [
78 | EnumDef("render_target",
79 | items=render_target_items,
80 | label="Render target",
81 | default=self.render_target)
82 | ]
83 |
84 | def get_pre_create_attr_defs(self):
85 | attrs = super().get_pre_create_attr_defs()
86 | # Use same attributes as for instance attributes
87 | return attrs + self.get_instance_attr_defs()
88 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/create/create_copernicus.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """Creator plugin for creating composite sequences."""
3 | from ayon_houdini.api import plugin
4 | from ayon_core.pipeline import CreatorError
5 | from ayon_core.lib import EnumDef
6 |
7 | import hou
8 |
9 |
10 | class CreateCopernicusROP(plugin.HoudiniCreator):
11 | """Copernicus ROP to Image Sequence"""
12 |
13 | identifier = "io.ayon.creators.houdini.copernicus"
14 | label = "Composite (Copernicus)"
15 | description = "Render using the Copernicus Image ROP"
16 | product_type = "render"
17 | product_base_type = "render"
18 | icon = "fa5.eye"
19 |
20 | ext = ".exr"
21 |
22 | # Copernicus was introduced in Houdini 20.5 so we only enable this
23 | # creator if the Houdini version is 20.5 or higher.
24 | enabled = hou.applicationVersion() >= (20, 5, 0)
25 |
26 | # Default render target
27 | render_target = "local"
28 |
29 | def create(self, product_name, instance_data, pre_create_data):
30 | instance_data["node_type"] = "image"
31 |
32 | instance = super().create(
33 | product_name,
34 | instance_data,
35 | pre_create_data)
36 |
37 | instance_node = hou.node(instance.get("instance_node"))
38 | parms = {
39 | "trange": 1,
40 | }
41 | if self.selected_nodes:
42 | if len(self.selected_nodes) > 1:
43 | raise CreatorError("More than one item selected.")
44 | path = self.selected_nodes[0].path()
45 | parms["coppath"] = path
46 |
47 | instance_node.setParms(parms)
48 |
49 | # Manually set f1 & f2 to $FSTART and $FEND respectively
50 | # to match other Houdini nodes default.
51 | instance_node.parm("f1").setExpression("$FSTART")
52 | instance_node.parm("f2").setExpression("$FEND")
53 |
54 | def set_node_staging_dir(
55 | self, node, staging_dir, instance, pre_create_data):
56 | node.parm("copoutput").set(f"{staging_dir}/$OS.$F4{self.ext}")
57 |
58 | def get_network_categories(self):
59 | return [
60 | hou.ropNodeTypeCategory(),
61 | hou.cop2NodeTypeCategory()
62 | ]
63 |
64 | def get_publish_families(self):
65 | return [
66 | "render",
67 | "image_rop",
68 | "publish.hou"
69 | ]
70 |
71 | def get_instance_attr_defs(self):
72 | render_target_items = {
73 | "local": "Local machine rendering",
74 | "local_no_render": "Use existing frames (local)",
75 | "farm": "Farm Rendering",
76 | }
77 |
78 | return [
79 | EnumDef("render_target",
80 | items=render_target_items,
81 | label="Render target",
82 | default=self.render_target)
83 | ]
84 |
85 | def get_pre_create_attr_defs(self):
86 | attrs = super().get_pre_create_attr_defs()
87 | # Use same attributes as for instance attributes
88 | return attrs + self.get_instance_attr_defs()
89 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/load/load_usd_layer.py:
--------------------------------------------------------------------------------
1 | import hou
2 |
3 | from ayon_core.pipeline import (
4 | AYON_CONTAINER_ID,
5 | )
6 | from ayon_houdini.api import (
7 | plugin,
8 | lib
9 | )
10 |
11 |
12 | class USDSublayerLoader(plugin.HoudiniLoader):
13 | """Sublayer USD file in Solaris"""
14 |
15 | product_types = {
16 | "usd",
17 | "usdCamera",
18 | }
19 | label = "Sublayer USD"
20 | representations = {"usd", "usda", "usdlc", "usdnc", "abc"}
21 | order = 1
22 |
23 | icon = "code-fork"
24 | color = "orange"
25 |
26 | use_ayon_entity_uri = False
27 |
28 | def load(self, context, name=None, namespace=None, data=None):
29 | # Format file name, Houdini only wants forward slashes
30 | file_path = self.filepath_from_context(context)
31 | file_path = file_path.replace("\\", "/")
32 |
33 | # Get the root node
34 | stage = hou.node("/stage")
35 |
36 | # Define node name
37 | namespace = namespace if namespace else context["folder"]["name"]
38 | node_name = "{}_{}".format(namespace, name) if namespace else name
39 |
40 | # Create USD reference
41 | container = stage.createNode("sublayer", node_name=node_name)
42 | container.setParms({"filepath1": file_path})
43 | container.moveToGoodPosition()
44 |
45 | # Imprint it manually
46 | data = {
47 | "schema": "ayon:container-3.0",
48 | "id": AYON_CONTAINER_ID,
49 | "name": node_name,
50 | "namespace": namespace,
51 | "loader": str(self.__class__.__name__),
52 | "representation": context["representation"]["id"],
53 | }
54 |
55 | # todo: add folder="AYON"
56 | lib.imprint(container, data)
57 |
58 | return container
59 |
60 | def update(self, container, context):
61 | node = container["node"]
62 |
63 | # Update the file path
64 | file_path = self.filepath_from_context(context)
65 | file_path = file_path.replace("\\", "/")
66 |
67 | # Update attributes
68 | node.setParms(
69 | {
70 | "filepath1": file_path,
71 | "representation": context["representation"]["id"],
72 | }
73 | )
74 |
75 | # Reload files
76 | node.parm("reload").pressButton()
77 |
78 | def remove(self, container):
79 | node = container["node"]
80 | node.destroy()
81 |
82 | def switch(self, container, context):
83 | self.update(container, context)
84 |
85 | def create_load_placeholder_node(
86 | self, node_name: str, placeholder_data: dict
87 | ) -> hou.Node:
88 | """Define how to create a placeholder node for this loader for the
89 | Workfile Template Builder system."""
90 | # Create node
91 | network = lib.find_active_network(
92 | category=hou.lopNodeTypeCategory(),
93 | default="/stage"
94 | )
95 | node = network.createNode("null", node_name=node_name)
96 | node.moveToGoodPosition()
97 | return node
98 |
99 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/increment_current_file.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pyblish.api
4 |
5 | from ayon_core.lib import version_up
6 | from ayon_core.pipeline import (
7 | registered_host,
8 | KnownPublishError
9 | )
10 | from ayon_core.host import IWorkfileHost
11 | from ayon_core.pipeline.publish import get_errored_plugins_from_context
12 |
13 | from ayon_houdini.api import plugin
14 |
15 |
16 | class IncrementCurrentFile(plugin.HoudiniContextPlugin):
17 | """Increment the current file.
18 |
19 | Saves the current scene with an increased version number.
20 |
21 | """
22 |
23 | label = "Increment current file"
24 | order = pyblish.api.IntegratorOrder + 9.0
25 | families = ["workfile",
26 | "usdrender",
27 | "mantra_rop",
28 | "karma_rop",
29 | "redshift_rop",
30 | "arnold_rop",
31 | "vray_rop",
32 | "render.local.hou",
33 | "publish.hou"]
34 | optional = True
35 |
36 | def process(self, context):
37 |
38 | errored_plugins = get_errored_plugins_from_context(context)
39 | if any(
40 | plugin.__name__ == "HoudiniSubmitPublishDeadline"
41 | for plugin in errored_plugins
42 | ):
43 | raise KnownPublishError(
44 | "Skipping incrementing current file because "
45 | "submission to deadline failed."
46 | )
47 |
48 | # Filename must not have changed since collecting.
49 | host = registered_host()
50 | current_filepath: str = host.get_current_workfile()
51 | if context.data["currentFile"] != current_filepath:
52 | raise KnownPublishError(
53 | f"Collected filename '{context.data['currentFile']}' differs"
54 | f" from current scene name '{current_filepath}'."
55 | )
56 |
57 | try:
58 | from ayon_core.pipeline.workfile import save_next_version
59 | from ayon_core.host.interfaces import SaveWorkfileOptionalData
60 |
61 | current_filename = os.path.basename(current_filepath)
62 | save_next_version(
63 | description=(
64 | f"Incremented by publishing from {current_filename}"
65 | ),
66 | # Optimize the save by reducing needed queries for context
67 | prepared_data=SaveWorkfileOptionalData(
68 | project_entity=context.data["projectEntity"],
69 | project_settings=context.data["project_settings"],
70 | anatomy=context.data["anatomy"],
71 | )
72 | )
73 | except ImportError:
74 | # Backwards compatibility before ayon-core 1.5.0
75 | self.log.debug(
76 | "Using legacy `version_up`. Update AYON core addon to "
77 | "use newer `save_next_version` function."
78 | )
79 | new_filepath = version_up(current_filepath)
80 | host: IWorkfileHost = registered_host()
81 | host.save_workfile(new_filepath)
82 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/publish/collect_karma_rop.py:
--------------------------------------------------------------------------------
1 | import re
2 | import os
3 |
4 | import hou
5 | import pyblish.api
6 |
7 | from ayon_houdini.api.lib import evalParmNoFrame
8 | from ayon_houdini.api import plugin
9 |
10 |
11 | class CollectKarmaROPRenderProducts(plugin.HoudiniInstancePlugin):
12 | """Collect Karma Render Products
13 |
14 | Collects the instance.data["files"] for the multipart render product.
15 |
16 | Provides:
17 | instance -> files
18 |
19 | """
20 |
21 | label = "Karma ROP Render Products"
22 | # This specific order value is used so that
23 | # this plugin runs after CollectFrames
24 | order = pyblish.api.CollectorOrder + 0.11
25 | families = ["karma_rop"]
26 |
27 | def process(self, instance):
28 |
29 | rop = hou.node(instance.data.get("instance_node"))
30 |
31 | default_prefix = evalParmNoFrame(rop, "picture")
32 | render_products = []
33 |
34 | # Default beauty AOV
35 | beauty_product = self.get_render_product_name(
36 | prefix=default_prefix, suffix=None
37 | )
38 | render_products.append(beauty_product)
39 |
40 | files_by_aov = {
41 | "beauty": self.generate_expected_files(instance,
42 | beauty_product)
43 | }
44 |
45 | # Review Logic expects this key to exist and be True
46 | # if render is a multipart Exr.
47 | # As long as we have one AOV then multipartExr should be True.
48 | # By default karma render is a multipart Exr.
49 | instance.data["multipartExr"] = True
50 |
51 | filenames = list(render_products)
52 | instance.data["files"] = filenames
53 |
54 | for product in render_products:
55 | self.log.debug("Found render product: %s" % product)
56 |
57 | if "expectedFiles" not in instance.data:
58 | instance.data["expectedFiles"] = list()
59 | instance.data["expectedFiles"].append(files_by_aov)
60 |
61 | def get_render_product_name(self, prefix, suffix):
62 | product_name = prefix
63 | if suffix:
64 | # Add ".{suffix}" before the extension
65 | prefix_base, ext = os.path.splitext(prefix)
66 | product_name = "{}.{}{}".format(prefix_base, suffix, ext)
67 |
68 | return product_name
69 |
70 | def generate_expected_files(self, instance, path):
71 | """Create expected files in instance data"""
72 |
73 | dir = os.path.dirname(path)
74 | file = os.path.basename(path)
75 |
76 | if "#" in file:
77 | def replace(match):
78 | return "%0{}d".format(len(match.group()))
79 |
80 | file = re.sub("#+", replace, file)
81 |
82 | if "%" not in file:
83 | return path
84 |
85 | expected_files = []
86 | start = instance.data["frameStartHandle"]
87 | end = instance.data["frameEndHandle"]
88 |
89 | for i in range(int(start), (int(end) + 1)):
90 | expected_files.append(
91 | os.path.join(dir, (file % i)).replace("\\", "/"))
92 |
93 | return expected_files
94 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/load/load_alembic_archive.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import hou
4 |
5 | from ayon_houdini.api import (
6 | pipeline,
7 | plugin,
8 | lib
9 | )
10 |
11 |
12 | class AbcArchiveLoader(plugin.HoudiniLoader):
13 | """Load Alembic as full geometry network hierarchy """
14 |
15 | product_types = {"model", "animation", "pointcache", "gpuCache"}
16 | label = "Load Alembic as Archive"
17 | representations = {"*"}
18 | extensions = {"abc"}
19 | order = -5
20 | icon = "code-fork"
21 | color = "orange"
22 |
23 | def load(self, context, name=None, namespace=None, data=None):
24 | # Format file name, Houdini only wants forward slashes
25 | file_path = self.filepath_from_context(context)
26 | file_path = os.path.normpath(file_path)
27 | file_path = file_path.replace("\\", "/")
28 |
29 | # Get the root node
30 | obj = hou.node("/obj")
31 |
32 | # Define node name
33 | namespace = namespace if namespace else context["folder"]["name"]
34 | node_name = "{}_{}".format(namespace, name) if namespace else name
35 |
36 | # Create an Alembic archive node
37 | node = obj.createNode("alembicarchive", node_name=node_name)
38 | node.moveToGoodPosition()
39 |
40 | # TODO: add FPS of project / folder
41 | node.setParms({"fileName": file_path,
42 | "channelRef": True})
43 |
44 | # Apply some magic
45 | node.parm("buildHierarchy").pressButton()
46 | node.moveToGoodPosition()
47 |
48 | nodes = [node]
49 |
50 | self[:] = nodes
51 |
52 | return pipeline.containerise(node_name,
53 | namespace,
54 | nodes,
55 | context,
56 | self.__class__.__name__,
57 | suffix="")
58 |
59 | def update(self, container, context):
60 | node = container["node"]
61 |
62 | # Update the file path
63 | file_path = self.filepath_from_context(context)
64 | file_path = file_path.replace("\\", "/")
65 |
66 | # Update attributes
67 | node.setParms({"fileName": file_path,
68 | "representation": context["representation"]["id"]})
69 |
70 | # Rebuild
71 | node.parm("buildHierarchy").pressButton()
72 |
73 | def remove(self, container):
74 |
75 | node = container["node"]
76 | node.destroy()
77 |
78 | def switch(self, container, context):
79 | self.update(container, context)
80 |
81 | def create_load_placeholder_node(
82 | self, node_name: str, placeholder_data: dict
83 | ) -> hou.Node:
84 | """Define how to create a placeholder node for this loader for the
85 | Workfile Template Builder system."""
86 | # Create node
87 | network = lib.find_active_network(
88 | category=hou.objNodeTypeCategory(),
89 | default="/obj"
90 | )
91 | node = network.createNode("null", node_name=node_name)
92 | node.moveToGoodPosition()
93 | return node
94 |
--------------------------------------------------------------------------------
/client/ayon_houdini/plugins/load/load_usd_sop.py:
--------------------------------------------------------------------------------
1 | import hou
2 |
3 | from ayon_houdini.api import (
4 | pipeline,
5 | plugin,
6 | lib
7 | )
8 |
9 |
10 | class SopUsdImportLoader(plugin.HoudiniLoader):
11 | """Load USD to SOPs via `usdimport`"""
12 |
13 | label = "Load USD to SOPs"
14 | product_types = {"*"}
15 | representations = {"usd"}
16 | order = -6
17 | icon = "code-fork"
18 | color = "orange"
19 |
20 | use_ayon_entity_uri = False
21 |
22 | def load(self, context, name=None, namespace=None, data=None):
23 | # Format file name, Houdini only wants forward slashes
24 | file_path = self.filepath_from_context(context)
25 | file_path = file_path.replace("\\", "/")
26 |
27 | # Get the root node
28 | obj = hou.node("/obj")
29 |
30 | # Define node name
31 | namespace = namespace if namespace else context["folder"]["name"]
32 | node_name = "{}_{}".format(namespace, name) if namespace else name
33 |
34 | # Create a new geo node
35 | container = obj.createNode("geo", node_name=node_name)
36 |
37 | # Create a usdimport node
38 | usdimport = container.createNode("usdimport", node_name=node_name)
39 | usdimport.setParms({"filepath1": file_path})
40 |
41 | # Set new position for unpack node else it gets cluttered
42 | nodes = [container, usdimport]
43 |
44 | return pipeline.containerise(
45 | node_name,
46 | namespace,
47 | nodes,
48 | context,
49 | self.__class__.__name__,
50 | suffix="",
51 | )
52 |
53 | def update(self, container, context):
54 | node = container["node"]
55 | try:
56 | usdimport_node = next(
57 | n for n in node.children() if n.type().name() == "usdimport"
58 | )
59 | except StopIteration:
60 | self.log.error("Could not find node of type `usdimport`")
61 | return
62 |
63 | # Update the file path
64 | file_path = self.filepath_from_context(context)
65 | file_path = file_path.replace("\\", "/")
66 |
67 | usdimport_node.setParms({"filepath1": file_path})
68 |
69 | # Update attribute
70 | node.setParms({"representation": context["representation"]["id"]})
71 |
72 | def remove(self, container):
73 | node = container["node"]
74 | node.destroy()
75 |
76 | def switch(self, container, representation):
77 | self.update(container, representation)
78 |
79 | def create_load_placeholder_node(
80 | self, node_name: str, placeholder_data: dict
81 | ) -> hou.Node:
82 | """Define how to create a placeholder node for this loader for the
83 | Workfile Template Builder system."""
84 | # Create node
85 | network = lib.find_active_network(
86 | category=hou.sopNodeTypeCategory(),
87 | default="/obj/geo1"
88 | )
89 | if not network:
90 | network = hou.node("/obj").createNode("geo", "geo1")
91 | node = network.createNode("null", node_name=node_name)
92 | node.moveToGoodPosition()
93 | return node
94 |
--------------------------------------------------------------------------------