GLTFAnimation.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using Newtonsoft.Json;
  5. using Siccity.GLTFUtility.Converters;
  6. using UnityEngine;
  7. using UnityEngine.Scripting;
  8. using Newtonsoft.Json.Linq;
  9. namespace Siccity.GLTFUtility {
  10. // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#animation
  11. /// <summary> Contains info for a single animation clip </summary>
  12. [Preserve] public class GLTFAnimation {
  13. /// <summary> Connects the output values of the key frame animation to a specific node in the hierarchy </summary>
  14. [JsonProperty(Required = Required.Always)] public Channel[] channels;
  15. [JsonProperty(Required = Required.Always)] public Sampler[] samplers;
  16. public string name;
  17. public JObject extras;
  18. #region Classes
  19. // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#animation-sampler
  20. [Preserve] public class Sampler {
  21. /// <summary> The index of an accessor containing keyframe input values, e.g., time. </summary>
  22. [JsonProperty(Required = Required.Always)] public int input;
  23. /// <summary> The index of an accessor containing keyframe output values. </summary>
  24. [JsonProperty(Required = Required.Always)] public int output;
  25. /// <summary> Valid names include: "LINEAR", "STEP", "CUBICSPLINE" </summary>
  26. [JsonConverter(typeof(EnumConverter))] public InterpolationMode interpolation = InterpolationMode.LINEAR;
  27. }
  28. // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#channel
  29. /// <summary> Connects the output values of the key frame animation to a specific node in the hierarchy </summary>
  30. [Preserve] public class Channel {
  31. /// <summary> Target sampler index </summary>
  32. [JsonProperty(Required = Required.Always)] public int sampler;
  33. /// <summary> Target sampler index </summary>
  34. [JsonProperty(Required = Required.Always)] public Target target;
  35. }
  36. // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#target
  37. /// <summary> Identifies which node and property to animate </summary>
  38. [Preserve] public class Target {
  39. /// <summary> Target node index.</summary>
  40. public int? node;
  41. /// <summary> Which property to animate. Valid names are: "translation", "rotation", "scale", "weights" </summary>
  42. [JsonProperty(Required = Required.Always)] public string path;
  43. }
  44. [Preserve] public class ImportResult {
  45. public AnimationClip clip;
  46. }
  47. #endregion
  48. public ImportResult Import(GLTFAccessor.ImportResult[] accessors, GLTFNode.ImportResult[] nodes, ImportSettings importSettings) {
  49. bool multiRoots = nodes.Where(x => x.IsRoot).Count() > 1;
  50. ImportResult result = new ImportResult();
  51. result.clip = new AnimationClip();
  52. result.clip.name = name;
  53. result.clip.frameRate = importSettings.animationSettings.frameRate;
  54. result.clip.legacy = importSettings.animationSettings.useLegacyClips;
  55. if (result.clip.legacy && importSettings.animationSettings.looping)
  56. {
  57. result.clip.wrapMode = WrapMode.Loop;
  58. }
  59. for (int i = 0; i < channels.Length; i++) {
  60. Channel channel = channels[i];
  61. if (samplers.Length <= channel.sampler) {
  62. Debug.LogWarning($"GLTFUtility: Animation channel points to sampler at index {channel.sampler} which doesn't exist. Skipping animation clip.");
  63. continue;
  64. }
  65. Sampler sampler = samplers[channel.sampler];
  66. // Get interpolation mode
  67. InterpolationMode interpolationMode = importSettings.animationSettings.interpolationMode;
  68. if (interpolationMode == InterpolationMode.ImportFromFile) {
  69. interpolationMode = sampler.interpolation;
  70. }
  71. if (interpolationMode == InterpolationMode.CUBICSPLINE) Debug.LogWarning("Animation interpolation mode CUBICSPLINE not fully supported, result might look different.");
  72. string relativePath = "";
  73. GLTFNode.ImportResult node = nodes[channel.target.node.Value];
  74. while (node != null && !node.IsRoot) {
  75. if (string.IsNullOrEmpty(relativePath)) relativePath = node.transform.name;
  76. else relativePath = node.transform.name + "/" + relativePath;
  77. if (node.parent.HasValue) node = nodes[node.parent.Value];
  78. else node = null;
  79. }
  80. // If file has multiple root nodes, a new parent will be created for them as a final step of the import process. This parent fucks up the curve relative paths.
  81. // Add node.transform.name to path if there are multiple roots. This is not the most elegant fix but it works.
  82. // See GLTFNodeExtensions.GetRoot
  83. if (multiRoots) relativePath = node.transform.name + "/" + relativePath;
  84. System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.InvariantCulture;
  85. float[] keyframeInput = accessors[sampler.input].ReadFloat().ToArray();
  86. switch (channel.target.path) {
  87. case "translation":
  88. Vector3[] pos = accessors[sampler.output].ReadVec3().ToArray();
  89. AnimationCurve posX = new AnimationCurve();
  90. AnimationCurve posY = new AnimationCurve();
  91. AnimationCurve posZ = new AnimationCurve();
  92. for (int k = 0; k < keyframeInput.Length; k++) {
  93. posX.AddKey(CreateKeyframe(k, keyframeInput, pos, x => -x.x, interpolationMode));
  94. posY.AddKey(CreateKeyframe(k, keyframeInput, pos, x => x.y, interpolationMode));
  95. posZ.AddKey(CreateKeyframe(k, keyframeInput, pos, x => x.z, interpolationMode));
  96. }
  97. result.clip.SetCurve(relativePath, typeof(Transform), "localPosition.x", posX);
  98. result.clip.SetCurve(relativePath, typeof(Transform), "localPosition.y", posY);
  99. result.clip.SetCurve(relativePath, typeof(Transform), "localPosition.z", posZ);
  100. break;
  101. case "rotation":
  102. Vector4[] rot = accessors[sampler.output].ReadVec4().ToArray();
  103. AnimationCurve rotX = new AnimationCurve();
  104. AnimationCurve rotY = new AnimationCurve();
  105. AnimationCurve rotZ = new AnimationCurve();
  106. AnimationCurve rotW = new AnimationCurve();
  107. for (int k = 0; k < keyframeInput.Length; k++) {
  108. // The Animation window in Unity shows keyframes incorrectly converted to euler. This is only to deceive you. The quaternions underneath work correctly
  109. rotX.AddKey(CreateKeyframe(k, keyframeInput, rot, x => x.x, interpolationMode));
  110. rotY.AddKey(CreateKeyframe(k, keyframeInput, rot, x => -x.y, interpolationMode));
  111. rotZ.AddKey(CreateKeyframe(k, keyframeInput, rot, x => -x.z, interpolationMode));
  112. rotW.AddKey(CreateKeyframe(k, keyframeInput, rot, x => x.w, interpolationMode));
  113. }
  114. result.clip.SetCurve(relativePath, typeof(Transform), "localRotation.x", rotX);
  115. result.clip.SetCurve(relativePath, typeof(Transform), "localRotation.y", rotY);
  116. result.clip.SetCurve(relativePath, typeof(Transform), "localRotation.z", rotZ);
  117. result.clip.SetCurve(relativePath, typeof(Transform), "localRotation.w", rotW);
  118. break;
  119. case "scale":
  120. Vector3[] scale = accessors[sampler.output].ReadVec3().ToArray();
  121. AnimationCurve scaleX = new AnimationCurve();
  122. AnimationCurve scaleY = new AnimationCurve();
  123. AnimationCurve scaleZ = new AnimationCurve();
  124. for (int k = 0; k < keyframeInput.Length; k++) {
  125. scaleX.AddKey(CreateKeyframe(k, keyframeInput, scale, x => x.x, interpolationMode));
  126. scaleY.AddKey(CreateKeyframe(k, keyframeInput, scale, x => x.y, interpolationMode));
  127. scaleZ.AddKey(CreateKeyframe(k, keyframeInput, scale, x => x.z, interpolationMode));
  128. }
  129. result.clip.SetCurve(relativePath, typeof(Transform), "localScale.x", scaleX);
  130. result.clip.SetCurve(relativePath, typeof(Transform), "localScale.y", scaleY);
  131. result.clip.SetCurve(relativePath, typeof(Transform), "localScale.z", scaleZ);
  132. break;
  133. case "weights":
  134. GLTFNode.ImportResult skinnedMeshNode = nodes[channel.target.node.Value];
  135. SkinnedMeshRenderer skinnedMeshRenderer = skinnedMeshNode.transform.GetComponent<SkinnedMeshRenderer>();
  136. int numberOfBlendShapes = skinnedMeshRenderer.sharedMesh.blendShapeCount;
  137. AnimationCurve[] blendShapeCurves = new AnimationCurve[numberOfBlendShapes];
  138. for(int j = 0; j < numberOfBlendShapes; ++j) {
  139. blendShapeCurves[j] = new AnimationCurve();
  140. }
  141. float[] weights = accessors[sampler.output].ReadFloat().ToArray();
  142. float[] weightValues = new float[keyframeInput.Length];
  143. float[] previouslyKeyedValues = new float[numberOfBlendShapes];
  144. // Reference for my future self:
  145. // keyframeInput.Length = number of keyframes
  146. // keyframeInput[ k ] = timestamp of keyframe
  147. // weights.Length = number of keyframes * number of blendshapes
  148. // weights[ j ] = actual animated weight of a specific blend shape
  149. // (index into weights[] array accounts for keyframe index and blend shape index)
  150. for(int k = 0; k < keyframeInput.Length; ++k) {
  151. for(int j = 0; j < numberOfBlendShapes; ++j) {
  152. int weightIndex = (k * numberOfBlendShapes) + j;
  153. weightValues[k] = weights[weightIndex];
  154. bool addKey = true;
  155. if(importSettings.animationSettings.compressBlendShapeKeyFrames) {
  156. if(k == 0 || !Mathf.Approximately(weightValues[k], previouslyKeyedValues[j])) {
  157. if(k > 0) {
  158. weightValues[k-1] = previouslyKeyedValues[j];
  159. blendShapeCurves[j].AddKey(CreateKeyframe(k-1, keyframeInput, weightValues, x => x, interpolationMode));
  160. }
  161. addKey = true;
  162. previouslyKeyedValues[j] = weightValues[k];
  163. } else {
  164. addKey = false;
  165. }
  166. }
  167. if(addKey) {
  168. blendShapeCurves[j].AddKey(CreateKeyframe(k, keyframeInput, weightValues, x => x, interpolationMode));
  169. }
  170. }
  171. }
  172. for(int j = 0; j < numberOfBlendShapes; ++j) {
  173. string propertyName = "blendShape." + skinnedMeshRenderer.sharedMesh.GetBlendShapeName(j);
  174. result.clip.SetCurve(relativePath, typeof(SkinnedMeshRenderer), propertyName, blendShapeCurves[j]);
  175. }
  176. break;
  177. }
  178. }
  179. return result;
  180. }
  181. public static Keyframe CreateKeyframe<T>(int index, float[] timeArray, T[] valueArray, Func<T, float> getValue, InterpolationMode interpolationMode) {
  182. float time = timeArray[index];
  183. Keyframe keyframe;
  184. #pragma warning disable CS0618
  185. if (interpolationMode == InterpolationMode.STEP) {
  186. keyframe = new Keyframe(time, getValue(valueArray[index]), float.PositiveInfinity, float.PositiveInfinity, 1, 1);
  187. } else if (interpolationMode == InterpolationMode.CUBICSPLINE) {
  188. // @TODO: Find out what the right math is to calculate the tangent/weight values.
  189. float inTangent = getValue(valueArray[index * 3]);
  190. float outTangent = getValue(valueArray[(index * 3) + 2]);
  191. keyframe = new Keyframe(time, getValue(valueArray[(index * 3) + 1]), inTangent, outTangent, 1, 1);
  192. } else { // LINEAR
  193. keyframe = new Keyframe(time, getValue(valueArray[index]), 0, 0, 0, 0);
  194. }
  195. #pragma warning restore CS0618
  196. return keyframe;
  197. }
  198. }
  199. public static class GLTFAnimationExtensions {
  200. public static GLTFAnimation.ImportResult[] Import(this List<GLTFAnimation> animations, GLTFAccessor.ImportResult[] accessors, GLTFNode.ImportResult[] nodes, ImportSettings importSettings) {
  201. if (animations == null) return null;
  202. GLTFAnimation.ImportResult[] results = new GLTFAnimation.ImportResult[animations.Count];
  203. for (int i = 0; i < results.Length; i++) {
  204. results[i] = animations[i].Import(accessors, nodes, importSettings);
  205. if (string.IsNullOrEmpty(results[i].clip.name)) results[i].clip.name = "animation" + i;
  206. }
  207. return results;
  208. }
  209. }
  210. }