Merge pull request #163 from aardschok/houdini

Improvements on Houdini pipeline
This commit is contained in:
Wijnand Koreman 2018-09-18 14:31:24 +02:00 committed by GitHub
commit a13b272ef1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 524 additions and 33 deletions

View file

@ -30,6 +30,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
"colorbleed.mayaAscii",
"colorbleed.model",
"colorbleed.pointcache",
"colorbleed.vdbcache",
"colorbleed.setdress",
"colorbleed.rig",
"colorbleed.vrayproxy",

View file

@ -1,7 +1,5 @@
from collections import OrderedDict
import hou
from avalon import houdini
@ -22,9 +20,18 @@ class CreatePointCache(houdini.Creator):
# 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
def process(self):
instance = super(CreatePointCache, self).process()
parms = {"build_from_path": 1,
"path_attrib": "path",
"use_sop_path": True,
"filename": "$HIP/%s.abc" % self.name}
if self.nodes:
node = self.nodes[0]
parms.update({"sop_path": "%s/OUT" % node.path()})
instance.setParms(parms)

View file

@ -0,0 +1,32 @@
from collections import OrderedDict
from avalon import houdini
class CreateVDBCache(houdini.Creator):
"""Alembic pointcache for animated data"""
name = "vbdcache"
label = "VDB Cache"
family = "colorbleed.vdbcache"
icon = "cloud"
def __init__(self, *args, **kwargs):
super(CreateVDBCache, 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"] = "geometry"
self.data = data
def process(self):
instance = super(CreateVDBCache, self).process()
parms = {"sopoutput": "$HIP/geo/%s.$F4.vdb" % self.name}
if self.nodes:
parms.update({"soppath": self.nodes[0].path()})
instance.setParms(parms)

View file

@ -6,8 +6,10 @@ 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"
families = ["colorbleed.model",
"colorbleed.animation",
"colorbleed.pointcache"]
label = "Load Alembic"
representations = ["abc"]
order = -10
icon = "code-fork"

View file

@ -0,0 +1,119 @@
from avalon import api
from avalon.houdini import pipeline, lib
class CameraLoader(api.Loader):
"""Specific loader of Alembic for the avalon.animation family"""
families = ["colorbleed.camera"]
label = "Load Camera (abc)"
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
asset_name = context["asset"]["name"]
namespace = namespace if namespace else 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 archive node
container = self.create_and_connect(obj, "alembicarchive", node_name)
# TODO: add FPS of project / asset
container.setParms({"fileName": file_path,
"channelRef": True})
# Apply some magic
container.parm("buildHierarchy").pressButton()
container.moveToGoodPosition()
# Create an alembic xform node
nodes = [container]
self[:] = nodes
return pipeline.containerise(node_name,
namespace,
nodes,
context,
self.__class__.__name__)
def update(self, container, representation):
node = container["node"]
# Update the file path
file_path = api.get_representation_path(representation)
file_path = file_path.replace("\\", "/")
# Update attributes
node.setParms({"fileName": file_path,
"representation": str(representation["_id"])})
# Rebuild
node.parm("buildHierarchy").pressButton()
def remove(self, container):
node = container["node"]
node.destroy()
def create_and_connect(self, node, node_type, name=None):
"""Create a node within a node which and connect it to the input
Args:
node(hou.Node): parent of the new node
node_type(str) name of the type of node, eg: 'alembic'
name(str, Optional): name of the node
Returns:
hou.Node
"""
import hou
try:
if name:
new_node = node.createNode(node_type, node_name=name)
else:
new_node = node.createNode(node_type)
new_node.moveToGoodPosition()
try:
input_node = next(i for i in node.allItems() if
isinstance(i, hou.SubnetIndirectInput))
except StopIteration:
return new_node
new_node.setInput(0, input_node)
return new_node
except Exception:
raise RuntimeError("Could not created node type `%s` in node `%s`"
% (node_type, node))

View file

@ -3,7 +3,7 @@ import hou
import pyblish.api
class CollectMayaCurrentFile(pyblish.api.ContextPlugin):
class CollectHoudiniCurrentFile(pyblish.api.ContextPlugin):
"""Inject the current working file into context"""
order = pyblish.api.CollectorOrder - 0.5

View file

@ -24,8 +24,8 @@ class CollectInstances(pyblish.api.ContextPlugin):
"""
order = pyblish.api.CollectorOrder - 0.01
label = "Collect Instances"
order = pyblish.api.CollectorOrder
hosts = ["houdini"]
def process(self, context):
@ -51,7 +51,17 @@ class CollectInstances(pyblish.api.ContextPlugin):
if "active" in data:
data["publish"] = data["active"]
instance = context.create_instance(data.get("name", node.name()))
data.update(self.get_frame_data(node))
# Create nice name
# All nodes in the Outputs graph have the 'Valid Frame Range'
# attribute, we check here if any frames are set
label = data.get("name", node.name())
if "startFrame" in data:
frames = "[{startFrame} - {endFrame}]".format(**data)
label = "{} {}".format(label, frames)
instance = context.create_instance(label)
instance[:] = [node]
instance.data.update(data)
@ -66,3 +76,27 @@ class CollectInstances(pyblish.api.ContextPlugin):
context[:] = sorted(context, key=sort_by_family)
return context
def get_frame_data(self, node):
"""Get the frame data: start frame, end frame and steps
Args:
node(hou.Node)
Returns:
dict
"""
data = {}
if node.parm("trange") is None:
return data
if node.evalParm("trange") == 0:
return data
data["startFrame"] = node.evalParm("f1")
data["endFrame"] = node.evalParm("f2")
data["steps"] = node.evalParm("f3")
return data

View file

@ -0,0 +1,27 @@
import pyblish.api
class CollectOutputNode(pyblish.api.InstancePlugin):
"""Collect the out node which of the instance"""
order = pyblish.api.CollectorOrder
families = ["*"]
hosts = ["houdini"]
label = "Collect Output Node"
def process(self, instance):
import hou
node = instance[0]
# Get sop path
if node.type().name() == "alembic":
sop_path_parm = "sop_path"
else:
sop_path_parm = "soppath"
sop_path = node.parm(sop_path_parm).eval()
out_node = hou.node(sop_path)
instance.data["output_node"] = out_node

View file

@ -0,0 +1,31 @@
import pyblish.api
class CollectAnimation(pyblish.api.InstancePlugin):
"""Collect the animation data for the data base
Data collected:
- start frame
- end frame
- nr of steps
"""
order = pyblish.api.CollectorOrder
families = ["colorbleed.pointcache"]
hosts = ["houdini"]
label = "Collect Animation"
def process(self, instance):
node = instance[0]
# Get animation parameters for data
parameters = {"f1": "startFrame",
"f2": "endFrame",
"f3": "steps"}
data = {name: node.parm(par).eval() for par, name in
parameters.items()}
instance.data.update(data)

View file

@ -2,7 +2,6 @@ import os
import pyblish.api
import colorbleed.api
from colorbleed.houdini import lib
class ExtractAlembic(colorbleed.api.Extractor):
@ -14,20 +13,19 @@ class ExtractAlembic(colorbleed.api.Extractor):
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))
# Get the filename from the filename parameter
# `.eval()` will make sure all tokens are resolved
output = ropnode.parm("filename").eval()
staging_dir = os.path.dirname(output)
instance.data["stagingDir"] = staging_dir
file_name = os.path.basename(output)
# We run the render
self.log.info("Writing alembic '%s' to '%s'" % (file_name, staging_dir))
ropnode.render()
if "files" not in instance.data:
instance.data["files"] = []

View file

@ -0,0 +1,42 @@
import os
import re
import pyblish.api
import colorbleed.api
class ExtractVDBCache(colorbleed.api.Extractor):
order = pyblish.api.ExtractorOrder + 0.1
label = "Extract VDB Cache"
families = ["colorbleed.vdbcache"]
hosts = ["houdini"]
def process(self, instance):
ropnode = instance[0]
# Get the filename from the filename parameter
# `.eval()` will make sure all tokens are resolved
output = ropnode.parm("sopoutput").eval()
staging_dir = os.path.dirname(output)
instance.data["stagingDir"] = staging_dir
# Replace the 4 digits to match file sequence token '%04d' if we have
# a sequence of frames
file_name = os.path.basename(output)
has_frame = re.match("\w\.(d+)\.vdb", file_name)
if has_frame:
frame_nr = has_frame.group()
file_name.replace(frame_nr, "%04d")
# We run the render
self.log.info(
"Starting render: {startFrame} - {endFrame}".format(**instance.data)
)
ropnode.render()
if "files" not in instance.data:
instance.data["files"] = []
instance.data["files"].append(file_name)

View file

@ -0,0 +1,46 @@
import pyblish.api
import colorbleed.api
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 = colorbleed.api.ValidateContentsOrder + 0.1
families = ["colorbleed.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

@ -0,0 +1,37 @@
import pyblish.api
import colorbleed.api
class ValidateAlembicInputNode(pyblish.api.InstancePlugin):
"""Validate that the node connected to the output is correct
The connected node cannot be of the following types for Alembic:
- VDB
- Volumne
"""
order = colorbleed.api.ValidateContentsOrder + 0.1
families = ["colorbleed.pointcache"]
hosts = ["houdini"]
label = "Validate Input Node (Abc)"
def process(self, instance):
invalid = self.get_invalid(instance)
if invalid:
raise RuntimeError("Node connected to the output node incorrect")
@classmethod
def get_invalid(cls, instance):
invalid_nodes = ["VDB", "Volume"]
node = instance.data["output_node"]
prims = node.geometry().prims()
for prim in prims:
prim_type = prim.type().name()
if prim_type in invalid_nodes:
cls.log.error("Found a primitive which is of type '%s' !"
% prim_type)
return [instance]

View file

@ -2,7 +2,7 @@ import pyblish.api
import colorbleed.api
class ValidatIntermediateDirectoriesChecked(pyblish.api.InstancePlugin):
class ValidateIntermediateDirectoriesChecked(pyblish.api.InstancePlugin):
"""Validate if node attribute Create intermediate Directories is turned on
Rules:

View file

@ -29,13 +29,21 @@ class ValidatOutputNodeExists(pyblish.api.InstancePlugin):
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 node.type().name() == "alembic":
soppath_parm = "sop_path"
else:
# Fall back to geometry node
soppath_parm = "soppath"
if hou.node(sop_path) is None:
sop_path = node.parm(soppath_parm).eval()
output_node = hou.node(sop_path)
if output_node is None:
cls.log.error("Node at '%s' does not exist" % sop_path)
result.add(node.path())
if output_node.type().name() != "output":
cls.log.error("SOP Path does not end path at output node")
result.add(node.path())
return result

View file

@ -0,0 +1,43 @@
import pyblish.api
class ValidateOutputNode(pyblish.api.InstancePlugin):
"""Validate if output node:
- exists
- is of type 'output'
- has an input"""
order = pyblish.api.ValidatorOrder
families = ["*"]
hosts = ["houdini"]
label = "Validate Output Node"
def process(self, instance):
invalid = self.get_invalid(instance)
if invalid:
raise RuntimeError("Output node(s) `%s` are incorrect" % invalid)
@classmethod
def get_invalid(cls, instance):
output_node = instance.data["output_node"]
if output_node is None:
node = instance[0]
cls.log.error("Output node at '%s' does not exist, see source" %
node.path())
return node.path()
# Check if type is correct
if output_node.type().name() != "output":
cls.log.error("Output node `%s` is not if type `output`" %
output_node.path())
return output_node.path()
# Check if node has incoming connections
if not output_node.inputConnections():
cls.log.error("Output node `%s` has no incoming connections"
% output_node.path())
return output_node.path()

View file

@ -2,7 +2,7 @@ import colorbleed.maya.plugin
class AbcLoader(colorbleed.maya.plugin.ReferenceLoader):
"""Specific loader of Alembic for the avalon.animation family"""
"""Specific loader of Alembic for the colorbleed.animation family"""
families = ["colorbleed.animation",
"colorbleed.pointcache"]

View file

@ -2,7 +2,7 @@ import colorbleed.maya.plugin
class CameraLoader(colorbleed.maya.plugin.ReferenceLoader):
"""Specific loader of Alembic for the avalon.animation family"""
"""Specific loader of Alembic for the colorbleed.camera family"""
families = ["colorbleed.camera"]
label = "Reference camera"

View file

@ -0,0 +1,64 @@
from avalon import api
# import colorbleed.maya.plugin
class LoadVDBtoVRay(api.Loader):
families = ["colorbleed.vdbcache"]
representations = ["vdb"]
name = "Load VDB to VRay"
icon = "cloud"
color = "orange"
def load(self, context, name, namespace, data):
# import pprint
from maya import cmds
import avalon.maya.lib as lib
from avalon.maya.pipeline import containerise
# Check if viewport drawing engine is Open GL Core (compat)
render_engine = None
compatible = "OpenGLCoreProfileCompat"
if cmds.optionVar(exists="vp2RenderingEngine"):
render_engine = cmds.optionVar(query="vp2RenderingEngine")
if not render_engine or render_engine != compatible:
raise RuntimeError("Current scene's settings are incompatible."
"See Preferences > Display > Viewport 2.0 to "
"set the render engine to '%s'" % compatible)
asset = context['asset']
version = context["version"]
asset_name = asset["name"]
namespace = namespace or lib.unique_namespace(
asset_name + "_",
prefix="_" if asset_name[0].isdigit() else "",
suffix="_",
)
# Root group
label = "{}:{}".format(namespace, name)
root = cmds.group(name=label, empty=True)
# Create VR
grid_node = cmds.createNode("VRayVolumeGrid",
name="{}VVGShape".format(label),
parent=root)
# Set attributes
cmds.setAttr("{}.inFile".format(grid_node), self.fname, type="string")
cmds.setAttr("{}.inReadOffset".format(grid_node),
version["startFrames"])
nodes = [root, grid_node]
self[:] = nodes
return containerise(
name=name,
namespace=namespace,
nodes=nodes,
context=context,
loader=self.__class__.__name__)