/* * Copyright (c) Meta Platforms, Inc. and affiliates. * All rights reserved. * * Licensed under the Oculus SDK License Agreement (the "License"); * you may not use the Oculus SDK except in compliance with the License, * which is provided at the time of installation or download, or which * otherwise accompanies this software in either electronic or hard copy form. * * You may obtain a copy of the License at * * https://developer.oculus.com/licenses/oculussdk/ * * Unless required by applicable law or agreed to in writing, the Oculus SDK * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using Oculus.Interaction.Input; using System.Collections.Generic; using System.IO; using System.Linq; using UnityEditor; using UnityEngine; namespace Oculus.Interaction.Utils { public static class HandAnimationUtils { public const string localEulerKey = "localEulerAnglesRaw."; public const string positionKey = "m_LocalPosition."; public static void Compress(ref AnimationClip clip, float rotationThreshold, float positionThreshold) { EditorCurveBinding[] bindings = AnimationUtility.GetCurveBindings(clip); foreach (EditorCurveBinding binding in bindings) { float threshold; if (binding.propertyName.StartsWith(localEulerKey)) { threshold = rotationThreshold; } else if (binding.propertyName.StartsWith(positionKey)) { threshold = positionThreshold; } else { return; } AnimationCurve curve = AnimationUtility.GetEditorCurve(clip, binding); for (int i = 2; i < curve.keys.Length; i++) { Keyframe prevFrame = curve.keys[i - 1]; Keyframe prevPrevFrame = curve.keys[i - 2]; Keyframe currentFrame = curve.keys[i]; Vector2 prevDelta = new Vector2(prevFrame.time - prevPrevFrame.time, prevFrame.value - prevPrevFrame.value); Vector2 currDelta = new Vector2(currentFrame.time - prevFrame.time, currentFrame.value - prevFrame.value); float slopeDifference = prevDelta.x * currDelta.y - prevDelta.y * currDelta.x; if (Mathf.Abs(slopeDifference) < threshold) { curve.RemoveKey(i--); } } SmoothCurveTangets(ref curve); AnimationUtility.SetEditorCurve(clip, binding, curve); } } public static void Trim(ref AnimationClip clip, float minTime, float maxTime) { EditorCurveBinding[] bindings = AnimationUtility.GetCurveBindings(clip); float min = minTime * clip.length; float max = maxTime * clip.length; foreach (var binding in bindings) { AnimationCurve curve = AnimationUtility.GetEditorCurve(clip, binding); float minValue = curve.Evaluate(min); float maxValue = curve.Evaluate(max); while (curve.length > 0 && curve.keys[0].time < min) { curve.RemoveKey(0); } while (curve.length > 0 && curve.keys[curve.keys.Length - 1].time > max) { curve.RemoveKey(curve.keys.Length - 1); } if (curve.length > 0) { if (curve.keys[0].time > min) { curve.AddKey(new Keyframe(min, minValue)); } if (curve.keys[curve.keys.Length - 1].time != max) { curve.AddKey(new Keyframe(max, maxValue)); } for (int i = 0; i < curve.keys.Length; i++) { Keyframe keyframe = curve.keys[i]; keyframe.time -= min; curve.MoveKey(i, keyframe); } } SmoothCurveTangets(ref curve); AnimationUtility.SetEditorCurve(clip, binding, curve); } } public static AnimationClip Mirror(AnimationClip clip, IList joints, Transform root, HandFingerJointFlags maskHandJointIds, Handedness fromHandedness, string leftPrefix, string rightPrefix, bool includePosition) { AnimationClip mirrorClip = new AnimationClip(); mirrorClip.frameRate = clip.frameRate; string fromPrefix = fromHandedness == Handedness.Left ? leftPrefix : rightPrefix; string toPrefix = fromHandedness == Handedness.Left ? rightPrefix : leftPrefix; var fromHandSpace = fromHandedness == Handedness.Left ? HandMirroring.LeftHandSpace : HandMirroring.RightHandSpace; var toHandSpace = fromHandedness == Handedness.Left ? HandMirroring.RightHandSpace : HandMirroring.LeftHandSpace; EditorCurveBinding[] curveBindings = AnimationUtility.GetCurveBindings(clip); foreach (EditorCurveBinding curveBinding in curveBindings) { string path = curveBinding.path; string mirrorPath = path.Replace(fromPrefix, toPrefix); AnimationCurve curve = AnimationUtility.GetEditorCurve(clip, curveBinding); mirrorClip.SetCurve(mirrorPath, curveBinding.type, curveBinding.propertyName, curve); } for (HandJointId jointId = HandJointId.HandStart; jointId < HandJointId.HandEnd; jointId++) { if (((int)maskHandJointIds & (1 << (int)jointId)) == 0) { continue; } string path = GetGameObjectPath(joints[(int)jointId], root); JointRecord mirroredRecord = new JointRecord(jointId, path); EditorCurveBinding bindingEulerX = curveBindings.First(cb => cb.path == path && cb.propertyName.StartsWith($"{localEulerKey}x")); EditorCurveBinding bindingEulerY = curveBindings.First(cb => cb.path == path && cb.propertyName.StartsWith($"{localEulerKey}y")); EditorCurveBinding bindingEulerZ = curveBindings.First(cb => cb.path == path && cb.propertyName.StartsWith($"{localEulerKey}z")); AnimationCurve curveEulerX = AnimationUtility.GetEditorCurve(clip, bindingEulerX); AnimationCurve curveEulerY = AnimationUtility.GetEditorCurve(clip, bindingEulerY); AnimationCurve curveEulerZ = AnimationUtility.GetEditorCurve(clip, bindingEulerZ); AnimationCurve curvePositionX = null; AnimationCurve curvePositionY = null; AnimationCurve curvePositionZ = null; if (includePosition) { EditorCurveBinding bindingPositionX = curveBindings.First(cb => cb.path == path && cb.propertyName.StartsWith($"{positionKey}x")); EditorCurveBinding bindingPositionY = curveBindings.First(cb => cb.path == path && cb.propertyName.StartsWith($"{positionKey}y")); EditorCurveBinding bindingPositionZ = curveBindings.First(cb => cb.path == path && cb.propertyName.StartsWith($"{positionKey}z")); curvePositionX = AnimationUtility.GetEditorCurve(clip, bindingPositionX); curvePositionY = AnimationUtility.GetEditorCurve(clip, bindingPositionY); curvePositionZ = AnimationUtility.GetEditorCurve(clip, bindingPositionZ); } for (float time = 0; time <= clip.length; time += 1f / clip.frameRate) { Quaternion rotation = Quaternion.Euler( curveEulerX.Evaluate(time), curveEulerY.Evaluate(time), curveEulerZ.Evaluate(time)); Quaternion rotationMirrored = HandMirroring.TransformRotation(rotation, fromHandSpace, toHandSpace); Vector3 positionMirrored = Vector3.zero; if (includePosition) { Vector3 position = new Vector3( curvePositionX.Evaluate(time), curvePositionY.Evaluate(time), curvePositionZ.Evaluate(time)); positionMirrored = HandMirroring.TransformPosition(position, fromHandSpace, toHandSpace); } Pose mirroredPose = new Pose(positionMirrored, rotationMirrored); mirroredRecord.RecordPose(time, mirroredPose); } WriteAnimationCurves(ref mirrorClip, mirroredRecord, includePosition); } return mirrorClip; } public static void WriteAnimationCurves(ref AnimationClip clip, JointRecord record, bool includePosition) { WriteCurve(ref clip, record.Path, $"{localEulerKey}x", record.RotationX); WriteCurve(ref clip, record.Path, $"{localEulerKey}y", record.RotationY); WriteCurve(ref clip, record.Path, $"{localEulerKey}z", record.RotationZ); if (includePosition) { WriteCurve(ref clip, record.Path, $"{positionKey}x", record.PositionX); WriteCurve(ref clip, record.Path, $"{positionKey}y", record.PositionY); WriteCurve(ref clip, record.Path, $"{positionKey}z", record.PositionZ); } } public static void WriteCurve(ref AnimationClip clip, string path, string propertyName, List frames) { AnimationCurve curve = new AnimationCurve(frames.ToArray()); clip.SetCurve(path, typeof(Transform), propertyName, curve); } public static bool TryGetClipHandedness(AnimationClip clip, string leftPrefix, string rightPrefix, out Handedness handedness) { if (clip == null) { handedness = Handedness.Left; return false; } EditorCurveBinding[] curveBindings = AnimationUtility.GetCurveBindings(clip); foreach (EditorCurveBinding curveBinding in curveBindings) { if (curveBinding.path.Contains(leftPrefix)) { handedness = Handedness.Left; return true; } else if (curveBinding.path.Contains(rightPrefix)) { handedness = Handedness.Right; return true; } } handedness = Handedness.Left; return false; } public static void StoreAsset(Object asset, string folder, string name) { string targetFolder = Path.Combine("Assets", folder); CreateFolder(targetFolder); string path = Path.Combine(targetFolder, name); AssetDatabase.CreateAsset(asset, path); AssetDatabase.Refresh(); Debug.Log($"Asset generated at {path}"); } public static void CreateFolder(string targetFolder) { if (!Directory.Exists(targetFolder)) { Directory.CreateDirectory(targetFolder); } } public static string GetGameObjectPath(Transform transform, Transform root) { string path = transform.name; while (transform.parent != null && transform.parent != root) { transform = transform.parent; path = $"{transform.name}/{path}"; } return path; } public static bool GenerateObjectField(ref T obj, string label = "") where T : Object { EditorGUI.BeginChangeCheck(); obj = EditorGUILayout.ObjectField(label, obj, typeof(T), true) as T; return EditorGUI.EndChangeCheck(); } private static void SmoothCurveTangets(ref AnimationCurve curve) { for (int i = 0; i < curve.keys.Length; i++) { AnimationUtility.SetKeyLeftTangentMode(curve, i, AnimationUtility.TangentMode.ClampedAuto); AnimationUtility.SetKeyRightTangentMode(curve, i, AnimationUtility.TangentMode.ClampedAuto); } } } public struct PoseFrame { public float time; public Pose pose; public PoseFrame(float time, Pose pose) { this.time = time; this.pose = pose; } } public class JointRecord { public HandJointId JointId { get; private set; } public string Path { get; private set; } private List _poseFrames = new List(); public List RotationX => FilterAngles(_poseFrames.Select(pf => new Keyframe(pf.time, pf.pose.rotation.eulerAngles.x)).ToList()); public List RotationY => FilterAngles(_poseFrames.Select(pf => new Keyframe(pf.time, pf.pose.rotation.eulerAngles.y)).ToList()); public List RotationZ => FilterAngles(_poseFrames.Select(pf => new Keyframe(pf.time, pf.pose.rotation.eulerAngles.z)).ToList()); public List PositionX => _poseFrames.Select(pf => new Keyframe(pf.time, pf.pose.position.x)).ToList(); public List PositionY => _poseFrames.Select(pf => new Keyframe(pf.time, pf.pose.position.y)).ToList(); public List PositionZ => _poseFrames.Select(pf => new Keyframe(pf.time, pf.pose.position.z)).ToList(); public JointRecord(HandJointId jointId, string path) { JointId = jointId; Path = path; } public void RecordPose(float time, Pose pose) { _poseFrames.Add(new PoseFrame(time, pose)); } private static List FilterAngles(List keyframes) { if (keyframes.Count < 1) { return keyframes; } List filtered = new List(); filtered.Add(keyframes[0]); for (int i = 1; i < keyframes.Count; i++) { float prevValue = filtered[filtered.Count - 1].value; float value = keyframes[i].value; while (Mathf.Abs(prevValue - value) > 180f) { value += 360f * Mathf.Sign(prevValue - value); } filtered.Add(new Keyframe(keyframes[i].time, value)); } return filtered; } } }