using System; using System.Linq; using System.Collections.Generic; using System.Text.RegularExpressions; using SevenZip; namespace AssetStudio { public class AnimationClipConverter { public static readonly Regex UnknownPathRegex = new Regex($@"^{UnknownPathPrefix}[0-9]{{1,10}}$", RegexOptions.Compiled); private const string UnknownPathPrefix = "path_"; private const string MissedPropertyPrefix = "missed_"; private const string ScriptPropertyPrefix = "script_"; private const string TypeTreePropertyPrefix = "typetree_"; private readonly Game game; private readonly AnimationClip animationClip; private readonly CustomCurveResolver m_customCurveResolver; private readonly Dictionary>> m_translations = new Dictionary>>(); private readonly Dictionary>> m_rotations = new Dictionary>>(); private readonly Dictionary>> m_scales = new Dictionary>>(); private readonly Dictionary>> m_eulers = new Dictionary>>(); private readonly Dictionary>> m_floats = new Dictionary>>(); private readonly Dictionary> m_pptrs = new Dictionary>(); public Vector3Curve[] Translations { get; private set; } public QuaternionCurve[] Rotations { get; private set; } public Vector3Curve[] Scales { get; private set; } public Vector3Curve[] Eulers { get; private set; } public FloatCurve[] Floats { get; private set; } public PPtrCurve[] PPtrs { get; private set; } public AnimationClipConverter(AnimationClip clip) { game = clip.assetsFile.game; animationClip = clip; m_customCurveResolver = new CustomCurveResolver(animationClip); } public static AnimationClipConverter Process(AnimationClip clip) { var converter = new AnimationClipConverter(clip); converter.ProcessInner(); return converter; } private void ProcessInner() { var m_Clip = animationClip.m_MuscleClip.m_Clip; var bindings = animationClip.m_ClipBindingConstant; var tos = animationClip.FindTOS(); var streamedFrames = m_Clip.m_StreamedClip.ReadData(); var lastDenseFrame = m_Clip.m_DenseClip.m_FrameCount / m_Clip.m_DenseClip.m_SampleRate; var lastSampleFrame = streamedFrames.Count > 1 ? streamedFrames[streamedFrames.Count - 2].time : 0.0f; var lastFrame = Math.Max(lastDenseFrame, lastSampleFrame); if (!m_Clip.m_ACLClip.m_ClipData.IsNullOrEmpty() && !game.Type.IsSRGroup()) { var lastACLFrame = ProcessACLClip(m_Clip, bindings, tos); lastFrame = Math.Max(lastFrame, lastACLFrame); animationClip.m_Compressed = false; } ProcessStreams(streamedFrames, bindings, tos, m_Clip.m_DenseClip.m_SampleRate); ProcessDenses(m_Clip, bindings, tos); if (!m_Clip.m_ACLClip.m_ClipData.IsNullOrEmpty() && game.Type.IsSRGroup()) { var lastACLFrame = ProcessACLClip(m_Clip, bindings, tos); lastFrame = Math.Max(lastFrame, lastACLFrame); animationClip.m_Compressed = false; } if (m_Clip.m_ConstantClip != null) { ProcessConstant(m_Clip, bindings, tos, lastFrame); } CreateCurves(); } private void CreateCurves() { m_translations.AsEnumerable().ToList().ForEach(x => x.Key.curve.m_Curve.AddRange(x.Value)); Translations = m_translations.Keys.ToArray(); m_rotations.AsEnumerable().ToList().ForEach(x => x.Key.curve.m_Curve.AddRange(x.Value)); Rotations = m_rotations.Keys.ToArray(); m_scales.AsEnumerable().ToList().ForEach(x => x.Key.curve.m_Curve.AddRange(x.Value)); Scales = m_scales.Keys.ToArray(); m_eulers.AsEnumerable().ToList().ForEach(x => x.Key.curve.m_Curve.AddRange(x.Value)); Eulers = m_eulers.Keys.ToArray(); m_floats.AsEnumerable().ToList().ForEach(x => x.Key.curve.m_Curve.AddRange(x.Value)); Floats = m_floats.Keys.ToArray(); m_pptrs.AsEnumerable().ToList().ForEach(x => x.Key.curve.AddRange(x.Value)); PPtrs = m_pptrs.Keys.ToArray(); } private void ProcessStreams(List streamFrames, AnimationClipBindingConstant bindings, Dictionary tos, float sampleRate) { var curveValues = new float[4]; var inSlopeValues = new float[4]; var outSlopeValues = new float[4]; var interval = 1.0f / sampleRate; // first (index [0]) stream frame is for slope calculation for the first real frame (index [1]) // last one (index [count - 1]) is +Infinity // it is made for slope processing, but we don't need them for (var frameIndex = 1; frameIndex < streamFrames.Count - 1; frameIndex++) { var frame = streamFrames[frameIndex]; for (var curveIndex = 0; curveIndex < frame.keyList.Length;) { var curve = frame.keyList[curveIndex]; var index = curve.index; if (!game.Type.IsSRGroup()) index += (int)animationClip.m_MuscleClip.m_Clip.m_ACLClip.m_CurveCount; var binding = bindings.FindBinding(index); var path = GetCurvePath(tos, binding.path); if (binding.typeID == ClassIDType.Transform) { GetPreviousFrame(streamFrames, curve.index, frameIndex, out var prevFrameIndex, out var prevCurveIndex); var dimension = binding.GetDimension(); for (int key = 0; key < dimension; key++) { var keyCurve = frame.keyList[curveIndex]; var prevFrame = streamFrames[prevFrameIndex]; var prevKeyCurve = prevFrame.keyList[prevCurveIndex + key]; var deltaTime = frame.time - prevFrame.time; curveValues[key] = keyCurve.value; inSlopeValues[key] = prevKeyCurve.CalculateNextInSlope(deltaTime, keyCurve); outSlopeValues[key] = keyCurve.outSlope; curveIndex = GetNextCurve(frame, curveIndex); } AddTransformCurve(frame.time, binding.attribute, curveValues, inSlopeValues, outSlopeValues, 0, path); } else if ((BindingCustomType)binding.customType == BindingCustomType.None) { AddDefaultCurve(binding, path, frame.time, frame.keyList[curveIndex].value); curveIndex = GetNextCurve(frame, curveIndex); } else { AddCustomCurve(bindings, binding, path, frame.time, frame.keyList[curveIndex].value); curveIndex = GetNextCurve(frame, curveIndex); } } } } private void ProcessDenses(Clip clip, AnimationClipBindingConstant bindings, Dictionary tos) { var dense = clip.m_DenseClip; var streamCount = clip.m_StreamedClip.curveCount; var slopeValues = new float[4]; // no slopes - 0 values for (var frameIndex = 0; frameIndex < dense.m_FrameCount; frameIndex++) { var time = frameIndex / dense.m_SampleRate; var frameOffset = frameIndex * (int)dense.m_CurveCount; for (var curveIndex = 0; curveIndex < dense.m_CurveCount;) { var index = (int)streamCount + curveIndex; if (!game.Type.IsSRGroup()) index += (int)clip.m_ACLClip.m_CurveCount; var binding = bindings.FindBinding(index); var path = GetCurvePath(tos, binding.path); var framePosition = frameOffset + curveIndex; if (binding.typeID == ClassIDType.Transform) { AddTransformCurve(time, binding.attribute, dense.m_SampleArray, slopeValues, slopeValues, framePosition, path); curveIndex += binding.GetDimension(); } else if ((BindingCustomType)binding.customType == BindingCustomType.None) { AddDefaultCurve(binding, path, time, dense.m_SampleArray[framePosition]); curveIndex++; } else { AddCustomCurve(bindings, binding, path, time, dense.m_SampleArray[framePosition]); curveIndex++; } } } } private float ProcessACLClip(Clip clip, AnimationClipBindingConstant bindings, Dictionary tos) { var acl = clip.m_ACLClip; acl.Process(game, out var values, out var times); float[] slopeValues = new float[4]; // no slopes - 0 values int frameCount = times.Length; for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) { float time = times[frameIndex]; int frameOffset = frameIndex * (int)acl.m_CurveCount; for (int curveIndex = 0; curveIndex < acl.m_CurveCount;) { var index = curveIndex; if (game.Type.IsSRGroup()) index += (int)(clip.m_DenseClip.m_CurveCount + clip.m_StreamedClip.curveCount); GenericBinding binding = bindings.FindBinding(index); string path = GetCurvePath(tos, binding.path); int framePosition = frameOffset + curveIndex; if (binding.typeID == ClassIDType.Transform) { AddTransformCurve(time, binding.attribute, values, slopeValues, slopeValues, framePosition, path); curveIndex += binding.GetDimension(); } else if ((BindingCustomType)binding.customType == BindingCustomType.None) { AddDefaultCurve(binding, path, time, values[framePosition]); curveIndex++; } else { AddCustomCurve(bindings, binding, path, time, values[framePosition]); curveIndex++; } } } return times[frameCount - 1]; } private void ProcessConstant(Clip clip, AnimationClipBindingConstant bindings, Dictionary tos, float lastFrame) { var constant = clip.m_ConstantClip; var streamCount = clip.m_StreamedClip.curveCount; var denseCount = clip.m_DenseClip.m_CurveCount; var slopeValues = new float[4]; // no slopes - 0 values // only first and last frames var time = 0.0f; for (var i = 0; i < 2; i++, time += lastFrame) { for (var curveIndex = 0; curveIndex < constant.data.Length;) { var index = (int)(streamCount + denseCount + curveIndex); if (!clip.m_ACLClip.m_ClipData.IsNullOrEmpty()) index += (int)clip.m_ACLClip.m_CurveCount; GenericBinding binding = bindings.FindBinding(index); string path = GetCurvePath(tos, binding.path); if (binding.typeID == ClassIDType.Transform) { AddTransformCurve(time, binding.attribute, constant.data, slopeValues, slopeValues, curveIndex, path); curveIndex += binding.GetDimension(); } else if ((BindingCustomType)binding.customType == BindingCustomType.None) { AddDefaultCurve(binding, path, time, constant.data[curveIndex]); curveIndex++; } else { AddCustomCurve(bindings, binding, path, time, constant.data[curveIndex]); curveIndex++; } } } } private void AddCustomCurve(AnimationClipBindingConstant bindings, GenericBinding binding, string path, float time, float value) { switch ((BindingCustomType)binding.customType) { case BindingCustomType.AnimatorMuscle: AddAnimatorMuscleCurve(binding, time, value); break; default: string attribute = m_customCurveResolver.ToAttributeName((BindingCustomType)binding.customType, binding.attribute, path); if (binding.isPPtrCurve == 0x01) { PPtrCurve curve = new PPtrCurve(path, attribute, binding.typeID, binding.script.Cast()); AddPPtrKeyframe(curve, bindings, time, (int)value); } else { FloatCurve curve = new FloatCurve(path, attribute, binding.typeID, binding.script.Cast()); AddFloatKeyframe(curve, time, value); } break; } } private void AddTransformCurve(float time, uint transType, float[] curveValues, float[] inSlopeValues, float[] outSlopeValues, int offset, string path) { switch (transType) { case 1: { var curve = new Vector3Curve(path); if (!m_translations.TryGetValue(curve, out List> transCurve)) { transCurve = new List>(); m_translations.Add(curve, transCurve); } float x = curveValues[offset + 0]; float y = curveValues[offset + 1]; float z = curveValues[offset + 2]; float inX = inSlopeValues[0]; float inY = inSlopeValues[1]; float inZ = inSlopeValues[2]; float outX = outSlopeValues[0]; float outY = outSlopeValues[1]; float outZ = outSlopeValues[2]; Vector3 value = new Vector3(x, y, z); Vector3 inSlope = new Vector3(inX, inY, inZ); Vector3 outSlope = new Vector3(outX, outY, outZ); Keyframe transKey = new Keyframe(time, value, inSlope, outSlope, AnimationClipExtensions.DefaultVector3Weight); transCurve.Add(transKey); } break; case 2: { var curve = new QuaternionCurve(path); if (!m_rotations.TryGetValue(curve, out List> rotCurve)) { rotCurve = new List>(); m_rotations.Add(curve, rotCurve); } float x = curveValues[offset + 0]; float y = curveValues[offset + 1]; float z = curveValues[offset + 2]; float w = curveValues[offset + 3]; float inX = inSlopeValues[0]; float inY = inSlopeValues[1]; float inZ = inSlopeValues[2]; float inW = inSlopeValues[3]; float outX = outSlopeValues[0]; float outY = outSlopeValues[1]; float outZ = outSlopeValues[2]; float outW = outSlopeValues[3]; Quaternion value = new Quaternion(x, y, z, w); Quaternion inSlope = new Quaternion(inX, inY, inZ, inW); Quaternion outSlope = new Quaternion(outX, outY, outZ, outW); Keyframe rotKey = new Keyframe(time, value, inSlope, outSlope, AnimationClipExtensions.DefaultQuaternionWeight); rotCurve.Add(rotKey); } break; case 3: { var curve = new Vector3Curve(path); if (!m_scales.TryGetValue(curve, out List> scaleCurve)) { scaleCurve = new List>(); m_scales.Add(curve, scaleCurve); } float x = curveValues[offset + 0]; float y = curveValues[offset + 1]; float z = curveValues[offset + 2]; float inX = inSlopeValues[0]; float inY = inSlopeValues[1]; float inZ = inSlopeValues[2]; float outX = outSlopeValues[0]; float outY = outSlopeValues[1]; float outZ = outSlopeValues[2]; Vector3 value = new Vector3(x, y, z); Vector3 inSlope = new Vector3(inX, inY, inZ); Vector3 outSlope = new Vector3(outX, outY, outZ); Keyframe scaleKey = new Keyframe(time, value, inSlope, outSlope, AnimationClipExtensions.DefaultVector3Weight); scaleCurve.Add(scaleKey); } break; case 4: { var curve = new Vector3Curve(path); if (!m_eulers.TryGetValue(curve, out List> eulerCurve)) { eulerCurve = new List>(); m_eulers.Add(curve, eulerCurve); } float x = curveValues[offset + 0]; float y = curveValues[offset + 1]; float z = curveValues[offset + 2]; float inX = inSlopeValues[0]; float inY = inSlopeValues[1]; float inZ = inSlopeValues[2]; float outX = outSlopeValues[0]; float outY = outSlopeValues[1]; float outZ = outSlopeValues[2]; Vector3 value = new Vector3(x, y, z); Vector3 inSlope = new Vector3(inX, inY, inZ); Vector3 outSlope = new Vector3(outX, outY, outZ); Keyframe eulerKey = new Keyframe(time, value, inSlope, outSlope, AnimationClipExtensions.DefaultVector3Weight); eulerCurve.Add(eulerKey); } break; default: throw new NotImplementedException(transType.ToString()); } } private void AddDefaultCurve(GenericBinding binding, string path, float time, float value) { switch (binding.typeID) { case ClassIDType.GameObject: { AddGameObjectCurve(binding, path, time, value); } break; case ClassIDType.MonoBehaviour: { AddScriptCurve(binding, path, time, value); } break; default: AddEngineCurve(binding, path, time, value); break; } } private void AddGameObjectCurve(GenericBinding binding, string path, float time, float value) { if (binding.attribute == CRC.CalculateDigestAscii("m_IsActive")) { FloatCurve curve = new FloatCurve(path, "m_IsActive", ClassIDType.GameObject, new PPtr(0, 0, null)); AddFloatKeyframe(curve, time, value); return; } else { // that means that dev exported animation clip with missing component FloatCurve curve = new FloatCurve(path, MissedPropertyPrefix + binding.attribute, ClassIDType.GameObject, new PPtr(0, 0, null)); AddFloatKeyframe(curve, time, value); } } private void AddScriptCurve(GenericBinding binding, string path, float time, float value) { #warning TODO: FloatCurve curve = new FloatCurve(path, ScriptPropertyPrefix + binding.attribute, ClassIDType.MonoBehaviour, binding.script.Cast()); AddFloatKeyframe(curve, time, value); } private void AddEngineCurve(GenericBinding binding, string path, float time, float value) { #warning TODO: FloatCurve curve = new FloatCurve(path, TypeTreePropertyPrefix + binding.attribute, binding.typeID, new PPtr(0, 0, null)); AddFloatKeyframe(curve, time, value); } private void AddAnimatorMuscleCurve(GenericBinding binding, float time, float value) { FloatCurve curve = new FloatCurve(string.Empty, binding.GetHumanoidMuscle().ToAttributeString(), ClassIDType.Animator, new PPtr(0, 0, null)); AddFloatKeyframe(curve, time, value); } private void AddFloatKeyframe(FloatCurve curve, float time, float value) { if (!m_floats.TryGetValue(curve, out List> floatCurve)) { floatCurve = new List>(); m_floats.Add(curve, floatCurve); } Keyframe floatKey = new Keyframe(time, value, default, default, AnimationClipExtensions.DefaultFloatWeight); floatCurve.Add(floatKey); } private void AddPPtrKeyframe(PPtrCurve curve, AnimationClipBindingConstant bindings, float time, int index) { if (!m_pptrs.TryGetValue(curve, out List pptrCurve)) { pptrCurve = new List(); m_pptrs.Add(curve, pptrCurve); AddPPtrKeyframe(curve, bindings, 0.0f, index - 1); } PPtr value = bindings.pptrCurveMapping[index]; PPtrKeyframe pptrKey = new PPtrKeyframe(time, value); pptrCurve.Add(pptrKey); } private void GetPreviousFrame(List streamFrames, int curveID, int currentFrame, out int frameIndex, out int curveIndex) { for (frameIndex = currentFrame - 1; frameIndex >= 0; frameIndex--) { var frame = streamFrames[frameIndex]; for (curveIndex = 0; curveIndex < frame.keyList.Length; curveIndex++) { var curve = frame.keyList[curveIndex]; if (curve.index == curveID) { return; } } } throw new Exception($"There is no curve with index {curveID} in any of previous frames"); } private int GetNextCurve(StreamedClip.StreamedFrame frame, int currentCurve) { var curve = frame.keyList[currentCurve]; int i = currentCurve + 1; for (; i < frame.keyList.Length; i++) { if (frame.keyList[i].index != curve.index) { return i; } } return i; } private static string GetCurvePath(Dictionary tos, uint hash) { if (tos.TryGetValue(hash, out string path)) { return path; } else { return UnknownPathPrefix + hash; } } } }