├── 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 | 5 | 6 | 7 | 8 | len(kwargs["parms"]) > 0 and kwargs["parms"][0].parmTemplate().type() == hou.parmTemplateType.String and kwargs["parms"][0].parmTemplate().stringType() == hou.stringParmType.FileReference and not isinstance(kwargs["parms"][0].node(), hou.RopNode) 9 | 10 | 16 | 17 | 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 | 8 | 9 | 10 | opmenu.unsynchronize 11 | 12 | opmenu.vhda_create 13 | 14 | 15 | 16 | 17 | 25 | 26 | 27 | 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 | --------------------------------------------------------------------------------