diff --git a/colorbleed/plugins/global/publish/integrate.py b/colorbleed/plugins/global/publish/integrate.py index edcb9ded77..f5d07b339e 100644 --- a/colorbleed/plugins/global/publish/integrate.py +++ b/colorbleed/plugins/global/publish/integrate.py @@ -30,6 +30,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "colorbleed.mayaAscii", "colorbleed.model", "colorbleed.pointcache", + "colorbleed.vdbcache", "colorbleed.setdress", "colorbleed.rig", "colorbleed.vrayproxy", diff --git a/colorbleed/plugins/houdini/create/create_pointcache.py b/colorbleed/plugins/houdini/create/create_pointcache.py index 93cd83a6b3..9169135245 100644 --- a/colorbleed/plugins/houdini/create/create_pointcache.py +++ b/colorbleed/plugins/houdini/create/create_pointcache.py @@ -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) diff --git a/colorbleed/plugins/houdini/create/create_vbd_cache.py b/colorbleed/plugins/houdini/create/create_vbd_cache.py new file mode 100644 index 0000000000..b103a046fa --- /dev/null +++ b/colorbleed/plugins/houdini/create/create_vbd_cache.py @@ -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) diff --git a/colorbleed/plugins/houdini/load/load_alembic.py b/colorbleed/plugins/houdini/load/load_alembic.py index 5043ba5a0d..94e3894d05 100644 --- a/colorbleed/plugins/houdini/load/load_alembic.py +++ b/colorbleed/plugins/houdini/load/load_alembic.py @@ -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" diff --git a/colorbleed/plugins/houdini/load/load_camera.py b/colorbleed/plugins/houdini/load/load_camera.py new file mode 100644 index 0000000000..f827244c6b --- /dev/null +++ b/colorbleed/plugins/houdini/load/load_camera.py @@ -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)) diff --git a/colorbleed/plugins/houdini/publish/collect_current_file.py b/colorbleed/plugins/houdini/publish/collect_current_file.py index e8612bdc12..7852943b34 100644 --- a/colorbleed/plugins/houdini/publish/collect_current_file.py +++ b/colorbleed/plugins/houdini/publish/collect_current_file.py @@ -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 diff --git a/colorbleed/plugins/houdini/publish/collect_instances.py b/colorbleed/plugins/houdini/publish/collect_instances.py index 5f77b9d805..effd6e392e 100644 --- a/colorbleed/plugins/houdini/publish/collect_instances.py +++ b/colorbleed/plugins/houdini/publish/collect_instances.py @@ -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 diff --git a/colorbleed/plugins/houdini/publish/collect_output_node.py b/colorbleed/plugins/houdini/publish/collect_output_node.py new file mode 100644 index 0000000000..dbfe8a5890 --- /dev/null +++ b/colorbleed/plugins/houdini/publish/collect_output_node.py @@ -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 diff --git a/colorbleed/plugins/houdini/publish/collection_animation.py b/colorbleed/plugins/houdini/publish/collection_animation.py new file mode 100644 index 0000000000..835e52eb68 --- /dev/null +++ b/colorbleed/plugins/houdini/publish/collection_animation.py @@ -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) diff --git a/colorbleed/plugins/houdini/publish/extract_alembic.py b/colorbleed/plugins/houdini/publish/extract_alembic.py index 098020b905..f66b5bde72 100644 --- a/colorbleed/plugins/houdini/publish/extract_alembic.py +++ b/colorbleed/plugins/houdini/publish/extract_alembic.py @@ -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"] = [] diff --git a/colorbleed/plugins/houdini/publish/extract_vdb_cache.py b/colorbleed/plugins/houdini/publish/extract_vdb_cache.py new file mode 100644 index 0000000000..ad85a5daf0 --- /dev/null +++ b/colorbleed/plugins/houdini/publish/extract_vdb_cache.py @@ -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) diff --git a/colorbleed/plugins/houdini/publish/valiate_vdb_input_node.py b/colorbleed/plugins/houdini/publish/valiate_vdb_input_node.py new file mode 100644 index 0000000000..24606046fa --- /dev/null +++ b/colorbleed/plugins/houdini/publish/valiate_vdb_input_node.py @@ -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] diff --git a/colorbleed/plugins/houdini/publish/validate_alembic_input_node.py b/colorbleed/plugins/houdini/publish/validate_alembic_input_node.py new file mode 100644 index 0000000000..91f9e9f97e --- /dev/null +++ b/colorbleed/plugins/houdini/publish/validate_alembic_input_node.py @@ -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] diff --git a/colorbleed/plugins/houdini/publish/validate_mkpaths_toggled.py b/colorbleed/plugins/houdini/publish/validate_mkpaths_toggled.py index 8dcc6e0509..7e298ce952 100644 --- a/colorbleed/plugins/houdini/publish/validate_mkpaths_toggled.py +++ b/colorbleed/plugins/houdini/publish/validate_mkpaths_toggled.py @@ -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: diff --git a/colorbleed/plugins/houdini/publish/validate_outnode_exists.py b/colorbleed/plugins/houdini/publish/validate_outnode_exists.py index 479579a8f0..011a1f63e7 100644 --- a/colorbleed/plugins/houdini/publish/validate_outnode_exists.py +++ b/colorbleed/plugins/houdini/publish/validate_outnode_exists.py @@ -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 diff --git a/colorbleed/plugins/houdini/publish/validate_output_node.py b/colorbleed/plugins/houdini/publish/validate_output_node.py new file mode 100644 index 0000000000..be7551cf0d --- /dev/null +++ b/colorbleed/plugins/houdini/publish/validate_output_node.py @@ -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() diff --git a/colorbleed/plugins/maya/load/load_alembic.py b/colorbleed/plugins/maya/load/load_alembic.py index afea761ab8..d34530a8d5 100644 --- a/colorbleed/plugins/maya/load/load_alembic.py +++ b/colorbleed/plugins/maya/load/load_alembic.py @@ -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"] diff --git a/colorbleed/plugins/maya/load/load_camera.py b/colorbleed/plugins/maya/load/load_camera.py index 1d66433a6f..067c4c0cde 100644 --- a/colorbleed/plugins/maya/load/load_camera.py +++ b/colorbleed/plugins/maya/load/load_camera.py @@ -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" diff --git a/colorbleed/plugins/maya/load/load_vdb_to_vray.py b/colorbleed/plugins/maya/load/load_vdb_to_vray.py new file mode 100644 index 0000000000..45294845d4 --- /dev/null +++ b/colorbleed/plugins/maya/load/load_vdb_to_vray.py @@ -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__)