Improve integration with Live2D assets

- Improved export method of AnimationClip motions
- [GUI] Added display of model info on the preview tab
- [GUI] Added support for partial export:
  - selected models
  - model + selected AnimationClip motions
  - model + selected Fade motions
  - model + selected Fade Motion List
This commit is contained in:
VaDiM
2024-02-07 03:51:52 +03:00
parent 2f6d9ec77f
commit 70213e3012
17 changed files with 1144 additions and 461 deletions

View File

@ -2,7 +2,7 @@
namespace CubismLive2DExtractor
{
public class AnimationCurve
public sealed class AnimationCurve
{
public CubismKeyframeData[] m_Curve { get; set; }
public int m_PreInfinity { get; set; }
@ -10,7 +10,7 @@ namespace CubismLive2DExtractor
public int m_RotationOrder { get; set; }
}
public class CubismFadeMotion
public sealed class CubismFadeMotion
{
public string m_Name { get; set; }
public string MotionName { get; set; }

View File

@ -10,14 +10,21 @@ namespace CubismLive2DExtractor
private Dictionary<uint, string> bonePathHash = new Dictionary<uint, string>();
public List<ImportedKeyframedAnimation> AnimationList { get; protected set; } = new List<ImportedKeyframedAnimation>();
public CubismMotion3Converter(GameObject rootGameObject, AnimationClip[] animationClips)
public CubismMotion3Converter(GameObject rootGameObject, List<AnimationClip> animationClips)
{
var rootTransform = GetTransform(rootGameObject);
CreateBonePathHash(rootTransform);
ConvertAnimations(animationClips);
}
private void ConvertAnimations(AnimationClip[] animationClips)
public CubismMotion3Converter(List<AnimationClip> animationClips, HashSet<string> partIds, HashSet<string> parameterIds)
{
CreateBonePathHash(partIds, pathType: "Parts/");
CreateBonePathHash(parameterIds, pathType: "Parameters/");
ConvertAnimations(animationClips);
}
private void ConvertAnimations(List<AnimationClip> animationClips)
{
foreach (var animationClip in animationClips)
{
@ -160,21 +167,30 @@ namespace CubismLive2DExtractor
return null;
}
private void CreateBonePathHash(HashSet<string> ids, string pathType)
{
foreach (var id in ids)
{
var name = pathType + id;;
bonePathHash[GetCRC(name)] = name;
int index;
while ((index = name.IndexOf("/", StringComparison.Ordinal)) >= 0)
{
name = name.Substring(index + 1);
bonePathHash[GetCRC(name)] = name;
}
}
}
private void CreateBonePathHash(Transform m_Transform)
{
var name = GetTransformPath(m_Transform);
var crc = new SevenZip.CRC();
var bytes = Encoding.UTF8.GetBytes(name);
crc.Update(bytes, 0, (uint)bytes.Length);
bonePathHash[crc.GetDigest()] = name;
bonePathHash[GetCRC(name)] = name;
int index;
while ((index = name.IndexOf("/", StringComparison.Ordinal)) >= 0)
{
name = name.Substring(index + 1);
crc = new SevenZip.CRC();
bytes = Encoding.UTF8.GetBytes(name);
crc.Update(bytes, 0, (uint)bytes.Length);
bonePathHash[crc.GetDigest()] = name;
bonePathHash[GetCRC(name)] = name;
}
foreach (var pptr in m_Transform.m_Children)
{
@ -183,7 +199,13 @@ namespace CubismLive2DExtractor
}
}
private string GetTransformPath(Transform transform)
private static uint GetCRC(string name)
{
var bytes = Encoding.UTF8.GetBytes(name);
return SevenZip.CRC.CalculateDigest(bytes, 0, (uint)bytes.Length);
}
private static string GetTransformPath(Transform transform)
{
transform.m_GameObject.TryGet(out var m_GameObject);
if (transform.m_Father.TryGet(out var father))

View File

@ -0,0 +1,62 @@
using System.Collections.Generic;
using System.Linq;
using AssetStudio;
namespace CubismLive2DExtractor
{
public sealed class CubismObjectList
{
public static SerializedFile AssetsFile { get; set; }
public HashSet<ObjectData> CubismExpressionObjects { get; set; }
public HashSet<ObjectData> CubismFadeMotionObjects { get; set; }
public class ObjectData
{
private long _pathID;
public Object Asset { get; set; }
public int m_FileID { get; set; }
public long m_PathID
{
get => _pathID;
set
{
_pathID = value;
Asset = GetObjByPathID(_pathID);
}
}
public override bool Equals(object obj)
{
return obj is ObjectData objectData && _pathID == objectData.m_PathID;
}
public override int GetHashCode()
{
return _pathID.GetHashCode();
}
}
public List<MonoBehaviour> GetFadeMotionAssetList()
{
return CubismFadeMotionObjects?.Where(x => x.Asset != null).Select(x => (MonoBehaviour)x.Asset).ToList();
}
public List<MonoBehaviour> GetExpressionList()
{
return CubismExpressionObjects?.Where(x => x.Asset != null).Select(x => (MonoBehaviour)x.Asset).ToList();
}
private static Object GetObjByPathID(long pathID)
{
var assetFileList = AssetsFile.assetsManager.assetsFileList;
foreach (var assetFile in assetFileList)
{
if (assetFile.ObjectsDic.TryGetValue(pathID, out var obj))
{
return obj;
}
}
return null;
}
}
}

View File

@ -0,0 +1,159 @@
using System;
using System.Collections.Specialized;
using System.Linq;
using AssetStudio;
using Newtonsoft.Json;
namespace CubismLive2DExtractor
{
public static class CubismParsers
{
public enum CubismMonoBehaviourType
{
FadeMotionList,
FadeMotion,
Expression,
Physics,
}
public static string ParsePhysics(OrderedDictionary physicsDict)
{
var cubismPhysicsRig = JsonConvert.DeserializeObject<CubismPhysics>(JsonConvert.SerializeObject(physicsDict))._rig;
var physicsSettings = new CubismPhysics3Json.SerializablePhysicsSettings[cubismPhysicsRig.SubRigs.Length];
for (int i = 0; i < physicsSettings.Length; i++)
{
var subRigs = cubismPhysicsRig.SubRigs[i];
physicsSettings[i] = new CubismPhysics3Json.SerializablePhysicsSettings
{
Id = $"PhysicsSetting{i + 1}",
Input = new CubismPhysics3Json.SerializableInput[subRigs.Input.Length],
Output = new CubismPhysics3Json.SerializableOutput[subRigs.Output.Length],
Vertices = new CubismPhysics3Json.SerializableVertex[subRigs.Particles.Length],
Normalization = new CubismPhysics3Json.SerializableNormalization
{
Position = new CubismPhysics3Json.SerializableNormalizationValue
{
Minimum = subRigs.Normalization.Position.Minimum,
Default = subRigs.Normalization.Position.Default,
Maximum = subRigs.Normalization.Position.Maximum
},
Angle = new CubismPhysics3Json.SerializableNormalizationValue
{
Minimum = subRigs.Normalization.Angle.Minimum,
Default = subRigs.Normalization.Angle.Default,
Maximum = subRigs.Normalization.Angle.Maximum
}
}
};
for (int j = 0; j < subRigs.Input.Length; j++)
{
var input = subRigs.Input[j];
physicsSettings[i].Input[j] = new CubismPhysics3Json.SerializableInput
{
Source = new CubismPhysics3Json.SerializableParameter
{
Target = "Parameter", //同名GameObject父节点的名称
Id = input.SourceId
},
Weight = input.Weight,
Type = Enum.GetName(typeof(CubismPhysicsSourceComponent), input.SourceComponent),
Reflect = input.IsInverted
};
}
for (int j = 0; j < subRigs.Output.Length; j++)
{
var output = subRigs.Output[j];
physicsSettings[i].Output[j] = new CubismPhysics3Json.SerializableOutput
{
Destination = new CubismPhysics3Json.SerializableParameter
{
Target = "Parameter", //同名GameObject父节点的名称
Id = output.DestinationId
},
VertexIndex = output.ParticleIndex,
Scale = output.AngleScale,
Weight = output.Weight,
Type = Enum.GetName(typeof(CubismPhysicsSourceComponent), output.SourceComponent),
Reflect = output.IsInverted
};
}
for (int j = 0; j < subRigs.Particles.Length; j++)
{
var particles = subRigs.Particles[j];
physicsSettings[i].Vertices[j] = new CubismPhysics3Json.SerializableVertex
{
Position = particles.InitialPosition,
Mobility = particles.Mobility,
Delay = particles.Delay,
Acceleration = particles.Acceleration,
Radius = particles.Radius
};
}
}
var physicsDictionary = new CubismPhysics3Json.SerializablePhysicsDictionary[physicsSettings.Length];
for (int i = 0; i < physicsSettings.Length; i++)
{
physicsDictionary[i] = new CubismPhysics3Json.SerializablePhysicsDictionary
{
Id = $"PhysicsSetting{i + 1}",
Name = $"Dummy{i + 1}"
};
}
var physicsJson = new CubismPhysics3Json
{
Version = 3,
Meta = new CubismPhysics3Json.SerializableMeta
{
PhysicsSettingCount = cubismPhysicsRig.SubRigs.Length,
TotalInputCount = cubismPhysicsRig.SubRigs.Sum(x => x.Input.Length),
TotalOutputCount = cubismPhysicsRig.SubRigs.Sum(x => x.Output.Length),
VertexCount = cubismPhysicsRig.SubRigs.Sum(x => x.Particles.Length),
EffectiveForces = new CubismPhysics3Json.SerializableEffectiveForces
{
Gravity = cubismPhysicsRig.Gravity,
Wind = cubismPhysicsRig.Wind
},
PhysicsDictionary = physicsDictionary
},
PhysicsSettings = physicsSettings
};
return JsonConvert.SerializeObject(physicsJson, Formatting.Indented, new MyJsonConverter2());
}
public static OrderedDictionary ParseMonoBehaviour(MonoBehaviour m_MonoBehaviour, CubismMonoBehaviourType cubismMonoBehaviourType, AssemblyLoader assemblyLoader)
{
var orderedDict = m_MonoBehaviour.ToType();
if (orderedDict != null)
return orderedDict;
var fieldName = "";
var m_Type = m_MonoBehaviour.ConvertToTypeTree(assemblyLoader);
switch (cubismMonoBehaviourType)
{
case CubismMonoBehaviourType.FadeMotionList:
fieldName = "cubismfademotionobjects";
break;
case CubismMonoBehaviourType.FadeMotion:
fieldName = "parameterids";
break;
case CubismMonoBehaviourType.Expression:
fieldName = "parameters";
break;
case CubismMonoBehaviourType.Physics:
fieldName = "_rig";
break;
}
if (m_Type.m_Nodes.FindIndex(x => x.m_Name.ToLower() == fieldName) < 0)
{
m_MonoBehaviour.m_Script.TryGet(out var m_MonoScript);
var assetName = m_MonoBehaviour.m_Name != "" ? m_MonoBehaviour.m_Name : m_MonoScript.m_ClassName;
Logger.Warning($"{cubismMonoBehaviourType} asset \"{assetName}\" is not readable");
return null;
}
orderedDict = m_MonoBehaviour.ToType(m_Type);
return orderedDict;
}
}
}

View File

@ -7,34 +7,43 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using AssetStudio;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using static CubismLive2DExtractor.CubismParsers;
namespace CubismLive2DExtractor
{
public static class Live2DExtractor
public sealed class Live2DExtractor
{
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;
var destExpressionPath = Path.Combine(destPath, "expressions") + Path.DirectorySeparatorChar;
Directory.CreateDirectory(destPath);
Directory.CreateDirectory(destTexturePath);
private List<MonoBehaviour> Expressions { get; set; }
private List<MonoBehaviour> FadeMotions { get; set; }
private List<GameObject> GameObjects { get; set; }
private List<AnimationClip> AnimationClips { get; set; }
private List<Texture2D> Texture2Ds { get; set; }
private HashSet<string> EyeBlinkParameters { get; set; }
private HashSet<string> LipSyncParameters { get; set; }
private HashSet<string> ParameterNames { get; set; }
private HashSet<string> PartNames { get; set; }
private MonoBehaviour MocMono { get; set; }
private MonoBehaviour PhysicsMono { get; set; }
private MonoBehaviour FadeMotionLst { get; set; }
var expressionList = new List<MonoBehaviour>();
var fadeMotionList = new List<MonoBehaviour>();
var gameObjects = new List<GameObject>();
var animationClips = new List<AnimationClip>();
var textures = new SortedSet<string>();
var eyeBlinkParameters = new HashSet<string>();
var lipSyncParameters = new HashSet<string>();
var parameterNames = new HashSet<string>();
var partNames = new HashSet<string>();
MonoBehaviour physics = null;
public Live2DExtractor(IGrouping<string, AssetStudio.Object> assets, List<AnimationClip> inClipMotions = null, List<MonoBehaviour> inFadeMotions = null, MonoBehaviour inFadeMotionLst = null)
{
Expressions = new List<MonoBehaviour>();
FadeMotions = inFadeMotions ?? new List<MonoBehaviour>();
AnimationClips = inClipMotions ?? new List<AnimationClip>();
GameObjects = new List<GameObject>();
Texture2Ds = new List<Texture2D>();
EyeBlinkParameters = new HashSet<string>();
LipSyncParameters = new HashSet<string>();
ParameterNames = new HashSet<string>();
PartNames = new HashSet<string>();
FadeMotionLst = inFadeMotionLst;
Logger.Info("Sorting model assets..");
foreach (var asset in assets)
{
switch (asset)
@ -45,210 +54,255 @@ namespace CubismLive2DExtractor
switch (m_Script.m_ClassName)
{
case "CubismMoc":
File.WriteAllBytes($"{destPath}{modelName}.moc3", ParseMoc(m_MonoBehaviour)); //moc
MocMono = m_MonoBehaviour;
break;
case "CubismPhysicsController":
physics = physics ?? m_MonoBehaviour;
PhysicsMono = m_MonoBehaviour;
break;
case "CubismExpressionData":
expressionList.Add(m_MonoBehaviour);
Expressions.Add(m_MonoBehaviour);
break;
case "CubismFadeMotionData":
fadeMotionList.Add(m_MonoBehaviour);
if (inFadeMotions == null && inFadeMotionLst == null)
{
FadeMotions.Add(m_MonoBehaviour);
}
break;
case "CubismFadeMotionList":
if (inFadeMotions == null && inFadeMotionLst == null)
{
FadeMotionLst = m_MonoBehaviour;
}
break;
case "CubismEyeBlinkParameter":
if (m_MonoBehaviour.m_GameObject.TryGet(out var blinkGameObject))
{
eyeBlinkParameters.Add(blinkGameObject.m_Name);
EyeBlinkParameters.Add(blinkGameObject.m_Name);
}
break;
case "CubismMouthParameter":
if (m_MonoBehaviour.m_GameObject.TryGet(out var mouthGameObject))
{
lipSyncParameters.Add(mouthGameObject.m_Name);
LipSyncParameters.Add(mouthGameObject.m_Name);
}
break;
case "CubismParameter":
if (m_MonoBehaviour.m_GameObject.TryGet(out var paramGameObject))
{
parameterNames.Add(paramGameObject.m_Name);
ParameterNames.Add(paramGameObject.m_Name);
}
break;
case "CubismPart":
if (m_MonoBehaviour.m_GameObject.TryGet(out var partGameObject))
{
partNames.Add(partGameObject.m_Name);
PartNames.Add(partGameObject.m_Name);
}
break;
}
}
break;
case Texture2D m_Texture2D:
using (var image = m_Texture2D.ConvertToImage(flip: true))
case AnimationClip m_AnimationClip:
if (inClipMotions == null)
{
using (var file = File.OpenWrite($"{destTexturePath}{m_Texture2D.m_Name}.png"))
{
image.WriteToStream(file, ImageFormat.Png);
}
textures.Add($"textures/{m_Texture2D.m_Name}.png"); //texture
AnimationClips.Add(m_AnimationClip);
}
break;
case GameObject m_GameObject:
gameObjects.Add(m_GameObject);
GameObjects.Add(m_GameObject);
break;
case AnimationClip m_AnimationClip:
animationClips.Add(m_AnimationClip);
case Texture2D m_Texture2D:
Texture2Ds.Add(m_Texture2D);
break;
}
}
}
if (textures.Count == 0)
public void ExtractCubismModel(string destPath, string modelName, Live2DMotionMode motionMode, AssemblyLoader assemblyLoader, bool forceBezier = false)
{
Directory.CreateDirectory(destPath);
#region moc3
using (var cubismModel = new CubismModel(MocMono))
{
Logger.Warning($"No textures found for \"{modelName}\" model.");
var sb = new StringBuilder();
sb.AppendLine("Model Stats:");
sb.AppendLine($"SDK Version: {cubismModel.VersionDescription}");
if (cubismModel.Version > 0)
{
sb.AppendLine($"Canvas Width: {cubismModel.CanvasWidth}");
sb.AppendLine($"Canvas Height: {cubismModel.CanvasHeight}");
sb.AppendLine($"Center X: {cubismModel.CentralPosX}");
sb.AppendLine($"Center Y: {cubismModel.CentralPosY}");
sb.AppendLine($"Pixel Per Unit: {cubismModel.PixelPerUnit}");
sb.AppendLine($"Part Count: {cubismModel.PartCount}");
sb.AppendLine($"Parameter Count: {cubismModel.ParamCount}");
Logger.Debug(sb.ToString());
ParameterNames = cubismModel.ParamNames;
PartNames = cubismModel.PartNames;
}
File.WriteAllBytes($"{destPath}{modelName}.moc3", cubismModel.ModelData);
}
#endregion
#region textures
var textures = new SortedSet<string>();
var destTexturePath = Path.Combine(destPath, "textures") + Path.DirectorySeparatorChar;
if (Texture2Ds.Count == 0)
{
Logger.Warning($"No textures found for \"{modelName}\" model");
}
else
{
Directory.CreateDirectory(destTexturePath);
}
//physics
if (physics != null)
foreach (var texture2D in Texture2Ds)
{
try
using (var image = texture2D.ConvertToImage(flip: true))
{
var buff = ParsePhysics(physics, assemblyLoader);
File.WriteAllText($"{destPath}{modelName}.physics3.json", buff);
}
catch (Exception e)
{
Logger.Warning($"Error in parsing physics data: {e.Message}");
physics = null;
using (var file = File.OpenWrite($"{destTexturePath}{texture2D.m_Name}.png"))
{
image.WriteToStream(file, ImageFormat.Png);
}
textures.Add($"textures/{texture2D.m_Name}.png");
}
}
#endregion
//motion
#region physics3.json
if (PhysicsMono != null)
{
var physicsDict = ParseMonoBehaviour(PhysicsMono, CubismMonoBehaviourType.Physics, assemblyLoader);
if (physicsDict != null)
{
try
{
var buff = ParsePhysics(physicsDict);
File.WriteAllText($"{destPath}{modelName}.physics3.json", buff);
}
catch (Exception e)
{
Logger.Warning($"Error in parsing physics data: {e.Message}");
PhysicsMono = null;
}
}
else
{
PhysicsMono = null;
}
}
#endregion
#region motion3.json
var motions = new SortedDictionary<string, JArray>();
var destMotionPath = Path.Combine(destPath, "motions") + Path.DirectorySeparatorChar;
if (motionMode == Live2DMotionMode.MonoBehaviour && fadeMotionList.Count > 0) //motion from MonoBehaviour
if (motionMode == Live2DMotionMode.MonoBehaviour && FadeMotionLst != null) //Fade motions from Fade Motion List
{
Logger.Debug("Motion export method: MonoBehaviour (Fade motion)");
Directory.CreateDirectory(destMotionPath);
foreach (var fadeMotionMono in fadeMotionList)
var fadeMotionLstDict = ParseMonoBehaviour(FadeMotionLst, CubismMonoBehaviourType.FadeMotionList, assemblyLoader);
if (fadeMotionLstDict != null)
{
var fadeMotionObj = fadeMotionMono.ToType();
if (fadeMotionObj == null)
CubismObjectList.AssetsFile = FadeMotionLst.assetsFile;
var fadeMotionAssetList = JsonConvert.DeserializeObject<CubismObjectList>(JsonConvert.SerializeObject(fadeMotionLstDict)).GetFadeMotionAssetList();
if (fadeMotionAssetList?.Count > 0)
{
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;
}
FadeMotions = fadeMotionAssetList;
Logger.Debug($"\"{FadeMotionLst.m_Name}\": found {fadeMotionAssetList.Count} motion(s)");
}
var fadeMotion = JsonConvert.DeserializeObject<CubismFadeMotion>(JsonConvert.SerializeObject(fadeMotionObj));
if (fadeMotion.ParameterIds.Length == 0)
continue;
var motionJson = new CubismMotion3Json(fadeMotion, parameterNames, partNames, 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()));
}
}
if (motions.Count == 0 && gameObjects.Count > 0) //motion from AnimationClip
if (motionMode == Live2DMotionMode.MonoBehaviour && FadeMotions.Count > 0) //motion from MonoBehaviour
{
ExportFadeMotions(destMotionPath, assemblyLoader, forceBezier, motions);
}
if (motions.Count == 0) //motion from AnimationClip
{
CubismMotion3Converter converter = null;
var exportMethod = "AnimationClip";
if (motionMode != Live2DMotionMode.AnimationClipV1) //AnimationClipV2
{
exportMethod += "V2";
converter = new CubismMotion3Converter(AnimationClips, PartNames, ParameterNames);
}
else if (GameObjects.Count > 0) //AnimationClipV1
{
exportMethod += "V1";
var rootTransform = GameObjects[0].m_Transform;
while (rootTransform.m_Father.TryGet(out var m_Father))
{
rootTransform = m_Father;
}
rootTransform.m_GameObject.TryGet(out var rootGameObject);
converter = new CubismMotion3Converter(rootGameObject, AnimationClips);
}
if (motionMode == Live2DMotionMode.MonoBehaviour)
{
exportMethod = fadeMotionList.Count > 0
? exportMethod + " (unable to export motion using Fade motion method)"
exportMethod = FadeMotions.Count > 0
? exportMethod + " (unable to export motions using Fade motion method)"
: exportMethod + " (no Fade motions found)";
}
Logger.Debug($"Motion export method: {exportMethod}");
var rootTransform = gameObjects[0].m_Transform;
while (rootTransform.m_Father.TryGet(out var m_Father))
{
rootTransform = m_Father;
}
rootTransform.m_GameObject.TryGet(out var rootGameObject);
var converter = new CubismMotion3Converter(rootGameObject, animationClips.ToArray());
if (converter.AnimationList.Count > 0)
{
Directory.CreateDirectory(destMotionPath);
}
foreach (var animation in converter.AnimationList)
{
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(motionJson, Formatting.Indented, new MyJsonConverter()));
}
ExportClipMotions(destMotionPath, converter, forceBezier, motions);
}
if (motions.Count == 0)
{
Logger.Warning($"No motions found for \"{modelName}\" model.");
Logger.Warning($"No exportable motions found for \"{modelName}\" model");
}
else
{
Logger.Debug($"Exported {motions.Count} motion(s)");
}
#endregion
//expression
#region exp3.json
var expressions = new JArray();
if (expressionList.Count > 0)
var destExpressionPath = Path.Combine(destPath, "expressions") + Path.DirectorySeparatorChar;
if (Expressions.Count > 0)
{
Directory.CreateDirectory(destExpressionPath);
}
foreach (var monoBehaviour in expressionList)
foreach (var monoBehaviour in Expressions)
{
var expressionName = monoBehaviour.m_Name.Replace(".exp3", "");
var expressionObj = monoBehaviour.ToType();
if (expressionObj == null)
{
var m_Type = monoBehaviour.ConvertToTypeTree(assemblyLoader);
expressionObj = monoBehaviour.ToType(m_Type);
if (expressionObj == null)
{
Logger.Warning($"Expression \"{expressionName}\" is not readable.");
continue;
}
}
var expression = JsonConvert.DeserializeObject<CubismExpression3Json>(JsonConvert.SerializeObject(expressionObj));
var expressionDict = ParseMonoBehaviour(monoBehaviour, CubismMonoBehaviourType.Expression, assemblyLoader);
if (expressionDict == null)
continue;
var expression = JsonConvert.DeserializeObject<CubismExpression3Json>(JsonConvert.SerializeObject(expressionDict));
expressions.Add(new JObject
{
{ "Name", expressionName },
{ "File", $"expressions/{expressionName}.exp3.json" }
});
{
{ "Name", expressionName },
{ "File", $"expressions/{expressionName}.exp3.json" }
});
File.WriteAllText($"{destExpressionPath}{expressionName}.exp3.json", JsonConvert.SerializeObject(expression, Formatting.Indented));
}
#endregion
//group
#region model3.json
var groups = new List<CubismModel3Json.SerializableGroup>();
//Try looking for group IDs among the parameter names manually
if (eyeBlinkParameters.Count == 0)
if (EyeBlinkParameters.Count == 0)
{
eyeBlinkParameters = parameterNames.Where(x =>
EyeBlinkParameters = ParameterNames.Where(x =>
x.ToLower().Contains("eye")
&& x.ToLower().Contains("open")
&& (x.ToLower().Contains('l') || x.ToLower().Contains('r'))
).ToHashSet();
}
if (lipSyncParameters.Count == 0)
if (LipSyncParameters.Count == 0)
{
lipSyncParameters = parameterNames.Where(x =>
LipSyncParameters = ParameterNames.Where(x =>
x.ToLower().Contains("mouth")
&& x.ToLower().Contains("open")
&& x.ToLower().Contains('y')
@ -259,16 +313,15 @@ namespace CubismLive2DExtractor
{
Target = "Parameter",
Name = "EyeBlink",
Ids = eyeBlinkParameters.ToArray()
Ids = EyeBlinkParameters.ToArray()
});
groups.Add(new CubismModel3Json.SerializableGroup
{
Target = "Parameter",
Name = "LipSync",
Ids = lipSyncParameters.ToArray()
Ids = LipSyncParameters.ToArray()
});
//model
var model3 = new CubismModel3Json
{
Version = 3,
@ -277,140 +330,74 @@ namespace CubismLive2DExtractor
{
Moc = $"{modelName}.moc3",
Textures = textures.ToArray(),
Physics = PhysicsMono == null ? null : $"{modelName}.physics3.json",
Motions = JObject.FromObject(motions),
Expressions = expressions,
},
Groups = groups.ToArray()
};
if (physics != null)
{
model3.FileReferences.Physics = $"{modelName}.physics3.json";
}
File.WriteAllText($"{destPath}{modelName}.model3.json", JsonConvert.SerializeObject(model3, Formatting.Indented));
#endregion
}
private static string ParsePhysics(MonoBehaviour physics, AssemblyLoader assemblyLoader)
private void ExportFadeMotions(string destMotionPath, AssemblyLoader assemblyLoader, bool forceBezier, SortedDictionary<string, JArray> motions)
{
var physicsObj = physics.ToType();
if (physicsObj == null)
Directory.CreateDirectory(destMotionPath);
foreach (var fadeMotionMono in FadeMotions)
{
var m_Type = physics.ConvertToTypeTree(assemblyLoader);
physicsObj = physics.ToType(m_Type);
if (physicsObj == null)
{
throw new Exception("MonoBehaviour is not readable.");
}
}
var cubismPhysicsRig = JsonConvert.DeserializeObject<CubismPhysics>(JsonConvert.SerializeObject(physicsObj))._rig;
var fadeMotionDict = ParseMonoBehaviour(fadeMotionMono, CubismMonoBehaviourType.FadeMotion, assemblyLoader);
if (fadeMotionDict == null)
continue;
var fadeMotion = JsonConvert.DeserializeObject<CubismFadeMotion>(JsonConvert.SerializeObject(fadeMotionDict));
if (fadeMotion.ParameterIds.Length == 0)
continue;
var physicsSettings = new CubismPhysics3Json.SerializablePhysicsSettings[cubismPhysicsRig.SubRigs.Length];
for (int i = 0; i < physicsSettings.Length; i++)
{
var subRigs = cubismPhysicsRig.SubRigs[i];
physicsSettings[i] = new CubismPhysics3Json.SerializablePhysicsSettings
var motionJson = new CubismMotion3Json(fadeMotion, ParameterNames, PartNames, forceBezier);
var animName = Path.GetFileNameWithoutExtension(fadeMotion.m_Name);
if (motions.ContainsKey(animName))
{
Id = $"PhysicsSetting{i + 1}",
Input = new CubismPhysics3Json.SerializableInput[subRigs.Input.Length],
Output = new CubismPhysics3Json.SerializableOutput[subRigs.Output.Length],
Vertices = new CubismPhysics3Json.SerializableVertex[subRigs.Particles.Length],
Normalization = new CubismPhysics3Json.SerializableNormalization
{
Position = new CubismPhysics3Json.SerializableNormalizationValue
{
Minimum = subRigs.Normalization.Position.Minimum,
Default = subRigs.Normalization.Position.Default,
Maximum = subRigs.Normalization.Position.Maximum
},
Angle = new CubismPhysics3Json.SerializableNormalizationValue
{
Minimum = subRigs.Normalization.Angle.Minimum,
Default = subRigs.Normalization.Angle.Default,
Maximum = subRigs.Normalization.Angle.Maximum
}
}
};
for (int j = 0; j < subRigs.Input.Length; j++)
{
var input = subRigs.Input[j];
physicsSettings[i].Input[j] = new CubismPhysics3Json.SerializableInput
{
Source = new CubismPhysics3Json.SerializableParameter
{
Target = "Parameter", //同名GameObject父节点的名称
Id = input.SourceId
},
Weight = input.Weight,
Type = Enum.GetName(typeof(CubismPhysicsSourceComponent), input.SourceComponent),
Reflect = input.IsInverted
};
}
for (int j = 0; j < subRigs.Output.Length; j++)
{
var output = subRigs.Output[j];
physicsSettings[i].Output[j] = new CubismPhysics3Json.SerializableOutput
{
Destination = new CubismPhysics3Json.SerializableParameter
{
Target = "Parameter", //同名GameObject父节点的名称
Id = output.DestinationId
},
VertexIndex = output.ParticleIndex,
Scale = output.AngleScale,
Weight = output.Weight,
Type = Enum.GetName(typeof(CubismPhysicsSourceComponent), output.SourceComponent),
Reflect = output.IsInverted
};
}
for (int j = 0; j < subRigs.Particles.Length; j++)
{
var particles = subRigs.Particles[j];
physicsSettings[i].Vertices[j] = new CubismPhysics3Json.SerializableVertex
{
Position = particles.InitialPosition,
Mobility = particles.Mobility,
Delay = particles.Delay,
Acceleration = particles.Acceleration,
Radius = particles.Radius
};
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()));
}
var physicsDictionary = new CubismPhysics3Json.SerializablePhysicsDictionary[physicsSettings.Length];
for (int i = 0; i < physicsSettings.Length; i++)
{
physicsDictionary[i] = new CubismPhysics3Json.SerializablePhysicsDictionary
{
Id = $"PhysicsSetting{i + 1}",
Name = $"Dummy{i + 1}"
};
}
var physicsJson = new CubismPhysics3Json
{
Version = 3,
Meta = new CubismPhysics3Json.SerializableMeta
{
PhysicsSettingCount = cubismPhysicsRig.SubRigs.Length,
TotalInputCount = cubismPhysicsRig.SubRigs.Sum(x => x.Input.Length),
TotalOutputCount = cubismPhysicsRig.SubRigs.Sum(x => x.Output.Length),
VertexCount = cubismPhysicsRig.SubRigs.Sum(x => x.Particles.Length),
EffectiveForces = new CubismPhysics3Json.SerializableEffectiveForces
{
Gravity = cubismPhysicsRig.Gravity,
Wind = cubismPhysicsRig.Wind
},
PhysicsDictionary = physicsDictionary
},
PhysicsSettings = physicsSettings
};
return JsonConvert.SerializeObject(physicsJson, Formatting.Indented, new MyJsonConverter2());
}
private static byte[] ParseMoc(MonoBehaviour moc)
private static void ExportClipMotions(string destMotionPath, CubismMotion3Converter converter, bool forceBezier, SortedDictionary<string, JArray> motions)
{
var reader = moc.reader;
reader.Reset();
reader.Position += 28; //PPtr<GameObject> m_GameObject, m_Enabled, PPtr<MonoScript>
reader.ReadAlignedString(); //m_Name
return reader.ReadBytes(reader.ReadInt32());
if (converter == null)
return;
if (converter.AnimationList.Count > 0)
{
Directory.CreateDirectory(destMotionPath);
}
foreach (var animation in converter.AnimationList)
{
var animName = animation.Name;
if (animation.TrackList.Count == 0)
{
Logger.Warning($"Motion \"{animName}\" is empty. Export skipped");
continue;
}
var motionJson = new CubismMotion3Json(animation, forceBezier);
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(motionJson, Formatting.Indented, new MyJsonConverter()));
}
}
}
}

View File

@ -3,6 +3,7 @@
public enum Live2DMotionMode
{
MonoBehaviour,
AnimationClip
AnimationClipV1,
AnimationClipV2,
}
}

View File

@ -1012,18 +1012,16 @@ namespace AssetStudio
private void CreateBonePathHash(Transform m_Transform)
{
var name = GetTransformPathByFather(m_Transform);
var crc = new SevenZip.CRC();
var bytes = Encoding.UTF8.GetBytes(name);
crc.Update(bytes, 0, (uint)bytes.Length);
bonePathHash[crc.GetDigest()] = name;
var crc = SevenZip.CRC.CalculateDigest(bytes, 0, (uint)bytes.Length);
bonePathHash[crc] = name;
int index;
while ((index = name.IndexOf("/", StringComparison.Ordinal)) >= 0)
{
name = name.Substring(index + 1);
crc = new SevenZip.CRC();
bytes = Encoding.UTF8.GetBytes(name);
crc.Update(bytes, 0, (uint)bytes.Length);
bonePathHash[crc.GetDigest()] = name;
crc = SevenZip.CRC.CalculateDigest(bytes, 0, (uint)bytes.Length);
bonePathHash[crc] = name;
}
foreach (var pptr in m_Transform.m_Children)
{