Some fixes for Live2D export

This commit is contained in:
VaDiM 2023-11-08 01:25:32 +03:00
parent 45bf9251c9
commit e415740373
3 changed files with 143 additions and 100 deletions

View File

@ -624,6 +624,7 @@ namespace AssetStudioCLI
} }
return false; return false;
}).Select(x => x.Asset).ToArray(); }).Select(x => x.Asset).ToArray();
if (cubismMocs.Length == 0) if (cubismMocs.Length == 0)
{ {
Logger.Default.Log(LoggerEvent.Info, "Live2D Cubism models were not found.", ignoreLevel: true); Logger.Default.Log(LoggerEvent.Info, "Live2D Cubism models were not found.", ignoreLevel: true);
@ -631,7 +632,18 @@ namespace AssetStudioCLI
} }
if (cubismMocs.Length > 1) 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) if (basePathSet.Count != cubismMocs.Length)
{ {
@ -639,9 +651,16 @@ namespace AssetStudioCLI
Logger.Debug($"useFullContainerPath: {useFullContainerPath}"); Logger.Debug($"useFullContainerPath: {useFullContainerPath}");
} }
} }
var basePathList = useFullContainerPath ?
cubismMocs.Select(x => containers[x]).ToList() : var basePathList = cubismMocs.Select(x =>
cubismMocs.Select(x => containers[x].Substring(0, containers[x].LastIndexOf("/"))).ToList(); {
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( var lookup = containers.ToLookup(
x => basePathList.Find(b => x.Value.Contains(b) && x.Value.Split('/').Any(y => y == b.Substring(b.LastIndexOf("/") + 1))), x => basePathList.Find(b => x.Value.Contains(b) && x.Value.Split('/').Any(y => y == b.Substring(b.LastIndexOf("/") + 1))),
x => x.Key x => x.Key
@ -649,16 +668,15 @@ namespace AssetStudioCLI
var totalModelCount = lookup.LongCount(x => x.Key != null); var totalModelCount = lookup.LongCount(x => x.Key != null);
Logger.Info($"Found {totalModelCount} model(s)."); Logger.Info($"Found {totalModelCount} model(s).");
var name = "";
var modelCounter = 0; var modelCounter = 0;
foreach (var assets in lookup) foreach (var assets in lookup)
{ {
var container = assets.Key; var srcContainer = assets.Key;
if (container == null) if (srcContainer == null)
continue; 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 try
{ {
var modelName = useFullContainerPath ? Path.GetFileNameWithoutExtension(container) : container.Substring(container.LastIndexOf('/') + 1); var modelName = useFullContainerPath ? Path.GetFileNameWithoutExtension(container) : container.Substring(container.LastIndexOf('/') + 1);
@ -670,10 +688,11 @@ namespace AssetStudioCLI
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.Error($"Live2D model export error: \"{name}\"", ex); Logger.Error($"Live2D model export error: \"{srcContainer}\"", ex);
} }
Progress.Report(modelCounter, (int)totalModelCount); Progress.Report(modelCounter, (int)totalModelCount);
} }
var status = modelCounter > 0 ? var status = modelCounter > 0 ?
$"Finished exporting [{modelCounter}/{totalModelCount}] Live2D model(s) to \"{CLIOptions.o_outputFolder.Value.Color(Ansi.BrightCyan)}\"" : $"Finished exporting [{modelCounter}/{totalModelCount}] Live2D model(s) to \"{CLIOptions.o_outputFolder.Value.Color(Ansi.BrightCyan)}\"" :
"Nothing exported."; "Nothing exported.";

View File

@ -259,6 +259,7 @@ namespace AssetStudioGUI
Progress.Report(++i, objectCount); Progress.Report(++i, objectCount);
} }
} }
allContainers.Clear();
foreach ((var pptr, var container) in containers) foreach ((var pptr, var container) in containers)
{ {
if (pptr.TryGet(out var obj)) if (pptr.TryGet(out var obj))
@ -753,32 +754,51 @@ namespace AssetStudioGUI
var useFullContainerPath = false; var useFullContainerPath = false;
if (cubismMocs.Length > 1) 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) if (basePathSet.Count != cubismMocs.Length)
{ {
useFullContainerPath = true; useFullContainerPath = true;
} }
} }
var basePathList = useFullContainerPath ?
cubismMocs.Select(x => allContainers[x]).ToList() : var basePathList = cubismMocs.Select(x =>
cubismMocs.Select(x => allContainers[x].Substring(0, allContainers[x].LastIndexOf("/"))).ToList(); {
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( var lookup = allContainers.ToLookup(
x => basePathList.Find(b => x.Value.Contains(b) && x.Value.Split('/').Any(y => y == b.Substring(b.LastIndexOf("/") + 1))), x => basePathList.Find(b => x.Value.Contains(b) && x.Value.Split('/').Any(y => y == b.Substring(b.LastIndexOf("/") + 1))),
x => x.Key x => x.Key
); );
var totalModelCount = lookup.LongCount(x => x.Key != null); var totalModelCount = lookup.LongCount(x => x.Key != null);
var name = "";
var modelCounter = 0; var modelCounter = 0;
foreach (var assets in lookup) foreach (var assets in lookup)
{ {
var container = assets.Key; var srcContainer = assets.Key;
if (container == null) if (srcContainer == null)
continue; continue;
name = container; var container = srcContainer;
Logger.Info($"[{modelCounter + 1}/{totalModelCount}] Exporting Live2D: \"{container}\"..."); Logger.Info($"[{modelCounter + 1}/{totalModelCount}] Exporting Live2D: \"{srcContainer}\"...");
try try
{ {
var modelName = useFullContainerPath ? Path.GetFileNameWithoutExtension(container) : container.Substring(container.LastIndexOf('/') + 1); var modelName = useFullContainerPath ? Path.GetFileNameWithoutExtension(container) : container.Substring(container.LastIndexOf('/') + 1);
@ -790,11 +810,17 @@ namespace AssetStudioGUI
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.Error($"Live2D model export error: \"{name}\"", ex); Logger.Error($"Live2D model export error: \"{srcContainer}\"", ex);
} }
Progress.Report(modelCounter, (int)totalModelCount); Progress.Report(modelCounter, (int)totalModelCount);
} }
Logger.Info($"Finished exporting [{modelCounter}/{totalModelCount}] Live2D model(s)."); 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) if (Properties.Settings.Default.openAfterExport && modelCounter > 0)
{ {
OpenFolderInExplorer(exportPath); OpenFolderInExplorer(exportPath);

View File

@ -26,20 +26,57 @@ namespace CubismLive2DExtractor
Directory.CreateDirectory(destPath); Directory.CreateDirectory(destPath);
Directory.CreateDirectory(destTexturePath); Directory.CreateDirectory(destTexturePath);
var monoBehaviours = new List<MonoBehaviour>(); var expressionList = new List<MonoBehaviour>();
var texture2Ds = new List<Texture2D>();
var gameObjects = new List<GameObject>(); var gameObjects = new List<GameObject>();
var animationClips = new List<AnimationClip>(); var animationClips = new List<AnimationClip>();
var textures = new SortedSet<string>();
var eyeBlinkParameters = new HashSet<string>();
var lipSyncParameters = new HashSet<string>();
MonoBehaviour physics = null;
foreach (var asset in assets) foreach (var asset in assets)
{ {
switch (asset) switch (asset)
{ {
case MonoBehaviour m_MonoBehaviour: 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; break;
case Texture2D m_Texture2D: 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; break;
case GameObject m_GameObject: case GameObject m_GameObject:
gameObjects.Add(m_GameObject); gameObjects.Add(m_GameObject);
@ -50,15 +87,12 @@ namespace CubismLive2DExtractor
} }
} }
//physics if (textures.Count == 0)
var physics = monoBehaviours.FirstOrDefault(x =>
{ {
if (x.m_Script.TryGet(out var m_Script)) Logger.Warning($"No textures found for \"{modelName}\" model.");
{ }
return m_Script.m_ClassName == "CubismPhysicsController";
} //physics
return false;
});
if (physics != null) if (physics != null)
{ {
try 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<string>();
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 //motion
var motions = new SortedDictionary<string, JArray>(); var motions = new SortedDictionary<string, JArray>();
@ -205,23 +214,30 @@ namespace CubismLive2DExtractor
} }
json.Meta.TotalUserDataSize = totalUserDataSize; json.Meta.TotalUserDataSize = totalUserDataSize;
var motionPath = new JObject(new JProperty("File", $"motions/{animation.Name}.motion3.json")); var animName = animation.Name;
motions.Add(animation.Name, new JArray(motionPath)); if (motions.ContainsKey(animName))
File.WriteAllText($"{destMotionPath}{animation.Name}.motion3.json", JsonConvert.SerializeObject(json, Formatting.Indented, new MyJsonConverter())); {
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 //expression
var expressions = new JArray(); var expressions = new JArray();
var monoBehaviourArray = monoBehaviours.Where(x => x.m_Name.EndsWith(".exp3")).ToArray(); if (expressionList.Count > 0)
if (monoBehaviourArray.Length > 0)
{ {
Directory.CreateDirectory(destExpressionPath); Directory.CreateDirectory(destExpressionPath);
} }
foreach (var monoBehaviour in monoBehaviourArray) foreach (var monoBehaviour in expressionList)
{ {
var fullName = monoBehaviour.m_Name; var expressionName = monoBehaviour.m_Name.Replace(".exp3", "");
var expressionName = fullName.Replace(".exp3", "");
var expressionObj = monoBehaviour.ToType(); var expressionObj = monoBehaviour.ToType();
if (expressionObj == null) if (expressionObj == null)
{ {
@ -238,57 +254,38 @@ namespace CubismLive2DExtractor
expressions.Add(new JObject expressions.Add(new JObject
{ {
{ "Name", expressionName }, { "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<CubismModel3Json.SerializableGroup>(); var groups = new List<CubismModel3Json.SerializableGroup>();
var eyeBlinkParameters = monoBehaviours.Where(x => //Try looking for group IDs among the gameObjects
{
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();
if (eyeBlinkParameters.Count == 0) if (eyeBlinkParameters.Count == 0)
{ {
eyeBlinkParameters = gameObjects.Where(x => eyeBlinkParameters = gameObjects.Where(x =>
{ x.m_Name.ToLower().Contains("eye")
return x.m_Name.ToLower().Contains("eye")
&& x.m_Name.ToLower().Contains("open") && x.m_Name.ToLower().Contains("open")
&& (x.m_Name.ToLower().Contains('l') || x.m_Name.ToLower().Contains('r')); && (x.m_Name.ToLower().Contains('l') || x.m_Name.ToLower().Contains('r'))
}).Select(x => x.m_Name).ToHashSet(); ).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 groups.Add(new CubismModel3Json.SerializableGroup
{ {
Target = "Parameter", Target = "Parameter",
Name = "EyeBlink", Name = "EyeBlink",
Ids = eyeBlinkParameters.ToArray() 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 groups.Add(new CubismModel3Json.SerializableGroup
{ {
Target = "Parameter", Target = "Parameter",
@ -296,6 +293,7 @@ namespace CubismLive2DExtractor
Ids = lipSyncParameters.ToArray() Ids = lipSyncParameters.ToArray()
}); });
//model
var model3 = new CubismModel3Json var model3 = new CubismModel3Json
{ {
Version = 3, Version = 3,