From e41574037387ac29ba52851acf1f9d1f4727398d Mon Sep 17 00:00:00 2001 From: VaDiM Date: Wed, 8 Nov 2023 01:25:32 +0300 Subject: [PATCH] Some fixes for Live2D export --- AssetStudioCLI/Studio.cs | 39 +++-- AssetStudioGUI/Studio.cs | 46 +++-- .../CubismLive2DExtractor/Live2DExtractor.cs | 158 +++++++++--------- 3 files changed, 143 insertions(+), 100 deletions(-) diff --git a/AssetStudioCLI/Studio.cs b/AssetStudioCLI/Studio.cs index 1e65df0..12f3529 100644 --- a/AssetStudioCLI/Studio.cs +++ b/AssetStudioCLI/Studio.cs @@ -624,6 +624,7 @@ namespace AssetStudioCLI } return false; }).Select(x => x.Asset).ToArray(); + if (cubismMocs.Length == 0) { Logger.Default.Log(LoggerEvent.Info, "Live2D Cubism models were not found.", ignoreLevel: true); @@ -631,7 +632,18 @@ namespace AssetStudioCLI } if (cubismMocs.Length > 1) { - var basePathSet = cubismMocs.Select(x => containers[x].Substring(0, containers[x].LastIndexOf("/"))).ToHashSet(); + var basePathSet = cubismMocs.Select(x => + { + var pathLen = containers.TryGetValue(x, out var itemContainer) ? itemContainer.LastIndexOf("/") : 0; + pathLen = pathLen < 0 ? containers[x].Length : pathLen; + return itemContainer?.Substring(0, pathLen); + }).ToHashSet(); + + if (basePathSet.All(x => x == null)) + { + Logger.Error($"Live2D Cubism export error: Cannot find any model related files."); + return; + } if (basePathSet.Count != cubismMocs.Length) { @@ -639,9 +651,16 @@ namespace AssetStudioCLI Logger.Debug($"useFullContainerPath: {useFullContainerPath}"); } } - var basePathList = useFullContainerPath ? - cubismMocs.Select(x => containers[x]).ToList() : - cubismMocs.Select(x => containers[x].Substring(0, containers[x].LastIndexOf("/"))).ToList(); + + var basePathList = cubismMocs.Select(x => + { + containers.TryGetValue(x, out var container); + container = useFullContainerPath + ? container + : container?.Substring(0, container.LastIndexOf("/")); + return container; + }).Where(x => x != null).ToList(); + var lookup = containers.ToLookup( x => basePathList.Find(b => x.Value.Contains(b) && x.Value.Split('/').Any(y => y == b.Substring(b.LastIndexOf("/") + 1))), x => x.Key @@ -649,16 +668,15 @@ namespace AssetStudioCLI var totalModelCount = lookup.LongCount(x => x.Key != null); Logger.Info($"Found {totalModelCount} model(s)."); - var name = ""; var modelCounter = 0; foreach (var assets in lookup) { - var container = assets.Key; - if (container == null) + var srcContainer = assets.Key; + if (srcContainer == null) continue; - name = container; + var container = srcContainer; - Logger.Info($"[{modelCounter + 1}/{totalModelCount}] Exporting Live2D: \"{container.Color(Ansi.BrightCyan)}\""); + Logger.Info($"[{modelCounter + 1}/{totalModelCount}] Exporting Live2D: \"{srcContainer.Color(Ansi.BrightCyan)}\""); try { var modelName = useFullContainerPath ? Path.GetFileNameWithoutExtension(container) : container.Substring(container.LastIndexOf('/') + 1); @@ -670,10 +688,11 @@ namespace AssetStudioCLI } catch (Exception ex) { - Logger.Error($"Live2D model export error: \"{name}\"", ex); + Logger.Error($"Live2D model export error: \"{srcContainer}\"", ex); } Progress.Report(modelCounter, (int)totalModelCount); } + var status = modelCounter > 0 ? $"Finished exporting [{modelCounter}/{totalModelCount}] Live2D model(s) to \"{CLIOptions.o_outputFolder.Value.Color(Ansi.BrightCyan)}\"" : "Nothing exported."; diff --git a/AssetStudioGUI/Studio.cs b/AssetStudioGUI/Studio.cs index 6d55393..8b0acc9 100644 --- a/AssetStudioGUI/Studio.cs +++ b/AssetStudioGUI/Studio.cs @@ -259,6 +259,7 @@ namespace AssetStudioGUI Progress.Report(++i, objectCount); } } + allContainers.Clear(); foreach ((var pptr, var container) in containers) { if (pptr.TryGet(out var obj)) @@ -753,32 +754,51 @@ namespace AssetStudioGUI var useFullContainerPath = false; if (cubismMocs.Length > 1) { - var basePathSet = cubismMocs.Select(x => allContainers[x].Substring(0, allContainers[x].LastIndexOf("/"))).ToHashSet(); + var basePathSet = cubismMocs.Select(x => + { + var pathLen = allContainers.TryGetValue(x, out var itemContainer) ? itemContainer.LastIndexOf("/") : 0; + pathLen = pathLen < 0 ? allContainers[x].Length : pathLen; + return itemContainer?.Substring(0, pathLen); + }).ToHashSet(); + + if (basePathSet.All(x => x == null)) + { + Logger.Error($"Live2D Cubism export error\r\nCannot find any model related files"); + StatusStripUpdate("Live2D export canceled"); + Progress.Reset(); + return; + } if (basePathSet.Count != cubismMocs.Length) { useFullContainerPath = true; } } - var basePathList = useFullContainerPath ? - cubismMocs.Select(x => allContainers[x]).ToList() : - cubismMocs.Select(x => allContainers[x].Substring(0, allContainers[x].LastIndexOf("/"))).ToList(); + + var basePathList = cubismMocs.Select(x => + { + allContainers.TryGetValue(x, out var container); + container = useFullContainerPath + ? container + : container?.Substring(0, container.LastIndexOf("/")); + return container; + }).Where(x => x != null).ToList(); + var lookup = allContainers.ToLookup( x => basePathList.Find(b => x.Value.Contains(b) && x.Value.Split('/').Any(y => y == b.Substring(b.LastIndexOf("/") + 1))), x => x.Key ); var totalModelCount = lookup.LongCount(x => x.Key != null); - var name = ""; var modelCounter = 0; foreach (var assets in lookup) { - var container = assets.Key; - if (container == null) + var srcContainer = assets.Key; + if (srcContainer == null) continue; - name = container; + var container = srcContainer; - Logger.Info($"[{modelCounter + 1}/{totalModelCount}] Exporting Live2D: \"{container}\"..."); + Logger.Info($"[{modelCounter + 1}/{totalModelCount}] Exporting Live2D: \"{srcContainer}\"..."); try { var modelName = useFullContainerPath ? Path.GetFileNameWithoutExtension(container) : container.Substring(container.LastIndexOf('/') + 1); @@ -790,11 +810,17 @@ namespace AssetStudioGUI } catch (Exception ex) { - Logger.Error($"Live2D model export error: \"{name}\"", ex); + Logger.Error($"Live2D model export error: \"{srcContainer}\"", ex); } Progress.Report(modelCounter, (int)totalModelCount); } + Logger.Info($"Finished exporting [{modelCounter}/{totalModelCount}] Live2D model(s)."); + if (modelCounter < totalModelCount) + { + var total = (int)totalModelCount; + Progress.Report(total, total); + } if (Properties.Settings.Default.openAfterExport && modelCounter > 0) { OpenFolderInExplorer(exportPath); diff --git a/AssetStudioUtility/CubismLive2DExtractor/Live2DExtractor.cs b/AssetStudioUtility/CubismLive2DExtractor/Live2DExtractor.cs index c312fb6..db4530c 100644 --- a/AssetStudioUtility/CubismLive2DExtractor/Live2DExtractor.cs +++ b/AssetStudioUtility/CubismLive2DExtractor/Live2DExtractor.cs @@ -26,20 +26,57 @@ namespace CubismLive2DExtractor Directory.CreateDirectory(destPath); Directory.CreateDirectory(destTexturePath); - var monoBehaviours = new List(); - var texture2Ds = new List(); + var expressionList = new List(); var gameObjects = new List(); var animationClips = new List(); + var textures = new SortedSet(); + var eyeBlinkParameters = new HashSet(); + var lipSyncParameters = new HashSet(); + MonoBehaviour physics = null; + foreach (var asset in assets) { switch (asset) { case MonoBehaviour m_MonoBehaviour: - monoBehaviours.Add(m_MonoBehaviour); + if (m_MonoBehaviour.m_Script.TryGet(out var m_Script)) + { + switch (m_Script.m_ClassName) + { + case "CubismMoc": + File.WriteAllBytes($"{destPath}{modelName}.moc3", ParseMoc(m_MonoBehaviour)); //moc + break; + case "CubismPhysicsController": + physics = physics ?? m_MonoBehaviour; + break; + case "CubismExpressionData": + expressionList.Add(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; + } + } break; case Texture2D m_Texture2D: - texture2Ds.Add(m_Texture2D); + using (var image = m_Texture2D.ConvertToImage(flip: true)) + { + 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 + } break; case GameObject m_GameObject: gameObjects.Add(m_GameObject); @@ -50,15 +87,12 @@ namespace CubismLive2DExtractor } } - //physics - var physics = monoBehaviours.FirstOrDefault(x => + if (textures.Count == 0) { - if (x.m_Script.TryGet(out var m_Script)) - { - return m_Script.m_ClassName == "CubismPhysicsController"; - } - return false; - }); + Logger.Warning($"No textures found for \"{modelName}\" model."); + } + + //physics if (physics != null) { try @@ -73,31 +107,6 @@ namespace CubismLive2DExtractor } } - //moc - var moc = monoBehaviours.First(x => - { - if (x.m_Script.TryGet(out var m_Script)) - { - return m_Script.m_ClassName == "CubismMoc"; - } - return false; - }); - File.WriteAllBytes($"{destPath}{modelName}.moc3", ParseMoc(moc)); - - //texture - var textures = new SortedSet(); - foreach (var texture2D in texture2Ds) - { - using (var image = texture2D.ConvertToImage(flip: true)) - { - textures.Add($"textures/{texture2D.m_Name}.png"); - using (var file = File.OpenWrite($"{destTexturePath}{texture2D.m_Name}.png")) - { - image.WriteToStream(file, ImageFormat.Png); - } - } - } - //motion var motions = new SortedDictionary(); @@ -205,23 +214,30 @@ namespace CubismLive2DExtractor } json.Meta.TotalUserDataSize = totalUserDataSize; - var motionPath = new JObject(new JProperty("File", $"motions/{animation.Name}.motion3.json")); - motions.Add(animation.Name, new JArray(motionPath)); - File.WriteAllText($"{destMotionPath}{animation.Name}.motion3.json", JsonConvert.SerializeObject(json, Formatting.Indented, new MyJsonConverter())); + 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())); } } //expression var expressions = new JArray(); - var monoBehaviourArray = monoBehaviours.Where(x => x.m_Name.EndsWith(".exp3")).ToArray(); - if (monoBehaviourArray.Length > 0) + if (expressionList.Count > 0) { Directory.CreateDirectory(destExpressionPath); } - foreach (var monoBehaviour in monoBehaviourArray) + foreach (var monoBehaviour in expressionList) { - var fullName = monoBehaviour.m_Name; - var expressionName = fullName.Replace(".exp3", ""); + var expressionName = monoBehaviour.m_Name.Replace(".exp3", ""); var expressionObj = monoBehaviour.ToType(); if (expressionObj == null) { @@ -238,57 +254,38 @@ namespace CubismLive2DExtractor expressions.Add(new JObject { { "Name", expressionName }, - { "File", $"expressions/{fullName}.json" } + { "File", $"expressions/{expressionName}.exp3.json" } }); - File.WriteAllText($"{destExpressionPath}{fullName}.json", JsonConvert.SerializeObject(expression, Formatting.Indented)); + File.WriteAllText($"{destExpressionPath}{expressionName}.exp3.json", JsonConvert.SerializeObject(expression, Formatting.Indented)); } - //model + //group var groups = new List(); - var eyeBlinkParameters = monoBehaviours.Where(x => - { - x.m_Script.TryGet(out var m_Script); - return m_Script?.m_ClassName == "CubismEyeBlinkParameter"; - }).Select(x => - { - x.m_GameObject.TryGet(out var m_GameObject); - return m_GameObject?.m_Name; - }).ToHashSet(); + //Try looking for group IDs among the gameObjects if (eyeBlinkParameters.Count == 0) { eyeBlinkParameters = gameObjects.Where(x => - { - return x.m_Name.ToLower().Contains("eye") + x.m_Name.ToLower().Contains("eye") && x.m_Name.ToLower().Contains("open") - && (x.m_Name.ToLower().Contains('l') || x.m_Name.ToLower().Contains('r')); - }).Select(x => x.m_Name).ToHashSet(); + && (x.m_Name.ToLower().Contains('l') || x.m_Name.ToLower().Contains('r')) + ).Select(x => x.m_Name).ToHashSet(); } + if (lipSyncParameters.Count == 0) + { + lipSyncParameters = gameObjects.Where(x => + x.m_Name.ToLower().Contains("mouth") + && x.m_Name.ToLower().Contains("open") + && x.m_Name.ToLower().Contains('y') + ).Select(x => x.m_Name).ToHashSet(); + } + groups.Add(new CubismModel3Json.SerializableGroup { Target = "Parameter", Name = "EyeBlink", Ids = eyeBlinkParameters.ToArray() }); - - var lipSyncParameters = monoBehaviours.Where(x => - { - x.m_Script.TryGet(out var m_Script); - return m_Script?.m_ClassName == "CubismMouthParameter"; - }).Select(x => - { - x.m_GameObject.TryGet(out var m_GameObject); - return m_GameObject?.m_Name; - }).ToHashSet(); - if (lipSyncParameters.Count == 0) - { - lipSyncParameters = gameObjects.Where(x => - { - return x.m_Name.ToLower().Contains("mouth") - && x.m_Name.ToLower().Contains("open") - && x.m_Name.ToLower().Contains('y'); - }).Select(x => x.m_Name).ToHashSet(); - } groups.Add(new CubismModel3Json.SerializableGroup { Target = "Parameter", @@ -296,6 +293,7 @@ namespace CubismLive2DExtractor Ids = lipSyncParameters.ToArray() }); + //model var model3 = new CubismModel3Json { Version = 3,