/**
* 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
}
}