This commit is contained in:
Roy Nieterau 2018-09-13 14:56:57 +02:00
commit 94f6bda8ee
13 changed files with 660 additions and 9 deletions

View file

@ -0,0 +1,99 @@
import os
import logging
import hou
from pyblish import api as pyblish
from avalon import api as avalon
from avalon.houdini import pipeline as houdini
from colorbleed.houdini import lib
from colorbleed.lib import (
any_outdated,
update_task_from_path
)
PARENT_DIR = os.path.dirname(__file__)
PACKAGE_DIR = os.path.dirname(PARENT_DIR)
PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins")
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "houdini", "publish")
LOAD_PATH = os.path.join(PLUGINS_DIR, "houdini", "load")
CREATE_PATH = os.path.join(PLUGINS_DIR, "houdini", "create")
log = logging.getLogger("colorbleed.houdini")
def install():
# Set
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("save", on_save)
avalon.on("open", on_open)
log.info("Overriding existing event 'taskChanged'")
log.info("Setting default family states for loader..")
avalon.data["familiesStateToggled"] = ["colorbleed.imagesequence"]
def on_init(_):
houdini.on_houdini_initialize()
def on_save(_):
avalon.logger.info("Running callback on save..")
update_task_from_path(hou.hipFile.path())
nodes = lib.get_id_required_nodes()
for node, new_id in lib.generate_ids(nodes):
lib.set_id(node, new_id, overwrite=False)
def on_open():
update_task_from_path(hou.hipFile.path())
if any_outdated():
from avalon.vendor.Qt import QtWidgets
from ..widgets import popup
log.warning("Scene has outdated content.")
# Find maya main window
top_level_widgets = {w.objectName(): w for w in
QtWidgets.QApplication.topLevelWidgets()}
parent = top_level_widgets.get("MayaWindow", None)
if parent is None:
log.info("Skipping outdated content pop-up "
"because Maya window can't be found.")
else:
# Show outdated pop-up
def _on_show_inventory():
import avalon.tools.cbsceneinventory as tool
tool.show(parent=parent)
dialog = popup.Popup(parent=parent)
dialog.setWindowTitle("Maya scene has outdated content")
dialog.setMessage("There are outdated containers in "
"your Maya scene.")
dialog.on_show.connect(_on_show_inventory)
dialog.show()
def on_task_changed(*args):
"""Wrapped function of app initialize and maya's on task changed"""
pass

108
colorbleed/houdini/lib.py Normal file
View file

@ -0,0 +1,108 @@
import uuid
from contextlib import contextmanager
import hou
from avalon import api, io
from avalon.houdini import lib
def set_id(node, unique_id, overwrite=False):
exists = node.parm("id")
if not exists:
lib.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
@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)

View file

@ -257,16 +257,52 @@ def get_project_fps():
Returns:
int, float
"""
data = get_project_data()
fps = data.get("fps", 25.0)
return fps
def get_project_data():
"""Get the data of the current project
The data of the project can contain things like:
resolution
fps
renderer
Returns:
dict:
"""
project_name = io.active_project()
project = io.find_one({"name": project_name,
"type": "project"},
projection={"config": True})
projection={"data": True})
config = project.get("config", None)
assert config, "This is a bug"
data = project.get("data", {})
fps = config.get("fps", 25.0)
return data
return fps
def get_asset_data(asset=None):
"""Get the data from the current asset
Args:
asset(str, Optional): name of the asset, eg:
Returns:
dict
"""
asset_name = asset or avalon.api.Session["AVALON_ASSET"]
document = io.find_one({"name": asset_name,
"type": "asset"})
data = document.get("data", {})
return data

View file

@ -39,6 +39,8 @@ def install():
avalon.before("save", on_before_save)
avalon.on("new", on_new)
log.info("Overriding existing event 'taskChanged'")
override_event("taskChanged", on_task_changed)
@ -158,6 +160,13 @@ def on_open(_):
dialog.show()
def on_new(_):
"""Set project resolution and fps when create a new file"""
avalon.logger.info("Running callback on new..")
with maya.suspended_refresh():
lib.set_context_settings()
def on_task_changed(*args):
"""Wrapped function of app initialize and maya's on task changed"""

View file

@ -90,7 +90,7 @@ _alembic_options = {
}
INT_FPS = {15, 24, 25, 30, 48, 50, 60, 44100, 48000}
FLOAT_FPS = {23.976, 29.97, 29.97, 47.952, 59.94}
FLOAT_FPS = {23.976, 29.97, 47.952, 59.94}
def matrix_equals(a, b, tolerance=1e-10):
@ -1371,6 +1371,7 @@ def get_id_from_history(node):
return _id
# Project settings
def set_scene_fps(fps, update=True):
"""Set FPS from project configuration
@ -1384,10 +1385,10 @@ def set_scene_fps(fps, update=True):
"""
if fps in FLOAT_FPS:
unit = "{:f}fps".format(fps)
unit = "{}fps".format(fps)
elif fps in INT_FPS:
unit = "{:d}fps".format(int(fps))
unit = "{}fps".format(int(fps))
else:
raise ValueError("Unsupported FPS value: `%s`" % fps)
@ -1399,6 +1400,69 @@ def set_scene_fps(fps, update=True):
cmds.file(modified=True)
def set_scene_resolution(width, height):
"""Set the render resolution
Args:
width(int): value of the width
height(int): value of the height
Returns:
None
"""
control_node = "defaultResolution"
current_renderer = cmds.getAttr("defaultRenderGlobals.currentRenderer")
# Give VRay a helping hand as it is slightly different from the rest
if current_renderer == "vray":
vray_node = "vraySettings"
if cmds.objExists(vray_node):
control_node = vray_node
else:
log.error("Can't set VRay resolution because there is no node "
"named: `%s`" % vray_node)
log.info("Setting project resolution to: %s x %s" % (width, height))
cmds.setAttr("%s.width" % control_node, width)
cmds.setAttr("%s.height" % control_node, height)
def set_context_settings():
"""Apply the project settings from the project definition
Settings can be overwritten by an asset if the asset.data contains
any information regarding those settings.
Examples of settings:
fps
resolution
renderer
Returns:
None
"""
# Todo (Wijnand): apply renderer and resolution of project
project_data = lib.get_project_data()
asset_data = lib.get_asset_data()
# Set project fps
fps = asset_data.get("fps", project_data.get("fps", 25))
set_scene_fps(fps)
# Set project resolution
width_key = "resolution_width"
height_key = "resolution_height"
width = asset_data.get(width_key, project_data.get(width_key, 1920))
height = asset_data.get(height_key, project_data.get(height_key, 1080))
set_scene_resolution(width, height)
# Valid FPS
def validate_fps():
"""Validate current scene FPS and show pop-up when it is incorrect
@ -1408,7 +1472,8 @@ def validate_fps():
"""
fps = lib.get_project_fps() # can be int or float
asset_data = lib.get_asset_data()
fps = asset_data.get("fps", lib.get_project_fps()) # can be int or float
current_fps = mel.eval('currentTimeUnitToFPS()') # returns float
if current_fps != fps:

View file

@ -0,0 +1,30 @@
from collections import OrderedDict
import hou
from avalon import houdini
class CreatePointCache(houdini.Creator):
"""Alembic pointcache for animated data"""
name = "pointcache"
label = "Point Cache"
family = "colorbleed.pointcache"
icon = "gears"
def __init__(self, *args, **kwargs):
super(CreatePointCache, self).__init__(*args, **kwargs)
# create an ordered dict with the existing data first
data = OrderedDict(**self.data)
# Set node type to create for output
data["node_type"] = "alembic"
# Collect animation data for point cache exporting
start, end = hou.playbar.timelineRange()
data["startFrame"] = start
data["endFrame"] = end
self.data = data

View file

@ -0,0 +1,98 @@
from avalon import api
from avalon.houdini import pipeline, lib
class AbcLoader(api.Loader):
"""Specific loader of Alembic for the avalon.animation family"""
families = ["colorbleed.animation", "colorbleed.pointcache"]
label = "Load Animation"
representations = ["abc"]
order = -10
icon = "code-fork"
color = "orange"
def load(self, context, name=None, namespace=None, data=None):
import os
import hou
# Format file name, Houdini only wants forward slashes
file_path = os.path.normpath(self.fname)
file_path = file_path.replace("\\", "/")
# Get the root node
obj = hou.node("/obj")
# Create a unique name
counter = 1
namespace = namespace if namespace else context["asset"]["name"]
formatted = "{}_{}".format(namespace, name) if namespace else name
node_name = "{0}_{1:03d}".format(formatted, counter)
children = lib.children_as_string(hou.node("/obj"))
while node_name in children:
counter += 1
node_name = "{0}_{1:03d}".format(formatted, counter)
# Create a new geo node
container = obj.createNode("geo", node_name=node_name)
# Remove the file node, it only loads static meshes
node_path = "/obj/{}/file1".format(node_name)
hou.node(node_path)
# Create an alembic node (supports animation)
alembic = container.createNode("alembic", node_name=node_name)
alembic.setParms({"fileName": file_path})
# Add unpack node
unpack = container.createNode("unpack")
unpack.setInput(0, alembic)
unpack.setParms({"transfer_attributes": "path"})
# Set new position for unpack node else it gets cluttered
unpack.setPosition([0, -1])
# set unpack as display node
unpack.setDisplayFlag(True)
null_node = container.createNode("null",
node_name="OUT_{}".format(name))
null_node.setPosition([0, -2])
null_node.setInput(0, unpack)
nodes = [container, alembic, unpack, null_node]
self[:] = nodes
return pipeline.containerise(node_name,
namespace,
nodes,
context,
self.__class__.__name__)
def update(self, container, representation):
node = container["node"]
try:
alembic_node = next(n for n in node.children() if
n.type().name() == "alembic")
except StopIteration:
self.log.error("Could not find node of type `alembic`")
return
# Update the file path
file_path = api.get_representation_path(representation)
file_path = file_path.replace("\\", "/")
alembic_node.setParms({"fileName": file_path})
# Update attribute
node.setParms({"representation": str(representation["_id"])})
def remove(self, container):
node = container["node"]
node.destroy()

View file

@ -0,0 +1,9 @@
import pyblish.api
class CollectAlembicNodes(pyblish.api.InstancePlugin):
label = "Collect Alembic Nodes"
def process(self, instance):
pass

View file

@ -0,0 +1,15 @@
import hou
import pyblish.api
class CollectMayaCurrentFile(pyblish.api.ContextPlugin):
"""Inject the current working file into context"""
order = pyblish.api.CollectorOrder - 0.5
label = "Houdini Current File"
hosts = ['houdini']
def process(self, context):
"""Inject the current working file"""
context.data['currentFile'] = hou.hipFile.path()

View file

@ -0,0 +1,68 @@
import hou
import pyblish.api
from avalon.houdini import lib
class CollectInstances(pyblish.api.ContextPlugin):
"""Gather instances by all node in out graph and pre-defined attributes
This collector takes into account assets that are associated with
an specific node and marked with a unique identifier;
Identifier:
id (str): "pyblish.avalon.instance
Specific node:
The specific node is important because it dictates in which way the subset
is being exported.
alembic: will export Alembic file which supports cascading attributes
like 'cbId' and 'path'
geometry: Can export a wide range of file types, default out
"""
label = "Collect Instances"
order = pyblish.api.CollectorOrder
hosts = ["houdini"]
def process(self, context):
instances = []
nodes = hou.node("/out").children()
for node in nodes:
if not node.parm("id"):
continue
if node.parm("id").eval() != "pyblish.avalon.instance":
continue
has_family = node.parm("family").eval()
assert has_family, "'%s' is missing 'family'" % node.name()
data = lib.read(node)
# temporarily translation of `active` to `publish` till issue has
# been resolved, https://github.com/pyblish/pyblish-base/issues/307
if "active" in data:
data["publish"] = data["active"]
instance = context.create_instance(data.get("name", node.name()))
instance[:] = [node]
instance.data.update(data)
instances.append(instance)
def sort_by_family(instance):
"""Sort by family"""
return instance.data.get("families", instance.data.get("family"))
# Sort/grouped by family (preserving local index)
context[:] = sorted(context, key=sort_by_family)
return context

View file

@ -0,0 +1,35 @@
import os
import pyblish.api
import colorbleed.api
from colorbleed.houdini import lib
class ExtractAlembic(colorbleed.api.Extractor):
order = pyblish.api.ExtractorOrder
label = "Extract Pointcache (Alembic)"
hosts = ["houdini"]
families = ["colorbleed.pointcache"]
def process(self, instance):
staging_dir = self.staging_dir(instance)
file_name = "{}.abc".format(instance.data["subset"])
tmp_filepath = os.path.join(staging_dir, file_name)
start_frame = float(instance.data["startFrame"])
end_frame = float(instance.data["endFrame"])
ropnode = instance[0]
attributes = {"filename": tmp_filepath,
"trange": 2}
with lib.attribute_values(ropnode, attributes):
ropnode.render(frame_range=(start_frame, end_frame, 1))
if "files" not in instance.data:
instance.data["files"] = []
instance.data["files"].append(file_name)

View file

@ -0,0 +1,38 @@
import pyblish.api
import colorbleed.api
class ValidatIntermediateDirectoriesChecked(pyblish.api.InstancePlugin):
"""Validate if node attribute Create intermediate Directories is turned on
Rules:
* The node must have Create intermediate Directories turned on to
ensure the output file will be created
"""
order = colorbleed.api.ValidateContentsOrder
families = ['colorbleed.pointcache']
hosts = ['houdini']
label = 'Create Intermediate Directories Checked'
def process(self, instance):
invalid = self.get_invalid(instance)
if invalid:
raise RuntimeError("Found ROP nodes with Create Intermediate "
"Directories turned off")
@classmethod
def get_invalid(cls, instance):
result = []
for node in instance[:]:
if node.parm("mkpath").eval() != 1:
cls.log.error("Invalid settings found on `%s`" % node.path())
result.append(node.path())
return result

View file

@ -0,0 +1,41 @@
import pyblish.api
import colorbleed.api
class ValidatOutputNodeExists(pyblish.api.InstancePlugin):
"""Validate if node attribute Create intermediate Directories is turned on
Rules:
* The node must have Create intermediate Directories turned on to
ensure the output file will be created
"""
order = colorbleed.api.ValidateContentsOrder
families = ["*"]
hosts = ['houdini']
label = "Output Node Exists"
def process(self, instance):
invalid = self.get_invalid(instance)
if invalid:
raise RuntimeError("Could not find output node(s)!")
@classmethod
def get_invalid(cls, instance):
import hou
result = set()
node = instance[0]
sop_path = node.parm("sop_path").eval()
if not sop_path.endswith("OUT"):
cls.log.error("SOP Path does not end path at output node")
result.add(node.path())
if hou.node(sop_path) is None:
cls.log.error("Node at '%s' does not exist" % sop_path)
result.add(node.path())
return result