Add support for Live2D Fade motions

This commit is contained in:
VaDiM
2023-11-19 02:52:55 +03:00
parent e415740373
commit d572bd0e64
13 changed files with 547 additions and 161 deletions

View File

@ -0,0 +1,30 @@
using System;
namespace CubismLive2DExtractor
{
public class AnimationCurve
{
public CubismKeyframeData[] m_Curve { get; set; }
public int m_PreInfinity { get; set; }
public int m_PostInfinity { get; set; }
public int m_RotationOrder { get; set; }
}
public class CubismFadeMotion
{
public string m_Name { get; set; }
public string MotionName { get; set; }
public float FadeInTime { get; set; }
public float FadeOutTime { get; set; }
public string[] ParameterIds { get; set; }
public AnimationCurve[] ParameterCurves { get; set; }
public float[] ParameterFadeInTimes { get; set; }
public float[] ParameterFadeOutTimes { get; set; }
public float MotionLength { get; set; }
public CubismFadeMotion()
{
ParameterIds = Array.Empty<string>();
}
}
}

View File

@ -0,0 +1,26 @@
namespace CubismLive2DExtractor
{
public class CubismKeyframeData
{
public float time { get; set; }
public float value { get; set; }
public float inSlope { get; set; }
public float outSlope { get; set; }
public int weightedMode { get; set; }
public float inWeight { get; set; }
public float outWeight { get; set; }
public CubismKeyframeData() { }
public CubismKeyframeData(ImportedKeyframe<float> keyframe)
{
time = keyframe.time;
value = keyframe.value;
inSlope = keyframe.inSlope;
outSlope = keyframe.outSlope;
weightedMode = 0;
inWeight = 0;
outWeight = 0;
}
}
}

View File

@ -1,7 +1,9 @@
using System;
// File Format Specifications
// https://github.com/Live2D/CubismSpecs/blob/master/FileFormats/motion3.json.md
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace CubismLive2DExtractor
{
@ -18,24 +20,213 @@ namespace CubismLive2DExtractor
public float Fps;
public bool Loop;
public bool AreBeziersRestricted;
public float FadeInTime;
public float FadeOutTime;
public int CurveCount;
public int TotalSegmentCount;
public int TotalPointCount;
public int UserDataCount;
public int TotalUserDataSize;
};
}
public class SerializableCurve
{
public string Target;
public string Id;
public float FadeInTime;
public float FadeOutTime;
public List<float> Segments;
};
}
public class SerializableUserData
{
public float Time;
public string Value;
}
private static void AddSegments(
CubismKeyframeData curve,
CubismKeyframeData preCurve,
CubismKeyframeData nextCurve,
SerializableCurve cubismCurve,
bool forceBezier,
ref int totalPointCount,
ref int totalSegmentCount,
ref int j
)
{
if (Math.Abs(curve.time - preCurve.time - 0.01f) < 0.0001f) // InverseSteppedSegment
{
if (nextCurve.value == curve.value)
{
cubismCurve.Segments.Add(3f); // Segment ID
cubismCurve.Segments.Add(nextCurve.time);
cubismCurve.Segments.Add(nextCurve.value);
j += 1;
totalPointCount += 1;
totalSegmentCount++;
return;
}
}
if (float.IsPositiveInfinity(curve.inSlope)) // SteppedSegment
{
cubismCurve.Segments.Add(2f); // Segment ID
cubismCurve.Segments.Add(curve.time);
cubismCurve.Segments.Add(curve.value);
totalPointCount += 1;
}
else if (preCurve.outSlope == 0f && Math.Abs(curve.inSlope) < 0.0001f && !forceBezier) // LinearSegment
{
cubismCurve.Segments.Add(0f); // Segment ID
cubismCurve.Segments.Add(curve.time);
cubismCurve.Segments.Add(curve.value);
totalPointCount += 1;
}
else // BezierSegment
{
var tangentLength = (curve.time - preCurve.time) / 3f;
cubismCurve.Segments.Add(1f); // Segment ID
cubismCurve.Segments.Add(preCurve.time + tangentLength);
cubismCurve.Segments.Add(preCurve.outSlope * tangentLength + preCurve.value);
cubismCurve.Segments.Add(curve.time - tangentLength);
cubismCurve.Segments.Add(curve.value - curve.inSlope * tangentLength);
cubismCurve.Segments.Add(curve.time);
cubismCurve.Segments.Add(curve.value);
totalPointCount += 3;
}
totalSegmentCount++;
}
public CubismMotion3Json(CubismFadeMotion fadeMotion, bool forceBezier)
{
Version = 3;
Meta = new SerializableMeta
{
// Duration of the motion in seconds.
Duration = fadeMotion.MotionLength,
// Framerate of the motion in seconds.
Fps = 30,
// [Optional] Status of the looping of the motion.
Loop = true,
// [Optional] Status of the restriction of Bezier handles'X translations.
AreBeziersRestricted = true,
// [Optional] Time of the overall Fade-In for easing in seconds.
FadeInTime = fadeMotion.FadeInTime,
// [Optional] Time of the overall Fade-Out for easing in seconds.
FadeOutTime = fadeMotion.FadeOutTime,
// The total number of curves.
CurveCount = (int)fadeMotion.ParameterCurves.LongCount(x => x.m_Curve.Length > 0),
// [Optional] The total number of UserData.
UserDataCount = 0
};
// Motion curves.
Curves = new SerializableCurve[Meta.CurveCount];
var totalSegmentCount = 1;
var totalPointCount = 1;
var actualCurveCount = 0;
for (var i = 0; i < fadeMotion.ParameterCurves.Length; i++)
{
if (fadeMotion.ParameterCurves[i].m_Curve.Length == 0)
continue;
Curves[actualCurveCount] = new SerializableCurve
{
// Target type.
Target = "Parameter",
// Identifier for mapping curve to target.
Id = fadeMotion.ParameterIds[i],
// [Optional] Time of the Fade - In for easing in seconds.
FadeInTime = fadeMotion.ParameterFadeInTimes[i],
// [Optional] Time of the Fade - Out for easing in seconds.
FadeOutTime = fadeMotion.ParameterFadeOutTimes[i],
// Flattened segments.
Segments = new List<float>
{
// First point
fadeMotion.ParameterCurves[i].m_Curve[0].time,
fadeMotion.ParameterCurves[i].m_Curve[0].value
}
};
for (var j = 1; j < fadeMotion.ParameterCurves[i].m_Curve.Length; j++)
{
var curve = fadeMotion.ParameterCurves[i].m_Curve[j];
var preCurve = fadeMotion.ParameterCurves[i].m_Curve[j - 1];
var next = fadeMotion.ParameterCurves[i].m_Curve.ElementAtOrDefault(j + 1);
var nextCurve = next ?? new CubismKeyframeData();
AddSegments(curve, preCurve, nextCurve, Curves[actualCurveCount], forceBezier, ref totalPointCount, ref totalSegmentCount, ref j);
}
actualCurveCount++;
}
// The total number of segments (from all curves).
Meta.TotalSegmentCount = totalSegmentCount;
// The total number of points (from all segments of all curves).
Meta.TotalPointCount = totalPointCount;
UserData = Array.Empty<SerializableUserData>();
// [Optional] The total size of UserData in bytes.
Meta.TotalUserDataSize = 0;
}
public CubismMotion3Json(ImportedKeyframedAnimation animation, bool forceBezier)
{
Version = 3;
Meta = new SerializableMeta
{
Duration = animation.Duration,
Fps = animation.SampleRate,
Loop = true,
AreBeziersRestricted = true,
FadeInTime = 0,
FadeOutTime = 0,
CurveCount = animation.TrackList.Count,
UserDataCount = animation.Events.Count
};
Curves = new SerializableCurve[Meta.CurveCount];
var totalSegmentCount = 1;
var totalPointCount = 1;
for (var i = 0; i < Meta.CurveCount; i++)
{
var track = animation.TrackList[i];
Curves[i] = new SerializableCurve
{
Target = track.Target,
Id = track.Name,
FadeInTime = -1,
FadeOutTime = -1,
Segments = new List<float>
{
0f,
track.Curve[0].value
}
};
for (var j = 1; j < track.Curve.Count; j++)
{
var curve = new CubismKeyframeData(track.Curve[j]);
var preCurve = new CubismKeyframeData(track.Curve[j - 1]);
var next = track.Curve.ElementAtOrDefault(j + 1);
var nextCurve = next != null ? new CubismKeyframeData(next) : new CubismKeyframeData();
AddSegments(curve, preCurve, nextCurve, Curves[i], forceBezier, ref totalPointCount, ref totalSegmentCount, ref j);
}
}
Meta.TotalSegmentCount = totalSegmentCount;
Meta.TotalPointCount = totalPointCount;
UserData = new SerializableUserData[Meta.UserDataCount];
var totalUserDataSize = 0;
for (var i = 0; i < Meta.UserDataCount; i++)
{
var @event = animation.Events[i];
UserData[i] = new SerializableUserData
{
Time = @event.time,
Value = @event.value
};
totalUserDataSize += @event.value.Length;
}
Meta.TotalUserDataSize = totalUserDataSize;
}
}
}

View File

@ -18,7 +18,7 @@ namespace CubismLive2DExtractor
{
public static class Live2DExtractor
{
public static void ExtractLive2D(IGrouping<string, AssetStudio.Object> assets, string destPath, string modelName, AssemblyLoader assemblyLoader)
public static void ExtractLive2D(IGrouping<string, AssetStudio.Object> assets, string destPath, string modelName, AssemblyLoader assemblyLoader, Live2DMotionMode motionMode, bool forceBezier = false)
{
var destTexturePath = Path.Combine(destPath, "textures") + Path.DirectorySeparatorChar;
var destMotionPath = Path.Combine(destPath, "motions") + Path.DirectorySeparatorChar;
@ -27,6 +27,7 @@ namespace CubismLive2DExtractor
Directory.CreateDirectory(destTexturePath);
var expressionList = new List<MonoBehaviour>();
var fadeMotionList = new List<MonoBehaviour>();
var gameObjects = new List<GameObject>();
var animationClips = new List<AnimationClip>();
@ -53,6 +54,9 @@ namespace CubismLive2DExtractor
case "CubismExpressionData":
expressionList.Add(m_MonoBehaviour);
break;
case "CubismFadeMotionData":
fadeMotionList.Add(m_MonoBehaviour);
break;
case "CubismEyeBlinkParameter":
if (m_MonoBehaviour.m_GameObject.TryGet(out var blinkGameObject))
{
@ -110,7 +114,42 @@ namespace CubismLive2DExtractor
//motion
var motions = new SortedDictionary<string, JArray>();
if (gameObjects.Count > 0)
if (motionMode == Live2DMotionMode.MonoBehaviour && fadeMotionList.Count > 0) //motion from MonoBehaviour
{
Directory.CreateDirectory(destMotionPath);
foreach (var fadeMotionMono in fadeMotionList)
{
var fadeMotionObj = fadeMotionMono.ToType();
if (fadeMotionObj == null)
{
var m_Type = fadeMotionMono.ConvertToTypeTree(assemblyLoader);
fadeMotionObj = fadeMotionMono.ToType(m_Type);
if (fadeMotionObj == null)
{
Logger.Warning($"Fade motion \"{fadeMotionMono.m_Name}\" is not readable.");
continue;
}
}
var fadeMotion = JsonConvert.DeserializeObject<CubismFadeMotion>(JsonConvert.SerializeObject(fadeMotionObj));
if (fadeMotion.ParameterIds.Length == 0)
continue;
var motionJson = new CubismMotion3Json(fadeMotion, forceBezier);
var animName = Path.GetFileNameWithoutExtension(fadeMotion.m_Name);
if (motions.ContainsKey(animName))
{
animName = $"{animName}_{fadeMotion.GetHashCode()}";
if (motions.ContainsKey(animName))
continue;
}
var motionPath = new JObject(new JProperty("File", $"motions/{animName}.motion3.json"));
motions.Add(animName, new JArray(motionPath));
File.WriteAllText($"{destMotionPath}{animName}.motion3.json", JsonConvert.SerializeObject(motionJson, Formatting.Indented, new MyJsonConverter()));
}
}
else if (gameObjects.Count > 0) //motion from AnimationClip
{
var rootTransform = gameObjects[0].m_Transform;
while (rootTransform.m_Father.TryGet(out var m_Father))
@ -123,111 +162,27 @@ namespace CubismLive2DExtractor
{
Directory.CreateDirectory(destMotionPath);
}
foreach (ImportedKeyframedAnimation animation in converter.AnimationList)
foreach (var animation in converter.AnimationList)
{
var json = new CubismMotion3Json
{
Version = 3,
Meta = new CubismMotion3Json.SerializableMeta
{
Duration = animation.Duration,
Fps = animation.SampleRate,
Loop = true,
AreBeziersRestricted = true,
CurveCount = animation.TrackList.Count,
UserDataCount = animation.Events.Count
},
Curves = new CubismMotion3Json.SerializableCurve[animation.TrackList.Count]
};
int totalSegmentCount = 1;
int totalPointCount = 1;
for (int i = 0; i < animation.TrackList.Count; i++)
{
var track = animation.TrackList[i];
json.Curves[i] = new CubismMotion3Json.SerializableCurve
{
Target = track.Target,
Id = track.Name,
Segments = new List<float> { 0f, track.Curve[0].value }
};
for (var j = 1; j < track.Curve.Count; j++)
{
var curve = track.Curve[j];
var preCurve = track.Curve[j - 1];
if (Math.Abs(curve.time - preCurve.time - 0.01f) < 0.0001f) //InverseSteppedSegment
{
var nextCurve = track.Curve[j + 1];
if (nextCurve.value == curve.value)
{
json.Curves[i].Segments.Add(3f);
json.Curves[i].Segments.Add(nextCurve.time);
json.Curves[i].Segments.Add(nextCurve.value);
j += 1;
totalPointCount += 1;
totalSegmentCount++;
continue;
}
}
if (float.IsPositiveInfinity(curve.inSlope)) //SteppedSegment
{
json.Curves[i].Segments.Add(2f);
json.Curves[i].Segments.Add(curve.time);
json.Curves[i].Segments.Add(curve.value);
totalPointCount += 1;
}
else if (preCurve.outSlope == 0f && Math.Abs(curve.inSlope) < 0.0001f) //LinearSegment
{
json.Curves[i].Segments.Add(0f);
json.Curves[i].Segments.Add(curve.time);
json.Curves[i].Segments.Add(curve.value);
totalPointCount += 1;
}
else //BezierSegment
{
var tangentLength = (curve.time - preCurve.time) / 3f;
json.Curves[i].Segments.Add(1f);
json.Curves[i].Segments.Add(preCurve.time + tangentLength);
json.Curves[i].Segments.Add(preCurve.outSlope * tangentLength + preCurve.value);
json.Curves[i].Segments.Add(curve.time - tangentLength);
json.Curves[i].Segments.Add(curve.value - curve.inSlope * tangentLength);
json.Curves[i].Segments.Add(curve.time);
json.Curves[i].Segments.Add(curve.value);
totalPointCount += 3;
}
totalSegmentCount++;
}
}
json.Meta.TotalSegmentCount = totalSegmentCount;
json.Meta.TotalPointCount = totalPointCount;
json.UserData = new CubismMotion3Json.SerializableUserData[animation.Events.Count];
var totalUserDataSize = 0;
for (var i = 0; i < animation.Events.Count; i++)
{
var @event = animation.Events[i];
json.UserData[i] = new CubismMotion3Json.SerializableUserData
{
Time = @event.time,
Value = @event.value
};
totalUserDataSize += @event.value.Length;
}
json.Meta.TotalUserDataSize = totalUserDataSize;
var motionJson = new CubismMotion3Json(animation, forceBezier);
var animName = animation.Name;
if (motions.ContainsKey(animName))
{
animName = $"{animName}_{animation.GetHashCode()}";
if (motions.ContainsKey(animName))
{
continue;
}
}
var motionPath = new JObject(new JProperty("File", $"motions/{animName}.motion3.json"));
motions.Add(animName, new JArray(motionPath));
File.WriteAllText($"{destMotionPath}{animName}.motion3.json", JsonConvert.SerializeObject(json, Formatting.Indented, new MyJsonConverter()));
File.WriteAllText($"{destMotionPath}{animName}.motion3.json", JsonConvert.SerializeObject(motionJson, Formatting.Indented, new MyJsonConverter()));
}
}
else
{
Logger.Warning($"No motions found for \"{modelName}\" model.");
}
//expression
var expressions = new JArray();

View File

@ -0,0 +1,8 @@
namespace CubismLive2DExtractor
{
public enum Live2DMotionMode
{
MonoBehaviour,
AnimationClip
}
}