//// // Based on UnityLive2DExtractor by Perfare // https://github.com/Perfare/UnityLive2DExtractor //// using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using AssetStudio; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using static CubismLive2DExtractor.CubismParsers; namespace CubismLive2DExtractor { public sealed class Live2DExtractor { private List Expressions { get; set; } private List FadeMotions { get; set; } private List GameObjects { get; set; } private List AnimationClips { get; set; } private List Texture2Ds { get; set; } private HashSet EyeBlinkParameters { get; set; } private HashSet LipSyncParameters { get; set; } private HashSet ParameterNames { get; set; } private HashSet PartNames { get; set; } private MonoBehaviour MocMono { get; set; } private MonoBehaviour PhysicsMono { get; set; } private MonoBehaviour FadeMotionLst { get; set; } private List ParametersCdi { get; set; } private List PartsCdi { get; set; } public Live2DExtractor(IGrouping assets, List inClipMotions = null, List inFadeMotions = null, MonoBehaviour inFadeMotionLst = null) { Expressions = new List(); FadeMotions = inFadeMotions ?? new List(); AnimationClips = inClipMotions ?? new List(); GameObjects = new List(); Texture2Ds = new List(); EyeBlinkParameters = new HashSet(); LipSyncParameters = new HashSet(); ParameterNames = new HashSet(); PartNames = new HashSet(); FadeMotionLst = inFadeMotionLst; ParametersCdi = new List(); PartsCdi = new List(); Logger.Debug("Sorting model assets.."); foreach (var asset in assets) { switch (asset) { case MonoBehaviour m_MonoBehaviour: if (m_MonoBehaviour.m_Script.TryGet(out var m_Script)) { switch (m_Script.m_ClassName) { case "CubismMoc": MocMono = m_MonoBehaviour; break; case "CubismPhysicsController": PhysicsMono = m_MonoBehaviour; break; case "CubismExpressionData": Expressions.Add(m_MonoBehaviour); break; case "CubismFadeMotionData": 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); } break; case "CubismMouthParameter": if (m_MonoBehaviour.m_GameObject.TryGet(out var mouthGameObject)) { LipSyncParameters.Add(mouthGameObject.m_Name); } break; case "CubismParameter": if (m_MonoBehaviour.m_GameObject.TryGet(out var paramGameObject)) { ParameterNames.Add(paramGameObject.m_Name); } break; case "CubismPart": if (m_MonoBehaviour.m_GameObject.TryGet(out var partGameObject)) { PartNames.Add(partGameObject.m_Name); } break; case "CubismDisplayInfoParameterName": if (m_MonoBehaviour.m_GameObject.TryGet(out _)) { ParametersCdi.Add(m_MonoBehaviour); } break; case "CubismDisplayInfoPartName": if (m_MonoBehaviour.m_GameObject.TryGet(out _)) { PartsCdi.Add(m_MonoBehaviour); } break; } } break; case AnimationClip m_AnimationClip: if (inClipMotions == null) { AnimationClips.Add(m_AnimationClip); } break; case GameObject m_GameObject: GameObjects.Add(m_GameObject); break; case Texture2D m_Texture2D: Texture2Ds.Add(m_Texture2D); break; } } } public void ExtractCubismModel(string destPath, string modelName, Live2DMotionMode motionMode, AssemblyLoader assemblyLoader, bool forceBezier = false, int parallelTaskCount = 1) { Directory.CreateDirectory(destPath); #region moc3 using (var cubismModel = new CubismModel(MocMono)) { 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(); var destTexturePath = Path.Combine(destPath, "textures") + Path.DirectorySeparatorChar; if (Texture2Ds.Count == 0) { Logger.Warning($"No textures found for \"{modelName}\" model"); } else { Directory.CreateDirectory(destTexturePath); } var textureBag = new ConcurrentBag(); var savePathHash = new ConcurrentDictionary(); Parallel.ForEach(Texture2Ds, new ParallelOptions { MaxDegreeOfParallelism = parallelTaskCount }, texture2D => { var savePath = $"{destTexturePath}{texture2D.m_Name}.png"; if (!savePathHash.TryAdd(savePath, true)) return; using (var image = texture2D.ConvertToImage(flip: true)) { using (var file = File.OpenWrite(savePath)) { image.WriteToStream(file, ImageFormat.Png); } textureBag.Add($"textures/{texture2D.m_Name}.png"); } }); textures.UnionWith(textureBag); #endregion #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 cdi3.json var isCdiParsed = false; if (ParametersCdi.Count > 0 || PartsCdi.Count > 0) { var cdiJson = new CubismCdi3Json { Version = 3, ParameterGroups = Array.Empty() }; var parameters = new SortedSet(); foreach (var paramMono in ParametersCdi) { var displayName = GetDisplayName(paramMono, assemblyLoader); if (displayName == null) break; paramMono.m_GameObject.TryGet(out var paramGameObject); var paramId = paramGameObject.m_Name; parameters.Add(new CubismCdi3Json.ParamGroupArray { Id = paramId, GroupId = "", Name = displayName }); } cdiJson.Parameters = parameters.ToArray(); var parts = new SortedSet(); foreach (var partMono in PartsCdi) { var displayName = GetDisplayName(partMono, assemblyLoader); if (displayName == null) break; partMono.m_GameObject.TryGet(out var partGameObject); var paramId = partGameObject.m_Name; parts.Add(new CubismCdi3Json.PartArray { Id = paramId, Name = displayName }); } cdiJson.Parts = parts.ToArray(); if (parts.Count > 0 || parameters.Count > 0) { File.WriteAllText($"{destPath}{modelName}.cdi3.json", JsonConvert.SerializeObject(cdiJson, Formatting.Indented)); isCdiParsed = true; } } #endregion #region motion3.json var motions = new SortedDictionary(); var destMotionPath = Path.Combine(destPath, "motions") + Path.DirectorySeparatorChar; if (motionMode == Live2DMotionMode.MonoBehaviour && FadeMotionLst != null) //Fade motions from Fade Motion List { Logger.Debug("Motion export method: MonoBehaviour (Fade motion)"); var fadeMotionLstDict = ParseMonoBehaviour(FadeMotionLst, CubismMonoBehaviourType.FadeMotionList, assemblyLoader); if (fadeMotionLstDict != null) { CubismObjectList.AssetsFile = FadeMotionLst.assetsFile; var fadeMotionAssetList = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(fadeMotionLstDict)).GetFadeMotionAssetList(); if (fadeMotionAssetList?.Count > 0) { FadeMotions = fadeMotionAssetList; Logger.Debug($"\"{FadeMotionLst.m_Name}\": found {fadeMotionAssetList.Count} motion(s)"); } } } 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 = FadeMotions.Count > 0 ? exportMethod + " (unable to export motions using Fade motion method)" : exportMethod + " (no Fade motions found)"; } Logger.Debug($"Motion export method: {exportMethod}"); ExportClipMotions(destMotionPath, converter, forceBezier, motions); } if (motions.Count == 0) { Logger.Warning($"No exportable motions found for \"{modelName}\" model"); } else { Logger.Info($"Exported {motions.Count} motion(s)"); } #endregion #region exp3.json var expressions = new JArray(); var destExpressionPath = Path.Combine(destPath, "expressions") + Path.DirectorySeparatorChar; if (Expressions.Count > 0) { Directory.CreateDirectory(destExpressionPath); } foreach (var monoBehaviour in Expressions) { var expressionName = monoBehaviour.m_Name.Replace(".exp3", ""); var expressionDict = ParseMonoBehaviour(monoBehaviour, CubismMonoBehaviourType.Expression, assemblyLoader); if (expressionDict == null) continue; var expression = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(expressionDict)); expressions.Add(new JObject { { "Name", expressionName }, { "File", $"expressions/{expressionName}.exp3.json" } }); File.WriteAllText($"{destExpressionPath}{expressionName}.exp3.json", JsonConvert.SerializeObject(expression, Formatting.Indented)); } #endregion #region model3.json var groups = new List(); //Try looking for group IDs among the parameter names manually if (EyeBlinkParameters.Count == 0) { 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) { LipSyncParameters = ParameterNames.Where(x => x.ToLower().Contains("mouth") && x.ToLower().Contains("open") && x.ToLower().Contains('y') ).ToHashSet(); } groups.Add(new CubismModel3Json.SerializableGroup { Target = "Parameter", Name = "EyeBlink", Ids = EyeBlinkParameters.ToArray() }); groups.Add(new CubismModel3Json.SerializableGroup { Target = "Parameter", Name = "LipSync", Ids = LipSyncParameters.ToArray() }); var model3 = new CubismModel3Json { Version = 3, Name = modelName, FileReferences = new CubismModel3Json.SerializableFileReferences { Moc = $"{modelName}.moc3", Textures = textures.ToArray(), DisplayInfo = isCdiParsed ? $"{modelName}.cdi3.json" : null, Physics = PhysicsMono != null ? $"{modelName}.physics3.json" : null, Motions = JObject.FromObject(motions), Expressions = expressions, }, Groups = groups.ToArray() }; File.WriteAllText($"{destPath}{modelName}.model3.json", JsonConvert.SerializeObject(model3, Formatting.Indented)); #endregion } private void ExportFadeMotions(string destMotionPath, AssemblyLoader assemblyLoader, bool forceBezier, SortedDictionary motions) { Directory.CreateDirectory(destMotionPath); foreach (var fadeMotionMono in FadeMotions) { var fadeMotionDict = ParseMonoBehaviour(fadeMotionMono, CubismMonoBehaviourType.FadeMotion, assemblyLoader); if (fadeMotionDict == null) continue; var fadeMotion = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(fadeMotionDict)); 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())); } } private static void ExportClipMotions(string destMotionPath, CubismMotion3Converter converter, bool forceBezier, SortedDictionary motions) { 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())); } } private static string GetDisplayName(MonoBehaviour cdiMono, AssemblyLoader assemblyLoader) { var dict = ParseMonoBehaviour(cdiMono, CubismMonoBehaviourType.DisplayInfo, assemblyLoader); if (dict == null) return null; var name = (string)dict["Name"]; if (dict.Contains("DisplayName")) { var displayName = (string)dict["DisplayName"]; name = displayName != "" ? displayName : name; } return name; } } }