/** * Copyright(c) Live2D Inc. All rights reserved. * * Use of this source code is governed by the Live2D Open Software license * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. */ using System; using System.Collections.Generic; using Live2D.Cubism.Core; using Live2D.Cubism.Framework.MouthMovement; using Live2D.Cubism.Rendering; #if UNITY_EDITOR using UnityEditor; #endif using UnityEngine; namespace Live2D.Cubism.Framework.Json { /// /// Contains Cubism motion3.json data. /// [Serializable] // ReSharper disable once ClassCannotBeInstantiated public sealed class CubismMotion3Json { #region Load Methods /// /// Loads a motion3.json asset. /// /// motion3.json to deserialize. /// Deserialized motion3.json on success; otherwise. public static CubismMotion3Json LoadFrom(string motion3Json) { if (string.IsNullOrEmpty(motion3Json)) { return null; } var cubismMotion3Json = JsonUtility.FromJson(motion3Json); cubismMotion3Json.Meta.FadeInTime = -1.0f; cubismMotion3Json.Meta.FadeOutTime = -1.0f; for (var i = 0; i < cubismMotion3Json.Curves.Length; ++i) { cubismMotion3Json.Curves[i].FadeInTime = -1.0f; cubismMotion3Json.Curves[i].FadeOutTime = -1.0f; } JsonUtility.FromJsonOverwrite(motion3Json, cubismMotion3Json); return cubismMotion3Json; } /// /// Loads a motion3.json asset. /// /// motion3.json to deserialize. /// Deserialized motion3.json on success; otherwise. public static CubismMotion3Json LoadFrom(TextAsset motion3JsonAsset) { return (motion3JsonAsset == null) ? null : LoadFrom(motion3JsonAsset.text); } #endregion #region Json Data /// /// The model3.json format version. /// [SerializeField] public int Version; /// /// Motion meta info. /// [SerializeField] public SerializableMeta Meta; /// /// Curves. /// [SerializeField] public SerializableCurve[] Curves; /// /// User data. /// [SerializeField] public SerializableUserData[] UserData; #endregion #region Constructors /// /// Makes construction only possible through factories. /// private CubismMotion3Json() { } #endregion /// /// Converts motion curve segments into s. /// /// Data to convert. /// Keyframes. public static Keyframe[] ConvertCurveSegmentsToKeyframes(float[] segments) { // Return early on invalid input. if (segments.Length < 1) { return new Keyframe[0]; } // Initialize container for keyframes. var keyframes = new List { new Keyframe(segments[0], segments[1]) }; // Parse segments. for (var i = 2; i < segments.Length;) { Parsers[segments[i]](segments, keyframes, ref i); } // Return result. return keyframes.ToArray(); } /// /// Converts stepped curves to liner curves. /// /// Data to convert. /// Animation curve. public static AnimationCurve ConvertSteppedCurveToLinerCurver(CubismMotion3Json.SerializableCurve curve, float poseFadeInTime) { poseFadeInTime = (poseFadeInTime < 0) ? 0.5f : poseFadeInTime; var segments = curve.Segments; var segmentsCount = 2; for(var index = 2; index < curve.Segments.Length; index += 3) { // if current segment type is stepped and // next segment type is stepped or next segment is last segment // then convert segment type to liner. var currentSegmentTypeIsStepped = (curve.Segments[index] == 2); var currentSegmentIsLast = (index == (curve.Segments.Length - 3)); var nextSegmentTypeIsStepped = (currentSegmentIsLast) ? false : (curve.Segments[index + 3] == 2); var nextSegmentIsLast = (currentSegmentIsLast) ? false : ((index + 3) == (curve.Segments.Length - 3)); if ( currentSegmentTypeIsStepped && (nextSegmentTypeIsStepped || nextSegmentIsLast) ) { Array.Resize(ref segments, segments.Length + 3); segments[segmentsCount + 0] = 0; segments[segmentsCount + 1] = curve.Segments[index + 1]; segments[segmentsCount + 2] = curve.Segments[index - 1]; segments[segmentsCount + 3] = 0; segments[segmentsCount + 4] = curve.Segments[index + 1] + poseFadeInTime; segments[segmentsCount + 5] = curve.Segments[index + 2]; segmentsCount += 6; } else if(curve.Segments[index] == 1) { segments[segmentsCount + 0] = curve.Segments[index + 0]; segments[segmentsCount + 1] = curve.Segments[index + 1]; segments[segmentsCount + 2] = curve.Segments[index + 2]; segments[segmentsCount + 3] = curve.Segments[index + 3]; segments[segmentsCount + 4] = curve.Segments[index + 4]; segments[segmentsCount + 5] = curve.Segments[index + 5]; segments[segmentsCount + 6] = curve.Segments[index + 6]; index += 4; segmentsCount += 7; } else { segments[segmentsCount + 0] = curve.Segments[index + 0]; segments[segmentsCount + 1] = curve.Segments[index + 1]; segments[segmentsCount + 2] = curve.Segments[index + 2]; segmentsCount += 3; } } return new AnimationCurve(ConvertCurveSegmentsToKeyframes(segments)); } /// /// Instantiates an . /// /// Should import as original workflow. /// Should clear animation clip curves. /// Is function call form . /// pose3.json asset. /// The instantiated clip on success; otherwise. /// /// Note this method generates clips when called at runtime. /// public AnimationClip ToAnimationClip(bool shouldImportAsOriginalWorkflow = false, bool shouldClearAnimationCurves = false, bool isCallFormModelJson = false, CubismPose3Json poseJson = null) { // Check béziers restriction flag. if (!Meta.AreBeziersRestricted) { Debug.LogWarning("Béziers are not restricted and curves might be off. Please export motions from Cubism in restricted mode for perfect match."); } // Create animation clip. var animationClip = new AnimationClip { #if UNITY_EDITOR frameRate = Meta.Fps #else frameRate = Meta.Fps, legacy = true, wrapMode = (Meta.Loop) ? WrapMode.Loop : WrapMode.Default #endif }; return ToAnimationClip(animationClip, shouldImportAsOriginalWorkflow, shouldClearAnimationCurves, isCallFormModelJson, poseJson); } /// /// Instantiates an . /// /// Previous animation clip. /// Should import as original workflow. /// Should clear animation clip curves. /// Is function call form . /// pose3.json asset. /// The instantiated clip on success; otherwise. /// /// Note this method generates clips when called at runtime. /// public AnimationClip ToAnimationClip(AnimationClip animationClip, bool shouldImportAsOriginalWorkflow = false, bool shouldClearAnimationCurves = false , bool isCallFormModelJson = false, CubismPose3Json poseJson = null) { // Clear curves. if (shouldClearAnimationCurves && (!shouldImportAsOriginalWorkflow || (isCallFormModelJson && shouldImportAsOriginalWorkflow))) { animationClip.ClearCurves(); } // Convert curves. for (var i = 0; i < Curves.Length; ++i) { var curve = Curves[i]; // If should import as original workflow mode, skip add part opacity curve when call not from model3.json. if (curve.Target == "PartOpacity" && shouldImportAsOriginalWorkflow && !isCallFormModelJson) { continue; } var relativePath = string.Empty; var type = default(Type); var propertyName = string.Empty; var animationCurve = new AnimationCurve(ConvertCurveSegmentsToKeyframes(curve.Segments)); // Create model binding. if (curve.Target == "Model") { // Bind opacity. if (curve.Id == "Opacity") { relativePath = string.Empty; propertyName = "Opacity"; type = typeof(CubismRenderController); } // Bind eye-blink. else if (curve.Id == "EyeBlink") { relativePath = string.Empty; propertyName = "EyeOpening"; type = typeof(CubismEyeBlinkController); } // Bind lip-sync. else if (curve.Id == "LipSync") { relativePath = string.Empty; propertyName = "MouthOpening"; type = typeof(CubismMouthController); } } // Create parameter binding. else if (curve.Target == "Parameter") { relativePath = "Parameters/" + curve.Id; propertyName = "Value"; type = typeof(CubismParameter); } // Create part opacity binding. else if (curve.Target == "PartOpacity") { relativePath = "Parts/" + curve.Id; propertyName = "Opacity"; type = typeof(CubismPart); // original workflow. if (shouldImportAsOriginalWorkflow && poseJson != null && poseJson.FadeInTime != 0.0f) { animationCurve = ConvertSteppedCurveToLinerCurver(curve, poseJson.FadeInTime); } } #if UNITY_EDITOR var curveBinding = new EditorCurveBinding { path = relativePath, propertyName = propertyName, type = type }; AnimationUtility.SetEditorCurve(animationClip, curveBinding, animationCurve); #else animationClip.SetCurve(relativePath, type, propertyName, animationCurve); #endif } #if UNITY_EDITOR // Apply settings. var animationClipSettings = new AnimationClipSettings { loopTime = Meta.Loop, stopTime = Meta.Duration }; AnimationUtility.SetAnimationClipSettings(animationClip, animationClipSettings); #endif #if UNITY_EDITOR // Add animation events from user data. if (UserData != null) { var animationEvents = new List(); for (var i = 0; i < UserData.Length; ++i) { var animationEvent = new AnimationEvent { time = UserData[i].Time, stringParameter = UserData[i].Value, }; animationEvents.Add(animationEvent); } if (animationEvents.Count > 0) { AnimationUtility.SetAnimationEvents(animationClip, animationEvents.ToArray()); } } #endif return animationClip; } #region Segment Parsing /// /// Offset to use for setting of keyframes. /// private const float OffsetGranularity = 0.01f; /// /// Handles parsing of a single segment. /// /// Curve segments. /// Buffer to append result to. /// Offset of segment. private delegate void SegmentParser(float[] segments, List result, ref int position); /// /// Available segment parsers. /// // ReSharper disable once InconsistentNaming private static Dictionary Parsers = new Dictionary { {0f, ParseLinearSegment}, {1f, ParseBezierSegment}, {2f, ParseSteppedSegment}, {3f, ParseInverseSteppedSegment} }; /// /// Parses a linear segment. /// /// Curve segments. /// Buffer to append result to. /// Offset of segment. private static void ParseLinearSegment(float[] segments, List result, ref int position) { // Compute slope. var length = (segments[position + 1] - result[result.Count - 1].time); var slope = (segments[position + 2] - result[result.Count - 1].value) / length; // Determine tangents. var outTangent = slope; var inTangent = outTangent; // Create keyframes. var keyframe = new Keyframe( result[result.Count - 1].time, result[result.Count - 1].value, result[result.Count - 1].inTangent, outTangent); result[result.Count - 1] = keyframe; keyframe = new Keyframe( segments[position + 1], segments[position + 2], inTangent, 0); result.Add(keyframe); // Update position. position += 3; } /// /// Parses a bezier segment. /// /// Curve segments. /// Buffer to append result to. /// Offset of segment. private static void ParseBezierSegment(float[] segments, List result, ref int position) { // Compute tangents. var tangentLength = Mathf.Abs(result[result.Count - 1].time - segments[position + 5]) * 0.333333f; var outTangent = (segments[position + 2] - result[result.Count - 1].value) / tangentLength; var inTangent = (segments[position + 6] - segments[position + 4]) / tangentLength; // Create keyframes. var keyframe = new Keyframe( result[result.Count - 1].time, result[result.Count - 1].value, result[result.Count - 1].inTangent, outTangent); result[result.Count - 1] = keyframe; keyframe = new Keyframe( segments[position + 5], segments[position + 6], inTangent, 0); result.Add(keyframe); // Update position. position += 7; } /// /// Parses a stepped segment. /// /// Curve segments. /// Buffer to append result to. /// Offset of segment. private static void ParseSteppedSegment(float[] segments, List result, ref int position) { // Create keyframe. result.Add( new Keyframe(segments[position + 1], segments[position + 2]) { inTangent = float.PositiveInfinity }); // Update position. position += 3; } /// /// Parses a inverse-stepped segment. /// /// Curve segments. /// Buffer to append result to. /// Offset of segment. private static void ParseInverseSteppedSegment(float[] segments, List result, ref int position) { // Compute tangents. var keyframe = result[result.Count - 1]; var tangent = (float)Math.Atan2( (segments[position + 2] - keyframe.value), (segments[position + 1] - keyframe.time)); keyframe.outTangent = tangent; result[result.Count - 1] = keyframe; result.Add( new Keyframe(keyframe.time + OffsetGranularity, segments[position + 2]) { inTangent = tangent, outTangent = 0 }); result.Add( new Keyframe(segments[position + 1], segments[position + 2]) { inTangent = 0 }); // Update position. position += 3; } #endregion #region Json Object Types /// /// Motion meta info. /// [Serializable] public struct SerializableMeta { /// /// Duration in seconds. /// [SerializeField] public float Duration; /// /// Framerate in seconds. /// [SerializeField] public float Fps; /// /// True if motion is looping. /// [SerializeField] public bool Loop; /// /// Number of curves. /// [SerializeField] public int CurveCount; /// /// Total number of curve segments. /// [SerializeField] public int TotalSegmentCount; /// /// Total number of curve points. /// [SerializeField] public int TotalPointCount; /// /// True if beziers are restricted. /// [SerializeField] public bool AreBeziersRestricted; /// /// Total number of UserData. /// [SerializeField] public int UserDataCount; /// /// Total size of UserData in bytes. /// [SerializeField] public int TotalUserDataSize; /// /// [Optional] Time of the Fade-In for easing in seconds. /// [SerializeField] public float FadeInTime; /// /// [Optional] Time of the Fade-Out for easing in seconds. /// [SerializeField] public float FadeOutTime; }; /// /// Single motion curve. /// [Serializable] public struct SerializableCurve { /// /// Target type. /// [SerializeField] public string Target; /// /// Id within target. /// [SerializeField] public string Id; /// /// Flattened curve segments. /// [SerializeField] public float[] Segments; /// /// [Optional] Time of the overall Fade-In for easing in seconds. /// [SerializeField] public float FadeInTime; /// /// [Optional] Time of the overall Fade-Out for easing in seconds. /// [SerializeField] public float FadeOutTime; }; /// /// User data. /// [Serializable] public struct SerializableUserData { /// /// Time in seconds. /// [SerializeField] public float Time; /// /// Content of user data. /// [SerializeField] public string Value; } #endregion } }