Skip to content

Commit 1cdb0b7

Browse files
committed
Some fixes and improvements for Live2D export
- Fixed l2d model export for bundles with multiple models inside - Added support of grouping exported models by model name
1 parent 02f64f3 commit 1cdb0b7

File tree

7 files changed

+233
-222
lines changed

7 files changed

+233
-222
lines changed

AssetStudioCLI/Options/CLIOptions.cs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -215,13 +215,13 @@ private static void InitOptions()
215215
optionDefaultValue: AssetGroupOption.ContainerPath,
216216
optionName: "-g, --group-option <value>",
217217
optionDescription: "Specify the way in which exported assets should be grouped\n" +
218-
"<Value: none | type | container(default) | containerFull | filename | sceneHierarchy>\n" +
218+
"<Value: none | type | container(default) | containerFull | fileName | sceneHierarchy>\n" +
219219
"None - Do not group exported assets\n" +
220220
"Type - Group exported assets by type name\n" +
221221
"Container - Group exported assets by container path\n" +
222222
"ContainerFull - Group exported assets by full container path (e.g. with prefab name)\n" +
223223
"SceneHierarchy - Group exported assets by their node path in scene hierarchy\n" +
224-
"Filename - Group exported assets by source file name\n",
224+
"FileName - Group exported assets by source file name\n",
225225
optionExample: "Example: \"-g containerFull\"\n",
226226
optionHelpGroup: HelpGroups.General
227227
);
@@ -307,10 +307,11 @@ private static void InitOptions()
307307
optionDefaultValue: CubismLive2DExtractor.Live2DModelGroupOption.ContainerPath,
308308
optionName: "--l2d-group-option <value>",
309309
optionDescription: "Specify the way in which exported models should be grouped\n" +
310-
"<Value: container(default) | filename >\n" +
310+
"<Value: container(default) | fileName | modelName>\n" +
311311
"Container - Group exported models by container path\n" +
312-
"Filename - Group exported models by source file name\n",
313-
optionExample: "Example: \"--l2d-group-option filename\"\n",
312+
"FileName - Group exported models by source file name\n" +
313+
"ModelName - Group exported models by model name\n",
314+
optionExample: "Example: \"--l2d-group-option modelName\"\n",
314315
optionHelpGroup: HelpGroups.Live2D
315316
);
316317
o_l2dMotionMode = new GroupedOption<CubismLive2DExtractor.Live2DMotionMode>
@@ -331,7 +332,8 @@ private static void InitOptions()
331332
optionName: "--l2d-search-by-filename",
332333
optionDescription: "(Flag) If specified, Studio will search for model-related Live2D assets by file name\n" +
333334
"rather than by container\n" +
334-
"(Preferred option when all model-related assets are stored in a single file)\n",
335+
"(Preferred option if all l2d assets of a single model are stored in a single file\n" +
336+
"or containers are obfuscated)\n",
335337
optionExample: "",
336338
optionHelpGroup: HelpGroups.Live2D,
337339
isFlag: true
@@ -880,6 +882,9 @@ public static void ParseArgs(string[] args)
880882
case "filename":
881883
o_l2dGroupOption.Value = CubismLive2DExtractor.Live2DModelGroupOption.SourceFileName;
882884
break;
885+
case "modelname":
886+
o_l2dGroupOption.Value = CubismLive2DExtractor.Live2DModelGroupOption.ModelName;
887+
break;
883888
default:
884889
Console.WriteLine($"{"Error".Color(brightRed)} during parsing [{option.Color(brightYellow)}] option. Unsupported model grouping option: [{value.Color(brightRed)}].\n");
885890
ShowOptionDescription(o_l2dGroupOption);
@@ -1255,7 +1260,7 @@ public static void ShowCurrentOptions()
12551260
else
12561261
{
12571262
sb.AppendLine($"# Model Group Option: {o_l2dGroupOption}");
1258-
sb.AppendFormat("# Search model-related assets by: {0}", f_l2dAssetSearchByFilename.Value ? "Filename" : "Container");
1263+
sb.AppendFormat("# Search model-related assets by: {0}\n", f_l2dAssetSearchByFilename.Value ? "FileName" : "Container");
12591264
sb.AppendLine($"# Motion Export Method: {o_l2dMotionMode}");
12601265
sb.AppendLine($"# Force Bezier: {f_l2dForceBezier }");
12611266
sb.AppendLine($"# Assembly Path: \"{o_assemblyPath}\"");

AssetStudioCLI/Studio.cs

Lines changed: 77 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ public static void ParseAssets()
6767
var objectCount = assetsManager.assetsFileList.Sum(x => x.Objects.Count);
6868
var objectAssetItemDic = new Dictionary<AssetStudio.Object, AssetItem>(objectCount);
6969
var isL2dMode = CLIOptions.o_workMode.Value == WorkMode.Live2D;
70-
var l2dSearchByFilename = CLIOptions.f_l2dAssetSearchByFilename.Value;
7170

7271
Progress.Reset();
7372
var i = 0;
@@ -175,10 +174,6 @@ public static void ParseAssets()
175174
if (m_GameObject.CubismModel != null && TryGetCubismMoc(m_GameObject.CubismModel.CubismModelMono, out var mocMono))
176175
{
177176
l2dModelDict[mocMono] = m_GameObject.CubismModel;
178-
if (!m_GameObject.CubismModel.IsRoot)
179-
{
180-
FixCubismModelName(m_GameObject);
181-
}
182177
}
183178
break;
184179
case Animator m_Animator:
@@ -210,9 +205,7 @@ public static void ParseAssets()
210205
{
211206
if (containers.TryGetValue(asset.Asset, out var container))
212207
{
213-
asset.Container = isL2dMode && l2dSearchByFilename
214-
? Path.GetFileName(asset.Asset.assetsFile.originalPath)
215-
: container;
208+
asset.Container = container;
216209

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

781-
private static void FixCubismModelName(GameObject m_GameObject)
782-
{
783-
var rootTransform = GetRootTransform(m_GameObject.m_Transform);
784-
if (rootTransform.m_GameObject.TryGet(out var rootGameObject))
785-
{
786-
m_GameObject.CubismModel.Name = rootGameObject.m_Name;
787-
}
788-
}
789-
790774
private static void BindCubismRenderer(MonoBehaviour m_MonoBehaviour)
791775
{
792776
if (!m_MonoBehaviour.m_GameObject.TryGet(out var m_GameObject))
@@ -847,58 +831,57 @@ private static Transform GetRootTransform(Transform m_Transform)
847831
return m_Transform;
848832
}
849833

850-
private static List<string> GenerateMocPathList(Dictionary<MonoBehaviour, CubismModel> mocDict, bool searchByFilename, ref bool useFullContainerPath)
834+
private static Dictionary<MonoBehaviour, string> GenerateMocPathDict(Dictionary<MonoBehaviour, CubismModel> mocDict, Dictionary<AssetStudio.Object, string> assetContainers, bool searchByFilename)
851835
{
852-
var mocPathDict = new Dictionary<MonoBehaviour, (string, string)>();
853-
var mocPathList = new List<string>();
836+
var tempMocPathDict = new Dictionary<MonoBehaviour, (string, string)>();
837+
var mocPathDict = new Dictionary<MonoBehaviour, string>();
854838
foreach (var mocMono in l2dModelDict.Keys)
855839
{
856-
if (!containers.TryGetValue(mocMono, out var containerPath))
840+
if (!containers.TryGetValue(mocMono, out var fullContainerPath))
857841
continue;
858-
var fullContainerPath = searchByFilename
859-
? l2dModelDict[mocMono]?.Container ?? containerPath
860-
: containerPath;
861842
var pathSepIndex = fullContainerPath.LastIndexOf('/');
862843
var basePath = pathSepIndex > 0
863844
? fullContainerPath.Substring(0, pathSepIndex)
864845
: fullContainerPath;
865-
mocPathDict.Add(mocMono, (fullContainerPath, basePath));
846+
tempMocPathDict.Add(mocMono, (fullContainerPath, basePath));
866847
}
867848

868-
if (mocPathDict.Count > 0)
849+
if (tempMocPathDict.Count > 0)
869850
{
870-
var basePathSet = mocPathDict.Values.Select(x => x.Item2).ToHashSet();
871-
useFullContainerPath = mocPathDict.Count != basePathSet.Count;
851+
var basePathSet = tempMocPathDict.Values.Select(x => x.Item2).ToHashSet();
852+
var useFullContainerPath = tempMocPathDict.Count != basePathSet.Count;
872853
foreach (var moc in mocDict.Keys)
873854
{
874855
var mocPath = useFullContainerPath
875-
? mocPathDict[moc].Item1 //fullContainerPath
876-
: mocPathDict[moc].Item2; //basePath
856+
? tempMocPathDict[moc].Item1 //fullContainerPath
857+
: tempMocPathDict[moc].Item2; //basePath
877858
if (searchByFilename)
878859
{
879-
mocPathList.Add(containers[moc]);
860+
mocPathDict.Add(moc, assetContainers[moc]);
880861
if (mocDict.TryGetValue(moc, out var model) && model != null)
881862
model.Container = mocPath;
882863
}
883864
else
884865
{
885-
mocPathList.Add(mocPath);
866+
mocPathDict.Add(moc, mocPath);
886867
}
887868
}
888-
mocPathDict.Clear();
869+
tempMocPathDict.Clear();
889870
}
890-
return mocPathList;
871+
return mocPathDict;
891872
}
892873

893874
public static void ExportLive2D()
894875
{
895876
var baseDestPath = Path.Combine(CLIOptions.o_outputFolder.Value, "Live2DOutput");
896-
var useFullContainerPath = true;
897877
var motionMode = CLIOptions.o_l2dMotionMode.Value;
898878
var forceBezier = CLIOptions.f_l2dForceBezier.Value;
899879
var modelGroupOption = CLIOptions.o_l2dGroupOption.Value;
900880
var searchByFilename = CLIOptions.f_l2dAssetSearchByFilename.Value;
901881
var mocDict = l2dModelDict; //TODO: filter by name
882+
var l2dContainers = searchByFilename
883+
? new Dictionary<AssetStudio.Object, string>()
884+
: containers;
902885

903886
if (l2dModelDict.Count == 0)
904887
{
@@ -907,41 +890,50 @@ public static void ExportLive2D()
907890
}
908891

909892
Progress.Reset();
910-
Logger.Info($"Searching for Live2D files...");
893+
Logger.Info("Searching for Live2D files...");
911894

912-
var mocPathList = GenerateMocPathList(mocDict, searchByFilename, ref useFullContainerPath);
895+
if (searchByFilename)
896+
{
897+
foreach (var assetKvp in containers)
898+
{
899+
l2dContainers[assetKvp.Key] = Path.GetFileName(assetKvp.Key.assetsFile.originalPath);
900+
}
901+
}
902+
var mocPathDict = GenerateMocPathDict(mocDict, l2dContainers, searchByFilename);
913903

914-
#if NET9_0_OR_GREATER
915-
var assetDict = new Dictionary<string, List<AssetStudio.Object>>();
916-
foreach (var (asset, container) in containers)
904+
var assetDict = new Dictionary<MonoBehaviour, List<AssetStudio.Object>>();
905+
foreach (var mocKvp in mocPathDict)
906+
{
907+
var mocPath = mocKvp.Value;
908+
var result = l2dContainers.Select(assetKvp =>
917909
{
918-
var result = mocPathList.Find(mocPath =>
919-
{
920-
if (!container.Contains(mocPath))
921-
return false;
922-
var mocPathSpan = mocPath.AsSpan();
923-
var mocPathLastSlice = mocPathSpan[(mocPathSpan.LastIndexOf('/') + 1)..];
924-
foreach (var range in container.AsSpan().Split('/'))
925-
{
926-
if (mocPathLastSlice.SequenceEqual(container.AsSpan()[range]))
927-
return true;
928-
}
929-
return false;
930-
});
931-
if (result != null)
910+
if (!assetKvp.Value.Contains(mocPath))
911+
return null;
912+
var mocPathSpan = mocPath.AsSpan();
913+
var modelNameFromPath = mocPathSpan.Slice(mocPathSpan.LastIndexOf('/') + 1);
914+
#if NET9_0_OR_GREATER
915+
foreach (var range in assetKvp.Value.AsSpan().Split('/'))
932916
{
933-
if (assetDict.TryGetValue(result, out var assets))
934-
assets.Add(asset);
935-
else
936-
assetDict[result] = [asset];
917+
if (modelNameFromPath.SequenceEqual(assetKvp.Value.AsSpan()[range]))
918+
return assetKvp.Key;
937919
}
938-
}
939920
#else
940-
var assetDict = containers.AsParallel().ToLookup(
941-
x => mocPathList.Find(b => x.Value.Contains(b) && x.Value.Split('/').Any(y => y == b.Substring(b.LastIndexOf("/") + 1))),
942-
x => x.Key
943-
).Where(x => x.Key != null).ToDictionary(x => x.Key, x => x.ToList());
921+
foreach (var str in assetKvp.Value.Split('/'))
922+
{
923+
if (modelNameFromPath.SequenceEqual(str.AsSpan()))
924+
return assetKvp.Key;
925+
}
944926
#endif
927+
return null;
928+
}).Where(x => x != null).ToList();
929+
930+
if (result.Count > 0)
931+
{
932+
assetDict[mocKvp.Key] = result;
933+
}
934+
}
935+
if (searchByFilename)
936+
l2dContainers.Clear();
945937
if (mocDict.Keys.First().serializedType?.m_Type == null && CLIOptions.o_assemblyPath.Value == "")
946938
{
947939
Logger.Warning("Specifying the assembly folder may be needed for proper extraction");
@@ -953,28 +945,34 @@ public static void ExportLive2D()
953945
var modelCounter = 0;
954946
Live2DExtractor.MocDict = mocDict;
955947
Live2DExtractor.Assembly = assemblyLoader;
956-
foreach (var assetKvp in assetDict)
948+
foreach (var assetGroupKvp in assetDict)
957949
{
958-
var srcContainer = assetKvp.Key;
950+
var srcContainer = containers[assetGroupKvp.Key];
959951

960952
Logger.Info($"[{modelCounter + 1}/{totalModelCount}] Exporting Live2D: \"{srcContainer.Color(Ansi.BrightCyan)}\"");
961953
try
962954
{
963-
var cubismExtractor = new Live2DExtractor(assetKvp.Value);
955+
var cubismExtractor = new Live2DExtractor(assetGroupKvp);
964956
string modelPath;
965-
if (modelGroupOption == Live2DModelGroupOption.SourceFileName)
966-
{
967-
modelPath = Path.GetFileNameWithoutExtension(cubismExtractor.MocMono.assetsFile.originalPath);
968-
}
969-
else
970-
{
971-
var container = searchByFilename && cubismExtractor.Model != null
972-
? cubismExtractor.Model.Container
973-
: srcContainer;
974-
modelPath = Path.HasExtension(container)
975-
? container.Replace(Path.GetExtension(container), "")
976-
: container;
977-
}
957+
switch (modelGroupOption)
958+
{
959+
case Live2DModelGroupOption.SourceFileName:
960+
modelPath = Path.GetFileNameWithoutExtension(cubismExtractor.MocMono.assetsFile.originalPath);
961+
break;
962+
case Live2DModelGroupOption.ModelName:
963+
modelPath = !string.IsNullOrEmpty(cubismExtractor.Model?.Name)
964+
? cubismExtractor.Model.Name
965+
: Path.GetFileNameWithoutExtension(cubismExtractor.MocMono.assetsFile.originalPath);
966+
break;
967+
default: //ContainerPath
968+
var container = searchByFilename && cubismExtractor.Model != null
969+
? cubismExtractor.Model.Container
970+
: srcContainer;
971+
modelPath = Path.HasExtension(container)
972+
? container.Replace(Path.GetExtension(container), "")
973+
: container;
974+
break;
975+
}
978976

979977
var destPath = Path.Combine(baseDestPath, modelPath) + Path.DirectorySeparatorChar;
980978
cubismExtractor.ExtractCubismModel(destPath, motionMode, forceBezier, parallelTaskCount);

AssetStudioGUI/AssetStudioGUIForm.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,6 +1216,10 @@ private void PreviewMoc(AssetItem assetItem, MonoBehaviour m_MonoBehaviour)
12161216
using (var cubismMoc = new CubismMoc(m_MonoBehaviour))
12171217
{
12181218
var sb = new StringBuilder();
1219+
if (Studio.l2dModelDict.TryGetValue(m_MonoBehaviour, out var model) && model != null)
1220+
{
1221+
sb.AppendLine($"Model Name: {model.Name}");
1222+
}
12191223
sb.AppendLine($"SDK Version: {cubismMoc.VersionDescription}");
12201224
if (cubismMoc.Version > 0)
12211225
{
@@ -1512,11 +1516,11 @@ private void StatusStripUpdate(string statusText)
15121516
private void ResetForm()
15131517
{
15141518
Text = guiTitle;
1515-
assetsManager.Clear();
1516-
assemblyLoader.Clear();
1517-
exportableAssets.Clear();
1518-
visibleAssets.Clear();
1519-
l2dModelDict.Clear();
1519+
Studio.assetsManager.Clear();
1520+
Studio.assemblyLoader.Clear();
1521+
Studio.exportableAssets.Clear();
1522+
Studio.visibleAssets.Clear();
1523+
Studio.l2dModelDict.Clear();
15201524
sceneTreeView.Nodes.Clear();
15211525
assetListView.VirtualListSize = 0;
15221526
assetListView.Items.Clear();

AssetStudioGUI/ExportOptions.Designer.cs

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)