11using System.Collections.Generic;
27 [Header(
"Recording Settings")]
33 #pragma warning disable 0649
34 [SerializeField, ImplementsTypeNameDropdown(typeof(
LeapRecording))]
35 private string _leapRecordingType;
36 #pragma warning restore 0649
38 [Header(
"Compression Settings")]
64 public void BuildPlaybackPrefab(
ProgressBar progress) {
65 var timeline = ScriptableObject.CreateInstance<TimelineAsset>();
67 var animationTrack = timeline.CreateTrack<AnimationTrack>(
null,
"Playback Animation");
69 var clip = generateCompressedClip(progress);
71 var playableAsset = ScriptableObject.CreateInstance<AnimationPlayableAsset>();
72 playableAsset.clip = clip;
73 playableAsset.hideFlags = HideFlags.HideInInspector | HideFlags.HideInHierarchy;
74 playableAsset.name =
"Recorded Animation";
76 var timelineClip = animationTrack.CreateClip(clip);
77 timelineClip.duration = clip.length;
78 timelineClip.asset = playableAsset;
79 timelineClip.displayName =
"Recorded Animation";
85 timelineClip.GetType().GetField(
"m_Recordable", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).SetValue(timelineClip,
true);
92 if (File.Exists(framesPath)) {
93 List<Frame> frames =
new List<Frame>();
95 progress.
Begin(1,
"Loading Leap Data",
"", () => {
97 using (var reader = File.OpenText(framesPath)) {
99 string line = reader.ReadLine();
100 if (
string.IsNullOrEmpty(line)) {
104 frames.Add(JsonUtility.FromJson<
Frame>(line));
109 leapRecording = ScriptableObject.CreateInstance(_leapRecordingType) as
LeapRecording;
110 if (leapRecording !=
null) {
111 leapRecording.name =
"Recorded Leap Data";
114 Debug.LogError(
"Unable to create Leap recording: Invalid type specification for "
115 +
"LeapRecording implementation.",
this);
120 AssetDatabase.CreateAsset(timeline, assetPath);
121 AssetDatabase.AddObjectToAsset(playableAsset, timeline);
122 AssetDatabase.AddObjectToAsset(animationTrack, timeline);
123 AssetDatabase.AddObjectToAsset(clip, timeline);
126 if (leapRecording !=
null) {
127 recordingTrack = timeline.CreateTrack<
RecordingTrack>(
null,
"Leap Recording");
129 var recordingClip = recordingTrack.CreateDefaultClip();
130 recordingClip.duration = leapRecording.
length;
133 recordingAsset.
recording = leapRecording;
135 AssetDatabase.AddObjectToAsset(leapRecording, timeline);
138 AssetDatabase.SaveAssets();
139 AssetDatabase.Refresh();
142 var director = gameObject.AddComponent<PlayableDirector>();
143 director.playableAsset = timeline;
146 gameObject.AddComponent<Animator>();
150 director.SetGenericBinding(animationTrack.outputs.Query().First().sourceObject, gameObject);
153 var provider = gameObject.GetComponentInChildren<
LeapProvider>();
154 if (provider !=
null) {
155 GameObject providerObj = provider.gameObject;
156 DestroyImmediate(provider);
158 if (recordingTrack !=
null) {
160 director.SetGenericBinding(recordingTrack.outputs.Query().First().sourceObject, playableProvider);
164 buildAudioTracks(progress, director, timeline);
165 buildMethodRecordingTracks(progress, director, timeline);
167 progress.
Begin(1,
"",
"Finalizing Prefab", () => {
168 GameObject myGameObject = gameObject;
169 DestroyImmediate(
this);
172 #if UNITY_2018_3_OR_NEWER
173 var path = prefabPath.Replace(
'\\',
'/');
174 PrefabUtility.SaveAsPrefabAsset(myGameObject, path, out
bool didSucceed);
175 if (!didSucceed) {
Debug.LogError($
"Error saving prefab asset to {path}"); }
177 PrefabUtility.CreatePrefab(prefabPath.Replace(
'\\',
'/'), myGameObject);
182 private void buildMethodRecordingTracks(
ProgressBar progress, PlayableDirector director, TimelineAsset timeline) {
183 var recordings = GetComponentsInChildren<MethodRecording>();
184 if (recordings.Length > 0) {
185 progress.
Begin(recordings.Length,
"",
"Building Method Tracks: ", () => {
186 foreach (var recording in recordings) {
187 progress.Step(recording.gameObject.name);
190 var track = timeline.CreateTrack<MethodRecordingTrack>(null, recording.gameObject.name);
191 director.SetGenericBinding(track.outputs.Query().First().sourceObject, recording);
193 var clip = track.CreateClip<MethodRecordingClip>();
196 clip.duration = recording.GetDuration();
197 } catch (Exception e) {
198 Debug.LogException(e);
205 private void buildAudioTracks(
ProgressBar progress, PlayableDirector director, TimelineAsset timeline) {
206 var audioData = GetComponentsInChildren<RecordedAudio>(includeInactive:
true);
207 var sourceToData = audioData.Query().ToDictionary(a => a.target, a => a);
209 progress.
Begin(sourceToData.Count,
"",
"Building Audio Track: ", () => {
210 foreach (var pair in sourceToData) {
211 var track = timeline.CreateTrack<AudioTrack>(null, pair.Value.name);
212 director.SetGenericBinding(track.outputs.Query().First().sourceObject, pair.Key);
214 progress.Begin(pair.Value.data.Count,
"",
"", () => {
215 foreach (var clipData in pair.Value.data) {
216 progress.Step(clipData.clip.name);
218 var clip = track.CreateClip(clipData.clip);
219 clip.start = clipData.startTime;
220 clip.timeScale = clipData.pitch;
221 clip.duration = clipData.clip.length;
228 private AnimationClip generateCompressedClip(
ProgressBar progress) {
229 var clip =
new AnimationClip();
230 clip.name =
"Recorded Animation";
232 List<EditorCurveBindingData> curveData =
new List<EditorCurveBindingData>();
233 progress.
Begin(1,
"Opening Curve Files...",
"", () => {
235 using (var reader =
File.OpenText(Path.Combine(dataFolder.
Path,
"Curves.data"))) {
237 string line = reader.ReadLine();
238 if (
string.IsNullOrEmpty(line)) {
242 curveData.Add(JsonUtility.FromJson<EditorCurveBindingData>(line));
247 progress.
Begin(2,
"",
"", () => {
248 var bindingMap =
new Dictionary<EditorCurveBinding, AnimationCurve>();
250 Dictionary<string, Type> nameToType =
new Dictionary<string, Type>();
251 foreach (var component
in GetComponentsInChildren<Component>()) {
252 nameToType[component.GetType().Name] = component.GetType();
254 nameToType[typeof(GameObject).Name] = typeof(GameObject);
256 var toCompress =
new Dictionary<EditorCurveBinding, AnimationCurve>();
257 var targetObjects =
new HashSet<
UnityEngine.Object>();
259 foreach (var data
in curveData) {
261 if (!nameToType.TryGetValue(data.typeName, out type)) {
265 var binding = EditorCurveBinding.FloatCurve(data.path, type, data.propertyName);
267 var targetObj = AnimationUtility.GetAnimatedObject(gameObject, binding);
268 if (targetObj ==
null) {
272 toCompress[binding] = data.curve;
273 targetObjects.Add(targetObj);
276 progress.
Begin(targetObjects.Count,
"Compressing Curves",
"", () => {
277 foreach (var targetObj
in targetObjects) {
279 var filteredCurves =
new Dictionary<EditorCurveBinding, AnimationCurve>();
280 foreach (var curve
in toCompress) {
281 if (AnimationUtility.GetAnimatedObject(gameObject, curve.Key) == targetObj) {
282 filteredCurves[curve.Key] = curve.Value;
286 doCompression(progress, targetObj, filteredCurves, bindingMap);
290 progress.
Begin(bindingMap.Count,
"Assigning Curves",
"", () => {
291 foreach (var binding
in bindingMap) {
292 progress.
Step(binding.Key.propertyName);
293 AnimationUtility.SetEditorCurve(clip, binding.Key, binding.Value);
302 Dictionary<EditorCurveBinding, AnimationCurve> toCompress,
303 Dictionary<EditorCurveBinding, AnimationCurve> bindingMap) {
304 var propertyToMaxError = calculatePropertyErrors(targetObject);
306 List<EditorCurveBinding> bindings;
308 progress.
Begin(6,
"", targetObject.name, () => {
311 bindings = toCompress.Keys.Query().ToList();
312 progress.Begin(bindings.Count,
"",
"", () => {
313 foreach (var wBinding in bindings) {
316 if (!wBinding.propertyName.EndsWith(
".w")) {
320 string property = wBinding.propertyName.Substring(0, wBinding.propertyName.Length - 2);
321 string xProp = property +
".x";
322 string yProp = property +
".y";
323 string zProp = property +
".z";
325 var xMaybe = toCompress.Keys.Query().FirstOrNone(t => t.propertyName == xProp);
326 var yMaybe = toCompress.Keys.Query().FirstOrNone(t => t.propertyName == yProp);
327 var zMaybe = toCompress.Keys.Query().FirstOrNone(t => t.propertyName == zProp);
329 Maybe.MatchAll(xMaybe, yMaybe, zMaybe, (xBinding, yBinding, zBinding) => {
331 if (!propertyToMaxError.TryGetValue(property, out maxAngleError)) {
332 maxAngleError = rotationMaxError;
335 AnimationCurve compressedX, compressedY, compressedZ, compressedW;
336 AnimationCurveUtil.CompressRotations(toCompress[xBinding],
337 toCompress[yBinding],
338 toCompress[zBinding],
339 toCompress[wBinding],
346 bindingMap[xBinding] = compressedX;
347 bindingMap[yBinding] = compressedY;
348 bindingMap[zBinding] = compressedZ;
349 bindingMap[wBinding] = compressedW;
351 toCompress.Remove(xBinding);
352 toCompress.Remove(yBinding);
353 toCompress.Remove(zBinding);
354 toCompress.Remove(wBinding);
360 bindings = toCompress.Keys.Query().ToList();
361 progress.
Begin(bindings.Count,
"",
"", () => {
362 foreach (var binding in bindings) {
365 if (!binding.propertyName.EndsWith(
".x") &&
366 !binding.propertyName.EndsWith(
".y") &&
367 !binding.propertyName.EndsWith(
".z")) {
371 if (!binding.propertyName.Contains(
"LocalScale")) {
375 bindingMap[binding] = AnimationCurveUtil.CompressScale(toCompress[binding], scaleMaxError);
376 toCompress.Remove(binding);
381 bindings = toCompress.Keys.Query().ToList();
382 progress.
Begin(bindings.Count,
"",
"", () => {
383 foreach (var xBinding in bindings) {
386 if (!xBinding.propertyName.EndsWith(
".x")) {
390 string property = xBinding.propertyName.Substring(0, xBinding.propertyName.Length - 2);
391 string yProp = property +
".y";
392 string zProp = property +
".z";
394 var yMaybe = toCompress.Keys.Query().FirstOrNone(t => t.propertyName == yProp);
395 var zMaybe = toCompress.Keys.Query().FirstOrNone(t => t.propertyName == zProp);
397 Maybe.MatchAll(yMaybe, zMaybe, (yBinding, zBinding) => {
398 float maxDistanceError;
399 if (!propertyToMaxError.TryGetValue(property, out maxDistanceError)) {
400 maxDistanceError = positionMaxError;
403 AnimationCurve compressedX, compressedY, compressedZ;
404 AnimationCurveUtil.CompressPositions(toCompress[xBinding],
405 toCompress[yBinding],
406 toCompress[zBinding],
412 bindingMap[xBinding] = compressedX;
413 bindingMap[yBinding] = compressedY;
414 bindingMap[zBinding] = compressedZ;
416 toCompress.Remove(xBinding);
417 toCompress.Remove(yBinding);
418 toCompress.Remove(zBinding);
424 bindings = toCompress.Keys.Query().ToList();
425 progress.
Begin(bindings.Count,
"",
"", () => {
426 foreach (var rBinding in bindings) {
429 if (!rBinding.propertyName.EndsWith(
".r")) {
433 string property = rBinding.propertyName.Substring(0, rBinding.propertyName.Length - 2);
434 string gProp = property +
".g";
435 string bProp = property +
".b";
437 var gMaybe = toCompress.Keys.Query().FirstOrNone(t => t.propertyName == gProp);
438 var bMaybe = toCompress.Keys.Query().FirstOrNone(t => t.propertyName == bProp);
440 Maybe.MatchAll(gMaybe, bMaybe, (gBinding, bBinding) => {
441 AnimationCurve compressedR, compressedG, compressedB;
442 AnimationCurveUtil.CompressColorsHSV(toCompress[rBinding],
443 toCompress[gBinding],
444 toCompress[bBinding],
449 colorSaturationMaxError,
452 bindingMap[rBinding] = compressedR;
453 bindingMap[gBinding] = compressedG;
454 bindingMap[bBinding] = compressedB;
460 bindings = toCompress.Keys.Query().ToList();
461 progress.
Begin(bindings.Count,
"",
"", () => {
462 foreach (var aBinding in bindings) {
465 if (!aBinding.propertyName.EndsWith(
".a")) {
469 var compressedA = AnimationCurveUtil.Compress(toCompress[aBinding], colorAlphaMaxError);
471 toCompress.Remove(aBinding);
472 bindingMap[aBinding] = compressedA;
477 bindings = toCompress.Keys.Query().ToList();
478 progress.
Begin(bindings.Count,
"",
"", () => {
479 foreach (var binding in bindings) {
483 if (!propertyToMaxError.TryGetValue(binding.propertyName, out maxError)) {
484 maxError = genericMaxError;
487 var compressedCurve = AnimationCurveUtil.Compress(toCompress[binding], maxError);
489 toCompress.Remove(binding);
490 bindingMap[binding] = compressedCurve;
496 private Dictionary<string, float> calculatePropertyErrors(
UnityEngine.Object targetObject) {
497 Transform targetTransform;
498 if (targetObject is GameObject) {
499 targetTransform = (targetObject as GameObject).transform;
501 targetTransform = (targetObject as
Component).transform;
503 throw new InvalidOperationException(
"Unexpected target object type " + targetObject);
506 var propertyToMaxError =
new Dictionary<string, float>();
507 Transform currTransform = targetTransform;
508 while (currTransform !=
null) {
509 var compressionSettings = currTransform.GetComponent<PropertyCompression>();
510 if (compressionSettings !=
null) {
511 foreach (var setting
in compressionSettings.compressionOverrides) {
512 if (!propertyToMaxError.ContainsKey(setting.propertyName)) {
513 propertyToMaxError.Add(setting.propertyName, setting.maxError);
517 currTransform = currTransform.parent;
520 return propertyToMaxError;
UnityEngine.Component Component
The Frame class represents a set of hand and finger tracking data detected in a single frame.
A convenient serializable representation of an asset folder. Only useful for editor scripts since ass...
virtual string Path
Gets or sets the folder path. This path will always be a path relative to the asset folder,...
Provides Frame object data to the Unity application by firing events as soon as Frame data is availab...
This class allows you to easily give feedback of an action as it completes.
void Begin(int sections, string title, string info, Action action)
Begins a new chunk. If this call is made from within a chunk it will generate a sub-chunk that repres...
void Step(string infoString="")
Steps through one section of the current chunk. You can provide an optional info string that will be ...
float colorSaturationMaxError
abstract float length
Returns the length of the recording in seconds.
abstract void LoadFrames(List< Frame > frames)
Loads this recording with data from the provided TEMPORARY list of frames. These frames reflect raw r...