-
Notifications
You must be signed in to change notification settings - Fork 1
/
extract_alembic.py
232 lines (187 loc) · 8.06 KB
/
extract_alembic.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
import os
import json
import contextlib
import pyblish_maya
import pyblish_magenta.plugin
from maya import cmds
@contextlib.contextmanager
def suspension():
try:
cmds.refresh(suspend=True)
yield
finally:
cmds.refresh(suspend=False)
class ExtractAlembic(pyblish_magenta.api.Extractor):
"""Extract 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 (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 = ["pointcache"]
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.
"""
from maya import cmds
start_frame = cmds.playbackOptions(q=True, animationStartTime=True)
end_frame = cmds.playbackOptions(q=True, animationEndTime=True)
# We include 5 frame handles by default
handles = 5
start_frame -= handles
end_frame += handles
return {
"frameRange": "%s %s" % (start_frame, 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)
# Define extract output file path
temp_dir = self.temp_dir(instance)
# parent_dir = os.path.join(temp_dir, instance.data("name"))
parent_dir = temp_dir
filename = "{0}.abc".format(instance.name)
path = os.path.join(parent_dir, filename)
# Alembic Exporter requires forward slashes
path = path.replace('\\', '/')
self.log.info("Extracting alembic to: {0}".format(path))
options = self.default_options
options["userAttr"] = ("uuid",)
options = self.parse_overrides(instance, options)
job_str = self.parse_options(options)
job_str += ' -file "{0}"'.format(path)
self.log.info("Extracting alembic to: {0}".format(path))
verbose = instance.data('verbose', False)
if verbose:
self.log.debug('Alembic job string: "{0}"'.format(job_str))
if not os.path.exists(parent_dir):
os.makedirs(parent_dir)
with suspension():
with pyblish_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, 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"""
# 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.iteritems():
if isinstance(value, (list, tuple)):
for entry in value:
job_args.append("-{0} {1}".format(key, entry))
if isinstance(value, bool):
job_args.append("-{0}".format(key))
else:
job_args.append("-{0} {1}".format(key, value))
job_str = " ".join(job_args)
return job_str