diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 249da3da0e..e377773007 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -35,6 +35,10 @@ body:
label: Version
description: What version are you running? Look to OpenPype Tray
options:
+ - 3.17.6-nightly.2
+ - 3.17.6-nightly.1
+ - 3.17.5
+ - 3.17.5-nightly.3
- 3.17.5-nightly.2
- 3.17.5-nightly.1
- 3.17.4
@@ -131,10 +135,6 @@ body:
- 3.15.2-nightly.1
- 3.15.1
- 3.15.1-nightly.6
- - 3.15.1-nightly.5
- - 3.15.1-nightly.4
- - 3.15.1-nightly.3
- - 3.15.1-nightly.2
validations:
required: true
- type: dropdown
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7432b33e24..b3daf581ac 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,463 @@
# Changelog
+## [3.17.5](https://github.com/ynput/OpenPype/tree/3.17.5)
+
+
+[Full Changelog](https://github.com/ynput/OpenPype/compare/3.17.4...3.17.5)
+
+### **🆕 New features**
+
+
+
+Fusion: Add USD loader #4896
+
+Add an OpenPype managed USD loader (`uLoader`) for Fusion.
+
+
+___
+
+
+
+
+
+Fusion: Resolution validator #5325
+
+Added a resolution validator.The code is from my old PR (https://github.com/ynput/OpenPype/pull/4921) that I closed because the PR also contained a frame range validator that no longer is needed.
+
+
+___
+
+
+
+
+
+Context Selection tool: Refactor Context tool (for AYON) #5766
+
+Context selection tool has AYON variant.
+
+
+___
+
+
+
+
+
+AYON: Use AYON username for user in template data #5842
+
+Use ayon username for template data in AYON mode.
+
+
+___
+
+
+
+
+
+Testing: app_group flag #5869
+
+`app_group` command flag. This is for changing which flavour of the host to launch. In the case of Maya, you can launch Maya and MayaPy, but it can be used for the Nuke family as well.Split from #5644
+
+
+___
+
+
+
+### **🚀 Enhancements**
+
+
+
+Enhancement: Fusion fix saver creation + minor Blender/Fusion logging tweaks #5558
+
+- Blender change logs to `debug` level in preparation for new publisher artist facing reports (note that it currently still uses the old publisher)
+- Fusion: Create Saver fix redeclaration of default_variants
+- Fusion: Fix saver being created in incorrect state without saving directly after create
+- Fusion: Allow reset frame range on render family
+- Fusion: Tweak logging level for artist-facing report
+
+
+___
+
+
+
+
+
+Resolve: load clip to timeline at set time #5665
+
+It is possible to load clip to correct place on timeline.
+
+
+___
+
+
+
+
+
+Nuke: Optional Deadline workfile dependency. #5732
+
+Adds option to add the workfile as dependency for the Deadline job.Think it used to have something like this, but it disappeared. Usecase is for remote workflow where the Nuke script needs to be synced before the job can start.
+
+
+___
+
+
+
+
+
+Enhancement/houdini rearrange ayon houdini settings files #5748
+
+Rearranging Houdini Settings to be more readable, easier to edit, update settings (include all families/product types)This PR is mainly for Ayon Settings to have more organized files. For Openpype, I'll make sure that each Houdini setting in Ayon has an equivalent in Openpype.
+- [x] update Ayon settings, fix typos and remove deprecated settings.
+- [x] Sync with Openpype
+- [x] Test in Openpype
+- [x] Test in Ayon
+
+
+___
+
+
+
+
+
+Chore: updating create ayon addon script #5822
+
+Adding developers environment options.
+
+
+___
+
+
+
+
+
+Max: Implement Validator for Properties/Attributes Value Check #5824
+
+Add optional validator which can check if the property attributes are valid in Max
+
+
+___
+
+
+
+
+
+Nuke: Remove unused 'get_render_path' function #5826
+
+Remove unused function `get_render_path` from nuke integration.
+
+
+___
+
+
+
+
+
+Chore: Limit current context template data function #5845
+
+Current implementation of `get_current_context_template_data` does return the same values as base template data function `get_template_data`.
+
+
+___
+
+
+
+
+
+Max: Make sure Collect Render not ignoring instance asset #5847
+
+- Make sure Collect Render is not always using asset from context.
+- Make sure Scene version being collected
+- Clean up unnecessary uses of code in the collector.
+
+
+___
+
+
+
+
+
+Ftrack: Events are not processed if project is not available in OpenPype #5853
+
+Events that happened on project which is not in OpenPype is not processed.
+
+
+___
+
+
+
+
+
+Nuke: Add Nuke 11.0 as default setting #5855
+
+Found I needed Nuke 11.0 in the default settings to help with unit testing.
+
+
+___
+
+
+
+
+
+TVPaint: Code cleanup #5857
+
+Removed unused import. Use `AYON` label in ayon mode. Removed unused data in publish context `"previous_context"`.
+
+
+___
+
+
+
+
+
+AYON settings: Use correct label for follow workfile version #5874
+
+Follow workfile version label was marked as Collect Anatomy Instance Data label.
+
+
+___
+
+
+
+### **🐛 Bug fixes**
+
+
+
+Nuke: Fix workfile template builder so representations get loaded next to each other #5061
+
+Refactor when the cleanup of the placeholder happens for the cases where multiple representations are loaded by a single placeholder.The existing code didn't take into account the case where a template placeholder can load multiple representations so it was trying to do the cleanup of the placeholder node and the re-arrangement of the imported nodes too early. I assume this was designed only for the cases where a single representation can load multiple nodes.
+
+
+___
+
+
+
+
+
+Nuke: Dont update node name on update #5704
+
+When updating `Image` containers the code is trying to set the name of the node. This results in a warning message from Nuke shown below;Suggesting to not change the node name when updating.
+
+
+___
+
+
+
+
+
+UIDefLabel can be unique #5827
+
+`UILabelDef` have implemented comparison and uniqueness.
+
+
+___
+
+
+
+
+
+AYON: Skip kitsu module when creating ayon addons #5828
+
+Create AYON packages is skipping kitsu module in creation of modules/addons and kitsu module is not loaded from modules on start. The addon already has it's repository https://github.com/ynput/ayon-kitsu.
+
+
+___
+
+
+
+
+
+Bugfix: Collect Rendered Files only collecting first instance #5832
+
+Collect all instances from the metadata file - don't return on first instance iteration.
+
+
+___
+
+
+
+
+
+Houdini: set frame range for the created composite ROP #5833
+
+Quick bug fix for created composite ROP, set its frame range to the frame range of the playbar.
+
+
+___
+
+
+
+
+
+Fix registering launcher actions from OpenPypeModules #5843
+
+Fix typo `actions_dir` -> `path` to fix register launcher actions fromm OpenPypeModule
+
+
+___
+
+
+
+
+
+Bugfix in houdini shelves manager and beautify settings #5844
+
+This PR fixes the problem in this PR https://github.com/ynput/OpenPype/issues/5457 by using the right function to load a pre-made houdini `.shelf` fileAlso, it beautifies houdini shelves settings to provide better guidance for users which helps with other issue https://github.com/ynput/OpenPype/issues/5458 , Rather adding default shelf and set names, I'll educate users how to use the tool correctly.Users now are able to select between the two options.| OpenPype | Ayon || -- | -- || | |
+
+
+___
+
+
+
+
+
+Blender: Fix missing Grease Pencils in review #5848
+
+Fix Grease Pencil missing in review when isolating objects.
+
+
+___
+
+
+
+
+
+Blender: Fix Render Settings in Ayon #5849
+
+Fix Render Settings in Ayon for Blender.
+
+
+___
+
+
+
+
+
+Bugfix: houdini tab menu working as expected #5850
+
+This PR:Tab menu name changes to Ayon when using ayon get_network_categories is checked in all creator plugins. | Product | Network Category | | -- | -- | | Alembic camera | rop, obj | | Arnold Ass | rop | | Arnold ROP | rop | | Bgeo | rop, sop | | composite sequence | cop2, rop | | hda | obj | | Karma ROP | rop | | Mantra ROP | rop | | ABC | rop, sop | | RS proxy | rop, sop| | RS ROP | rop | | Review | rop | | Static mesh | rop, obj, sop | | USD | lop, rop | | USD Render | rop | | VDB | rop, obj, sop | | V Ray | rop |
+
+
+___
+
+
+
+
+
+Bigfix: Houdini skip frame_range_validator if node has no 'trange' parameter #5851
+
+I faced a bug when publishing HDA instance as it has no `trange` parameter. As this PR title says : skip frame_range_validator if node has no 'trange' parameter
+
+
+___
+
+
+
+
+
+Bugfix: houdini image sequence loading and missing frames #5852
+
+I made this PR in to fix issues mentioned here https://github.com/ynput/OpenPype/pull/5833#issuecomment-1789207727in short:
+- image load doesn't work
+- publisher only publish one frame
+
+
+___
+
+
+
+
+
+Nuke: loaders' containers updating as nodes #5854
+
+Nuke loaded containers are updating correctly even they have been duplicating of originally loaded nodes. This had previously been removed duplicated nodes.
+
+
+___
+
+
+
+
+
+deadline: settings are not blocking extension input #5864
+
+Settings are not blocking user input.
+
+
+___
+
+
+
+
+
+Blender: Fix loading of blend layouts #5866
+
+Fix a problem with loading blend layouts.
+
+
+___
+
+
+
+
+
+AYON: Launcher refresh issues #5867
+
+Fixed refresh of projects issue in launcher tool. And renamed Qt models to contain `Qt` in their name (it was really hard to find out where were used). It is not possible to click on disabled item in launcher's projects view.
+
+
+___
+
+
+
+
+
+Fix the Wrong key words for tycache workfile template settings in AYON #5870
+
+Fix the wrong key words for the tycache workfile template settings in AYON(i.e. Instead of families, product_types should be used)
+
+
+___
+
+
+
+
+
+AYON tools: Handle empty icon definition #5876
+
+Ignore if passed icon definition is `None`.
+
+
+___
+
+
+
+### **🔀 Refactored code**
+
+
+
+Houdini: Remove on instance toggled callback #5860
+
+Remove on instance toggled callback which isn't relevant to the new publisher
+
+
+___
+
+
+
+
+
+Chore: Remove unused `instanceToggled` callbacks #5862
+
+The `instanceToggled` callbacks should be irrelevant for new publisher.
+
+
+___
+
+
+
+
+
+
## [3.17.4](https://github.com/ynput/OpenPype/tree/3.17.4)
diff --git a/openpype/cli.py b/openpype/cli.py
index 7422f32f13..f0fe550a1f 100644
--- a/openpype/cli.py
+++ b/openpype/cli.py
@@ -282,6 +282,9 @@ def run(script):
"--app_variant",
help="Provide specific app variant for test, empty for latest",
default=None)
+@click.option("--app_group",
+ help="Provide specific app group for test, empty for default",
+ default=None)
@click.option("-t",
"--timeout",
help="Provide specific timeout value for test case",
@@ -294,11 +297,11 @@ def run(script):
help="MongoDB for testing.",
default=None)
def runtests(folder, mark, pyargs, test_data_folder, persist, app_variant,
- timeout, setup_only, mongo_url):
+ timeout, setup_only, mongo_url, app_group):
"""Run all automatic tests after proper initialization via start.py"""
PypeCommands().run_tests(folder, mark, pyargs, test_data_folder,
persist, app_variant, timeout, setup_only,
- mongo_url)
+ mongo_url, app_group)
@main.command(help="DEPRECATED - run sync server")
diff --git a/openpype/client/__init__.py b/openpype/client/__init__.py
index 7831afd8ad..fe6dc97877 100644
--- a/openpype/client/__init__.py
+++ b/openpype/client/__init__.py
@@ -1,6 +1,7 @@
from .mongo import (
OpenPypeMongoConnection,
)
+from .server.utils import get_ayon_server_api_connection
from .entities import (
get_projects,
@@ -59,6 +60,8 @@ from .operations import (
__all__ = (
"OpenPypeMongoConnection",
+ "get_ayon_server_api_connection",
+
"get_projects",
"get_project",
"get_whole_project",
diff --git a/openpype/client/server/entities.py b/openpype/client/server/entities.py
index 16223d3d91..b41727a797 100644
--- a/openpype/client/server/entities.py
+++ b/openpype/client/server/entities.py
@@ -1,9 +1,8 @@
import collections
-from ayon_api import get_server_api_connection
-
from openpype.client.mongo.operations import CURRENT_THUMBNAIL_SCHEMA
+from .utils import get_ayon_server_api_connection
from .openpype_comp import get_folders_with_tasks
from .conversion_utils import (
project_fields_v3_to_v4,
@@ -37,7 +36,7 @@ def get_projects(active=True, inactive=False, library=None, fields=None):
elif inactive:
active = False
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
fields = project_fields_v3_to_v4(fields, con)
for project in con.get_projects(active, library, fields=fields):
yield convert_v4_project_to_v3(project)
@@ -45,7 +44,7 @@ def get_projects(active=True, inactive=False, library=None, fields=None):
def get_project(project_name, active=True, inactive=False, fields=None):
# Skip if both are disabled
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
fields = project_fields_v3_to_v4(fields, con)
return convert_v4_project_to_v3(
con.get_project(project_name, fields=fields)
@@ -66,7 +65,7 @@ def _get_subsets(
fields=None
):
# Convert fields and add minimum required fields
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
fields = subset_fields_v3_to_v4(fields, con)
if fields is not None:
for key in (
@@ -102,7 +101,7 @@ def _get_versions(
active=None,
fields=None
):
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
fields = version_fields_v3_to_v4(fields, con)
@@ -198,7 +197,7 @@ def get_assets(
if archived:
active = None
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
fields = folder_fields_v3_to_v4(fields, con)
kwargs = dict(
folder_ids=asset_ids,
@@ -236,7 +235,7 @@ def get_archived_assets(
def get_asset_ids_with_subsets(project_name, asset_ids=None):
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
return con.get_folder_ids_with_products(project_name, asset_ids)
@@ -282,7 +281,7 @@ def get_subsets(
def get_subset_families(project_name, subset_ids=None):
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
return con.get_product_type_names(project_name, subset_ids)
@@ -430,7 +429,7 @@ def get_output_link_versions(project_name, version_id, fields=None):
if not version_id:
return []
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
version_links = con.get_version_links(
project_name, version_id, link_direction="out")
@@ -446,7 +445,7 @@ def get_output_link_versions(project_name, version_id, fields=None):
def version_is_latest(project_name, version_id):
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
return con.version_is_latest(project_name, version_id)
@@ -501,7 +500,7 @@ def get_representations(
else:
active = None
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
fields = representation_fields_v3_to_v4(fields, con)
if fields and active is not None:
fields.add("active")
@@ -535,7 +534,7 @@ def get_representations_parents(project_name, representations):
repre["_id"]
for repre in representations
}
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
parents_by_repre_id = con.get_representations_parents(project_name,
repre_ids)
folder_ids = set()
@@ -677,7 +676,7 @@ def get_workfile_info(
if not asset_id or not task_name or not filename:
return None
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
task = con.get_task_by_name(
project_name, asset_id, task_name, fields=["id", "name", "folderId"]
)
diff --git a/openpype/client/server/entity_links.py b/openpype/client/server/entity_links.py
index d8395aabe7..368dcdcb9d 100644
--- a/openpype/client/server/entity_links.py
+++ b/openpype/client/server/entity_links.py
@@ -1,6 +1,4 @@
-import ayon_api
-from ayon_api import get_folder_links, get_versions_links
-
+from .utils import get_ayon_server_api_connection
from .entities import get_assets, get_representation_by_id
@@ -28,7 +26,8 @@ def get_linked_asset_ids(project_name, asset_doc=None, asset_id=None):
if not asset_id:
asset_id = asset_doc["_id"]
- links = get_folder_links(project_name, asset_id, link_direction="in")
+ con = get_ayon_server_api_connection()
+ links = con.get_folder_links(project_name, asset_id, link_direction="in")
return [
link["entityId"]
for link in links
@@ -115,6 +114,7 @@ def get_linked_representation_id(
if link_type:
link_types = [link_type]
+ con = get_ayon_server_api_connection()
# Store already found version ids to avoid recursion, and also to store
# output -> Don't forget to remove 'version_id' at the end!!!
linked_version_ids = {version_id}
@@ -124,7 +124,7 @@ def get_linked_representation_id(
if not versions_to_check:
break
- links = get_versions_links(
+ links = con.get_versions_links(
project_name,
versions_to_check,
link_types=link_types,
@@ -145,8 +145,8 @@ def get_linked_representation_id(
linked_version_ids.remove(version_id)
if not linked_version_ids:
return []
-
- representations = ayon_api.get_representations(
+ con = get_ayon_server_api_connection()
+ representations = con.get_representations(
project_name,
version_ids=linked_version_ids,
fields=["id"])
diff --git a/openpype/client/server/operations.py b/openpype/client/server/operations.py
index 5b38405c34..eddc1eaf60 100644
--- a/openpype/client/server/operations.py
+++ b/openpype/client/server/operations.py
@@ -5,7 +5,6 @@ import uuid
import datetime
from bson.objectid import ObjectId
-from ayon_api import get_server_api_connection
from openpype.client.operations_base import (
REMOVED_VALUE,
@@ -41,7 +40,7 @@ from .conversion_utils import (
convert_update_representation_to_v4,
convert_update_workfile_info_to_v4,
)
-from .utils import create_entity_id
+from .utils import create_entity_id, get_ayon_server_api_connection
def _create_or_convert_to_id(entity_id=None):
@@ -680,7 +679,7 @@ class OperationsSession(BaseOperationsSession):
def __init__(self, con=None, *args, **kwargs):
super(OperationsSession, self).__init__(*args, **kwargs)
if con is None:
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
self._con = con
self._project_cache = {}
self._nested_operations = collections.defaultdict(list)
@@ -858,7 +857,7 @@ def create_project(
"""
if con is None:
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
return con.create_project(
project_name,
@@ -870,12 +869,12 @@ def create_project(
def delete_project(project_name, con=None):
if con is None:
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
return con.delete_project(project_name)
def create_thumbnail(project_name, src_filepath, thumbnail_id=None, con=None):
if con is None:
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
return con.create_thumbnail(project_name, src_filepath, thumbnail_id)
diff --git a/openpype/client/server/utils.py b/openpype/client/server/utils.py
index ed128cfad9..a9dcf539bd 100644
--- a/openpype/client/server/utils.py
+++ b/openpype/client/server/utils.py
@@ -1,8 +1,33 @@
+import os
import uuid
+import ayon_api
+
from openpype.client.operations_base import REMOVED_VALUE
+class _GlobalCache:
+ initialized = False
+
+
+def get_ayon_server_api_connection():
+ if _GlobalCache.initialized:
+ con = ayon_api.get_server_api_connection()
+ else:
+ from openpype.lib.local_settings import get_local_site_id
+
+ _GlobalCache.initialized = True
+ site_id = get_local_site_id()
+ version = os.getenv("AYON_VERSION")
+ if ayon_api.is_connection_created():
+ con = ayon_api.get_server_api_connection()
+ con.set_site_id(site_id)
+ con.set_client_version(version)
+ else:
+ con = ayon_api.create_connection(site_id, version)
+ return con
+
+
def create_entity_id():
return uuid.uuid1().hex
diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py
index 8fc7a70dd8..e059f7c272 100644
--- a/openpype/hosts/aftereffects/api/pipeline.py
+++ b/openpype/hosts/aftereffects/api/pipeline.py
@@ -74,11 +74,6 @@ class AfterEffectsHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
register_loader_plugin_path(LOAD_PATH)
register_creator_plugin_path(CREATE_PATH)
- log.info(PUBLISH_PATH)
-
- pyblish.api.register_callback(
- "instanceToggled", on_pyblish_instance_toggled
- )
register_event_callback("application.launched", application_launch)
@@ -186,11 +181,6 @@ def application_launch():
check_inventory()
-def on_pyblish_instance_toggled(instance, old_value, new_value):
- """Toggle layer visibility on instance toggles."""
- instance[0].Visible = new_value
-
-
def ls():
"""Yields containers from active AfterEffects document.
diff --git a/openpype/hosts/blender/api/lib.py b/openpype/hosts/blender/api/lib.py
index 9bb560c364..1f68dd0839 100644
--- a/openpype/hosts/blender/api/lib.py
+++ b/openpype/hosts/blender/api/lib.py
@@ -266,9 +266,57 @@ def read(node: bpy.types.bpy_struct_meta_idprop):
return data
-def get_selection() -> List[bpy.types.Object]:
- """Return the selected objects from the current scene."""
- return [obj for obj in bpy.context.scene.objects if obj.select_get()]
+def get_selected_collections():
+ """
+ Returns a list of the currently selected collections in the outliner.
+
+ Raises:
+ RuntimeError: If the outliner cannot be found in the main Blender
+ window.
+
+ Returns:
+ list: A list of `bpy.types.Collection` objects that are currently
+ selected in the outliner.
+ """
+ try:
+ area = next(
+ area for area in bpy.context.window.screen.areas
+ if area.type == 'OUTLINER')
+ region = next(
+ region for region in area.regions
+ if region.type == 'WINDOW')
+ except StopIteration as e:
+ raise RuntimeError("Could not find outliner. An outliner space "
+ "must be in the main Blender window.") from e
+
+ with bpy.context.temp_override(
+ window=bpy.context.window,
+ area=area,
+ region=region,
+ screen=bpy.context.window.screen
+ ):
+ ids = bpy.context.selected_ids
+
+ return [id for id in ids if isinstance(id, bpy.types.Collection)]
+
+
+def get_selection(include_collections: bool = False) -> List[bpy.types.Object]:
+ """
+ Returns a list of selected objects in the current Blender scene.
+
+ Args:
+ include_collections (bool, optional): Whether to include selected
+ collections in the result. Defaults to False.
+
+ Returns:
+ List[bpy.types.Object]: A list of selected objects.
+ """
+ selection = [obj for obj in bpy.context.scene.objects if obj.select_get()]
+
+ if include_collections:
+ selection.extend(get_selected_collections())
+
+ return selection
@contextlib.contextmanager
diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py
index fb87d08cce..2f940011ba 100644
--- a/openpype/hosts/blender/api/plugin.py
+++ b/openpype/hosts/blender/api/plugin.py
@@ -9,7 +9,10 @@ from openpype.pipeline import (
LegacyCreator,
LoaderPlugin,
)
-from .pipeline import AVALON_CONTAINERS
+from .pipeline import (
+ AVALON_CONTAINERS,
+ AVALON_PROPERTY,
+)
from .ops import (
MainThreadItem,
execute_in_main_thread
@@ -40,9 +43,16 @@ def get_unique_number(
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
if not avalon_container:
return "01"
- asset_groups = avalon_container.all_objects
-
- container_names = [c.name for c in asset_groups if c.type == 'EMPTY']
+ # Check the names of both object and collection containers
+ obj_asset_groups = avalon_container.objects
+ obj_group_names = {
+ c.name for c in obj_asset_groups
+ if c.type == 'EMPTY' and c.get(AVALON_PROPERTY)}
+ coll_asset_groups = avalon_container.children
+ coll_group_names = {
+ c.name for c in coll_asset_groups
+ if c.get(AVALON_PROPERTY)}
+ container_names = obj_group_names.union(coll_group_names)
count = 1
name = f"{asset}_{count:0>2}_{subset}"
while name in container_names:
diff --git a/openpype/hosts/blender/plugins/create/create_blendScene.py b/openpype/hosts/blender/plugins/create/create_blendScene.py
index 63bcf212ff..bb57a16888 100644
--- a/openpype/hosts/blender/plugins/create/create_blendScene.py
+++ b/openpype/hosts/blender/plugins/create/create_blendScene.py
@@ -15,6 +15,8 @@ class CreateBlendScene(plugin.Creator):
family = "blendScene"
icon = "cubes"
+ maintain_selection = False
+
def process(self):
""" Run the creator on Blender main thread"""
mti = ops.MainThreadItem(self._process)
@@ -31,21 +33,20 @@ class CreateBlendScene(plugin.Creator):
asset = self.data["asset"]
subset = self.data["subset"]
name = plugin.asset_name(asset, subset)
- asset_group = bpy.data.objects.new(name=name, object_data=None)
- asset_group.empty_display_type = 'SINGLE_ARROW'
- instances.objects.link(asset_group)
+
+ # Create the new asset group as collection
+ asset_group = bpy.data.collections.new(name=name)
+ instances.children.link(asset_group)
self.data['task'] = get_current_task_name()
lib.imprint(asset_group, self.data)
- # Add selected objects to instance
if (self.options or {}).get("useSelection"):
- bpy.context.view_layer.objects.active = asset_group
- selected = lib.get_selection()
- for obj in selected:
- if obj.parent in selected:
- obj.select_set(False)
- continue
- selected.append(asset_group)
- bpy.ops.object.parent_set(keep_transform=True)
+ selection = lib.get_selection(include_collections=True)
+
+ for data in selection:
+ if isinstance(data, bpy.types.Collection):
+ asset_group.children.link(data)
+ elif isinstance(data, bpy.types.Object):
+ asset_group.objects.link(data)
return asset_group
diff --git a/openpype/hosts/blender/plugins/load/load_blend.py b/openpype/hosts/blender/plugins/load/load_blend.py
index 25d6568889..f7bbc630de 100644
--- a/openpype/hosts/blender/plugins/load/load_blend.py
+++ b/openpype/hosts/blender/plugins/load/load_blend.py
@@ -20,7 +20,7 @@ from openpype.hosts.blender.api.pipeline import (
class BlendLoader(plugin.AssetLoader):
"""Load assets from a .blend file."""
- families = ["model", "rig", "layout", "camera", "blendScene"]
+ families = ["model", "rig", "layout", "camera"]
representations = ["blend"]
label = "Append Blend"
@@ -32,7 +32,7 @@ class BlendLoader(plugin.AssetLoader):
empties = [obj for obj in objects if obj.type == 'EMPTY']
for empty in empties:
- if empty.get(AVALON_PROPERTY):
+ if empty.get(AVALON_PROPERTY) and empty.parent is None:
return empty
return None
@@ -90,6 +90,7 @@ class BlendLoader(plugin.AssetLoader):
members.append(data)
container = self._get_asset_container(data_to.objects)
+ print(container)
assert container, "No asset group found"
container.name = group_name
@@ -100,8 +101,11 @@ class BlendLoader(plugin.AssetLoader):
# Link all the container children to the collection
for obj in container.children_recursive:
+ print(obj)
bpy.context.scene.collection.objects.link(obj)
+ print("")
+
# Remove the library from the blend file
library = bpy.data.libraries.get(bpy.path.basename(libpath))
bpy.data.libraries.remove(library)
diff --git a/openpype/hosts/blender/plugins/load/load_blendscene.py b/openpype/hosts/blender/plugins/load/load_blendscene.py
new file mode 100644
index 0000000000..2c955af9e8
--- /dev/null
+++ b/openpype/hosts/blender/plugins/load/load_blendscene.py
@@ -0,0 +1,221 @@
+from typing import Dict, List, Optional
+from pathlib import Path
+
+import bpy
+
+from openpype.pipeline import (
+ get_representation_path,
+ AVALON_CONTAINER_ID,
+)
+from openpype.hosts.blender.api import plugin
+from openpype.hosts.blender.api.lib import imprint
+from openpype.hosts.blender.api.pipeline import (
+ AVALON_CONTAINERS,
+ AVALON_PROPERTY,
+)
+
+
+class BlendSceneLoader(plugin.AssetLoader):
+ """Load assets from a .blend file."""
+
+ families = ["blendScene"]
+ representations = ["blend"]
+
+ label = "Append Blend"
+ icon = "code-fork"
+ color = "orange"
+
+ @staticmethod
+ def _get_asset_container(collections):
+ for coll in collections:
+ parents = [c for c in collections if c.user_of_id(coll)]
+ if coll.get(AVALON_PROPERTY) and not parents:
+ return coll
+
+ return None
+
+ def _process_data(self, libpath, group_name, family):
+ # Append all the data from the .blend file
+ with bpy.data.libraries.load(
+ libpath, link=False, relative=False
+ ) as (data_from, data_to):
+ for attr in dir(data_to):
+ setattr(data_to, attr, getattr(data_from, attr))
+
+ members = []
+
+ # Rename the object to add the asset name
+ for attr in dir(data_to):
+ for data in getattr(data_to, attr):
+ data.name = f"{group_name}:{data.name}"
+ members.append(data)
+
+ container = self._get_asset_container(
+ data_to.collections)
+ assert container, "No asset group found"
+
+ container.name = group_name
+
+ # Link the group to the scene
+ bpy.context.scene.collection.children.link(container)
+
+ # Remove the library from the blend file
+ library = bpy.data.libraries.get(bpy.path.basename(libpath))
+ bpy.data.libraries.remove(library)
+
+ return container, members
+
+ def process_asset(
+ self, context: dict, name: str, namespace: Optional[str] = None,
+ options: Optional[Dict] = None
+ ) -> Optional[List]:
+ """
+ Arguments:
+ name: Use pre-defined name
+ namespace: Use pre-defined namespace
+ context: Full parenthood of representation to load
+ options: Additional settings dictionary
+ """
+ libpath = self.filepath_from_context(context)
+ asset = context["asset"]["name"]
+ subset = context["subset"]["name"]
+
+ try:
+ family = context["representation"]["context"]["family"]
+ except ValueError:
+ family = "model"
+
+ asset_name = plugin.asset_name(asset, subset)
+ unique_number = plugin.get_unique_number(asset, subset)
+ group_name = plugin.asset_name(asset, subset, unique_number)
+ namespace = namespace or f"{asset}_{unique_number}"
+
+ avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
+ if not avalon_container:
+ avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS)
+ bpy.context.scene.collection.children.link(avalon_container)
+
+ container, members = self._process_data(libpath, group_name, family)
+
+ avalon_container.children.link(container)
+
+ data = {
+ "schema": "openpype:container-2.0",
+ "id": AVALON_CONTAINER_ID,
+ "name": name,
+ "namespace": namespace or '',
+ "loader": str(self.__class__.__name__),
+ "representation": str(context["representation"]["_id"]),
+ "libpath": libpath,
+ "asset_name": asset_name,
+ "parent": str(context["representation"]["parent"]),
+ "family": context["representation"]["context"]["family"],
+ "objectName": group_name,
+ "members": members,
+ }
+
+ container[AVALON_PROPERTY] = data
+
+ objects = [
+ obj for obj in bpy.data.objects
+ if obj.name.startswith(f"{group_name}:")
+ ]
+
+ self[:] = objects
+ return objects
+
+ def exec_update(self, container: Dict, representation: Dict):
+ """
+ Update the loaded asset.
+ """
+ group_name = container["objectName"]
+ asset_group = bpy.data.collections.get(group_name)
+ libpath = Path(get_representation_path(representation)).as_posix()
+
+ assert asset_group, (
+ f"The asset is not loaded: {container['objectName']}"
+ )
+
+ # Get the parents of the members of the asset group, so we can
+ # re-link them after the update.
+ # Also gets the transform for each object to reapply after the update.
+ collection_parents = {}
+ member_transforms = {}
+ members = asset_group.get(AVALON_PROPERTY).get("members", [])
+ loaded_collections = {c for c in bpy.data.collections if c in members}
+ loaded_collections.add(bpy.data.collections.get(AVALON_CONTAINERS))
+ for member in members:
+ if isinstance(member, bpy.types.Object):
+ member_parents = set(member.users_collection)
+ member_transforms[member.name] = member.matrix_basis.copy()
+ elif isinstance(member, bpy.types.Collection):
+ member_parents = {
+ c for c in bpy.data.collections if c.user_of_id(member)}
+ else:
+ continue
+
+ member_parents = member_parents.difference(loaded_collections)
+ if member_parents:
+ collection_parents[member.name] = list(member_parents)
+
+ old_data = dict(asset_group.get(AVALON_PROPERTY))
+
+ self.exec_remove(container)
+
+ family = container["family"]
+ asset_group, members = self._process_data(libpath, group_name, family)
+
+ for member in members:
+ if member.name in collection_parents:
+ for parent in collection_parents[member.name]:
+ if isinstance(member, bpy.types.Object):
+ parent.objects.link(member)
+ elif isinstance(member, bpy.types.Collection):
+ parent.children.link(member)
+ if member.name in member_transforms and isinstance(
+ member, bpy.types.Object
+ ):
+ member.matrix_basis = member_transforms[member.name]
+
+ avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
+ avalon_container.children.link(asset_group)
+
+ # Restore the old data, but reset members, as they don't exist anymore
+ # This avoids a crash, because the memory addresses of those members
+ # are not valid anymore
+ old_data["members"] = []
+ asset_group[AVALON_PROPERTY] = old_data
+
+ new_data = {
+ "libpath": libpath,
+ "representation": str(representation["_id"]),
+ "parent": str(representation["parent"]),
+ "members": members,
+ }
+
+ imprint(asset_group, new_data)
+
+ def exec_remove(self, container: Dict) -> bool:
+ """
+ Remove an existing container from a Blender scene.
+ """
+ group_name = container["objectName"]
+ asset_group = bpy.data.collections.get(group_name)
+
+ members = set(asset_group.get(AVALON_PROPERTY).get("members", []))
+
+ if members:
+ for attr_name in dir(bpy.data):
+ attr = getattr(bpy.data, attr_name)
+ if not isinstance(attr, bpy.types.bpy_prop_collection):
+ continue
+
+ # ensure to make a list copy because we
+ # we remove members as we iterate
+ for data in list(attr):
+ if data not in members or data == asset_group:
+ continue
+
+ attr.remove(data)
+
+ bpy.data.collections.remove(asset_group)
diff --git a/openpype/hosts/blender/plugins/publish/collect_instances.py b/openpype/hosts/blender/plugins/publish/collect_instances.py
index ad2ce54147..2d56e5fd7b 100644
--- a/openpype/hosts/blender/plugins/publish/collect_instances.py
+++ b/openpype/hosts/blender/plugins/publish/collect_instances.py
@@ -1,4 +1,3 @@
-import json
from typing import Generator
import bpy
@@ -50,6 +49,7 @@ class CollectInstances(pyblish.api.ContextPlugin):
for group in asset_groups:
instance = self.create_instance(context, group)
+ instance.data["instance_group"] = group
members = []
if isinstance(group, bpy.types.Collection):
members = list(group.objects)
@@ -65,6 +65,6 @@ class CollectInstances(pyblish.api.ContextPlugin):
members.append(group)
instance[:] = members
- self.log.debug(json.dumps(instance.data, indent=4))
+ self.log.debug(instance.data)
for obj in instance:
self.log.debug(obj)
diff --git a/openpype/hosts/blender/plugins/publish/collect_review.py b/openpype/hosts/blender/plugins/publish/collect_review.py
index 3bf2e39e24..2760ab9811 100644
--- a/openpype/hosts/blender/plugins/publish/collect_review.py
+++ b/openpype/hosts/blender/plugins/publish/collect_review.py
@@ -31,11 +31,12 @@ class CollectReview(pyblish.api.InstancePlugin):
focal_length = cameras[0].data.lens
- # get isolate objects list from meshes instance members .
+ # get isolate objects list from meshes instance members.
+ types = {"MESH", "GPENCIL"}
isolate_objects = [
obj
for obj in instance
- if isinstance(obj, bpy.types.Object) and obj.type == "MESH"
+ if isinstance(obj, bpy.types.Object) and obj.type in types
]
if not instance.data.get("remove"):
diff --git a/openpype/hosts/blender/plugins/publish/extract_blend.py b/openpype/hosts/blender/plugins/publish/extract_blend.py
index c8eeef7fd7..17e574c1be 100644
--- a/openpype/hosts/blender/plugins/publish/extract_blend.py
+++ b/openpype/hosts/blender/plugins/publish/extract_blend.py
@@ -25,19 +25,27 @@ class ExtractBlend(publish.Extractor):
data_blocks = set()
- for obj in instance:
- data_blocks.add(obj)
+ for data in instance:
+ data_blocks.add(data)
# Pack used images in the blend files.
- if obj.type == 'MESH':
- for material_slot in obj.material_slots:
- mat = material_slot.material
- if mat and mat.use_nodes:
- tree = mat.node_tree
- if tree.type == 'SHADER':
- for node in tree.nodes:
- if node.bl_idname == 'ShaderNodeTexImage':
- if node.image:
- node.image.pack()
+ if not (
+ isinstance(data, bpy.types.Object) and data.type == 'MESH'
+ ):
+ continue
+ for material_slot in data.material_slots:
+ mat = material_slot.material
+ if not (mat and mat.use_nodes):
+ continue
+ tree = mat.node_tree
+ if tree.type != 'SHADER':
+ continue
+ for node in tree.nodes:
+ if node.bl_idname != 'ShaderNodeTexImage':
+ continue
+ # Check if image is not packed already
+ # and pack it if not.
+ if node.image and node.image.packed_file is None:
+ node.image.pack()
bpy.data.libraries.write(filepath, data_blocks)
diff --git a/openpype/hosts/blender/plugins/publish/validate_instance_empty.py b/openpype/hosts/blender/plugins/publish/validate_instance_empty.py
new file mode 100644
index 0000000000..3ebc6515d3
--- /dev/null
+++ b/openpype/hosts/blender/plugins/publish/validate_instance_empty.py
@@ -0,0 +1,23 @@
+import bpy
+
+import pyblish.api
+
+
+class ValidateInstanceEmpty(pyblish.api.InstancePlugin):
+ """Validator to verify that the instance is not empty"""
+
+ order = pyblish.api.ValidatorOrder - 0.01
+ hosts = ["blender"]
+ families = ["model", "pointcache", "rig", "camera" "layout", "blendScene"]
+ label = "Validate Instance is not Empty"
+ optional = False
+
+ def process(self, instance):
+ asset_group = instance.data["instance_group"]
+
+ if isinstance(asset_group, bpy.types.Collection):
+ if not (asset_group.objects or asset_group.children):
+ raise RuntimeError(f"Instance {instance.name} is empty.")
+ elif isinstance(asset_group, bpy.types.Object):
+ if not asset_group.children:
+ raise RuntimeError(f"Instance {instance.name} is empty.")
diff --git a/openpype/hosts/houdini/api/creator_node_shelves.py b/openpype/hosts/houdini/api/creator_node_shelves.py
index 1f9fef7417..14662dc419 100644
--- a/openpype/hosts/houdini/api/creator_node_shelves.py
+++ b/openpype/hosts/houdini/api/creator_node_shelves.py
@@ -173,6 +173,7 @@ def install():
os.remove(filepath)
icon = get_openpype_icon_filepath()
+ tab_menu_label = os.environ.get("AVALON_LABEL") or "AYON"
# Create context only to get creator plugins, so we don't reset and only
# populate what we need to retrieve the list of creator plugins
@@ -197,14 +198,14 @@ def install():
if not network_categories:
continue
- key = "openpype_create.{}".format(identifier)
+ key = "ayon_create.{}".format(identifier)
log.debug(f"Registering {key}")
script = CREATE_SCRIPT.format(identifier=identifier)
data = {
"script": script,
"language": hou.scriptLanguage.Python,
"icon": icon,
- "help": "Create OpenPype publish instance for {}".format(
+ "help": "Create Ayon publish instance for {}".format(
creator.label
),
"help_url": None,
@@ -213,7 +214,7 @@ def install():
"cop_viewer_categories": [],
"network_op_type": None,
"viewer_op_type": None,
- "locations": ["OpenPype"]
+ "locations": [tab_menu_label]
}
label = "Create {}".format(creator.label)
tool = hou.shelves.tool(key)
diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py
index ac375c56d6..8b058e605d 100644
--- a/openpype/hosts/houdini/api/lib.py
+++ b/openpype/hosts/houdini/api/lib.py
@@ -569,9 +569,9 @@ def get_template_from_value(key, value):
return parm
-def get_frame_data(node, handle_start=0, handle_end=0, log=None):
- """Get the frame data: start frame, end frame, steps,
- start frame with start handle and end frame with end handle.
+def get_frame_data(node, log=None):
+ """Get the frame data: `frameStartHandle`, `frameEndHandle`
+ and `byFrameStep`.
This function uses Houdini node's `trange`, `t1, `t2` and `t3`
parameters as the source of truth for the full inclusive frame
@@ -579,20 +579,17 @@ def get_frame_data(node, handle_start=0, handle_end=0, log=None):
range including the handles.
The non-inclusive frame start and frame end without handles
- are computed by subtracting the handles from the inclusive
+ can be computed by subtracting the handles from the inclusive
frame range.
Args:
node (hou.Node): ROP node to retrieve frame range from,
the frame range is assumed to be the frame range
*including* the start and end handles.
- handle_start (int): Start handles.
- handle_end (int): End handles.
- log (logging.Logger): Logger to log to.
Returns:
- dict: frame data for start, end, steps,
- start with handle and end with handle
+ dict: frame data for `frameStartHandle`, `frameEndHandle`
+ and `byFrameStep`.
"""
@@ -623,11 +620,6 @@ def get_frame_data(node, handle_start=0, handle_end=0, log=None):
data["frameEndHandle"] = int(node.evalParm("f2"))
data["byFrameStep"] = node.evalParm("f3")
- data["handleStart"] = handle_start
- data["handleEnd"] = handle_end
- data["frameStart"] = data["frameStartHandle"] + data["handleStart"]
- data["frameEnd"] = data["frameEndHandle"] - data["handleEnd"]
-
return data
@@ -1018,7 +1010,7 @@ def self_publish():
def add_self_publish_button(node):
"""Adds a self publish button to the rop node."""
- label = os.environ.get("AVALON_LABEL") or "OpenPype"
+ label = os.environ.get("AVALON_LABEL") or "AYON"
button_parm = hou.ButtonParmTemplate(
"ayon_self_publish",
diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py
index f8db45c56b..11135e20b2 100644
--- a/openpype/hosts/houdini/api/pipeline.py
+++ b/openpype/hosts/houdini/api/pipeline.py
@@ -3,7 +3,6 @@
import os
import sys
import logging
-import contextlib
import hou # noqa
@@ -66,10 +65,6 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
register_event_callback("open", on_open)
register_event_callback("new", on_new)
- pyblish.api.register_callback(
- "instanceToggled", on_pyblish_instance_toggled
- )
-
self._has_been_setup = True
# add houdini vendor packages
hou_pythonpath = os.path.join(HOUDINI_HOST_DIR, "vendor")
@@ -406,54 +401,3 @@ def _set_context_settings():
lib.reset_framerange()
lib.update_houdini_vars_context()
-
-
-def on_pyblish_instance_toggled(instance, new_value, old_value):
- """Toggle saver tool passthrough states on instance toggles."""
- @contextlib.contextmanager
- def main_take(no_update=True):
- """Enter root take during context"""
- original_take = hou.takes.currentTake()
- original_update_mode = hou.updateModeSetting()
- root = hou.takes.rootTake()
- has_changed = False
- try:
- if original_take != root:
- has_changed = True
- if no_update:
- hou.setUpdateMode(hou.updateMode.Manual)
- hou.takes.setCurrentTake(root)
- yield
- finally:
- if has_changed:
- if no_update:
- hou.setUpdateMode(original_update_mode)
- hou.takes.setCurrentTake(original_take)
-
- if not instance.data.get("_allowToggleBypass", True):
- return
-
- nodes = instance[:]
- if not nodes:
- return
-
- # Assume instance node is first node
- instance_node = nodes[0]
-
- if not hasattr(instance_node, "isBypassed"):
- # Likely not a node that can actually be bypassed
- log.debug("Can't bypass node: %s", instance_node.path())
- return
-
- if instance_node.isBypassed() != (not old_value):
- print("%s old bypass state didn't match old instance state, "
- "updating anyway.." % instance_node.path())
-
- try:
- # Go into the main take, because when in another take changing
- # the bypass state of a note cannot be done due to it being locked
- # by default.
- with main_take(no_update=True):
- instance_node.bypass(not new_value)
- except hou.PermissionError as exc:
- log.warning("%s - %s", instance_node.path(), exc)
diff --git a/openpype/hosts/houdini/api/shelves.py b/openpype/hosts/houdini/api/shelves.py
index 5df45a1f72..5093a90988 100644
--- a/openpype/hosts/houdini/api/shelves.py
+++ b/openpype/hosts/houdini/api/shelves.py
@@ -24,29 +24,33 @@ def generate_shelves():
# load configuration of houdini shelves
project_name = get_current_project_name()
project_settings = get_project_settings(project_name)
- shelves_set_config = project_settings["houdini"]["shelves"]
+ shelves_configs = project_settings["houdini"]["shelves"]
- if not shelves_set_config:
+ if not shelves_configs:
log.debug("No custom shelves found in project settings.")
return
# Get Template data
template_data = get_current_context_template_data_with_asset_data()
- for shelf_set_config in shelves_set_config:
- shelf_set_filepath = shelf_set_config.get('shelf_set_source_path')
- shelf_set_os_filepath = shelf_set_filepath[current_os]
- if shelf_set_os_filepath:
- shelf_set_os_filepath = get_path_using_template_data(
- shelf_set_os_filepath, template_data
- )
- if not os.path.isfile(shelf_set_os_filepath):
- log.error("Shelf path doesn't exist - "
- "{}".format(shelf_set_os_filepath))
- continue
+ for config in shelves_configs:
+ selected_option = config["options"]
+ shelf_set_config = config[selected_option]
- hou.shelves.newShelfSet(file_path=shelf_set_os_filepath)
- continue
+ shelf_set_filepath = shelf_set_config.get('shelf_set_source_path')
+ if shelf_set_filepath:
+ shelf_set_os_filepath = shelf_set_filepath[current_os]
+ if shelf_set_os_filepath:
+ shelf_set_os_filepath = get_path_using_template_data(
+ shelf_set_os_filepath, template_data
+ )
+ if not os.path.isfile(shelf_set_os_filepath):
+ log.error("Shelf path doesn't exist - "
+ "{}".format(shelf_set_os_filepath))
+ continue
+
+ hou.shelves.loadFile(shelf_set_os_filepath)
+ continue
shelf_set_name = shelf_set_config.get('shelf_set_name')
if not shelf_set_name:
diff --git a/openpype/hosts/houdini/plugins/create/create_bgeo.py b/openpype/hosts/houdini/plugins/create/create_bgeo.py
index a3f31e7e94..0f629cf9c9 100644
--- a/openpype/hosts/houdini/plugins/create/create_bgeo.py
+++ b/openpype/hosts/houdini/plugins/create/create_bgeo.py
@@ -3,6 +3,7 @@
from openpype.hosts.houdini.api import plugin
from openpype.pipeline import CreatedInstance, CreatorError
from openpype.lib import EnumDef
+import hou
class CreateBGEO(plugin.HoudiniCreator):
@@ -13,7 +14,6 @@ class CreateBGEO(plugin.HoudiniCreator):
icon = "gears"
def create(self, subset_name, instance_data, pre_create_data):
- import hou
instance_data.pop("active", None)
@@ -90,3 +90,9 @@ class CreateBGEO(plugin.HoudiniCreator):
return attrs + [
EnumDef("bgeo_type", bgeo_enum, label="BGEO Options"),
]
+
+ def get_network_categories(self):
+ return [
+ hou.ropNodeTypeCategory(),
+ hou.sopNodeTypeCategory()
+ ]
diff --git a/openpype/hosts/houdini/plugins/create/create_composite.py b/openpype/hosts/houdini/plugins/create/create_composite.py
index 9d4f7969bb..52ea6fa054 100644
--- a/openpype/hosts/houdini/plugins/create/create_composite.py
+++ b/openpype/hosts/houdini/plugins/create/create_composite.py
@@ -45,6 +45,11 @@ class CreateCompositeSequence(plugin.HoudiniCreator):
instance_node.setParms(parms)
+ # Manually set f1 & f2 to $FSTART and $FEND respectively
+ # to match other Houdini nodes default.
+ instance_node.parm("f1").setExpression("$FSTART")
+ instance_node.parm("f2").setExpression("$FEND")
+
# Lock any parameters in this list
to_lock = ["prim_to_detail_pattern"]
self.lock_parameters(instance_node, to_lock)
diff --git a/openpype/hosts/houdini/plugins/create/create_hda.py b/openpype/hosts/houdini/plugins/create/create_hda.py
index c4093bfbc6..ac075d2072 100644
--- a/openpype/hosts/houdini/plugins/create/create_hda.py
+++ b/openpype/hosts/houdini/plugins/create/create_hda.py
@@ -5,6 +5,7 @@ from openpype.client import (
get_subsets,
)
from openpype.hosts.houdini.api import plugin
+import hou
class CreateHDA(plugin.HoudiniCreator):
@@ -35,7 +36,6 @@ class CreateHDA(plugin.HoudiniCreator):
def create_instance_node(
self, node_name, parent, node_type="geometry"):
- import hou
parent_node = hou.node("/obj")
if self.selected_nodes:
@@ -81,3 +81,8 @@ class CreateHDA(plugin.HoudiniCreator):
pre_create_data) # type: plugin.CreatedInstance
return instance
+
+ def get_network_categories(self):
+ return [
+ hou.objNodeTypeCategory()
+ ]
diff --git a/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py b/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py
index b814dd9d57..3a4ab7008b 100644
--- a/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py
+++ b/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""Creator plugin for creating Redshift proxies."""
from openpype.hosts.houdini.api import plugin
-from openpype.pipeline import CreatedInstance
+import hou
class CreateRedshiftProxy(plugin.HoudiniCreator):
@@ -12,7 +12,7 @@ class CreateRedshiftProxy(plugin.HoudiniCreator):
icon = "magic"
def create(self, subset_name, instance_data, pre_create_data):
- import hou # noqa
+
# Remove the active, we are checking the bypass flag of the nodes
instance_data.pop("active", None)
@@ -28,7 +28,7 @@ class CreateRedshiftProxy(plugin.HoudiniCreator):
instance = super(CreateRedshiftProxy, self).create(
subset_name,
instance_data,
- pre_create_data) # type: CreatedInstance
+ pre_create_data)
instance_node = hou.node(instance.get("instance_node"))
@@ -44,3 +44,9 @@ class CreateRedshiftProxy(plugin.HoudiniCreator):
# Lock some Avalon attributes
to_lock = ["family", "id", "prim_to_detail_pattern"]
self.lock_parameters(instance_node, to_lock)
+
+ def get_network_categories(self):
+ return [
+ hou.ropNodeTypeCategory(),
+ hou.sopNodeTypeCategory()
+ ]
diff --git a/openpype/hosts/houdini/plugins/create/create_staticmesh.py b/openpype/hosts/houdini/plugins/create/create_staticmesh.py
index ea0b36f03f..d0985198bd 100644
--- a/openpype/hosts/houdini/plugins/create/create_staticmesh.py
+++ b/openpype/hosts/houdini/plugins/create/create_staticmesh.py
@@ -54,6 +54,7 @@ class CreateStaticMesh(plugin.HoudiniCreator):
def get_network_categories(self):
return [
hou.ropNodeTypeCategory(),
+ hou.objNodeTypeCategory(),
hou.sopNodeTypeCategory()
]
diff --git a/openpype/hosts/houdini/plugins/create/create_vbd_cache.py b/openpype/hosts/houdini/plugins/create/create_vbd_cache.py
index 9c96e48e3a..69418f9575 100644
--- a/openpype/hosts/houdini/plugins/create/create_vbd_cache.py
+++ b/openpype/hosts/houdini/plugins/create/create_vbd_cache.py
@@ -40,6 +40,7 @@ class CreateVDBCache(plugin.HoudiniCreator):
def get_network_categories(self):
return [
hou.ropNodeTypeCategory(),
+ hou.objNodeTypeCategory(),
hou.sopNodeTypeCategory()
]
diff --git a/openpype/hosts/houdini/plugins/load/load_image.py b/openpype/hosts/houdini/plugins/load/load_image.py
index 663a93e48b..cff2b74e52 100644
--- a/openpype/hosts/houdini/plugins/load/load_image.py
+++ b/openpype/hosts/houdini/plugins/load/load_image.py
@@ -119,7 +119,8 @@ class ImageLoader(load.LoaderPlugin):
if not parent.children():
parent.destroy()
- def _get_file_sequence(self, root):
+ def _get_file_sequence(self, file_path):
+ root = os.path.dirname(file_path)
files = sorted(os.listdir(root))
first_fname = files[0]
diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py
index b489f83b29..d95f763826 100644
--- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py
+++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py
@@ -21,8 +21,8 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin):
label = "Arnold ROP Render Products"
# This specific order value is used so that
- # this plugin runs after CollectRopFrameRange
- order = pyblish.api.CollectorOrder + 0.4999
+ # this plugin runs after CollectFrames
+ order = pyblish.api.CollectorOrder + 0.11
hosts = ["houdini"]
families = ["arnold_rop"]
diff --git a/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py b/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py
new file mode 100644
index 0000000000..67a281639d
--- /dev/null
+++ b/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py
@@ -0,0 +1,124 @@
+# -*- coding: utf-8 -*-
+"""Collector plugin for frames data on ROP instances."""
+import hou # noqa
+import pyblish.api
+from openpype.lib import BoolDef
+from openpype.pipeline import OpenPypePyblishPluginMixin
+
+
+class CollectAssetHandles(pyblish.api.InstancePlugin,
+ OpenPypePyblishPluginMixin):
+ """Apply asset handles.
+
+ If instance does not have:
+ - frameStart
+ - frameEnd
+ - handleStart
+ - handleEnd
+ But it does have:
+ - frameStartHandle
+ - frameEndHandle
+
+ Then we will retrieve the asset's handles to compute
+ the exclusive frame range and actual handle ranges.
+ """
+
+ hosts = ["houdini"]
+
+ # This specific order value is used so that
+ # this plugin runs after CollectAnatomyInstanceData
+ order = pyblish.api.CollectorOrder + 0.499
+
+ label = "Collect Asset Handles"
+ use_asset_handles = True
+
+ def process(self, instance):
+ # Only process instances without already existing handles data
+ # but that do have frameStartHandle and frameEndHandle defined
+ # like the data collected from CollectRopFrameRange
+ if "frameStartHandle" not in instance.data:
+ return
+ if "frameEndHandle" not in instance.data:
+ return
+
+ has_existing_data = {
+ "handleStart",
+ "handleEnd",
+ "frameStart",
+ "frameEnd"
+ }.issubset(instance.data)
+ if has_existing_data:
+ return
+
+ attr_values = self.get_attr_values_from_data(instance.data)
+ if attr_values.get("use_handles", self.use_asset_handles):
+ asset_data = instance.data["assetEntity"]["data"]
+ handle_start = asset_data.get("handleStart", 0)
+ handle_end = asset_data.get("handleEnd", 0)
+ else:
+ handle_start = 0
+ handle_end = 0
+
+ frame_start = instance.data["frameStartHandle"] + handle_start
+ frame_end = instance.data["frameEndHandle"] - handle_end
+
+ instance.data.update({
+ "handleStart": handle_start,
+ "handleEnd": handle_end,
+ "frameStart": frame_start,
+ "frameEnd": frame_end
+ })
+
+ # Log debug message about the collected frame range
+ if attr_values.get("use_handles", self.use_asset_handles):
+ self.log.debug(
+ "Full Frame range with Handles "
+ "[{frame_start_handle} - {frame_end_handle}]"
+ .format(
+ frame_start_handle=instance.data["frameStartHandle"],
+ frame_end_handle=instance.data["frameEndHandle"]
+ )
+ )
+ else:
+ self.log.debug(
+ "Use handles is deactivated for this instance, "
+ "start and end handles are set to 0."
+ )
+
+ # Log collected frame range to the user
+ message = "Frame range [{frame_start} - {frame_end}]".format(
+ frame_start=frame_start,
+ frame_end=frame_end
+ )
+ if handle_start or handle_end:
+ message += " with handles [{handle_start}]-[{handle_end}]".format(
+ handle_start=handle_start,
+ handle_end=handle_end
+ )
+ self.log.info(message)
+
+ if instance.data.get("byFrameStep", 1.0) != 1.0:
+ self.log.info(
+ "Frame steps {}".format(instance.data["byFrameStep"]))
+
+ # Add frame range to label if the instance has a frame range.
+ label = instance.data.get("label", instance.data["name"])
+ instance.data["label"] = (
+ "{label} [{frame_start_handle} - {frame_end_handle}]"
+ .format(
+ label=label,
+ frame_start_handle=instance.data["frameStartHandle"],
+ frame_end_handle=instance.data["frameEndHandle"]
+ )
+ )
+
+ @classmethod
+ def get_attribute_defs(cls):
+ return [
+ BoolDef("use_handles",
+ tooltip="Disable this if you want the publisher to"
+ " ignore start and end handles specified in the"
+ " asset data for this publish instance",
+ default=cls.use_asset_handles,
+ label="Use asset handles")
+ ]
diff --git a/openpype/hosts/houdini/plugins/publish/collect_frames.py b/openpype/hosts/houdini/plugins/publish/collect_frames.py
index 01df809d4c..cdef642174 100644
--- a/openpype/hosts/houdini/plugins/publish/collect_frames.py
+++ b/openpype/hosts/houdini/plugins/publish/collect_frames.py
@@ -11,7 +11,9 @@ from openpype.hosts.houdini.api import lib
class CollectFrames(pyblish.api.InstancePlugin):
"""Collect all frames which would be saved from the ROP nodes"""
- order = pyblish.api.CollectorOrder + 0.01
+ # This specific order value is used so that
+ # this plugin runs after CollectRopFrameRange
+ order = pyblish.api.CollectorOrder + 0.1
label = "Collect Frames"
families = ["vdbcache", "imagesequence", "ass",
"redshiftproxy", "review", "bgeo"]
@@ -20,8 +22,8 @@ class CollectFrames(pyblish.api.InstancePlugin):
ropnode = hou.node(instance.data["instance_node"])
- start_frame = instance.data.get("frameStart", None)
- end_frame = instance.data.get("frameEnd", None)
+ start_frame = instance.data.get("frameStartHandle", None)
+ end_frame = instance.data.get("frameEndHandle", None)
output_parm = lib.get_output_parameter(ropnode)
if start_frame is not None:
diff --git a/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py b/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py
index 0600730d00..d154cdc7c0 100644
--- a/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py
+++ b/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py
@@ -122,10 +122,6 @@ class CollectInstancesUsdLayered(pyblish.api.ContextPlugin):
instance.data.update(save_data)
instance.data["usdLayer"] = layer
- # Don't allow the Pyblish `instanceToggled` we have installed
- # to set this node to bypass.
- instance.data["_allowToggleBypass"] = False
-
instances.append(instance)
# Store the collected ROP node dependencies
diff --git a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py
index fe0b8711fc..dac350a6ef 100644
--- a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py
+++ b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py
@@ -25,8 +25,8 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin):
label = "Karma ROP Render Products"
# This specific order value is used so that
- # this plugin runs after CollectRopFrameRange
- order = pyblish.api.CollectorOrder + 0.4999
+ # this plugin runs after CollectFrames
+ order = pyblish.api.CollectorOrder + 0.11
hosts = ["houdini"]
families = ["karma_rop"]
diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py
index cc412f30a1..a3e7927807 100644
--- a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py
+++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py
@@ -25,8 +25,8 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin):
label = "Mantra ROP Render Products"
# This specific order value is used so that
- # this plugin runs after CollectRopFrameRange
- order = pyblish.api.CollectorOrder + 0.4999
+ # this plugin runs after CollectFrames
+ order = pyblish.api.CollectorOrder + 0.11
hosts = ["houdini"]
families = ["mantra_rop"]
diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py
index deb9eac971..0acddab011 100644
--- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py
+++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py
@@ -25,8 +25,8 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
label = "Redshift ROP Render Products"
# This specific order value is used so that
- # this plugin runs after CollectRopFrameRange
- order = pyblish.api.CollectorOrder + 0.4999
+ # this plugin runs after CollectFrames
+ order = pyblish.api.CollectorOrder + 0.11
hosts = ["houdini"]
families = ["redshift_rop"]
diff --git a/openpype/hosts/houdini/plugins/publish/collect_review_data.py b/openpype/hosts/houdini/plugins/publish/collect_review_data.py
index 3efb75e66c..9671945b9a 100644
--- a/openpype/hosts/houdini/plugins/publish/collect_review_data.py
+++ b/openpype/hosts/houdini/plugins/publish/collect_review_data.py
@@ -6,6 +6,8 @@ class CollectHoudiniReviewData(pyblish.api.InstancePlugin):
"""Collect Review Data."""
label = "Collect Review Data"
+ # This specific order value is used so that
+ # this plugin runs after CollectRopFrameRange
order = pyblish.api.CollectorOrder + 0.1
hosts = ["houdini"]
families = ["review"]
@@ -41,8 +43,8 @@ class CollectHoudiniReviewData(pyblish.api.InstancePlugin):
return
if focal_length_parm.isTimeDependent():
- start = instance.data["frameStart"]
- end = instance.data["frameEnd"] + 1
+ start = instance.data["frameStartHandle"]
+ end = instance.data["frameEndHandle"] + 1
focal_length = [
focal_length_parm.evalAsFloatAtFrame(t)
for t in range(int(start), int(end))
diff --git a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py
index 186244fedd..1e6bc3b16e 100644
--- a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py
+++ b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py
@@ -2,22 +2,15 @@
"""Collector plugin for frames data on ROP instances."""
import hou # noqa
import pyblish.api
-from openpype.lib import BoolDef
from openpype.hosts.houdini.api import lib
-from openpype.pipeline import OpenPypePyblishPluginMixin
-class CollectRopFrameRange(pyblish.api.InstancePlugin,
- OpenPypePyblishPluginMixin):
-
+class CollectRopFrameRange(pyblish.api.InstancePlugin):
"""Collect all frames which would be saved from the ROP nodes"""
hosts = ["houdini"]
- # This specific order value is used so that
- # this plugin runs after CollectAnatomyInstanceData
- order = pyblish.api.CollectorOrder + 0.499
+ order = pyblish.api.CollectorOrder
label = "Collect RopNode Frame Range"
- use_asset_handles = True
def process(self, instance):
@@ -30,78 +23,16 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin,
return
ropnode = hou.node(node_path)
-
- attr_values = self.get_attr_values_from_data(instance.data)
-
- if attr_values.get("use_handles", self.use_asset_handles):
- asset_data = instance.data["assetEntity"]["data"]
- handle_start = asset_data.get("handleStart", 0)
- handle_end = asset_data.get("handleEnd", 0)
- else:
- handle_start = 0
- handle_end = 0
-
frame_data = lib.get_frame_data(
- ropnode, handle_start, handle_end, self.log
+ ropnode, self.log
)
if not frame_data:
return
# Log debug message about the collected frame range
- frame_start = frame_data["frameStart"]
- frame_end = frame_data["frameEnd"]
-
- if attr_values.get("use_handles", self.use_asset_handles):
- self.log.debug(
- "Full Frame range with Handles "
- "[{frame_start_handle} - {frame_end_handle}]"
- .format(
- frame_start_handle=frame_data["frameStartHandle"],
- frame_end_handle=frame_data["frameEndHandle"]
- )
- )
- else:
- self.log.debug(
- "Use handles is deactivated for this instance, "
- "start and end handles are set to 0."
- )
-
- # Log collected frame range to the user
- message = "Frame range [{frame_start} - {frame_end}]".format(
- frame_start=frame_start,
- frame_end=frame_end
+ self.log.debug(
+ "Collected frame_data: {}".format(frame_data)
)
- if handle_start or handle_end:
- message += " with handles [{handle_start}]-[{handle_end}]".format(
- handle_start=handle_start,
- handle_end=handle_end
- )
- self.log.info(message)
-
- if frame_data.get("byFrameStep", 1.0) != 1.0:
- self.log.info("Frame steps {}".format(frame_data["byFrameStep"]))
instance.data.update(frame_data)
-
- # Add frame range to label if the instance has a frame range.
- label = instance.data.get("label", instance.data["name"])
- instance.data["label"] = (
- "{label} [{frame_start} - {frame_end}]"
- .format(
- label=label,
- frame_start=frame_start,
- frame_end=frame_end
- )
- )
-
- @classmethod
- def get_attribute_defs(cls):
- return [
- BoolDef("use_handles",
- tooltip="Disable this if you want the publisher to"
- " ignore start and end handles specified in the"
- " asset data for this publish instance",
- default=cls.use_asset_handles,
- label="Use asset handles")
- ]
diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py
index 53072aebc6..64de2079cd 100644
--- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py
+++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py
@@ -25,8 +25,8 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin):
label = "VRay ROP Render Products"
# This specific order value is used so that
- # this plugin runs after CollectRopFrameRange
- order = pyblish.api.CollectorOrder + 0.4999
+ # this plugin runs after CollectFrames
+ order = pyblish.api.CollectorOrder + 0.11
hosts = ["houdini"]
families = ["vray_rop"]
diff --git a/openpype/hosts/houdini/plugins/publish/extract_ass.py b/openpype/hosts/houdini/plugins/publish/extract_ass.py
index 0d246625ba..be60217055 100644
--- a/openpype/hosts/houdini/plugins/publish/extract_ass.py
+++ b/openpype/hosts/houdini/plugins/publish/extract_ass.py
@@ -56,7 +56,7 @@ class ExtractAss(publish.Extractor):
'ext': ext,
"files": files,
"stagingDir": staging_dir,
- "frameStart": instance.data["frameStart"],
- "frameEnd": instance.data["frameEnd"],
+ "frameStart": instance.data["frameStartHandle"],
+ "frameEnd": instance.data["frameEndHandle"],
}
instance.data["representations"].append(representation)
diff --git a/openpype/hosts/houdini/plugins/publish/extract_bgeo.py b/openpype/hosts/houdini/plugins/publish/extract_bgeo.py
index c9625ec880..d13141b426 100644
--- a/openpype/hosts/houdini/plugins/publish/extract_bgeo.py
+++ b/openpype/hosts/houdini/plugins/publish/extract_bgeo.py
@@ -47,7 +47,7 @@ class ExtractBGEO(publish.Extractor):
"ext": ext.lstrip("."),
"files": output,
"stagingDir": staging_dir,
- "frameStart": instance.data["frameStart"],
- "frameEnd": instance.data["frameEnd"]
+ "frameStart": instance.data["frameStartHandle"],
+ "frameEnd": instance.data["frameEndHandle"]
}
instance.data["representations"].append(representation)
diff --git a/openpype/hosts/houdini/plugins/publish/extract_composite.py b/openpype/hosts/houdini/plugins/publish/extract_composite.py
index 7a1ab36b93..11cf83a46d 100644
--- a/openpype/hosts/houdini/plugins/publish/extract_composite.py
+++ b/openpype/hosts/houdini/plugins/publish/extract_composite.py
@@ -41,8 +41,8 @@ class ExtractComposite(publish.Extractor):
"ext": ext,
"files": output,
"stagingDir": staging_dir,
- "frameStart": instance.data["frameStart"],
- "frameEnd": instance.data["frameEnd"],
+ "frameStart": instance.data["frameStartHandle"],
+ "frameEnd": instance.data["frameEndHandle"],
}
from pprint import pformat
diff --git a/openpype/hosts/houdini/plugins/publish/extract_fbx.py b/openpype/hosts/houdini/plugins/publish/extract_fbx.py
index 7993b3352f..7dc193c6a9 100644
--- a/openpype/hosts/houdini/plugins/publish/extract_fbx.py
+++ b/openpype/hosts/houdini/plugins/publish/extract_fbx.py
@@ -40,9 +40,9 @@ class ExtractFBX(publish.Extractor):
}
# A single frame may also be rendered without start/end frame.
- if "frameStart" in instance.data and "frameEnd" in instance.data:
- representation["frameStart"] = instance.data["frameStart"]
- representation["frameEnd"] = instance.data["frameEnd"]
+ if "frameStartHandle" in instance.data and "frameEndHandle" in instance.data: # noqa
+ representation["frameStart"] = instance.data["frameStartHandle"]
+ representation["frameEnd"] = instance.data["frameEndHandle"]
# set value type for 'representations' key to list
if "representations" not in instance.data:
diff --git a/openpype/hosts/houdini/plugins/publish/extract_opengl.py b/openpype/hosts/houdini/plugins/publish/extract_opengl.py
index 6c36dec5f5..38808089ac 100644
--- a/openpype/hosts/houdini/plugins/publish/extract_opengl.py
+++ b/openpype/hosts/houdini/plugins/publish/extract_opengl.py
@@ -39,8 +39,8 @@ class ExtractOpenGL(publish.Extractor):
"ext": instance.data["imageFormat"],
"files": output,
"stagingDir": staging_dir,
- "frameStart": instance.data["frameStart"],
- "frameEnd": instance.data["frameEnd"],
+ "frameStart": instance.data["frameStartHandle"],
+ "frameEnd": instance.data["frameEndHandle"],
"tags": tags,
"preview": True,
"camera_name": instance.data.get("review_camera")
diff --git a/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py
index 1d99ac665c..ef5991924f 100644
--- a/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py
+++ b/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py
@@ -44,8 +44,8 @@ class ExtractRedshiftProxy(publish.Extractor):
}
# A single frame may also be rendered without start/end frame.
- if "frameStart" in instance.data and "frameEnd" in instance.data:
- representation["frameStart"] = instance.data["frameStart"]
- representation["frameEnd"] = instance.data["frameEnd"]
+ if "frameStartHandle" in instance.data and "frameEndHandle" in instance.data: # noqa
+ representation["frameStart"] = instance.data["frameStartHandle"]
+ representation["frameEnd"] = instance.data["frameEndHandle"]
instance.data["representations"].append(representation)
diff --git a/openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py b/openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py
index 4bca758f08..89af8e1756 100644
--- a/openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py
+++ b/openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py
@@ -40,7 +40,7 @@ class ExtractVDBCache(publish.Extractor):
"ext": "vdb",
"files": output,
"stagingDir": staging_dir,
- "frameStart": instance.data["frameStart"],
- "frameEnd": instance.data["frameEnd"],
+ "frameStart": instance.data["frameStartHandle"],
+ "frameEnd": instance.data["frameEndHandle"],
}
instance.data["representations"].append(representation)
diff --git a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py
index 6a66f3de9f..1b12fa7096 100644
--- a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py
+++ b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py
@@ -57,7 +57,17 @@ class ValidateFrameRange(pyblish.api.InstancePlugin):
return
rop_node = hou.node(instance.data["instance_node"])
- if instance.data["frameStart"] > instance.data["frameEnd"]:
+ frame_start = instance.data.get("frameStart")
+ frame_end = instance.data.get("frameEnd")
+
+ if frame_start is None or frame_end is None:
+ cls.log.debug(
+ "Skipping frame range validation for "
+ "instance without frame data: {}".format(rop_node.path())
+ )
+ return
+
+ if frame_start > frame_end:
cls.log.info(
"The ROP node render range is set to "
"{0[frameStartHandle]} - {0[frameEndHandle]} "
@@ -89,7 +99,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin):
.format(instance))
return
- created_instance.publish_attributes["CollectRopFrameRange"]["use_handles"] = False # noqa
+ created_instance.publish_attributes["CollectAssetHandles"]["use_handles"] = False # noqa
create_context.save_changes()
cls.log.debug("use asset handles is turned off for '{}'"
diff --git a/openpype/hosts/houdini/startup/MainMenuCommon.xml b/openpype/hosts/houdini/startup/MainMenuCommon.xml
index b2e32a70f9..0903aef7bc 100644
--- a/openpype/hosts/houdini/startup/MainMenuCommon.xml
+++ b/openpype/hosts/houdini/startup/MainMenuCommon.xml
@@ -4,7 +4,7 @@