diff --git a/colorbleed/houdini/__init__.py b/colorbleed/houdini/__init__.py index e4640e2857..05d5195605 100644 --- a/colorbleed/houdini/__init__.py +++ b/colorbleed/houdini/__init__.py @@ -39,6 +39,8 @@ def install(): avalon.on("save", on_save) avalon.on("open", on_open) + pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled) + log.info("Setting default family states for loader..") avalon.data["familiesStateToggled"] = ["colorbleed.imagesequence"] @@ -91,3 +93,20 @@ def on_open(*args): "your Maya scene.") dialog.on_show.connect(_on_show_inventory) dialog.show() + + +def on_pyblish_instance_toggled(instance, new_value, old_value): + """Toggle saver tool passthrough states on instance toggles.""" + + nodes = instance[:] + if not nodes: + return + + # Assume instance node is first node + instance_node = nodes[0] + + if instance_node.isBypassed() != (not old_value): + print("%s old bypass state didn't match old instance state, " + "updating anyway.." % instance_node.path()) + + instance_node.bypass(not new_value) diff --git a/colorbleed/plugins/houdini/create/create_alembic_camera.py b/colorbleed/plugins/houdini/create/create_alembic_camera.py index 8da8c4d2b5..96449fb3db 100644 --- a/colorbleed/plugins/houdini/create/create_alembic_camera.py +++ b/colorbleed/plugins/houdini/create/create_alembic_camera.py @@ -2,6 +2,7 @@ from avalon import houdini class CreateAlembicCamera(houdini.Creator): + """Single baked camera from Alembic ROP""" name = "camera" label = "Camera (Abc)" @@ -20,11 +21,25 @@ class CreateAlembicCamera(houdini.Creator): def process(self): instance = super(CreateAlembicCamera, self).process() - parms = {"use_sop_path": True, - "filename": "$HIP/pyblish/%s.abc" % self.name} + parms = { + "filename": "$HIP/pyblish/%s.abc" % self.name, + "use_sop_path": False + } if self.nodes: node = self.nodes[0] - parms.update({"sop_path": node.path()}) + path = node.path() + + # Split the node path into the first root and the remainder + # So we can set the root and objects parameters correctly + _, root, remainder = path.split("/", 2) + parms.update({ + "root": "/" + root, + "objects": remainder + }) instance.setParms(parms) + + # Lock the Use Sop Path setting so the + # user doesn't accidentally enable it. + instance.parm("use_sop_path").lock(True) diff --git a/colorbleed/plugins/houdini/create/create_pointcache.py b/colorbleed/plugins/houdini/create/create_pointcache.py index c54a6c91a6..85993678c2 100644 --- a/colorbleed/plugins/houdini/create/create_pointcache.py +++ b/colorbleed/plugins/houdini/create/create_pointcache.py @@ -2,7 +2,7 @@ from avalon import houdini class CreatePointCache(houdini.Creator): - """Alembic pointcache for animated data""" + """Alembic ROP to pointcache""" name = "pointcache" label = "Point Cache" @@ -22,7 +22,7 @@ class CreatePointCache(houdini.Creator): 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\ + "path_attrib": "path", # Pass path attribute for output "prim_to_detail_pattern": "cbId", "format": 2, # Set format to Ogawa "filename": "$HIP/pyblish/%s.abc" % self.name} diff --git a/colorbleed/plugins/houdini/create/create_vbd_cache.py b/colorbleed/plugins/houdini/create/create_vbd_cache.py index 617975a711..ebdb1271ce 100644 --- a/colorbleed/plugins/houdini/create/create_vbd_cache.py +++ b/colorbleed/plugins/houdini/create/create_vbd_cache.py @@ -2,7 +2,7 @@ from avalon import houdini class CreateVDBCache(houdini.Creator): - """Alembic pointcache for animated data""" + """OpenVDB from Geometry ROP""" name = "vbdcache" label = "VDB Cache" diff --git a/colorbleed/plugins/houdini/publish/collect_current_file.py b/colorbleed/plugins/houdini/publish/collect_current_file.py index 7852943b34..ea954c4791 100644 --- a/colorbleed/plugins/houdini/publish/collect_current_file.py +++ b/colorbleed/plugins/houdini/publish/collect_current_file.py @@ -1,3 +1,4 @@ +import os import hou import pyblish.api @@ -12,4 +13,24 @@ class CollectHoudiniCurrentFile(pyblish.api.ContextPlugin): def process(self, context): """Inject the current working file""" - context.data['currentFile'] = hou.hipFile.path() + + filepath = hou.hipFile.path() + if not os.path.exists(filepath): + # By default Houdini will even point a new scene to a path. + # However if the file is not saved at all and does not exist, + # we assume the user never set it. + filepath = "" + + elif os.path.basename(filepath) == "untitled.hip": + # Due to even a new file being called 'untitled.hip' we are unable + # to confirm the current scene was ever saved because the file + # could have existed already. We will allow it if the file exists, + # but show a warning for this edge case to clarify the potential + # false positive. + self.log.warning("Current file is 'untitled.hip' and we are " + "unable to detect whether the current scene is " + "saved correctly.") + + context.data['currentFile'] = filepath + + diff --git a/colorbleed/plugins/houdini/publish/collect_output_node.py b/colorbleed/plugins/houdini/publish/collect_output_node.py index a3f49761b9..d90898944f 100644 --- a/colorbleed/plugins/houdini/publish/collect_output_node.py +++ b/colorbleed/plugins/houdini/publish/collect_output_node.py @@ -5,7 +5,8 @@ class CollectOutputSOPPath(pyblish.api.InstancePlugin): """Collect the out node's SOP Path value.""" order = pyblish.api.CollectorOrder - families = ["*"] + families = ["colorbleed.pointcache", + "colorbleed.vdbcache"] hosts = ["houdini"] label = "Collect Output SOP Path" diff --git a/colorbleed/plugins/houdini/publish/validate_alembic_input_node.py b/colorbleed/plugins/houdini/publish/validate_alembic_input_node.py index 91f9e9f97e..663b1198eb 100644 --- a/colorbleed/plugins/houdini/publish/validate_alembic_input_node.py +++ b/colorbleed/plugins/houdini/publish/validate_alembic_input_node.py @@ -7,7 +7,7 @@ class ValidateAlembicInputNode(pyblish.api.InstancePlugin): The connected node cannot be of the following types for Alembic: - VDB - - Volumne + - Volume """ diff --git a/colorbleed/plugins/houdini/publish/validate_bypass.py b/colorbleed/plugins/houdini/publish/validate_bypass.py new file mode 100644 index 0000000000..9af8a2b9ae --- /dev/null +++ b/colorbleed/plugins/houdini/publish/validate_bypass.py @@ -0,0 +1,34 @@ +import pyblish.api +import colorbleed.api + + +class ValidateBypassed(pyblish.api.InstancePlugin): + """Validate all primitives build hierarchy from attribute when enabled. + + The name of the attribute must exist on the prims and have the same name + as Build Hierarchy from Attribute's `Path Attribute` value on the Alembic + ROP node whenever Build Hierarchy from Attribute is enabled. + + """ + + order = colorbleed.api.ValidateContentsOrder - 0.1 + families = ["*"] + hosts = ["houdini"] + label = "Validate ROP Bypass" + + def process(self, instance): + + invalid = self.get_invalid(instance) + if invalid: + rop = invalid[0] + raise RuntimeError( + "ROP node %s is set to bypass, publishing cannot continue.." % + rop.path() + ) + + @classmethod + def get_invalid(cls, instance): + + rop = instance[0] + if rop.isBypassed(): + return [rop] diff --git a/colorbleed/plugins/houdini/publish/validate_camera_rop.py b/colorbleed/plugins/houdini/publish/validate_camera_rop.py new file mode 100644 index 0000000000..83f4dd5a57 --- /dev/null +++ b/colorbleed/plugins/houdini/publish/validate_camera_rop.py @@ -0,0 +1,41 @@ +import pyblish.api +import colorbleed.api + + +class ValidateCameraROP(pyblish.api.InstancePlugin): + """Validate Camera ROP settings.""" + + order = colorbleed.api.ValidateContentsOrder + families = ['colorbleed.camera'] + hosts = ['houdini'] + label = 'Camera ROP' + + def process(self, instance): + + import hou + + node = instance[0] + if node.parm("use_sop_path").eval(): + raise RuntimeError("Alembic ROP for Camera export should not be " + "set to 'Use Sop Path'. Please disable.") + + # Get the root and objects parameter of the Alembic ROP node + root = node.parm("root").eval() + objects = node.parm("objects").eval() + assert root, "Root parameter must be set on Alembic ROP" + assert root.startswith("/"), "Root parameter must start with slash /" + assert objects, "Objects parameter must be set on Alembic ROP" + assert len(objects.split(" ")) == 1, "Must have only a single object." + + # Check if the object exists and is a camera + path = root + "/" + objects + camera = hou.node(path) + + if not camera: + raise ValueError("Camera path does not exist: %s" % path) + + if not camera.type().name() == "cam": + raise ValueError("Object set in Alembic ROP is not a camera: " + "%s (type: %s)" % (camera, camera.type().name())) + + diff --git a/colorbleed/plugins/houdini/publish/validate_mkpaths_toggled.py b/colorbleed/plugins/houdini/publish/validate_mkpaths_toggled.py index 1a83565d11..826dedf933 100644 --- a/colorbleed/plugins/houdini/publish/validate_mkpaths_toggled.py +++ b/colorbleed/plugins/houdini/publish/validate_mkpaths_toggled.py @@ -6,7 +6,9 @@ class ValidateIntermediateDirectoriesChecked(pyblish.api.InstancePlugin): """Validate Create Intermediate Directories is enabled on ROP node.""" order = colorbleed.api.ValidateContentsOrder - families = ['colorbleed.pointcache'] + families = ['colorbleed.pointcache', + 'colorbleed.camera', + 'colorbleed.vdbcache'] hosts = ['houdini'] label = 'Create Intermediate Directories Checked' @@ -14,8 +16,8 @@ class ValidateIntermediateDirectoriesChecked(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Found ROP nodes with Create Intermediate " - "Directories turned off") + raise RuntimeError("Found ROP node with Create Intermediate " + "Directories turned off: %s" % invalid) @classmethod def get_invalid(cls, instance): diff --git a/colorbleed/plugins/houdini/publish/validate_output_node.py b/colorbleed/plugins/houdini/publish/validate_output_node.py index bb9eec508e..eb84ea721b 100644 --- a/colorbleed/plugins/houdini/publish/validate_output_node.py +++ b/colorbleed/plugins/houdini/publish/validate_output_node.py @@ -7,13 +7,15 @@ class ValidateOutputNode(pyblish.api.InstancePlugin): This will ensure: - The SOP Path is set. - The SOP Path refers to an existing object. - - The SOP Path node is of type 'output' or 'camera' + - The SOP Path node is a SOP node. - The SOP Path node has at least one input connection (has an input) + - The SOP Path has geometry data. """ order = pyblish.api.ValidatorOrder - families = ["*"] + families = ["colorbleed.pointcache", + "colorbleed.vdbcache"] hosts = ["houdini"] label = "Validate Output Node" @@ -27,6 +29,8 @@ class ValidateOutputNode(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): + import hou + output_node = instance.data["output_node"] if output_node is None: @@ -35,18 +39,35 @@ class ValidateOutputNode(pyblish.api.InstancePlugin): "Ensure a valid SOP output path is set." % node.path()) - return node.path() + return [node.path()] - # 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 not an accepted type." - "Expected types: `output` or `camera`" % - output_node.path()) + # Output node must be a Sop node. + if not isinstance(output_node, hou.SopNode): + cls.log.error("Output node %s is not a SOP node. " + "SOP Path must point to a SOP node, " + "instead found category type: %s" % ( + output_node.path(), + output_node.type().category().name() + ) + ) return [output_node.path()] + # For the sake of completeness also assert the category type + # is Sop to avoid potential edge case scenarios even though + # the isinstance check above should be stricter than this category + assert output_node.type().category().name() == "Sop", ( + "Output node %s is not of category Sop. This is a bug.." % + output_node.path() + ) + # Check if output node has incoming connections - if type_name == "output" and not output_node.inputConnections(): + if output_node.inputConnections(): cls.log.error("Output node `%s` has no incoming connections" % output_node.path()) return [output_node.path()] + + # Ensure the output node has at least Geometry data + if not output_node.geometry(): + cls.log.error("Output node `%s` has no geometry data." + % output_node.path()) + return [output_node.path()]