Merge pull request #173 from aardschok/PLN-171

Major improvements on Houdini pipeline, both for code and user experience.
This commit is contained in:
Wijnand Koreman 2018-10-02 12:54:23 +02:00 committed by GitHub
commit e19a412339
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 237 additions and 89 deletions

View file

@ -95,6 +95,88 @@ def get_additional_data(container):
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")
else:
raise TypeError("Node type '%s' not supported" % node_type)
@contextmanager
def attribute_values(node, data):

View file

@ -13,13 +13,11 @@ class CreateAlembicCamera(houdini.Creator):
def __init__(self, *args, **kwargs):
super(CreateAlembicCamera, self).__init__(*args, **kwargs)
# create an ordered dict with the existing data first
data = OrderedDict(**self.data)
# Remove the active, we are checking the bypass flag of the nodes
self.data.pop("active", None)
# Set node type to create for output
data["node_type"] = "alembic"
self.data = data
self.data.update({"node_type": "alembic"})
def process(self):
instance = super(CreateAlembicCamera, self).process()
@ -27,7 +25,7 @@ class CreateAlembicCamera(houdini.Creator):
parms = {"use_sop_path": True,
"build_from_path": True,
"path_attrib": "path",
"filename": "$HIP/%s.abc" % self.name}
"filename": "$HIP/pyblish/%s.abc" % self.name}
if self.nodes:
node = self.nodes[0]

View file

@ -1,5 +1,3 @@
from collections import OrderedDict
from avalon import houdini
@ -14,21 +12,19 @@ class CreatePointCache(houdini.Creator):
def __init__(self, *args, **kwargs):
super(CreatePointCache, self).__init__(*args, **kwargs)
# create an ordered dict with the existing data first
data = OrderedDict(**self.data)
# Remove the active, we are checking the bypass flag of the nodes
self.data.pop("active", None)
# Set node type to create for output
data["node_type"] = "alembic"
self.data = data
self.data.update({"node_type": "alembic"})
def process(self):
instance = super(CreatePointCache, self).process()
parms = {"use_sop_path": True,
"build_from_path": True,
"path_attrib": "path",
"filename": "$HIP/%s.abc" % self.name}
parms = {"use_sop_path": True, # Export single node from SOP Path
"build_from_path": True, # Direct path of primitive in output
"path_attrib": "path", # Pass path attribute for output
"format": 2, # Set format to Ogawa
"filename": "$HIP/pyblish/%s.abc" % self.name}
if self.nodes:
node = self.nodes[0]

View file

@ -1,5 +1,3 @@
from collections import OrderedDict
from avalon import houdini
@ -14,18 +12,20 @@ class CreateVDBCache(houdini.Creator):
def __init__(self, *args, **kwargs):
super(CreateVDBCache, self).__init__(*args, **kwargs)
# create an ordered dict with the existing data first
data = OrderedDict(**self.data)
# Remove the active, we are checking the bypass flag of the nodes
self.data.pop("active", None)
# Set node type to create for output
data["node_type"] = "geometry"
self.data = data
self.data.update({
"node_type": "geometry", # Set node type to create for output
"executeBackground": True # Render node in background
})
def process(self):
instance = super(CreateVDBCache, self).process()
parms = {"sopoutput": "$HIP/geo/%s.$F4.vdb" % self.name}
parms = {"sopoutput": "$HIP/pyblish/%s.$F4.vdb" % self.name,
"initsim": True}
if self.nodes:
parms.update({"soppath": self.nodes[0].path()})

View file

@ -42,7 +42,7 @@ class AbcLoader(api.Loader):
container = obj.createNode("geo", node_name=node_name)
# Remove the file node, it only loads static meshes
file_node = hou.node("/obj/{}/file1".format(node_name))
file_node = container.node("file1".format(node_name))
file_node.destroy()
# Create an alembic node (supports animation)

View file

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

View file

@ -0,0 +1,66 @@
import os
import re
import pyblish.api
from colorbleed.houdini import lib
class CollectFrames(pyblish.api.InstancePlugin):
"""Collect all frames which would be a resukl"""
order = pyblish.api.CollectorOrder
label = "Collect Frames"
families = ["colorbleed.vdbcache"]
def process(self, instance):
ropnode = instance[0]
output_parm = lib.get_output_parameter(ropnode)
output = output_parm.eval()
file_name = os.path.basename(output)
match = re.match("(\w+)\.(\d+)\.vdb", file_name)
result = file_name
start_frame = instance.data.get("startFrame", None)
end_frame = instance.data.get("endFrame", None)
if match and start_frame is not None:
# Check if frames are bigger than 1 (file collection)
# override the result
if end_frame - start_frame > 1:
result = self.create_file_list(match,
int(start_frame),
int(end_frame))
instance.data.update({"frames": result})
def create_file_list(self, match, start_frame, end_frame):
"""Collect files based on frame range and regex.match
Args:
match(re.match): match object
start_frame(int): start of the animation
end_frame(int): end of the animation
Returns:
list
"""
result = []
padding = len(match.group(2))
name = match.group(1)
padding_format = "{number:0{width}d}"
count = start_frame
while count <= end_frame:
str_count = padding_format.format(number=count, width=padding)
file_name = "{}.{}.vdb".format(name, str_count)
result.append(file_name)
count += 1
return result

View file

@ -38,13 +38,15 @@ class CollectInstances(pyblish.api.ContextPlugin):
if not node.parm("id"):
continue
if node.parm("id").eval() != "pyblish.avalon.instance":
if node.evalParm("id") != "pyblish.avalon.instance":
continue
has_family = node.parm("family").eval()
has_family = node.evalParm("family")
assert has_family, "'%s' is missing 'family'" % node.name()
data = lib.read(node)
# Check bypass state and reverse
data.update({"active": not node.isBypassed()})
# temporarily translation of `active` to `publish` till issue has
# been resolved, https://github.com/pyblish/pyblish-base/issues/307

View file

@ -1,31 +0,0 @@
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.evalParm(parm) for parm, name in
parameters.items()}
instance.data.update(data)

View file

@ -1,5 +1,4 @@
import os
import re
import pyblish.api
import colorbleed.api
@ -17,26 +16,21 @@ class ExtractVDBCache(colorbleed.api.Extractor):
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)
# `.evalParm(parameter)` will make sure all tokens are resolved
sop_output = ropnode.evalParm("sopoutput")
staging_dir = os.path.normpath(os.path.dirname(sop_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 instance.data.get("executeBackground", True):
self.log.info("Creating background task..")
ropnode.parm("executebackground").pressButton()
self.log.info("Finished")
else:
ropnode.render()
if "files" not in instance.data:
instance.data["files"] = []
instance.data["files"].append(file_name)
output = instance.data["frames"]
instance.data["files"].append(output)

View file

@ -0,0 +1,50 @@
import pyblish.api
from colorbleed.houdini import lib
class ValidateAnimationSettings(pyblish.api.InstancePlugin):
"""Validate if the unexpanded string contains the frame ('$F') token
This validator will only check the output parameter of the node if
the Valid Frame Range is not set to 'Render Current Frame'
Rules:
If you render out a frame range it is mandatory to have the
frame token - '$F4' or similar - to ensure that each frame gets
written. If this is not the case you will override the same file
every time a frame is written out.
Examples:
Good: 'my_vbd_cache.$F4.vdb'
Bad: 'my_vbd_cache.vdb'
"""
order = pyblish.api.ValidatorOrder
label = "Validate Frame Settings"
families = ["colorbleed.vdbcache"]
def process(self, instance):
invalid = self.get_invalid(instance)
if invalid:
raise RuntimeError("Output settings do no match for '%s'" %
instance)
@classmethod
def get_invalid(cls, instance):
node = instance[0]
# Check trange parm, 0 means Render Current Frame
frame_range = node.evalParm("trange")
if frame_range == 0:
return []
output_parm = lib.get_output_parameter(node)
unexpanded_str = output_parm.unexpandedString()
if "$F" not in unexpanded_str:
cls.log.error("No frame token found in '%s'" % node.path())
return [instance]

View file

@ -33,7 +33,7 @@ class ValidateOutputNode(pyblish.api.InstancePlugin):
# Check if type is correct
type_name = output_node.type().name()
if type_name not in ["output", "cam"]:
cls.log.error("Output node `%s` is an accepted type `output` "
cls.log.error("Output node `%s` is not an accepted type `output` "
"or `camera`" %
output_node.path())
return [output_node.path()]