validate mesh name

This commit is contained in:
Mustafa-Zarkash 2023-09-01 17:04:22 +03:00
parent d1e82ceb7e
commit e6b9a7d038
3 changed files with 102 additions and 364 deletions

View file

@ -1,235 +0,0 @@
# -*- coding: utf-8 -*-
"""Validate path attribute for all primitives.
Validators are used to verify the work of artists,
by running some checks which automates the approval process.
It's almost the same as
'validate_primitive_hierarchy_paths.py'
however this one includes extra comments for demonstration.
FYI, path for fbx behaves a little differently.
In maya terms:
in Filmbox FBX: it sets the name of the object
in Alembic ROP: it sets the name of the shape
This plugin is part of publish process guide.
"""
import pyblish.api
from openpype.pipeline import (
PublishValidationError,
OptionalPyblishPluginMixin
)
from openpype.pipeline.publish import (
ValidateContentsOrder,
RepairAction,
)
from openpype.hosts.houdini.api.action import (
SelectInvalidAction,
SelectROPAction,
)
import hou
# Each validation can have a single repair action
# which calls the repair method
class AddDefaultPathAction(RepairAction):
label = "Add a default path"
icon = "mdi.pencil-plus-outline"
class ValidateFBXPrimitiveHierarchyPaths(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
"""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 FilmBox node.
This validation enables 'Build Hierarchy from Attribute'
by default.
"""
families = ["fbx"]
hosts = ["houdini"]
label = "Validate Prims Hierarchy Path (FBX)"
# Usually you will use this value as default
order = ValidateContentsOrder + 0.1
# Validation can have as many actions as you want
# all of these actions are defined in a seperate place
# unlike the repair action
actions = [SelectInvalidAction, AddDefaultPathAction,
SelectROPAction]
# 'OptionalPyblishPluginMixin' adds the functionality to
# enable/disable plugins, It requires adding new settings.
optional = True
# overrides InstancePlugin.process()
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=self.label
)
# This method was named get_invalid as a convention
# it's also used by SelectInvalidAction to select
# the returned nodes
@classmethod
def get_invalid(cls, instance):
output_node = instance.data.get("output_node")
rop_node = hou.node(instance.data["instance_node"])
if output_node is None:
cls.log.error(
"SOP Output node in '%s' does not exist. "
"Ensure a valid SOP output path is set.",
rop_node.path()
)
return [rop_node]
build_from_path = rop_node.parm("buildfrompath").eval()
if not build_from_path:
cls.log.debug(
"Filmbox FBX has 'Build from Path' disabled. "
"Enbaling it as default."
)
rop_node.parm("buildfrompath").set(1)
path_attr = rop_node.parm("pathattrib").eval()
if not path_attr:
cls.log.debug(
"Filmbox FBX node has no Path Attribute"
"value set, setting it to 'path' as default."
)
rop_node.parm("pathattrib").set("path")
cls.log.debug("Checking for attribute: %s", path_attr)
# Use current frame if "frameStart" doesn't exist
# This only happens when ""trange" is 0
frame = instance.data.get("frameStart", hou.intFrame())
# Get Geo at that frame
geo = output_node.geometryAtFrame(frame)
if not geo:
cls.log.warning("No geometry found,"
" skipping check..")
return
# If there are no primitives on the current frame then
# we can't check whether the path names are correct.
# So we'll just issue a warning that the check can't
# be done consistently and skip validation.
if len(geo.iterPrims()) == 0:
cls.log.warning(
"No primitives found on current frame."
" Validation for primitive hierarchy"
" paths will be skipped,"
" thus can't be validated."
)
return
# Check if there are any values for the primitives
attrib = geo.findPrimAttrib(path_attr)
if not attrib:
cls.log.info(
"Geometry Primitives are missing "
"path attribute: `%s`", path_attr
)
return [output_node]
# Ensure at least a single string value is present
if not attrib.strings():
cls.log.info(
"Primitive path attribute has no "
"string values: %s", path_attr
)
return [output_node]
paths = geo.primStringAttribValues(path_attr)
# Ensure all primitives are set to a valid path
# Collect all invalid primitive numbers
invalid_prims = [i for i, path in enumerate(paths) if not path]
if invalid_prims:
num_prims = len(geo.iterPrims()) # faster than len(geo.prims())
cls.log.info(
"Prims have no value for attribute `%s` "
"(%s of %s prims)",
path_attr, len(invalid_prims), num_prims
)
return [output_node]
# what repair action expects to find and call
@classmethod
def repair(cls, instance):
"""Add a default path attribute Action.
It is a helper action more than a repair action,
used to add a default single value for the path.
"""
rop_node = hou.node(instance.data["instance_node"])
# I'm doing so because an artist may change output node
# before clicking the button.
output_node = rop_node.parm("startnode").evalAsNode()
if not output_node:
cls.log.debug(
"Action isn't performed, invalid SOP Path on %s",
rop_node
)
return
# This check to prevent the action from running multiple times.
# git_invalid only returns [output_node] when
# path attribute is the problem
if cls.get_invalid(instance) != [output_node]:
return
path_attr = rop_node.parm("pathattrib").eval()
path_node = output_node.parent().createNode("name",
"AUTO_PATH")
path_node.parm("attribname").set(path_attr)
path_node.parm("name1").set('`opname("..")`_GEO')
cls.log.debug(
"'%s' was created. It adds '%s' with a default"
" single value", path_node, path_attr
)
path_node.setGenericFlag(hou.nodeFlag.DisplayComment, True)
path_node.setComment(
'Auto path node was created automatically by '
'"Add a default path attribute"'
'\nFeel free to modify or replace it.'
)
if output_node.type().name() in ["null", "output"]:
# Connect before
path_node.setFirstInput(output_node.input(0))
path_node.moveToGoodPosition()
output_node.setFirstInput(path_node)
output_node.moveToGoodPosition()
else:
# Connect after
path_node.setFirstInput(output_node)
rop_node.parm("startnode").set(path_node.path())
path_node.moveToGoodPosition()
cls.log.debug(
"SOP path on '%s' updated to new output node '%s'",
rop_node, path_node
)

View file

@ -1,129 +0,0 @@
# -*- coding: utf-8 -*-
"""Validate mesh is static.
This plugin is part of publish process guide.
"""
import pyblish.api
from openpype.pipeline import PublishValidationError
from openpype.pipeline.publish import (
ValidateContentsOrder,
RepairAction,
)
from openpype.hosts.houdini.api.action import (
SelectInvalidAction,
SelectROPAction,
)
import hou
# Each validation can have a single repair action
# which calls the repair method
class FreezeTimeAction(RepairAction):
label = "Freeze Time"
icon = "ei.pause-alt"
class ValidateMeshIsStatic(pyblish.api.InstancePlugin):
"""Validate mesh is static.
It checks if node is time dependant.
"""
families = ["staticMesh"]
hosts = ["houdini"]
label = "Validate mesh is static"
# Usually you will use this value as default
order = ValidateContentsOrder + 0.1
# Validation can have as many actions as you want
# all of these actions are defined in a seperate place
# unlike the repair action
actions = [FreezeTimeAction, SelectInvalidAction,
SelectROPAction]
# overrides InstancePlugin.process()
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=self.label
)
# This method was named get_invalid as a convention
# it's also used by SelectInvalidAction to select
# the returned nodes
@classmethod
def get_invalid(cls, instance):
output_node = instance.data.get("output_node")
if output_node.isTimeDependent():
cls.log.info("Mesh is not static!")
return [output_node]
# what repair action expects to find and call
@classmethod
def repair(cls, instance):
"""Adds a time shift node.
It should kill time dependency.
"""
rop_node = hou.node(instance.data["instance_node"])
# I'm doing so because an artist may change output node
# before clicking the button.
output_node = rop_node.parm("startnode").evalAsNode()
if not output_node:
cls.log.debug(
"Action isn't performed, invalid SOP Path on %s",
rop_node
)
return
# This check to prevent the action from running multiple times.
# git_invalid only returns [output_node] when
# path attribute is the problem
if cls.get_invalid(instance) != [output_node]:
return
time_shift = output_node.parent().createNode("timeshift",
"freeze_time")
time_shift.parm("frame").deleteAllKeyframes()
frame = instance.data.get("frameStart", hou.intFrame())
time_shift.parm("frame").set(frame)
cls.log.debug(
"'%s' was created. It will kill time dependency.",
time_shift
)
time_shift.setGenericFlag(hou.nodeFlag.DisplayComment, True)
time_shift.setComment(
'This node was created automatically by '
'"Freeze Time" Action'
'\nFeel free to modify or replace it.'
)
if output_node.type().name() in ["null", "output"]:
# Connect before
time_shift.setFirstInput(output_node.input(0))
time_shift.moveToGoodPosition()
output_node.setFirstInput(time_shift)
output_node.moveToGoodPosition()
else:
# Connect after
time_shift.setFirstInput(output_node)
rop_node.parm("startnode").set(time_shift.path())
time_shift.moveToGoodPosition()
cls.log.debug(
"SOP path on '%s' updated to new output node '%s'",
rop_node, time_shift
)

View file

@ -0,0 +1,102 @@
# -*- 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
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
@classmethod
def apply_settings(cls, project_settings, system_settings):
settings = (
project_settings["houdini"]["create"]["CreateUnrealStaticMesh"]
)
cls.collision_prefixes = settings["collision_prefixes"]
cls.static_mesh_prefix = settings["static_mesh_prefix"]
def process(self, instance):
invalid = self.get_invalid(instance)
if invalid:
nodes = [n.path() for n in invalid if isinstance(n, hou.Node)]
raise PublishValidationError(
"See log for details. "
"Invalid nodes: {0}".format(nodes),
title=self.label
)
@classmethod
def get_invalid(cls, instance):
invalid = []
rop_node = hou.node(instance.data["instance_node"])
output_node = instance.data.get("output_node")
cls.log.debug(cls.collision_prefixes)
# Check nodes names
if output_node.childTypeCategory() == hou.objNodeTypeCategory():
for child in output_node.children():
for prefix in cls.collision_prefixes:
if child.name().startswith(prefix):
invalid.append(child)
cls.log.error(
"Invalid name: Child node '%s' in '%s' "
"has a collision prefix '%s'"
, child.name(), output_node.path(), prefix
)
break
else:
cls.log.debug(output_node.name())
for prefix in cls.collision_prefixes:
if output_node.name().startswith(prefix):
invalid.append(output_node)
cls.log.error(
"Invalid name: output node '%s' "
"has a collision prefix '%s'"
, output_node.name(), prefix
)
# Check subset name
subset_name = "{}_{}{}".format(
cls.static_mesh_prefix,
instance.data["asset"],
instance.data.get("variant", "")
)
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