mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
Implement extract animation
This commit is contained in:
parent
6190980df6
commit
7bc42e9fd9
3 changed files with 251 additions and 208 deletions
|
|
@ -3,6 +3,11 @@
|
|||
import re
|
||||
import contextlib
|
||||
from collections import OrderedDict
|
||||
import logging
|
||||
import os
|
||||
import json
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
from maya import cmds
|
||||
|
||||
|
|
@ -301,4 +306,179 @@ def is_visible(node,
|
|||
visibility=visibility):
|
||||
return False
|
||||
|
||||
return True
|
||||
return True
|
||||
|
||||
|
||||
# The maya alembic export types
|
||||
_alembic_options = {
|
||||
"startFrame": float,
|
||||
"endFrame": float,
|
||||
"frameRange": str, # "start end"; overrides startFrame & endFrame
|
||||
"eulerFilter": bool,
|
||||
"frameRelativeSample": float,
|
||||
"noNormals": bool,
|
||||
"renderableOnly": bool,
|
||||
"step": float,
|
||||
"stripNamespaces": bool,
|
||||
"uvWrite": bool,
|
||||
"wholeFrameGeo": bool,
|
||||
"worldSpace": bool,
|
||||
"writeVisibility": bool,
|
||||
"writeColorSets": bool,
|
||||
"writeFaceSets": bool,
|
||||
"writeCreases": bool, # Maya 2015 Ext1+
|
||||
"dataFormat": str,
|
||||
"root": (list, tuple),
|
||||
"attr": (list, tuple),
|
||||
"attrPrefix": (list, tuple),
|
||||
"userAttr": (list, tuple),
|
||||
"melPerFrameCallback": str,
|
||||
"melPostJobCallback": str,
|
||||
"pythonPerFrameCallback": str,
|
||||
"pythonPostJobCallback": str,
|
||||
"selection": bool
|
||||
}
|
||||
|
||||
|
||||
def extract_alembic(file,
|
||||
startFrame=None,
|
||||
endFrame=None,
|
||||
selection= True,
|
||||
uvWrite= True,
|
||||
eulerFilter= True,
|
||||
dataFormat="ogawa",
|
||||
verbose=False,
|
||||
**kwargs):
|
||||
"""Extract a single Alembic Cache.
|
||||
|
||||
This extracts an Alembic cache using the `-selection` flag to minimize
|
||||
the extracted content to solely what was Collected into the instance.
|
||||
|
||||
Arguments:
|
||||
|
||||
startFrame (float): Start frame of output. Ignored if `frameRange`
|
||||
provided.
|
||||
|
||||
endFrame (float): End frame of output. Ignored if `frameRange`
|
||||
provided.
|
||||
|
||||
frameRange (tuple or str): Two-tuple with start and end frame or a
|
||||
string formatted as: "startFrame endFrame". This argument
|
||||
overrides `startFrame` and `endFrame` arguments.
|
||||
|
||||
dataFormat (str): The data format to use for the cache,
|
||||
defaults to "ogawa"
|
||||
|
||||
verbose (bool): When on, outputs frame number information to the
|
||||
Script Editor or output window during extraction.
|
||||
|
||||
noNormals (bool): When on, normal data from the original polygon
|
||||
objects is not included in the exported Alembic cache file.
|
||||
|
||||
renderableOnly (bool): When on, any non-renderable nodes or hierarchy,
|
||||
such as hidden objects, are not included in the Alembic file.
|
||||
Defaults to False.
|
||||
|
||||
stripNamespaces (bool): When on, any namespaces associated with the
|
||||
exported objects are removed from the Alembic file. For example, an
|
||||
object with the namespace taco:foo:bar appears as bar in the
|
||||
Alembic file.
|
||||
|
||||
uvWrite (bool): When on, UV data from polygon meshes and subdivision
|
||||
objects are written to the Alembic file. Only the current UV map is
|
||||
included.
|
||||
|
||||
worldSpace (bool): When on, the top node in the node hierarchy is
|
||||
stored as world space. By default, these nodes are stored as local
|
||||
space. Defaults to False.
|
||||
|
||||
eulerFilter (bool): When on, X, Y, and Z rotation data is filtered with
|
||||
an Euler filter. Euler filtering helps resolve irregularities in
|
||||
rotations especially if X, Y, and Z rotations exceed 360 degrees.
|
||||
Defaults to True.
|
||||
|
||||
"""
|
||||
|
||||
# Ensure alembic exporter is loaded
|
||||
cmds.loadPlugin('AbcExport', quiet=True)
|
||||
|
||||
# Alembic Exporter requires forward slashes
|
||||
file = file.replace('\\', '/')
|
||||
|
||||
# Pass the start and end frame on as `frameRange` so that it
|
||||
# never conflicts with that argument
|
||||
if "frameRange" not in kwargs:
|
||||
# Fallback to maya timeline if no start or end frame provided.
|
||||
if startFrame is None:
|
||||
startFrame = cmds.playbackOptions(query=True,
|
||||
animationStartTime=True)
|
||||
if endFrame is None:
|
||||
endFrame = cmds.playbackOptions(query=True,
|
||||
animationEndTime=True)
|
||||
|
||||
# Ensure valid types are converted to frame range
|
||||
assert isinstance(startFrame, _alembic_options["startFrame"])
|
||||
assert isinstance(endFrame, _alembic_options["endFrame"])
|
||||
kwargs["frameRange"] = "{0} {1}".format(startFrame, endFrame)
|
||||
else:
|
||||
# Allow conversion from tuple for `frameRange`
|
||||
frame_range = kwargs["frameRange"]
|
||||
if isinstance(frame_range, (list, tuple)):
|
||||
assert len(frame_range) == 2
|
||||
kwargs["frameRange"] = "{0} {1}".format(frame_range[0],
|
||||
frame_range[1])
|
||||
|
||||
# Assemble options
|
||||
options = {
|
||||
"selection": selection,
|
||||
"uvWrite": uvWrite,
|
||||
"eulerFilter": eulerFilter,
|
||||
"dataFormat": dataFormat
|
||||
}
|
||||
options.update(kwargs)
|
||||
|
||||
# Validate options
|
||||
for key, value in options.copy().items():
|
||||
|
||||
# Discard unknown options
|
||||
if key not in _alembic_options:
|
||||
options.pop(key)
|
||||
continue
|
||||
|
||||
# Validate value type
|
||||
valid_types = _alembic_options[key]
|
||||
if not isinstance(value, valid_types):
|
||||
raise TypeError("Alembic option unsupported type: "
|
||||
"{0} (expected {1}}".format(value, valid_types))
|
||||
|
||||
# Format the job string from options
|
||||
job_args = list()
|
||||
for key, value in options.items():
|
||||
if isinstance(value, (list, tuple)):
|
||||
for entry in value:
|
||||
job_args.append("-{0} {1}".format(key=key, value=entry))
|
||||
elif isinstance(value, bool):
|
||||
job_args.append("-{0}".format(key))
|
||||
else:
|
||||
job_args.append("-{0} {1}".format(key, value))
|
||||
|
||||
job_str = " ".join(job_args)
|
||||
job_str += ' -file "%s"' % file
|
||||
|
||||
# Ensure output directory exists
|
||||
parent_dir = os.path.dirname(file)
|
||||
if not os.path.exists(parent_dir):
|
||||
os.makedirs(parent_dir)
|
||||
|
||||
if verbose:
|
||||
log.debug("Preparing Alembic export with options: %s",
|
||||
json.dumps(options, indent=4))
|
||||
log.debug("Extracting Alembic with job arguments: %s", job_str)
|
||||
|
||||
# Perform extraction
|
||||
cmds.AbcExport(j=job_str, verbose=verbose)
|
||||
|
||||
if verbose:
|
||||
log.debug("Extracted Alembic to: %s", file)
|
||||
|
||||
return file
|
||||
|
|
|
|||
|
|
@ -1,20 +1,9 @@
|
|||
import os
|
||||
import json
|
||||
import contextlib
|
||||
|
||||
from maya import cmds
|
||||
import copy
|
||||
|
||||
import avalon.maya
|
||||
import colorbleed.api
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def suspension():
|
||||
try:
|
||||
cmds.refresh(suspend=True)
|
||||
yield
|
||||
finally:
|
||||
cmds.refresh(suspend=False)
|
||||
from colorbleed.maya.lib import extract_alembic
|
||||
|
||||
|
||||
class ExtractAlembic(colorbleed.api.Extractor):
|
||||
|
|
@ -23,213 +12,23 @@ class ExtractAlembic(colorbleed.api.Extractor):
|
|||
This extracts an Alembic cache using the `-selection` flag to minimize
|
||||
the extracted content to solely what was Collected into the instance.
|
||||
|
||||
Arguments:
|
||||
|
||||
startFrame (float): Start frame of output. Ignored if `frameRange`
|
||||
provided.
|
||||
|
||||
endFrame (float): End frame of output. Ignored if `frameRange`
|
||||
provided.
|
||||
|
||||
frameRange (str): Frame range in the format of "startFrame endFrame".
|
||||
Overrides `startFrame` and `endFrame` arguments.
|
||||
|
||||
dataFormat (str): The data format to use for the cache,
|
||||
defaults to "ogawa"
|
||||
|
||||
verbose (bool): When on, outputs frame number information to the
|
||||
Script Editor or output window during extraction.
|
||||
|
||||
noNormals (bool): When on, normal data from the original polygon
|
||||
objects is not included in the exported Alembic cache file.
|
||||
|
||||
renderableOnly (bool): When on, any non-renderable nodes or hierarchy,
|
||||
such as hidden objects, are not included in the Alembic file.
|
||||
Defaults to False.
|
||||
|
||||
stripNamespaces (bool): When on, any namespaces associated with the
|
||||
exported objects are removed from the Alembic file. For example, an
|
||||
object with the namespace taco:foo:bar appears as bar in the
|
||||
Alembic file.
|
||||
|
||||
uvWrite (bool): When on, UV data from polygon meshes and subdivision
|
||||
objects are written to the Alembic file. Only the current UV map is
|
||||
included.
|
||||
|
||||
worldSpace (bool): When on, the top node in the node hierarchy is
|
||||
stored as world space. By default, these nodes are stored as local
|
||||
space. Defaults to False.
|
||||
|
||||
eulerFilter (bool): When on, X, Y, and Z rotation data is filtered with
|
||||
an Euler filter. Euler filtering helps resolve irregularities in
|
||||
rotations especially if X, Y, and Z rotations exceed 360 degrees.
|
||||
Defaults to True.
|
||||
"""
|
||||
|
||||
label = "Alembic"
|
||||
families = ["colorbleed.model",
|
||||
"colorbleed.pointcache",
|
||||
"colorbleed.animation",
|
||||
"colorbleed.proxy"]
|
||||
optional = True
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
"""Overridable options for Alembic export
|
||||
|
||||
Given in the following format
|
||||
- {NAME: EXPECTED TYPE}
|
||||
|
||||
If the overridden option's type does not match,
|
||||
the option is not included and a warning is logged.
|
||||
|
||||
"""
|
||||
|
||||
return {"startFrame": float,
|
||||
"endFrame": float,
|
||||
"frameRange": str, # "start end"; overrides startFrame & endFrame
|
||||
"eulerFilter": bool,
|
||||
"frameRelativeSample": float,
|
||||
"noNormals": bool,
|
||||
"renderableOnly": bool,
|
||||
"step": float,
|
||||
"stripNamespaces": bool,
|
||||
"uvWrite": bool,
|
||||
"wholeFrameGeo": bool,
|
||||
"worldSpace": bool,
|
||||
"writeVisibility": bool,
|
||||
"writeColorSets": bool,
|
||||
"writeFaceSets": bool,
|
||||
"writeCreases": bool, # Maya 2015 Ext1+
|
||||
"dataFormat": str,
|
||||
"root": (list, tuple),
|
||||
"attr": (list, tuple),
|
||||
"attrPrefix": (list, tuple),
|
||||
"userAttr": (list, tuple),
|
||||
"melPerFrameCallback": str,
|
||||
"melPostJobCallback": str,
|
||||
"pythonPerFrameCallback": str,
|
||||
"pythonPostJobCallback": str,
|
||||
"selection": bool}
|
||||
|
||||
@property
|
||||
def default_options(self):
|
||||
"""Supply default options to extraction.
|
||||
|
||||
This may be overridden by a subclass to provide
|
||||
alternative defaults.
|
||||
|
||||
"""
|
||||
|
||||
start_frame = cmds.playbackOptions(query=True, animationStartTime=True)
|
||||
end_frame = cmds.playbackOptions(query=True, animationEndTime=True)
|
||||
|
||||
return {"startFrame": start_frame,
|
||||
"endFrame": end_frame,
|
||||
"selection": True,
|
||||
"uvWrite": True,
|
||||
"eulerFilter": True,
|
||||
"dataFormat": "ogawa" # ogawa, hdf5
|
||||
}
|
||||
|
||||
def process(self, instance):
|
||||
# Ensure alembic exporter is loaded
|
||||
cmds.loadPlugin('AbcExport', quiet=True)
|
||||
|
||||
parent_dir = self.staging_dir(instance)
|
||||
filename = "%s.abc" % instance.name
|
||||
path = os.path.join(parent_dir, filename)
|
||||
|
||||
# Alembic Exporter requires forward slashes
|
||||
path = path.replace('\\', '/')
|
||||
options = copy.deepcopy(instance.data)
|
||||
|
||||
options = self.default_options
|
||||
options["userAttr"] = ("uuid",)
|
||||
options = self.parse_overrides(instance, options)
|
||||
options['selection'] = True
|
||||
|
||||
job_str = self.parse_options(options)
|
||||
job_str += ' -file "%s"' % path
|
||||
|
||||
self.log.info('Extracting alembic to: "%s"' % path)
|
||||
|
||||
verbose = instance.data('verbose', False)
|
||||
if verbose:
|
||||
self.log.debug('Alembic job string: "%s"'% job_str)
|
||||
|
||||
if not os.path.exists(parent_dir):
|
||||
os.makedirs(parent_dir)
|
||||
|
||||
with suspension():
|
||||
with avalon.maya.suspended_refresh():
|
||||
with avalon.maya.maintained_selection():
|
||||
self.log.debug(
|
||||
"Preparing %s for export using the following options: %s\n"
|
||||
"and the following string: %s"
|
||||
% (list(instance),
|
||||
json.dumps(options, indent=4),
|
||||
job_str))
|
||||
cmds.select(instance.data("setMembers"), hierarchy=True)
|
||||
cmds.AbcExport(j=job_str, verbose=verbose)
|
||||
|
||||
def parse_overrides(self, instance, options):
|
||||
"""Inspect data of instance to determine overridden options
|
||||
|
||||
An instance may supply any of the overridable options
|
||||
as data, the option is then added to the extraction.
|
||||
|
||||
"""
|
||||
|
||||
for key in instance.data():
|
||||
if key not in self.options:
|
||||
continue
|
||||
|
||||
# Ensure the data is of correct type
|
||||
value = instance.data(key)
|
||||
if not isinstance(value, self.options[key]):
|
||||
self.log.warning(
|
||||
"Overridden attribute {key} was of "
|
||||
"the wrong type: {invalid_type} "
|
||||
"- should have been {valid_type}".format(
|
||||
key=key,
|
||||
invalid_type=type(value).__name__,
|
||||
valid_type=self.options[key].__name__))
|
||||
continue
|
||||
|
||||
options[key] = value
|
||||
|
||||
return options
|
||||
|
||||
@classmethod
|
||||
def parse_options(cls, options):
|
||||
"""Convert key-word arguments to job arguments string
|
||||
|
||||
Args:
|
||||
options (dict): the options for the command
|
||||
"""
|
||||
|
||||
# Convert `startFrame` and `endFrame` arguments
|
||||
if 'startFrame' in options or 'endFrame' in options:
|
||||
start_frame = options.pop('startFrame', None)
|
||||
end_frame = options.pop('endFrame', None)
|
||||
|
||||
if 'frameRange' in options:
|
||||
cls.log.debug("The `startFrame` and/or `endFrame` arguments "
|
||||
"are overridden by the provided `frameRange`.")
|
||||
elif start_frame is None or end_frame is None:
|
||||
cls.log.warning("The `startFrame` and `endFrame` arguments "
|
||||
"must be supplied together.")
|
||||
else:
|
||||
options['frameRange'] = "%s %s" % (start_frame, end_frame)
|
||||
|
||||
job_args = list()
|
||||
for key, value in options.items():
|
||||
if isinstance(value, (list, tuple)):
|
||||
for entry in value:
|
||||
job_args.append("-%s %s" % (key, entry))
|
||||
elif isinstance(value, bool):
|
||||
job_args.append("%s" % key)
|
||||
else:
|
||||
job_args.append("-%s %s" % (key, value))
|
||||
|
||||
job_str = " ".join(job_args)
|
||||
|
||||
return job_str
|
||||
extract_alembic(file=path, **options)
|
||||
|
|
|
|||
64
colorbleed/plugins/maya/publish/extract_animation.py
Normal file
64
colorbleed/plugins/maya/publish/extract_animation.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import colorbleed.api
|
||||
|
||||
|
||||
class ExtractColorbleedAnimation(colorbleed.api.Extractor):
|
||||
"""Produce an alembic of just point positions and normals.
|
||||
|
||||
Positions and normals are preserved, but nothing more,
|
||||
for plain and predictable point caches.
|
||||
|
||||
"""
|
||||
|
||||
label = "Extract Animation"
|
||||
hosts = ["maya"]
|
||||
families = ["colorbleed.animation"]
|
||||
|
||||
def process(self, instance):
|
||||
import os
|
||||
from maya import cmds
|
||||
import avalon.maya
|
||||
from colorbleed.maya.lib import extract_alembic
|
||||
|
||||
# Collect the out set nodes
|
||||
out_sets = [node for node in instance if node.endswith("out_SET")]
|
||||
if len(out_sets) != 1:
|
||||
raise RuntimeError("Couldn't find exactly one out_SET: "
|
||||
"{0}".format(out_sets))
|
||||
out_set = out_sets[0]
|
||||
nodes = cmds.sets(out_set, query=True)
|
||||
|
||||
# Include all descendents
|
||||
nodes += cmds.listRelatives(nodes,
|
||||
allDescendents=True,
|
||||
fullPath=True) or []
|
||||
|
||||
# Collect the start and end including handles
|
||||
start = instance.data["startFrame"]
|
||||
end = instance.data["endFrame"]
|
||||
handles = instance.data.get("handles", 0)
|
||||
if handles:
|
||||
start -= handles
|
||||
end += handles
|
||||
|
||||
self.log.info("Extracting animation..")
|
||||
dirname = self.staging_dir(instance)
|
||||
|
||||
self.log.info("nodes: %s" % str(nodes))
|
||||
|
||||
parent_dir = self.staging_dir(instance)
|
||||
filename = "{name}.abc".format(**instance.data)
|
||||
path = os.path.join(parent_dir, filename)
|
||||
|
||||
with avalon.maya.suspended_refresh():
|
||||
with avalon.maya.maintained_selection():
|
||||
cmds.select(nodes, noExpand=True)
|
||||
extract_alembic(file=path, **{
|
||||
"selection": True,
|
||||
"frameRange": (start, end),
|
||||
"writeVisibility": True,
|
||||
"writeUV": True,
|
||||
"step": instance.data.get("step", 1.0),
|
||||
"attributePrefix": ("mb",)
|
||||
})
|
||||
|
||||
self.log.info("Extracted {} to {}".format(instance, dirname))
|
||||
Loading…
Add table
Add a link
Reference in a new issue