Skip to content

Commit

Permalink
Some fixes and improvements for Live2D export
Browse files Browse the repository at this point in the history
- Fixed l2d model export for bundles with multiple models inside
- Added support of grouping exported models by model name
  • Loading branch information
aelurum committed Jan 15, 2025
1 parent 02f64f3 commit 1cdb0b7
Show file tree
Hide file tree
Showing 7 changed files with 233 additions and 222 deletions.
19 changes: 12 additions & 7 deletions AssetStudioCLI/Options/CLIOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,13 +215,13 @@ private static void InitOptions()
optionDefaultValue: AssetGroupOption.ContainerPath,
optionName: "-g, --group-option <value>",
optionDescription: "Specify the way in which exported assets should be grouped\n" +
"<Value: none | type | container(default) | containerFull | filename | sceneHierarchy>\n" +
"<Value: none | type | container(default) | containerFull | fileName | sceneHierarchy>\n" +
"None - Do not group exported assets\n" +
"Type - Group exported assets by type name\n" +
"Container - Group exported assets by container path\n" +
"ContainerFull - Group exported assets by full container path (e.g. with prefab name)\n" +
"SceneHierarchy - Group exported assets by their node path in scene hierarchy\n" +
"Filename - Group exported assets by source file name\n",
"FileName - Group exported assets by source file name\n",
optionExample: "Example: \"-g containerFull\"\n",
optionHelpGroup: HelpGroups.General
);
Expand Down Expand Up @@ -307,10 +307,11 @@ private static void InitOptions()
optionDefaultValue: CubismLive2DExtractor.Live2DModelGroupOption.ContainerPath,
optionName: "--l2d-group-option <value>",
optionDescription: "Specify the way in which exported models should be grouped\n" +
"<Value: container(default) | filename >\n" +
"<Value: container(default) | fileName | modelName>\n" +
"Container - Group exported models by container path\n" +
"Filename - Group exported models by source file name\n",
optionExample: "Example: \"--l2d-group-option filename\"\n",
"FileName - Group exported models by source file name\n" +
"ModelName - Group exported models by model name\n",
optionExample: "Example: \"--l2d-group-option modelName\"\n",
optionHelpGroup: HelpGroups.Live2D
);
o_l2dMotionMode = new GroupedOption<CubismLive2DExtractor.Live2DMotionMode>
Expand All @@ -331,7 +332,8 @@ private static void InitOptions()
optionName: "--l2d-search-by-filename",
optionDescription: "(Flag) If specified, Studio will search for model-related Live2D assets by file name\n" +
"rather than by container\n" +
"(Preferred option when all model-related assets are stored in a single file)\n",
"(Preferred option if all l2d assets of a single model are stored in a single file\n" +
"or containers are obfuscated)\n",
optionExample: "",
optionHelpGroup: HelpGroups.Live2D,
isFlag: true
Expand Down Expand Up @@ -880,6 +882,9 @@ public static void ParseArgs(string[] args)
case "filename":
o_l2dGroupOption.Value = CubismLive2DExtractor.Live2DModelGroupOption.SourceFileName;
break;
case "modelname":
o_l2dGroupOption.Value = CubismLive2DExtractor.Live2DModelGroupOption.ModelName;
break;
default:
Console.WriteLine($"{"Error".Color(brightRed)} during parsing [{option.Color(brightYellow)}] option. Unsupported model grouping option: [{value.Color(brightRed)}].\n");
ShowOptionDescription(o_l2dGroupOption);
Expand Down Expand Up @@ -1255,7 +1260,7 @@ public static void ShowCurrentOptions()
else
{
sb.AppendLine($"# Model Group Option: {o_l2dGroupOption}");
sb.AppendFormat("# Search model-related assets by: {0}", f_l2dAssetSearchByFilename.Value ? "Filename" : "Container");
sb.AppendFormat("# Search model-related assets by: {0}\n", f_l2dAssetSearchByFilename.Value ? "FileName" : "Container");
sb.AppendLine($"# Motion Export Method: {o_l2dMotionMode}");
sb.AppendLine($"# Force Bezier: {f_l2dForceBezier }");
sb.AppendLine($"# Assembly Path: \"{o_assemblyPath}\"");
Expand Down
156 changes: 77 additions & 79 deletions AssetStudioCLI/Studio.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ public static void ParseAssets()
var objectCount = assetsManager.assetsFileList.Sum(x => x.Objects.Count);
var objectAssetItemDic = new Dictionary<AssetStudio.Object, AssetItem>(objectCount);
var isL2dMode = CLIOptions.o_workMode.Value == WorkMode.Live2D;
var l2dSearchByFilename = CLIOptions.f_l2dAssetSearchByFilename.Value;

Progress.Reset();
var i = 0;
Expand Down Expand Up @@ -175,10 +174,6 @@ public static void ParseAssets()
if (m_GameObject.CubismModel != null && TryGetCubismMoc(m_GameObject.CubismModel.CubismModelMono, out var mocMono))
{
l2dModelDict[mocMono] = m_GameObject.CubismModel;
if (!m_GameObject.CubismModel.IsRoot)
{
FixCubismModelName(m_GameObject);
}
}
break;
case Animator m_Animator:
Expand Down Expand Up @@ -210,9 +205,7 @@ public static void ParseAssets()
{
if (containers.TryGetValue(asset.Asset, out var container))
{
asset.Container = isL2dMode && l2dSearchByFilename
? Path.GetFileName(asset.Asset.assetsFile.originalPath)
: container;
asset.Container = container;

if (asset.Asset is GameObject m_GameObject && m_GameObject.CubismModel != null)
{
Expand Down Expand Up @@ -778,15 +771,6 @@ private static bool TryGetCubismMoc(MonoBehaviour m_MonoBehaviour, out MonoBehav
return mocPPtr.TryGet(out mocMono);
}

private static void FixCubismModelName(GameObject m_GameObject)
{
var rootTransform = GetRootTransform(m_GameObject.m_Transform);
if (rootTransform.m_GameObject.TryGet(out var rootGameObject))
{
m_GameObject.CubismModel.Name = rootGameObject.m_Name;
}
}

private static void BindCubismRenderer(MonoBehaviour m_MonoBehaviour)
{
if (!m_MonoBehaviour.m_GameObject.TryGet(out var m_GameObject))
Expand Down Expand Up @@ -847,58 +831,57 @@ private static Transform GetRootTransform(Transform m_Transform)
return m_Transform;
}

private static List<string> GenerateMocPathList(Dictionary<MonoBehaviour, CubismModel> mocDict, bool searchByFilename, ref bool useFullContainerPath)
private static Dictionary<MonoBehaviour, string> GenerateMocPathDict(Dictionary<MonoBehaviour, CubismModel> mocDict, Dictionary<AssetStudio.Object, string> assetContainers, bool searchByFilename)
{
var mocPathDict = new Dictionary<MonoBehaviour, (string, string)>();
var mocPathList = new List<string>();
var tempMocPathDict = new Dictionary<MonoBehaviour, (string, string)>();
var mocPathDict = new Dictionary<MonoBehaviour, string>();
foreach (var mocMono in l2dModelDict.Keys)
{
if (!containers.TryGetValue(mocMono, out var containerPath))
if (!containers.TryGetValue(mocMono, out var fullContainerPath))
continue;
var fullContainerPath = searchByFilename
? l2dModelDict[mocMono]?.Container ?? containerPath
: containerPath;
var pathSepIndex = fullContainerPath.LastIndexOf('/');
var basePath = pathSepIndex > 0
? fullContainerPath.Substring(0, pathSepIndex)
: fullContainerPath;
mocPathDict.Add(mocMono, (fullContainerPath, basePath));
tempMocPathDict.Add(mocMono, (fullContainerPath, basePath));
}

if (mocPathDict.Count > 0)
if (tempMocPathDict.Count > 0)
{
var basePathSet = mocPathDict.Values.Select(x => x.Item2).ToHashSet();
useFullContainerPath = mocPathDict.Count != basePathSet.Count;
var basePathSet = tempMocPathDict.Values.Select(x => x.Item2).ToHashSet();
var useFullContainerPath = tempMocPathDict.Count != basePathSet.Count;
foreach (var moc in mocDict.Keys)
{
var mocPath = useFullContainerPath
? mocPathDict[moc].Item1 //fullContainerPath
: mocPathDict[moc].Item2; //basePath
? tempMocPathDict[moc].Item1 //fullContainerPath
: tempMocPathDict[moc].Item2; //basePath
if (searchByFilename)
{
mocPathList.Add(containers[moc]);
mocPathDict.Add(moc, assetContainers[moc]);
if (mocDict.TryGetValue(moc, out var model) && model != null)
model.Container = mocPath;
}
else
{
mocPathList.Add(mocPath);
mocPathDict.Add(moc, mocPath);
}
}
mocPathDict.Clear();
tempMocPathDict.Clear();
}
return mocPathList;
return mocPathDict;
}

public static void ExportLive2D()
{
var baseDestPath = Path.Combine(CLIOptions.o_outputFolder.Value, "Live2DOutput");
var useFullContainerPath = true;
var motionMode = CLIOptions.o_l2dMotionMode.Value;
var forceBezier = CLIOptions.f_l2dForceBezier.Value;
var modelGroupOption = CLIOptions.o_l2dGroupOption.Value;
var searchByFilename = CLIOptions.f_l2dAssetSearchByFilename.Value;
var mocDict = l2dModelDict; //TODO: filter by name
var l2dContainers = searchByFilename
? new Dictionary<AssetStudio.Object, string>()
: containers;

if (l2dModelDict.Count == 0)
{
Expand All @@ -907,41 +890,50 @@ public static void ExportLive2D()
}

Progress.Reset();
Logger.Info($"Searching for Live2D files...");
Logger.Info("Searching for Live2D files...");

var mocPathList = GenerateMocPathList(mocDict, searchByFilename, ref useFullContainerPath);
if (searchByFilename)
{
foreach (var assetKvp in containers)
{
l2dContainers[assetKvp.Key] = Path.GetFileName(assetKvp.Key.assetsFile.originalPath);
}
}
var mocPathDict = GenerateMocPathDict(mocDict, l2dContainers, searchByFilename);

#if NET9_0_OR_GREATER
var assetDict = new Dictionary<string, List<AssetStudio.Object>>();
foreach (var (asset, container) in containers)
var assetDict = new Dictionary<MonoBehaviour, List<AssetStudio.Object>>();
foreach (var mocKvp in mocPathDict)
{
var mocPath = mocKvp.Value;
var result = l2dContainers.Select(assetKvp =>
{
var result = mocPathList.Find(mocPath =>
{
if (!container.Contains(mocPath))
return false;
var mocPathSpan = mocPath.AsSpan();
var mocPathLastSlice = mocPathSpan[(mocPathSpan.LastIndexOf('/') + 1)..];
foreach (var range in container.AsSpan().Split('/'))
{
if (mocPathLastSlice.SequenceEqual(container.AsSpan()[range]))
return true;
}
return false;
});
if (result != null)
if (!assetKvp.Value.Contains(mocPath))
return null;
var mocPathSpan = mocPath.AsSpan();
var modelNameFromPath = mocPathSpan.Slice(mocPathSpan.LastIndexOf('/') + 1);
#if NET9_0_OR_GREATER
foreach (var range in assetKvp.Value.AsSpan().Split('/'))
{
if (assetDict.TryGetValue(result, out var assets))
assets.Add(asset);
else
assetDict[result] = [asset];
if (modelNameFromPath.SequenceEqual(assetKvp.Value.AsSpan()[range]))
return assetKvp.Key;
}
}
#else
var assetDict = containers.AsParallel().ToLookup(
x => mocPathList.Find(b => x.Value.Contains(b) && x.Value.Split('/').Any(y => y == b.Substring(b.LastIndexOf("/") + 1))),
x => x.Key
).Where(x => x.Key != null).ToDictionary(x => x.Key, x => x.ToList());
foreach (var str in assetKvp.Value.Split('/'))
{
if (modelNameFromPath.SequenceEqual(str.AsSpan()))
return assetKvp.Key;
}
#endif
return null;
}).Where(x => x != null).ToList();

if (result.Count > 0)
{
assetDict[mocKvp.Key] = result;
}
}
if (searchByFilename)
l2dContainers.Clear();
if (mocDict.Keys.First().serializedType?.m_Type == null && CLIOptions.o_assemblyPath.Value == "")
{
Logger.Warning("Specifying the assembly folder may be needed for proper extraction");
Expand All @@ -953,28 +945,34 @@ public static void ExportLive2D()
var modelCounter = 0;
Live2DExtractor.MocDict = mocDict;
Live2DExtractor.Assembly = assemblyLoader;
foreach (var assetKvp in assetDict)
foreach (var assetGroupKvp in assetDict)
{
var srcContainer = assetKvp.Key;
var srcContainer = containers[assetGroupKvp.Key];

Logger.Info($"[{modelCounter + 1}/{totalModelCount}] Exporting Live2D: \"{srcContainer.Color(Ansi.BrightCyan)}\"");
try
{
var cubismExtractor = new Live2DExtractor(assetKvp.Value);
var cubismExtractor = new Live2DExtractor(assetGroupKvp);
string modelPath;
if (modelGroupOption == Live2DModelGroupOption.SourceFileName)
{
modelPath = Path.GetFileNameWithoutExtension(cubismExtractor.MocMono.assetsFile.originalPath);
}
else
{
var container = searchByFilename && cubismExtractor.Model != null
? cubismExtractor.Model.Container
: srcContainer;
modelPath = Path.HasExtension(container)
? container.Replace(Path.GetExtension(container), "")
: container;
}
switch (modelGroupOption)
{
case Live2DModelGroupOption.SourceFileName:
modelPath = Path.GetFileNameWithoutExtension(cubismExtractor.MocMono.assetsFile.originalPath);
break;
case Live2DModelGroupOption.ModelName:
modelPath = !string.IsNullOrEmpty(cubismExtractor.Model?.Name)
? cubismExtractor.Model.Name
: Path.GetFileNameWithoutExtension(cubismExtractor.MocMono.assetsFile.originalPath);
break;
default: //ContainerPath
var container = searchByFilename && cubismExtractor.Model != null
? cubismExtractor.Model.Container
: srcContainer;
modelPath = Path.HasExtension(container)
? container.Replace(Path.GetExtension(container), "")
: container;
break;
}

var destPath = Path.Combine(baseDestPath, modelPath) + Path.DirectorySeparatorChar;
cubismExtractor.ExtractCubismModel(destPath, motionMode, forceBezier, parallelTaskCount);
Expand Down
14 changes: 9 additions & 5 deletions AssetStudioGUI/AssetStudioGUIForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1216,6 +1216,10 @@ private void PreviewMoc(AssetItem assetItem, MonoBehaviour m_MonoBehaviour)
using (var cubismMoc = new CubismMoc(m_MonoBehaviour))
{
var sb = new StringBuilder();
if (Studio.l2dModelDict.TryGetValue(m_MonoBehaviour, out var model) && model != null)
{
sb.AppendLine($"Model Name: {model.Name}");
}
sb.AppendLine($"SDK Version: {cubismMoc.VersionDescription}");
if (cubismMoc.Version > 0)
{
Expand Down Expand Up @@ -1512,11 +1516,11 @@ private void StatusStripUpdate(string statusText)
private void ResetForm()
{
Text = guiTitle;
assetsManager.Clear();
assemblyLoader.Clear();
exportableAssets.Clear();
visibleAssets.Clear();
l2dModelDict.Clear();
Studio.assetsManager.Clear();
Studio.assemblyLoader.Clear();
Studio.exportableAssets.Clear();
Studio.visibleAssets.Clear();
Studio.l2dModelDict.Clear();
sceneTreeView.Nodes.Clear();
assetListView.VirtualListSize = 0;
assetListView.Items.Clear();
Expand Down
6 changes: 4 additions & 2 deletions AssetStudioGUI/ExportOptions.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 1cdb0b7

Please sign in to comment.