mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-27 14:22:37 +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
|
||||
|
||||
|
||||
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):
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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()})
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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 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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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()]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue