add ornatrix family support for maya

This commit is contained in:
Kayla Man 2024-06-14 23:02:36 +08:00
parent 13168b4c5d
commit 0bcc602eac
8 changed files with 421 additions and 2 deletions

View file

@ -64,7 +64,8 @@ class CollectResourcesPath(pyblish.api.InstancePlugin):
"skeletalMesh",
"xgen",
"yeticacheUE",
"tycache"
"tycache",
"ornatrixRig"
]
def process(self, instance):

View file

@ -769,6 +769,37 @@ def attribute_values(attr_values):
else:
cmds.setAttr(attr, value)
@contextlib.contextmanager
def attribute_values_from_list(attr_values):
"""Remaps node attributes to values for ornatrix during context.
Arguments:
attr_values (dict): Dictionary with (attr, value)
"""
for texture_attr in attr_values:
original = [(texture_attr[attr], cmds.getAttr(texture_attr[attr]))
for attr in texture_attr.keys()]
try:
for attr, value in attr_values.items():
if isinstance(value, string_types):
cmds.setAttr(attr, value, type="string")
else:
cmds.setAttr(attr, value)
yield
finally:
for attr, value in original:
if isinstance(value, string_types):
cmds.setAttr(attr, value, type="string")
elif value is None and cmds.getAttr(attr, type=True) == "string":
# In some cases the maya.cmds.getAttr command returns None
# for string attributes but this value cannot assigned.
# Note: After setting it once to "" it will then return ""
# instead of None. So this would only happen once.
cmds.setAttr(attr, "", type="string")
else:
cmds.setAttr(attr, value)
@contextlib.contextmanager
def keytangent_default(in_tangent_type='auto',

View file

@ -0,0 +1,22 @@
from ayon_maya.api import (
lib,
plugin
)
class CreateOrnatrixCache(plugin.MayaCreator):
"""Output for procedural plugin nodes of Yeti """
identifier = "io.openpype.creators.maya.ornatrixcache"
label = "Ornatrix Cache"
product_type = "ornatrixCache"
icon = "pagelines"
def get_instance_attr_defs(self):
# Add animation data without step and handles
remove = {"step", "handleStart", "handleEnd"}
defs = [attr_def for attr_def in lib.collect_animation_defs()
if attr_def.key not in remove]
return defs

View file

@ -0,0 +1,27 @@
from maya import cmds
from ayon_maya.api import (
lib,
plugin
)
class CreateOrnatrixRig(plugin.MayaCreator):
"""Output for Ornatrix nodes"""
identifier = "io.openpype.creators.maya.ornatrixrig"
label = "Ornatrix Rig"
product_type = "ornatrixRig"
icon = "usb"
def create(self, product_name, instance_data, pre_create_data):
with lib.undo_chunk():
instance = super(CreateOrnatrixRig, self).create(product_name,
instance_data,
pre_create_data)
instance_node = instance.get("instance_node")
self.log.info("Creating Rig instance set up ...")
input_meshes = cmds.sets(name="input_SET", empty=True)
cmds.sets(input_meshes, forceElement=instance_node)

View file

@ -0,0 +1,121 @@
from typing import List
import os
import json
import maya.cmds as cmds
from ayon_core.pipeline import registered_host
from ayon_core.pipeline.create import CreateContext
from ayon_maya.api import lib, plugin
class OrnatrixRigLoader(plugin.ReferenceLoader):
"""This loader will load Ornatix rig."""
product_types = {"ornatrixRig"}
representations = {"ma"}
label = "Load Ornatrix Rig"
order = -9
icon = "code-fork"
color = "orange"
# From settings
create_cache_instance_on_load = True
def process_reference(
self, context, name=None, namespace=None, options=None
):
path = self.filepath_from_context(context)
attach_to_root = options.get("attach_to_root", True)
group_name = options["group_name"]
# no group shall be created
if not attach_to_root:
group_name = namespace
with lib.maintained_selection():
file_url = self.prepare_root_value(
path, context["project"]["name"]
)
nodes = cmds.file(
file_url,
namespace=namespace,
reference=True,
returnNewNodes=True,
groupReference=attach_to_root,
groupName=group_name
)
color = plugin.get_load_color_for_product_type("ornatrixRig")
if color is not None:
red, green, blue = color
cmds.setAttr(group_name + ".useOutlinerColor", 1)
cmds.setAttr(
group_name + ".outlinerColor", red, green, blue
)
self.use_resources_textures(namespace, path)
self[:] = nodes
if self.create_cache_instance_on_load:
self._create_ox_cache_instance(nodes, variant=namespace)
return nodes
def _create_ox_cache_instance(self, nodes: List[str], variant: str):
"""Create a onratrixcache product type instance to publish the output.
This is similar to how loading animation rig will automatically create
an animation instance for publishing any loaded character rigs, but
then for Onratrix rigs.
Args:
nodes (List[str]): Nodes generated on load.
variant (str): Variant for the onratrix cache instance to create.
"""
# Check of the nodes connect to the ornatrix-related nodes
ox_nodes = [node for node in nodes if cmds.nodeType(nodes) in
{"HairFromGuidesNode", "GuidesFromMeshNode",
"MeshFromStrandsNode", "SurfaceCombNode"}]
assert not ox_nodes, "No Ornatrix nodes in rig, this is a bug."
ox_geo_nodes = cmds.ls(nodes, assemblies=True, long=True)
ox_input = next((node for node in nodes if
node.endswith("input_SET")), None)
self.log.info("Creating variant: {}".format(variant))
creator_identifier = "io.openpype.creators.maya.ornatrixcache"
host = registered_host()
create_context = CreateContext(host)
with lib.maintained_selection():
cmds.select(ox_geo_nodes + [ox_input], noExpand=True)
create_context.create(
creator_identifier=creator_identifier,
variant=variant,
pre_create_data={"use_selection": True}
)
def use_resources_textures(self, namespace, path):
"""Use texture maps from resources directories
Args:
namespace (str): namespace
path (str): published filepath
"""
_, maya_extension = os.path.splitext(path)
settings_path = path.replace(maya_extension, ".rigsettings")
with open(settings_path, "r") as fp:
image_attributes = json.load(fp)
fp.close()
if image_attributes:
for image_attribute in image_attributes:
texture_attribute = "{}:{}".format(
namespace, image_attribute["texture_attribute"])
cmds.setAttr(texture_attribute,
image_attribute["destination_file"],
type="string")

View file

@ -34,7 +34,7 @@ class CollectYetiRig(plugin.MayaInstancePlugin):
yeti_nodes = cmds.ls(instance[:], type="pgYetiMaya", long=True)
for node in yeti_nodes:
# Get Yeti resources (textures)
resources = self.get_yeti_resources(node)
resources = self.get_texture_resources(node)
yeti_resources.extend(resources)
instance.data["rigsettings"] = {"inputs": input_connections}
@ -304,3 +304,86 @@ class CollectYetiRig(plugin.MayaInstancePlugin):
raise RuntimeError(msg)
replaced.append(s)
return replaced
class CollectOrnatrixRig(CollectYetiRig):
"""Collect all information of the Ornatrix Rig"""
order = pyblish.api.CollectorOrder + 0.4
label = "Collect Ornatrix Rig"
families = ["ornatrixRig"]
def process(self, instance):
assert "input_SET" in instance.data["setMembers"], (
"Ornatrix Rig must have an input_SET")
ornatrix_nodes = cmds.ls(instance[:], long=True)
self.log.debug(f"Getting ornatrix nodes: {ornatrix_nodes}")
# Force frame range for yeti cache export for the rig
# Collect any textures if used
ornatrix_resources = []
for node in ornatrix_nodes:
# Get Yeti resources (textures)
resources = self.get_texture_resources(node)
ornatrix_resources.extend(resources)
# avoid duplicate dictionary data
instance.data["resources"] = [
i for n, i in enumerate(ornatrix_resources)
if i not in ornatrix_resources[n + 1:]
]
self.log.debug("{}".format(instance.data["resources"]))
start = cmds.playbackOptions(query=True, animationStartTime=True)
for key in ["frameStart", "frameEnd",
"frameStartHandle", "frameEndHandle"]:
instance.data[key] = start
def get_texture_resources(self, node):
# add docstrings
resources = []
node_shape = cmds.listRelatives(node, shapes=True)
if not node_shape:
return []
ox_nodes = [
ox_node for ox_node in cmds.listConnections(node_shape, destination=True)
if cmds.nodeType(ox_node) in {
"HairFromGuidesNode", "GuidesFromMeshNode",
"MeshFromStrandsNode", "SurfaceCombNode"
}
]
ox_imageFile = [
ox_img for ox_img in cmds.listConnections(ox_nodes, destination=False)
if cmds.nodeType(ox_img) == "file"
]
if not ox_imageFile:
return []
for img in ox_imageFile:
texture_attr = "{}.fileTextureName".format(img)
texture = cmds.getAttr("{}.fileTextureName".format(img))
files = []
if os.path.isabs(texture):
self.log.debug("Texture is absolute path, ignoring "
"image search paths for: %s" % texture)
files = self.search_textures(texture)
else:
root = os.environ["AYON_WORKDIR"]
filepath = os.path.join(root, texture)
files = self.search_textures(filepath)
if files:
# Break out on first match in search paths..
break
if not files:
raise KnownPublishError(
"No texture found for: %s "
"(searched: %s)" % (texture))
item = {
"files": files,
"source": texture,
"texture_attribute": texture_attr
}
resources.append(item)
return resources

View file

@ -0,0 +1,113 @@
# -*- coding: utf-8 -*-
"""Extract Ornatrix rig."""
import os
import json
from ayon_maya.api import lib
from ayon_maya.api import plugin
from maya import cmds
class ExtractOrnatrixRig(plugin.MayaExtractorPlugin):
"""Extract the Ornatrix rig to a Maya Scene and write the Ornatrix rig data."""
label = "Extract Ornatrix Rig"
families = ["ornatrixRig"]
scene_type = "ma"
def process(self, instance):
"""Plugin entry point."""
maya_settings = instance.context.data["project_settings"]["maya"]
ext_mapping = {
item["name"]: item["value"]
for item in maya_settings["ext_mapping"]
}
if ext_mapping:
self.log.debug("Looking in settings for scene type ...")
# use extension mapping for first family found
for family in self.families:
try:
self.scene_type = ext_mapping[family]
self.log.debug(
"Using {} as scene type".format(self.scene_type))
break
except KeyError:
# no preset found
pass
# Define extract output file path
dirname = self.staging_dir(instance)
settings_path = os.path.join(dirname, "ornatrix.rigsettings")
image_search_path = instance.data["resourcesDir"]
# add textures to transfers
if 'transfers' not in instance.data:
instance.data['transfers'] = []
resources = instance.data.get("resources", [])
for resource in instance.data.get('resources', []):
for file in resource['files']:
src = file
dst = os.path.join(image_search_path, os.path.basename(file))
resource["destination_file"] = dst
instance.data['transfers'].append([src, dst])
self.log.debug("adding transfer {} -> {}". format(src, dst))
self.log.debug("Writing metadata file: {}".format(settings_path))
with open(settings_path, "w") as fp:
json.dump(resources, fp, ensure_ascii=False)
# Get input_SET members
input_set = next(i for i in instance if i == "input_SET")
# Get all items
set_members = cmds.sets(input_set, query=True) or []
set_members += cmds.listRelatives(set_members,
allDescendents=True,
fullPath=True) or []
# Yeti related staging dirs
maya_path = os.path.join(dirname,
"ornatrix_rig.{}".format(self.scene_type))
nodes = instance.data["setMembers"]
with lib.maintained_selection():
cmds.select(nodes, noExpand=True)
cmds.file(maya_path,
force=True,
exportSelected=True,
typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501
preserveReferences=False,
constructionHistory=True,
shader=False)
# Ensure files can be stored
# build representations
if "representations" not in instance.data:
instance.data["representations"] = []
self.log.debug("rig file: {}".format(maya_path))
instance.data["representations"].append(
{
'name': self.scene_type,
'ext': self.scene_type,
'files': os.path.basename(maya_path),
'stagingDir': dirname
}
)
self.log.debug("settings file: {}".format(settings_path))
instance.data["representations"].append(
{
'name': "rigsettings",
'ext': "rigsettings",
'files': os.path.basename(settings_path),
'stagingDir': dirname
}
)
self.log.debug("Extracted {} to {}".format(instance, dirname))
cmds.select(clear=True)

View file

@ -39,6 +39,8 @@ class ColorsSetting(BaseSettingsModel):
(99, 206, 220, 1.0), title="Yeti Cache:")
yetiRig: ColorRGBA_uint8 = SettingsField(
(0, 205, 125, 1.0), title="Yeti Rig:")
ornatrixRig: ColorRGBA_uint8 = SettingsField(
(206, 234, 195, 1.0), title="Ornatrix Rig:")
# model: ColorRGB_float = SettingsField(
# (0.82, 0.52, 0.12), title="Model:"
# )
@ -114,6 +116,17 @@ class YetiRigLoaderModel(LoaderEnabledModel):
)
class OrnatrixRigLoaderModel(LoaderEnabledModel):
create_cache_instance_on_load: bool = SettingsField(
title="Create Ornatrix Cache instance on load",
description=(
"When enabled, upon loading a Ornatrix Rig product a new Ornatrix cache "
"instance is automatically created as preparation to publishing "
"the output directly."
)
)
class LoadersModel(BaseSettingsModel):
colors: ColorsSetting = SettingsField(
default_factory=ColorsSetting,
@ -210,6 +223,10 @@ class LoadersModel(BaseSettingsModel):
default_factory=YetiRigLoaderModel,
title="Yeti Rig Loader"
)
OrnatrixRigLoader: OrnatrixRigLoaderModel = SettingsField(
default_factory=OrnatrixRigLoaderModel,
title="Ornatrix Rig Loader"
)
DEFAULT_LOADERS_SETTING = {
@ -281,4 +298,8 @@ DEFAULT_LOADERS_SETTING = {
"enabled": True,
"create_cache_instance_on_load": True
},
"OrnatrixRigLoader": {
"enabled": True,
"create_cache_instance_on_load": True
},
}