diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index eff98c05f1..a3f691e1fc 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -651,6 +651,57 @@ def get_color_management_preferences(): } +def get_obj_node_output(obj_node): + """Find output node. + + If the node has any output node return the + output node with the minimum `outputidx`. + When no output is present return the node + with the display flag set. If no output node is + detected then None is returned. + + Arguments: + node (hou.Node): The node to retrieve a single + the output node for. + + Returns: + Optional[hou.Node]: The child output node. + + """ + + outputs = obj_node.subnetOutputs() + if not outputs: + return + + elif len(outputs) == 1: + return outputs[0] + + else: + return min(outputs, + key=lambda node: node.evalParm('outputidx')) + + +def get_output_children(output_node, include_sops=True): + """Recursively return a list of all output nodes + contained in this node including this node. + + It works in a similar manner to output_node.allNodes(). + """ + out_list = [output_node] + + if output_node.childTypeCategory() == hou.objNodeTypeCategory(): + for child in output_node.children(): + out_list += get_output_children(child, include_sops=include_sops) + + elif include_sops and \ + output_node.childTypeCategory() == hou.sopNodeTypeCategory(): + out = get_obj_node_output(output_node) + if out: + out_list += [out] + + return out_list + + def get_resolution_from_doc(doc): """Get resolution from the given asset document. """ diff --git a/openpype/hosts/houdini/plugins/create/create_staticmesh.py b/openpype/hosts/houdini/plugins/create/create_staticmesh.py new file mode 100644 index 0000000000..ea0b36f03f --- /dev/null +++ b/openpype/hosts/houdini/plugins/create/create_staticmesh.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +"""Creator for Unreal Static Meshes.""" +from openpype.hosts.houdini.api import plugin +from openpype.lib import BoolDef, EnumDef + +import hou + + +class CreateStaticMesh(plugin.HoudiniCreator): + """Static Meshes as FBX. """ + + identifier = "io.openpype.creators.houdini.staticmesh.fbx" + label = "Static Mesh (FBX)" + family = "staticMesh" + icon = "fa5s.cubes" + + default_variants = ["Main"] + + def create(self, subset_name, instance_data, pre_create_data): + + instance_data.update({"node_type": "filmboxfbx"}) + + instance = super(CreateStaticMesh, self).create( + subset_name, + instance_data, + pre_create_data) + + # get the created rop node + instance_node = hou.node(instance.get("instance_node")) + + # prepare parms + output_path = hou.text.expandString( + "$HIP/pyblish/{}.fbx".format(subset_name) + ) + + parms = { + "startnode": self.get_selection(), + "sopoutput": output_path, + # vertex cache format + "vcformat": pre_create_data.get("vcformat"), + "convertunits": pre_create_data.get("convertunits"), + # set render range to use frame range start-end frame + "trange": 1, + "createsubnetroot": pre_create_data.get("createsubnetroot") + } + + # set parms + instance_node.setParms(parms) + + # Lock any parameters in this list + to_lock = ["family", "id"] + self.lock_parameters(instance_node, to_lock) + + def get_network_categories(self): + return [ + hou.ropNodeTypeCategory(), + hou.sopNodeTypeCategory() + ] + + def get_pre_create_attr_defs(self): + """Add settings for users. """ + + attrs = super(CreateStaticMesh, self).get_pre_create_attr_defs() + createsubnetroot = BoolDef("createsubnetroot", + tooltip="Create an extra root for the " + "Export node when it's a " + "subnetwork. This causes the " + "exporting subnetwork node to be " + "represented in the FBX file.", + default=False, + label="Create Root for Subnet") + vcformat = EnumDef("vcformat", + items={ + 0: "Maya Compatible (MC)", + 1: "3DS MAX Compatible (PC2)" + }, + default=0, + label="Vertex Cache Format") + convert_units = BoolDef("convertunits", + tooltip="When on, the FBX is converted" + "from the current Houdini " + "system units to the native " + "FBX unit of centimeters.", + default=False, + label="Convert Units") + + return attrs + [createsubnetroot, vcformat, convert_units] + + def get_dynamic_data( + self, variant, task_name, asset_doc, project_name, host_name, instance + ): + """ + The default subset name templates for Unreal include {asset} and thus + we should pass that along as dynamic data. + """ + dynamic_data = super(CreateStaticMesh, self).get_dynamic_data( + variant, task_name, asset_doc, project_name, host_name, instance + ) + dynamic_data["asset"] = asset_doc["name"] + return dynamic_data + + def get_selection(self): + """Selection Logic. + + how self.selected_nodes should be processed to get + the desirable node from selection. + + Returns: + str : node path + """ + + selection = "" + + if self.selected_nodes: + selected_node = self.selected_nodes[0] + + # Accept sop level nodes (e.g. /obj/geo1/box1) + if isinstance(selected_node, hou.SopNode): + selection = selected_node.path() + self.log.debug( + "Valid SopNode selection, 'Export' in filmboxfbx" + " will be set to '%s'.", selected_node + ) + + # Accept object level nodes (e.g. /obj/geo1) + elif isinstance(selected_node, hou.ObjNode): + selection = selected_node.path() + self.log.debug( + "Valid ObjNode selection, 'Export' in filmboxfbx " + "will be set to the child path '%s'.", selection + ) + + else: + self.log.debug( + "Selection isn't valid. 'Export' in " + "filmboxfbx will be empty." + ) + else: + self.log.debug( + "No Selection. 'Export' in filmboxfbx will be empty." + ) + + return selection diff --git a/openpype/hosts/houdini/plugins/load/load_fbx.py b/openpype/hosts/houdini/plugins/load/load_fbx.py new file mode 100644 index 0000000000..cac22d62d4 --- /dev/null +++ b/openpype/hosts/houdini/plugins/load/load_fbx.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- +"""Fbx Loader for houdini. """ +from openpype.pipeline import ( + load, + get_representation_path, +) +from openpype.hosts.houdini.api import pipeline + + +class FbxLoader(load.LoaderPlugin): + """Load fbx files. """ + + label = "Load FBX" + icon = "code-fork" + color = "orange" + + order = -10 + + families = ["staticMesh", "fbx"] + representations = ["fbx"] + + def load(self, context, name=None, namespace=None, data=None): + + # get file path from context + file_path = self.filepath_from_context(context) + file_path = file_path.replace("\\", "/") + + # get necessary data + namespace, node_name = self.get_node_name(context, name, namespace) + + # create load tree + nodes = self.create_load_node_tree(file_path, node_name, name) + + self[:] = nodes + + # Call containerise function which does some automations for you + # like moving created nodes to the AVALON_CONTAINERS subnetwork + containerised_nodes = pipeline.containerise( + node_name, + namespace, + nodes, + context, + self.__class__.__name__, + suffix="", + ) + + return containerised_nodes + + def update(self, container, representation): + + node = container["node"] + try: + file_node = next( + n for n in node.children() if n.type().name() == "file" + ) + except StopIteration: + self.log.error("Could not find node of type `file`") + return + + # Update the file path from representation + file_path = get_representation_path(representation) + file_path = file_path.replace("\\", "/") + + file_node.setParms({"file": file_path}) + + # Update attribute + node.setParms({"representation": str(representation["_id"])}) + + def remove(self, container): + + node = container["node"] + node.destroy() + + def switch(self, container, representation): + self.update(container, representation) + + def get_node_name(self, context, name=None, namespace=None): + """Define node name.""" + + if not namespace: + namespace = context["asset"]["name"] + + if namespace: + node_name = "{}_{}".format(namespace, name) + else: + node_name = name + + return namespace, node_name + + def create_load_node_tree(self, file_path, node_name, subset_name): + """Create Load network. + + you can start building your tree at any obj level. + it'll be much easier to build it in the root obj level. + + Afterwards, your tree will be automatically moved to + '/obj/AVALON_CONTAINERS' subnetwork. + """ + import hou + + # Get the root obj level + obj = hou.node("/obj") + + # Create a new obj geo node + parent_node = obj.createNode("geo", node_name=node_name) + + # In older houdini, + # when reating a new obj geo node, a default file node will be + # automatically created. + # so, we will delete it if exists. + file_node = parent_node.node("file1") + if file_node: + file_node.destroy() + + # Create a new file node + file_node = parent_node.createNode("file", node_name=node_name) + file_node.setParms({"file": file_path}) + + # Create attribute delete + attribdelete_name = "attribdelete_{}".format(subset_name) + attribdelete = parent_node.createNode("attribdelete", + node_name=attribdelete_name) + attribdelete.setParms({"ptdel": "fbx_*"}) + attribdelete.setInput(0, file_node) + + # Create a Null node + null_name = "OUT_{}".format(subset_name) + null = parent_node.createNode("null", node_name=null_name) + null.setInput(0, attribdelete) + + # Ensure display flag is on the file_node input node and not on the OUT + # node to optimize "debug" displaying in the viewport. + file_node.setDisplayFlag(True) + + # Set new position for children nodes + parent_node.layoutChildren() + + # Return all the nodes + return [parent_node, file_node, attribdelete, null] diff --git a/openpype/hosts/houdini/plugins/publish/collect_output_node.py b/openpype/hosts/houdini/plugins/publish/collect_output_node.py index 0b27678ed0..bca3d9fdc1 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/collect_output_node.py @@ -14,7 +14,8 @@ class CollectOutputSOPPath(pyblish.api.InstancePlugin): "imagesequence", "usd", "usdrender", - "redshiftproxy" + "redshiftproxy", + "staticMesh" ] hosts = ["houdini"] @@ -59,6 +60,10 @@ class CollectOutputSOPPath(pyblish.api.InstancePlugin): elif node_type == "Redshift_Proxy_Output": out_node = node.parm("RS_archive_sopPath").evalAsNode() + + elif node_type == "filmboxfbx": + out_node = node.parm("startnode").evalAsNode() + else: raise KnownPublishError( "ROP node type '{}' is not supported.".format(node_type) diff --git a/openpype/hosts/houdini/plugins/publish/collect_staticmesh_type.py b/openpype/hosts/houdini/plugins/publish/collect_staticmesh_type.py new file mode 100644 index 0000000000..db9efec7a1 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/collect_staticmesh_type.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +"""Collector for staticMesh types. """ + +import pyblish.api + + +class CollectStaticMeshType(pyblish.api.InstancePlugin): + """Collect data type for fbx instance.""" + + hosts = ["houdini"] + families = ["staticMesh"] + label = "Collect type of staticMesh" + + order = pyblish.api.CollectorOrder + + def process(self, instance): + + if instance.data["creator_identifier"] == "io.openpype.creators.houdini.staticmesh.fbx": # noqa: E501 + # Marking this instance as FBX triggers the FBX extractor. + instance.data["families"] += ["fbx"] diff --git a/openpype/hosts/houdini/plugins/publish/extract_fbx.py b/openpype/hosts/houdini/plugins/publish/extract_fbx.py new file mode 100644 index 0000000000..7993b3352f --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/extract_fbx.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +"""Fbx Extractor for houdini. """ + +import os +import pyblish.api +from openpype.pipeline import publish +from openpype.hosts.houdini.api.lib import render_rop + +import hou + + +class ExtractFBX(publish.Extractor): + + label = "Extract FBX" + families = ["fbx"] + hosts = ["houdini"] + + order = pyblish.api.ExtractorOrder + 0.1 + + def process(self, instance): + + # get rop node + ropnode = hou.node(instance.data.get("instance_node")) + output_file = ropnode.evalParm("sopoutput") + + # get staging_dir and file_name + staging_dir = os.path.normpath(os.path.dirname(output_file)) + file_name = os.path.basename(output_file) + + # render rop + self.log.debug("Writing FBX '%s' to '%s'", file_name, staging_dir) + render_rop(ropnode) + + # prepare representation + representation = { + "name": "fbx", + "ext": "fbx", + "files": file_name, + "stagingDir": staging_dir + } + + # A single frame may also be rendered without start/end frame. + if "frameStart" in instance.data and "frameEnd" in instance.data: + representation["frameStart"] = instance.data["frameStart"] + representation["frameEnd"] = instance.data["frameEnd"] + + # set value type for 'representations' key to list + if "representations" not in instance.data: + instance.data["representations"] = [] + + # update instance data + instance.data["stagingDir"] = staging_dir + instance.data["representations"].append(representation) diff --git a/openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py new file mode 100644 index 0000000000..894dad7d72 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +import pyblish.api +from openpype.pipeline import PublishValidationError +from openpype.hosts.houdini.api.action import ( + SelectInvalidAction, + SelectROPAction, +) +from openpype.hosts.houdini.api.lib import get_obj_node_output +import hou + + +class ValidateFBXOutputNode(pyblish.api.InstancePlugin): + """Validate the instance Output Node. + + This will ensure: + - The Output Node Path is set. + - The Output Node Path refers to an existing object. + - The Output Node is a Sop or Obj node. + - The Output Node has geometry data. + - The Output Node doesn't include invalid primitive types. + """ + + order = pyblish.api.ValidatorOrder + families = ["fbx"] + hosts = ["houdini"] + label = "Validate FBX Output Node" + actions = [SelectROPAction, SelectInvalidAction] + + def process(self, instance): + + invalid = self.get_invalid(instance) + if invalid: + nodes = [n.path() for n in invalid] + raise PublishValidationError( + "See log for details. " + "Invalid nodes: {0}".format(nodes), + title="Invalid output node(s)" + ) + + @classmethod + def get_invalid(cls, instance): + output_node = instance.data.get("output_node") + + # Check if The Output Node Path is set and + # refers to an existing object. + if output_node is None: + rop_node = hou.node(instance.data["instance_node"]) + cls.log.error( + "Output node in '%s' does not exist. " + "Ensure a valid output path is set.", rop_node.path() + ) + + return [rop_node] + + # Check if the Output Node is a Sop or an Obj node + # also, list all sop output nodes inside as well as + # invalid empty nodes. + all_out_sops = [] + invalid = [] + + # if output_node is an ObjSubnet or an ObjNetwork + if output_node.childTypeCategory() == hou.objNodeTypeCategory(): + for node in output_node.allSubChildren(): + if node.type().name() == "geo": + out = get_obj_node_output(node) + if out: + all_out_sops.append(out) + else: + invalid.append(node) # empty_objs + cls.log.error( + "Geo Obj Node '%s' is empty!", + node.path() + ) + if not all_out_sops: + invalid.append(output_node) # empty_objs + cls.log.error( + "Output Node '%s' is empty!", + node.path() + ) + + # elif output_node is an ObjNode + elif output_node.type().name() == "geo": + out = get_obj_node_output(output_node) + if out: + all_out_sops.append(out) + else: + invalid.append(node) # empty_objs + cls.log.error( + "Output Node '%s' is empty!", + node.path() + ) + + # elif output_node is a SopNode + elif output_node.type().category().name() == "Sop": + all_out_sops.append(output_node) + + # Then it's a wrong node type + else: + cls.log.error( + "Output node %s is not a SOP or OBJ Geo or OBJ SubNet node. " + "Instead found category type: %s %s", + output_node.path(), output_node.type().category().name(), + output_node.type().name() + ) + return [output_node] + + # Check if all output sop nodes have geometry + # and don't contain invalid prims + invalid_prim_types = ["VDB", "Volume"] + for sop_node in all_out_sops: + # Empty Geometry test + if not hasattr(sop_node, "geometry"): + invalid.append(sop_node) # empty_geometry + cls.log.error( + "Sop node '%s' doesn't include any prims.", + sop_node.path() + ) + continue + + frame = instance.data.get("frameStart", 0) + geo = sop_node.geometryAtFrame(frame) + if len(geo.iterPrims()) == 0: + invalid.append(sop_node) # empty_geometry + cls.log.error( + "Sop node '%s' doesn't include any prims.", + sop_node.path() + ) + continue + + # Invalid Prims test + for prim_type in invalid_prim_types: + if geo.countPrimType(prim_type) > 0: + invalid.append(sop_node) # invalid_prims + cls.log.error( + "Sop node '%s' includes invalid prims of type '%s'.", + sop_node.path(), prim_type + ) + + if invalid: + return invalid diff --git a/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py b/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py new file mode 100644 index 0000000000..b499682e0b --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +"""Validator for correct naming of Static Meshes.""" +import pyblish.api +from openpype.pipeline import ( + PublishValidationError, + OptionalPyblishPluginMixin +) +from openpype.pipeline.publish import ValidateContentsOrder + +from openpype.hosts.houdini.api.action import SelectInvalidAction +from openpype.hosts.houdini.api.lib import get_output_children + + +class ValidateMeshIsStatic(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Validate mesh is static. + + It checks if output node is time dependent. + """ + + families = ["staticMesh"] + hosts = ["houdini"] + label = "Validate Mesh is Static" + order = ValidateContentsOrder + 0.1 + actions = [SelectInvalidAction] + + def process(self, instance): + + invalid = self.get_invalid(instance) + if invalid: + nodes = [n.path() for n in invalid] + raise PublishValidationError( + "See log for details. " + "Invalid nodes: {0}".format(nodes) + ) + + @classmethod + def get_invalid(cls, instance): + + invalid = [] + + output_node = instance.data.get("output_node") + if output_node is None: + cls.log.debug( + "No Output Node, skipping check.." + ) + return + + all_outputs = get_output_children(output_node) + + for output in all_outputs: + if output.isTimeDependent(): + invalid.append(output) + cls.log.error( + "Output node '%s' is time dependent.", + output.path() + ) + + return invalid diff --git a/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py index d9dee38680..9590e37d26 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py @@ -24,7 +24,7 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder families = ["pointcache", "vdbcache"] hosts = ["houdini"] - label = "Validate Output Node" + label = "Validate Output Node (SOP)" actions = [SelectROPAction, SelectInvalidAction] def process(self, instance): diff --git a/openpype/hosts/houdini/plugins/publish/validate_subset_name.py b/openpype/hosts/houdini/plugins/publish/validate_subset_name.py new file mode 100644 index 0000000000..bb3648f361 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/validate_subset_name.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +"""Validator for correct naming of Static Meshes.""" +import pyblish.api +from openpype.pipeline import ( + PublishValidationError, + OptionalPyblishPluginMixin +) +from openpype.pipeline.publish import ( + ValidateContentsOrder, + RepairAction, +) +from openpype.hosts.houdini.api.action import SelectInvalidAction +from openpype.pipeline.create import get_subset_name + +import hou + + +class FixSubsetNameAction(RepairAction): + label = "Fix Subset Name" + + +class ValidateSubsetName(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Validate Subset name. + + """ + + families = ["staticMesh"] + hosts = ["houdini"] + label = "Validate Subset Name" + order = ValidateContentsOrder + 0.1 + actions = [FixSubsetNameAction, SelectInvalidAction] + + optional = True + + def process(self, instance): + + if not self.is_active(instance.data): + return + + invalid = self.get_invalid(instance) + if invalid: + nodes = [n.path() for n in invalid] + raise PublishValidationError( + "See log for details. " + "Invalid nodes: {0}".format(nodes) + ) + + @classmethod + def get_invalid(cls, instance): + + invalid = [] + + rop_node = hou.node(instance.data["instance_node"]) + + # Check subset name + subset_name = get_subset_name( + family=instance.data["family"], + variant=instance.data["variant"], + task_name=instance.data["task"], + asset_doc=instance.data["assetEntity"], + dynamic_data={"asset": instance.data["asset"]} + ) + + if instance.data.get("subset") != subset_name: + invalid.append(rop_node) + cls.log.error( + "Invalid subset name on rop node '%s' should be '%s'.", + rop_node.path(), subset_name + ) + + return invalid + + @classmethod + def repair(cls, instance): + rop_node = hou.node(instance.data["instance_node"]) + + # Check subset name + subset_name = get_subset_name( + family=instance.data["family"], + variant=instance.data["variant"], + task_name=instance.data["task"], + asset_doc=instance.data["assetEntity"], + dynamic_data={"asset": instance.data["asset"]} + ) + + instance.data["subset"] = subset_name + rop_node.parm("subset").set(subset_name) + + cls.log.debug( + "Subset name on rop node '%s' has been set to '%s'.", + rop_node.path(), subset_name + ) diff --git a/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py new file mode 100644 index 0000000000..ae3c7e5602 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +"""Validator for correct naming of Static Meshes.""" +import pyblish.api +from openpype.pipeline import ( + PublishValidationError, + OptionalPyblishPluginMixin +) +from openpype.pipeline.publish import ValidateContentsOrder + +from openpype.hosts.houdini.api.action import SelectInvalidAction +from openpype.hosts.houdini.api.lib import get_output_children + +import hou + + +class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Validate name of Unreal Static Mesh. + + This validator checks if output node name has a collision prefix: + - UBX + - UCP + - USP + - UCX + + This validator also checks if subset name is correct + - {static mesh prefix}_{Asset-Name}{Variant}. + + """ + + families = ["staticMesh"] + hosts = ["houdini"] + label = "Unreal Static Mesh Name (FBX)" + order = ValidateContentsOrder + 0.1 + actions = [SelectInvalidAction] + + optional = True + collision_prefixes = [] + static_mesh_prefix = "" + + @classmethod + def apply_settings(cls, project_settings, system_settings): + + settings = ( + project_settings["houdini"]["create"]["CreateStaticMesh"] + ) + cls.collision_prefixes = settings["collision_prefixes"] + cls.static_mesh_prefix = settings["static_mesh_prefix"] + + def process(self, instance): + + if not self.is_active(instance.data): + return + + invalid = self.get_invalid(instance) + if invalid: + nodes = [n.path() for n in invalid] + raise PublishValidationError( + "See log for details. " + "Invalid nodes: {0}".format(nodes) + ) + + @classmethod + def get_invalid(cls, instance): + + invalid = [] + + rop_node = hou.node(instance.data["instance_node"]) + output_node = instance.data.get("output_node") + if output_node is None: + cls.log.debug( + "No Output Node, skipping check.." + ) + return + + if rop_node.evalParm("buildfrompath"): + # This validator doesn't support naming check if + # building hierarchy from path' is used + cls.log.info( + "Using 'Build Hierarchy from Path Attribute', skipping check.." + ) + return + + # Check nodes names + all_outputs = get_output_children(output_node, include_sops=False) + for output in all_outputs: + for prefix in cls.collision_prefixes: + if output.name().startswith(prefix): + invalid.append(output) + cls.log.error( + "Invalid node name: Node '%s' " + "includes a collision prefix '%s'", + output.path(), prefix + ) + break + + return invalid diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 93d5c50d5e..5392fc34dd 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -19,6 +19,19 @@ ], "ext": ".ass" }, + "CreateStaticMesh": { + "enabled": true, + "default_variants": [ + "Main" + ], + "static_mesh_prefix": "S", + "collision_prefixes": [ + "UBX", + "UCP", + "USP", + "UCX" + ] + }, "CreateAlembicCamera": { "enabled": true, "default_variants": [ @@ -102,6 +115,21 @@ "enabled": true, "optional": true, "active": true + }, + "ValidateSubsetName": { + "enabled": true, + "optional": true, + "active": true + }, + "ValidateMeshIsStatic": { + "enabled": true, + "optional": true, + "active": true + }, + "ValidateUnrealStaticMeshName": { + "enabled": false, + "optional": true, + "active": true } } } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json index 799bc0e81a..cd8c260124 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json @@ -39,6 +39,37 @@ ] }, + { + "type": "dict", + "collapsible": true, + "key": "CreateStaticMesh", + "label": "Create Static Mesh", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "list", + "key": "default_variants", + "label": "Default Variants", + "object_type": "text" + }, + { + "type": "text", + "key": "static_mesh_prefix", + "label": "Static Mesh Prefix" + }, + { + "type": "list", + "key": "collision_prefixes", + "label": "Collision Mesh Prefixes", + "object_type": "text" + } + ] + }, { "type": "schema_template", "name": "template_create_plugin", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json index b57089007e..d5f70b0312 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json @@ -47,6 +47,18 @@ { "key": "ValidateContainers", "label": "ValidateContainers" + }, + { + "key": "ValidateSubsetName", + "label": "Validate Subset Name" + }, + { + "key": "ValidateMeshIsStatic", + "label": "Validate Mesh is Static" + }, + { + "key": "ValidateUnrealStaticMeshName", + "label": "Validate Unreal Static Mesh Name" } ] } diff --git a/server_addon/houdini/server/settings/publish_plugins.py b/server_addon/houdini/server/settings/publish_plugins.py index 4534d8d0d9..58240b0205 100644 --- a/server_addon/houdini/server/settings/publish_plugins.py +++ b/server_addon/houdini/server/settings/publish_plugins.py @@ -21,10 +21,28 @@ class CreateArnoldAssModel(BaseSettingsModel): ext: str = Field(Title="Extension") +class CreateStaticMeshModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + default_variants: list[str] = Field( + default_factory=list, + title="Default Products" + ) + static_mesh_prefixes: str = Field("S", title="Static Mesh Prefix") + collision_prefixes: list[str] = Field( + default_factory=list, + title="Collision Prefixes" + ) + + class CreatePluginsModel(BaseSettingsModel): CreateArnoldAss: CreateArnoldAssModel = Field( default_factory=CreateArnoldAssModel, title="Create Alembic Camera") + # "-" is not compatible in the new model + CreateStaticMesh: CreateStaticMeshModel = Field( + default_factory=CreateStaticMeshModel, + title="Create Static Mesh" + ) CreateAlembicCamera: CreatorModel = Field( default_factory=CreatorModel, title="Create Alembic Camera") @@ -63,6 +81,19 @@ DEFAULT_HOUDINI_CREATE_SETTINGS = { "default_variants": ["Main"], "ext": ".ass" }, + "CreateStaticMesh": { + "enabled": True, + "default_variants": [ + "Main" + ], + "static_mesh_prefix": "S", + "collision_prefixes": [ + "UBX", + "UCP", + "USP", + "UCX" + ] + }, "CreateAlembicCamera": { "enabled": True, "default_variants": ["Main"] @@ -136,6 +167,15 @@ class PublishPluginsModel(BaseSettingsModel): ValidateContainers: BasicValidateModel = Field( default_factory=BasicValidateModel, title="Validate Latest Containers.") + ValidateSubsetName: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Subset Name.") + ValidateMeshIsStatic: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Mesh is Static.") + ValidateUnrealStaticMeshName: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Unreal Static Mesh Name.") DEFAULT_HOUDINI_PUBLISH_SETTINGS = { @@ -160,5 +200,20 @@ DEFAULT_HOUDINI_PUBLISH_SETTINGS = { "enabled": True, "optional": True, "active": True + }, + "ValidateSubsetName": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateMeshIsStatic": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateUnrealStaticMeshName": { + "enabled": False, + "optional": True, + "active": True } } diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index b3f4756216..ae7362549b 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.1.2" +__version__ = "0.1.3"