Skip to content
This repository has been archived by the owner on Sep 20, 2024. It is now read-only.

Commit

Permalink
Merge pull request #1821 from pypeclub/feature/houdini-cb-update
Browse files Browse the repository at this point in the history
  • Loading branch information
mkolar authored Aug 25, 2021
2 parents 27a16b2 + 3b73367 commit 0c613ab
Show file tree
Hide file tree
Showing 89 changed files with 5,084 additions and 285 deletions.
97 changes: 84 additions & 13 deletions openpype/hosts/houdini/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import os
import sys
import logging
import contextlib

import hou

from pyblish import api as pyblish

from avalon import api as avalon
from avalon.houdini import pipeline as houdini

import openpype.hosts.houdini
from openpype.hosts.houdini.api import lib

from openpype.lib import any_outdated
from openpype.lib import (
any_outdated
)

from .lib import get_asset_fps

log = logging.getLogger("openpype.hosts.houdini")

Expand All @@ -22,26 +26,36 @@
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")


def install():

pyblish.register_plugin_path(PUBLISH_PATH)
avalon.register_plugin_path(avalon.Loader, LOAD_PATH)
avalon.register_plugin_path(avalon.Creator, CREATE_PATH)

log.info("Installing callbacks ... ")
avalon.on("init", on_init)
# avalon.on("init", on_init)
avalon.before("save", before_save)
avalon.on("save", on_save)
avalon.on("open", on_open)
avalon.on("new", on_new)

pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled)

log.info("Setting default family states for loader..")
avalon.data["familiesStateToggled"] = ["imagesequence"]
avalon.data["familiesStateToggled"] = [
"imagesequence",
"review"
]

# add houdini vendor packages
hou_pythonpath = os.path.join(os.path.dirname(HOST_DIR), "vendor")

sys.path.append(hou_pythonpath)

def on_init(*args):
houdini.on_houdini_initialize()
# Set asset FPS for the empty scene directly after launch of Houdini
# so it initializes into the correct scene FPS
_set_asset_fps()


def before_save(*args):
Expand All @@ -59,18 +73,26 @@ def on_save(*args):

def on_open(*args):

if not hou.isUIAvailable():
log.debug("Batch mode detected, ignoring `on_open` callbacks..")
return

avalon.logger.info("Running callback on open..")

# Validate FPS after update_task_from_path to
# ensure it is using correct FPS for the asset
lib.validate_fps()

if any_outdated():
from ..widgets import popup
from openpype.widgets import popup

log.warning("Scene has outdated content.")

# Get main window
parent = hou.ui.mainQtWindow()
if parent is None:
log.info("Skipping outdated content pop-up "
"because Maya window can't be found.")
"because Houdini window can't be found.")
else:

# Show outdated pop-up
Expand All @@ -79,15 +101,52 @@ def _on_show_inventory():
tool.show(parent=parent)

dialog = popup.Popup(parent=parent)
dialog.setWindowTitle("Maya scene has outdated content")
dialog.setWindowTitle("Houdini scene has outdated content")
dialog.setMessage("There are outdated containers in "
"your Maya scene.")
dialog.on_show.connect(_on_show_inventory)
"your Houdini scene.")
dialog.on_clicked.connect(_on_show_inventory)
dialog.show()


def on_new(_):
"""Set project resolution and fps when create a new file"""
avalon.logger.info("Running callback on new..")
_set_asset_fps()


def _set_asset_fps():
"""Set Houdini scene FPS to the default required for current asset"""

# Set new scene fps
fps = get_asset_fps()
print("Setting scene FPS to %i" % fps)
lib.set_scene_fps(fps)


def on_pyblish_instance_toggled(instance, new_value, old_value):
"""Toggle saver tool passthrough states on instance toggles."""
@contextlib.contextmanager
def main_take(no_update=True):
"""Enter root take during context"""
original_take = hou.takes.currentTake()
original_update_mode = hou.updateModeSetting()
root = hou.takes.rootTake()
has_changed = False
try:
if original_take != root:
has_changed = True
if no_update:
hou.setUpdateMode(hou.updateMode.Manual)
hou.takes.setCurrentTake(root)
yield
finally:
if has_changed:
if no_update:
hou.setUpdateMode(original_update_mode)
hou.takes.setCurrentTake(original_take)

if not instance.data.get("_allowToggleBypass", True):
return

nodes = instance[:]
if not nodes:
Expand All @@ -96,8 +155,20 @@ def on_pyblish_instance_toggled(instance, new_value, old_value):
# Assume instance node is first node
instance_node = nodes[0]

if not hasattr(instance_node, "isBypassed"):
# Likely not a node that can actually be bypassed
log.debug("Can't bypass node: %s", instance_node.path())
return

if instance_node.isBypassed() != (not old_value):
print("%s old bypass state didn't match old instance state, "
"updating anyway.." % instance_node.path())

instance_node.bypass(not new_value)
try:
# Go into the main take, because when in another take changing
# the bypass state of a note cannot be done due to it being locked
# by default.
with main_take(no_update=True):
instance_node.bypass(not new_value)
except hou.PermissionError as exc:
log.warning("%s - %s", instance_node.path(), exc)
138 changes: 124 additions & 14 deletions openpype/hosts/houdini/api/lib.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import uuid

import logging
from contextlib import contextmanager

from openpype.api import get_asset
from avalon import api, io
from avalon.houdini import lib as houdini

import hou

from openpype import lib
log = logging.getLogger(__name__)

from avalon import api, io
from avalon.houdini import lib as houdini

def get_asset_fps():
"""Return current asset fps."""
return get_asset()["data"].get("fps")

def set_id(node, unique_id, overwrite=False):

Expand Down Expand Up @@ -171,10 +176,10 @@ def get_output_parameter(node):
node_type = node.type().name()
if node_type == "geometry":
return node.parm("sopoutput")

elif node_type == "alembic":
return node.parm("filename")

elif node_type == "comp":
return node.parm("copoutput")
else:
raise TypeError("Node type '%s' not supported" % node_type)

Expand Down Expand Up @@ -205,7 +210,7 @@ def validate_fps():
"""

fps = lib.get_asset()["data"]["fps"]
fps = get_asset_fps()
current_fps = hou.fps() # returns float

if current_fps != fps:
Expand All @@ -217,18 +222,123 @@ def validate_fps():
if parent is None:
pass
else:
dialog = popup.Popup2(parent=parent)
dialog = popup.Popup(parent=parent)
dialog.setModal(True)
dialog.setWindowTitle("Houdini scene not in line with project")
dialog.setMessage("The FPS is out of sync, please fix it")
dialog.setWindowTitle("Houdini scene does not match project FPS")
dialog.setMessage("Scene %i FPS does not match project %i FPS" %
(current_fps, fps))
dialog.setButtonText("Fix")

# Set new text for button (add optional argument for the popup?)
toggle = dialog.widgets["toggle"]
toggle.setEnabled(False)
dialog.on_show.connect(lambda: set_scene_fps(fps))
# on_show is the Fix button clicked callback
dialog.on_clicked.connect(lambda: set_scene_fps(fps))

dialog.show()

return False

return True


def create_remote_publish_node(force=True):
"""Function to create a remote publish node in /out
This is a hacked "Shell" node that does *nothing* except for triggering
`colorbleed.lib.publish_remote()` as pre-render script.
All default attributes of the Shell node are hidden to the Artist to
avoid confusion.
Additionally some custom attributes are added that can be collected
by a Collector to set specific settings for the publish, e.g. whether
to separate the jobs per instance or process in one single job.
"""

cmd = "import colorbleed.lib; colorbleed.lib.publish_remote()"

existing = hou.node("/out/REMOTE_PUBLISH")
if existing:
if force:
log.warning("Removing existing '/out/REMOTE_PUBLISH' node..")
existing.destroy()
else:
raise RuntimeError("Node already exists /out/REMOTE_PUBLISH. "
"Please remove manually or set `force` to "
"True.")

# Create the shell node
out = hou.node("/out")
node = out.createNode("shell", node_name="REMOTE_PUBLISH")
node.moveToGoodPosition()

# Set color make it stand out (avalon/pyblish color)
node.setColor(hou.Color(0.439, 0.709, 0.933))

# Set the pre-render script
node.setParms({
"prerender": cmd,
"lprerender": "python" # command language
})

# Lock the attributes to ensure artists won't easily mess things up.
node.parm("prerender").lock(True)
node.parm("lprerender").lock(True)

# Lock up the actual shell command
command_parm = node.parm("command")
command_parm.set("")
command_parm.lock(True)
shellexec_parm = node.parm("shellexec")
shellexec_parm.set(False)
shellexec_parm.lock(True)

# Get the node's parm template group so we can customize it
template = node.parmTemplateGroup()

# Hide default tabs
template.hideFolder("Shell", True)
template.hideFolder("Scripts", True)

# Hide default settings
template.hide("execute", True)
template.hide("renderdialog", True)
template.hide("trange", True)
template.hide("f", True)
template.hide("take", True)

# Add custom settings to this node.
parm_folder = hou.FolderParmTemplate("folder", "Submission Settings")

# Separate Jobs per Instance
parm = hou.ToggleParmTemplate(name="separateJobPerInstance",
label="Separate Job per Instance",
default_value=False)
parm_folder.addParmTemplate(parm)

# Add our custom Submission Settings folder
template.append(parm_folder)

# Apply template back to the node
node.setParmTemplateGroup(template)


def render_rop(ropnode):
"""Render ROP node utility for Publishing.
This renders a ROP node with the settings we want during Publishing.
"""
# Print verbose when in batch mode without UI
verbose = not hou.isUIAvailable()

# Render
try:
ropnode.render(verbose=verbose,
# Allow Deadline to capture completion percentage
output_progress=verbose)
except hou.Error as exc:
# The hou.Error is not inherited from a Python Exception class,
# so we explicitly capture the houdini error, otherwise pyblish
# will remain hanging.
import traceback
traceback.print_exc()
raise RuntimeError("Render failed: {0}".format(exc))
22 changes: 21 additions & 1 deletion openpype/hosts/houdini/api/plugin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
# -*- coding: utf-8 -*-
"""Houdini specific Avalon/Pyblish plugin definitions."""
import sys
from avalon import houdini
import six

import hou
from openpype.api import PypeCreatorMixin


class Creator(PypeCreatorMixin, houdini.Creator):
class OpenPypeCreatorError(Exception):
pass


class Creator(PypeCreatorMixin, houdini.Creator):
def process(self):
try:
# re-raise as standard Python exception so
# Avalon can catch it
instance = super(Creator, self).process()
self._process(instance)
except hou.Error as er:
six.reraise(
OpenPypeCreatorError,
OpenPypeCreatorError("Creator error: {}".format(er)),
sys.exc_info()[2])
Loading

0 comments on commit 0c613ab

Please sign in to comment.