mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 16:34:53 +01:00
Merge pull request #173 from aardschok/PLN-171
Major improvements on Houdini pipeline, both for code and user experience.
This commit is contained in:
commit
e19a412339
12 changed files with 237 additions and 89 deletions
|
|
@ -95,6 +95,88 @@ def get_additional_data(container):
|
||||||
return 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
|
@contextmanager
|
||||||
def attribute_values(node, data):
|
def attribute_values(node, data):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,11 @@ class CreateAlembicCamera(houdini.Creator):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(CreateAlembicCamera, self).__init__(*args, **kwargs)
|
super(CreateAlembicCamera, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
# create an ordered dict with the existing data first
|
# Remove the active, we are checking the bypass flag of the nodes
|
||||||
data = OrderedDict(**self.data)
|
self.data.pop("active", None)
|
||||||
|
|
||||||
# Set node type to create for output
|
# Set node type to create for output
|
||||||
data["node_type"] = "alembic"
|
self.data.update({"node_type": "alembic"})
|
||||||
|
|
||||||
self.data = data
|
|
||||||
|
|
||||||
def process(self):
|
def process(self):
|
||||||
instance = super(CreateAlembicCamera, self).process()
|
instance = super(CreateAlembicCamera, self).process()
|
||||||
|
|
@ -27,7 +25,7 @@ class CreateAlembicCamera(houdini.Creator):
|
||||||
parms = {"use_sop_path": True,
|
parms = {"use_sop_path": True,
|
||||||
"build_from_path": True,
|
"build_from_path": True,
|
||||||
"path_attrib": "path",
|
"path_attrib": "path",
|
||||||
"filename": "$HIP/%s.abc" % self.name}
|
"filename": "$HIP/pyblish/%s.abc" % self.name}
|
||||||
|
|
||||||
if self.nodes:
|
if self.nodes:
|
||||||
node = self.nodes[0]
|
node = self.nodes[0]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
from avalon import houdini
|
from avalon import houdini
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -14,21 +12,19 @@ class CreatePointCache(houdini.Creator):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(CreatePointCache, self).__init__(*args, **kwargs)
|
super(CreatePointCache, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
# create an ordered dict with the existing data first
|
# Remove the active, we are checking the bypass flag of the nodes
|
||||||
data = OrderedDict(**self.data)
|
self.data.pop("active", None)
|
||||||
|
|
||||||
# Set node type to create for output
|
self.data.update({"node_type": "alembic"})
|
||||||
data["node_type"] = "alembic"
|
|
||||||
|
|
||||||
self.data = data
|
|
||||||
|
|
||||||
def process(self):
|
def process(self):
|
||||||
instance = super(CreatePointCache, self).process()
|
instance = super(CreatePointCache, self).process()
|
||||||
|
|
||||||
parms = {"use_sop_path": True,
|
parms = {"use_sop_path": True, # Export single node from SOP Path
|
||||||
"build_from_path": True,
|
"build_from_path": True, # Direct path of primitive in output
|
||||||
"path_attrib": "path",
|
"path_attrib": "path", # Pass path attribute for output
|
||||||
"filename": "$HIP/%s.abc" % self.name}
|
"format": 2, # Set format to Ogawa
|
||||||
|
"filename": "$HIP/pyblish/%s.abc" % self.name}
|
||||||
|
|
||||||
if self.nodes:
|
if self.nodes:
|
||||||
node = self.nodes[0]
|
node = self.nodes[0]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
from avalon import houdini
|
from avalon import houdini
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -14,18 +12,20 @@ class CreateVDBCache(houdini.Creator):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(CreateVDBCache, self).__init__(*args, **kwargs)
|
super(CreateVDBCache, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
# create an ordered dict with the existing data first
|
# Remove the active, we are checking the bypass flag of the nodes
|
||||||
data = OrderedDict(**self.data)
|
self.data.pop("active", None)
|
||||||
|
|
||||||
# Set node type to create for output
|
self.data.update({
|
||||||
data["node_type"] = "geometry"
|
"node_type": "geometry", # Set node type to create for output
|
||||||
|
"executeBackground": True # Render node in background
|
||||||
self.data = data
|
})
|
||||||
|
|
||||||
def process(self):
|
def process(self):
|
||||||
instance = super(CreateVDBCache, self).process()
|
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:
|
if self.nodes:
|
||||||
parms.update({"soppath": self.nodes[0].path()})
|
parms.update({"soppath": self.nodes[0].path()})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ class AbcLoader(api.Loader):
|
||||||
container = obj.createNode("geo", node_name=node_name)
|
container = obj.createNode("geo", node_name=node_name)
|
||||||
|
|
||||||
# Remove the file node, it only loads static meshes
|
# 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()
|
file_node.destroy()
|
||||||
|
|
||||||
# Create an alembic node (supports animation)
|
# Create an alembic node (supports animation)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
import pyblish.api
|
|
||||||
|
|
||||||
|
|
||||||
class CollectAlembicNodes(pyblish.api.InstancePlugin):
|
|
||||||
|
|
||||||
label = "Collect Alembic Nodes"
|
|
||||||
|
|
||||||
def process(self, instance):
|
|
||||||
pass
|
|
||||||
66
colorbleed/plugins/houdini/publish/collect_frames.py
Normal file
66
colorbleed/plugins/houdini/publish/collect_frames.py
Normal 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
|
||||||
|
|
@ -38,13 +38,15 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
||||||
if not node.parm("id"):
|
if not node.parm("id"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if node.parm("id").eval() != "pyblish.avalon.instance":
|
if node.evalParm("id") != "pyblish.avalon.instance":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
has_family = node.parm("family").eval()
|
has_family = node.evalParm("family")
|
||||||
assert has_family, "'%s' is missing 'family'" % node.name()
|
assert has_family, "'%s' is missing 'family'" % node.name()
|
||||||
|
|
||||||
data = lib.read(node)
|
data = lib.read(node)
|
||||||
|
# Check bypass state and reverse
|
||||||
|
data.update({"active": not node.isBypassed()})
|
||||||
|
|
||||||
# temporarily translation of `active` to `publish` till issue has
|
# temporarily translation of `active` to `publish` till issue has
|
||||||
# been resolved, https://github.com/pyblish/pyblish-base/issues/307
|
# been resolved, https://github.com/pyblish/pyblish-base/issues/307
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
|
|
||||||
import pyblish.api
|
import pyblish.api
|
||||||
import colorbleed.api
|
import colorbleed.api
|
||||||
|
|
@ -17,26 +16,21 @@ class ExtractVDBCache(colorbleed.api.Extractor):
|
||||||
ropnode = instance[0]
|
ropnode = instance[0]
|
||||||
|
|
||||||
# Get the filename from the filename parameter
|
# Get the filename from the filename parameter
|
||||||
# `.eval()` will make sure all tokens are resolved
|
# `.evalParm(parameter)` will make sure all tokens are resolved
|
||||||
output = ropnode.parm("sopoutput").eval()
|
sop_output = ropnode.evalParm("sopoutput")
|
||||||
staging_dir = os.path.dirname(output)
|
staging_dir = os.path.normpath(os.path.dirname(sop_output))
|
||||||
instance.data["stagingDir"] = staging_dir
|
instance.data["stagingDir"] = staging_dir
|
||||||
|
|
||||||
# Replace the 4 digits to match file sequence token '%04d' if we have
|
if instance.data.get("executeBackground", True):
|
||||||
# a sequence of frames
|
self.log.info("Creating background task..")
|
||||||
file_name = os.path.basename(output)
|
ropnode.parm("executebackground").pressButton()
|
||||||
has_frame = re.match("\w\.(d+)\.vdb", file_name)
|
self.log.info("Finished")
|
||||||
if has_frame:
|
else:
|
||||||
frame_nr = has_frame.group()
|
ropnode.render()
|
||||||
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:
|
if "files" not in instance.data:
|
||||||
instance.data["files"] = []
|
instance.data["files"] = []
|
||||||
|
|
||||||
instance.data["files"].append(file_name)
|
output = instance.data["frames"]
|
||||||
|
|
||||||
|
instance.data["files"].append(output)
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
@ -33,7 +33,7 @@ class ValidateOutputNode(pyblish.api.InstancePlugin):
|
||||||
# Check if type is correct
|
# Check if type is correct
|
||||||
type_name = output_node.type().name()
|
type_name = output_node.type().name()
|
||||||
if type_name not in ["output", "cam"]:
|
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`" %
|
"or `camera`" %
|
||||||
output_node.path())
|
output_node.path())
|
||||||
return [output_node.path()]
|
return [output_node.path()]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue