Merge pull request #473 from ynput/feature/AY-976_arnold-scene-source-raw

Arnold Scene Source Raw - AY-976
This commit is contained in:
Ondřej Samohel 2024-05-06 17:05:03 +02:00 committed by GitHub
commit f3fec757a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 194 additions and 135 deletions

View file

@ -1299,7 +1299,7 @@ def is_visible(node,
override_enabled = cmds.getAttr('{}.overrideEnabled'.format(node))
override_visibility = cmds.getAttr('{}.overrideVisibility'.format(
node))
if override_enabled and override_visibility:
if override_enabled and not override_visibility:
return False
if parentHidden:

View file

@ -1,3 +1,5 @@
from maya import cmds
from ayon_core.hosts.maya.api import (
lib,
plugin
@ -87,16 +89,24 @@ class CreateArnoldSceneSource(plugin.MayaCreator):
return defs
class CreateArnoldSceneSourceProxy(CreateArnoldSceneSource):
"""Arnold Scene Source Proxy
This product type facilitates working with proxy geometry in the viewport.
"""
identifier = "io.openpype.creators.maya.assproxy"
label = "Arnold Scene Source Proxy"
product_type = "assProxy"
icon = "cube"
def create(self, product_name, instance_data, pre_create_data):
from maya import cmds
instance = super(CreateArnoldSceneSource, self).create(
product_name, instance_data, pre_create_data
)
instance_node = instance.get("instance_node")
content = cmds.sets(name=instance_node + "_content_SET", empty=True)
proxy = cmds.sets(name=instance_node + "_proxy_SET", empty=True)
cmds.sets([content, proxy], forceElement=instance_node)
cmds.sets([proxy], forceElement=instance_node)

View file

@ -12,6 +12,7 @@ from ayon_core.hosts.maya.api.lib import (
unique_namespace,
get_attribute_input,
maintained_selection,
get_fps_for_current_context
)
from ayon_core.hosts.maya.api.pipeline import containerise
from ayon_core.hosts.maya.api.plugin import get_load_color_for_product_type
@ -29,7 +30,13 @@ class ArnoldStandinLoader(load.LoaderPlugin):
"""Load as Arnold standin"""
product_types = {
"ass", "animation", "model", "proxyAbc", "pointcache", "usd"
"ass",
"assProxy",
"animation",
"model",
"proxyAbc",
"pointcache",
"usd"
}
representations = {"ass", "abc", "usda", "usdc", "usd"}
@ -95,8 +102,10 @@ class ArnoldStandinLoader(load.LoaderPlugin):
sequence = is_sequence(os.listdir(os.path.dirname(repre_path)))
cmds.setAttr(standin_shape + ".useFrameExtension", sequence)
fps = float(version_attributes.get("fps")) or 25
cmds.setAttr(standin_shape + ".abcFPS", fps)
fps = (
version_attributes.get("fps") or get_fps_for_current_context()
)
cmds.setAttr(standin_shape + ".abcFPS", float(fps))
nodes = [root, standin, standin_shape]
if operator is not None:
@ -128,6 +137,18 @@ class ArnoldStandinLoader(load.LoaderPlugin):
proxy_path = "/".join([os.path.dirname(path), proxy_basename])
return proxy_basename, proxy_path
def _update_operators(self, string_replace_operator, proxy_basename, path):
cmds.setAttr(
string_replace_operator + ".match",
proxy_basename.split(".")[0],
type="string"
)
cmds.setAttr(
string_replace_operator + ".replace",
os.path.basename(path).split(".")[0],
type="string"
)
def _setup_proxy(self, shape, path, namespace):
proxy_basename, proxy_path = self._get_proxy_path(path)
@ -150,16 +171,7 @@ class ArnoldStandinLoader(load.LoaderPlugin):
"*.(@node=='{}')".format(node_type),
type="string"
)
cmds.setAttr(
string_replace_operator + ".match",
proxy_basename,
type="string"
)
cmds.setAttr(
string_replace_operator + ".replace",
os.path.basename(path),
type="string"
)
self._update_operators(string_replace_operator, proxy_basename, path)
cmds.connectAttr(
string_replace_operator + ".out",
@ -194,18 +206,9 @@ class ArnoldStandinLoader(load.LoaderPlugin):
path = get_representation_path(repre_entity)
proxy_basename, proxy_path = self._get_proxy_path(path)
# Whether there is proxy or so, we still update the string operator.
# Whether there is proxy or not, we still update the string operator.
# If no proxy exists, the string operator won't replace anything.
cmds.setAttr(
string_replace_operator + ".match",
proxy_basename,
type="string"
)
cmds.setAttr(
string_replace_operator + ".replace",
os.path.basename(path),
type="string"
)
self._update_operators(string_replace_operator, proxy_basename, path)
dso_path = path
if os.path.exists(proxy_path):

View file

@ -10,21 +10,23 @@ class CollectArnoldSceneSource(pyblish.api.InstancePlugin):
# Offset to be after renderable camera collection.
order = pyblish.api.CollectorOrder + 0.2
label = "Collect Arnold Scene Source"
families = ["ass"]
families = ["ass", "assProxy"]
def process(self, instance):
objsets = instance.data["setMembers"]
instance.data["members"] = []
for set_member in instance.data["setMembers"]:
if cmds.nodeType(set_member) != "objectSet":
instance.data["members"].extend(self.get_hierarchy(set_member))
continue
for objset in objsets:
objset = str(objset)
members = cmds.sets(objset, query=True)
members = cmds.sets(set_member, query=True)
members = cmds.ls(members, long=True)
if members is None:
self.log.warning("Skipped empty instance: \"%s\" " % objset)
self.log.warning(
"Skipped empty instance: \"%s\" " % set_member
)
continue
if objset.endswith("content_SET"):
instance.data["contentMembers"] = self.get_hierarchy(members)
if objset.endswith("proxy_SET"):
if set_member.endswith("proxy_SET"):
instance.data["proxy"] = self.get_hierarchy(members)
# Use camera in object set if present else default to render globals
@ -33,7 +35,7 @@ class CollectArnoldSceneSource(pyblish.api.InstancePlugin):
renderable = [c for c in cameras if cmds.getAttr("%s.renderable" % c)]
if renderable:
camera = renderable[0]
for node in instance.data["contentMembers"]:
for node in instance.data["members"]:
camera_shapes = cmds.listRelatives(
node, shapes=True, type="camera"
)
@ -46,18 +48,11 @@ class CollectArnoldSceneSource(pyblish.api.InstancePlugin):
self.log.debug("data: {}".format(instance.data))
def get_hierarchy(self, nodes):
"""Return nodes with all their children.
Arguments:
nodes (List[str]): List of nodes to collect children hierarchy for
Returns:
list: Input nodes with their children hierarchy
"""
"""Return nodes with all their children"""
nodes = cmds.ls(nodes, long=True)
if not nodes:
return []
children = get_all_children(nodes, ignore_intermediate_objects=True)
return list(children.union(nodes))
children = get_all_children(nodes)
# Make sure nodes merged with children only
# contains unique entries
return list(set(nodes + list(children)))

View file

@ -17,8 +17,7 @@ class ExtractArnoldSceneSource(publish.Extractor):
families = ["ass"]
asciiAss = False
def process(self, instance):
staging_dir = self.staging_dir(instance)
def _pre_process(self, instance, staging_dir):
file_path = os.path.join(staging_dir, "{}.ass".format(instance.name))
# Mask
@ -70,24 +69,38 @@ class ExtractArnoldSceneSource(publish.Extractor):
"mask": mask
}
filenames, nodes_by_id = self._extract(
instance.data["contentMembers"], attribute_data, kwargs
)
if "representations" not in instance.data:
instance.data["representations"] = []
return attribute_data, kwargs
def process(self, instance):
staging_dir = self.staging_dir(instance)
attribute_data, kwargs = self._pre_process(instance, staging_dir)
filenames = self._extract(
instance.data["members"], attribute_data, kwargs
)
self._post_process(
instance, filenames, staging_dir, kwargs["startFrame"]
)
def _post_process(self, instance, filenames, staging_dir, frame_start):
nodes_by_id = self._nodes_by_id(instance[:])
representation = {
"name": "ass",
"ext": "ass",
"files": filenames if len(filenames) > 1 else filenames[0],
"stagingDir": staging_dir,
"frameStart": kwargs["startFrame"]
"frameStart": frame_start
}
instance.data["representations"].append(representation)
json_path = os.path.join(staging_dir, "{}.json".format(instance.name))
json_path = os.path.join(
staging_dir, "{}.json".format(instance.name)
)
with open(json_path, "w") as f:
json.dump(nodes_by_id, f)
@ -104,13 +117,68 @@ class ExtractArnoldSceneSource(publish.Extractor):
"Extracted instance {} to: {}".format(instance.name, staging_dir)
)
# Extract proxy.
if not instance.data.get("proxy", []):
return
def _nodes_by_id(self, nodes):
nodes_by_id = defaultdict(list)
kwargs["filename"] = file_path.replace(".ass", "_proxy.ass")
for node in nodes:
id = lib.get_id(node)
filenames, _ = self._extract(
if id is None:
continue
# Converting Maya hierarchy separator "|" to Arnold separator "/".
nodes_by_id[id].append(node.replace("|", "/"))
return nodes_by_id
def _extract(self, nodes, attribute_data, kwargs):
filenames = []
with lib.attribute_values(attribute_data):
with lib.maintained_selection():
self.log.debug(
"Writing: {}".format(nodes)
)
cmds.select(nodes, noExpand=True)
self.log.debug(
"Extracting ass sequence with: {}".format(kwargs)
)
exported_files = cmds.arnoldExportAss(**kwargs)
for file in exported_files:
filenames.append(os.path.split(file)[1])
self.log.debug("Exported: {}".format(filenames))
return filenames
class ExtractArnoldSceneSourceProxy(ExtractArnoldSceneSource):
"""Extract the content of the instance to an Arnold Scene Source file."""
label = "Extract Arnold Scene Source Proxy"
hosts = ["maya"]
families = ["assProxy"]
asciiAss = True
def process(self, instance):
staging_dir = self.staging_dir(instance)
attribute_data, kwargs = self._pre_process(instance, staging_dir)
filenames, _ = self._duplicate_extract(
instance.data["members"], attribute_data, kwargs
)
self._post_process(
instance, filenames, staging_dir, kwargs["startFrame"]
)
kwargs["filename"] = os.path.join(
staging_dir, "{}_proxy.ass".format(instance.name)
)
filenames, _ = self._duplicate_extract(
instance.data["proxy"], attribute_data, kwargs
)
@ -125,12 +193,11 @@ class ExtractArnoldSceneSource(publish.Extractor):
instance.data["representations"].append(representation)
def _extract(self, nodes, attribute_data, kwargs):
def _duplicate_extract(self, nodes, attribute_data, kwargs):
self.log.debug(
"Writing {} with:\n{}".format(kwargs["filename"], kwargs)
)
filenames = []
nodes_by_id = defaultdict(list)
# Duplicating nodes so they are direct children of the world. This
# makes the hierarchy of any exported ass file the same.
with lib.delete_after() as delete_bin:
@ -147,7 +214,9 @@ class ExtractArnoldSceneSource(publish.Extractor):
if not shapes:
continue
duplicate_transform = cmds.duplicate(node)[0]
basename = cmds.duplicate(node)[0]
parents = cmds.ls(node, long=True)[0].split("|")[:-1]
duplicate_transform = "|".join(parents + [basename])
if cmds.listRelatives(duplicate_transform, parent=True):
duplicate_transform = cmds.parent(
@ -172,28 +241,7 @@ class ExtractArnoldSceneSource(publish.Extractor):
duplicate_nodes.extend(shapes)
delete_bin.append(duplicate_transform)
# Copy cbId to mtoa_constant.
for node in duplicate_nodes:
# Converting Maya hierarchy separator "|" to Arnold
# separator "/".
nodes_by_id[lib.get_id(node)].append(node.replace("|", "/"))
with lib.attribute_values(attribute_data):
with lib.maintained_selection():
self.log.debug(
"Writing: {}".format(duplicate_nodes)
)
cmds.select(duplicate_nodes, noExpand=True)
self.log.debug(
"Extracting ass sequence with: {}".format(kwargs)
)
exported_files = cmds.arnoldExportAss(**kwargs)
for file in exported_files:
filenames.append(os.path.split(file)[1])
self.log.debug("Exported: {}".format(filenames))
nodes_by_id = self._nodes_by_id(duplicate_nodes)
filenames = self._extract(duplicate_nodes, attribute_data, kwargs)
return filenames, nodes_by_id

View file

@ -1,30 +1,56 @@
from maya import cmds
import pyblish.api
from ayon_core.pipeline.publish import (
ValidateContentsOrder, PublishValidationError
)
from ayon_core.hosts.maya.api.lib import is_visible
class ValidateArnoldSceneSource(pyblish.api.InstancePlugin):
"""Validate Arnold Scene Source.
We require at least 1 root node/parent for the meshes. This is to ensure we
can duplicate the nodes and preserve the names.
Ensure no nodes are hidden.
"""
If using proxies we need the nodes to share the same names and not be
order = ValidateContentsOrder
hosts = ["maya"]
families = ["ass", "assProxy"]
label = "Validate Arnold Scene Source"
def process(self, instance):
# Validate against having nodes hidden, which will result in the
# extraction to ignore the node.
nodes = instance.data["members"] + instance.data.get("proxy", [])
nodes = [x for x in nodes if cmds.objectType(x, isAType='dagNode')]
hidden_nodes = [
x for x in nodes if not is_visible(x, intermediateObject=False)
]
if hidden_nodes:
raise PublishValidationError(
"Found hidden nodes:\n\n{}\n\nPlease unhide for"
" publishing.".format("\n".join(hidden_nodes))
)
class ValidateArnoldSceneSourceProxy(pyblish.api.InstancePlugin):
"""Validate Arnold Scene Source Proxy.
When using proxies we need the nodes to share the same names and not be
parent to the world. This ends up needing at least two groups with content
nodes and proxy nodes in another.
"""
order = ValidateContentsOrder
hosts = ["maya"]
families = ["ass"]
label = "Validate Arnold Scene Source"
families = ["assProxy"]
label = "Validate Arnold Scene Source Proxy"
def _get_nodes_by_name(self, nodes):
ungrouped_nodes = []
nodes_by_name = {}
parents = []
same_named_nodes = {}
for node in nodes:
node_split = node.split("|")
if len(node_split) == 2:
@ -35,33 +61,16 @@ class ValidateArnoldSceneSource(pyblish.api.InstancePlugin):
parents.append(parent)
node_name = node.rsplit("|", 1)[-1].rsplit(":", 1)[-1]
# Check for same same nodes, which can happen in different
# hierarchies.
if node_name in nodes_by_name:
try:
same_named_nodes[node_name].append(node)
except KeyError:
same_named_nodes[node_name] = [
nodes_by_name[node_name], node
]
nodes_by_name[node_name] = node
if same_named_nodes:
message = "Found nodes with the same name:"
for name, nodes in same_named_nodes.items():
message += "\n\n\"{}\":\n{}".format(name, "\n".join(nodes))
raise PublishValidationError(message)
return ungrouped_nodes, nodes_by_name, parents
def process(self, instance):
# Validate against nodes directly parented to world.
ungrouped_nodes = []
nodes, content_nodes_by_name, content_parents = (
self._get_nodes_by_name(instance.data["contentMembers"])
self._get_nodes_by_name(instance.data["members"])
)
ungrouped_nodes.extend(nodes)
@ -70,24 +79,21 @@ class ValidateArnoldSceneSource(pyblish.api.InstancePlugin):
)
ungrouped_nodes.extend(nodes)
# Validate against nodes directly parented to world.
if ungrouped_nodes:
raise PublishValidationError(
"Found nodes parented to the world: {}\n"
"All nodes need to be grouped.".format(ungrouped_nodes)
)
# Proxy validation.
if not instance.data.get("proxy", []):
return
# Validate for content and proxy nodes amount being the same.
if len(instance.data["contentMembers"]) != len(instance.data["proxy"]):
if len(instance.data["members"]) != len(instance.data["proxy"]):
raise PublishValidationError(
"Amount of content nodes ({}) and proxy nodes ({}) needs to "
"be the same.".format(
len(instance.data["contentMembers"]),
len(instance.data["proxy"])
"be the same.\nContent nodes: {}\nProxy nodes:{}".format(
len(instance.data["members"]),
len(instance.data["proxy"]),
instance.data["members"],
instance.data["proxy"]
)
)

View file

@ -17,7 +17,7 @@ class ValidateArnoldSceneSourceCbid(pyblish.api.InstancePlugin,
order = ValidateContentsOrder
hosts = ["maya"]
families = ["ass"]
families = ["assProxy"]
label = "Validate Arnold Scene Source CBID"
actions = [RepairAction]
optional = False
@ -40,15 +40,11 @@ class ValidateArnoldSceneSourceCbid(pyblish.api.InstancePlugin,
@classmethod
def get_invalid_couples(cls, instance):
content_nodes_by_name = cls._get_nodes_by_name(
instance.data["contentMembers"]
)
proxy_nodes_by_name = cls._get_nodes_by_name(
instance.data.get("proxy", [])
)
nodes_by_name = cls._get_nodes_by_name(instance.data["members"])
proxy_nodes_by_name = cls._get_nodes_by_name(instance.data["proxy"])
invalid_couples = []
for content_name, content_node in content_nodes_by_name.items():
for content_name, content_node in nodes_by_name.items():
proxy_node = proxy_nodes_by_name.get(content_name, None)
if not proxy_node:
@ -70,7 +66,7 @@ class ValidateArnoldSceneSourceCbid(pyblish.api.InstancePlugin,
if not self.is_active(instance.data):
return
# Proxy validation.
if not instance.data.get("proxy", []):
if not instance.data["proxy"]:
return
# Validate for proxy nodes sharing the same cbId as content nodes.

View file

@ -42,7 +42,7 @@ def prepare_changes(old_entity, new_entity):
Returns:
dict[str, Any]: Changes that have new entity.
"""
changes = {}
for key in set(new_entity.keys()):
@ -121,6 +121,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
"setdress",
"layout",
"ass",
"assProxy",
"vdbcache",
"scene",
"vrayproxy",