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
This commit is contained in:
VaDiM
2025-01-15 00:46:52 +03:00
parent 02f64f3c97
commit 1cdb0b762a
7 changed files with 239 additions and 228 deletions

View File

@ -1216,6 +1216,10 @@ namespace AssetStudioGUI
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)
{
@ -1512,11 +1516,11 @@ namespace AssetStudioGUI
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();

View File

@ -379,7 +379,8 @@
this.l2dAssetSearchByFilenameCheckBox.Size = new System.Drawing.Size(270, 17);
this.l2dAssetSearchByFilenameCheckBox.TabIndex = 3;
this.l2dAssetSearchByFilenameCheckBox.Text = "Search for model-related Live2D assets by file name";
this.optionTooltip.SetToolTip(this.l2dAssetSearchByFilenameCheckBox, "Preferred option when all model-related assets are stored in a single file");
this.optionTooltip.SetToolTip(this.l2dAssetSearchByFilenameCheckBox, "Preferred option if all l2d assets of a single model are stored in a single file " +
"or containers are obfuscated");
this.l2dAssetSearchByFilenameCheckBox.UseVisualStyleBackColor = true;
//
// l2dModelGroupComboBox
@ -388,7 +389,8 @@
this.l2dModelGroupComboBox.FormattingEnabled = true;
this.l2dModelGroupComboBox.Items.AddRange(new object[] {
"container path",
"source file name"});
"source file name",
"model name"});
this.l2dModelGroupComboBox.Location = new System.Drawing.Point(142, 18);
this.l2dModelGroupComboBox.Name = "l2dModelGroupComboBox";
this.l2dModelGroupComboBox.Size = new System.Drawing.Size(154, 21);

View File

@ -191,7 +191,6 @@ namespace AssetStudioGUI
var objectAssetItemDic = new Dictionary<Object, AssetItem>(objectCount);
var containers = new List<(PPtr<Object>, string)>();
var tex2dArrayAssetList = new List<AssetItem>();
var l2dSearchByFilename = Properties.Settings.Default.l2dAssetSearchByFilename;
l2dAssetContainers.Clear();
var i = 0;
Progress.Reset();
@ -215,10 +214,6 @@ namespace AssetStudioGUI
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 Texture2D m_Texture2D:
@ -359,9 +354,7 @@ namespace AssetStudioGUI
case AnimationClip _:
case Texture2D _:
case MonoBehaviour _:
l2dAssetContainers[obj] = l2dSearchByFilename
? Path.GetFileName(obj.assetsFile.originalPath)
: container;
l2dAssetContainers[obj] = container;
break;
}
}
@ -1012,15 +1005,6 @@ namespace AssetStudioGUI
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))
@ -1081,47 +1065,44 @@ namespace AssetStudioGUI
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<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 (!l2dAssetContainers.TryGetValue(mocMono, out var containerPath))
if (!l2dAssetContainers.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(l2dAssetContainers[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(string exportPath, List<MonoBehaviour> selMocs = null, List<AnimationClip> selClipMotions = null, List<MonoBehaviour> selFadeMotions = null, MonoBehaviour selFadeLst = null)
@ -1138,46 +1119,57 @@ namespace AssetStudioGUI
var mocDict = selMocs != null
? selMocs.ToDictionary(moc => moc, moc => l2dModelDict[moc])
: l2dModelDict;
var l2dContainers = searchByFilename
? new Dictionary<Object, string>()
: l2dAssetContainers;
ThreadPool.QueueUserWorkItem(state =>
{
Logger.Info("Searching for Live2D assets...");
var useFullContainerPath = true;
var mocPathList = GenerateMocPathList(mocDict, searchByFilename, ref useFullContainerPath);
#if NET9_0_OR_GREATER
var assetDict = new Dictionary<string, List<Object>>();
foreach (var (asset, container) in l2dAssetContainers)
if (searchByFilename)
{
var result = mocPathList.Find(mocPath =>
foreach (var assetKvp in l2dAssetContainers)
{
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 (assetDict.TryGetValue(result, out var assets))
assets.Add(asset);
else
assetDict[result] = [asset];
l2dContainers[assetKvp.Key] = Path.GetFileName(assetKvp.Key.assetsFile.originalPath);
}
}
#else
var assetDict = l2dAssetContainers.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());
#endif
var mocPathDict = GenerateMocPathDict(mocDict, l2dContainers, searchByFilename);
var assetDict = new Dictionary<MonoBehaviour, List<Object>>();
foreach (var mocKvp in mocPathDict)
{
var mocPath = mocKvp.Value;
var result = l2dContainers.Select(assetKvp =>
{
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 (modelNameFromPath.SequenceEqual(assetKvp.Value.AsSpan()[range]))
return assetKvp.Key;
}
#else
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 && !assemblyLoader.Loaded)
{
Logger.Warning("Specifying the assembly folder may be needed for proper extraction");
@ -1192,27 +1184,33 @@ namespace AssetStudioGUI
parallelExportCount = Properties.Settings.Default.parallelExport ? parallelExportCount : 1;
Live2DExtractor.MocDict = mocDict;
Live2DExtractor.Assembly = assemblyLoader;
foreach (var assetKvp in assetDict)
foreach (var assetGroupKvp in assetDict)
{
var srcContainer = assetKvp.Key;
var srcContainer = l2dAssetContainers[assetGroupKvp.Key];
Logger.Info($"[{modelCounter + 1}/{totalModelCount}] Exporting Live2D: \"{srcContainer}\"...");
try
{
var cubismExtractor = new Live2DExtractor(assetKvp.Value, selClipMotions, selFadeMotions, selFadeLst);
var cubismExtractor = new Live2DExtractor(assetGroupKvp, selClipMotions, selFadeMotions, selFadeLst);
string modelPath;
if (modelGroupOption == Live2DModelGroupOption.SourceFileName)
switch (modelGroupOption)
{
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;
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;