mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge pull request #5481 from ynput/tests/publish_process
This commit is contained in:
commit
da3f1efaca
16 changed files with 929 additions and 3 deletions
|
|
@ -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. """
|
||||
|
||||
|
|
|
|||
143
openpype/hosts/houdini/plugins/create/create_staticmesh.py
Normal file
143
openpype/hosts/houdini/plugins/create/create_staticmesh.py
Normal file
|
|
@ -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
|
||||
139
openpype/hosts/houdini/plugins/load/load_fbx.py
Normal file
139
openpype/hosts/houdini/plugins/load/load_fbx.py
Normal file
|
|
@ -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]
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
53
openpype/hosts/houdini/plugins/publish/extract_fbx.py
Normal file
53
openpype/hosts/houdini/plugins/publish/extract_fbx.py
Normal file
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
__version__ = "0.1.2"
|
||||
__version__ = "0.1.3"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue