mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-02 00:44:52 +01:00
add ornatrix family support for maya
This commit is contained in:
parent
13168b4c5d
commit
0bcc602eac
8 changed files with 421 additions and 2 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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")
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
},
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue