Tanoda
HierarchyPostProcess.cs
Go to the documentation of this file.
1/******************************************************************************
2 * Copyright (C) Ultraleap, Inc. 2011-2020. *
3 * *
4 * Use subject to the terms of the Apache License 2.0 available at *
5 * http://www.apache.org/licenses/LICENSE-2.0, or another agreement *
6 * between Ultraleap and you, your company or other organization. *
7 ******************************************************************************/
8
9using System;
10using System.IO;
11using System.Collections.Generic;
12using UnityEngine;
13using UnityEngine.Playables;
14using UnityEngine.Timeline;
15#if UNITY_EDITOR
16using UnityEditor;
17#endif
18using Leap.Unity;
19using Leap.Unity.Query;
22
23namespace Leap.Unity.Recording {
24
25 public class HierarchyPostProcess : MonoBehaviour {
26
27 [Header("Recording Settings")]
28 public string recordingName;
30
32
33 #pragma warning disable 0649
34 [SerializeField, ImplementsTypeNameDropdown(typeof(LeapRecording))]
35 private string _leapRecordingType;
36 #pragma warning restore 0649
37
38 [Header("Compression Settings")]
39 [MinValue(0)]
40 public float positionMaxError = 0.005f;
41
42 [Range(0, 90)]
43 public float rotationMaxError = 1;
44
45 [MinValue(1)]
46 public float scaleMaxError = 1.02f;
47
48 [Range(0, 1)]
49 public float colorHueMaxError = 0.05f;
50
51 [Range(0, 1)]
52 public float colorSaturationMaxError = 0.05f;
53
54 [Range(0, 1)]
55 public float colorValueMaxError = 0.05f;
56
57 [Range(0, 1)]
58 public float colorAlphaMaxError = 0.05f;
59
60 [MinValue(0)]
61 public float genericMaxError = 0.05f;
62
63#if UNITY_EDITOR
64 public void BuildPlaybackPrefab(ProgressBar progress) {
65 var timeline = ScriptableObject.CreateInstance<TimelineAsset>();
66
67 var animationTrack = timeline.CreateTrack<AnimationTrack>(null, "Playback Animation");
68
69 var clip = generateCompressedClip(progress);
70
71 var playableAsset = ScriptableObject.CreateInstance<AnimationPlayableAsset>();
72 playableAsset.clip = clip;
73 playableAsset.hideFlags = HideFlags.HideInInspector | HideFlags.HideInHierarchy;
74 playableAsset.name = "Recorded Animation";
75
76 var timelineClip = animationTrack.CreateClip(clip);
77 timelineClip.duration = clip.length;
78 timelineClip.asset = playableAsset;
79 timelineClip.displayName = "Recorded Animation";
80
81 //If a clip is not recordable, it will not show up as editable in the timeline view.
82 //For whatever reason unity decided that imported clips are not recordable, so we hack a
83 //private variable to force them to be! This seems to have no ill effects but if things go
84 //wrong we can just revert this line
85 timelineClip.GetType().GetField("m_Recordable", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).SetValue(timelineClip, true);
86
87 //Try to generate a leap recording if we have leap data
88 RecordingTrack recordingTrack = null;
89 LeapRecording leapRecording = null;
90
91 string framesPath = Path.Combine(dataFolder.Path, "Frames.data");
92 if (File.Exists(framesPath)) {
93 List<Frame> frames = new List<Frame>();
94
95 progress.Begin(1, "Loading Leap Data", "", () => {
96 progress.Step();
97 using (var reader = File.OpenText(framesPath)) {
98 while (true) {
99 string line = reader.ReadLine();
100 if (string.IsNullOrEmpty(line)) {
101 break;
102 }
103
104 frames.Add(JsonUtility.FromJson<Frame>(line));
105 }
106 }
107 });
108
109 leapRecording = ScriptableObject.CreateInstance(_leapRecordingType) as LeapRecording;
110 if (leapRecording != null) {
111 leapRecording.name = "Recorded Leap Data";
112 leapRecording.LoadFrames(frames);
113 } else {
114 Debug.LogError("Unable to create Leap recording: Invalid type specification for "
115 + "LeapRecording implementation.", this);
116 }
117 }
118
119 string assetPath = Path.Combine(assetFolder.Path, recordingName + ".asset");
120 AssetDatabase.CreateAsset(timeline, assetPath);
121 AssetDatabase.AddObjectToAsset(playableAsset, timeline);
122 AssetDatabase.AddObjectToAsset(animationTrack, timeline);
123 AssetDatabase.AddObjectToAsset(clip, timeline);
124
125 //If we do have a leap recording, create a recording track to house it
126 if (leapRecording != null) {
127 recordingTrack = timeline.CreateTrack<RecordingTrack>(null, "Leap Recording");
128
129 var recordingClip = recordingTrack.CreateDefaultClip();
130 recordingClip.duration = leapRecording.length;
131
132 var recordingAsset = recordingClip.asset as RecordingClip;
133 recordingAsset.recording = leapRecording;
134
135 AssetDatabase.AddObjectToAsset(leapRecording, timeline);
136 }
137
138 AssetDatabase.SaveAssets();
139 AssetDatabase.Refresh();
140
141 //Create the playable director and link it to the new timeline
142 var director = gameObject.AddComponent<PlayableDirector>();
143 director.playableAsset = timeline;
144
145 //Create the animator
146 gameObject.AddComponent<Animator>();
147
148 //Link the animation track to the animator
149 //(it likes to point to gameobject instead of the animator directly)
150 director.SetGenericBinding(animationTrack.outputs.Query().First().sourceObject, gameObject);
151
152 //Destroy existing provider
153 var provider = gameObject.GetComponentInChildren<LeapProvider>();
154 if (provider != null) {
155 GameObject providerObj = provider.gameObject;
156 DestroyImmediate(provider);
157 //If a leap recording track exists, spawn a playable provider and link it to the track
158 if (recordingTrack != null) {
159 var playableProvider = providerObj.AddComponent<LeapPlayableProvider>();
160 director.SetGenericBinding(recordingTrack.outputs.Query().First().sourceObject, playableProvider);
161 }
162 }
163
164 buildAudioTracks(progress, director, timeline);
165 buildMethodRecordingTracks(progress, director, timeline);
166
167 progress.Begin(1, "", "Finalizing Prefab", () => {
168 GameObject myGameObject = gameObject;
169 DestroyImmediate(this);
170
171 string prefabPath = Path.Combine(assetFolder.Path, recordingName + ".prefab");
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}"); }
176 #else
177 PrefabUtility.CreatePrefab(prefabPath.Replace('\\', '/'), myGameObject);
178 #endif
179 });
180 }
181
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);
188
189 try {
190 var track = timeline.CreateTrack<MethodRecordingTrack>(null, recording.gameObject.name);
191 director.SetGenericBinding(track.outputs.Query().First().sourceObject, recording);
192
193 var clip = track.CreateClip<MethodRecordingClip>();
194
195
196 clip.duration = recording.GetDuration();
197 } catch (Exception e) {
198 Debug.LogException(e);
199 }
200 }
201 });
202 }
203 }
204
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);
208
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);
213
214 progress.Begin(pair.Value.data.Count, "", "", () => {
215 foreach (var clipData in pair.Value.data) {
216 progress.Step(clipData.clip.name);
217
218 var clip = track.CreateClip(clipData.clip);
219 clip.start = clipData.startTime;
220 clip.timeScale = clipData.pitch;
221 clip.duration = clipData.clip.length;
222 }
223 });
224 }
225 });
226 }
227
228 private AnimationClip generateCompressedClip(ProgressBar progress) {
229 var clip = new AnimationClip();
230 clip.name = "Recorded Animation";
231
232 List<EditorCurveBindingData> curveData = new List<EditorCurveBindingData>();
233 progress.Begin(1, "Opening Curve Files...", "", () => {
234 progress.Step();
235 using (var reader = File.OpenText(Path.Combine(dataFolder.Path, "Curves.data"))) {
236 while (true) {
237 string line = reader.ReadLine();
238 if (string.IsNullOrEmpty(line)) {
239 break;
240 }
241
242 curveData.Add(JsonUtility.FromJson<EditorCurveBindingData>(line));
243 }
244 }
245 });
246
247 progress.Begin(2, "", "", () => {
248 var bindingMap = new Dictionary<EditorCurveBinding, AnimationCurve>();
249
250 Dictionary<string, Type> nameToType = new Dictionary<string, Type>();
251 foreach (var component in GetComponentsInChildren<Component>()) {
252 nameToType[component.GetType().Name] = component.GetType();
253 }
254 nameToType[typeof(GameObject).Name] = typeof(GameObject);
255
256 var toCompress = new Dictionary<EditorCurveBinding, AnimationCurve>();
257 var targetObjects = new HashSet<UnityEngine.Object>();
258
259 foreach (var data in curveData) {
260 Type type;
261 if (!nameToType.TryGetValue(data.typeName, out type)) {
262 continue;
263 }
264
265 var binding = EditorCurveBinding.FloatCurve(data.path, type, data.propertyName);
266
267 var targetObj = AnimationUtility.GetAnimatedObject(gameObject, binding);
268 if (targetObj == null) {
269 continue;
270 }
271
272 toCompress[binding] = data.curve;
273 targetObjects.Add(targetObj);
274 }
275
276 progress.Begin(targetObjects.Count, "Compressing Curves", "", () => {
277 foreach (var targetObj in targetObjects) {
278
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;
283 }
284 }
285
286 doCompression(progress, targetObj, filteredCurves, bindingMap);
287 }
288 });
289
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);
294 }
295 });
296 });
297 return clip;
298 }
299
300 private void doCompression(ProgressBar progress,
301 UnityEngine.Object targetObject,
302 Dictionary<EditorCurveBinding, AnimationCurve> toCompress,
303 Dictionary<EditorCurveBinding, AnimationCurve> bindingMap) {
304 var propertyToMaxError = calculatePropertyErrors(targetObject);
305
306 List<EditorCurveBinding> bindings;
307
308 progress.Begin(6, "", targetObject.name, () => {
309
310 //First do rotations
311 bindings = toCompress.Keys.Query().ToList();
312 progress.Begin(bindings.Count, "", "", () => {
313 foreach (var wBinding in bindings) {
314 progress.Step();
315
316 if (!wBinding.propertyName.EndsWith(".w")) {
317 continue;
318 }
319
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";
324
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);
328
329 Maybe.MatchAll(xMaybe, yMaybe, zMaybe, (xBinding, yBinding, zBinding) => {
330 float maxAngleError;
331 if (!propertyToMaxError.TryGetValue(property, out maxAngleError)) {
332 maxAngleError = rotationMaxError;
333 }
334
335 AnimationCurve compressedX, compressedY, compressedZ, compressedW;
336 AnimationCurveUtil.CompressRotations(toCompress[xBinding],
337 toCompress[yBinding],
338 toCompress[zBinding],
339 toCompress[wBinding],
340 out compressedX,
341 out compressedY,
342 out compressedZ,
343 out compressedW,
344 maxAngleError);
345
346 bindingMap[xBinding] = compressedX;
347 bindingMap[yBinding] = compressedY;
348 bindingMap[zBinding] = compressedZ;
349 bindingMap[wBinding] = compressedW;
350
351 toCompress.Remove(xBinding);
352 toCompress.Remove(yBinding);
353 toCompress.Remove(zBinding);
354 toCompress.Remove(wBinding);
355 });
356 }
357 });
358
359 //Next do scales
360 bindings = toCompress.Keys.Query().ToList();
361 progress.Begin(bindings.Count, "", "", () => {
362 foreach (var binding in bindings) {
363 progress.Step();
364
365 if (!binding.propertyName.EndsWith(".x") &&
366 !binding.propertyName.EndsWith(".y") &&
367 !binding.propertyName.EndsWith(".z")) {
368 continue;
369 }
370
371 if (!binding.propertyName.Contains("LocalScale")) {
372 continue;
373 }
374
375 bindingMap[binding] = AnimationCurveUtil.CompressScale(toCompress[binding], scaleMaxError);
376 toCompress.Remove(binding);
377 }
378 });
379
380 //Next do positions
381 bindings = toCompress.Keys.Query().ToList();
382 progress.Begin(bindings.Count, "", "", () => {
383 foreach (var xBinding in bindings) {
384 progress.Step();
385
386 if (!xBinding.propertyName.EndsWith(".x")) {
387 continue;
388 }
389
390 string property = xBinding.propertyName.Substring(0, xBinding.propertyName.Length - 2);
391 string yProp = property + ".y";
392 string zProp = property + ".z";
393
394 var yMaybe = toCompress.Keys.Query().FirstOrNone(t => t.propertyName == yProp);
395 var zMaybe = toCompress.Keys.Query().FirstOrNone(t => t.propertyName == zProp);
396
397 Maybe.MatchAll(yMaybe, zMaybe, (yBinding, zBinding) => {
398 float maxDistanceError;
399 if (!propertyToMaxError.TryGetValue(property, out maxDistanceError)) {
400 maxDistanceError = positionMaxError;
401 }
402
403 AnimationCurve compressedX, compressedY, compressedZ;
404 AnimationCurveUtil.CompressPositions(toCompress[xBinding],
405 toCompress[yBinding],
406 toCompress[zBinding],
407 out compressedX,
408 out compressedY,
409 out compressedZ,
410 maxDistanceError);
411
412 bindingMap[xBinding] = compressedX;
413 bindingMap[yBinding] = compressedY;
414 bindingMap[zBinding] = compressedZ;
415
416 toCompress.Remove(xBinding);
417 toCompress.Remove(yBinding);
418 toCompress.Remove(zBinding);
419 });
420 }
421 });
422
423 //Next do colors
424 bindings = toCompress.Keys.Query().ToList();
425 progress.Begin(bindings.Count, "", "", () => {
426 foreach (var rBinding in bindings) {
427 progress.Step();
428
429 if (!rBinding.propertyName.EndsWith(".r")) {
430 continue;
431 }
432
433 string property = rBinding.propertyName.Substring(0, rBinding.propertyName.Length - 2);
434 string gProp = property + ".g";
435 string bProp = property + ".b";
436
437 var gMaybe = toCompress.Keys.Query().FirstOrNone(t => t.propertyName == gProp);
438 var bMaybe = toCompress.Keys.Query().FirstOrNone(t => t.propertyName == bProp);
439
440 Maybe.MatchAll(gMaybe, bMaybe, (gBinding, bBinding) => {
441 AnimationCurve compressedR, compressedG, compressedB;
442 AnimationCurveUtil.CompressColorsHSV(toCompress[rBinding],
443 toCompress[gBinding],
444 toCompress[bBinding],
445 out compressedR,
446 out compressedG,
447 out compressedB,
448 colorHueMaxError,
449 colorSaturationMaxError,
450 colorValueMaxError);
451
452 bindingMap[rBinding] = compressedR;
453 bindingMap[gBinding] = compressedG;
454 bindingMap[bBinding] = compressedB;
455 });
456 }
457 });
458
459 //Then do color alpha
460 bindings = toCompress.Keys.Query().ToList();
461 progress.Begin(bindings.Count, "", "", () => {
462 foreach (var aBinding in bindings) {
463 progress.Step();
464
465 if (!aBinding.propertyName.EndsWith(".a")) {
466 continue;
467 }
468
469 var compressedA = AnimationCurveUtil.Compress(toCompress[aBinding], colorAlphaMaxError);
470
471 toCompress.Remove(aBinding);
472 bindingMap[aBinding] = compressedA;
473 }
474 });
475
476 //Then everything else
477 bindings = toCompress.Keys.Query().ToList();
478 progress.Begin(bindings.Count, "", "", () => {
479 foreach (var binding in bindings) {
480 progress.Step();
481
482 float maxError;
483 if (!propertyToMaxError.TryGetValue(binding.propertyName, out maxError)) {
484 maxError = genericMaxError;
485 }
486
487 var compressedCurve = AnimationCurveUtil.Compress(toCompress[binding], maxError);
488
489 toCompress.Remove(binding);
490 bindingMap[binding] = compressedCurve;
491 }
492 });
493 });
494 }
495
496 private Dictionary<string, float> calculatePropertyErrors(UnityEngine.Object targetObject) {
497 Transform targetTransform;
498 if (targetObject is GameObject) {
499 targetTransform = (targetObject as GameObject).transform;
500 } else if (targetObject is Component) {
501 targetTransform = (targetObject as Component).transform;
502 } else {
503 throw new InvalidOperationException("Unexpected target object type " + targetObject);
504 }
505
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);
514 }
515 }
516 }
517 currTransform = currTransform.parent;
518 }
519
520 return propertyToMaxError;
521 }
522#endif
523 }
524}
UnityEngine.Component Component
UnityEngine.Debug Debug
Definition: TanodaServer.cs:19
The Frame class represents a set of hand and finger tracking data detected in a single frame.
Definition: Frame.cs:24
A convenient serializable representation of an asset folder. Only useful for editor scripts since ass...
Definition: AssetFolder.cs:26
virtual string Path
Gets or sets the folder path. This path will always be a path relative to the asset folder,...
Definition: AssetFolder.cs:43
Provides Frame object data to the Unity application by firing events as soon as Frame data is availab...
Definition: LeapProvider.cs:21
This class allows you to easily give feedback of an action as it completes.
Definition: ProgressBar.cs:66
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...
Definition: ProgressBar.cs:109
void Step(string infoString="")
Steps through one section of the current chunk. You can provide an optional info string that will be ...
Definition: ProgressBar.cs:147
float colorSaturationMaxError
float colorHueMaxError
float positionMaxError
AssetFolder dataFolder
AssetFolder assetFolder
float scaleMaxError
float colorAlphaMaxError
string recordingName
float rotationMaxError
float genericMaxError
float colorValueMaxError
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...