Merge develop

This commit is contained in:
Petr Kalis 2022-12-08 11:21:38 +01:00
commit 492e5bbd03
169 changed files with 4267 additions and 1648 deletions

View file

@ -1,5 +1,27 @@
# Changelog
## [3.14.8](https://github.com/pypeclub/OpenPype/tree/3.14.8)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.7...3.14.8)
**🚀 Enhancements**
- General: Refactored extract hierarchy plugin [\#4139](https://github.com/pypeclub/OpenPype/pull/4139)
- General: Find executable enhancement [\#4137](https://github.com/pypeclub/OpenPype/pull/4137)
- Ftrack: Reset session before instance processing [\#4129](https://github.com/pypeclub/OpenPype/pull/4129)
- Ftrack: Editorial asset sync issue [\#4126](https://github.com/pypeclub/OpenPype/pull/4126)
- Deadline: Build version resolving [\#4115](https://github.com/pypeclub/OpenPype/pull/4115)
- Houdini: New Publisher [\#3046](https://github.com/pypeclub/OpenPype/pull/3046)
- Fix: Standalone Publish Directories [\#4148](https://github.com/pypeclub/OpenPype/pull/4148)
**🐛 Bug fixes**
- Ftrack: Fix occational double parents issue [\#4153](https://github.com/pypeclub/OpenPype/pull/4153)
- General: Maketx executable issue [\#4136](https://github.com/pypeclub/OpenPype/pull/4136)
- Maya: Looks - add all connections [\#4135](https://github.com/pypeclub/OpenPype/pull/4135)
- General: Fix variable check in collect anatomy instance data [\#4117](https://github.com/pypeclub/OpenPype/pull/4117)
## [3.14.7](https://github.com/pypeclub/OpenPype/tree/3.14.7)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.6...3.14.7)

View file

@ -1,5 +1,25 @@
# Changelog
## [3.14.8](https://github.com/pypeclub/OpenPype/tree/3.14.8)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.7...3.14.8)
**🚀 Enhancements**
- General: Refactored extract hierarchy plugin [\#4139](https://github.com/pypeclub/OpenPype/pull/4139)
- General: Find executable enhancement [\#4137](https://github.com/pypeclub/OpenPype/pull/4137)
- Ftrack: Reset session before instance processing [\#4129](https://github.com/pypeclub/OpenPype/pull/4129)
- Ftrack: Editorial asset sync issue [\#4126](https://github.com/pypeclub/OpenPype/pull/4126)
- Deadline: Build version resolving [\#4115](https://github.com/pypeclub/OpenPype/pull/4115)
- Houdini: New Publisher [\#3046](https://github.com/pypeclub/OpenPype/pull/3046)
- Fix: Standalone Publish Directories [\#4148](https://github.com/pypeclub/OpenPype/pull/4148)
**🐛 Bug fixes**
- Ftrack: Fix occational double parents issue [\#4153](https://github.com/pypeclub/OpenPype/pull/4153)
- General: Maketx executable issue [\#4136](https://github.com/pypeclub/OpenPype/pull/4136)
- Maya: Looks - add all connections [\#4135](https://github.com/pypeclub/OpenPype/pull/4135)
- General: Fix variable check in collect anatomy instance data [\#4117](https://github.com/pypeclub/OpenPype/pull/4117)
## [3.14.7](https://github.com/pypeclub/OpenPype/tree/3.14.7)

View file

@ -72,17 +72,19 @@ def get_errored_plugins_from_data(context):
return get_errored_plugins_from_context(context)
# 'RepairAction' and 'RepairContextAction' were moved to
# 'openpype.pipeline.publish' please change you imports.
# There is no "reasonable" way hot mark these classes as deprecated to show
# warning of wrong import.
# Deprecated since 3.14.* will be removed in 3.16.*
class RepairAction(pyblish.api.Action):
"""Repairs the action
To process the repairing this requires a static `repair(instance)` method
is available on the plugin.
Deprecated:
'RepairAction' and 'RepairContextAction' were moved to
'openpype.pipeline.publish' please change you imports.
There is no "reasonable" way hot mark these classes as deprecated
to show warning of wrong import. Deprecated since 3.14.* will be
removed in 3.16.*
"""
label = "Repair"
on = "failed" # This action is only available on a failed plug-in
@ -103,13 +105,19 @@ class RepairAction(pyblish.api.Action):
plugin.repair(instance)
# Deprecated since 3.14.* will be removed in 3.16.*
class RepairContextAction(pyblish.api.Action):
"""Repairs the action
To process the repairing this requires a static `repair(instance)` method
is available on the plugin.
Deprecated:
'RepairAction' and 'RepairContextAction' were moved to
'openpype.pipeline.publish' please change you imports.
There is no "reasonable" way hot mark these classes as deprecated
to show warning of wrong import. Deprecated since 3.14.* will be
removed in 3.16.*
"""
label = "Repair"
on = "failed" # This action is only available on a failed plug-in

View file

@ -252,7 +252,7 @@ class IWorkfileHost:
Remove when all usages are replaced.
"""
self.save_workfile()
self.save_workfile(dst_path)
def open_file(self, filepath):
"""Deprecated variant of 'open_workfile'.

View file

@ -0,0 +1,72 @@
import os
import bpy
from openpype.pipeline import publish
from openpype.hosts.blender.api import plugin
class ExtractAnimationABC(publish.Extractor):
"""Extract as ABC."""
label = "Extract Animation ABC"
hosts = ["blender"]
families = ["animation"]
optional = True
def process(self, instance):
# Define extract output file path
stagingdir = self.staging_dir(instance)
filename = f"{instance.name}.abc"
filepath = os.path.join(stagingdir, filename)
context = bpy.context
# Perform extraction
self.log.info("Performing extraction..")
plugin.deselect_all()
selected = []
asset_group = None
objects = []
for obj in instance:
if isinstance(obj, bpy.types.Collection):
for child in obj.all_objects:
objects.append(child)
for obj in objects:
children = [o for o in bpy.data.objects if o.parent == obj]
for child in children:
objects.append(child)
for obj in objects:
obj.select_set(True)
selected.append(obj)
context = plugin.create_blender_context(
active=asset_group, selected=selected)
# We export the abc
bpy.ops.wm.alembic_export(
context,
filepath=filepath,
selected=True,
flatten=False
)
plugin.deselect_all()
if "representations" not in instance.data:
instance.data["representations"] = []
representation = {
'name': 'abc',
'ext': 'abc',
'files': filename,
"stagingDir": stagingdir,
}
instance.data["representations"].append(representation)
self.log.info("Extracted instance '%s' to: %s",
instance.name, representation)

View file

@ -0,0 +1,10 @@
from .addon import (
CELACTION_ROOT_DIR,
CelactionAddon,
)
__all__ = (
"CELACTION_ROOT_DIR",
"CelactionAddon",
)

View file

@ -0,0 +1,31 @@
import os
from openpype.modules import OpenPypeModule, IHostAddon
CELACTION_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
class CelactionAddon(OpenPypeModule, IHostAddon):
name = "celaction"
host_name = "celaction"
def initialize(self, module_settings):
self.enabled = True
def get_launch_hook_paths(self, app):
if app.host_name != self.host_name:
return []
return [
os.path.join(CELACTION_ROOT_DIR, "hooks")
]
def add_implementation_envs(self, env, _app):
# Set default values if are not already set via settings
defaults = {
"LOGLEVEL": "DEBUG"
}
for key, value in defaults.items():
if not env.get(key):
env[key] = value
def get_workfile_extensions(self):
return [".scn"]

View file

@ -1 +0,0 @@
kwargs = None

View file

@ -1,87 +0,0 @@
import os
import sys
import copy
import argparse
import pyblish.api
import pyblish.util
import openpype.hosts.celaction
from openpype.lib import Logger
from openpype.hosts.celaction import api as celaction
from openpype.tools.utils import host_tools
from openpype.pipeline import install_openpype_plugins
log = Logger.get_logger("Celaction_cli_publisher")
publish_host = "celaction"
HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.celaction.__file__))
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
def cli():
parser = argparse.ArgumentParser(prog="celaction_publish")
parser.add_argument("--currentFile",
help="Pass file to Context as `currentFile`")
parser.add_argument("--chunk",
help=("Render chanks on farm"))
parser.add_argument("--frameStart",
help=("Start of frame range"))
parser.add_argument("--frameEnd",
help=("End of frame range"))
parser.add_argument("--resolutionWidth",
help=("Width of resolution"))
parser.add_argument("--resolutionHeight",
help=("Height of resolution"))
celaction.kwargs = parser.parse_args(sys.argv[1:]).__dict__
def _prepare_publish_environments():
"""Prepares environments based on request data."""
env = copy.deepcopy(os.environ)
project_name = os.getenv("AVALON_PROJECT")
asset_name = os.getenv("AVALON_ASSET")
env["AVALON_PROJECT"] = project_name
env["AVALON_ASSET"] = asset_name
env["AVALON_TASK"] = os.getenv("AVALON_TASK")
env["AVALON_WORKDIR"] = os.getenv("AVALON_WORKDIR")
env["AVALON_APP"] = f"hosts.{publish_host}"
env["AVALON_APP_NAME"] = "celaction/local"
env["PYBLISH_HOSTS"] = publish_host
os.environ.update(env)
def main():
# prepare all environments
_prepare_publish_environments()
# Registers pype's Global pyblish plugins
install_openpype_plugins()
if os.path.exists(PUBLISH_PATH):
log.info(f"Registering path: {PUBLISH_PATH}")
pyblish.api.register_plugin_path(PUBLISH_PATH)
pyblish.api.register_host(publish_host)
return host_tools.show_publish()
if __name__ == "__main__":
cli()
result = main()
sys.exit(not bool(result))

View file

@ -1,122 +0,0 @@
import os
import shutil
import winreg
from openpype.lib import PreLaunchHook
from openpype.hosts.celaction import api as celaction
class CelactionPrelaunchHook(PreLaunchHook):
"""
Bootstrap celacion with pype
"""
workfile_ext = "scn"
app_groups = ["celaction"]
platforms = ["windows"]
def execute(self):
# Add workfile path to launch arguments
workfile_path = self.workfile_path()
if workfile_path:
self.launch_context.launch_args.append(workfile_path)
project_name = self.data["project_name"]
asset_name = self.data["asset_name"]
task_name = self.data["task_name"]
# get publish version of celaction
app = "celaction_publish"
# setting output parameters
path = r"Software\CelAction\CelAction2D\User Settings"
winreg.CreateKey(winreg.HKEY_CURRENT_USER, path)
hKey = winreg.OpenKey(
winreg.HKEY_CURRENT_USER,
"Software\\CelAction\\CelAction2D\\User Settings", 0,
winreg.KEY_ALL_ACCESS)
# TODO: this will need to be checked more thoroughly
pype_exe = os.getenv("OPENPYPE_EXECUTABLE")
winreg.SetValueEx(hKey, "SubmitAppTitle", 0, winreg.REG_SZ, pype_exe)
parameters = [
"launch",
f"--app {app}",
f"--project {project_name}",
f"--asset {asset_name}",
f"--task {task_name}",
"--currentFile \\\"\"*SCENE*\"\\\"",
"--chunk 10",
"--frameStart *START*",
"--frameEnd *END*",
"--resolutionWidth *X*",
"--resolutionHeight *Y*",
# "--programDir \"'*PROGPATH*'\""
]
winreg.SetValueEx(hKey, "SubmitParametersTitle", 0, winreg.REG_SZ,
" ".join(parameters))
# setting resolution parameters
path = r"Software\CelAction\CelAction2D\User Settings\Dialogs"
path += r"\SubmitOutput"
winreg.CreateKey(winreg.HKEY_CURRENT_USER, path)
hKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0,
winreg.KEY_ALL_ACCESS)
winreg.SetValueEx(hKey, "SaveScene", 0, winreg.REG_DWORD, 1)
winreg.SetValueEx(hKey, "CustomX", 0, winreg.REG_DWORD, 1920)
winreg.SetValueEx(hKey, "CustomY", 0, winreg.REG_DWORD, 1080)
# making sure message dialogs don't appear when overwriting
path = r"Software\CelAction\CelAction2D\User Settings\Messages"
path += r"\OverwriteScene"
winreg.CreateKey(winreg.HKEY_CURRENT_USER, path)
hKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0,
winreg.KEY_ALL_ACCESS)
winreg.SetValueEx(hKey, "Result", 0, winreg.REG_DWORD, 6)
winreg.SetValueEx(hKey, "Valid", 0, winreg.REG_DWORD, 1)
path = r"Software\CelAction\CelAction2D\User Settings\Messages"
path += r"\SceneSaved"
winreg.CreateKey(winreg.HKEY_CURRENT_USER, path)
hKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0,
winreg.KEY_ALL_ACCESS)
winreg.SetValueEx(hKey, "Result", 0, winreg.REG_DWORD, 1)
winreg.SetValueEx(hKey, "Valid", 0, winreg.REG_DWORD, 1)
def workfile_path(self):
workfile_path = self.data["last_workfile_path"]
# copy workfile from template if doesnt exist any on path
if not os.path.exists(workfile_path):
# TODO add ability to set different template workfile path via
# settings
pype_celaction_dir = os.path.dirname(os.path.dirname(
os.path.abspath(celaction.__file__)
))
template_path = os.path.join(
pype_celaction_dir,
"resources",
"celaction_template_scene.scn"
)
if not os.path.exists(template_path):
self.log.warning(
"Couldn't find workfile template file in {}".format(
template_path
)
)
return
self.log.info(
f"Creating workfile from template: \"{template_path}\""
)
# Copy template workfile to new destinantion
shutil.copy2(
os.path.normpath(template_path),
os.path.normpath(workfile_path)
)
self.log.info(f"Workfile to open: \"{workfile_path}\"")
return workfile_path

View file

@ -0,0 +1,137 @@
import os
import shutil
import winreg
import subprocess
from openpype.lib import PreLaunchHook, get_openpype_execute_args
from openpype.hosts.celaction import scripts
CELACTION_SCRIPTS_DIR = os.path.dirname(
os.path.abspath(scripts.__file__)
)
class CelactionPrelaunchHook(PreLaunchHook):
"""
Bootstrap celacion with pype
"""
app_groups = ["celaction"]
platforms = ["windows"]
def execute(self):
asset_doc = self.data["asset_doc"]
width = asset_doc["data"]["resolutionWidth"]
height = asset_doc["data"]["resolutionHeight"]
# Add workfile path to launch arguments
workfile_path = self.workfile_path()
if workfile_path:
self.launch_context.launch_args.append(workfile_path)
# setting output parameters
path_user_settings = "\\".join([
"Software", "CelAction", "CelAction2D", "User Settings"
])
winreg.CreateKey(winreg.HKEY_CURRENT_USER, path_user_settings)
hKey = winreg.OpenKey(
winreg.HKEY_CURRENT_USER, path_user_settings, 0,
winreg.KEY_ALL_ACCESS
)
path_to_cli = os.path.join(CELACTION_SCRIPTS_DIR, "publish_cli.py")
subproces_args = get_openpype_execute_args("run", path_to_cli)
openpype_executable = subproces_args.pop(0)
winreg.SetValueEx(
hKey,
"SubmitAppTitle",
0,
winreg.REG_SZ,
openpype_executable
)
parameters = subproces_args + [
"--currentFile", "*SCENE*",
"--chunk", "*CHUNK*",
"--frameStart", "*START*",
"--frameEnd", "*END*",
"--resolutionWidth", "*X*",
"--resolutionHeight", "*Y*"
]
winreg.SetValueEx(
hKey, "SubmitParametersTitle", 0, winreg.REG_SZ,
subprocess.list2cmdline(parameters)
)
# setting resolution parameters
path_submit = "\\".join([
path_user_settings, "Dialogs", "SubmitOutput"
])
winreg.CreateKey(winreg.HKEY_CURRENT_USER, path_submit)
hKey = winreg.OpenKey(
winreg.HKEY_CURRENT_USER, path_submit, 0,
winreg.KEY_ALL_ACCESS
)
winreg.SetValueEx(hKey, "SaveScene", 0, winreg.REG_DWORD, 1)
winreg.SetValueEx(hKey, "CustomX", 0, winreg.REG_DWORD, width)
winreg.SetValueEx(hKey, "CustomY", 0, winreg.REG_DWORD, height)
# making sure message dialogs don't appear when overwriting
path_overwrite_scene = "\\".join([
path_user_settings, "Messages", "OverwriteScene"
])
winreg.CreateKey(winreg.HKEY_CURRENT_USER, path_overwrite_scene)
hKey = winreg.OpenKey(
winreg.HKEY_CURRENT_USER, path_overwrite_scene, 0,
winreg.KEY_ALL_ACCESS
)
winreg.SetValueEx(hKey, "Result", 0, winreg.REG_DWORD, 6)
winreg.SetValueEx(hKey, "Valid", 0, winreg.REG_DWORD, 1)
# set scane as not saved
path_scene_saved = "\\".join([
path_user_settings, "Messages", "SceneSaved"
])
winreg.CreateKey(winreg.HKEY_CURRENT_USER, path_scene_saved)
hKey = winreg.OpenKey(
winreg.HKEY_CURRENT_USER, path_scene_saved, 0,
winreg.KEY_ALL_ACCESS
)
winreg.SetValueEx(hKey, "Result", 0, winreg.REG_DWORD, 1)
winreg.SetValueEx(hKey, "Valid", 0, winreg.REG_DWORD, 1)
def workfile_path(self):
workfile_path = self.data["last_workfile_path"]
# copy workfile from template if doesnt exist any on path
if not os.path.exists(workfile_path):
# TODO add ability to set different template workfile path via
# settings
openpype_celaction_dir = os.path.dirname(CELACTION_SCRIPTS_DIR)
template_path = os.path.join(
openpype_celaction_dir,
"resources",
"celaction_template_scene.scn"
)
if not os.path.exists(template_path):
self.log.warning(
"Couldn't find workfile template file in {}".format(
template_path
)
)
return
self.log.info(
f"Creating workfile from template: \"{template_path}\""
)
# Copy template workfile to new destinantion
shutil.copy2(
os.path.normpath(template_path),
os.path.normpath(workfile_path)
)
self.log.info(f"Workfile to open: \"{workfile_path}\"")
return workfile_path

View file

@ -1,5 +1,7 @@
import pyblish.api
from openpype.hosts.celaction import api as celaction
import argparse
import sys
from pprint import pformat
class CollectCelactionCliKwargs(pyblish.api.Collector):
@ -9,15 +11,31 @@ class CollectCelactionCliKwargs(pyblish.api.Collector):
order = pyblish.api.Collector.order - 0.1
def process(self, context):
kwargs = celaction.kwargs.copy()
parser = argparse.ArgumentParser(prog="celaction")
parser.add_argument("--currentFile",
help="Pass file to Context as `currentFile`")
parser.add_argument("--chunk",
help=("Render chanks on farm"))
parser.add_argument("--frameStart",
help=("Start of frame range"))
parser.add_argument("--frameEnd",
help=("End of frame range"))
parser.add_argument("--resolutionWidth",
help=("Width of resolution"))
parser.add_argument("--resolutionHeight",
help=("Height of resolution"))
passing_kwargs = parser.parse_args(sys.argv[1:]).__dict__
self.log.info("Storing kwargs: %s" % kwargs)
context.set_data("kwargs", kwargs)
self.log.info("Storing kwargs ...")
self.log.debug("_ passing_kwargs: {}".format(pformat(passing_kwargs)))
# set kwargs to context data
context.set_data("passingKwargs", passing_kwargs)
# get kwargs onto context data as keys with values
for k, v in kwargs.items():
for k, v in passing_kwargs.items():
self.log.info(f"Setting `{k}` to instance.data with value: `{v}`")
if k in ["frameStart", "frameEnd"]:
context.data[k] = kwargs[k] = int(v)
context.data[k] = passing_kwargs[k] = int(v)
else:
context.data[k] = v

View file

@ -36,7 +36,8 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin):
"version": version
}
celaction_kwargs = context.data.get("kwargs", {})
celaction_kwargs = context.data.get(
"passingKwargs", {})
if celaction_kwargs:
shared_instance_data.update(celaction_kwargs)
@ -52,8 +53,8 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin):
"subset": subset,
"label": scene_file,
"family": family,
"families": [family, "ftrack"],
"representations": list()
"families": [],
"representations": []
})
# adding basic script data
@ -72,7 +73,6 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin):
self.log.info('Publishing Celaction workfile')
# render instance
family = "render.farm"
subset = f"render{task}Main"
instance = context.create_instance(name=subset)
# getting instance state
@ -81,8 +81,8 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin):
# add assetEntity data into instance
instance.data.update({
"label": "{} - farm".format(subset),
"family": family,
"families": [family],
"family": "render.farm",
"families": [],
"subset": subset
})

View file

@ -11,28 +11,31 @@ class CollectRenderPath(pyblish.api.InstancePlugin):
families = ["render.farm"]
# Presets
anatomy_render_key = None
publish_render_metadata = None
output_extension = "png"
anatomy_template_key_render_files = None
anatomy_template_key_metadata = None
def process(self, instance):
anatomy = instance.context.data["anatomy"]
anatomy_data = copy.deepcopy(instance.data["anatomyData"])
anatomy_data["family"] = "render"
padding = anatomy.templates.get("frame_padding", 4)
anatomy_data.update({
"frame": f"%0{padding}d",
"representation": "png"
"family": "render",
"representation": self.output_extension,
"ext": self.output_extension
})
anatomy_filled = anatomy.format(anatomy_data)
# get anatomy rendering keys
anatomy_render_key = self.anatomy_render_key or "render"
publish_render_metadata = self.publish_render_metadata or "render"
r_anatomy_key = self.anatomy_template_key_render_files
m_anatomy_key = self.anatomy_template_key_metadata
# get folder and path for rendering images from celaction
render_dir = anatomy_filled[anatomy_render_key]["folder"]
render_path = anatomy_filled[anatomy_render_key]["path"]
render_dir = anatomy_filled[r_anatomy_key]["folder"]
render_path = anatomy_filled[r_anatomy_key]["path"]
self.log.debug("__ render_path: `{}`".format(render_path))
# create dir if it doesnt exists
try:
@ -46,9 +49,9 @@ class CollectRenderPath(pyblish.api.InstancePlugin):
instance.data["path"] = render_path
# get anatomy for published renders folder path
if anatomy_filled.get(publish_render_metadata):
if anatomy_filled.get(m_anatomy_key):
instance.data["publishRenderMetadataFolder"] = anatomy_filled[
publish_render_metadata]["folder"]
m_anatomy_key]["folder"]
self.log.info("Metadata render path: `{}`".format(
instance.data["publishRenderMetadataFolder"]
))

View file

@ -0,0 +1,37 @@
import os
import sys
import pyblish.api
import pyblish.util
import openpype.hosts.celaction
from openpype.lib import Logger
from openpype.tools.utils import host_tools
from openpype.pipeline import install_openpype_plugins
log = Logger.get_logger("celaction")
PUBLISH_HOST = "celaction"
HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.celaction.__file__))
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
def main():
# Registers pype's Global pyblish plugins
install_openpype_plugins()
if os.path.exists(PUBLISH_PATH):
log.info(f"Registering path: {PUBLISH_PATH}")
pyblish.api.register_plugin_path(PUBLISH_PATH)
pyblish.api.register_host(PUBLISH_HOST)
pyblish.api.register_target("local")
return host_tools.show_publish()
if __name__ == "__main__":
result = main()
sys.exit(not bool(result))

View file

@ -596,18 +596,28 @@ class PublishableClip:
if not hero_track and self.vertical_sync:
# driving layer is set as negative match
for (_in, _out), hero_data in self.vertical_clip_match.items():
hero_data.update({"heroTrack": False})
if _in == self.clip_in and _out == self.clip_out:
"""
Since only one instance of hero clip is expected in
`self.vertical_clip_match`, this will loop only once
until none hero clip will be matched with hero clip.
`tag_hierarchy_data` will be set only once for every
clip which is not hero clip.
"""
_hero_data = deepcopy(hero_data)
_hero_data.update({"heroTrack": False})
if _in <= self.clip_in and _out >= self.clip_out:
data_subset = hero_data["subset"]
# add track index in case duplicity of names in hero data
if self.subset in data_subset:
hero_data["subset"] = self.subset + str(
_hero_data["subset"] = self.subset + str(
self.track_index)
# in case track name and subset name is the same then add
if self.subset_name == self.track_name:
hero_data["subset"] = self.subset
_hero_data["subset"] = self.subset
# assing data to return hierarchy data to tag
tag_hierarchy_data = hero_data
tag_hierarchy_data = _hero_data
break
# add data to return data dict
self.marker_data.update(tag_hierarchy_data)

View file

@ -27,7 +27,12 @@ class HieroAddon(OpenPypeModule, IHostAddon):
new_hiero_paths.append(norm_path)
env["HIERO_PLUGIN_PATH"] = os.pathsep.join(new_hiero_paths)
# Remove auto screen scale factor for Qt
# - let Hiero decide it's value
env.pop("QT_AUTO_SCREEN_SCALE_FACTOR", None)
# Remove tkinter library paths if are set
env.pop("TK_LIBRARY", None)
env.pop("TCL_LIBRARY", None)
# Add vendor to PYTHONPATH
python_path = env["PYTHONPATH"]

View file

@ -1,24 +1,13 @@
from .pipeline import (
install,
uninstall,
HoudiniHost,
ls,
containerise,
containerise
)
from .plugin import (
Creator,
)
from .workio import (
open_file,
save_file,
current_file,
has_unsaved_changes,
file_extensions,
work_root
)
from .lib import (
lsattr,
lsattrs,
@ -29,22 +18,13 @@ from .lib import (
__all__ = [
"install",
"uninstall",
"HoudiniHost",
"ls",
"containerise",
"Creator",
# Workfiles API
"open_file",
"save_file",
"current_file",
"has_unsaved_changes",
"file_extensions",
"work_root",
# Utility functions
"lsattr",
"lsattrs",
@ -52,7 +32,3 @@ __all__ = [
"maintained_selection"
]
# Backwards API compatibility
open = open_file
save = save_file

View file

@ -1,6 +1,10 @@
# -*- coding: utf-8 -*-
import sys
import os
import uuid
import logging
from contextlib import contextmanager
import json
import six
@ -8,10 +12,13 @@ from openpype.client import get_asset_by_name
from openpype.pipeline import legacy_io
from openpype.pipeline.context_tools import get_current_project_asset
import hou
self = sys.modules[__name__]
self._parent = None
log = logging.getLogger(__name__)
JSON_PREFIX = "JSON:::"
def get_asset_fps():
@ -29,23 +36,18 @@ def set_id(node, unique_id, overwrite=False):
def get_id(node):
"""
Get the `cbId` attribute of the given node
"""Get the `cbId` attribute of the given node.
Args:
node (hou.Node): the name of the node to retrieve the attribute from
Returns:
str
str: cbId attribute of the node.
"""
if node is None:
return
id = node.parm("id")
if node is None:
return
return id
if node is not None:
return node.parm("id")
def generate_ids(nodes, asset_id=None):
@ -281,7 +283,7 @@ def render_rop(ropnode):
raise RuntimeError("Render failed: {0}".format(exc))
def imprint(node, data):
def imprint(node, data, update=False):
"""Store attributes with value on a node
Depending on the type of attribute it creates the correct parameter
@ -290,49 +292,76 @@ def imprint(node, data):
http://www.sidefx.com/docs/houdini/hom/hou/ParmTemplate.html
Because of some update glitch where you cannot overwrite existing
ParmTemplates on node using:
`setParmTemplates()` and `parmTuplesInFolder()`
update is done in another pass.
Args:
node(hou.Node): node object from Houdini
data(dict): collection of attributes and their value
update (bool, optional): flag if imprint should update
already existing data or leave them untouched and only
add new.
Returns:
None
"""
if not data:
return
if not node:
self.log.error("Node is not set, calling imprint on invalid data.")
return
parm_group = node.parmTemplateGroup()
current_parms = {p.name(): p for p in node.spareParms()}
update_parms = []
templates = []
parm_folder = hou.FolderParmTemplate("folder", "Extra")
for key, value in data.items():
if value is None:
continue
if isinstance(value, float):
parm = hou.FloatParmTemplate(name=key,
label=key,
num_components=1,
default_value=(value,))
elif isinstance(value, bool):
parm = hou.ToggleParmTemplate(name=key,
label=key,
default_value=value)
elif isinstance(value, int):
parm = hou.IntParmTemplate(name=key,
label=key,
num_components=1,
default_value=(value,))
elif isinstance(value, six.string_types):
parm = hou.StringParmTemplate(name=key,
label=key,
num_components=1,
default_value=(value,))
else:
raise TypeError("Unsupported type: %r" % type(value))
parm = get_template_from_value(key, value)
parm_folder.addParmTemplate(parm)
if key in current_parms:
if node.evalParm(key) == data[key]:
continue
if not update:
log.debug(f"{key} already exists on {node}")
else:
log.debug(f"replacing {key}")
update_parms.append(parm)
continue
templates.append(parm)
parm_group = node.parmTemplateGroup()
parm_folder = parm_group.findFolder("Extra")
# if folder doesn't exist yet, create one and append to it,
# else append to existing one
if not parm_folder:
parm_folder = hou.FolderParmTemplate("folder", "Extra")
parm_folder.setParmTemplates(templates)
parm_group.append(parm_folder)
else:
for template in templates:
parm_group.appendToFolder(parm_folder, template)
# this is needed because the pointer to folder
# is for some reason lost every call to `appendToFolder()`
parm_folder = parm_group.findFolder("Extra")
parm_group.append(parm_folder)
node.setParmTemplateGroup(parm_group)
# TODO: Updating is done here, by calling probably deprecated functions.
# This needs to be addressed in the future.
if not update_parms:
return
for parm in update_parms:
node.replaceSpareParmTuple(parm.name(), parm)
def lsattr(attr, value=None, root="/"):
"""Return nodes that have `attr`
@ -397,8 +426,22 @@ def read(node):
"""
# `spareParms` returns a tuple of hou.Parm objects
return {parameter.name(): parameter.eval() for
parameter in node.spareParms()}
data = {}
if not node:
return data
for parameter in node.spareParms():
value = parameter.eval()
# test if value is json encoded dict
if isinstance(value, six.string_types) and \
value.startswith(JSON_PREFIX):
try:
value = json.loads(value[len(JSON_PREFIX):])
except json.JSONDecodeError:
# not a json
pass
data[parameter.name()] = value
return data
@contextmanager
@ -460,3 +503,89 @@ def reset_framerange():
hou.playbar.setFrameRange(frame_start, frame_end)
hou.playbar.setPlaybackRange(frame_start, frame_end)
hou.setFrame(frame_start)
def get_main_window():
"""Acquire Houdini's main window"""
if self._parent is None:
self._parent = hou.ui.mainQtWindow()
return self._parent
def get_template_from_value(key, value):
if isinstance(value, float):
parm = hou.FloatParmTemplate(name=key,
label=key,
num_components=1,
default_value=(value,))
elif isinstance(value, bool):
parm = hou.ToggleParmTemplate(name=key,
label=key,
default_value=value)
elif isinstance(value, int):
parm = hou.IntParmTemplate(name=key,
label=key,
num_components=1,
default_value=(value,))
elif isinstance(value, six.string_types):
parm = hou.StringParmTemplate(name=key,
label=key,
num_components=1,
default_value=(value,))
elif isinstance(value, (dict, list, tuple)):
parm = hou.StringParmTemplate(name=key,
label=key,
num_components=1,
default_value=(
JSON_PREFIX + json.dumps(value),))
else:
raise TypeError("Unsupported type: %r" % type(value))
return parm
def get_frame_data(node):
"""Get the frame data: start frame, end frame and steps.
Args:
node(hou.Node)
Returns:
dict: frame data for star, end and steps.
"""
data = {}
if node.parm("trange") is None:
return data
if node.evalParm("trange") == 0:
self.log.debug("trange is 0")
return data
data["frameStart"] = node.evalParm("f1")
data["frameEnd"] = node.evalParm("f2")
data["steps"] = node.evalParm("f3")
return data
def splitext(name, allowed_multidot_extensions):
# type: (str, list) -> tuple
"""Split file name to name and extension.
Args:
name (str): File name to split.
allowed_multidot_extensions (list of str): List of allowed multidot
extensions.
Returns:
tuple: Name and extension.
"""
for ext in allowed_multidot_extensions:
if name.endswith(ext):
return name[:-len(ext)], ext
return os.path.splitext(name)

View file

@ -1,9 +1,13 @@
# -*- coding: utf-8 -*-
"""Pipeline tools for OpenPype Houdini integration."""
import os
import sys
import logging
import contextlib
import hou
import hou # noqa
from openpype.host import HostBase, IWorkfileHost, ILoadHost, INewPublisher
import pyblish.api
@ -26,6 +30,7 @@ from .lib import get_asset_fps
log = logging.getLogger("openpype.hosts.houdini")
AVALON_CONTAINERS = "/obj/AVALON_CONTAINERS"
CONTEXT_CONTAINER = "/obj/OpenPypeContext"
IS_HEADLESS = not hasattr(hou, "ui")
PLUGINS_DIR = os.path.join(HOUDINI_HOST_DIR, "plugins")
@ -35,71 +40,139 @@ CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
self = sys.modules[__name__]
self._has_been_setup = False
self._parent = None
self._events = dict()
class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher):
name = "houdini"
def __init__(self):
super(HoudiniHost, self).__init__()
self._op_events = {}
self._has_been_setup = False
def install():
_register_callbacks()
def install(self):
pyblish.api.register_host("houdini")
pyblish.api.register_host("hython")
pyblish.api.register_host("hpython")
pyblish.api.register_host("houdini")
pyblish.api.register_host("hython")
pyblish.api.register_host("hpython")
pyblish.api.register_plugin_path(PUBLISH_PATH)
register_loader_plugin_path(LOAD_PATH)
register_creator_plugin_path(CREATE_PATH)
pyblish.api.register_plugin_path(PUBLISH_PATH)
register_loader_plugin_path(LOAD_PATH)
register_creator_plugin_path(CREATE_PATH)
log.info("Installing callbacks ... ")
# register_event_callback("init", on_init)
self._register_callbacks()
register_event_callback("before.save", before_save)
register_event_callback("save", on_save)
register_event_callback("open", on_open)
register_event_callback("new", on_new)
log.info("Installing callbacks ... ")
# register_event_callback("init", on_init)
register_event_callback("before.save", before_save)
register_event_callback("save", on_save)
register_event_callback("open", on_open)
register_event_callback("new", on_new)
pyblish.api.register_callback(
"instanceToggled", on_pyblish_instance_toggled
)
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")
self._has_been_setup = True
# add houdini vendor packages
hou_pythonpath = os.path.join(HOUDINI_HOST_DIR, "vendor")
sys.path.append(hou_pythonpath)
sys.path.append(hou_pythonpath)
# Set asset settings for the empty scene directly after launch of
# Houdini so it initializes into the correct scene FPS,
# Frame Range, etc.
# TODO: make sure this doesn't trigger when
# opening with last workfile.
_set_context_settings()
shelves.generate_shelves()
# Set asset settings for the empty scene directly after launch of Houdini
# so it initializes into the correct scene FPS, Frame Range, etc.
# todo: make sure this doesn't trigger when opening with last workfile
_set_context_settings()
shelves.generate_shelves()
def has_unsaved_changes(self):
return hou.hipFile.hasUnsavedChanges()
def get_workfile_extensions(self):
return [".hip", ".hiplc", ".hipnc"]
def uninstall():
"""Uninstall Houdini-specific functionality of avalon-core.
def save_workfile(self, dst_path=None):
# Force forwards slashes to avoid segfault
if dst_path:
dst_path = dst_path.replace("\\", "/")
hou.hipFile.save(file_name=dst_path,
save_to_recent_files=True)
return dst_path
This function is called automatically on calling `api.uninstall()`.
"""
def open_workfile(self, filepath):
# Force forwards slashes to avoid segfault
filepath = filepath.replace("\\", "/")
pyblish.api.deregister_host("hython")
pyblish.api.deregister_host("hpython")
pyblish.api.deregister_host("houdini")
hou.hipFile.load(filepath,
suppress_save_prompt=True,
ignore_load_warnings=False)
return filepath
def _register_callbacks():
for event in self._events.copy().values():
if event is None:
continue
def get_current_workfile(self):
current_filepath = hou.hipFile.path()
if (os.path.basename(current_filepath) == "untitled.hip" and
not os.path.exists(current_filepath)):
# By default a new scene in houdini is saved in the current
# working directory as "untitled.hip" so we need to capture
# that and consider it 'not saved' when it's in that state.
return None
try:
hou.hipFile.removeEventCallback(event)
except RuntimeError as e:
log.info(e)
return current_filepath
self._events[on_file_event_callback] = hou.hipFile.addEventCallback(
on_file_event_callback
)
def get_containers(self):
return ls()
def _register_callbacks(self):
for event in self._op_events.copy().values():
if event is None:
continue
try:
hou.hipFile.removeEventCallback(event)
except RuntimeError as e:
log.info(e)
self._op_events[on_file_event_callback] = hou.hipFile.addEventCallback(
on_file_event_callback
)
@staticmethod
def create_context_node():
"""Helper for creating context holding node.
Returns:
hou.Node: context node
"""
obj_network = hou.node("/obj")
op_ctx = obj_network.createNode(
"null", node_name="OpenPypeContext")
op_ctx.moveToGoodPosition()
op_ctx.setBuiltExplicitly(False)
op_ctx.setCreatorState("OpenPype")
op_ctx.setComment("OpenPype node to hold context metadata")
op_ctx.setColor(hou.Color((0.081, 0.798, 0.810)))
op_ctx.hide(True)
return op_ctx
def update_context_data(self, data, changes):
op_ctx = hou.node(CONTEXT_CONTAINER)
if not op_ctx:
op_ctx = self.create_context_node()
lib.imprint(op_ctx, data)
def get_context_data(self):
op_ctx = hou.node(CONTEXT_CONTAINER)
if not op_ctx:
op_ctx = self.create_context_node()
return lib.read(op_ctx)
def save_file(self, dst_path=None):
# Force forwards slashes to avoid segfault
dst_path = dst_path.replace("\\", "/")
hou.hipFile.save(file_name=dst_path,
save_to_recent_files=True)
def on_file_event_callback(event):
@ -113,22 +186,6 @@ def on_file_event_callback(event):
emit_event("new")
def get_main_window():
"""Acquire Houdini's main window"""
if self._parent is None:
self._parent = hou.ui.mainQtWindow()
return self._parent
def teardown():
"""Remove integration"""
if not self._has_been_setup:
return
self._has_been_setup = False
print("pyblish: Integration torn down successfully")
def containerise(name,
namespace,
nodes,
@ -251,7 +308,7 @@ def on_open():
log.warning("Scene has outdated content.")
# Get main window
parent = get_main_window()
parent = lib.get_main_window()
if parent is None:
log.info("Skipping outdated content pop-up "
"because Houdini window can't be found.")

View file

@ -1,14 +1,19 @@
# -*- coding: utf-8 -*-
"""Houdini specific Avalon/Pyblish plugin definitions."""
import sys
from abc import (
ABCMeta
)
import six
import hou
from openpype.pipeline import (
CreatorError,
LegacyCreator
LegacyCreator,
Creator as NewCreator,
CreatedInstance
)
from .lib import imprint
from openpype.lib import BoolDef
from .lib import imprint, read, lsattr
class OpenPypeCreatorError(CreatorError):
@ -30,12 +35,15 @@ class Creator(LegacyCreator):
when hovering over a node. The information is visible under the name of
the node.
Deprecated:
This creator is deprecated and will be removed in future version.
"""
defaults = ['Main']
def __init__(self, *args, **kwargs):
super(Creator, self).__init__(*args, **kwargs)
self.nodes = list()
self.nodes = []
def process(self):
"""This is the base functionality to create instances in Houdini
@ -84,3 +92,187 @@ class Creator(LegacyCreator):
OpenPypeCreatorError,
OpenPypeCreatorError("Creator error: {}".format(er)),
sys.exc_info()[2])
class HoudiniCreatorBase(object):
@staticmethod
def cache_subsets(shared_data):
"""Cache instances for Creators to shared data.
Create `houdini_cached_subsets` key when needed in shared data and
fill it with all collected instances from the scene under its
respective creator identifiers.
If legacy instances are detected in the scene, create
`houdini_cached_legacy_subsets` there and fill it with
all legacy subsets under family as a key.
Args:
Dict[str, Any]: Shared data.
Return:
Dict[str, Any]: Shared data dictionary.
"""
if shared_data.get("houdini_cached_subsets") is None:
shared_data["houdini_cached_subsets"] = {}
if shared_data.get("houdini_cached_legacy_subsets") is None:
shared_data["houdini_cached_legacy_subsets"] = {}
cached_instances = lsattr("id", "pyblish.avalon.instance")
for i in cached_instances:
if not i.parm("creator_identifier"):
# we have legacy instance
family = i.parm("family").eval()
if family not in shared_data[
"houdini_cached_legacy_subsets"]:
shared_data["houdini_cached_legacy_subsets"][
family] = [i]
else:
shared_data[
"houdini_cached_legacy_subsets"][family].append(i)
continue
creator_id = i.parm("creator_identifier").eval()
if creator_id not in shared_data["houdini_cached_subsets"]:
shared_data["houdini_cached_subsets"][creator_id] = [i]
else:
shared_data[
"houdini_cached_subsets"][creator_id].append(i) # noqa
return shared_data
@staticmethod
def create_instance_node(
node_name, parent,
node_type="geometry"):
# type: (str, str, str) -> hou.Node
"""Create node representing instance.
Arguments:
node_name (str): Name of the new node.
parent (str): Name of the parent node.
node_type (str, optional): Type of the node.
Returns:
hou.Node: Newly created instance node.
"""
parent_node = hou.node(parent)
instance_node = parent_node.createNode(
node_type, node_name=node_name)
instance_node.moveToGoodPosition()
return instance_node
@six.add_metaclass(ABCMeta)
class HoudiniCreator(NewCreator, HoudiniCreatorBase):
"""Base class for most of the Houdini creator plugins."""
selected_nodes = []
def create(self, subset_name, instance_data, pre_create_data):
try:
if pre_create_data.get("use_selection"):
self.selected_nodes = hou.selectedNodes()
# Get the node type and remove it from the data, not needed
node_type = instance_data.pop("node_type", None)
if node_type is None:
node_type = "geometry"
instance_node = self.create_instance_node(
subset_name, "/out", node_type)
self.customize_node_look(instance_node)
instance_data["instance_node"] = instance_node.path()
instance = CreatedInstance(
self.family,
subset_name,
instance_data,
self)
self._add_instance_to_context(instance)
imprint(instance_node, instance.data_to_store())
return instance
except hou.Error as er:
six.reraise(
OpenPypeCreatorError,
OpenPypeCreatorError("Creator error: {}".format(er)),
sys.exc_info()[2])
def lock_parameters(self, node, parameters):
"""Lock list of specified parameters on the node.
Args:
node (hou.Node): Houdini node to lock parameters on.
parameters (list of str): List of parameter names.
"""
for name in parameters:
try:
parm = node.parm(name)
parm.lock(True)
except AttributeError:
self.log.debug("missing lock pattern {}".format(name))
def collect_instances(self):
# cache instances if missing
self.cache_subsets(self.collection_shared_data)
for instance in self.collection_shared_data[
"houdini_cached_subsets"].get(self.identifier, []):
created_instance = CreatedInstance.from_existing(
read(instance), self
)
self._add_instance_to_context(created_instance)
def update_instances(self, update_list):
for created_inst, _changes in update_list:
instance_node = hou.node(created_inst.get("instance_node"))
new_values = {
key: new_value
for key, (_old_value, new_value) in _changes.items()
}
imprint(
instance_node,
new_values,
update=True
)
def remove_instances(self, instances):
"""Remove specified instance from the scene.
This is only removing `id` parameter so instance is no longer
instance, because it might contain valuable data for artist.
"""
for instance in instances:
instance_node = hou.node(instance.data.get("instance_node"))
if instance_node:
instance_node.destroy()
self._remove_instance_from_context(instance)
def get_pre_create_attr_defs(self):
return [
BoolDef("use_selection", label="Use selection")
]
@staticmethod
def customize_node_look(
node, color=None,
shape="chevron_down"):
"""Set custom look for instance nodes.
Args:
node (hou.Node): Node to set look.
color (hou.Color, Optional): Color of the node.
shape (str, Optional): Shape name of the node.
Returns:
None
"""
if not color:
color = hou.Color((0.616, 0.871, 0.769))
node.setUserData('nodeshape', shape)
node.setColor(color)

View file

@ -1,57 +0,0 @@
"""Host API required Work Files tool"""
import os
import hou
def file_extensions():
return [".hip", ".hiplc", ".hipnc"]
def has_unsaved_changes():
return hou.hipFile.hasUnsavedChanges()
def save_file(filepath):
# Force forwards slashes to avoid segfault
filepath = filepath.replace("\\", "/")
hou.hipFile.save(file_name=filepath,
save_to_recent_files=True)
return filepath
def open_file(filepath):
# Force forwards slashes to avoid segfault
filepath = filepath.replace("\\", "/")
hou.hipFile.load(filepath,
suppress_save_prompt=True,
ignore_load_warnings=False)
return filepath
def current_file():
current_filepath = hou.hipFile.path()
if (os.path.basename(current_filepath) == "untitled.hip" and
not os.path.exists(current_filepath)):
# By default a new scene in houdini is saved in the current
# working directory as "untitled.hip" so we need to capture
# that and consider it 'not saved' when it's in that state.
return None
return current_filepath
def work_root(session):
work_dir = session["AVALON_WORKDIR"]
scene_dir = session.get("AVALON_SCENEDIR")
if scene_dir:
return os.path.join(work_dir, scene_dir)
else:
return work_dir

View file

@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
"""Convertor for legacy Houdini subsets."""
from openpype.pipeline.create.creator_plugins import SubsetConvertorPlugin
from openpype.hosts.houdini.api.lib import imprint
class HoudiniLegacyConvertor(SubsetConvertorPlugin):
"""Find and convert any legacy subsets in the scene.
This Convertor will find all legacy subsets in the scene and will
transform them to the current system. Since the old subsets doesn't
retain any information about their original creators, the only mapping
we can do is based on their families.
Its limitation is that you can have multiple creators creating subset
of the same family and there is no way to handle it. This code should
nevertheless cover all creators that came with OpenPype.
"""
identifier = "io.openpype.creators.houdini.legacy"
family_to_id = {
"camera": "io.openpype.creators.houdini.camera",
"ass": "io.openpype.creators.houdini.ass",
"imagesequence": "io.openpype.creators.houdini.imagesequence",
"hda": "io.openpype.creators.houdini.hda",
"pointcache": "io.openpype.creators.houdini.pointcache",
"redshiftproxy": "io.openpype.creators.houdini.redshiftproxy",
"redshift_rop": "io.openpype.creators.houdini.redshift_rop",
"usd": "io.openpype.creators.houdini.usd",
"usdrender": "io.openpype.creators.houdini.usdrender",
"vdbcache": "io.openpype.creators.houdini.vdbcache"
}
def __init__(self, *args, **kwargs):
super(HoudiniLegacyConvertor, self).__init__(*args, **kwargs)
self.legacy_subsets = {}
def find_instances(self):
"""Find legacy subsets in the scene.
Legacy subsets are the ones that doesn't have `creator_identifier`
parameter on them.
This is using cached entries done in
:py:meth:`~HoudiniCreatorBase.cache_subsets()`
"""
self.legacy_subsets = self.collection_shared_data.get(
"houdini_cached_legacy_subsets")
if not self.legacy_subsets:
return
self.add_convertor_item("Found {} incompatible subset{}.".format(
len(self.legacy_subsets), "s" if len(self.legacy_subsets) > 1 else "")
)
def convert(self):
"""Convert all legacy subsets to current.
It is enough to add `creator_identifier` and `instance_node`.
"""
if not self.legacy_subsets:
return
for family, subsets in self.legacy_subsets.items():
if family in self.family_to_id:
for subset in subsets:
data = {
"creator_identifier": self.family_to_id[family],
"instance_node": subset.path()
}
self.log.info("Converting {} to {}".format(
subset.path(), self.family_to_id[family]))
imprint(subset, data)

View file

@ -1,46 +1,49 @@
# -*- coding: utf-8 -*-
"""Creator plugin for creating alembic camera subsets."""
from openpype.hosts.houdini.api import plugin
from openpype.pipeline import CreatedInstance, CreatorError
class CreateAlembicCamera(plugin.Creator):
"""Single baked camera from Alembic ROP"""
class CreateAlembicCamera(plugin.HoudiniCreator):
"""Single baked camera from Alembic ROP."""
name = "camera"
identifier = "io.openpype.creators.houdini.camera"
label = "Camera (Abc)"
family = "camera"
icon = "camera"
def __init__(self, *args, **kwargs):
super(CreateAlembicCamera, self).__init__(*args, **kwargs)
def create(self, subset_name, instance_data, pre_create_data):
import hou
# Remove the active, we are checking the bypass flag of the nodes
self.data.pop("active", None)
instance_data.pop("active", None)
instance_data.update({"node_type": "alembic"})
# Set node type to create for output
self.data.update({"node_type": "alembic"})
instance = super(CreateAlembicCamera, self).create(
subset_name,
instance_data,
pre_create_data) # type: CreatedInstance
def _process(self, instance):
"""Creator main entry point.
Args:
instance (hou.Node): Created Houdini instance.
"""
instance_node = hou.node(instance.get("instance_node"))
parms = {
"filename": "$HIP/pyblish/%s.abc" % self.name,
"filename": hou.text.expandString(
"$HIP/pyblish/{}.abc".format(subset_name)),
"use_sop_path": False,
}
if self.nodes:
node = self.nodes[0]
path = node.path()
if self.selected_nodes:
if len(self.selected_nodes) > 1:
raise CreatorError("More than one item selected.")
path = self.selected_nodes[0].path()
# Split the node path into the first root and the remainder
# So we can set the root and objects parameters correctly
_, root, remainder = path.split("/", 2)
parms.update({"root": "/" + root, "objects": remainder})
instance.setParms(parms)
instance_node.setParms(parms)
# Lock the Use Sop Path setting so the
# user doesn't accidentally enable it.
instance.parm("use_sop_path").lock(True)
instance.parm("trange").set(1)
to_lock = ["use_sop_path"]
self.lock_parameters(instance_node, to_lock)
instance_node.parm("trange").set(1)

View file

@ -1,9 +1,12 @@
# -*- coding: utf-8 -*-
"""Creator plugin for creating Arnold ASS files."""
from openpype.hosts.houdini.api import plugin
class CreateArnoldAss(plugin.Creator):
class CreateArnoldAss(plugin.HoudiniCreator):
"""Arnold .ass Archive"""
identifier = "io.openpype.creators.houdini.ass"
label = "Arnold ASS"
family = "ass"
icon = "magic"
@ -12,42 +15,39 @@ class CreateArnoldAss(plugin.Creator):
# Default extension: `.ass` or `.ass.gz`
ext = ".ass"
def __init__(self, *args, **kwargs):
super(CreateArnoldAss, self).__init__(*args, **kwargs)
def create(self, subset_name, instance_data, pre_create_data):
import hou
# Remove the active, we are checking the bypass flag of the nodes
self.data.pop("active", None)
instance_data.pop("active", None)
instance_data.update({"node_type": "arnold"})
self.data.update({"node_type": "arnold"})
instance = super(CreateArnoldAss, self).create(
subset_name,
instance_data,
pre_create_data) # type: plugin.CreatedInstance
def process(self):
node = super(CreateArnoldAss, self).process()
basename = node.name()
node.setName(basename + "_ASS", unique_name=True)
instance_node = hou.node(instance.get("instance_node"))
# Hide Properties Tab on Arnold ROP since that's used
# for rendering instead of .ass Archive Export
parm_template_group = node.parmTemplateGroup()
parm_template_group = instance_node.parmTemplateGroup()
parm_template_group.hideFolder("Properties", True)
node.setParmTemplateGroup(parm_template_group)
instance_node.setParmTemplateGroup(parm_template_group)
filepath = '$HIP/pyblish/`chs("subset")`.$F4{}'.format(self.ext)
filepath = "{}{}".format(
hou.text.expandString("$HIP/pyblish/"),
"{}.$F4{}".format(subset_name, self.ext)
)
parms = {
# Render frame range
"trange": 1,
# Arnold ROP settings
"ar_ass_file": filepath,
"ar_ass_export_enable": 1
}
node.setParms(parms)
# Lock the ASS export attribute
node.parm("ar_ass_export_enable").lock(True)
instance_node.setParms(parms)
# Lock some Avalon attributes
to_lock = ["family", "id"]
for name in to_lock:
parm = node.parm(name)
parm.lock(True)
# Lock any parameters in this list
to_lock = ["ar_ass_export_enable", "family", "id"]
self.lock_parameters(instance_node, to_lock)

View file

@ -1,44 +1,42 @@
# -*- coding: utf-8 -*-
"""Creator plugin for creating composite sequences."""
from openpype.hosts.houdini.api import plugin
from openpype.pipeline import CreatedInstance
class CreateCompositeSequence(plugin.Creator):
class CreateCompositeSequence(plugin.HoudiniCreator):
"""Composite ROP to Image Sequence"""
identifier = "io.openpype.creators.houdini.imagesequence"
label = "Composite (Image Sequence)"
family = "imagesequence"
icon = "gears"
def __init__(self, *args, **kwargs):
super(CreateCompositeSequence, self).__init__(*args, **kwargs)
ext = ".exr"
# Remove the active, we are checking the bypass flag of the nodes
self.data.pop("active", None)
def create(self, subset_name, instance_data, pre_create_data):
import hou # noqa
# Type of ROP node to create
self.data.update({"node_type": "comp"})
instance_data.pop("active", None)
instance_data.update({"node_type": "comp"})
def _process(self, instance):
"""Creator main entry point.
instance = super(CreateCompositeSequence, self).create(
subset_name,
instance_data,
pre_create_data) # type: CreatedInstance
Args:
instance (hou.Node): Created Houdini instance.
instance_node = hou.node(instance.get("instance_node"))
filepath = "{}{}".format(
hou.text.expandString("$HIP/pyblish/"),
"{}.$F4{}".format(subset_name, self.ext)
)
parms = {
"trange": 1,
"copoutput": filepath
}
"""
parms = {"copoutput": "$HIP/pyblish/%s.$F4.exr" % self.name}
if self.nodes:
node = self.nodes[0]
parms.update({"coppath": node.path()})
instance.setParms(parms)
instance_node.setParms(parms)
# Lock any parameters in this list
to_lock = ["prim_to_detail_pattern"]
for name in to_lock:
try:
parm = instance.parm(name)
parm.lock(True)
except AttributeError:
# missing lock pattern
self.log.debug(
"missing lock pattern {}".format(name))
self.lock_parameters(instance_node, to_lock)

View file

@ -1,28 +1,22 @@
# -*- coding: utf-8 -*-
import hou
"""Creator plugin for creating publishable Houdini Digital Assets."""
from openpype.client import (
get_asset_by_name,
get_subsets,
)
from openpype.pipeline import legacy_io
from openpype.hosts.houdini.api import lib
from openpype.hosts.houdini.api import plugin
class CreateHDA(plugin.Creator):
class CreateHDA(plugin.HoudiniCreator):
"""Publish Houdini Digital Asset file."""
name = "hda"
identifier = "io.openpype.creators.houdini.hda"
label = "Houdini Digital Asset (Hda)"
family = "hda"
icon = "gears"
maintain_selection = False
def __init__(self, *args, **kwargs):
super(CreateHDA, self).__init__(*args, **kwargs)
self.data.pop("active", None)
def _check_existing(self, subset_name):
# type: (str) -> bool
"""Check if existing subset name versions already exists."""
@ -40,55 +34,51 @@ class CreateHDA(plugin.Creator):
}
return subset_name.lower() in existing_subset_names_low
def _process(self, instance):
subset_name = self.data["subset"]
# get selected nodes
out = hou.node("/obj")
self.nodes = hou.selectedNodes()
def _create_instance_node(
self, node_name, parent, node_type="geometry"):
import hou
if (self.options or {}).get("useSelection") and self.nodes:
# if we have `use selection` enabled and we have some
parent_node = hou.node("/obj")
if self.selected_nodes:
# if we have `use selection` enabled, and we have some
# selected nodes ...
subnet = out.collapseIntoSubnet(
self.nodes,
subnet_name="{}_subnet".format(self.name))
subnet = parent_node.collapseIntoSubnet(
self.selected_nodes,
subnet_name="{}_subnet".format(node_name))
subnet.moveToGoodPosition()
to_hda = subnet
else:
to_hda = out.createNode(
"subnet", node_name="{}_subnet".format(self.name))
to_hda = parent_node.createNode(
"subnet", node_name="{}_subnet".format(node_name))
if not to_hda.type().definition():
# if node type has not its definition, it is not user
# created hda. We test if hda can be created from the node.
if not to_hda.canCreateDigitalAsset():
raise Exception(
raise plugin.OpenPypeCreatorError(
"cannot create hda from node {}".format(to_hda))
hda_node = to_hda.createDigitalAsset(
name=subset_name,
hda_file_name="$HIP/{}.hda".format(subset_name)
name=node_name,
hda_file_name="$HIP/{}.hda".format(node_name)
)
hda_node.layoutChildren()
elif self._check_existing(subset_name):
elif self._check_existing(node_name):
raise plugin.OpenPypeCreatorError(
("subset {} is already published with different HDA"
"definition.").format(subset_name))
"definition.").format(node_name))
else:
hda_node = to_hda
hda_node.setName(subset_name)
# delete node created by Avalon in /out
# this needs to be addressed in future Houdini workflow refactor.
hou.node("/out/{}".format(subset_name)).destroy()
try:
lib.imprint(hda_node, self.data)
except hou.OperationFailed:
raise plugin.OpenPypeCreatorError(
("Cannot set metadata on asset. Might be that it already is "
"OpenPype asset.")
)
hda_node.setName(node_name)
self.customize_node_look(hda_node)
return hda_node
def create(self, subset_name, instance_data, pre_create_data):
instance_data.pop("active", None)
instance = super(CreateHDA, self).create(
subset_name,
instance_data,
pre_create_data) # type: plugin.CreatedInstance
return instance

View file

@ -1,48 +1,51 @@
# -*- coding: utf-8 -*-
"""Creator plugin for creating pointcache alembics."""
from openpype.hosts.houdini.api import plugin
from openpype.pipeline import CreatedInstance
class CreatePointCache(plugin.Creator):
class CreatePointCache(plugin.HoudiniCreator):
"""Alembic ROP to pointcache"""
name = "pointcache"
identifier = "io.openpype.creators.houdini.pointcache"
label = "Point Cache"
family = "pointcache"
icon = "gears"
def __init__(self, *args, **kwargs):
super(CreatePointCache, self).__init__(*args, **kwargs)
def create(self, subset_name, instance_data, pre_create_data):
import hou
# Remove the active, we are checking the bypass flag of the nodes
self.data.pop("active", None)
instance_data.pop("active", None)
instance_data.update({"node_type": "alembic"})
self.data.update({"node_type": "alembic"})
instance = super(CreatePointCache, self).create(
subset_name,
instance_data,
pre_create_data) # type: CreatedInstance
def _process(self, instance):
"""Creator main entry point.
Args:
instance (hou.Node): Created Houdini instance.
"""
instance_node = hou.node(instance.get("instance_node"))
parms = {
"use_sop_path": True, # Export single node from SOP Path
"build_from_path": True, # Direct path of primitive in output
"path_attrib": "path", # Pass path attribute for output
"use_sop_path": True,
"build_from_path": True,
"path_attrib": "path",
"prim_to_detail_pattern": "cbId",
"format": 2, # Set format to Ogawa
"facesets": 0, # No face sets (by default exclude them)
"filename": "$HIP/pyblish/%s.abc" % self.name,
"format": 2,
"facesets": 0,
"filename": hou.text.expandString(
"$HIP/pyblish/{}.abc".format(subset_name))
}
if self.nodes:
node = self.nodes[0]
parms.update({"sop_path": node.path()})
if self.selected_nodes:
parms["sop_path"] = self.selected_nodes[0].path()
instance.setParms(parms)
instance.parm("trange").set(1)
# try to find output node
for child in self.selected_nodes[0].children():
if child.type().name() == "output":
parms["sop_path"] = child.path()
break
instance_node.setParms(parms)
instance_node.parm("trange").set(1)
# Lock any parameters in this list
to_lock = ["prim_to_detail_pattern"]
for name in to_lock:
parm = instance.parm(name)
parm.lock(True)
self.lock_parameters(instance_node, to_lock)

View file

@ -1,18 +1,20 @@
# -*- coding: utf-8 -*-
"""Creator plugin for creating Redshift proxies."""
from openpype.hosts.houdini.api import plugin
from openpype.pipeline import CreatedInstance
class CreateRedshiftProxy(plugin.Creator):
class CreateRedshiftProxy(plugin.HoudiniCreator):
"""Redshift Proxy"""
identifier = "io.openpype.creators.houdini.redshiftproxy"
label = "Redshift Proxy"
family = "redshiftproxy"
icon = "magic"
def __init__(self, *args, **kwargs):
super(CreateRedshiftProxy, self).__init__(*args, **kwargs)
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
self.data.pop("active", None)
instance_data.pop("active", None)
# Redshift provides a `Redshift_Proxy_Output` node type which shows
# a limited set of parameters by default and is set to extract a
@ -21,28 +23,24 @@ class CreateRedshiftProxy(plugin.Creator):
# why this happens.
# TODO: Somehow enforce so that it only shows the original limited
# attributes of the Redshift_Proxy_Output node type
self.data.update({"node_type": "Redshift_Proxy_Output"})
instance_data.update({"node_type": "Redshift_Proxy_Output"})
def _process(self, instance):
"""Creator main entry point.
instance = super(CreateRedshiftProxy, self).create(
subset_name,
instance_data,
pre_create_data) # type: CreatedInstance
Args:
instance (hou.Node): Created Houdini instance.
instance_node = hou.node(instance.get("instance_node"))
"""
parms = {
"RS_archive_file": '$HIP/pyblish/`chs("subset")`.$F4.rs',
"RS_archive_file": '$HIP/pyblish/`{}.$F4.rs'.format(subset_name),
}
if self.nodes:
node = self.nodes[0]
path = node.path()
parms["RS_archive_sopPath"] = path
if self.selected_nodes:
parms["RS_archive_sopPath"] = self.selected_nodes[0].path()
instance.setParms(parms)
instance_node.setParms(parms)
# Lock some Avalon attributes
to_lock = ["family", "id"]
for name in to_lock:
parm = instance.parm(name)
parm.lock(True)
to_lock = ["family", "id", "prim_to_detail_pattern"]
self.lock_parameters(instance_node, to_lock)

View file

@ -1,41 +1,40 @@
import hou
# -*- coding: utf-8 -*-
"""Creator plugin to create Redshift ROP."""
from openpype.hosts.houdini.api import plugin
from openpype.pipeline import CreatedInstance
class CreateRedshiftROP(plugin.Creator):
class CreateRedshiftROP(plugin.HoudiniCreator):
"""Redshift ROP"""
identifier = "io.openpype.creators.houdini.redshift_rop"
label = "Redshift ROP"
family = "redshift_rop"
icon = "magic"
defaults = ["master"]
def __init__(self, *args, **kwargs):
super(CreateRedshiftROP, self).__init__(*args, **kwargs)
def create(self, subset_name, instance_data, pre_create_data):
import hou # noqa
instance_data.pop("active", None)
instance_data.update({"node_type": "Redshift_ROP"})
# Add chunk size attribute
instance_data["chunkSize"] = 10
# Clear the family prefix from the subset
subset = self.data["subset"]
subset = subset_name
subset_no_prefix = subset[len(self.family):]
subset_no_prefix = subset_no_prefix[0].lower() + subset_no_prefix[1:]
self.data["subset"] = subset_no_prefix
subset_name = subset_no_prefix
# Add chunk size attribute
self.data["chunkSize"] = 10
instance = super(CreateRedshiftROP, self).create(
subset_name,
instance_data,
pre_create_data) # type: CreatedInstance
# Remove the active, we are checking the bypass flag of the nodes
self.data.pop("active", None)
instance_node = hou.node(instance.get("instance_node"))
self.data.update({"node_type": "Redshift_ROP"})
def _process(self, instance):
"""Creator main entry point.
Args:
instance (hou.Node): Created Houdini instance.
"""
basename = instance.name()
instance.setName(basename + "_ROP", unique_name=True)
basename = instance_node.name()
instance_node.setName(basename + "_ROP", unique_name=True)
# Also create the linked Redshift IPR Rop
try:
@ -43,11 +42,12 @@ class CreateRedshiftROP(plugin.Creator):
"Redshift_IPR", node_name=basename + "_IPR"
)
except hou.OperationFailed:
raise Exception(("Cannot create Redshift node. Is Redshift "
"installed and enabled?"))
raise plugin.OpenPypeCreatorError(
("Cannot create Redshift node. Is Redshift "
"installed and enabled?"))
# Move it to directly under the Redshift ROP
ipr_rop.setPosition(instance.position() + hou.Vector2(0, -1))
ipr_rop.setPosition(instance_node.position() + hou.Vector2(0, -1))
# Set the linked rop to the Redshift ROP
ipr_rop.parm("linked_rop").set(ipr_rop.relativePathTo(instance))
@ -61,10 +61,8 @@ class CreateRedshiftROP(plugin.Creator):
"RS_outputMultilayerMode": 0, # no multi-layered exr
"RS_outputBeautyAOVSuffix": "beauty",
}
instance.setParms(parms)
instance_node.setParms(parms)
# Lock some Avalon attributes
to_lock = ["family", "id"]
for name in to_lock:
parm = instance.parm(name)
parm.lock(True)
self.lock_parameters(instance_node, to_lock)

View file

@ -1,39 +1,39 @@
# -*- coding: utf-8 -*-
"""Creator plugin for creating USDs."""
from openpype.hosts.houdini.api import plugin
from openpype.pipeline import CreatedInstance
class CreateUSD(plugin.Creator):
class CreateUSD(plugin.HoudiniCreator):
"""Universal Scene Description"""
identifier = "io.openpype.creators.houdini.usd"
label = "USD (experimental)"
family = "usd"
icon = "gears"
enabled = False
def __init__(self, *args, **kwargs):
super(CreateUSD, self).__init__(*args, **kwargs)
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
self.data.pop("active", None)
instance_data.pop("active", None)
instance_data.update({"node_type": "usd"})
self.data.update({"node_type": "usd"})
instance = super(CreateUSD, self).create(
subset_name,
instance_data,
pre_create_data) # type: CreatedInstance
def _process(self, instance):
"""Creator main entry point.
instance_node = hou.node(instance.get("instance_node"))
Args:
instance (hou.Node): Created Houdini instance.
"""
parms = {
"lopoutput": "$HIP/pyblish/%s.usd" % self.name,
"lopoutput": "$HIP/pyblish/{}.usd".format(subset_name),
"enableoutputprocessor_simplerelativepaths": False,
}
if self.nodes:
node = self.nodes[0]
parms.update({"loppath": node.path()})
if self.selected_nodes:
parms["loppath"] = self.selected_nodes[0].path()
instance.setParms(parms)
instance_node.setParms(parms)
# Lock any parameters in this list
to_lock = [
@ -42,6 +42,4 @@ class CreateUSD(plugin.Creator):
"family",
"id",
]
for name in to_lock:
parm = instance.parm(name)
parm.lock(True)
self.lock_parameters(instance_node, to_lock)

View file

@ -1,42 +1,41 @@
import hou
# -*- coding: utf-8 -*-
"""Creator plugin for creating USD renders."""
from openpype.hosts.houdini.api import plugin
from openpype.pipeline import CreatedInstance
class CreateUSDRender(plugin.Creator):
class CreateUSDRender(plugin.HoudiniCreator):
"""USD Render ROP in /stage"""
identifier = "io.openpype.creators.houdini.usdrender"
label = "USD Render (experimental)"
family = "usdrender"
icon = "magic"
def __init__(self, *args, **kwargs):
super(CreateUSDRender, self).__init__(*args, **kwargs)
def create(self, subset_name, instance_data, pre_create_data):
import hou # noqa
self.parent = hou.node("/stage")
instance_data["parent"] = hou.node("/stage")
# Remove the active, we are checking the bypass flag of the nodes
self.data.pop("active", None)
instance_data.pop("active", None)
instance_data.update({"node_type": "usdrender"})
self.data.update({"node_type": "usdrender"})
instance = super(CreateUSDRender, self).create(
subset_name,
instance_data,
pre_create_data) # type: CreatedInstance
def _process(self, instance):
"""Creator main entry point.
instance_node = hou.node(instance.get("instance_node"))
Args:
instance (hou.Node): Created Houdini instance.
"""
parms = {
# Render frame range
"trange": 1
}
if self.nodes:
node = self.nodes[0]
parms.update({"loppath": node.path()})
instance.setParms(parms)
if self.selected_nodes:
parms["loppath"] = self.selected_nodes[0].path()
instance_node.setParms(parms)
# Lock some Avalon attributes
to_lock = ["family", "id"]
for name in to_lock:
parm = instance.parm(name)
parm.lock(True)
self.lock_parameters(instance_node, to_lock)

View file

@ -1,38 +1,36 @@
# -*- coding: utf-8 -*-
"""Creator plugin for creating VDB Caches."""
from openpype.hosts.houdini.api import plugin
from openpype.pipeline import CreatedInstance
class CreateVDBCache(plugin.Creator):
class CreateVDBCache(plugin.HoudiniCreator):
"""OpenVDB from Geometry ROP"""
identifier = "io.openpype.creators.houdini.vdbcache"
name = "vbdcache"
label = "VDB Cache"
family = "vdbcache"
icon = "cloud"
def __init__(self, *args, **kwargs):
super(CreateVDBCache, self).__init__(*args, **kwargs)
def create(self, subset_name, instance_data, pre_create_data):
import hou
# Remove the active, we are checking the bypass flag of the nodes
self.data.pop("active", None)
instance_data.pop("active", None)
instance_data.update({"node_type": "geometry"})
# Set node type to create for output
self.data["node_type"] = "geometry"
instance = super(CreateVDBCache, self).create(
subset_name,
instance_data,
pre_create_data) # type: CreatedInstance
def _process(self, instance):
"""Creator main entry point.
Args:
instance (hou.Node): Created Houdini instance.
"""
instance_node = hou.node(instance.get("instance_node"))
parms = {
"sopoutput": "$HIP/pyblish/%s.$F4.vdb" % self.name,
"sopoutput": "$HIP/pyblish/{}.$F4.vdb".format(subset_name),
"initsim": True,
"trange": 1
}
if self.nodes:
node = self.nodes[0]
parms.update({"soppath": node.path()})
if self.selected_nodes:
parms["soppath"] = self.selected_nodes[0].path()
instance.setParms(parms)
instance_node.setParms(parms)

View file

@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
"""Creator plugin for creating workfiles."""
from openpype.hosts.houdini.api import plugin
from openpype.hosts.houdini.api.lib import read, imprint
from openpype.hosts.houdini.api.pipeline import CONTEXT_CONTAINER
from openpype.pipeline import CreatedInstance, AutoCreator
from openpype.pipeline import legacy_io
from openpype.client import get_asset_by_name
import hou
class CreateWorkfile(plugin.HoudiniCreatorBase, AutoCreator):
"""Workfile auto-creator."""
identifier = "io.openpype.creators.houdini.workfile"
label = "Workfile"
family = "workfile"
icon = "document"
default_variant = "Main"
def create(self):
variant = self.default_variant
current_instance = next(
(
instance for instance in self.create_context.instances
if instance.creator_identifier == self.identifier
), None)
project_name = self.project_name
asset_name = legacy_io.Session["AVALON_ASSET"]
task_name = legacy_io.Session["AVALON_TASK"]
host_name = legacy_io.Session["AVALON_APP"]
if current_instance is None:
asset_doc = get_asset_by_name(project_name, asset_name)
subset_name = self.get_subset_name(
variant, task_name, asset_doc, project_name, host_name
)
data = {
"asset": asset_name,
"task": task_name,
"variant": variant
}
data.update(
self.get_dynamic_data(
variant, task_name, asset_doc,
project_name, host_name, current_instance)
)
self.log.info("Auto-creating workfile instance...")
current_instance = CreatedInstance(
self.family, subset_name, data, self
)
self._add_instance_to_context(current_instance)
elif (
current_instance["asset"] != asset_name
or current_instance["task"] != task_name
):
# Update instance context if is not the same
asset_doc = get_asset_by_name(project_name, asset_name)
subset_name = self.get_subset_name(
variant, task_name, asset_doc, project_name, host_name
)
current_instance["asset"] = asset_name
current_instance["task"] = task_name
current_instance["subset"] = subset_name
# write workfile information to context container.
op_ctx = hou.node(CONTEXT_CONTAINER)
if not op_ctx:
op_ctx = self.create_context_node()
workfile_data = {"workfile": current_instance.data_to_store()}
imprint(op_ctx, workfile_data)
def collect_instances(self):
op_ctx = hou.node(CONTEXT_CONTAINER)
instance = read(op_ctx)
if not instance:
return
workfile = instance.get("workfile")
if not workfile:
return
created_instance = CreatedInstance.from_existing(
workfile, self
)
self._add_instance_to_context(created_instance)
def update_instances(self, update_list):
op_ctx = hou.node(CONTEXT_CONTAINER)
for created_inst, _changes in update_list:
if created_inst["creator_identifier"] == self.identifier:
workfile_data = {"workfile": created_inst.data_to_store()}
imprint(op_ctx, workfile_data, update=True)

View file

@ -1,4 +1,5 @@
import pyblish.api
import hou
class CollectInstanceActiveState(pyblish.api.InstancePlugin):
@ -24,7 +25,7 @@ class CollectInstanceActiveState(pyblish.api.InstancePlugin):
# Check bypass state and reverse
active = True
node = instance[0]
node = hou.node(instance.get("instance_node"))
if hasattr(node, "isBypassed"):
active = not node.isBypassed()

View file

@ -5,19 +5,20 @@ from openpype.pipeline import legacy_io
import pyblish.api
class CollectHoudiniCurrentFile(pyblish.api.ContextPlugin):
class CollectHoudiniCurrentFile(pyblish.api.InstancePlugin):
"""Inject the current working file into context"""
order = pyblish.api.CollectorOrder - 0.01
label = "Houdini Current File"
hosts = ["houdini"]
family = ["workfile"]
def process(self, context):
def process(self, instance):
"""Inject the current working file"""
current_file = hou.hipFile.path()
if not os.path.exists(current_file):
# By default Houdini will even point a new scene to a path.
# By default, Houdini will even point a new scene to a path.
# However if the file is not saved at all and does not exist,
# we assume the user never set it.
filepath = ""
@ -34,43 +35,26 @@ class CollectHoudiniCurrentFile(pyblish.api.ContextPlugin):
"saved correctly."
)
context.data["currentFile"] = current_file
instance.context.data["currentFile"] = current_file
folder, file = os.path.split(current_file)
filename, ext = os.path.splitext(file)
task = legacy_io.Session["AVALON_TASK"]
data = {}
# create instance
instance = context.create_instance(name=filename)
subset = 'workfile' + task.capitalize()
data.update({
"subset": subset,
"asset": os.getenv("AVALON_ASSET", None),
"label": subset,
"publish": True,
"family": 'workfile',
"families": ['workfile'],
instance.data.update({
"setMembers": [current_file],
"frameStart": context.data['frameStart'],
"frameEnd": context.data['frameEnd'],
"handleStart": context.data['handleStart'],
"handleEnd": context.data['handleEnd']
"frameStart": instance.context.data['frameStart'],
"frameEnd": instance.context.data['frameEnd'],
"handleStart": instance.context.data['handleStart'],
"handleEnd": instance.context.data['handleEnd']
})
data['representations'] = [{
instance.data['representations'] = [{
'name': ext.lstrip("."),
'ext': ext.lstrip("."),
'files': file,
"stagingDir": folder,
}]
instance.data.update(data)
self.log.info('Collected instance: {}'.format(file))
self.log.info('Scene path: {}'.format(current_file))
self.log.info('staging Dir: {}'.format(folder))
self.log.info('subset: {}'.format(subset))

View file

@ -1,19 +1,13 @@
# -*- coding: utf-8 -*-
"""Collector plugin for frames data on ROP instances."""
import os
import re
import hou
import hou # noqa
import pyblish.api
from openpype.hosts.houdini.api import lib
def splitext(name, allowed_multidot_extensions):
for ext in allowed_multidot_extensions:
if name.endswith(ext):
return name[:-len(ext)], ext
return os.path.splitext(name)
class CollectFrames(pyblish.api.InstancePlugin):
"""Collect all frames which would be saved from the ROP nodes"""
@ -24,7 +18,9 @@ class CollectFrames(pyblish.api.InstancePlugin):
def process(self, instance):
ropnode = instance[0]
ropnode = hou.node(instance.data["instance_node"])
frame_data = lib.get_frame_data(ropnode)
instance.data.update(frame_data)
start_frame = instance.data.get("frameStart", None)
end_frame = instance.data.get("frameEnd", None)
@ -38,13 +34,13 @@ class CollectFrames(pyblish.api.InstancePlugin):
self.log.warning("Using current frame: {}".format(hou.frame()))
output = output_parm.eval()
_, ext = splitext(output,
_, ext = lib.splitext(output,
allowed_multidot_extensions=[".ass.gz"])
file_name = os.path.basename(output)
result = file_name
# Get the filename pattern match from the output
# path so we can compute all frames that would
# path, so we can compute all frames that would
# come out from rendering the ROP node if there
# is a frame pattern in the name
pattern = r"\w+\.(\d+)" + re.escape(ext)
@ -63,8 +59,9 @@ class CollectFrames(pyblish.api.InstancePlugin):
# for a custom frame list. So this should be refactored.
instance.data.update({"frames": result})
def create_file_list(self, match, start_frame, end_frame):
"""Collect files based on frame range and regex.match
@staticmethod
def create_file_list(match, start_frame, end_frame):
"""Collect files based on frame range and `regex.match`
Args:
match(re.match): match object

View file

@ -47,6 +47,11 @@ class CollectInstances(pyblish.api.ContextPlugin):
if node.evalParm("id") != "pyblish.avalon.instance":
continue
# instance was created by new creator code, skip it as
# it is already collected.
if node.parm("creator_identifier"):
continue
has_family = node.evalParm("family")
assert has_family, "'%s' is missing 'family'" % node.name()
@ -58,7 +63,8 @@ class CollectInstances(pyblish.api.ContextPlugin):
data.update({"active": not node.isBypassed()})
# temporarily translation of `active` to `publish` till issue has
# been resolved, https://github.com/pyblish/pyblish-base/issues/307
# been resolved.
# https://github.com/pyblish/pyblish-base/issues/307
if "active" in data:
data["publish"] = data["active"]
@ -78,6 +84,7 @@ class CollectInstances(pyblish.api.ContextPlugin):
instance.data["families"] = [instance.data["family"]]
instance[:] = [node]
instance.data["instance_node"] = node.path()
instance.data.update(data)
def sort_by_family(instance):

View file

@ -22,7 +22,7 @@ class CollectOutputSOPPath(pyblish.api.InstancePlugin):
import hou
node = instance[0]
node = hou.node(instance.data["instance_node"])
# Get sop path
node_type = node.type().name()

View file

@ -69,7 +69,7 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
def process(self, instance):
rop = instance[0]
rop = hou.node(instance.get("instance_node"))
# Collect chunkSize
chunk_size_parm = rop.parm("chunkSize")

View file

@ -53,7 +53,7 @@ class CollectRenderProducts(pyblish.api.InstancePlugin):
node = instance.data.get("output_node")
if not node:
rop_path = instance[0].path()
rop_path = instance.data["instance_node"].path()
raise RuntimeError(
"No output node found. Make sure to connect an "
"input to the USD ROP: %s" % rop_path

View file

@ -1,6 +1,6 @@
import pyblish.api
from openyppe.client import get_subset_by_name, get_asset_by_name
from openpype.client import get_subset_by_name, get_asset_by_name
from openpype.pipeline import legacy_io
import openpype.lib.usdlib as usdlib

View file

@ -3,6 +3,8 @@ import os
import pyblish.api
import openpype.hosts.houdini.api.usd as usdlib
import hou
class CollectUsdLayers(pyblish.api.InstancePlugin):
"""Collect the USD Layers that have configured save paths."""
@ -19,7 +21,7 @@ class CollectUsdLayers(pyblish.api.InstancePlugin):
self.log.debug("No output node found..")
return
rop_node = instance[0]
rop_node = hou.node(instance.get("instance_node"))
save_layers = []
for layer in usdlib.get_configured_save_layers(rop_node):
@ -54,8 +56,10 @@ class CollectUsdLayers(pyblish.api.InstancePlugin):
layer_inst.data["subset"] = "__stub__"
layer_inst.data["label"] = label
layer_inst.data["asset"] = instance.data["asset"]
layer_inst.append(instance[0]) # include same USD ROP
layer_inst.append((layer, save_path)) # include layer data
# include same USD ROP
layer_inst.append(rop_node)
# include layer data
layer_inst.append((layer, save_path))
# Allow this subset to be grouped into a USD Layer on creation
layer_inst.data["subsetGroup"] = "USD Layer"

View file

@ -5,6 +5,8 @@ import pyblish.api
from openpype.pipeline import publish
from openpype.hosts.houdini.api.lib import render_rop
import hou
class ExtractAlembic(publish.Extractor):
@ -15,7 +17,7 @@ class ExtractAlembic(publish.Extractor):
def process(self, instance):
ropnode = instance[0]
ropnode = hou.node(instance.data["instance_node"])
# Get the filename from the filename parameter
output = ropnode.evalParm("filename")

View file

@ -5,6 +5,8 @@ import pyblish.api
from openpype.pipeline import publish
from openpype.hosts.houdini.api.lib import render_rop
import hou
class ExtractAss(publish.Extractor):
@ -15,7 +17,7 @@ class ExtractAss(publish.Extractor):
def process(self, instance):
ropnode = instance[0]
ropnode = hou.node(instance.data["instance_node"])
# Get the filename from the filename parameter
# `.evalParm(parameter)` will make sure all tokens are resolved
@ -33,8 +35,12 @@ class ExtractAss(publish.Extractor):
# error and thus still continues to the integrator. To capture that
# we make sure all files exist
files = instance.data["frames"]
missing = [fname for fname in files
if not os.path.exists(os.path.join(staging_dir, fname))]
missing = []
for file_name in files:
full_path = os.path.normpath(os.path.join(staging_dir, file_name))
if not os.path.exists(full_path):
missing.append(full_path)
if missing:
raise RuntimeError("Failed to complete Arnold ass extraction. "
"Missing output files: {}".format(missing))

View file

@ -1,9 +1,10 @@
import os
import pyblish.api
from openpype.pipeline import publish
from openpype.hosts.houdini.api.lib import render_rop
from openpype.hosts.houdini.api.lib import render_rop, splitext
import hou
class ExtractComposite(publish.Extractor):
@ -15,7 +16,7 @@ class ExtractComposite(publish.Extractor):
def process(self, instance):
ropnode = instance[0]
ropnode = hou.node(instance.data["instance_node"])
# Get the filename from the copoutput parameter
# `.evalParm(parameter)` will make sure all tokens are resolved
@ -28,8 +29,24 @@ class ExtractComposite(publish.Extractor):
render_rop(ropnode)
if "files" not in instance.data:
instance.data["files"] = []
output = instance.data["frames"]
_, ext = splitext(output[0], [])
ext = ext.lstrip(".")
frames = instance.data["frames"]
instance.data["files"].append(frames)
if "representations" not in instance.data:
instance.data["representations"] = []
representation = {
"name": ext,
"ext": ext,
"files": output,
"stagingDir": staging_dir,
"frameStart": instance.data["frameStart"],
"frameEnd": instance.data["frameEnd"],
}
from pprint import pformat
self.log.info(pformat(representation))
instance.data["representations"].append(representation)

View file

@ -1,11 +1,9 @@
# -*- coding: utf-8 -*-
import os
from pprint import pformat
import pyblish.api
from openpype.pipeline import publish
import hou
class ExtractHDA(publish.Extractor):
@ -17,7 +15,7 @@ class ExtractHDA(publish.Extractor):
def process(self, instance):
self.log.info(pformat(instance.data))
hda_node = instance[0]
hda_node = hou.node(instance.data.get("instance_node"))
hda_def = hda_node.type().definition()
hda_options = hda_def.options()
hda_options.setSaveInitialParmsAndContents(True)

View file

@ -5,6 +5,8 @@ import pyblish.api
from openpype.pipeline import publish
from openpype.hosts.houdini.api.lib import render_rop
import hou
class ExtractRedshiftProxy(publish.Extractor):
@ -15,7 +17,7 @@ class ExtractRedshiftProxy(publish.Extractor):
def process(self, instance):
ropnode = instance[0]
ropnode = hou.node(instance.get("instance_node"))
# Get the filename from the filename parameter
# `.evalParm(parameter)` will make sure all tokens are resolved

View file

@ -5,6 +5,7 @@ import pyblish.api
from openpype.pipeline import publish
from openpype.hosts.houdini.api.lib import render_rop
import hou
class ExtractUSD(publish.Extractor):
@ -17,7 +18,7 @@ class ExtractUSD(publish.Extractor):
def process(self, instance):
ropnode = instance[0]
ropnode = hou.node(instance.get("instance_node"))
# Get the filename from the filename parameter
output = ropnode.evalParm("lopoutput")

View file

@ -187,7 +187,7 @@ class ExtractUSDLayered(publish.Extractor):
# Main ROP node, either a USD Rop or ROP network with
# multiple USD ROPs
node = instance[0]
node = hou.node(instance.get("instance_node"))
# Collect any output dependencies that have not been processed yet
# during extraction of other instances

View file

@ -5,6 +5,8 @@ import pyblish.api
from openpype.pipeline import publish
from openpype.hosts.houdini.api.lib import render_rop
import hou
class ExtractVDBCache(publish.Extractor):
@ -15,7 +17,7 @@ class ExtractVDBCache(publish.Extractor):
def process(self, instance):
ropnode = instance[0]
ropnode = hou.node(instance.get("instance_node"))
# Get the filename from the filename parameter
# `.evalParm(parameter)` will make sure all tokens are resolved

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<error id="main">
<title>Scene setting</title>
<description>
## Invalid input node
VDB input must have the same number of VDBs, points, primitives and vertices as output.
</description>
<detail>
### __Detailed Info__ (optional)
A VDB is an inherited type of Prim, holds the following data:
- Primitives: 1
- Points: 1
- Vertices: 1
- VDBs: 1
</detail>
</error>
</root>

View file

@ -2,7 +2,7 @@ import pyblish.api
from openpype.lib import version_up
from openpype.pipeline import registered_host
from openpype.hosts.houdini.api import HoudiniHost
class IncrementCurrentFile(pyblish.api.ContextPlugin):
"""Increment the current file.
@ -20,11 +20,11 @@ class IncrementCurrentFile(pyblish.api.ContextPlugin):
def process(self, context):
# Filename must not have changed since collecting
host = registered_host()
host = registered_host() # type: HoudiniHost
current_file = host.current_file()
assert (
context.data["currentFile"] == current_file
), "Collected filename from current scene name."
new_filepath = version_up(current_file)
host.save(new_filepath)
host.save_workfile(new_filepath)

View file

@ -14,13 +14,13 @@ class SaveCurrentScene(pyblish.api.ContextPlugin):
# Filename must not have changed since collecting
host = registered_host()
current_file = host.current_file()
current_file = host.get_current_workfile()
assert context.data['currentFile'] == current_file, (
"Collected filename from current scene name."
)
if host.has_unsaved_changes():
self.log.info("Saving current file..")
host.save_file(current_file)
self.log.info("Saving current file {}...".format(current_file))
host.save_workfile(current_file)
else:
self.log.debug("No unsaved changes, skipping file save..")

View file

@ -1,47 +0,0 @@
import pyblish.api
from openpype.pipeline.publish import ValidateContentsOrder
class ValidateVDBInputNode(pyblish.api.InstancePlugin):
"""Validate that the node connected to the output node is of type VDB.
Regardless of the amount of VDBs create the output will need to have an
equal amount of VDBs, points, primitives and vertices
A VDB is an inherited type of Prim, holds the following data:
- Primitives: 1
- Points: 1
- Vertices: 1
- VDBs: 1
"""
order = ValidateContentsOrder + 0.1
families = ["vdbcache"]
hosts = ["houdini"]
label = "Validate Input Node (VDB)"
def process(self, instance):
invalid = self.get_invalid(instance)
if invalid:
raise RuntimeError(
"Node connected to the output node is not" "of type VDB!"
)
@classmethod
def get_invalid(cls, instance):
node = instance.data["output_node"]
prims = node.geometry().prims()
nr_of_prims = len(prims)
nr_of_points = len(node.geometry().points())
if nr_of_points != nr_of_prims:
cls.log.error("The number of primitives and points do not match")
return [instance]
for prim in prims:
if prim.numVertices() != 1:
cls.log.error("Found primitive with more than 1 vertex!")
return [instance]

View file

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
import pyblish.api
from collections import defaultdict
from openpype.pipeline.publish import ValidateContentsOrder
from openpype.pipeline import PublishValidationError
class ValidateAbcPrimitiveToDetail(pyblish.api.InstancePlugin):
@ -16,7 +16,7 @@ class ValidateAbcPrimitiveToDetail(pyblish.api.InstancePlugin):
"""
order = ValidateContentsOrder + 0.1
order = pyblish.api.ValidatorOrder + 0.1
families = ["pointcache"]
hosts = ["houdini"]
label = "Validate Primitive to Detail (Abc)"
@ -24,18 +24,26 @@ class ValidateAbcPrimitiveToDetail(pyblish.api.InstancePlugin):
def process(self, instance):
invalid = self.get_invalid(instance)
if invalid:
raise RuntimeError(
"Primitives found with inconsistent primitive "
"to detail attributes. See log."
raise PublishValidationError(
("Primitives found with inconsistent primitive "
"to detail attributes. See log."),
title=self.label
)
@classmethod
def get_invalid(cls, instance):
import hou # noqa
output_node = instance.data.get("output_node")
rop_node = hou.node(instance.data["instance_node"])
if output_node is None:
cls.log.error(
"SOP Output node in '%s' does not exist. "
"Ensure a valid SOP output path is set." % rop_node.path()
)
output = instance.data["output_node"]
return [rop_node.path()]
rop = instance[0]
pattern = rop.parm("prim_to_detail_pattern").eval().strip()
pattern = rop_node.parm("prim_to_detail_pattern").eval().strip()
if not pattern:
cls.log.debug(
"Alembic ROP has no 'Primitive to Detail' pattern. "
@ -43,7 +51,7 @@ class ValidateAbcPrimitiveToDetail(pyblish.api.InstancePlugin):
)
return
build_from_path = rop.parm("build_from_path").eval()
build_from_path = rop_node.parm("build_from_path").eval()
if not build_from_path:
cls.log.debug(
"Alembic ROP has 'Build from Path' disabled. "
@ -51,14 +59,14 @@ class ValidateAbcPrimitiveToDetail(pyblish.api.InstancePlugin):
)
return
path_attr = rop.parm("path_attrib").eval()
path_attr = rop_node.parm("path_attrib").eval()
if not path_attr:
cls.log.error(
"The Alembic ROP node has no Path Attribute"
"value set, but 'Build Hierarchy from Attribute'"
"is enabled."
)
return [rop.path()]
return [rop_node.path()]
# Let's assume each attribute is explicitly named for now and has no
# wildcards for Primitive to Detail. This simplifies the check.
@ -67,7 +75,7 @@ class ValidateAbcPrimitiveToDetail(pyblish.api.InstancePlugin):
# Check if the primitive attribute exists
frame = instance.data.get("frameStart", 0)
geo = output.geometryAtFrame(frame)
geo = output_node.geometryAtFrame(frame)
# If there are no primitives on the start frame then it might be
# something that is emitted over time. As such we can't actually
@ -86,7 +94,7 @@ class ValidateAbcPrimitiveToDetail(pyblish.api.InstancePlugin):
"Geometry Primitives are missing "
"path attribute: `%s`" % path_attr
)
return [output.path()]
return [output_node.path()]
# Ensure at least a single string value is present
if not attrib.strings():
@ -94,7 +102,7 @@ class ValidateAbcPrimitiveToDetail(pyblish.api.InstancePlugin):
"Primitive path attribute has no "
"string values: %s" % path_attr
)
return [output.path()]
return [output_node.path()]
paths = None
for attr in pattern.split(" "):
@ -130,4 +138,4 @@ class ValidateAbcPrimitiveToDetail(pyblish.api.InstancePlugin):
"Path has multiple values: %s (path: %s)"
% (list(values), path)
)
return [output.path()]
return [output_node.path()]

View file

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
import pyblish.api
from openpype.pipeline.publish import ValidateContentsOrder
import hou
class ValidateAlembicROPFaceSets(pyblish.api.InstancePlugin):
"""Validate Face Sets are disabled for extraction to pointcache.
@ -18,14 +17,14 @@ class ValidateAlembicROPFaceSets(pyblish.api.InstancePlugin):
"""
order = ValidateContentsOrder + 0.1
order = pyblish.api.ValidatorOrder + 0.1
families = ["pointcache"]
hosts = ["houdini"]
label = "Validate Alembic ROP Face Sets"
def process(self, instance):
rop = instance[0]
rop = hou.node(instance.data["instance_node"])
facesets = rop.parm("facesets").eval()
# 0 = No Face Sets

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import pyblish.api
from openpype.pipeline.publish import ValidateContentsOrder
from openpype.pipeline import PublishValidationError
import hou
class ValidateAlembicInputNode(pyblish.api.InstancePlugin):
@ -12,7 +13,7 @@ class ValidateAlembicInputNode(pyblish.api.InstancePlugin):
"""
order = ValidateContentsOrder + 0.1
order = pyblish.api.ValidatorOrder + 0.1
families = ["pointcache"]
hosts = ["houdini"]
label = "Validate Input Node (Abc)"
@ -20,18 +21,28 @@ class ValidateAlembicInputNode(pyblish.api.InstancePlugin):
def process(self, instance):
invalid = self.get_invalid(instance)
if invalid:
raise RuntimeError(
"Primitive types found that are not supported"
"for Alembic output."
raise PublishValidationError(
("Primitive types found that are not supported"
"for Alembic output."),
title=self.label
)
@classmethod
def get_invalid(cls, instance):
invalid_prim_types = ["VDB", "Volume"]
node = instance.data["output_node"]
output_node = instance.data.get("output_node")
if not hasattr(node, "geometry"):
if output_node is None:
node = hou.node(instance.data["instance_node"])
cls.log.error(
"SOP Output node in '%s' does not exist. "
"Ensure a valid SOP output path is set." % node.path()
)
return [node.path()]
if not hasattr(output_node, "geometry"):
# In the case someone has explicitly set an Object
# node instead of a SOP node in Geometry context
# then for now we ignore - this allows us to also
@ -40,7 +51,7 @@ class ValidateAlembicInputNode(pyblish.api.InstancePlugin):
return
frame = instance.data.get("frameStart", 0)
geo = node.geometryAtFrame(frame)
geo = output_node.geometryAtFrame(frame)
invalid = False
for prim_type in invalid_prim_types:

View file

@ -1,6 +1,7 @@
import pyblish.api
from openpype.hosts.houdini.api import lib
import hou
class ValidateAnimationSettings(pyblish.api.InstancePlugin):
@ -36,7 +37,7 @@ class ValidateAnimationSettings(pyblish.api.InstancePlugin):
@classmethod
def get_invalid(cls, instance):
node = instance[0]
node = hou.node(instance.get("instance_node"))
# Check trange parm, 0 means Render Current Frame
frame_range = node.evalParm("trange")

View file

@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
import pyblish.api
from openpype.pipeline.publish import ValidateContentsOrder
from openpype.pipeline import PublishValidationError
import hou
class ValidateBypassed(pyblish.api.InstancePlugin):
"""Validate all primitives build hierarchy from attribute when enabled.
@ -11,7 +13,7 @@ class ValidateBypassed(pyblish.api.InstancePlugin):
"""
order = ValidateContentsOrder - 0.1
order = pyblish.api.ValidatorOrder - 0.1
families = ["*"]
hosts = ["houdini"]
label = "Validate ROP Bypass"
@ -26,14 +28,15 @@ class ValidateBypassed(pyblish.api.InstancePlugin):
invalid = self.get_invalid(instance)
if invalid:
rop = invalid[0]
raise RuntimeError(
"ROP node %s is set to bypass, publishing cannot continue.."
% rop.path()
raise PublishValidationError(
("ROP node {} is set to bypass, publishing cannot "
"continue.".format(rop.path())),
title=self.label
)
@classmethod
def get_invalid(cls, instance):
rop = instance[0]
rop = hou.node(instance.get("instance_node"))
if hasattr(rop, "isBypassed") and rop.isBypassed():
return [rop]

View file

@ -1,11 +1,13 @@
# -*- coding: utf-8 -*-
"""Validator plugin for Houdini Camera ROP settings."""
import pyblish.api
from openpype.pipeline.publish import ValidateContentsOrder
from openpype.pipeline import PublishValidationError
class ValidateCameraROP(pyblish.api.InstancePlugin):
"""Validate Camera ROP settings."""
order = ValidateContentsOrder
order = pyblish.api.ValidatorOrder
families = ["camera"]
hosts = ["houdini"]
label = "Camera ROP"
@ -14,30 +16,45 @@ class ValidateCameraROP(pyblish.api.InstancePlugin):
import hou
node = instance[0]
node = hou.node(instance.data.get("instance_node"))
if node.parm("use_sop_path").eval():
raise RuntimeError(
"Alembic ROP for Camera export should not be "
"set to 'Use Sop Path'. Please disable."
raise PublishValidationError(
("Alembic ROP for Camera export should not be "
"set to 'Use Sop Path'. Please disable."),
title=self.label
)
# Get the root and objects parameter of the Alembic ROP node
root = node.parm("root").eval()
objects = node.parm("objects").eval()
assert root, "Root parameter must be set on Alembic ROP"
assert root.startswith("/"), "Root parameter must start with slash /"
assert objects, "Objects parameter must be set on Alembic ROP"
assert len(objects.split(" ")) == 1, "Must have only a single object."
errors = []
if not root:
errors.append("Root parameter must be set on Alembic ROP")
if not root.startswith("/"):
errors.append("Root parameter must start with slash /")
if not objects:
errors.append("Objects parameter must be set on Alembic ROP")
if len(objects.split(" ")) != 1:
errors.append("Must have only a single object.")
if errors:
for error in errors:
self.log.error(error)
raise PublishValidationError(
"Some checks failed, see validator log.",
title=self.label)
# Check if the object exists and is a camera
path = root + "/" + objects
camera = hou.node(path)
if not camera:
raise ValueError("Camera path does not exist: %s" % path)
raise PublishValidationError(
"Camera path does not exist: %s" % path,
title=self.label)
if camera.type().name() != "cam":
raise ValueError(
"Object set in Alembic ROP is not a camera: "
"%s (type: %s)" % (camera, camera.type().name())
)
raise PublishValidationError(
("Object set in Alembic ROP is not a camera: "
"{} (type: {})").format(camera, camera.type().name()),
title=self.label)

View file

@ -1,4 +1,9 @@
# -*- coding: utf-8 -*-
import sys
import pyblish.api
import six
from openpype.pipeline import PublishValidationError
class ValidateCopOutputNode(pyblish.api.InstancePlugin):
@ -20,9 +25,10 @@ class ValidateCopOutputNode(pyblish.api.InstancePlugin):
invalid = self.get_invalid(instance)
if invalid:
raise RuntimeError(
"Output node(s) `%s` are incorrect. "
"See plug-in log for details." % invalid
raise PublishValidationError(
("Output node(s) `{}` are incorrect. "
"See plug-in log for details.").format(invalid),
title=self.label
)
@classmethod
@ -30,10 +36,19 @@ class ValidateCopOutputNode(pyblish.api.InstancePlugin):
import hou
output_node = instance.data["output_node"]
try:
output_node = instance.data["output_node"]
except KeyError:
six.reraise(
PublishValidationError,
PublishValidationError(
"Can't determine COP output node.",
title=cls.__name__),
sys.exc_info()[2]
)
if output_node is None:
node = instance[0]
node = hou.node(instance.get("instance_node"))
cls.log.error(
"COP Output node in '%s' does not exist. "
"Ensure a valid COP output path is set." % node.path()
@ -54,7 +69,8 @@ class ValidateCopOutputNode(pyblish.api.InstancePlugin):
# For the sake of completeness also assert the category type
# is Cop2 to avoid potential edge case scenarios even though
# the isinstance check above should be stricter than this category
assert output_node.type().category().name() == "Cop2", (
"Output node %s is not of category Cop2. This is a bug.."
% output_node.path()
)
if output_node.type().category().name() != "Cop2":
raise PublishValidationError(
("Output node %s is not of category Cop2. "
"This is a bug...").format(output_node.path()),
title=cls.label)

View file

@ -1,7 +1,11 @@
# -*- coding: utf-8 -*-
import os
import pyblish.api
from openpype.hosts.houdini.api import lib
from openpype.pipeline import PublishValidationError
import hou
class ValidateFileExtension(pyblish.api.InstancePlugin):
@ -29,15 +33,16 @@ class ValidateFileExtension(pyblish.api.InstancePlugin):
invalid = self.get_invalid(instance)
if invalid:
raise RuntimeError(
"ROP node has incorrect " "file extension: %s" % invalid
raise PublishValidationError(
"ROP node has incorrect file extension: {}".format(invalid),
title=self.label
)
@classmethod
def get_invalid(cls, instance):
# Get ROP node from instance
node = instance[0]
node = hou.node(instance.data["instance_node"])
# Create lookup for current family in instance
families = []
@ -53,7 +58,9 @@ class ValidateFileExtension(pyblish.api.InstancePlugin):
for family in families:
extension = cls.family_extensions.get(family, None)
if extension is None:
raise RuntimeError("Unsupported family: %s" % family)
raise PublishValidationError(
"Unsupported family: {}".format(family),
title=cls.label)
if output_extension != extension:
return [node.path()]

View file

@ -1,6 +1,7 @@
import pyblish.api
from openpype.hosts.houdini.api import lib
import hou
class ValidateFrameToken(pyblish.api.InstancePlugin):
@ -36,7 +37,7 @@ class ValidateFrameToken(pyblish.api.InstancePlugin):
@classmethod
def get_invalid(cls, instance):
node = instance[0]
node = hou.node(instance.get("instance_node"))
# Check trange parm, 0 means Render Current Frame
frame_range = node.evalParm("trange")

View file

@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
import pyblish.api
from openpype.pipeline import PublishValidationError
class ValidateHoudiniCommercialLicense(pyblish.api.InstancePlugin):
@ -24,7 +26,7 @@ class ValidateHoudiniCommercialLicense(pyblish.api.InstancePlugin):
license = hou.licenseCategory()
if license != hou.licenseCategoryType.Commercial:
raise RuntimeError(
"USD Publishing requires a full Commercial "
"license. You are on: %s" % license
)
raise PublishValidationError(
("USD Publishing requires a full Commercial "
"license. You are on: {}").format(license),
title=self.label)

View file

@ -1,11 +1,12 @@
# -*- coding: utf-8 -*-
import pyblish.api
from openpype.pipeline.publish import ValidateContentsOrder
from openpype.pipeline import PublishValidationError
class ValidateIntermediateDirectoriesChecked(pyblish.api.InstancePlugin):
"""Validate Create Intermediate Directories is enabled on ROP node."""
order = ValidateContentsOrder
order = pyblish.api.ValidatorOrder
families = ["pointcache", "camera", "vdbcache"]
hosts = ["houdini"]
label = "Create Intermediate Directories Checked"
@ -14,10 +15,10 @@ class ValidateIntermediateDirectoriesChecked(pyblish.api.InstancePlugin):
invalid = self.get_invalid(instance)
if invalid:
raise RuntimeError(
"Found ROP node with Create Intermediate "
"Directories turned off: %s" % invalid
)
raise PublishValidationError(
("Found ROP node with Create Intermediate "
"Directories turned off: {}".format(invalid)),
title=self.label)
@classmethod
def get_invalid(cls, instance):

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import pyblish.api
import hou
from openpype.pipeline.publish import ValidateContentsOrder
from openpype.pipeline import PublishValidationError
def cook_in_range(node, start, end):
@ -28,7 +29,7 @@ def get_errors(node):
class ValidateNoErrors(pyblish.api.InstancePlugin):
"""Validate the Instance has no current cooking errors."""
order = ValidateContentsOrder
order = pyblish.api.ValidatorOrder
hosts = ["houdini"]
label = "Validate no errors"
@ -37,7 +38,7 @@ class ValidateNoErrors(pyblish.api.InstancePlugin):
validate_nodes = []
if len(instance) > 0:
validate_nodes.append(instance[0])
validate_nodes.append(hou.node(instance.get("instance_node")))
output_node = instance.data.get("output_node")
if output_node:
validate_nodes.append(output_node)
@ -62,4 +63,6 @@ class ValidateNoErrors(pyblish.api.InstancePlugin):
errors = get_errors(node)
if errors:
self.log.error(errors)
raise RuntimeError("Node has errors: %s" % node.path())
raise PublishValidationError(
"Node has errors: {}".format(node.path()),
title=self.label)

View file

@ -1,5 +1,8 @@
# -*- coding: utf-8 -*-
import pyblish.api
from openpype.pipeline.publish import ValidateContentsOrder
from openpype.pipeline import PublishValidationError
import hou
class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin):
@ -19,19 +22,26 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin):
def process(self, instance):
invalid = self.get_invalid(instance)
if invalid:
raise RuntimeError(
"See log for details. " "Invalid nodes: {0}".format(invalid)
raise PublishValidationError(
"See log for details. " "Invalid nodes: {0}".format(invalid),
title=self.label
)
@classmethod
def get_invalid(cls, instance):
import hou
output_node = instance.data.get("output_node")
rop_node = hou.node(instance.data["instance_node"])
output = instance.data["output_node"]
if output_node is None:
cls.log.error(
"SOP Output node in '%s' does not exist. "
"Ensure a valid SOP output path is set." % rop_node.path()
)
rop = instance[0]
build_from_path = rop.parm("build_from_path").eval()
return [rop_node.path()]
build_from_path = rop_node.parm("build_from_path").eval()
if not build_from_path:
cls.log.debug(
"Alembic ROP has 'Build from Path' disabled. "
@ -39,20 +49,20 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin):
)
return
path_attr = rop.parm("path_attrib").eval()
path_attr = rop_node.parm("path_attrib").eval()
if not path_attr:
cls.log.error(
"The Alembic ROP node has no Path Attribute"
"value set, but 'Build Hierarchy from Attribute'"
"is enabled."
)
return [rop.path()]
return [rop_node.path()]
cls.log.debug("Checking for attribute: %s" % path_attr)
# Check if the primitive attribute exists
frame = instance.data.get("frameStart", 0)
geo = output.geometryAtFrame(frame)
geo = output_node.geometryAtFrame(frame)
# If there are no primitives on the current frame then we can't
# check whether the path names are correct. So we'll just issue a
@ -73,7 +83,7 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin):
"Geometry Primitives are missing "
"path attribute: `%s`" % path_attr
)
return [output.path()]
return [output_node.path()]
# Ensure at least a single string value is present
if not attrib.strings():
@ -81,7 +91,7 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin):
"Primitive path attribute has no "
"string values: %s" % path_attr
)
return [output.path()]
return [output_node.path()]
paths = geo.primStringAttribValues(path_attr)
# Ensure all primitives are set to a valid path
@ -93,4 +103,4 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin):
"Prims have no value for attribute `%s` "
"(%s of %s prims)" % (path_attr, len(invalid_prims), num_prims)
)
return [output.path()]
return [output_node.path()]

View file

@ -1,7 +1,9 @@
# -*-coding: utf-8 -*-
import pyblish.api
from openpype.hosts.houdini.api import lib
from openpype.pipeline.publish import RepairContextAction
from openpype.pipeline import PublishValidationError
import hou
@ -27,17 +29,24 @@ class ValidateRemotePublishOutNode(pyblish.api.ContextPlugin):
# We ensure it's a shell node and that it has the pre-render script
# set correctly. Plus the shell script it will trigger should be
# completely empty (doing nothing)
assert node.type().name() == "shell", "Must be shell ROP node"
assert node.parm("command").eval() == "", "Must have no command"
assert not node.parm("shellexec").eval(), "Must not execute in shell"
assert (
node.parm("prerender").eval() == cmd
), "REMOTE_PUBLISH node does not have correct prerender script."
assert (
node.parm("lprerender").eval() == "python"
), "REMOTE_PUBLISH node prerender script type not set to 'python'"
if node.type().name() != "shell":
self.raise_error("Must be shell ROP node")
if node.parm("command").eval() != "":
self.raise_error("Must have no command")
if node.parm("shellexec").eval():
self.raise_error("Must not execute in shell")
if node.parm("prerender").eval() != cmd:
self.raise_error(("REMOTE_PUBLISH node does not have "
"correct prerender script."))
if node.parm("lprerender").eval() != "python":
self.raise_error(("REMOTE_PUBLISH node prerender script "
"type not set to 'python'"))
@classmethod
def repair(cls, context):
"""(Re)create the node if it fails to pass validation."""
lib.create_remote_publish_node(force=True)
def raise_error(self, message):
self.log.error(message)
raise PublishValidationError(message, title=self.label)

View file

@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
import pyblish.api
import hou
from openpype.pipeline.publish import RepairContextAction
from openpype.pipeline import PublishValidationError
class ValidateRemotePublishEnabled(pyblish.api.ContextPlugin):
@ -18,10 +20,12 @@ class ValidateRemotePublishEnabled(pyblish.api.ContextPlugin):
node = hou.node("/out/REMOTE_PUBLISH")
if not node:
raise RuntimeError("Missing REMOTE_PUBLISH node.")
raise PublishValidationError(
"Missing REMOTE_PUBLISH node.", title=self.label)
if node.isBypassed():
raise RuntimeError("REMOTE_PUBLISH must not be bypassed.")
raise PublishValidationError(
"REMOTE_PUBLISH must not be bypassed.", title=self.label)
@classmethod
def repair(cls, context):
@ -29,7 +33,8 @@ class ValidateRemotePublishEnabled(pyblish.api.ContextPlugin):
node = hou.node("/out/REMOTE_PUBLISH")
if not node:
raise RuntimeError("Missing REMOTE_PUBLISH node.")
raise PublishValidationError(
"Missing REMOTE_PUBLISH node.", title=cls.label)
cls.log.info("Disabling bypass on /out/REMOTE_PUBLISH")
node.bypass(False)

View file

@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
import pyblish.api
from openpype.pipeline import PublishValidationError
class ValidateSopOutputNode(pyblish.api.InstancePlugin):
@ -22,9 +24,9 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin):
invalid = self.get_invalid(instance)
if invalid:
raise RuntimeError(
"Output node(s) `%s` are incorrect. "
"See plug-in log for details." % invalid
raise PublishValidationError(
"Output node(s) are incorrect",
title="Invalid output node(s)"
)
@classmethod
@ -32,10 +34,10 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin):
import hou
output_node = instance.data["output_node"]
output_node = instance.data.get("output_node")
if output_node is None:
node = instance[0]
node = hou.node(instance.data["instance_node"])
cls.log.error(
"SOP Output node in '%s' does not exist. "
"Ensure a valid SOP output path is set." % node.path()
@ -56,10 +58,11 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin):
# For the sake of completeness also assert the category type
# is Sop to avoid potential edge case scenarios even though
# the isinstance check above should be stricter than this category
assert output_node.type().category().name() == "Sop", (
"Output node %s is not of category Sop. This is a bug.."
% output_node.path()
)
if output_node.type().category().name() != "Sop":
raise PublishValidationError(
("Output node {} is not of category Sop. "
"This is a bug.").format(output_node.path()),
title=cls.label)
# Ensure the node is cooked and succeeds to cook so we can correctly
# check for its geometry data.

View file

@ -1,6 +1,10 @@
# -*- coding: utf-8 -*-
import pyblish.api
import openpype.hosts.houdini.api.usd as hou_usdlib
from openpype.pipeline import PublishValidationError
import hou
class ValidateUSDLayerPathBackslashes(pyblish.api.InstancePlugin):
@ -24,7 +28,7 @@ class ValidateUSDLayerPathBackslashes(pyblish.api.InstancePlugin):
def process(self, instance):
rop = instance[0]
rop = hou.node(instance.get("instance_node"))
lop_path = hou_usdlib.get_usd_rop_loppath(rop)
stage = lop_path.stage(apply_viewport_overrides=False)
@ -44,7 +48,7 @@ class ValidateUSDLayerPathBackslashes(pyblish.api.InstancePlugin):
invalid.append(layer)
if invalid:
raise RuntimeError(
raise PublishValidationError((
"Loaded layers have backslashes. "
"This is invalid for HUSK USD rendering."
)
"This is invalid for HUSK USD rendering."),
title=self.label)

View file

@ -1,10 +1,13 @@
# -*- coding: utf-8 -*-
import pyblish.api
import openpype.hosts.houdini.api.usd as hou_usdlib
from openpype.pipeline import PublishValidationError
from pxr import UsdShade, UsdRender, UsdLux
import hou
def fullname(o):
"""Get fully qualified class name"""
@ -37,7 +40,7 @@ class ValidateUsdModel(pyblish.api.InstancePlugin):
def process(self, instance):
rop = instance[0]
rop = hou.node(instance.get("instance_node"))
lop_path = hou_usdlib.get_usd_rop_loppath(rop)
stage = lop_path.stage(apply_viewport_overrides=False)
@ -55,7 +58,8 @@ class ValidateUsdModel(pyblish.api.InstancePlugin):
if invalid:
prim_paths = sorted([str(prim.GetPath()) for prim in invalid])
raise RuntimeError("Found invalid primitives: %s" % prim_paths)
raise PublishValidationError(
"Found invalid primitives: {}".format(prim_paths))
class ValidateUsdShade(ValidateUsdModel):

View file

@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
import pyblish.api
from openpype.pipeline import PublishValidationError
class ValidateUSDOutputNode(pyblish.api.InstancePlugin):
@ -20,9 +22,10 @@ class ValidateUSDOutputNode(pyblish.api.InstancePlugin):
invalid = self.get_invalid(instance)
if invalid:
raise RuntimeError(
"Output node(s) `%s` are incorrect. "
"See plug-in log for details." % invalid
raise PublishValidationError(
("Output node(s) `{}` are incorrect. "
"See plug-in log for details.").format(invalid),
title=self.label
)
@classmethod
@ -33,7 +36,7 @@ class ValidateUSDOutputNode(pyblish.api.InstancePlugin):
output_node = instance.data["output_node"]
if output_node is None:
node = instance[0]
node = hou.node(instance.get("instance_node"))
cls.log.error(
"USD node '%s' LOP path does not exist. "
"Ensure a valid LOP path is set." % node.path()

View file

@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
import os
import pyblish.api
import os
from openpype.pipeline import PublishValidationError
class ValidateUSDRenderProductNames(pyblish.api.InstancePlugin):
@ -28,4 +30,5 @@ class ValidateUSDRenderProductNames(pyblish.api.InstancePlugin):
if invalid:
for message in invalid:
self.log.error(message)
raise RuntimeError("USD Render Paths are invalid.")
raise PublishValidationError(
"USD Render Paths are invalid.", title=self.label)

View file

@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
import pyblish.api
import openpype.hosts.houdini.api.usd as hou_usdlib
from openpype.pipeline import PublishValidationError
class ValidateUsdSetDress(pyblish.api.InstancePlugin):
@ -20,8 +22,9 @@ class ValidateUsdSetDress(pyblish.api.InstancePlugin):
def process(self, instance):
from pxr import UsdGeom
import hou
rop = instance[0]
rop = hou.node(instance.get("instance_node"))
lop_path = hou_usdlib.get_usd_rop_loppath(rop)
stage = lop_path.stage(apply_viewport_overrides=False)
@ -47,8 +50,9 @@ class ValidateUsdSetDress(pyblish.api.InstancePlugin):
invalid.append(node)
if invalid:
raise RuntimeError(
raise PublishValidationError((
"SetDress contains local geometry. "
"This is not allowed, it must be an assembly "
"of referenced assets."
"of referenced assets."),
title=self.label
)

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
import re
import pyblish.api
@ -5,6 +6,7 @@ import pyblish.api
from openpype.client import get_subset_by_name
from openpype.pipeline import legacy_io
from openpype.pipeline.publish import ValidateContentsOrder
from openpype.pipeline import PublishValidationError
class ValidateUSDShadeModelExists(pyblish.api.InstancePlugin):
@ -32,7 +34,8 @@ class ValidateUSDShadeModelExists(pyblish.api.InstancePlugin):
project_name, model_subset, asset_doc["_id"], fields=["_id"]
)
if not subset_doc:
raise RuntimeError(
"USD Model subset not found: "
"%s (%s)" % (model_subset, asset_name)
raise PublishValidationError(
("USD Model subset not found: "
"{} ({})").format(model_subset, asset_name),
title=self.label
)

View file

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import pyblish.api
from openpype.pipeline.publish import ValidateContentsOrder
from openpype.pipeline import PublishValidationError
import hou
@ -12,14 +13,14 @@ class ValidateUsdShadeWorkspace(pyblish.api.InstancePlugin):
"""
order = ValidateContentsOrder
order = pyblish.api.ValidatorOrder
hosts = ["houdini"]
families = ["usdShade"]
label = "USD Shade Workspace"
def process(self, instance):
rop = instance[0]
rop = hou.node(instance.get("instance_node"))
workspace = rop.parent()
definition = workspace.type().definition()
@ -39,13 +40,14 @@ class ValidateUsdShadeWorkspace(pyblish.api.InstancePlugin):
if node_type != other_node_type:
continue
# Get highest version
# Get the highest version
highest = max(highest, other_version)
if version != highest:
raise RuntimeError(
"Shading Workspace is not the latest version."
" Found %s. Latest is %s." % (version, highest)
raise PublishValidationError(
("Shading Workspace is not the latest version."
" Found {}. Latest is {}.").format(version, highest),
title=self.label
)
# There were some issues with the editable node not having the right
@ -56,8 +58,9 @@ class ValidateUsdShadeWorkspace(pyblish.api.InstancePlugin):
)
rop_value = rop.parm("lopoutput").rawValue()
if rop_value != value:
raise RuntimeError(
"Shading Workspace has invalid 'lopoutput'"
" parameter value. The Shading Workspace"
" needs to be reset to its default values."
raise PublishValidationError(
("Shading Workspace has invalid 'lopoutput'"
" parameter value. The Shading Workspace"
" needs to be reset to its default values."),
title=self.label
)

View file

@ -1,5 +1,8 @@
# -*- coding: utf-8 -*-
import pyblish.api
from openpype.pipeline.publish import ValidateContentsOrder
from openpype.pipeline import (
PublishValidationError
)
class ValidateVDBInputNode(pyblish.api.InstancePlugin):
@ -16,7 +19,7 @@ class ValidateVDBInputNode(pyblish.api.InstancePlugin):
"""
order = ValidateContentsOrder + 0.1
order = pyblish.api.ValidatorOrder + 0.1
families = ["vdbcache"]
hosts = ["houdini"]
label = "Validate Input Node (VDB)"
@ -24,8 +27,10 @@ class ValidateVDBInputNode(pyblish.api.InstancePlugin):
def process(self, instance):
invalid = self.get_invalid(instance)
if invalid:
raise RuntimeError(
"Node connected to the output node is not" "of type VDB!"
raise PublishValidationError(
self,
"Node connected to the output node is not of type VDB",
title=self.label
)
@classmethod

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import pyblish.api
import hou
from openpype.pipeline.publish import ValidateContentsOrder
from openpype.pipeline import PublishValidationError
class ValidateVDBOutputNode(pyblish.api.InstancePlugin):
@ -17,7 +18,7 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin):
"""
order = ValidateContentsOrder + 0.1
order = pyblish.api.ValidatorOrder + 0.1
families = ["vdbcache"]
hosts = ["houdini"]
label = "Validate Output Node (VDB)"
@ -25,8 +26,9 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin):
def process(self, instance):
invalid = self.get_invalid(instance)
if invalid:
raise RuntimeError(
"Node connected to the output node is not" " of type VDB!"
raise PublishValidationError(
"Node connected to the output node is not" " of type VDB!",
title=self.label
)
@classmethod
@ -36,7 +38,7 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin):
if node is None:
cls.log.error(
"SOP path is not correctly set on "
"ROP node '%s'." % instance[0].path()
"ROP node '%s'." % instance.get("instance_node")
)
return [instance]

View file

@ -1,11 +1,17 @@
# -*- coding: utf-8 -*-
import pyblish.api
import hou
from openpype.pipeline import (
PublishValidationError,
OptionalPyblishPluginMixin
)
from openpype.pipeline.publish import RepairAction
from openpype.pipeline.publish import RepairAction
class ValidateWorkfilePaths(pyblish.api.InstancePlugin):
class ValidateWorkfilePaths(
pyblish.api.InstancePlugin, OptionalPyblishPluginMixin):
"""Validate workfile paths so they are absolute."""
order = pyblish.api.ValidatorOrder
@ -19,6 +25,8 @@ class ValidateWorkfilePaths(pyblish.api.InstancePlugin):
prohibited_vars = ["$HIP", "$JOB"]
def process(self, instance):
if not self.is_active(instance.data):
return
invalid = self.get_invalid()
self.log.info(
"node types to check: {}".format(", ".join(self.node_types)))
@ -30,15 +38,16 @@ class ValidateWorkfilePaths(pyblish.api.InstancePlugin):
self.log.error(
"{}: {}".format(param.path(), param.unexpandedString()))
raise RuntimeError("Invalid paths found")
raise PublishValidationError(
"Invalid paths found", title=self.label)
@classmethod
def get_invalid(cls):
invalid = []
for param, _ in hou.fileReferences():
if param is None:
# it might return None for some reason
if not param:
continue
# skip nodes we are not interested in
if param.node().type().name() not in cls.node_types:
continue

View file

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<mainMenu>
<menuBar>
<subMenu id="avalon_menu">
<subMenu id="openpype_menu">
<label>OpenPype</label>
<scriptItem id="avalon_create">
<scriptItem id="openpype_create">
<label>Create...</label>
<scriptCode><![CDATA[
import hou
@ -14,7 +14,7 @@ host_tools.show_creator(parent)
]]></scriptCode>
</scriptItem>
<scriptItem id="avalon_load">
<scriptItem id="openpype_load">
<label>Load...</label>
<scriptCode><![CDATA[
import hou
@ -30,11 +30,11 @@ host_tools.show_loader(parent=parent, use_context=True)
import hou
from openpype.tools.utils import host_tools
parent = hou.qt.mainWindow()
host_tools.show_publish(parent)
host_tools.show_publisher(parent)
]]></scriptCode>
</scriptItem>
<scriptItem id="avalon_manage">
<scriptItem id="openpype_manage">
<label>Manage...</label>
<scriptCode><![CDATA[
import hou

View file

@ -1,10 +1,12 @@
# -*- coding: utf-8 -*-
"""OpenPype startup script."""
from openpype.pipeline import install_host
from openpype.hosts.houdini import api
from openpype.hosts.houdini.api import HoudiniHost
def main():
print("Installing OpenPype ...")
install_host(api)
install_host(HoudiniHost())
main()

View file

@ -1,10 +1,12 @@
# -*- coding: utf-8 -*-
"""OpenPype startup script."""
from openpype.pipeline import install_host
from openpype.hosts.houdini import api
from openpype.hosts.houdini.api import HoudiniHost
def main():
print("Installing OpenPype ...")
install_host(api)
install_host(HoudiniHost())
main()

View file

@ -1,10 +1,12 @@
# -*- coding: utf-8 -*-
"""OpenPype startup script."""
from openpype.pipeline import install_host
from openpype.hosts.houdini import api
from openpype.hosts.houdini.api import HoudiniHost
def main():
print("Installing OpenPype ...")
install_host(api)
install_host(HoudiniHost())
main()

View file

@ -0,0 +1,10 @@
from .addon import (
MaxAddon,
MAX_HOST_DIR,
)
__all__ = (
"MaxAddon",
"MAX_HOST_DIR",
)

View file

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
import os
from openpype.modules import OpenPypeModule, IHostAddon
MAX_HOST_DIR = os.path.dirname(os.path.abspath(__file__))
class MaxAddon(OpenPypeModule, IHostAddon):
name = "max"
host_name = "max"
def initialize(self, module_settings):
self.enabled = True
def get_workfile_extensions(self):
return [".max"]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
"""Public API for 3dsmax"""
from .pipeline import (
MaxHost,
)
from .lib import (
maintained_selection,
lsattr,
get_all_children
)
__all__ = [
"MaxHost",
"maintained_selection",
"lsattr",
"get_all_children"
]

View file

@ -0,0 +1,122 @@
# -*- coding: utf-8 -*-
"""Library of functions useful for 3dsmax pipeline."""
import json
import six
from pymxs import runtime as rt
from typing import Union
import contextlib
JSON_PREFIX = "JSON::"
def imprint(node_name: str, data: dict) -> bool:
node = rt.getNodeByName(node_name)
if not node:
return False
for k, v in data.items():
if isinstance(v, (dict, list)):
rt.setUserProp(node, k, f'{JSON_PREFIX}{json.dumps(v)}')
else:
rt.setUserProp(node, k, v)
return True
def lsattr(
attr: str,
value: Union[str, None] = None,
root: Union[str, None] = None) -> list:
"""List nodes having attribute with specified value.
Args:
attr (str): Attribute name to match.
value (str, Optional): Value to match, of omitted, all nodes
with specified attribute are returned no matter of value.
root (str, Optional): Root node name. If omitted, scene root is used.
Returns:
list of nodes.
"""
root = rt.rootnode if root is None else rt.getNodeByName(root)
def output_node(node, nodes):
nodes.append(node)
for child in node.Children:
output_node(child, nodes)
nodes = []
output_node(root, nodes)
return [
n for n in nodes
if rt.getUserProp(n, attr) == value
] if value else [
n for n in nodes
if rt.getUserProp(n, attr)
]
def read(container) -> dict:
data = {}
props = rt.getUserPropBuffer(container)
# this shouldn't happen but let's guard against it anyway
if not props:
return data
for line in props.split("\r\n"):
try:
key, value = line.split("=")
except ValueError:
# if the line cannot be split we can't really parse it
continue
value = value.strip()
if isinstance(value.strip(), six.string_types) and \
value.startswith(JSON_PREFIX):
try:
value = json.loads(value[len(JSON_PREFIX):])
except json.JSONDecodeError:
# not a json
pass
data[key.strip()] = value
data["instance_node"] = container.name
return data
@contextlib.contextmanager
def maintained_selection():
previous_selection = rt.getCurrentSelection()
try:
yield
finally:
if previous_selection:
rt.select(previous_selection)
else:
rt.select()
def get_all_children(parent, node_type=None):
"""Handy function to get all the children of a given node
Args:
parent (3dsmax Node1): Node to get all children of.
node_type (None, runtime.class): give class to check for
e.g. rt.FFDBox/rt.GeometryClass etc.
Returns:
list: list of all children of the parent node
"""
def list_children(node):
children = []
for c in node.Children:
children.append(c)
children = children + list_children(c)
return children
child_list = list_children(parent)
return ([x for x in child_list if rt.superClassOf(x) == node_type]
if node_type else child_list)

View file

@ -0,0 +1,130 @@
# -*- coding: utf-8 -*-
"""3dsmax menu definition of OpenPype."""
from Qt import QtWidgets, QtCore
from pymxs import runtime as rt
from openpype.tools.utils import host_tools
class OpenPypeMenu(object):
"""Object representing OpenPype menu.
This is using "hack" to inject itself before "Help" menu of 3dsmax.
For some reason `postLoadingMenus` event doesn't fire, and main menu
if probably re-initialized by menu templates, se we wait for at least
1 event Qt event loop before trying to insert.
"""
def __init__(self):
super().__init__()
self.main_widget = self.get_main_widget()
self.menu = None
timer = QtCore.QTimer()
# set number of event loops to wait.
timer.setInterval(1)
timer.timeout.connect(self._on_timer)
timer.start()
self._timer = timer
self._counter = 0
def _on_timer(self):
if self._counter < 1:
self._counter += 1
return
self._counter = 0
self._timer.stop()
self.build_openpype_menu()
@staticmethod
def get_main_widget():
"""Get 3dsmax main window."""
return QtWidgets.QWidget.find(rt.windows.getMAXHWND())
def get_main_menubar(self) -> QtWidgets.QMenuBar:
"""Get main Menubar by 3dsmax main window."""
return list(self.main_widget.findChildren(QtWidgets.QMenuBar))[0]
def get_or_create_openpype_menu(
self, name: str = "&OpenPype",
before: str = "&Help") -> QtWidgets.QAction:
"""Create OpenPype menu.
Args:
name (str, Optional): OpenPypep menu name.
before (str, Optional): Name of the 3dsmax main menu item to
add OpenPype menu before.
Returns:
QtWidgets.QAction: OpenPype menu action.
"""
if self.menu is not None:
return self.menu
menu_bar = self.get_main_menubar()
menu_items = menu_bar.findChildren(
QtWidgets.QMenu, options=QtCore.Qt.FindDirectChildrenOnly)
help_action = None
for item in menu_items:
if name in item.title():
# we already have OpenPype menu
return item
if before in item.title():
help_action = item.menuAction()
op_menu = QtWidgets.QMenu("&OpenPype")
menu_bar.insertMenu(help_action, op_menu)
self.menu = op_menu
return op_menu
def build_openpype_menu(self) -> QtWidgets.QAction:
"""Build items in OpenPype menu."""
openpype_menu = self.get_or_create_openpype_menu()
load_action = QtWidgets.QAction("Load...", openpype_menu)
load_action.triggered.connect(self.load_callback)
openpype_menu.addAction(load_action)
publish_action = QtWidgets.QAction("Publish...", openpype_menu)
publish_action.triggered.connect(self.publish_callback)
openpype_menu.addAction(publish_action)
manage_action = QtWidgets.QAction("Manage...", openpype_menu)
manage_action.triggered.connect(self.manage_callback)
openpype_menu.addAction(manage_action)
library_action = QtWidgets.QAction("Library...", openpype_menu)
library_action.triggered.connect(self.library_callback)
openpype_menu.addAction(library_action)
openpype_menu.addSeparator()
workfiles_action = QtWidgets.QAction("Work Files...", openpype_menu)
workfiles_action.triggered.connect(self.workfiles_callback)
openpype_menu.addAction(workfiles_action)
return openpype_menu
def load_callback(self):
"""Callback to show Loader tool."""
host_tools.show_loader(parent=self.main_widget)
def publish_callback(self):
"""Callback to show Publisher tool."""
host_tools.show_publisher(parent=self.main_widget)
def manage_callback(self):
"""Callback to show Scene Manager/Inventory tool."""
host_tools.show_subset_manager(parent=self.main_widget)
def library_callback(self):
"""Callback to show Library Loader tool."""
host_tools.show_library_loader(parent=self.main_widget)
def workfiles_callback(self):
"""Callback to show Workfiles tool."""
host_tools.show_workfiles(parent=self.main_widget)

View file

@ -0,0 +1,145 @@
# -*- coding: utf-8 -*-
"""Pipeline tools for OpenPype Houdini integration."""
import os
import logging
import json
from openpype.host import HostBase, IWorkfileHost, ILoadHost, INewPublisher
import pyblish.api
from openpype.pipeline import (
register_creator_plugin_path,
register_loader_plugin_path,
AVALON_CONTAINER_ID,
)
from openpype.hosts.max.api.menu import OpenPypeMenu
from openpype.hosts.max.api import lib
from openpype.hosts.max import MAX_HOST_DIR
from pymxs import runtime as rt # noqa
log = logging.getLogger("openpype.hosts.max")
PLUGINS_DIR = os.path.join(MAX_HOST_DIR, "plugins")
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
class MaxHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher):
name = "max"
menu = None
def __init__(self):
super(MaxHost, self).__init__()
self._op_events = {}
self._has_been_setup = False
def install(self):
pyblish.api.register_host("max")
pyblish.api.register_plugin_path(PUBLISH_PATH)
register_loader_plugin_path(LOAD_PATH)
register_creator_plugin_path(CREATE_PATH)
# self._register_callbacks()
self.menu = OpenPypeMenu()
self._has_been_setup = True
def has_unsaved_changes(self):
# TODO: how to get it from 3dsmax?
return True
def get_workfile_extensions(self):
return [".max"]
def save_workfile(self, dst_path=None):
rt.saveMaxFile(dst_path)
return dst_path
def open_workfile(self, filepath):
rt.checkForSave()
rt.loadMaxFile(filepath)
return filepath
def get_current_workfile(self):
return os.path.join(rt.maxFilePath, rt.maxFileName)
def get_containers(self):
return ls()
def _register_callbacks(self):
rt.callbacks.removeScripts(id=rt.name("OpenPypeCallbacks"))
rt.callbacks.addScript(
rt.Name("postLoadingMenus"),
self._deferred_menu_creation, id=rt.Name('OpenPypeCallbacks'))
def _deferred_menu_creation(self):
self.log.info("Building menu ...")
self.menu = OpenPypeMenu()
@staticmethod
def create_context_node():
"""Helper for creating context holding node."""
root_scene = rt.rootScene
create_attr_script = ("""
attributes "OpenPypeContext"
(
parameters main rollout:params
(
context type: #string
)
rollout params "OpenPype Parameters"
(
editText editTextContext "Context" type: #string
)
)
""")
attr = rt.execute(create_attr_script)
rt.custAttributes.add(root_scene, attr)
return root_scene.OpenPypeContext.context
def update_context_data(self, data, changes):
try:
_ = rt.rootScene.OpenPypeContext.context
except AttributeError:
# context node doesn't exists
self.create_context_node()
rt.rootScene.OpenPypeContext.context = json.dumps(data)
def get_context_data(self):
try:
context = rt.rootScene.OpenPypeContext.context
except AttributeError:
# context node doesn't exists
context = self.create_context_node()
if not context:
context = "{}"
return json.loads(context)
def save_file(self, dst_path=None):
# Force forwards slashes to avoid segfault
dst_path = dst_path.replace("\\", "/")
rt.saveMaxFile(dst_path)
def ls() -> list:
"""Get all OpenPype instances."""
objs = rt.objects
containers = [
obj for obj in objs
if rt.getUserProp(obj, "id") == AVALON_CONTAINER_ID
]
for container in sorted(containers, key=lambda name: container.name):
yield lib.read(container)

View file

@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
"""3dsmax specific Avalon/Pyblish plugin definitions."""
from pymxs import runtime as rt
import six
from abc import ABCMeta
from openpype.pipeline import (
CreatorError,
Creator,
CreatedInstance
)
from openpype.lib import BoolDef
from .lib import imprint, read, lsattr
class OpenPypeCreatorError(CreatorError):
pass
class MaxCreatorBase(object):
@staticmethod
def cache_subsets(shared_data):
if shared_data.get("max_cached_subsets") is None:
shared_data["max_cached_subsets"] = {}
cached_instances = lsattr("id", "pyblish.avalon.instance")
for i in cached_instances:
creator_id = rt.getUserProp(i, "creator_identifier")
if creator_id not in shared_data["max_cached_subsets"]:
shared_data["max_cached_subsets"][creator_id] = [i.name]
else:
shared_data[
"max_cached_subsets"][creator_id].append(i.name) # noqa
return shared_data
@staticmethod
def create_instance_node(node_name: str, parent: str = ""):
parent_node = rt.getNodeByName(parent) if parent else rt.rootScene
if not parent_node:
raise OpenPypeCreatorError(f"Specified parent {parent} not found")
container = rt.container(name=node_name)
container.Parent = parent_node
return container
@six.add_metaclass(ABCMeta)
class MaxCreator(Creator, MaxCreatorBase):
selected_nodes = []
def create(self, subset_name, instance_data, pre_create_data):
if pre_create_data.get("use_selection"):
self.selected_nodes = rt.getCurrentSelection()
instance_node = self.create_instance_node(subset_name)
instance_data["instance_node"] = instance_node.name
instance = CreatedInstance(
self.family,
subset_name,
instance_data,
self
)
for node in self.selected_nodes:
node.Parent = instance_node
self._add_instance_to_context(instance)
imprint(instance_node.name, instance.data_to_store())
return instance
def collect_instances(self):
self.cache_subsets(self.collection_shared_data)
for instance in self.collection_shared_data[
"max_cached_subsets"].get(self.identifier, []):
created_instance = CreatedInstance.from_existing(
read(rt.getNodeByName(instance)), self
)
self._add_instance_to_context(created_instance)
def update_instances(self, update_list):
for created_inst, _changes in update_list:
instance_node = created_inst.get("instance_node")
new_values = {
key: new_value
for key, (_old_value, new_value) in _changes.items()
}
imprint(
instance_node,
new_values,
)
def remove_instances(self, instances):
"""Remove specified instance from the scene.
This is only removing `id` parameter so instance is no longer
instance, because it might contain valuable data for artist.
"""
for instance in instances:
instance_node = rt.getNodeByName(
instance.data.get("instance_node"))
if instance_node:
rt.delete(rt.getNodeByName(instance_node))
self._remove_instance_from_context(instance)
def get_pre_create_attr_defs(self):
return [
BoolDef("use_selection", label="Use selection")
]

View file

@ -0,0 +1,17 @@
from openpype.lib import PreLaunchHook
class SetPath(PreLaunchHook):
"""Set current dir to workdir.
Hook `GlobalHostDataHook` must be executed before this hook.
"""
app_groups = ["max"]
def execute(self):
workdir = self.launch_context.env.get("AVALON_WORKDIR", "")
if not workdir:
self.log.warning("BUG: Workdir is not filled.")
return
self.launch_context.kwargs["cwd"] = workdir

View file

View file

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
"""Creator plugin for creating pointcache alembics."""
from openpype.hosts.max.api import plugin
from openpype.pipeline import CreatedInstance
class CreatePointCache(plugin.MaxCreator):
identifier = "io.openpype.creators.max.pointcache"
label = "Point Cache"
family = "pointcache"
icon = "gear"
def create(self, subset_name, instance_data, pre_create_data):
# from pymxs import runtime as rt
_ = super(CreatePointCache, self).create(
subset_name,
instance_data,
pre_create_data) # type: CreatedInstance
# for additional work on the node:
# instance_node = rt.getNodeByName(instance.get("instance_node"))

View file

@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
"""Simple alembic loader for 3dsmax.
Because of limited api, alembics can be only loaded, but not easily updated.
"""
import os
from openpype.pipeline import (
load
)
class AbcLoader(load.LoaderPlugin):
"""Alembic loader."""
families = ["model", "animation", "pointcache"]
label = "Load Alembic"
representations = ["abc"]
order = -10
icon = "code-fork"
color = "orange"
def load(self, context, name=None, namespace=None, data=None):
from pymxs import runtime as rt
file_path = os.path.normpath(self.fname)
abc_before = {
c for c in rt.rootNode.Children
if rt.classOf(c) == rt.AlembicContainer
}
abc_export_cmd = (f"""
AlembicImport.ImportToRoot = false
importFile @"{file_path}" #noPrompt
""")
self.log.debug(f"Executing command: {abc_export_cmd}")
rt.execute(abc_export_cmd)
abc_after = {
c for c in rt.rootNode.Children
if rt.classOf(c) == rt.AlembicContainer
}
# This should yield new AlembicContainer node
abc_containers = abc_after.difference(abc_before)
if len(abc_containers) != 1:
self.log.error("Something failed when loading.")
abc_container = abc_containers.pop()
container_name = f"{name}_CON"
container = rt.container(name=container_name)
abc_container.Parent = container
return container
def remove(self, container):
from pymxs import runtime as rt
node = container["node"]
rt.delete(node)

View file

@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
"""Collect current work file."""
import os
import pyblish.api
from pymxs import runtime as rt
from openpype.pipeline import legacy_io
class CollectWorkfile(pyblish.api.ContextPlugin):
"""Inject the current working file into context"""
order = pyblish.api.CollectorOrder - 0.01
label = "Collect 3dsmax Workfile"
hosts = ['max']
def process(self, context):
"""Inject the current working file."""
folder = rt.maxFilePath
file = rt.maxFileName
if not folder or not file:
self.log.error("Scene is not saved.")
current_file = os.path.join(folder, file)
context.data['currentFile'] = current_file
filename, ext = os.path.splitext(file)
task = legacy_io.Session["AVALON_TASK"]
data = {}
# create instance
instance = context.create_instance(name=filename)
subset = 'workfile' + task.capitalize()
data.update({
"subset": subset,
"asset": os.getenv("AVALON_ASSET", None),
"label": subset,
"publish": True,
"family": 'workfile',
"families": ['workfile'],
"setMembers": [current_file],
"frameStart": context.data['frameStart'],
"frameEnd": context.data['frameEnd'],
"handleStart": context.data['handleStart'],
"handleEnd": context.data['handleEnd']
})
data['representations'] = [{
'name': ext.lstrip("."),
'ext': ext.lstrip("."),
'files': file,
"stagingDir": folder,
}]
instance.data.update(data)
self.log.info('Collected instance: {}'.format(file))
self.log.info('Scene path: {}'.format(current_file))
self.log.info('staging Dir: {}'.format(folder))
self.log.info('subset: {}'.format(subset))

View file

@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
"""
Export alembic file.
Note:
Parameters on AlembicExport (AlembicExport.Parameter):
ParticleAsMesh (bool): Sets whether particle shapes are exported
as meshes.
AnimTimeRange (enum): How animation is saved:
#CurrentFrame: saves current frame
#TimeSlider: saves the active time segments on time slider (default)
#StartEnd: saves a range specified by the Step
StartFrame (int)
EnFrame (int)
ShapeSuffix (bool): When set to true, appends the string "Shape" to the
name of each exported mesh. This property is set to false by default.
SamplesPerFrame (int): Sets the number of animation samples per frame.
Hidden (bool): When true, export hidden geometry.
UVs (bool): When true, export the mesh UV map channel.
Normals (bool): When true, export the mesh normals.
VertexColors (bool): When true, export the mesh vertex color map 0 and the
current vertex color display data when it differs
ExtraChannels (bool): When true, export the mesh extra map channels
(map channels greater than channel 1)
Velocity (bool): When true, export the meh vertex and particle velocity
data.
MaterialIDs (bool): When true, export the mesh material ID as
Alembic face sets.
Visibility (bool): When true, export the node visibility data.
LayerName (bool): When true, export the node layer name as an Alembic
object property.
MaterialName (bool): When true, export the geometry node material name as
an Alembic object property
ObjectID (bool): When true, export the geometry node g-buffer object ID as
an Alembic object property.
CustomAttributes (bool): When true, export the node and its modifiers
custom attributes into an Alembic object compound property.
"""
import os
import pyblish.api
from openpype.pipeline import publish
from pymxs import runtime as rt
from openpype.hosts.max.api import (
maintained_selection,
get_all_children
)
class ExtractAlembic(publish.Extractor):
order = pyblish.api.ExtractorOrder
label = "Extract Pointcache"
hosts = ["max"]
families = ["pointcache", "camera"]
def process(self, instance):
start = float(instance.data.get("frameStartHandle", 1))
end = float(instance.data.get("frameEndHandle", 1))
container = instance.data["instance_node"]
self.log.info("Extracting pointcache ...")
parent_dir = self.staging_dir(instance)
file_name = "{name}.abc".format(**instance.data)
path = os.path.join(parent_dir, file_name)
# We run the render
self.log.info("Writing alembic '%s' to '%s'" % (file_name,
parent_dir))
abc_export_cmd = (
f"""
AlembicExport.ArchiveType = #ogawa
AlembicExport.CoordinateSystem = #maya
AlembicExport.StartFrame = {start}
AlembicExport.EndFrame = {end}
exportFile @"{path}" #noPrompt selectedOnly:on using:AlembicExport
""")
self.log.debug(f"Executing command: {abc_export_cmd}")
with maintained_selection():
# select and export
rt.select(get_all_children(rt.getNodeByName(container)))
rt.execute(abc_export_cmd)
if "representations" not in instance.data:
instance.data["representations"] = []
representation = {
'name': 'abc',
'ext': 'abc',
'files': file_name,
"stagingDir": parent_dir,
}
instance.data["representations"].append(representation)

View file

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
import pyblish.api
from openpype.pipeline import PublishValidationError
from pymxs import runtime as rt
class ValidateSceneSaved(pyblish.api.InstancePlugin):
"""Validate that workfile was saved."""
order = pyblish.api.ValidatorOrder
families = ["workfile"]
hosts = ["max"]
label = "Validate Workfile is saved"
def process(self, instance):
if not rt.maxFilePath or not rt.maxFileName:
raise PublishValidationError(
"Workfile is not saved", title=self.label)

Some files were not shown because too many files have changed in this diff Show more