ayon-core/openpype/hosts/houdini/api/lib.py
2022-02-20 22:56:07 +01:00

578 lines
16 KiB
Python

import uuid
import logging
from contextlib import contextmanager
import six
from avalon import api, io
from openpype.api import get_asset
import hou
log = logging.getLogger(__name__)
def get_asset_fps():
"""Return current asset fps."""
return get_asset()["data"].get("fps")
def set_id(node, unique_id, overwrite=False):
exists = node.parm("id")
if not exists:
imprint(node, {"id": unique_id})
if not exists and overwrite:
node.setParm("id", unique_id)
def get_id(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
"""
if node is None:
return
id = node.parm("id")
if node is None:
return
return id
def generate_ids(nodes, asset_id=None):
"""Returns new unique ids for the given nodes.
Note: This does not assign the new ids, it only generates the values.
To assign new ids using this method:
>>> nodes = ["a", "b", "c"]
>>> for node, id in generate_ids(nodes):
>>> set_id(node, id)
To also override any existing values (and assign regenerated ids):
>>> nodes = ["a", "b", "c"]
>>> for node, id in generate_ids(nodes):
>>> set_id(node, id, overwrite=True)
Args:
nodes (list): List of nodes.
asset_id (str or bson.ObjectId): The database id for the *asset* to
generate for. When None provided the current asset in the
active session is used.
Returns:
list: A list of (node, id) tuples.
"""
if asset_id is None:
# Get the asset ID from the database for the asset of current context
asset_data = io.find_one({"type": "asset",
"name": api.Session["AVALON_ASSET"]},
projection={"_id": True})
assert asset_data, "No current asset found in Session"
asset_id = asset_data['_id']
node_ids = []
for node in nodes:
_, uid = str(uuid.uuid4()).rsplit("-", 1)
unique_id = "{}:{}".format(asset_id, uid)
node_ids.append((node, unique_id))
return node_ids
def get_id_required_nodes():
valid_types = ["geometry"]
nodes = {n for n in hou.node("/out").children() if
n.type().name() in valid_types}
return list(nodes)
def get_additional_data(container):
"""Not implemented yet!"""
return container
def set_parameter_callback(node, parameter, language, callback):
"""Link a callback to a parameter of a node
Args:
node(hou.Node): instance of the nodee
parameter(str): name of the parameter
language(str): name of the language, e.g.: python
callback(str): command which needs to be triggered
Returns:
None
"""
template_grp = node.parmTemplateGroup()
template = template_grp.find(parameter)
if not template:
return
script_language = (hou.scriptLanguage.Python if language == "python" else
hou.scriptLanguage.Hscript)
template.setScriptCallbackLanguage(script_language)
template.setScriptCallback(callback)
template.setTags({"script_callback": callback,
"script_callback_language": language.lower()})
# Replace the existing template with the adjusted one
template_grp.replace(parameter, template)
node.setParmTemplateGroup(template_grp)
def set_parameter_callbacks(node, parameter_callbacks):
"""Set callbacks for multiple parameters of a node
Args:
node(hou.Node): instance of a hou.Node
parameter_callbacks(dict): collection of parameter and callback data
example: {"active" :
{"language": "python",
"callback": "print('hello world)'"}
}
Returns:
None
"""
for parameter, data in parameter_callbacks.items():
language = data["language"]
callback = data["callback"]
set_parameter_callback(node, parameter, language, callback)
def get_output_parameter(node):
"""Return the render output parameter name of the given node
Example:
root = hou.node("/obj")
my_alembic_node = root.createNode("alembic")
get_output_parameter(my_alembic_node)
# Result: "output"
Args:
node(hou.Node): node instance
Returns:
hou.Parm
"""
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")
elif node_type == "arnold":
if node.evalParm("ar_ass_export_enable"):
return node.parm("ar_ass_file")
raise TypeError("Node type '%s' not supported" % node_type)
@contextmanager
def attribute_values(node, data):
previous_attrs = {key: node.parm(key).eval() for key in data.keys()}
try:
node.setParms(data)
yield
except Exception as exc:
pass
finally:
node.setParms(previous_attrs)
def set_scene_fps(fps):
hou.setFps(fps)
# Valid FPS
def validate_fps():
"""Validate current scene FPS and show pop-up when it is incorrect
Returns:
bool
"""
fps = get_asset_fps()
current_fps = hou.fps() # returns float
if current_fps != fps:
from openpype.widgets import popup
# Find main window
parent = hou.ui.mainQtWindow()
if parent is None:
pass
else:
dialog = popup.Popup(parent=parent)
dialog.setModal(True)
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")
# 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))
def children_as_string(node):
return [c.name() for c in node.children()]
def imprint(node, data):
"""Store attributes with value on a node
Depending on the type of attribute it creates the correct parameter
template. Houdini uses a template per type, see the docs for more
information.
http://www.sidefx.com/docs/houdini/hom/hou/ParmTemplate.html
Args:
node(hou.Node): node object from Houdini
data(dict): collection of attributes and their value
Returns:
None
"""
parm_group = node.parmTemplateGroup()
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_folder.addParmTemplate(parm)
parm_group.append(parm_folder)
node.setParmTemplateGroup(parm_group)
def lsattr(attr, value=None, root="/"):
"""Return nodes that have `attr`
When `value` is not None it will only return nodes matching that value
for the given attribute.
Args:
attr (str): Name of the attribute (hou.Parm)
value (object, Optional): The value to compare the attribute too.
When the default None is provided the value check is skipped.
root (str): The root path in Houdini to search in.
Returns:
list: Matching nodes that have attribute with value.
"""
if value is None:
# Use allSubChildren() as allNodes() errors on nodes without
# permission to enter without a means to continue of querying
# the rest
nodes = hou.node(root).allSubChildren()
return [n for n in nodes if n.parm(attr)]
return lsattrs({attr: value})
def lsattrs(attrs, root="/"):
"""Return nodes matching `key` and `value`
Arguments:
attrs (dict): collection of attribute: value
root (str): The root path in Houdini to search in.
Example:
>> lsattrs({"id": "myId"})
["myNode"]
>> lsattr("id")
["myNode", "myOtherNode"]
Returns:
list: Matching nodes that have attribute with value.
"""
matches = set()
# Use allSubChildren() as allNodes() errors on nodes without
# permission to enter without a means to continue of querying
# the rest
nodes = hou.node(root).allSubChildren()
for node in nodes:
for attr in attrs:
if not node.parm(attr):
continue
elif node.evalParm(attr) != attrs[attr]:
continue
else:
matches.add(node)
return list(matches)
def read(node):
"""Read the container data in to a dict
Args:
node(hou.Node): Houdini node
Returns:
dict
"""
# `spareParms` returns a tuple of hou.Parm objects
return {parameter.name(): parameter.eval() for
parameter in node.spareParms()}
def unique_name(name, format="%03d", namespace="", prefix="", suffix="",
separator="_"):
"""Return unique `name`
The function takes into consideration an optional `namespace`
and `suffix`. The suffix is included in evaluating whether a
name exists - such as `name` + "_GRP" - but isn't included
in the returned value.
If a namespace is provided, only names within that namespace
are considered when evaluating whether the name is unique.
Arguments:
format (str, optional): The `name` is given a number, this determines
how this number is formatted. Defaults to a padding of 2.
E.g. my_name01, my_name02.
namespace (str, optional): Only consider names within this namespace.
suffix (str, optional): Only consider names with this suffix.
Example:
>>> name = hou.node("/obj").createNode("geo", name="MyName")
>>> assert hou.node("/obj/MyName")
True
>>> unique = unique_name(name)
>>> assert hou.node("/obj/{}".format(unique))
False
"""
iteration = 1
parts = [prefix, name, format % iteration, suffix]
if namespace:
parts.insert(0, namespace)
unique = separator.join(parts)
children = children_as_string(hou.node("/obj"))
while unique in children:
iteration += 1
unique = separator.join(parts)
if suffix:
return unique[:-len(suffix)]
return unique
@contextmanager
def maintained_selection():
"""Maintain selection during context
Example:
>>> with maintained_selection():
... # Modify selection
... node.setSelected(on=False, clear_all_selected=True)
>>> # Selection restored
"""
previous_selection = hou.selectedNodes()
try:
yield
finally:
# Clear the selection
# todo: does hou.clearAllSelected() do the same?
for node in hou.selectedNodes():
node.setSelected(on=False)
if previous_selection:
for node in previous_selection:
node.setSelected(on=True)
def reset_framerange():
"""Set frame range to current asset"""
asset_name = api.Session["AVALON_ASSET"]
asset = io.find_one({"name": asset_name, "type": "asset"})
frame_start = asset["data"].get("frameStart")
frame_end = asset["data"].get("frameEnd")
# Backwards compatibility
if frame_start is None or frame_end is None:
frame_start = asset["data"].get("edit_in")
frame_end = asset["data"].get("edit_out")
if frame_start is None or frame_end is None:
log.warning("No edit information found for %s" % asset_name)
return
handles = asset["data"].get("handles") or 0
handle_start = asset["data"].get("handleStart")
if handle_start is None:
handle_start = handles
handle_end = asset["data"].get("handleEnd")
if handle_end is None:
handle_end = handles
frame_start -= int(handle_start)
frame_end += int(handle_end)
hou.playbar.setFrameRange(frame_start, frame_end)
hou.playbar.setPlaybackRange(frame_start, frame_end)
hou.setFrame(frame_start)