diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py
index a3f691e1fc3..3db18ca69a8 100644
--- a/openpype/hosts/houdini/api/lib.py
+++ b/openpype/hosts/houdini/api/lib.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import sys
import os
+import errno
import re
import uuid
import logging
@@ -9,10 +10,15 @@
import six
+from openpype.lib import StringTemplate
from openpype.client import get_asset_by_name
+from openpype.settings import get_current_project_settings
from openpype.pipeline import get_current_project_name, get_current_asset_name
-from openpype.pipeline.context_tools import get_current_project_asset
-
+from openpype.pipeline.context_tools import (
+ get_current_context_template_data,
+ get_current_project_asset
+)
+from openpype.widgets import popup
import hou
@@ -160,8 +166,6 @@ def validate_fps():
if current_fps != fps:
- from openpype.widgets import popup
-
# Find main window
parent = hou.ui.mainQtWindow()
if parent is None:
@@ -747,3 +751,99 @@ def get_camera_from_container(container):
assert len(cameras) == 1, "Camera instance must have only one camera"
return cameras[0]
+
+
+def get_context_var_changes():
+ """get context var changes."""
+
+ houdini_vars_to_update = {}
+
+ project_settings = get_current_project_settings()
+ houdini_vars_settings = \
+ project_settings["houdini"]["general"]["update_houdini_var_context"]
+
+ if not houdini_vars_settings["enabled"]:
+ return houdini_vars_to_update
+
+ houdini_vars = houdini_vars_settings["houdini_vars"]
+
+ # No vars specified - nothing to do
+ if not houdini_vars:
+ return houdini_vars_to_update
+
+ # Get Template data
+ template_data = get_current_context_template_data()
+
+ # Set Houdini Vars
+ for item in houdini_vars:
+ # For consistency reasons we always force all vars to be uppercase
+ # Also remove any leading, and trailing whitespaces.
+ var = item["var"].strip().upper()
+
+ # get and resolve template in value
+ item_value = StringTemplate.format_template(
+ item["value"],
+ template_data
+ )
+
+ if var == "JOB" and item_value == "":
+ # sync $JOB to $HIP if $JOB is empty
+ item_value = os.environ["HIP"]
+
+ if item["is_directory"]:
+ item_value = item_value.replace("\\", "/")
+
+ current_value = hou.hscript("echo -n `${}`".format(var))[0]
+
+ if current_value != item_value:
+ houdini_vars_to_update[var] = (
+ current_value, item_value, item["is_directory"]
+ )
+
+ return houdini_vars_to_update
+
+
+def update_houdini_vars_context():
+ """Update asset context variables"""
+
+ for var, (_old, new, is_directory) in get_context_var_changes().items():
+ if is_directory:
+ try:
+ os.makedirs(new)
+ except OSError as e:
+ if e.errno != errno.EEXIST:
+ print(
+ "Failed to create ${} dir. Maybe due to "
+ "insufficient permissions.".format(var)
+ )
+
+ hou.hscript("set {}={}".format(var, new))
+ os.environ[var] = new
+ print("Updated ${} to {}".format(var, new))
+
+
+def update_houdini_vars_context_dialog():
+ """Show pop-up to update asset context variables"""
+ update_vars = get_context_var_changes()
+ if not update_vars:
+ # Nothing to change
+ print("Nothing to change, Houdini vars are already up to date.")
+ return
+
+ message = "\n".join(
+ "${}: {} -> {}".format(var, old or "None", new or "None")
+ for var, (old, new, _is_directory) in update_vars.items()
+ )
+
+ # TODO: Use better UI!
+ parent = hou.ui.mainQtWindow()
+ dialog = popup.Popup(parent=parent)
+ dialog.setModal(True)
+ dialog.setWindowTitle("Houdini scene has outdated asset variables")
+ dialog.setMessage(message)
+ dialog.setButtonText("Fix")
+
+ # on_show is the Fix button clicked callback
+ dialog.on_clicked.connect(update_houdini_vars_context)
+
+ dialog.show()
diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py
index 6aa65deb896..f8db45c56bd 100644
--- a/openpype/hosts/houdini/api/pipeline.py
+++ b/openpype/hosts/houdini/api/pipeline.py
@@ -300,6 +300,9 @@ def on_save():
log.info("Running callback on save..")
+ # update houdini vars
+ lib.update_houdini_vars_context_dialog()
+
nodes = lib.get_id_required_nodes()
for node, new_id in lib.generate_ids(nodes):
lib.set_id(node, new_id, overwrite=False)
@@ -335,6 +338,9 @@ def on_open():
log.info("Running callback on open..")
+ # update houdini vars
+ lib.update_houdini_vars_context_dialog()
+
# Validate FPS after update_task_from_path to
# ensure it is using correct FPS for the asset
lib.validate_fps()
@@ -399,6 +405,7 @@ def _set_context_settings():
"""
lib.reset_framerange()
+ lib.update_houdini_vars_context()
def on_pyblish_instance_toggled(instance, new_value, old_value):
diff --git a/openpype/hosts/houdini/startup/MainMenuCommon.xml b/openpype/hosts/houdini/startup/MainMenuCommon.xml
index 5818a117eb2..b2e32a70f93 100644
--- a/openpype/hosts/houdini/startup/MainMenuCommon.xml
+++ b/openpype/hosts/houdini/startup/MainMenuCommon.xml
@@ -86,6 +86,14 @@ openpype.hosts.houdini.api.lib.reset_framerange()
]]>
+
+
+
+
+
diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py
index f567118062d..13630ae7caa 100644
--- a/openpype/pipeline/context_tools.py
+++ b/openpype/pipeline/context_tools.py
@@ -25,7 +25,10 @@
from .publish.lib import filter_pyblish_plugins
from .anatomy import Anatomy
-from .template_data import get_template_data_with_names
+from .template_data import (
+ get_template_data_with_names,
+ get_template_data
+)
from .workfile import (
get_workfile_template_key,
get_custom_workfile_template_by_string_context,
@@ -658,3 +661,70 @@ def get_process_id():
if _process_id is None:
_process_id = str(uuid.uuid4())
return _process_id
+
+
+def get_current_context_template_data():
+ """Template data for template fill from current context
+
+ Returns:
+ Dict[str, Any] of the following tokens and their values
+ Supported Tokens:
+ - Regular Tokens
+ - app
+ - user
+ - asset
+ - parent
+ - hierarchy
+ - folder[name]
+ - root[work, ...]
+ - studio[code, name]
+ - project[code, name]
+ - task[type, name, short]
+
+ - Context Specific Tokens
+ - assetData[frameStart]
+ - assetData[frameEnd]
+ - assetData[handleStart]
+ - assetData[handleEnd]
+ - assetData[frameStartHandle]
+ - assetData[frameEndHandle]
+ - assetData[resolutionHeight]
+ - assetData[resolutionWidth]
+
+ """
+
+ # pre-prepare get_template_data args
+ current_context = get_current_context()
+ project_name = current_context["project_name"]
+ asset_name = current_context["asset_name"]
+ anatomy = Anatomy(project_name)
+
+ # prepare get_template_data args
+ project_doc = get_project(project_name)
+ asset_doc = get_asset_by_name(project_name, asset_name)
+ task_name = current_context["task_name"]
+ host_name = get_current_host_name()
+
+ # get regular template data
+ template_data = get_template_data(
+ project_doc, asset_doc, task_name, host_name
+ )
+
+ template_data["root"] = anatomy.roots
+
+ # get context specific vars
+ asset_data = asset_doc["data"].copy()
+
+ # compute `frameStartHandle` and `frameEndHandle`
+ if "frameStart" in asset_data and "handleStart" in asset_data:
+ asset_data["frameStartHandle"] = \
+ asset_data["frameStart"] - asset_data["handleStart"]
+
+ if "frameEnd" in asset_data and "handleEnd" in asset_data:
+ asset_data["frameEndHandle"] = \
+ asset_data["frameEnd"] + asset_data["handleEnd"]
+
+ # add assetData
+ template_data["assetData"] = asset_data
+
+ return template_data
diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json
index 5392fc34ddb..4f57ee52c63 100644
--- a/openpype/settings/defaults/project_settings/houdini.json
+++ b/openpype/settings/defaults/project_settings/houdini.json
@@ -1,4 +1,16 @@
{
+ "general": {
+ "update_houdini_var_context": {
+ "enabled": true,
+ "houdini_vars":[
+ {
+ "var": "JOB",
+ "value": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}",
+ "is_directory": true
+ }
+ ]
+ }
+ },
"imageio": {
"activate_host_color_management": true,
"ocio_config": {
diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json
index 7f782e36473..d4d0565ec97 100644
--- a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json
+++ b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json
@@ -5,6 +5,10 @@
"label": "Houdini",
"is_file": true,
"children": [
+ {
+ "type": "schema",
+ "name": "schema_houdini_general"
+ },
{
"key": "imageio",
"type": "dict",
diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json
new file mode 100644
index 00000000000..de1a0396ec5
--- /dev/null
+++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json
@@ -0,0 +1,53 @@
+{
+ "type": "dict",
+ "key": "general",
+ "label": "General",
+ "collapsible": true,
+ "is_group": true,
+ "children": [
+ {
+ "type": "dict",
+ "collapsible": true,
+ "checkbox_key": "enabled",
+ "key": "update_houdini_var_context",
+ "label": "Update Houdini Vars on context change",
+ "children": [
+ {
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ },
+ {
+ "type": "label",
+ "label": "Sync vars with context changes.
If a value is treated as a directory on update it will be ensured the folder exists"
+ },
+ {
+ "type": "list",
+ "key": "houdini_vars",
+ "label": "Houdini Vars",
+ "collapsible": false,
+ "object_type": {
+ "type": "dict",
+ "children": [
+ {
+ "type": "text",
+ "key": "var",
+ "label": "Var"
+ },
+ {
+ "type": "text",
+ "key": "value",
+ "label": "Value"
+ },
+ {
+ "type": "boolean",
+ "key": "is_directory",
+ "label": "Treat as directory"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/server_addon/houdini/server/settings/general.py b/server_addon/houdini/server/settings/general.py
new file mode 100644
index 00000000000..21cc4c452cf
--- /dev/null
+++ b/server_addon/houdini/server/settings/general.py
@@ -0,0 +1,45 @@
+from pydantic import Field
+from ayon_server.settings import BaseSettingsModel
+
+
+class HoudiniVarModel(BaseSettingsModel):
+ _layout = "expanded"
+ var: str = Field("", title="Var")
+ value: str = Field("", title="Value")
+ is_directory: bool = Field(False, title="Treat as directory")
+
+
+class UpdateHoudiniVarcontextModel(BaseSettingsModel):
+ """Sync vars with context changes.
+
+ If a value is treated as a directory on update
+ it will be ensured the folder exists.
+ """
+
+ enabled: bool = Field(title="Enabled")
+ # TODO this was dynamic dictionary '{var: path}'
+ houdini_vars: list[HoudiniVarModel] = Field(
+ default_factory=list,
+ title="Houdini Vars"
+ )
+
+
+class GeneralSettingsModel(BaseSettingsModel):
+ update_houdini_var_context: UpdateHoudiniVarcontextModel = Field(
+ default_factory=UpdateHoudiniVarcontextModel,
+ title="Update Houdini Vars on context change"
+ )
+
+
+DEFAULT_GENERAL_SETTINGS = {
+ "update_houdini_var_context": {
+ "enabled": True,
+ "houdini_vars": [
+ {
+ "var": "JOB",
+ "value": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}", # noqa
+ "is_directory": True
+ }
+ ]
+ }
+}
diff --git a/server_addon/houdini/server/settings/main.py b/server_addon/houdini/server/settings/main.py
index fdb6838f5c1..0c2e160c876 100644
--- a/server_addon/houdini/server/settings/main.py
+++ b/server_addon/houdini/server/settings/main.py
@@ -4,7 +4,10 @@
MultiplatformPathModel,
MultiplatformPathListModel,
)
-
+from .general import (
+ GeneralSettingsModel,
+ DEFAULT_GENERAL_SETTINGS
+)
from .imageio import HoudiniImageIOModel
from .publish_plugins import (
PublishPluginsModel,
@@ -52,6 +55,10 @@ class ShelvesModel(BaseSettingsModel):
class HoudiniSettings(BaseSettingsModel):
+ general: GeneralSettingsModel = Field(
+ default_factory=GeneralSettingsModel,
+ title="General"
+ )
imageio: HoudiniImageIOModel = Field(
default_factory=HoudiniImageIOModel,
title="Color Management (ImageIO)"
@@ -73,6 +80,7 @@ class HoudiniSettings(BaseSettingsModel):
DEFAULT_VALUES = {
+ "general": DEFAULT_GENERAL_SETTINGS,
"shelves": [],
"create": DEFAULT_HOUDINI_CREATE_SETTINGS,
"publish": DEFAULT_HOUDINI_PUBLISH_SETTINGS
diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py
index ae7362549b3..bbab0242f6a 100644
--- a/server_addon/houdini/server/version.py
+++ b/server_addon/houdini/server/version.py
@@ -1 +1 @@
-__version__ = "0.1.3"
+__version__ = "0.1.4"
diff --git a/website/docs/admin_hosts_houdini.md b/website/docs/admin_hosts_houdini.md
index 64c54db591a..18c390e07fc 100644
--- a/website/docs/admin_hosts_houdini.md
+++ b/website/docs/admin_hosts_houdini.md
@@ -3,9 +3,36 @@ id: admin_hosts_houdini
title: Houdini
sidebar_label: Houdini
---
+## General Settings
+### Houdini Vars
+
+Allows admins to have a list of vars (e.g. JOB) with (dynamic) values that will be updated on context changes, e.g. when switching to another asset or task.
+
+Using template keys is supported but formatting keys capitalization variants is not, e.g. `{Asset}` and `{ASSET}` won't work
+
+
+:::note
+If `Treat as directory` toggle is activated, Openpype will consider the given value is a path of a folder.
+
+If the folder does not exist on the context change it will be created by this feature so that the path will always try to point to an existing folder.
+:::
+
+Disabling `Update Houdini vars on context change` feature will leave all Houdini vars unmanaged and thus no context update changes will occur.
+
+> If `$JOB` is present in the Houdini var list and has an empty value, OpenPype will set its value to `$HIP`
+
+
+:::note
+For consistency reasons we always force all vars to be uppercase.
+e.g. `myvar` will be `MYVAR`
+:::
+
+![update-houdini-vars-context-change](assets/houdini/update-houdini-vars-context-change.png)
+
+
## Shelves Manager
You can add your custom shelf set into Houdini by setting your shelf sets, shelves and tools in **Houdini -> Shelves Manager**.
![Custom menu definition](assets/houdini-admin_shelvesmanager.png)
-The Shelf Set Path is used to load a .shelf file to generate your shelf set. If the path is specified, you don't have to set the shelves and tools.
\ No newline at end of file
+The Shelf Set Path is used to load a .shelf file to generate your shelf set. If the path is specified, you don't have to set the shelves and tools.
diff --git a/website/docs/assets/houdini/update-houdini-vars-context-change.png b/website/docs/assets/houdini/update-houdini-vars-context-change.png
new file mode 100644
index 00000000000..74ac8d86c9f
Binary files /dev/null and b/website/docs/assets/houdini/update-houdini-vars-context-change.png differ