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

Houdini: add Camera, Point Cache, Composite, Redshift ROP and VDB Cache support #1821

Merged
merged 21 commits into from
Aug 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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