diff --git a/AssetStudio/AssetsManager.cs b/AssetStudio/AssetsManager.cs index 85b0e57..3c69c19 100644 --- a/AssetStudio/AssetsManager.cs +++ b/AssetStudio/AssetsManager.cs @@ -701,6 +701,34 @@ namespace AssetStudio case Animation m_Animation: m_GameObject.m_Animation = m_Animation; break; + case MonoBehaviour m_MonoBehaviour: + if (m_MonoBehaviour.m_Script.TryGet(out var m_Script)) + { + switch (m_Script.m_ClassName) + { + case "CubismModel": + if (m_GameObject.m_Transform == null) + break; + m_GameObject.CubismModel = new CubismModel(m_GameObject) + { + CubismModelMono = m_MonoBehaviour + }; + break; + case "CubismPhysicsController": + if (m_GameObject.CubismModel != null) + m_GameObject.CubismModel.PhysicsController = m_MonoBehaviour; + break; + case "CubismFadeController": + if (m_GameObject.CubismModel != null) + m_GameObject.CubismModel.FadeController = m_MonoBehaviour; + break; + case "CubismExpressionController": + if (m_GameObject.CubismModel != null) + m_GameObject.CubismModel.ExpressionController = m_MonoBehaviour; + break; + } + } + break; } } } diff --git a/AssetStudio/Classes/Behaviour.cs b/AssetStudio/Classes/Behaviour.cs index 2076202..b619d28 100644 --- a/AssetStudio/Classes/Behaviour.cs +++ b/AssetStudio/Classes/Behaviour.cs @@ -1,14 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace AssetStudio +namespace AssetStudio { public abstract class Behaviour : Component { public byte m_Enabled; + public Behaviour() { } + protected Behaviour(ObjectReader reader) : base(reader) { m_Enabled = reader.ReadByte(); diff --git a/AssetStudio/Classes/Component.cs b/AssetStudio/Classes/Component.cs index 99f30e7..57512bb 100644 --- a/AssetStudio/Classes/Component.cs +++ b/AssetStudio/Classes/Component.cs @@ -1,14 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace AssetStudio +namespace AssetStudio { public abstract class Component : EditorExtension { public PPtr m_GameObject; + public Component() { } + protected Component(ObjectReader reader) : base(reader) { m_GameObject = new PPtr(reader); diff --git a/AssetStudio/Classes/GameObject.cs b/AssetStudio/Classes/GameObject.cs index 1fec233..cf4a761 100644 --- a/AssetStudio/Classes/GameObject.cs +++ b/AssetStudio/Classes/GameObject.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Text.Json.Serialization; namespace AssetStudio { @@ -16,6 +13,8 @@ namespace AssetStudio public SkinnedMeshRenderer m_SkinnedMeshRenderer; public Animator m_Animator; public Animation m_Animation; + [JsonIgnore] + public CubismModel CubismModel; public GameObject(ObjectReader reader) : base(reader) { diff --git a/AssetStudio/Classes/MonoBehaviour.cs b/AssetStudio/Classes/MonoBehaviour.cs index 7657919..2478fe2 100644 --- a/AssetStudio/Classes/MonoBehaviour.cs +++ b/AssetStudio/Classes/MonoBehaviour.cs @@ -1,15 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace AssetStudio +namespace AssetStudio { - public sealed class MonoBehaviour : Behaviour + public class MonoBehaviour : Behaviour { public PPtr m_Script; public string m_Name; + public MonoBehaviour() { } + public MonoBehaviour(ObjectReader reader) : base(reader) { m_Script = new PPtr(reader); diff --git a/AssetStudio/Classes/PPtr.cs b/AssetStudio/Classes/PPtr.cs index 4a56de4..bd973e5 100644 --- a/AssetStudio/Classes/PPtr.cs +++ b/AssetStudio/Classes/PPtr.cs @@ -1,4 +1,5 @@ using System; +using System.Text.Json.Serialization; namespace AssetStudio { @@ -6,10 +7,16 @@ namespace AssetStudio { public int m_FileID; public long m_PathID; - public string Name => TryGet(out var result) ? result.Name : string.Empty; + public string Name => _assetsFile != null && TryGet(out var result) ? result.Name : string.Empty; - private SerializedFile _assetsFile; private int _index = -2; //-2 - Prepare, -1 - Missing + private SerializedFile _assetsFile; + [JsonIgnore] + public SerializedFile AssetsFile + { + get => _assetsFile; + set => _assetsFile = value; + } public PPtr(ObjectReader reader) { @@ -20,11 +27,6 @@ namespace AssetStudio public PPtr() { } - public void SetAssetsFile(SerializedFile assetsFile) - { - _assetsFile = assetsFile; - } - private bool TryGetAssetsFile(out SerializedFile result) { result = null; diff --git a/AssetStudio/CubismMoc.cs b/AssetStudio/CubismMoc.cs new file mode 100644 index 0000000..07c6ed1 --- /dev/null +++ b/AssetStudio/CubismMoc.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using static AssetStudio.EndianSpanReader; + +namespace AssetStudio +{ + public enum CubismSDKVersion : byte + { + V30 = 1, + V33, + V40, + V42, + V50 + } + + public sealed class CubismMoc : IDisposable + { + public CubismSDKVersion Version { get; } + public string VersionDescription { get; } + public float CanvasWidth { get; } + public float CanvasHeight { get; } + public float CentralPosX { get; } + public float CentralPosY { get; } + public float PixelPerUnit { get; } + public uint PartCount { get; } + public uint ParamCount { get; } + public HashSet PartNames { get; } + public HashSet ParamNames { get; } + + private byte[] modelData; + private int modelDataSize; + private bool isBigEndian; + + public CubismMoc(MonoBehaviour moc) + { + var reader = moc.reader; + reader.Reset(); + reader.Position += 28; //PPtr m_GameObject, m_Enabled, PPtr + reader.ReadAlignedString(); //m_Name + modelDataSize = (int)reader.ReadUInt32(); + modelData = BigArrayPool.Shared.Rent(modelDataSize); + _ = reader.Read(modelData, 0, modelDataSize); + + var sdkVer = modelData[4]; + if (Enum.IsDefined(typeof(CubismSDKVersion), sdkVer)) + { + Version = (CubismSDKVersion)sdkVer; + VersionDescription = ParseVersion(); + } + else + { + var msg = $"Unknown SDK version ({sdkVer})"; + VersionDescription = msg; + Version = 0; + Logger.Warning($"Live2D model \"{moc.m_Name}\": " + msg); + return; + } + isBigEndian = BitConverter.ToBoolean(modelData, 5); + + //offsets + var countInfoTableOffset = (int)SpanToUint32(modelData, 64, isBigEndian); + var canvasInfoOffset = (int)SpanToUint32(modelData, 68, isBigEndian); + var partIdsOffset = SpanToUint32(modelData, 76, isBigEndian); + var parameterIdsOffset = SpanToUint32(modelData, 264, isBigEndian); + + //canvas + PixelPerUnit = ToSingle(modelData, canvasInfoOffset, isBigEndian); + CentralPosX = ToSingle(modelData, canvasInfoOffset + 4, isBigEndian); + CentralPosY = ToSingle(modelData, canvasInfoOffset + 8, isBigEndian); + CanvasWidth = ToSingle(modelData, canvasInfoOffset + 12, isBigEndian); + CanvasHeight = ToSingle(modelData, canvasInfoOffset + 16, isBigEndian); + + //model + PartCount = SpanToUint32(modelData, countInfoTableOffset, isBigEndian); + ParamCount = SpanToUint32(modelData, countInfoTableOffset + 20, isBigEndian); + PartNames = ReadMocStringHashSet(modelData, (int)partIdsOffset, (int)PartCount); + ParamNames = ReadMocStringHashSet(modelData, (int)parameterIdsOffset, (int)ParamCount); + } + + public void SaveMoc3(string savePath) + { + if (!savePath.EndsWith(".moc3")) + savePath += ".moc3"; + + using (var file = File.OpenWrite(savePath)) + { + file.Write(modelData, 0, modelDataSize); + } + } + + private string ParseVersion() + { + switch (Version) + { + case CubismSDKVersion.V30: return "SDK3.0/Cubism3.0(3.2)"; + case CubismSDKVersion.V33: return "SDK3.3/Cubism3.3"; + case CubismSDKVersion.V40: return "SDK4.0/Cubism4.0"; + case CubismSDKVersion.V42: return "SDK4.2/Cubism4.2"; + case CubismSDKVersion.V50: return "SDK5.0/Cubism5.0"; + default: return ""; + } + } + + private static float ToSingle(ReadOnlySpan data, int index, bool isBigEndian) //net framework ver + { + var bytes = data.Slice(index, index + 4).ToArray(); + if ((isBigEndian && BitConverter.IsLittleEndian) || (!isBigEndian && !BitConverter.IsLittleEndian)) + (bytes[0], bytes[1], bytes[2], bytes[3]) = (bytes[3], bytes[2], bytes[1], bytes[0]); + + return BitConverter.ToSingle(bytes, 0); + } + + private static HashSet ReadMocStringHashSet(ReadOnlySpan data, int index, int count) + { + const int strLen = 64; + var strHashSet = new HashSet(); + for (var i = 0; i < count; i++) + { + if (index + i * strLen <= data.Length) + { + var buff = data.Slice(index + i * strLen, strLen); + strHashSet.Add(Encoding.UTF8.GetString(buff.ToArray()).TrimEnd('\0')); + } + } + return strHashSet; + } + + private void Dispose(bool disposing) + { + if (disposing) + { + BigArrayPool.Shared.Return(modelData, clearArray: true); + } + } + + public void Dispose() + { + Dispose(true); + } + } +} diff --git a/AssetStudio/CubismModel.cs b/AssetStudio/CubismModel.cs index e5c00ed..c45a3b5 100644 --- a/AssetStudio/CubismModel.cs +++ b/AssetStudio/CubismModel.cs @@ -1,149 +1,31 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using static AssetStudio.EndianSpanReader; +using System.Collections.Generic; namespace AssetStudio { - public enum CubismSDKVersion : byte + public class CubismModel { - V30 = 1, - V33, - V40, - V42, - V50 - } + public string Name { get; set; } + public string Container { get; set; } + public bool IsRoot { get; set; } + public MonoBehaviour CubismModelMono { get; set; } + public MonoBehaviour PhysicsController { get; set; } + public MonoBehaviour FadeController { get; set; } + public MonoBehaviour ExpressionController { get; set; } + public List RenderTextureList { get; set; } + public List ParamDisplayInfoList { get; set; } + public List PartDisplayInfoList { get; set; } + public List PosePartList { get; set; } + public GameObject ModelGameObject { get; set; } - public sealed class CubismModel : IDisposable - { - public CubismSDKVersion Version { get; } - public string VersionDescription { get; } - public float CanvasWidth { get; } - public float CanvasHeight { get; } - public float CentralPosX { get; } - public float CentralPosY { get; } - public float PixelPerUnit { get; } - public uint PartCount { get; } - public uint ParamCount { get; } - public HashSet PartNames { get; } - public HashSet ParamNames { get; } - - private byte[] modelData; - private int modelDataSize; - private bool isBigEndian; - - public CubismModel(MonoBehaviour moc) + public CubismModel(GameObject m_GameObject) { - var reader = moc.reader; - reader.Reset(); - reader.Position += 28; //PPtr m_GameObject, m_Enabled, PPtr - reader.ReadAlignedString(); //m_Name - modelDataSize = (int)reader.ReadUInt32(); - modelData = BigArrayPool.Shared.Rent(modelDataSize); - _ = reader.Read(modelData, 0, modelDataSize); - - var sdkVer = modelData[4]; - if (Enum.IsDefined(typeof(CubismSDKVersion), sdkVer)) - { - Version = (CubismSDKVersion)sdkVer; - VersionDescription = ParseVersion(); - } - else - { - var msg = $"Unknown SDK version ({sdkVer})"; - VersionDescription = msg; - Version = 0; - Logger.Warning($"Live2D model \"{moc.m_Name}\": " + msg); - return; - } - isBigEndian = BitConverter.ToBoolean(modelData, 5); - - //offsets - var countInfoTableOffset = (int)SpanToUint32(modelData, 64, isBigEndian); - var canvasInfoOffset = (int)SpanToUint32(modelData, 68, isBigEndian); - var partIdsOffset = SpanToUint32(modelData, 76, isBigEndian); - var parameterIdsOffset = SpanToUint32(modelData, 264, isBigEndian); - - //canvas - PixelPerUnit = ToSingle(modelData, canvasInfoOffset, isBigEndian); - CentralPosX = ToSingle(modelData, canvasInfoOffset + 4, isBigEndian); - CentralPosY = ToSingle(modelData, canvasInfoOffset + 8, isBigEndian); - CanvasWidth = ToSingle(modelData, canvasInfoOffset + 12, isBigEndian); - CanvasHeight = ToSingle(modelData, canvasInfoOffset + 16, isBigEndian); - - //model - PartCount = SpanToUint32(modelData, countInfoTableOffset, isBigEndian); - ParamCount = SpanToUint32(modelData, countInfoTableOffset + 20, isBigEndian); - PartNames = ReadMocStringHashSet(modelData, (int)partIdsOffset, (int)PartCount); - ParamNames = ReadMocStringHashSet(modelData, (int)parameterIdsOffset, (int)ParamCount); - } - - public void SaveMoc3(string savePath) - { - if (!savePath.EndsWith(".moc3")) - savePath += ".moc3"; - - using (var file = File.OpenWrite(savePath)) - { - file.Write(modelData, 0, modelDataSize); - } - } - - private string ParseVersion() - { - switch (Version) - { - case CubismSDKVersion.V30: - return "SDK3.0/Cubism3.0(3.2)"; - case CubismSDKVersion.V33: - return "SDK3.3/Cubism3.3"; - case CubismSDKVersion.V40: - return "SDK4.0/Cubism4.0"; - case CubismSDKVersion.V42: - return "SDK4.2/Cubism4.2"; - case CubismSDKVersion.V50: - return "SDK5.0/Cubism5.0"; - default: - return ""; - } - } - - private static float ToSingle(ReadOnlySpan data, int index, bool isBigEndian) //net framework ver - { - var bytes = data.Slice(index, index + 4).ToArray(); - if ((isBigEndian && BitConverter.IsLittleEndian) || (!isBigEndian && !BitConverter.IsLittleEndian)) - (bytes[0], bytes[1], bytes[2], bytes[3]) = (bytes[3], bytes[2], bytes[1], bytes[0]); - - return BitConverter.ToSingle(bytes, 0); - } - - private static HashSet ReadMocStringHashSet(ReadOnlySpan data, int index, int count) - { - const int strLen = 64; - var strHashSet = new HashSet(); - for (var i = 0; i < count; i++) - { - if (index + i * strLen <= data.Length) - { - var buff = data.Slice(index + i * strLen, strLen); - strHashSet.Add(Encoding.UTF8.GetString(buff.ToArray()).TrimEnd('\0')); - } - } - return strHashSet; - } - - private void Dispose(bool disposing) - { - if (disposing) - { - BigArrayPool.Shared.Return(modelData, clearArray: true); - } - } - - public void Dispose() - { - Dispose(true); + Name = m_GameObject.m_Name; + IsRoot = m_GameObject.m_Transform.m_Father.IsNull; + ModelGameObject = m_GameObject; + RenderTextureList = new List(); + ParamDisplayInfoList = new List(); + PartDisplayInfoList = new List(); + PosePartList = new List(); } } } diff --git a/AssetStudio/JsonConverterHelpers/PPtrConverter.cs b/AssetStudio/JsonConverterHelpers/PPtrConverter.cs index 9cfadc4..9d5963f 100644 --- a/AssetStudio/JsonConverterHelpers/PPtrConverter.cs +++ b/AssetStudio/JsonConverterHelpers/PPtrConverter.cs @@ -39,7 +39,7 @@ namespace AssetStudio public override PPtr Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var pptrObj = JsonSerializer.Deserialize>(ref reader, new JsonSerializerOptions { IncludeFields = true }); - pptrObj.SetAssetsFile(_assetsFile); + pptrObj.AssetsFile = _assetsFile; return pptrObj; } diff --git a/AssetStudio/TypeTreeHelper.cs b/AssetStudio/TypeTreeHelper.cs index ba40fb1..8d88f60 100644 --- a/AssetStudio/TypeTreeHelper.cs +++ b/AssetStudio/TypeTreeHelper.cs @@ -24,17 +24,26 @@ namespace AssetStudio public static string ReadTypeString(TypeTree m_Type, ObjectReader reader) { reader.Reset(); + var readed = 0L; var sb = new StringBuilder(); var m_Nodes = m_Type.m_Nodes; - for (int i = 0; i < m_Nodes.Count; i++) + try { - ReadStringValue(sb, m_Nodes, reader, ref i); + for (int i = 0; i < m_Nodes.Count; i++) + { + ReadStringValue(sb, m_Nodes, reader, ref i); + } + readed = reader.Position - reader.byteStart; + } + catch (Exception) + { + //Ignore } - var readed = reader.Position - reader.byteStart; if (readed != reader.byteSize) { Logger.Info($"Failed to read type, read {readed} bytes but expected {reader.byteSize} bytes"); } + return sb.ToString(); } diff --git a/AssetStudioCLI/Options/CLIOptions.cs b/AssetStudioCLI/Options/CLIOptions.cs index 4cc5f7d..f30df44 100644 --- a/AssetStudioCLI/Options/CLIOptions.cs +++ b/AssetStudioCLI/Options/CLIOptions.cs @@ -100,6 +100,8 @@ namespace AssetStudioCLI.Options public static Option o_imageFormat; public static Option o_audioFormat; //live2d + public static Option o_l2dGroupOption; + public static Option f_l2dAssetSearchByFilename; public static Option o_l2dMotionMode; public static Option f_l2dForceBezier; //fbx @@ -228,10 +230,10 @@ namespace AssetStudioCLI.Options optionDefaultValue: FilenameFormat.AssetName, optionName: "-f, --filename-format ", optionDescription: "Specify the file name format for exported assets\n" + - "\n" + - "AssetName - Asset file names will look like \"assetName.extension\"\n" + - "AssetName_pathID - Asset file names will look like \"assetName @pathID.extension\"\n" + - "PathID - Asset file names will look like \"pathID.extension\"\n", + "\n" + + "AssetName - Asset file names will look like \"assetName.extension\"\n" + + "AssetName_pathID - Asset file names will look like \"assetName @pathID.extension\"\n" + + "PathID - Asset file names will look like \"pathID.extension\"\n", optionExample: "Example: \"-f assetName_pathID\"\n", optionHelpGroup: HelpGroups.General ); @@ -293,13 +295,24 @@ namespace AssetStudioCLI.Options optionName: "--audio-format ", optionDescription: "Specify the format for converting FMOD audio assets\n" + "\n" + - "None - Do not convert fmod audios and export them in their own format\n", + "None - Do not convert FMOD audios and export them in their own format\n", optionExample: "Example: \"--audio-format wav\"", optionHelpGroup: HelpGroups.Convert ); #endregion #region Init Cubism Live2D Options + o_l2dGroupOption = new GroupedOption + ( + optionDefaultValue: CubismLive2DExtractor.Live2DModelGroupOption.ContainerPath, + optionName: "--l2d-group-option ", + optionDescription: "Specify the way in which exported models should be grouped\n" + + "\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", + optionHelpGroup: HelpGroups.Live2D + ); o_l2dMotionMode = new GroupedOption ( optionDefaultValue: CubismLive2DExtractor.Live2DMotionMode.MonoBehaviour, @@ -312,6 +325,17 @@ namespace AssetStudioCLI.Options optionExample: "Example: \"--l2d-motion-mode animationClip\"\n", optionHelpGroup: HelpGroups.Live2D ); + f_l2dAssetSearchByFilename = new GroupedOption + ( + optionDefaultValue: false, + 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", + optionExample: "", + optionHelpGroup: HelpGroups.Live2D, + isFlag: true + ); f_l2dForceBezier = new GroupedOption ( optionDefaultValue: false, @@ -569,12 +593,22 @@ namespace AssetStudioCLI.Options #endregion #region Parse Flags - for (int i = 0; i < resplittedArgs.Count; i++) + for (var i = 0; i < resplittedArgs.Count; i++) { - string flag = resplittedArgs[i].ToLower(); + var flag = resplittedArgs[i].ToLower(); switch(flag) { + case "--l2d-search-by-filename": + if (o_workMode.Value != WorkMode.Live2D) + { + Console.WriteLine($"{"Error".Color(brightRed)} during parsing [{flag.Color(brightYellow)}] flag. This flag is not suitable for the current working mode [{o_workMode.Value}].\n"); + ShowOptionDescription(o_workMode); + return; + } + f_l2dAssetSearchByFilename.Value = true; + resplittedArgs.RemoveAt(i); + break; case "--l2d-force-bezier": if (o_workMode.Value != WorkMode.Live2D) { @@ -831,6 +865,27 @@ namespace AssetStudioCLI.Options return; } break; + case "--l2d-group-option": + if (o_workMode.Value != WorkMode.Live2D) + { + Console.WriteLine($"{"Error".Color(brightRed)} during parsing [{option.Color(brightYellow)}] option. This option is not suitable for the current working mode [{o_workMode.Value}].\n"); + ShowOptionDescription(o_workMode); + return; + } + switch (value.ToLower()) + { + case "container": + o_l2dGroupOption.Value = CubismLive2DExtractor.Live2DModelGroupOption.ContainerPath; + break; + case "filename": + o_l2dGroupOption.Value = CubismLive2DExtractor.Live2DModelGroupOption.SourceFileName; + break; + default: + Console.WriteLine($"{"Error".Color(brightRed)} during parsing [{option.Color(brightYellow)}] option. Unsupported model grouping option: [{value.Color(brightRed)}].\n"); + ShowOptionDescription(o_l2dGroupOption); + return; + } + break; case "--l2d-motion-mode": if (o_workMode.Value != WorkMode.Live2D) { @@ -1199,7 +1254,9 @@ namespace AssetStudioCLI.Options } else { - sb.AppendLine($"# Live2D Motion Export Method: {o_l2dMotionMode}"); + sb.AppendLine($"# Model Group Option: {o_l2dGroupOption}"); + sb.AppendFormat("# Search model-related assets by: {0}", f_l2dAssetSearchByFilename.Value ? "Filename" : "Container"); + sb.AppendLine($"# Motion Export Method: {o_l2dMotionMode}"); sb.AppendLine($"# Force Bezier: {f_l2dForceBezier }"); sb.AppendLine($"# Assembly Path: \"{o_assemblyPath}\""); } diff --git a/AssetStudioCLI/Studio.cs b/AssetStudioCLI/Studio.cs index 59b754f..2f595bf 100644 --- a/AssetStudioCLI/Studio.cs +++ b/AssetStudioCLI/Studio.cs @@ -4,6 +4,7 @@ using CubismLive2DExtractor; using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Specialized; using System.IO; using System.Linq; using System.Threading; @@ -20,7 +21,7 @@ namespace AssetStudioCLI public static List parsedAssetsList = new List(); public static List gameObjectTree = new List(); public static AssemblyLoader assemblyLoader = new AssemblyLoader(); - public static List cubismMocList = new List(); + public static Dictionary l2dModelDict = new Dictionary(); private static Dictionary containers = new Dictionary(); static Studio() @@ -65,6 +66,8 @@ namespace AssetStudioCLI var tex2dArrayAssetList = new List(); var objectCount = assetsManager.assetsFileList.Sum(x => x.Objects.Count); var objectAssetItemDic = new Dictionary(objectCount); + var isL2dMode = CLIOptions.o_workMode.Value == WorkMode.Live2D; + var l2dSearchByFilename = CLIOptions.f_l2dAssetSearchByFilename.Value; Progress.Reset(); var i = 0; @@ -143,15 +146,40 @@ namespace AssetStudioCLI if (m_MonoBehaviour.m_Script.TryGet(out var m_Script)) { assetName = assetName == "" ? m_Script.m_ClassName : assetName; - if (m_Script.m_ClassName == "CubismMoc") + switch (m_Script.m_ClassName) { - cubismMocList.Add(m_MonoBehaviour); + case "CubismMoc": + if (!l2dModelDict.ContainsKey(m_MonoBehaviour)) + { + l2dModelDict.Add(m_MonoBehaviour, null); + } + break; + case "CubismRenderer": + BindCubismRenderer(m_MonoBehaviour); + break; + case "CubismDisplayInfoParameterName": + BindParamDisplayInfo(m_MonoBehaviour); + break; + case "CubismDisplayInfoPartName": + BindPartDisplayInfo(m_MonoBehaviour); + break; + case "CubismPosePart": + BindCubismPosePart(m_MonoBehaviour); + break; } } assetItem.Text = assetName; break; case GameObject m_GameObject: assetItem.Text = m_GameObject.m_Name; + 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: if (m_Animator.m_GameObject.TryGet(out var gameObject)) @@ -177,11 +205,19 @@ namespace AssetStudioCLI asset.Name = assetItem.Text; Progress.Report(++i, objectCount); } + foreach (var asset in fileAssetsList) { if (containers.TryGetValue(asset.Asset, out var container)) { - asset.Container = container; + asset.Container = isL2dMode && l2dSearchByFilename + ? Path.GetFileName(asset.Asset.assetsFile.originalPath) + : container; + + if (asset.Asset is GameObject m_GameObject && m_GameObject.CubismModel != null) + { + m_GameObject.CubismModel.Container = container; + } } } foreach (var tex2dAssetItem in tex2dArrayAssetList) @@ -196,7 +232,7 @@ namespace AssetStudioCLI parsedAssetsList.AddRange(fileAssetsList); fileAssetsList.Clear(); tex2dArrayAssetList.Clear(); - if (CLIOptions.o_workMode.Value != WorkMode.Live2D) + if (!isL2dMode) { containers.Clear(); } @@ -726,16 +762,145 @@ namespace AssetStudioCLI } } + private static bool TryGetCubismMoc(MonoBehaviour m_MonoBehaviour, out MonoBehaviour mocMono) + { + mocMono = null; + var pptrDict = (OrderedDictionary)CubismParsers.ParseMonoBehaviour(m_MonoBehaviour, CubismParsers.CubismMonoBehaviourType.Model, assemblyLoader)?["_moc"]; + if (pptrDict == null) + return false; + + var mocPPtr = new PPtr + { + m_FileID = (int)pptrDict["m_FileID"], + m_PathID = (long)pptrDict["m_PathID"], + AssetsFile = m_MonoBehaviour.assetsFile + }; + 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)) + return; + + var rootTransform = GetRootTransform(m_GameObject.m_Transform); + if (rootTransform.m_GameObject.TryGet(out var rootGameObject) && rootGameObject.CubismModel != null) + { + rootGameObject.CubismModel.RenderTextureList.Add(m_MonoBehaviour); + } + } + + private static void BindParamDisplayInfo(MonoBehaviour m_MonoBehaviour) + { + if (!m_MonoBehaviour.m_GameObject.TryGet(out var m_GameObject)) + return; + + var rootTransform = GetRootTransform(m_GameObject.m_Transform); + if (rootTransform.m_GameObject.TryGet(out var rootGameObject) && rootGameObject.CubismModel != null) + { + rootGameObject.CubismModel.ParamDisplayInfoList.Add(m_MonoBehaviour); + } + } + + private static void BindPartDisplayInfo(MonoBehaviour m_MonoBehaviour) + { + if (!m_MonoBehaviour.m_GameObject.TryGet(out var m_GameObject)) + return; + + var rootTransform = GetRootTransform(m_GameObject.m_Transform); + if (rootTransform.m_GameObject.TryGet(out var rootGameObject) && rootGameObject.CubismModel != null) + { + rootGameObject.CubismModel.PartDisplayInfoList.Add(m_MonoBehaviour); + } + } + + private static void BindCubismPosePart(MonoBehaviour m_MonoBehaviour) + { + if (!m_MonoBehaviour.m_GameObject.TryGet(out var m_GameObject)) + return; + + var rootTransform = GetRootTransform(m_GameObject.m_Transform); + if (rootTransform.m_GameObject.TryGet(out var rootGameObject) && rootGameObject.CubismModel != null) + { + rootGameObject.CubismModel.PosePartList.Add(m_MonoBehaviour); + } + } + + private static Transform GetRootTransform(Transform m_Transform) + { + if (m_Transform == null) + return null; + + while (m_Transform.m_Father.TryGet(out var m_Father)) + { + m_Transform = m_Father; + } + return m_Transform; + } + + private static List GenerateMocPathList(Dictionary mocDict, bool searchByFilename, ref bool useFullContainerPath) + { + var mocPathDict = new Dictionary(); + var mocPathList = new List(); + foreach (var mocMono in l2dModelDict.Keys) + { + if (!containers.TryGetValue(mocMono, out var containerPath)) + 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)); + } + + if (mocPathDict.Count > 0) + { + var basePathSet = mocPathDict.Values.Select(x => x.Item2).ToHashSet(); + useFullContainerPath = mocPathDict.Count != basePathSet.Count; + foreach (var moc in mocDict.Keys) + { + var mocPath = useFullContainerPath + ? mocPathDict[moc].Item1 //fullContainerPath + : mocPathDict[moc].Item2; //basePath + if (searchByFilename) + { + mocPathList.Add(containers[moc]); + if (mocDict.TryGetValue(moc, out var model) && model != null) + model.Container = mocPath; + } + else + { + mocPathList.Add(mocPath); + } + } + mocPathDict.Clear(); + } + return mocPathList; + } + public static void ExportLive2D() { var baseDestPath = Path.Combine(CLIOptions.o_outputFolder.Value, "Live2DOutput"); var useFullContainerPath = true; - var mocPathList = new List(); - var basePathSet = new HashSet(); 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 - if (cubismMocList.Count == 0) + if (l2dModelDict.Count == 0) { Logger.Default.Log(LoggerEvent.Info, "Live2D Cubism models were not found.", ignoreLevel: true); return; @@ -744,66 +909,75 @@ namespace AssetStudioCLI Progress.Reset(); Logger.Info($"Searching for Live2D files..."); - foreach (var mocMonoBehaviour in cubismMocList) - { - if (!containers.TryGetValue(mocMonoBehaviour, out var fullContainerPath)) - continue; + var mocPathList = GenerateMocPathList(mocDict, searchByFilename, ref useFullContainerPath); - var pathSepIndex = fullContainerPath.LastIndexOf('/'); - var basePath = pathSepIndex > 0 - ? fullContainerPath.Substring(0, pathSepIndex) - : fullContainerPath; - basePathSet.Add(basePath); - mocPathList.Add(fullContainerPath); - } - - if (mocPathList.Count == 0) - { - Logger.Error("Live2D Cubism export error: Cannot find any model related files."); - return; - } - if (basePathSet.Count == mocPathList.Count) - { - mocPathList = basePathSet.ToList(); - useFullContainerPath = false; - Logger.Debug($"useFullContainerPath: {useFullContainerPath}"); - } - basePathSet.Clear(); - - var lookup = containers.AsParallel().ToLookup( +#if NET9_0_OR_GREATER + var assetDict = new Dictionary>(); + foreach (var (asset, container) in containers) + { + 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 (assetDict.TryGetValue(result, out var assets)) + assets.Add(asset); + else + assetDict[result] = [asset]; + } + } +#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 - ); - - if (cubismMocList[0].serializedType?.m_Type == null && CLIOptions.o_assemblyPath.Value == "") + ).Where(x => x.Key != null).ToDictionary(x => x.Key, x => x.ToList()); +#endif + if (mocDict.Keys.First().serializedType?.m_Type == null && CLIOptions.o_assemblyPath.Value == "") { Logger.Warning("Specifying the assembly folder may be needed for proper extraction"); } - var totalModelCount = lookup.LongCount(x => x.Key != null); + var totalModelCount = assetDict.Count; Logger.Info($"Found {totalModelCount} model(s)."); var parallelTaskCount = CLIOptions.o_maxParallelExportTasks.Value; var modelCounter = 0; - foreach (var assets in lookup) + Live2DExtractor.MocDict = mocDict; + Live2DExtractor.Assembly = assemblyLoader; + foreach (var assetKvp in assetDict) { - var srcContainer = assets.Key; - if (srcContainer == null) - continue; - var container = srcContainer; + var srcContainer = assetKvp.Key; Logger.Info($"[{modelCounter + 1}/{totalModelCount}] Exporting Live2D: \"{srcContainer.Color(Ansi.BrightCyan)}\""); try { - var modelName = useFullContainerPath - ? Path.GetFileNameWithoutExtension(container) - : container.Substring(container.LastIndexOf('/') + 1); - container = Path.HasExtension(container) - ? container.Replace(Path.GetExtension(container), "") - : container; - var destPath = Path.Combine(baseDestPath, container) + Path.DirectorySeparatorChar; + var cubismExtractor = new Live2DExtractor(assetKvp.Value); + 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; + } - var modelExtractor = new Live2DExtractor(assets); - modelExtractor.ExtractCubismModel(destPath, modelName, motionMode, assemblyLoader, forceBezier, parallelTaskCount); + var destPath = Path.Combine(baseDestPath, modelPath) + Path.DirectorySeparatorChar; + cubismExtractor.ExtractCubismModel(destPath, motionMode, forceBezier, parallelTaskCount); modelCounter++; } catch (Exception ex) diff --git a/AssetStudioGUI/AssetStudioGUIForm.cs b/AssetStudioGUI/AssetStudioGUIForm.cs index 4b62e0a..6c181b8 100644 --- a/AssetStudioGUI/AssetStudioGUIForm.cs +++ b/AssetStudioGUI/AssetStudioGUIForm.cs @@ -293,7 +293,7 @@ namespace AssetStudioGUI var types = new SortedSet(); types.UnionWith(exportableAssets.Select(x => x.TypeString)); - if (Studio.cubismMocList.Count > 0) + if (Studio.l2dModelDict.Count > 0) { types.Add("MonoBehaviour (Live2D Model)"); } @@ -1181,19 +1181,19 @@ namespace AssetStudioGUI private void PreviewMoc(AssetItem assetItem, MonoBehaviour m_MonoBehaviour) { - using (var cubismModel = new CubismModel(m_MonoBehaviour)) + using (var cubismMoc = new CubismMoc(m_MonoBehaviour)) { var sb = new StringBuilder(); - sb.AppendLine($"SDK Version: {cubismModel.VersionDescription}"); - if (cubismModel.Version > 0) + sb.AppendLine($"SDK Version: {cubismMoc.VersionDescription}"); + if (cubismMoc.Version > 0) { - sb.AppendLine($"Canvas Width: {cubismModel.CanvasWidth}"); - sb.AppendLine($"Canvas Height: {cubismModel.CanvasHeight}"); - sb.AppendLine($"Center X: {cubismModel.CentralPosX}"); - sb.AppendLine($"Center Y: {cubismModel.CentralPosY}"); - sb.AppendLine($"Pixel Per Unit: {cubismModel.PixelPerUnit}"); - sb.AppendLine($"Parameter Count: {cubismModel.ParamCount}"); - sb.AppendLine($"Part Count: {cubismModel.PartCount}"); + sb.AppendLine($"Canvas Width: {cubismMoc.CanvasWidth}"); + sb.AppendLine($"Canvas Height: {cubismMoc.CanvasHeight}"); + sb.AppendLine($"Center X: {cubismMoc.CentralPosX}"); + sb.AppendLine($"Center Y: {cubismMoc.CentralPosY}"); + sb.AppendLine($"Pixel Per Unit: {cubismMoc.PixelPerUnit}"); + sb.AppendLine($"Parameter Count: {cubismMoc.ParamCount}"); + sb.AppendLine($"Part Count: {cubismMoc.PartCount}"); } assetItem.InfoText = sb.ToString(); } @@ -1484,6 +1484,7 @@ namespace AssetStudioGUI assemblyLoader.Clear(); exportableAssets.Clear(); visibleAssets.Clear(); + l2dModelDict.Clear(); sceneTreeView.Nodes.Clear(); assetListView.VirtualListSize = 0; assetListView.Items.Clear(); @@ -1491,7 +1492,6 @@ namespace AssetStudioGUI classesListView.Groups.Clear(); selectedAnimationAssetsList.Clear(); selectedIndicesPrevList.Clear(); - cubismMocList.Clear(); previewPanel.Image = Properties.Resources.preview; previewPanel.SizeMode = PictureBoxSizeMode.CenterImage; imageTexture?.Dispose(); @@ -1556,7 +1556,7 @@ namespace AssetStudioGUI switch (asset.Asset) { case MonoBehaviour m_MonoBehaviour: - if (Studio.cubismMocList.Count > 0 && m_MonoBehaviour.m_Script.TryGet(out var m_Script)) + if (Studio.l2dModelDict.Count > 0 && m_MonoBehaviour.m_Script.TryGet(out var m_Script)) { if (m_Script.m_ClassName == "CubismMoc") { @@ -1892,7 +1892,7 @@ namespace AssetStudioGUI } } visibleAssets = filterMoc - ? exportableAssets.FindAll(x => cubismMocList.Contains(x.Asset) || show.Contains(x.Type)) + ? exportableAssets.FindAll(x => (x.Asset is MonoBehaviour monoBehaviour && l2dModelDict.ContainsKey(monoBehaviour)) || show.Contains(x.Type)) : exportableAssets.FindAll(x => show.Contains(x.Type)); } else @@ -2217,7 +2217,7 @@ namespace AssetStudioGUI { if (exportableAssets.Count > 0) { - if (Studio.cubismMocList.Count == 0) + if (Studio.l2dModelDict.Count == 0) { Logger.Info("Live2D Cubism models were not found."); return; @@ -2257,7 +2257,7 @@ namespace AssetStudioGUI Logger.Info("No exportable assets loaded"); return; } - if (Studio.cubismMocList.Count == 0) + if (Studio.l2dModelDict.Count == 0) { Logger.Info("Live2D Cubism models were not found."); return; diff --git a/AssetStudioGUI/ExportOptions.Designer.cs b/AssetStudioGUI/ExportOptions.Designer.cs index cd858a9..06686f8 100644 --- a/AssetStudioGUI/ExportOptions.Designer.cs +++ b/AssetStudioGUI/ExportOptions.Designer.cs @@ -51,6 +51,9 @@ this.tobmp = new System.Windows.Forms.RadioButton(); this.converttexture = new System.Windows.Forms.CheckBox(); this.l2dGroupBox = new System.Windows.Forms.GroupBox(); + this.l2dAssetSearchByFilenameCheckBox = new System.Windows.Forms.CheckBox(); + this.l2dModelGroupComboBox = new System.Windows.Forms.ComboBox(); + this.l2dModelGroupLabel = new System.Windows.Forms.Label(); this.l2dMotionExportMethodPanel = new System.Windows.Forms.Panel(); this.l2dMonoBehaviourRadioButton = new System.Windows.Forms.RadioButton(); this.l2dAnimationClipRadioButton = new System.Windows.Forms.RadioButton(); @@ -89,7 +92,7 @@ // OKbutton // this.OKbutton.BackColor = System.Drawing.SystemColors.ButtonFace; - this.OKbutton.Location = new System.Drawing.Point(396, 380); + this.OKbutton.Location = new System.Drawing.Point(396, 430); this.OKbutton.Name = "OKbutton"; this.OKbutton.Size = new System.Drawing.Size(75, 23); this.OKbutton.TabIndex = 4; @@ -101,7 +104,7 @@ // this.Cancel.BackColor = System.Drawing.SystemColors.ButtonFace; this.Cancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; - this.Cancel.Location = new System.Drawing.Point(477, 380); + this.Cancel.Location = new System.Drawing.Point(477, 430); this.Cancel.Name = "Cancel"; this.Cancel.Size = new System.Drawing.Size(75, 23); this.Cancel.TabIndex = 5; @@ -193,8 +196,8 @@ this.filenameFormatComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; this.filenameFormatComboBox.FormattingEnabled = true; this.filenameFormatComboBox.Items.AddRange(new object[] { - "assetName", - "assetName@pathID", + "asset name", + "asset name@pathID", "pathID"}); this.filenameFormatComboBox.Location = new System.Drawing.Point(177, 35); this.filenameFormatComboBox.Name = "filenameFormatComboBox"; @@ -353,24 +356,59 @@ // // l2dGroupBox // + this.l2dGroupBox.Controls.Add(this.l2dAssetSearchByFilenameCheckBox); + this.l2dGroupBox.Controls.Add(this.l2dModelGroupComboBox); + this.l2dGroupBox.Controls.Add(this.l2dModelGroupLabel); this.l2dGroupBox.Controls.Add(this.l2dMotionExportMethodPanel); this.l2dGroupBox.Controls.Add(this.l2dMotionExportMethodLabel); this.l2dGroupBox.Controls.Add(this.l2dForceBezierCheckBox); this.l2dGroupBox.Location = new System.Drawing.Point(12, 275); this.l2dGroupBox.Name = "l2dGroupBox"; - this.l2dGroupBox.Size = new System.Drawing.Size(316, 100); + this.l2dGroupBox.Size = new System.Drawing.Size(316, 149); this.l2dGroupBox.TabIndex = 2; this.l2dGroupBox.TabStop = false; this.l2dGroupBox.Text = "Cubism Live2D"; // + // l2dAssetSearchByFilenameCheckBox + // + this.l2dAssetSearchByFilenameCheckBox.AutoSize = true; + this.l2dAssetSearchByFilenameCheckBox.Location = new System.Drawing.Point(6, 45); + this.l2dAssetSearchByFilenameCheckBox.Name = "l2dAssetSearchByFilenameCheckBox"; + 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.l2dAssetSearchByFilenameCheckBox.UseVisualStyleBackColor = true; + // + // l2dModelGroupComboBox + // + this.l2dModelGroupComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.l2dModelGroupComboBox.FormattingEnabled = true; + this.l2dModelGroupComboBox.Items.AddRange(new object[] { + "container path", + "source file name"}); + this.l2dModelGroupComboBox.Location = new System.Drawing.Point(142, 18); + this.l2dModelGroupComboBox.Name = "l2dModelGroupComboBox"; + this.l2dModelGroupComboBox.Size = new System.Drawing.Size(154, 21); + this.l2dModelGroupComboBox.TabIndex = 2; + // + // l2dModelGroupLabel + // + this.l2dModelGroupLabel.AutoSize = true; + this.l2dModelGroupLabel.Location = new System.Drawing.Point(6, 21); + this.l2dModelGroupLabel.Name = "l2dModelGroupLabel"; + this.l2dModelGroupLabel.Size = new System.Drawing.Size(130, 13); + this.l2dModelGroupLabel.TabIndex = 1; + this.l2dModelGroupLabel.Text = "Group exported models by"; + // // l2dMotionExportMethodPanel // this.l2dMotionExportMethodPanel.Controls.Add(this.l2dMonoBehaviourRadioButton); this.l2dMotionExportMethodPanel.Controls.Add(this.l2dAnimationClipRadioButton); - this.l2dMotionExportMethodPanel.Location = new System.Drawing.Point(18, 40); + this.l2dMotionExportMethodPanel.Location = new System.Drawing.Point(18, 89); this.l2dMotionExportMethodPanel.Name = "l2dMotionExportMethodPanel"; this.l2dMotionExportMethodPanel.Size = new System.Drawing.Size(279, 27); - this.l2dMotionExportMethodPanel.TabIndex = 2; + this.l2dMotionExportMethodPanel.TabIndex = 5; // // l2dMonoBehaviourRadioButton // @@ -400,19 +438,19 @@ // l2dMotionExportMethodLabel // this.l2dMotionExportMethodLabel.AutoSize = true; - this.l2dMotionExportMethodLabel.Location = new System.Drawing.Point(6, 21); + this.l2dMotionExportMethodLabel.Location = new System.Drawing.Point(6, 70); this.l2dMotionExportMethodLabel.Name = "l2dMotionExportMethodLabel"; this.l2dMotionExportMethodLabel.Size = new System.Drawing.Size(109, 13); - this.l2dMotionExportMethodLabel.TabIndex = 1; + this.l2dMotionExportMethodLabel.TabIndex = 4; this.l2dMotionExportMethodLabel.Text = "Motion export method"; // // l2dForceBezierCheckBox // this.l2dForceBezierCheckBox.AutoSize = true; - this.l2dForceBezierCheckBox.Location = new System.Drawing.Point(6, 77); + this.l2dForceBezierCheckBox.Location = new System.Drawing.Point(6, 122); this.l2dForceBezierCheckBox.Name = "l2dForceBezierCheckBox"; this.l2dForceBezierCheckBox.Size = new System.Drawing.Size(278, 17); - this.l2dForceBezierCheckBox.TabIndex = 3; + this.l2dForceBezierCheckBox.TabIndex = 6; this.l2dForceBezierCheckBox.Text = "Calculate Linear motion segments as Bezier segments"; this.optionTooltip.SetToolTip(this.l2dForceBezierCheckBox, "May help if the exported motions look jerky/not smooth enough"); this.l2dForceBezierCheckBox.UseVisualStyleBackColor = true; @@ -439,7 +477,7 @@ this.groupBox2.Controls.Add(this.eulerFilter); this.groupBox2.Location = new System.Drawing.Point(328, 13); this.groupBox2.Name = "groupBox2"; - this.groupBox2.Size = new System.Drawing.Size(224, 362); + this.groupBox2.Size = new System.Drawing.Size(224, 411); this.groupBox2.TabIndex = 3; this.groupBox2.TabStop = false; this.groupBox2.Text = "Fbx"; @@ -655,7 +693,7 @@ this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.CancelButton = this.Cancel; - this.ClientSize = new System.Drawing.Size(564, 416); + this.ClientSize = new System.Drawing.Size(564, 461); this.Controls.Add(this.l2dGroupBox); this.Controls.Add(this.groupBox2); this.Controls.Add(this.groupBox1); @@ -735,5 +773,8 @@ private System.Windows.Forms.NumericUpDown parallelExportUpDown; private System.Windows.Forms.CheckBox parallelExportCheckBox; private System.Windows.Forms.Label parallelExportMaxLabel; + private System.Windows.Forms.Label l2dModelGroupLabel; + private System.Windows.Forms.ComboBox l2dModelGroupComboBox; + private System.Windows.Forms.CheckBox l2dAssetSearchByFilenameCheckBox; } } \ No newline at end of file diff --git a/AssetStudioGUI/ExportOptions.cs b/AssetStudioGUI/ExportOptions.cs index 9dc0add..0543c96 100644 --- a/AssetStudioGUI/ExportOptions.cs +++ b/AssetStudioGUI/ExportOptions.cs @@ -11,6 +11,7 @@ namespace AssetStudioGUI { InitializeComponent(); assetGroupOptions.SelectedIndex = Properties.Settings.Default.assetGroupOption; + filenameFormatComboBox.SelectedIndex = Properties.Settings.Default.filenameFormat; restoreExtensionName.Checked = Properties.Settings.Default.restoreExtensionName; converttexture.Checked = Properties.Settings.Default.convertTexture; exportSpriteWithAlphaMask.Checked = Properties.Settings.Default.exportSpriteWithMask; @@ -18,6 +19,12 @@ namespace AssetStudioGUI var defaultImageType = Properties.Settings.Default.convertType.ToString(); ((RadioButton)panel1.Controls.Cast().First(x => x.Text == defaultImageType)).Checked = true; openAfterExport.Checked = Properties.Settings.Default.openAfterExport; + var maxParallelTasks = Environment.ProcessorCount; + var taskCount = Properties.Settings.Default.parallelExportCount; + parallelExportUpDown.Maximum = maxParallelTasks; + parallelExportUpDown.Value = taskCount <= 0 ? maxParallelTasks : Math.Min(taskCount, maxParallelTasks); + parallelExportMaxLabel.Text += maxParallelTasks; + parallelExportCheckBox.Checked = Properties.Settings.Default.parallelExport; eulerFilter.Checked = Properties.Settings.Default.eulerFilter; filterPrecision.Value = Properties.Settings.Default.filterPrecision; exportAllNodes.Checked = Properties.Settings.Default.exportAllNodes; @@ -30,21 +37,17 @@ namespace AssetStudioGUI scaleFactor.Value = Properties.Settings.Default.scaleFactor; fbxVersion.SelectedIndex = Properties.Settings.Default.fbxVersion; fbxFormat.SelectedIndex = Properties.Settings.Default.fbxFormat; + l2dModelGroupComboBox.SelectedIndex = (int)Properties.Settings.Default.l2dModelGroupOption; + l2dAssetSearchByFilenameCheckBox.Checked = Properties.Settings.Default.l2dAssetSearchByFilename; var defaultMotionMode = Properties.Settings.Default.l2dMotionMode.ToString(); ((RadioButton)l2dMotionExportMethodPanel.Controls.Cast().First(x => x.AccessibleName == defaultMotionMode)).Checked = true; l2dForceBezierCheckBox.Checked = Properties.Settings.Default.l2dForceBezier; - filenameFormatComboBox.SelectedIndex = Properties.Settings.Default.filenameFormat; - var maxParallelTasks = Environment.ProcessorCount; - var taskCount = Properties.Settings.Default.parallelExportCount; - parallelExportUpDown.Maximum = maxParallelTasks; - parallelExportUpDown.Value = taskCount <= 0 ? maxParallelTasks : Math.Min(taskCount, maxParallelTasks); - parallelExportMaxLabel.Text += maxParallelTasks; - parallelExportCheckBox.Checked = Properties.Settings.Default.parallelExport; } private void OKbutton_Click(object sender, EventArgs e) { Properties.Settings.Default.assetGroupOption = assetGroupOptions.SelectedIndex; + Properties.Settings.Default.filenameFormat = filenameFormatComboBox.SelectedIndex; Properties.Settings.Default.restoreExtensionName = restoreExtensionName.Checked; Properties.Settings.Default.convertTexture = converttexture.Checked; Properties.Settings.Default.exportSpriteWithMask = exportSpriteWithAlphaMask.Checked; @@ -52,6 +55,8 @@ namespace AssetStudioGUI var checkedImageType = (RadioButton)panel1.Controls.Cast().First(x => ((RadioButton)x).Checked); Properties.Settings.Default.convertType = (ImageFormat)Enum.Parse(typeof(ImageFormat), checkedImageType.Text); Properties.Settings.Default.openAfterExport = openAfterExport.Checked; + Properties.Settings.Default.parallelExport = parallelExportCheckBox.Checked; + Properties.Settings.Default.parallelExportCount = (int)parallelExportUpDown.Value; Properties.Settings.Default.eulerFilter = eulerFilter.Checked; Properties.Settings.Default.filterPrecision = filterPrecision.Value; Properties.Settings.Default.exportAllNodes = exportAllNodes.Checked; @@ -64,12 +69,11 @@ namespace AssetStudioGUI Properties.Settings.Default.scaleFactor = scaleFactor.Value; Properties.Settings.Default.fbxVersion = fbxVersion.SelectedIndex; Properties.Settings.Default.fbxFormat = fbxFormat.SelectedIndex; + Properties.Settings.Default.l2dModelGroupOption = (CubismLive2DExtractor.Live2DModelGroupOption)l2dModelGroupComboBox.SelectedIndex; + Properties.Settings.Default.l2dAssetSearchByFilename = l2dAssetSearchByFilenameCheckBox.Checked; var checkedMotionMode = (RadioButton)l2dMotionExportMethodPanel.Controls.Cast().First(x => ((RadioButton)x).Checked); Properties.Settings.Default.l2dMotionMode = (CubismLive2DExtractor.Live2DMotionMode)Enum.Parse(typeof(CubismLive2DExtractor.Live2DMotionMode), checkedMotionMode.AccessibleName); Properties.Settings.Default.l2dForceBezier = l2dForceBezierCheckBox.Checked; - Properties.Settings.Default.filenameFormat = filenameFormatComboBox.SelectedIndex; - Properties.Settings.Default.parallelExport = parallelExportCheckBox.Checked; - Properties.Settings.Default.parallelExportCount = (int)parallelExportUpDown.Value; Properties.Settings.Default.Save(); DialogResult = DialogResult.OK; Close(); diff --git a/AssetStudioGUI/ExportOptions.resx b/AssetStudioGUI/ExportOptions.resx index ea45d2d..7da86a7 100644 --- a/AssetStudioGUI/ExportOptions.resx +++ b/AssetStudioGUI/ExportOptions.resx @@ -120,7 +120,4 @@ 17, 17 - - 17, 17 - \ No newline at end of file diff --git a/AssetStudioGUI/Properties/Settings.Designer.cs b/AssetStudioGUI/Properties/Settings.Designer.cs index 1302be9..89dc268 100644 --- a/AssetStudioGUI/Properties/Settings.Designer.cs +++ b/AssetStudioGUI/Properties/Settings.Designer.cs @@ -406,5 +406,29 @@ namespace AssetStudioGUI.Properties { this["guiColorTheme"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("ContainerPath")] + public global::CubismLive2DExtractor.Live2DModelGroupOption l2dModelGroupOption { + get { + return ((global::CubismLive2DExtractor.Live2DModelGroupOption)(this["l2dModelGroupOption"])); + } + set { + this["l2dModelGroupOption"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool l2dAssetSearchByFilename { + get { + return ((bool)(this["l2dAssetSearchByFilename"])); + } + set { + this["l2dAssetSearchByFilename"] = value; + } + } } } diff --git a/AssetStudioGUI/Properties/Settings.settings b/AssetStudioGUI/Properties/Settings.settings index b271543..b4b2b74 100644 --- a/AssetStudioGUI/Properties/Settings.settings +++ b/AssetStudioGUI/Properties/Settings.settings @@ -98,5 +98,11 @@ Light + + ContainerPath + + + False + \ No newline at end of file diff --git a/AssetStudioGUI/Studio.cs b/AssetStudioGUI/Studio.cs index 9adaf90..2879f76 100644 --- a/AssetStudioGUI/Studio.cs +++ b/AssetStudioGUI/Studio.cs @@ -3,6 +3,7 @@ using CubismLive2DExtractor; using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Specialized; using System.Diagnostics; using System.Globalization; using System.IO; @@ -84,8 +85,8 @@ namespace AssetStudioGUI public static AssemblyLoader assemblyLoader = new AssemblyLoader(); public static List exportableAssets = new List(); public static List visibleAssets = new List(); - public static List cubismMocList = new List(); - private static Dictionary l2dResourceContainers = new Dictionary(); + public static Dictionary l2dModelDict = new Dictionary(); + private static Dictionary l2dAssetContainers = new Dictionary(); internal static Action StatusStripUpdate = x => { }; public static int ExtractFolder(string path, string savePath) @@ -189,7 +190,8 @@ namespace AssetStudioGUI var objectAssetItemDic = new Dictionary(objectCount); var containers = new List<(PPtr, string)>(); var tex2dArrayAssetList = new List(); - l2dResourceContainers.Clear(); + var l2dSearchByFilename = Properties.Settings.Default.l2dAssetSearchByFilename; + l2dAssetContainers.Clear(); var i = 0; Progress.Reset(); foreach (var assetsFile in assetsManager.assetsFileList) @@ -209,6 +211,14 @@ namespace AssetStudioGUI break; case GameObject m_GameObject: assetItem.Text = m_GameObject.m_Name; + 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: if (!string.IsNullOrEmpty(m_Texture2D.m_StreamData?.path)) @@ -260,9 +270,26 @@ namespace AssetStudioGUI if (m_MonoBehaviour.m_Script.TryGet(out var m_Script)) { assetName = assetName == "" ? m_Script.m_ClassName : assetName; - if (m_Script.m_ClassName == "CubismMoc") + switch (m_Script.m_ClassName) { - cubismMocList.Add(m_MonoBehaviour); + case "CubismMoc": + if (!l2dModelDict.ContainsKey(m_MonoBehaviour)) + { + l2dModelDict.Add(m_MonoBehaviour, null); + } + break; + case "CubismRenderer": + BindCubismRenderer(m_MonoBehaviour); + break; + case "CubismDisplayInfoParameterName": + BindParamDisplayInfo(m_MonoBehaviour); + break; + case "CubismDisplayInfoPartName": + BindPartDisplayInfo(m_MonoBehaviour); + break; + case "CubismPosePart": + BindCubismPosePart(m_MonoBehaviour); + break; } } assetItem.Text = assetName; @@ -319,11 +346,18 @@ namespace AssetStudioGUI objectAssetItemDic[obj].Container = container; switch (obj) { + case GameObject m_GameObject: + if (m_GameObject.CubismModel != null) + { + m_GameObject.CubismModel.Container = container; + } + break; case AnimationClip _: - case GameObject _: case Texture2D _: case MonoBehaviour _: - l2dResourceContainers[obj] = container; + l2dAssetContainers[obj] = l2dSearchByFilename + ? Path.GetFileName(obj.assetsFile.originalPath) + : container; break; } } @@ -943,105 +977,239 @@ namespace AssetStudioGUI Process.Start(info); } + private static bool TryGetCubismMoc(MonoBehaviour m_MonoBehaviour, out MonoBehaviour mocMono) + { + mocMono = null; + var pptrDict = (OrderedDictionary)CubismParsers.ParseMonoBehaviour(m_MonoBehaviour, CubismParsers.CubismMonoBehaviourType.Model, assemblyLoader)?["_moc"]; + if (pptrDict == null) + return false; + + var mocPPtr = new PPtr + { + m_FileID = (int)pptrDict["m_FileID"], + m_PathID = (long)pptrDict["m_PathID"], + AssetsFile = m_MonoBehaviour.assetsFile + }; + 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)) + return; + + var rootTransform = GetRootTransform(m_GameObject.m_Transform); + if (rootTransform.m_GameObject.TryGet(out var rootGameObject) && rootGameObject.CubismModel != null) + { + rootGameObject.CubismModel.RenderTextureList.Add(m_MonoBehaviour); + } + } + + private static void BindParamDisplayInfo(MonoBehaviour m_MonoBehaviour) + { + if (!m_MonoBehaviour.m_GameObject.TryGet(out var m_GameObject)) + return; + + var rootTransform = GetRootTransform(m_GameObject.m_Transform); + if (rootTransform.m_GameObject.TryGet(out var rootGameObject) && rootGameObject.CubismModel != null) + { + rootGameObject.CubismModel.ParamDisplayInfoList.Add(m_MonoBehaviour); + } + } + + private static void BindPartDisplayInfo(MonoBehaviour m_MonoBehaviour) + { + if (!m_MonoBehaviour.m_GameObject.TryGet(out var m_GameObject)) + return; + + var rootTransform = GetRootTransform(m_GameObject.m_Transform); + if (rootTransform.m_GameObject.TryGet(out var rootGameObject) && rootGameObject.CubismModel != null) + { + rootGameObject.CubismModel.PartDisplayInfoList.Add(m_MonoBehaviour); + } + } + + private static void BindCubismPosePart(MonoBehaviour m_MonoBehaviour) + { + if (!m_MonoBehaviour.m_GameObject.TryGet(out var m_GameObject)) + return; + + var rootTransform = GetRootTransform(m_GameObject.m_Transform); + if (rootTransform.m_GameObject.TryGet(out var rootGameObject) && rootGameObject.CubismModel != null) + { + rootGameObject.CubismModel.PosePartList.Add(m_MonoBehaviour); + } + } + + private static Transform GetRootTransform(Transform m_Transform) + { + if (m_Transform == null) + return null; + + while (m_Transform.m_Father.TryGet(out var m_Father)) + { + m_Transform = m_Father; + } + return m_Transform; + } + + private static List GenerateMocPathList(Dictionary mocDict, bool searchByFilename, ref bool useFullContainerPath) + { + var mocPathDict = new Dictionary(); + var mocPathList = new List(); + foreach (var mocMono in l2dModelDict.Keys) + { + if (!l2dAssetContainers.TryGetValue(mocMono, out var containerPath)) + 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)); + } + + if (mocPathDict.Count > 0) + { + var basePathSet = mocPathDict.Values.Select(x => x.Item2).ToHashSet(); + useFullContainerPath = mocPathDict.Count != basePathSet.Count; + foreach (var moc in mocDict.Keys) + { + var mocPath = useFullContainerPath + ? mocPathDict[moc].Item1 //fullContainerPath + : mocPathDict[moc].Item2; //basePath + if (searchByFilename) + { + mocPathList.Add(l2dAssetContainers[moc]); + if (mocDict.TryGetValue(moc, out var model) && model != null) + model.Container = mocPath; + } + else + { + mocPathList.Add(mocPath); + } + } + mocPathDict.Clear(); + } + return mocPathList; + } + public static void ExportLive2D(string exportPath, List selMocs = null, List selClipMotions = null, List selFadeMotions = null, MonoBehaviour selFadeLst = null) { var baseDestPath = Path.Combine(exportPath, "Live2DOutput"); var forceBezier = Properties.Settings.Default.l2dForceBezier; - var mocList = selMocs ?? cubismMocList; + var modelGroupOption = Properties.Settings.Default.l2dModelGroupOption; + var searchByFilename = Properties.Settings.Default.l2dAssetSearchByFilename; var motionMode = Properties.Settings.Default.l2dMotionMode; if (selClipMotions != null) motionMode = Live2DMotionMode.AnimationClipV2; else if (selFadeMotions != null || selFadeLst != null) motionMode = Live2DMotionMode.MonoBehaviour; + var mocDict = selMocs != null + ? selMocs.ToDictionary(moc => moc, moc => l2dModelDict[moc]) + : l2dModelDict; ThreadPool.QueueUserWorkItem(state => { - Logger.Info($"Searching for Live2D files..."); + Logger.Info("Searching for Live2D assets..."); - var mocPathDict = new Dictionary(); - var mocPathList = new List(); - foreach (var mocMonoBehaviour in cubismMocList) + var useFullContainerPath = true; + var mocPathList = GenerateMocPathList(mocDict, searchByFilename, ref useFullContainerPath); + +#if NET9_0_OR_GREATER + var assetDict = new Dictionary>(); + foreach (var (asset, container) in l2dAssetContainers) { - if (!l2dResourceContainers.TryGetValue(mocMonoBehaviour, out var fullContainerPath)) - continue; - - var pathSepIndex = fullContainerPath.LastIndexOf('/'); - var basePath = pathSepIndex > 0 - ? fullContainerPath.Substring(0, pathSepIndex) - : fullContainerPath; - mocPathDict.Add(mocMonoBehaviour, (fullContainerPath, basePath)); + 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 (assetDict.TryGetValue(result, out var assets)) + assets.Add(asset); + else + assetDict[result] = [asset]; + } } - if (mocPathDict.Count == 0) - { - Logger.Error("Live2D Cubism export error\r\nCannot find any model related files"); - StatusStripUpdate("Live2D export canceled"); - Progress.Reset(); - return; - } - - var basePathSet = mocPathDict.Values.Select(x => x.Item2).ToHashSet(); - var useFullContainerPath = mocPathDict.Count != basePathSet.Count; - foreach (var moc in mocList) - { - var mocPath = useFullContainerPath - ? mocPathDict[moc].Item1 //fullContainerPath - : mocPathDict[moc].Item2; //basePath - mocPathList.Add(mocPath); - } - mocPathDict.Clear(); - - var lookup = l2dResourceContainers.AsParallel().ToLookup( +#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 - if (mocList[0].serializedType?.m_Type == null && !assemblyLoader.Loaded) + if (mocDict.Keys.First().serializedType?.m_Type == null && !assemblyLoader.Loaded) { Logger.Warning("Specifying the assembly folder may be needed for proper extraction"); SelectAssemblyFolder(); } - var totalModelCount = lookup.LongCount(x => x.Key != null); + var totalModelCount = assetDict.Count; var modelCounter = 0; var parallelExportCount = Properties.Settings.Default.parallelExportCount <= 0 ? Environment.ProcessorCount - 1 : Math.Min(Properties.Settings.Default.parallelExportCount, Environment.ProcessorCount - 1); parallelExportCount = Properties.Settings.Default.parallelExport ? parallelExportCount : 1; - foreach (var assets in lookup) + Live2DExtractor.MocDict = mocDict; + Live2DExtractor.Assembly = assemblyLoader; + foreach (var assetKvp in assetDict) { - var srcContainer = assets.Key; - if (srcContainer == null) - continue; - var container = srcContainer; + var srcContainer = assetKvp.Key; Logger.Info($"[{modelCounter + 1}/{totalModelCount}] Exporting Live2D: \"{srcContainer}\"..."); try { - var modelName = useFullContainerPath - ? Path.GetFileNameWithoutExtension(container) - : container.Substring(container.LastIndexOf('/') + 1); - container = Path.HasExtension(container) - ? container.Replace(Path.GetExtension(container), "") - : container; - var destPath = Path.Combine(baseDestPath, container) + Path.DirectorySeparatorChar; - - var modelExtractor = new Live2DExtractor(assets, selClipMotions, selFadeMotions, selFadeLst); - modelExtractor.ExtractCubismModel(destPath, modelName, motionMode, assemblyLoader, forceBezier, parallelExportCount); + var cubismExtractor = new Live2DExtractor(assetKvp.Value, selClipMotions, selFadeMotions, selFadeLst); + 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; + } + + var destPath = Path.Combine(baseDestPath, modelPath) + Path.DirectorySeparatorChar; + cubismExtractor.ExtractCubismModel(destPath, motionMode, forceBezier, parallelExportCount); modelCounter++; } catch (Exception ex) { Logger.Error($"Live2D model export error: \"{srcContainer}\"", ex); } - Progress.Report(modelCounter, (int)totalModelCount); + Progress.Report(modelCounter, totalModelCount); } Logger.Info($"Finished exporting [{modelCounter}/{totalModelCount}] Live2D model(s)."); - if (modelCounter < totalModelCount) - { - var total = (int)totalModelCount; - Progress.Report(total, total); - } + Progress.Report(1, 1); + if (Properties.Settings.Default.openAfterExport && modelCounter > 0) { OpenFolderInExplorer(exportPath); diff --git a/AssetStudioUtility/CubismLive2DExtractor/BlendType.cs b/AssetStudioUtility/CubismLive2DExtractor/BlendType.cs new file mode 100644 index 0000000..f43daeb --- /dev/null +++ b/AssetStudioUtility/CubismLive2DExtractor/BlendType.cs @@ -0,0 +1,9 @@ +namespace CubismLive2DExtractor +{ + public enum BlendType + { + Add, + Multiply, + Overwrite, + } +} diff --git a/AssetStudioUtility/CubismLive2DExtractor/CubismExpression3Json.cs b/AssetStudioUtility/CubismLive2DExtractor/CubismExpression3Json.cs index f31726f..3927131 100644 --- a/AssetStudioUtility/CubismLive2DExtractor/CubismExpression3Json.cs +++ b/AssetStudioUtility/CubismLive2DExtractor/CubismExpression3Json.cs @@ -1,24 +1,17 @@ namespace CubismLive2DExtractor { - public enum BlendType - { - Add, - Multiply, - Overwrite, - } - public class CubismExpression3Json { - public string Type; - public float FadeInTime; - public float FadeOutTime; - public SerializableExpressionParameter[] Parameters; + public string Type { get; set; } + public float FadeInTime { get; set; } + public float FadeOutTime { get; set; } + public SerializableExpressionParameter[] Parameters { get; set; } public class SerializableExpressionParameter { - public string Id; - public float Value; - public BlendType Blend; + public string Id { get; set; } + public float Value { get; set; } + public BlendType Blend { get; set; } } } } diff --git a/AssetStudioUtility/CubismLive2DExtractor/CubismFadeMotion.cs b/AssetStudioUtility/CubismLive2DExtractor/CubismFadeMotion.cs deleted file mode 100644 index 2817a37..0000000 --- a/AssetStudioUtility/CubismLive2DExtractor/CubismFadeMotion.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; - -namespace CubismLive2DExtractor -{ - public sealed class AnimationCurve - { - public CubismKeyframeData[] m_Curve { get; set; } - public int m_PreInfinity { get; set; } - public int m_PostInfinity { get; set; } - public int m_RotationOrder { get; set; } - } - - public sealed class CubismFadeMotion - { - public string m_Name { get; set; } - public string MotionName { get; set; } - public float FadeInTime { get; set; } - public float FadeOutTime { get; set; } - public string[] ParameterIds { get; set; } - public AnimationCurve[] ParameterCurves { get; set; } - public float[] ParameterFadeInTimes { get; set; } - public float[] ParameterFadeOutTimes { get; set; } - public float MotionLength { get; set; } - - public CubismFadeMotion() - { - ParameterIds = Array.Empty(); - } - } -} diff --git a/AssetStudioUtility/CubismLive2DExtractor/CubismKeyframeData.cs b/AssetStudioUtility/CubismLive2DExtractor/CubismKeyframeData.cs deleted file mode 100644 index 2b01705..0000000 --- a/AssetStudioUtility/CubismLive2DExtractor/CubismKeyframeData.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace CubismLive2DExtractor -{ - public class CubismKeyframeData - { - public float time { get; set; } - public float value { get; set; } - public float inSlope { get; set; } - public float outSlope { get; set; } - public int weightedMode { get; set; } - public float inWeight { get; set; } - public float outWeight { get; set; } - - public CubismKeyframeData() { } - - public CubismKeyframeData(ImportedKeyframe keyframe) - { - time = keyframe.time; - value = keyframe.value; - inSlope = keyframe.inSlope; - outSlope = keyframe.outSlope; - weightedMode = 0; - inWeight = 0; - outWeight = 0; - } - } -} diff --git a/AssetStudioUtility/CubismLive2DExtractor/CubismMotion3Json.cs b/AssetStudioUtility/CubismLive2DExtractor/CubismMotion3Json.cs index b6f51ee..13dd603 100644 --- a/AssetStudioUtility/CubismLive2DExtractor/CubismMotion3Json.cs +++ b/AssetStudioUtility/CubismLive2DExtractor/CubismMotion3Json.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using AssetStudio; namespace CubismLive2DExtractor { @@ -45,9 +46,9 @@ namespace CubismLive2DExtractor } private static void AddSegments( - CubismKeyframeData curve, - CubismKeyframeData preCurve, - CubismKeyframeData nextCurve, + Keyframe curve, + Keyframe preCurve, + Keyframe nextCurve, SerializableCurve cubismCurve, bool forceBezier, ref int totalPointCount, @@ -97,7 +98,7 @@ namespace CubismLive2DExtractor totalSegmentCount++; } - public CubismMotion3Json(CubismFadeMotion fadeMotion, HashSet paramNames, HashSet partNames, bool forceBezier) + public CubismMotion3Json(CubismUnityClasses.CubismFadeMotionData fadeMotion, HashSet paramNames, HashSet partNames, bool forceBezier) { Version = 3; Meta = new SerializableMeta @@ -151,7 +152,7 @@ namespace CubismLive2DExtractor else { target = paramId.ToLower().Contains("part") ? "PartOpacity" : "Parameter"; - AssetStudio.Logger.Warning($"[{fadeMotion.m_Name}] Binding error: Unable to find \"{paramId}\" among the model parts/parameters"); + Logger.Warning($"[{fadeMotion.m_Name}] Binding error: Unable to find \"{paramId}\" among the model parts/parameters"); } break; } @@ -178,7 +179,7 @@ namespace CubismLive2DExtractor var curve = fadeMotion.ParameterCurves[i].m_Curve[j]; var preCurve = fadeMotion.ParameterCurves[i].m_Curve[j - 1]; var next = fadeMotion.ParameterCurves[i].m_Curve.ElementAtOrDefault(j + 1); - var nextCurve = next ?? new CubismKeyframeData(); + var nextCurve = next ?? new Keyframe(); AddSegments(curve, preCurve, nextCurve, Curves[actualCurveCount], forceBezier, ref totalPointCount, ref totalSegmentCount, ref j); } actualCurveCount++; @@ -230,10 +231,10 @@ namespace CubismLive2DExtractor }; for (var j = 1; j < track.Curve.Count; j++) { - var curve = new CubismKeyframeData(track.Curve[j]); - var preCurve = new CubismKeyframeData(track.Curve[j - 1]); + var curve = CreateKeyFrame(track.Curve[j]); + var preCurve = CreateKeyFrame(track.Curve[j - 1]); var next = track.Curve.ElementAtOrDefault(j + 1); - var nextCurve = next != null ? new CubismKeyframeData(next) : new CubismKeyframeData(); + var nextCurve = next != null ? CreateKeyFrame(next) : new Keyframe(); AddSegments(curve, preCurve, nextCurve, Curves[i], forceBezier, ref totalPointCount, ref totalSegmentCount, ref j); } totalPointCount++; @@ -255,5 +256,19 @@ namespace CubismLive2DExtractor } Meta.TotalUserDataSize = totalUserDataSize; } + + private static Keyframe CreateKeyFrame(ImportedKeyframe iKeyframe) + { + return new Keyframe + { + time = iKeyframe.time, + value = iKeyframe.value, + inSlope = iKeyframe.inSlope, + outSlope = iKeyframe.outSlope, + weightedMode = 0, + inWeight = 0, + outWeight = 0, + }; + } } } diff --git a/AssetStudioUtility/CubismLive2DExtractor/CubismObjectList.cs b/AssetStudioUtility/CubismLive2DExtractor/CubismObjectList.cs deleted file mode 100644 index 0604e86..0000000 --- a/AssetStudioUtility/CubismLive2DExtractor/CubismObjectList.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using AssetStudio; - -namespace CubismLive2DExtractor -{ - public sealed class CubismObjectList - { - public static SerializedFile AssetsFile { get; set; } - public HashSet CubismExpressionObjects { get; set; } - public HashSet CubismFadeMotionObjects { get; set; } - - public class ObjectData - { - private long _pathID; - public Object Asset { get; set; } - public int m_FileID { get; set; } - public long m_PathID - { - get => _pathID; - set - { - _pathID = value; - Asset = GetObjByPathID(_pathID); - } - } - - public override bool Equals(object obj) - { - return obj is ObjectData objectData && _pathID == objectData.m_PathID; - } - - public override int GetHashCode() - { - return _pathID.GetHashCode(); - } - } - - public List GetFadeMotionAssetList() - { - return CubismFadeMotionObjects?.Where(x => x.Asset != null).Select(x => (MonoBehaviour)x.Asset).ToList(); - } - - public List GetExpressionList() - { - return CubismExpressionObjects?.Where(x => x.Asset != null).Select(x => (MonoBehaviour)x.Asset).ToList(); - } - - private static Object GetObjByPathID(long pathID) - { - var assetFileList = AssetsFile.assetsManager.assetsFileList; - foreach (var assetFile in assetFileList) - { - if (assetFile.ObjectsDic.TryGetValue(pathID, out var obj)) - { - return obj; - } - } - return null; - } - } -} diff --git a/AssetStudioUtility/CubismLive2DExtractor/CubismParsers.cs b/AssetStudioUtility/CubismLive2DExtractor/CubismParsers.cs index aa38a5f..d75b083 100644 --- a/AssetStudioUtility/CubismLive2DExtractor/CubismParsers.cs +++ b/AssetStudioUtility/CubismLive2DExtractor/CubismParsers.cs @@ -2,6 +2,7 @@ using System.Collections.Specialized; using System.Linq; using AssetStudio; +using CubismLive2DExtractor.CubismUnityClasses; using Newtonsoft.Json; namespace CubismLive2DExtractor @@ -10,20 +11,25 @@ namespace CubismLive2DExtractor { public enum CubismMonoBehaviourType { + FadeController, FadeMotionList, FadeMotion, + ExpressionController, + ExpressionList, Expression, Physics, DisplayInfo, PosePart, + Model, + RenderTexture, } - public static string ParsePhysics(OrderedDictionary physicsDict) + public static string ParsePhysics(OrderedDictionary physicsDict, float motionFps) { - var cubismPhysicsRig = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(physicsDict))._rig; + var cubismPhysicsRig = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(physicsDict)).Rig; var physicsSettings = new CubismPhysics3Json.SerializablePhysicsSettings[cubismPhysicsRig.SubRigs.Length]; - for (int i = 0; i < physicsSettings.Length; i++) + for (var i = 0; i < physicsSettings.Length; i++) { var subRigs = cubismPhysicsRig.SubRigs[i]; physicsSettings[i] = new CubismPhysics3Json.SerializablePhysicsSettings @@ -48,7 +54,7 @@ namespace CubismLive2DExtractor } } }; - for (int j = 0; j < subRigs.Input.Length; j++) + for (var j = 0; j < subRigs.Input.Length; j++) { var input = subRigs.Input[j]; physicsSettings[i].Input[j] = new CubismPhysics3Json.SerializableInput @@ -63,7 +69,7 @@ namespace CubismLive2DExtractor Reflect = input.IsInverted }; } - for (int j = 0; j < subRigs.Output.Length; j++) + for (var j = 0; j < subRigs.Output.Length; j++) { var output = subRigs.Output[j]; physicsSettings[i].Output[j] = new CubismPhysics3Json.SerializableOutput @@ -80,7 +86,7 @@ namespace CubismLive2DExtractor Reflect = output.IsInverted }; } - for (int j = 0; j < subRigs.Particles.Length; j++) + for (var j = 0; j < subRigs.Particles.Length; j++) { var particles = subRigs.Particles[j]; physicsSettings[i].Vertices[j] = new CubismPhysics3Json.SerializableVertex @@ -94,7 +100,7 @@ namespace CubismLive2DExtractor } } var physicsDictionary = new CubismPhysics3Json.SerializablePhysicsDictionary[physicsSettings.Length]; - for (int i = 0; i < physicsSettings.Length; i++) + for (var i = 0; i < physicsSettings.Length; i++) { physicsDictionary[i] = new CubismPhysics3Json.SerializablePhysicsDictionary { @@ -102,6 +108,8 @@ namespace CubismLive2DExtractor Name = $"Dummy{i + 1}" }; } + + var fps = cubismPhysicsRig.Fps == 0 ? motionFps : cubismPhysicsRig.Fps; var physicsJson = new CubismPhysics3Json { Version = 3, @@ -111,6 +119,7 @@ namespace CubismLive2DExtractor TotalInputCount = cubismPhysicsRig.SubRigs.Sum(x => x.Input.Length), TotalOutputCount = cubismPhysicsRig.SubRigs.Sum(x => x.Output.Length), VertexCount = cubismPhysicsRig.SubRigs.Sum(x => x.Particles.Length), + Fps = fps == 0 ? 30f : fps, EffectiveForces = new CubismPhysics3Json.SerializableEffectiveForces { Gravity = cubismPhysicsRig.Gravity, @@ -133,12 +142,21 @@ namespace CubismLive2DExtractor var m_Type = m_MonoBehaviour.ConvertToTypeTree(assemblyLoader); switch (cubismMonoBehaviourType) { + case CubismMonoBehaviourType.FadeController: + fieldName = "cubismfademotionlist"; + break; case CubismMonoBehaviourType.FadeMotionList: fieldName = "cubismfademotionobjects"; break; case CubismMonoBehaviourType.FadeMotion: fieldName = "parameterids"; break; + case CubismMonoBehaviourType.ExpressionController: + fieldName = "expressionslist"; + break; + case CubismMonoBehaviourType.ExpressionList: + fieldName = "cubismexpressionobjects"; + break; case CubismMonoBehaviourType.Expression: fieldName = "parameters"; break; @@ -151,6 +169,12 @@ namespace CubismLive2DExtractor case CubismMonoBehaviourType.PosePart: fieldName = "groupindex"; break; + case CubismMonoBehaviourType.Model: + fieldName = "_moc"; + break; + case CubismMonoBehaviourType.RenderTexture: + fieldName = "_maintexture"; + break; } if (m_Type.m_Nodes.FindIndex(x => x.m_Name.ToLower() == fieldName) < 0) { diff --git a/AssetStudioUtility/CubismLive2DExtractor/CubismPhysics3Json.cs b/AssetStudioUtility/CubismLive2DExtractor/CubismPhysics3Json.cs index 3205b14..d477a2c 100644 --- a/AssetStudioUtility/CubismLive2DExtractor/CubismPhysics3Json.cs +++ b/AssetStudioUtility/CubismLive2DExtractor/CubismPhysics3Json.cs @@ -69,6 +69,7 @@ namespace CubismLive2DExtractor public int TotalInputCount; public int TotalOutputCount; public int VertexCount; + public float Fps; public SerializableEffectiveForces EffectiveForces; public SerializablePhysicsDictionary[] PhysicsDictionary; } diff --git a/AssetStudioUtility/CubismLive2DExtractor/CubismPhysicsRig.cs b/AssetStudioUtility/CubismLive2DExtractor/CubismPhysicsRig.cs deleted file mode 100644 index d9c7219..0000000 --- a/AssetStudioUtility/CubismLive2DExtractor/CubismPhysicsRig.cs +++ /dev/null @@ -1,75 +0,0 @@ -using AssetStudio; - -namespace CubismLive2DExtractor -{ - public class CubismPhysicsNormalizationTuplet - { - public float Maximum; - public float Minimum; - public float Default; - } - - public class CubismPhysicsNormalization - { - public CubismPhysicsNormalizationTuplet Position; - public CubismPhysicsNormalizationTuplet Angle; - } - - public class CubismPhysicsParticle - { - public Vector2 InitialPosition; - public float Mobility; - public float Delay; - public float Acceleration; - public float Radius; - } - - public class CubismPhysicsOutput - { - public string DestinationId; - public int ParticleIndex; - public Vector2 TranslationScale; - public float AngleScale; - public float Weight; - public CubismPhysicsSourceComponent SourceComponent; - public bool IsInverted; - } - - public enum CubismPhysicsSourceComponent - { - X, - Y, - Angle, - } - - public class CubismPhysicsInput - { - public string SourceId; - public Vector2 ScaleOfTranslation; - public float AngleScale; - public float Weight; - public CubismPhysicsSourceComponent SourceComponent; - public bool IsInverted; - } - - public class CubismPhysicsSubRig - { - public CubismPhysicsInput[] Input; - public CubismPhysicsOutput[] Output; - public CubismPhysicsParticle[] Particles; - public CubismPhysicsNormalization Normalization; - } - - public class CubismPhysicsRig - { - public CubismPhysicsSubRig[] SubRigs; - public Vector2 Gravity = new Vector2(0, -1); - public Vector2 Wind; - } - - public class CubismPhysics - { - public string m_Name; - public CubismPhysicsRig _rig; - } -} diff --git a/AssetStudioUtility/CubismLive2DExtractor/CubismUnityClasses/CubismExpressionData.cs b/AssetStudioUtility/CubismLive2DExtractor/CubismUnityClasses/CubismExpressionData.cs new file mode 100644 index 0000000..572d083 --- /dev/null +++ b/AssetStudioUtility/CubismLive2DExtractor/CubismUnityClasses/CubismExpressionData.cs @@ -0,0 +1,19 @@ +using AssetStudio; + +namespace CubismLive2DExtractor.CubismUnityClasses +{ + public sealed class CubismExpressionData : MonoBehaviour + { + public string Type { get; set; } + public float FadeInTime { get; set; } + public float FadeOutTime { get; set; } + public SerializableExpressionParameter[] Parameters { get; set; } + + public class SerializableExpressionParameter + { + public string Id { get; set; } + public float Value { get; set; } + public BlendType Blend { get; set; } + } + } +} diff --git a/AssetStudioUtility/CubismLive2DExtractor/CubismUnityClasses/CubismExpressionList.cs b/AssetStudioUtility/CubismLive2DExtractor/CubismUnityClasses/CubismExpressionList.cs new file mode 100644 index 0000000..24a774d --- /dev/null +++ b/AssetStudioUtility/CubismLive2DExtractor/CubismUnityClasses/CubismExpressionList.cs @@ -0,0 +1,9 @@ +using AssetStudio; + +namespace CubismLive2DExtractor.CubismUnityClasses +{ + public sealed class CubismExpressionList : MonoBehaviour + { + public PPtr[] CubismExpressionObjects { get; set; } + } +} diff --git a/AssetStudioUtility/CubismLive2DExtractor/CubismUnityClasses/CubismFadeMotionData.cs b/AssetStudioUtility/CubismLive2DExtractor/CubismUnityClasses/CubismFadeMotionData.cs new file mode 100644 index 0000000..f5e6272 --- /dev/null +++ b/AssetStudioUtility/CubismLive2DExtractor/CubismUnityClasses/CubismFadeMotionData.cs @@ -0,0 +1,17 @@ +using System; +using AssetStudio; + +namespace CubismLive2DExtractor.CubismUnityClasses +{ + public sealed class CubismFadeMotionData : MonoBehaviour + { + public string MotionName { get; set; } + public float FadeInTime { get; set; } + public float FadeOutTime { get; set; } + public string[] ParameterIds { get; set; } = Array.Empty(); + public AnimationCurve[] ParameterCurves { get; set; } + public float[] ParameterFadeInTimes { get; set; } + public float[] ParameterFadeOutTimes { get; set; } + public float MotionLength { get; set; } + } +} diff --git a/AssetStudioUtility/CubismLive2DExtractor/CubismUnityClasses/CubismFadeMotionList.cs b/AssetStudioUtility/CubismLive2DExtractor/CubismUnityClasses/CubismFadeMotionList.cs new file mode 100644 index 0000000..e3bfbdc --- /dev/null +++ b/AssetStudioUtility/CubismLive2DExtractor/CubismUnityClasses/CubismFadeMotionList.cs @@ -0,0 +1,10 @@ +using AssetStudio; + +namespace CubismLive2DExtractor.CubismUnityClasses +{ + public sealed class CubismFadeMotionList : MonoBehaviour + { + public int[] MotionInstanceIds { get; set; } + public PPtr[] CubismFadeMotionObjects { get; set; } + } +} diff --git a/AssetStudioUtility/CubismLive2DExtractor/CubismUnityClasses/CubismPhysics.cs b/AssetStudioUtility/CubismLive2DExtractor/CubismUnityClasses/CubismPhysics.cs new file mode 100644 index 0000000..210c705 --- /dev/null +++ b/AssetStudioUtility/CubismLive2DExtractor/CubismUnityClasses/CubismPhysics.cs @@ -0,0 +1,77 @@ +using AssetStudio; +using Newtonsoft.Json; + +namespace CubismLive2DExtractor.CubismUnityClasses +{ + public class CubismPhysicsNormalizationTuplet + { + public float Maximum { get; set; } + public float Minimum { get; set; } + public float Default { get; set; } + } + + public class CubismPhysicsNormalization + { + public CubismPhysicsNormalizationTuplet Position { get; set; } + public CubismPhysicsNormalizationTuplet Angle { get; set; } + } + + public class CubismPhysicsParticle + { + public Vector2 InitialPosition { get; set; } + public float Mobility { get; set; } + public float Delay { get; set; } + public float Acceleration { get; set; } + public float Radius { get; set; } + } + + public class CubismPhysicsOutput + { + public string DestinationId { get; set; } + public int ParticleIndex { get; set; } + public Vector2 TranslationScale { get; set; } + public float AngleScale { get; set; } + public float Weight { get; set; } + public CubismPhysicsSourceComponent SourceComponent { get; set; } + public bool IsInverted { get; set; } + } + + public enum CubismPhysicsSourceComponent + { + X, + Y, + Angle, + } + + public class CubismPhysicsInput + { + public string SourceId { get; set; } + public Vector2 ScaleOfTranslation { get; set; } + public float AngleScale { get; set; } + public float Weight { get; set; } + public CubismPhysicsSourceComponent SourceComponent { get; set; } + public bool IsInverted { get; set; } + } + + public class CubismPhysicsSubRig + { + public CubismPhysicsInput[] Input { get; set; } + public CubismPhysicsOutput[] Output { get; set; } + public CubismPhysicsParticle[] Particles { get; set; } + public CubismPhysicsNormalization Normalization { get; set; } + } + + public class CubismPhysicsRig + { + public CubismPhysicsSubRig[] SubRigs { get; set; } + public Vector2 Gravity { get; set; } = new Vector2(0, -1); + public Vector2 Wind { get; set; } + public float Fps { get; set; } + } + + public sealed class CubismPhysics : MonoBehaviour + { + [JsonProperty("_rig")] + public CubismPhysicsRig Rig { get; set; } + } +} diff --git a/AssetStudioUtility/CubismLive2DExtractor/Live2DExtractor.cs b/AssetStudioUtility/CubismLive2DExtractor/Live2DExtractor.cs index 7fe07a5..bf09603 100644 --- a/AssetStudioUtility/CubismLive2DExtractor/Live2DExtractor.cs +++ b/AssetStudioUtility/CubismLive2DExtractor/Live2DExtractor.cs @@ -6,41 +6,46 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Specialized; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using AssetStudio; +using CubismLive2DExtractor.CubismUnityClasses; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using static CubismLive2DExtractor.CubismParsers; +using Object = AssetStudio.Object; namespace CubismLive2DExtractor { public sealed class Live2DExtractor { - private List Expressions { get; set; } + public static Dictionary MocDict { get; set; } + public static AssemblyLoader Assembly { get; set; } + public CubismModel Model { get; set; } private List FadeMotions { get; set; } - private List GameObjects { get; set; } private List AnimationClips { get; set; } - private List Texture2Ds { get; set; } - private HashSet EyeBlinkParameters { get; set; } - private HashSet LipSyncParameters { get; set; } - private HashSet ParameterNames { get; set; } - private HashSet PartNames { get; set; } - private MonoBehaviour MocMono { get; set; } - private MonoBehaviour PhysicsMono { get; set; } - private MonoBehaviour FadeMotionLst { get; set; } + private List Expressions { get; set; } private List ParametersCdi { get; set; } private List PartsCdi { get; set; } private List PoseParts { get; set; } + private List Texture2Ds { get; set; } + public MonoBehaviour MocMono { get; set; } + private MonoBehaviour PhysicsMono { get; set; } + private MonoBehaviour FadeMotionLst { get; set; } + private MonoBehaviour ExpressionLst { get; set; } + private HashSet ParameterNames { get; set; } + private HashSet PartNames { get; set; } + private HashSet EyeBlinkParameters { get; set; } + private HashSet LipSyncParameters { get; set; } - public Live2DExtractor(IGrouping assets, List inClipMotions = null, List inFadeMotions = null, MonoBehaviour inFadeMotionLst = null) + public Live2DExtractor(List assets, List inClipMotions = null, List inFadeMotions = null, MonoBehaviour inFadeMotionLst = null) { Expressions = new List(); FadeMotions = inFadeMotions ?? new List(); AnimationClips = inClipMotions ?? new List(); - GameObjects = new List(); Texture2Ds = new List(); EyeBlinkParameters = new HashSet(); LipSyncParameters = new HashSet(); @@ -50,6 +55,12 @@ namespace CubismLive2DExtractor ParametersCdi = new List(); PartsCdi = new List(); PoseParts = new List(); + var renderTextureSet = new HashSet(); + var isRenderReadable = true; + var searchRenderTextures = true; + var searchModelParamCdi = true; + var searchModelPartCdi = true; + var searchPoseParts = true; Logger.Debug("Sorting model assets.."); foreach (var asset in assets) @@ -63,12 +74,53 @@ namespace CubismLive2DExtractor { case "CubismMoc": MocMono = m_MonoBehaviour; + Model = MocDict[MocMono]; + if (Model != null) + { + PhysicsMono = Model.PhysicsController; + if (inFadeMotionLst == null && TryGetFadeList(Model.FadeController, out var fadeMono)) + { + FadeMotionLst = inFadeMotionLst = fadeMono; + } + if (TryGetExpressionList(Model.ExpressionController, out var expressionMono)) + { + ExpressionLst = expressionMono; + } + if (Model.RenderTextureList.Count > 0) + { + var renderList = Model.RenderTextureList; + foreach (var renderMono in renderList) + { + if (!TryGetRenderTexture(renderMono, out var tex)) + break; + renderTextureSet.Add(tex); + } + searchRenderTextures = renderTextureSet.Count == 0; + } + if (Model.ParamDisplayInfoList.Count > 0) + { + ParametersCdi = Model.ParamDisplayInfoList; + searchModelParamCdi = false; + } + if (Model.PartDisplayInfoList.Count > 0) + { + PartsCdi = Model.PartDisplayInfoList; + searchModelPartCdi = false; + } + if (Model.PosePartList.Count > 0) + { + PoseParts = Model.PosePartList; + searchPoseParts = false; + } + } break; case "CubismPhysicsController": - PhysicsMono = m_MonoBehaviour; + if (PhysicsMono == null) + PhysicsMono = m_MonoBehaviour; break; case "CubismExpressionData": - Expressions.Add(m_MonoBehaviour); + if (ExpressionLst == null) + Expressions.Add(m_MonoBehaviour); break; case "CubismFadeMotionData": if (inFadeMotions == null && inFadeMotionLst == null) @@ -107,23 +159,31 @@ namespace CubismLive2DExtractor } break; case "CubismDisplayInfoParameterName": - if (m_MonoBehaviour.m_GameObject.TryGet(out _)) + if (searchModelParamCdi && m_MonoBehaviour.m_GameObject.TryGet(out _)) { ParametersCdi.Add(m_MonoBehaviour); } break; case "CubismDisplayInfoPartName": - if (m_MonoBehaviour.m_GameObject.TryGet(out _)) + if (searchModelPartCdi && m_MonoBehaviour.m_GameObject.TryGet(out _)) { PartsCdi.Add(m_MonoBehaviour); } break; case "CubismPosePart": - if (m_MonoBehaviour.m_GameObject.TryGet(out _)) + if (searchPoseParts && m_MonoBehaviour.m_GameObject.TryGet(out _)) { PoseParts.Add(m_MonoBehaviour); } break; + case "CubismRenderer": + if (searchRenderTextures && isRenderReadable) + { + isRenderReadable = TryGetRenderTexture(m_MonoBehaviour, out var renderTex); + if (isRenderReadable) + renderTextureSet.Add(renderTex); + } + break; } } break; @@ -133,41 +193,43 @@ namespace CubismLive2DExtractor AnimationClips.Add(m_AnimationClip); } break; - case GameObject m_GameObject: - GameObjects.Add(m_GameObject); - break; case Texture2D m_Texture2D: Texture2Ds.Add(m_Texture2D); break; } } + if (renderTextureSet.Count > 0) + { + Texture2Ds = renderTextureSet.ToList(); + } } - public void ExtractCubismModel(string destPath, string modelName, Live2DMotionMode motionMode, AssemblyLoader assemblyLoader, bool forceBezier = false, int parallelTaskCount = 1) + public void ExtractCubismModel(string destPath, Live2DMotionMode motionMode, bool forceBezier = false, int parallelTaskCount = 1) { Directory.CreateDirectory(destPath); + var modelName = Model?.Name ?? "model"; #region moc3 - using (var cubismModel = new CubismModel(MocMono)) + using (var cubismMoc = new CubismMoc(MocMono)) { var sb = new StringBuilder(); sb.AppendLine("Model Stats:"); - sb.AppendLine($"SDK Version: {cubismModel.VersionDescription}"); - if (cubismModel.Version > 0) + sb.AppendLine($"SDK Version: {cubismMoc.VersionDescription}"); + if (cubismMoc.Version > 0) { - sb.AppendLine($"Canvas Width: {cubismModel.CanvasWidth}"); - sb.AppendLine($"Canvas Height: {cubismModel.CanvasHeight}"); - sb.AppendLine($"Center X: {cubismModel.CentralPosX}"); - sb.AppendLine($"Center Y: {cubismModel.CentralPosY}"); - sb.AppendLine($"Pixel Per Unit: {cubismModel.PixelPerUnit}"); - sb.AppendLine($"Part Count: {cubismModel.PartCount}"); - sb.AppendLine($"Parameter Count: {cubismModel.ParamCount}"); + sb.AppendLine($"Canvas Width: {cubismMoc.CanvasWidth}"); + sb.AppendLine($"Canvas Height: {cubismMoc.CanvasHeight}"); + sb.AppendLine($"Center X: {cubismMoc.CentralPosX}"); + sb.AppendLine($"Center Y: {cubismMoc.CentralPosY}"); + sb.AppendLine($"Pixel Per Unit: {cubismMoc.PixelPerUnit}"); + sb.AppendLine($"Part Count: {cubismMoc.PartCount}"); + sb.AppendLine($"Parameter Count: {cubismMoc.ParamCount}"); Logger.Debug(sb.ToString()); - ParameterNames = cubismModel.ParamNames; - PartNames = cubismModel.PartNames; + ParameterNames = cubismMoc.ParamNames; + PartNames = cubismMoc.PartNames; } - cubismModel.SaveMoc3($"{destPath}{modelName}.moc3"); + cubismMoc.SaveMoc3($"{destPath}{modelName}.moc3"); } #endregion @@ -204,34 +266,13 @@ namespace CubismLive2DExtractor textures.UnionWith(textureBag); #endregion - #region physics3.json - var isPhysicsExported = false; - if (PhysicsMono != null) - { - var physicsDict = ParseMonoBehaviour(PhysicsMono, CubismMonoBehaviourType.Physics, assemblyLoader); - if (physicsDict != null) - { - try - { - var buff = ParsePhysics(physicsDict); - File.WriteAllText($"{destPath}{modelName}.physics3.json", buff); - isPhysicsExported = true; - } - catch (Exception e) - { - Logger.Warning($"Error in parsing physics data: {e.Message}"); - } - } - } - #endregion - #region cdi3.json var isCdiExported = false; if (ParametersCdi.Count > 0 || PartsCdi.Count > 0) { try { - isCdiExported = ExportCdiJson(destPath, modelName, assemblyLoader); + isCdiExported = ExportCdiJson(destPath, modelName); } catch (Exception e) { @@ -243,26 +284,34 @@ namespace CubismLive2DExtractor #region motion3.json var motions = new SortedDictionary(); var destMotionPath = Path.Combine(destPath, "motions") + Path.DirectorySeparatorChar; + var motionFps = 0f; if (motionMode == Live2DMotionMode.MonoBehaviour && FadeMotionLst != null) //Fade motions from Fade Motion List { Logger.Debug("Motion export method: MonoBehaviour (Fade motion)"); - var fadeMotionLstDict = ParseMonoBehaviour(FadeMotionLst, CubismMonoBehaviourType.FadeMotionList, assemblyLoader); + var fadeMotionLstDict = ParseMonoBehaviour(FadeMotionLst, CubismMonoBehaviourType.FadeMotionList, Assembly); if (fadeMotionLstDict != null) { - CubismObjectList.AssetsFile = FadeMotionLst.assetsFile; - var fadeMotionAssetList = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(fadeMotionLstDict)).GetFadeMotionAssetList(); - if (fadeMotionAssetList?.Count > 0) + var cubismFadeList = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(fadeMotionLstDict)); + var fadeMotionAssetList = new List(); + foreach (var motionPPtr in cubismFadeList.CubismFadeMotionObjects) + { + if (motionPPtr.TryGet(out var fadeMono, FadeMotionLst.assetsFile)) + { + fadeMotionAssetList.Add(fadeMono); + } + } + + if (fadeMotionAssetList.Count > 0) { FadeMotions = fadeMotionAssetList; Logger.Debug($"\"{FadeMotionLst.m_Name}\": found {fadeMotionAssetList.Count} motion(s)"); } } } - if (motionMode == Live2DMotionMode.MonoBehaviour && FadeMotions.Count > 0) //motion from MonoBehaviour { - ExportFadeMotions(destMotionPath, assemblyLoader, forceBezier, motions); + ExportFadeMotions(destMotionPath, forceBezier, motions, ref motionFps); } if (motions.Count == 0) //motion from AnimationClip @@ -274,16 +323,10 @@ namespace CubismLive2DExtractor exportMethod += "V2"; converter = new CubismMotion3Converter(AnimationClips, PartNames, ParameterNames); } - else if (GameObjects.Count > 0) //AnimationClipV1 + else if (Model?.ModelGameObject != null) //AnimationClipV1 { exportMethod += "V1"; - var rootTransform = GameObjects[0].m_Transform; - while (rootTransform.m_Father.TryGet(out var m_Father)) - { - rootTransform = m_Father; - } - rootTransform.m_GameObject.TryGet(out var rootGameObject); - converter = new CubismMotion3Converter(rootGameObject, AnimationClips); + converter = new CubismMotion3Converter(Model.ModelGameObject, AnimationClips); } if (motionMode == Live2DMotionMode.MonoBehaviour) @@ -294,7 +337,7 @@ namespace CubismLive2DExtractor } Logger.Debug($"Motion export method: {exportMethod}"); - ExportClipMotions(destMotionPath, converter, forceBezier, motions); + ExportClipMotions(destMotionPath, converter, forceBezier, motions, ref motionFps); } if (motions.Count == 0) @@ -311,6 +354,30 @@ namespace CubismLive2DExtractor var expressions = new JArray(); var destExpressionPath = Path.Combine(destPath, "expressions") + Path.DirectorySeparatorChar; + if (ExpressionLst != null) //Expressions from Expression List + { + Logger.Debug("Parsing expression list.."); + var expLstDict = ParseMonoBehaviour(ExpressionLst, CubismMonoBehaviourType.ExpressionList, Assembly); + if (expLstDict != null) + { + var cubismExpList = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(expLstDict)); + var expAssetList = new List(); + foreach (var expPPtr in cubismExpList.CubismExpressionObjects) + { + if (expPPtr.TryGet(out var expMono, ExpressionLst.assetsFile)) + { + expAssetList.Add(expMono); + } + } + + if (expAssetList.Count > 0) + { + Expressions = expAssetList; + Logger.Debug($"\"{ExpressionLst.m_Name}\": found {expAssetList.Count} expression(s)"); + } + } + } + if (Expressions.Count > 0) { Directory.CreateDirectory(destExpressionPath); @@ -318,7 +385,7 @@ namespace CubismLive2DExtractor foreach (var monoBehaviour in Expressions) { var expressionName = monoBehaviour.m_Name.Replace(".exp3", ""); - var expressionDict = ParseMonoBehaviour(monoBehaviour, CubismMonoBehaviourType.Expression, assemblyLoader); + var expressionDict = ParseMonoBehaviour(monoBehaviour, CubismMonoBehaviourType.Expression, Assembly); if (expressionDict == null) continue; @@ -339,7 +406,7 @@ namespace CubismLive2DExtractor { try { - isPoseExported = ExportPoseJson(destPath, modelName, assemblyLoader); + isPoseExported = ExportPoseJson(destPath, modelName); } catch (Exception e) { @@ -348,6 +415,27 @@ namespace CubismLive2DExtractor } #endregion + #region physics3.json + var isPhysicsExported = false; + if (PhysicsMono != null) + { + var physicsDict = ParseMonoBehaviour(PhysicsMono, CubismMonoBehaviourType.Physics, Assembly); + if (physicsDict != null) + { + try + { + var buff = ParsePhysics(physicsDict, motionFps); + File.WriteAllText($"{destPath}{modelName}.physics3.json", buff); + isPhysicsExported = true; + } + catch (Exception e) + { + Logger.Warning($"Error in parsing physics data: {e.Message}"); + } + } + } + #endregion + #region model3.json var groups = new List(); @@ -402,20 +490,21 @@ namespace CubismLive2DExtractor #endregion } - private void ExportFadeMotions(string destMotionPath, AssemblyLoader assemblyLoader, bool forceBezier, SortedDictionary motions) + private void ExportFadeMotions(string destMotionPath, bool forceBezier, SortedDictionary motions, ref float fps) { Directory.CreateDirectory(destMotionPath); foreach (var fadeMotionMono in FadeMotions) { - var fadeMotionDict = ParseMonoBehaviour(fadeMotionMono, CubismMonoBehaviourType.FadeMotion, assemblyLoader); + var fadeMotionDict = ParseMonoBehaviour(fadeMotionMono, CubismMonoBehaviourType.FadeMotion, Assembly); if (fadeMotionDict == null) continue; - var fadeMotion = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(fadeMotionDict)); + var fadeMotion = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(fadeMotionDict)); if (fadeMotion.ParameterIds.Length == 0) continue; var motionJson = new CubismMotion3Json(fadeMotion, ParameterNames, PartNames, forceBezier); + fps = motionJson.Meta.Fps; var animName = Path.GetFileNameWithoutExtension(fadeMotion.m_Name); if (motions.ContainsKey(animName)) @@ -430,7 +519,7 @@ namespace CubismLive2DExtractor } } - private static void ExportClipMotions(string destMotionPath, CubismMotion3Converter converter, bool forceBezier, SortedDictionary motions) + private static void ExportClipMotions(string destMotionPath, CubismMotion3Converter converter, bool forceBezier, SortedDictionary motions, ref float fps) { if (converter == null) return; @@ -448,7 +537,8 @@ namespace CubismLive2DExtractor continue; } var motionJson = new CubismMotion3Json(animation, forceBezier); - + fps = motionJson.Meta.Fps; + if (motions.ContainsKey(animName)) { animName = $"{animName}_{animation.GetHashCode()}"; @@ -462,12 +552,12 @@ namespace CubismLive2DExtractor } } - private bool ExportPoseJson(string destPath, string modelName, AssemblyLoader assemblyLoader) + private bool ExportPoseJson(string destPath, string modelName) { var groupDict = new SortedDictionary>(); foreach (var posePartMono in PoseParts) { - var posePartDict = ParseMonoBehaviour(posePartMono, CubismMonoBehaviourType.PosePart, assemblyLoader); + var posePartDict = ParseMonoBehaviour(posePartMono, CubismMonoBehaviourType.PosePart, Assembly); if (posePartDict == null) break; @@ -507,7 +597,7 @@ namespace CubismLive2DExtractor return true; } - private bool ExportCdiJson(string destPath, string modelName, AssemblyLoader assemblyLoader) + private bool ExportCdiJson(string destPath, string modelName) { var cdiJson = new CubismCdi3Json { @@ -518,7 +608,7 @@ namespace CubismLive2DExtractor var parameters = new SortedSet(); foreach (var paramMono in ParametersCdi) { - var displayName = GetDisplayName(paramMono, assemblyLoader); + var displayName = GetDisplayName(paramMono); if (displayName == null) break; @@ -536,7 +626,7 @@ namespace CubismLive2DExtractor var parts = new SortedSet(); foreach (var partMono in PartsCdi) { - var displayName = GetDisplayName(partMono, assemblyLoader); + var displayName = GetDisplayName(partMono); if (displayName == null) break; @@ -557,9 +647,9 @@ namespace CubismLive2DExtractor return true; } - private static string GetDisplayName(MonoBehaviour cdiMono, AssemblyLoader assemblyLoader) + private string GetDisplayName(MonoBehaviour cdiMono) { - var dict = ParseMonoBehaviour(cdiMono, CubismMonoBehaviourType.DisplayInfo, assemblyLoader); + var dict = ParseMonoBehaviour(cdiMono, CubismMonoBehaviourType.DisplayInfo, Assembly); if (dict == null) return null; @@ -571,5 +661,44 @@ namespace CubismLive2DExtractor } return name; } + + private bool TryGetFadeList(MonoBehaviour m_MonoBehaviour, out MonoBehaviour listMono) + { + return TryGetAsset(m_MonoBehaviour, CubismMonoBehaviourType.FadeController, "CubismFadeMotionList", out listMono); + } + + private bool TryGetExpressionList(MonoBehaviour m_MonoBehaviour, out MonoBehaviour listMono) + { + return TryGetAsset(m_MonoBehaviour, CubismMonoBehaviourType.ExpressionController, "ExpressionsList", out listMono); + } + + private bool TryGetRenderTexture(MonoBehaviour m_MonoBehaviour, out Texture2D renderTex) + { + return TryGetAsset(m_MonoBehaviour, CubismMonoBehaviourType.RenderTexture, "_mainTexture", out renderTex); + } + + private bool TryGetAsset(MonoBehaviour m_MonoBehaviour, CubismMonoBehaviourType cubismMonoType, string pptrField, out T result) where T : Object + { + result = null; + if (m_MonoBehaviour == null) + return false; + + var pptrDict = (OrderedDictionary)ParseMonoBehaviour(m_MonoBehaviour, cubismMonoType, Assembly)?[pptrField]; + if (pptrDict == null) + return false; + + var resultPPtr = GeneratePPtr(pptrDict, m_MonoBehaviour.assetsFile); + return resultPPtr.TryGet(out result); + } + + private PPtr GeneratePPtr(OrderedDictionary pptrDict, SerializedFile assetsFile = null) where T : Object + { + return new PPtr + { + m_FileID = (int)pptrDict["m_FileID"], + m_PathID = (long)pptrDict["m_PathID"], + AssetsFile = assetsFile + }; + } } } diff --git a/AssetStudioUtility/CubismLive2DExtractor/Live2DModelGroupOption.cs b/AssetStudioUtility/CubismLive2DExtractor/Live2DModelGroupOption.cs new file mode 100644 index 0000000..e15800e --- /dev/null +++ b/AssetStudioUtility/CubismLive2DExtractor/Live2DModelGroupOption.cs @@ -0,0 +1,8 @@ +namespace CubismLive2DExtractor +{ + public enum Live2DModelGroupOption + { + ContainerPath, + SourceFileName, + } +} diff --git a/AssetStudioUtility/MonoBehaviourConverter.cs b/AssetStudioUtility/MonoBehaviourConverter.cs index 9365e12..172e901 100644 --- a/AssetStudioUtility/MonoBehaviourConverter.cs +++ b/AssetStudioUtility/MonoBehaviourConverter.cs @@ -18,6 +18,45 @@ namespace AssetStudio var typeDefinitionConverter = new TypeDefinitionConverter(typeDef, helper, 1); m_Type.m_Nodes.AddRange(typeDefinitionConverter.ConvertToTypeTreeNodes()); } + else + { + switch (m_Script.m_ClassName) + { + case "CubismModel": + helper.AddMonoCubismModel(m_Type.m_Nodes, 1); + break; + case "CubismMoc": + helper.AddMonoCubismMoc(m_Type.m_Nodes, 1); + break; + case "CubismFadeController": + helper.AddMonoCubismFadeController(m_Type.m_Nodes, 1); + break; + case "CubismFadeMotionList": + helper.AddMonoCubismFadeList(m_Type.m_Nodes, 1); + break; + case "CubismFadeMotionData": + helper.AddMonoCubismFadeData(m_Type.m_Nodes, 1); + break; + case "CubismExpressionController": + helper.AddMonoCubismExpressionController(m_Type.m_Nodes, 1); + break; + case "CubismExpressionList": + helper.AddMonoCubismExpressionList(m_Type.m_Nodes, 1); + break; + case "CubismExpressionData": + helper.AddMonoCubismExpressionData(m_Type.m_Nodes, 1); + break; + case "CubismDisplayInfoParameterName": + helper.AddMonoCubismDisplayInfo(m_Type.m_Nodes, 1); + break; + case "CubismDisplayInfoPartName": + helper.AddMonoCubismDisplayInfo(m_Type.m_Nodes, 1); + break; + case "CubismPosePart": + helper.AddMonoCubismPosePart(m_Type.m_Nodes, 1); + break; + } + } } return m_Type; } diff --git a/AssetStudioUtility/SerializedTypeHelper.cs b/AssetStudioUtility/SerializedTypeHelper.cs index bc9839f..b81a59e 100644 --- a/AssetStudioUtility/SerializedTypeHelper.cs +++ b/AssetStudioUtility/SerializedTypeHelper.cs @@ -277,5 +277,100 @@ namespace AssetStudio nodes.Add(new TypeTreeNode("PropertyName", name, indent, false)); AddString(nodes, "id", indent + 1); } + + #region CubismLive2D + public void AddMonoCubismModel(List nodes, int indent) + { + AddPPtr(nodes, "CubismMoc", "_moc", indent); + } + + public void AddMonoCubismMoc(List nodes, int indent) + { + nodes.Add(new TypeTreeNode("vector", "_bytes", indent, align: true)); + AddArray(nodes, indent + 2); + nodes.Add(new TypeTreeNode("UInt8", "data", indent + 2, false)); + } + + public void AddMonoCubismPosePart(List nodes, int indent) + { + nodes.Add(new TypeTreeNode("int", "GroupIndex", indent, false)); + nodes.Add(new TypeTreeNode("int", "PartIndex", indent, false)); + nodes.Add(new TypeTreeNode("vector", "Link", indent, align: false)); + AddArray(nodes, indent + 2); + AddString(nodes, "data", indent + 2); + } + + public void AddMonoCubismDisplayInfo(List nodes, int indent) + { + AddString(nodes, "Name", indent); + AddString(nodes, "DisplayName", indent); + } + + public void AddMonoCubismFadeController(List nodes, int indent) + { + AddPPtr(nodes, "CubismFadeMotionList", "CubismFadeMotionList", indent); + } + + public void AddMonoCubismFadeList(List nodes, int indent) + { + nodes.Add(new TypeTreeNode("vector", "MotionInstanceIds", indent, align: false)); + AddArray(nodes, indent + 2); + nodes.Add(new TypeTreeNode("int", "data", indent + 2, align: false)); + nodes.Add(new TypeTreeNode("vector", "CubismFadeMotionObjects", indent, align: false)); + AddArray(nodes, indent + 2); + AddPPtr(nodes, "CubismFadeMotionData", "data", indent + 2); + } + + public void AddMonoCubismFadeData(List nodes, int indent) + { + AddString(nodes, "MotionName", indent); + nodes.Add(new TypeTreeNode("float", "FadeInTime", indent, false)); + nodes.Add(new TypeTreeNode("float", "FadeOutTime", indent, false)); + nodes.Add(new TypeTreeNode("vector", "ParameterIds", indent, align: false)); + AddArray(nodes, indent + 2); + AddString(nodes, "data", indent + 2); + nodes.Add(new TypeTreeNode("vector", "ParameterCurves", indent, align: false)); + AddArray(nodes, indent + 2); + AddAnimationCurve(nodes, "data", indent + 2); + nodes.Add(new TypeTreeNode("vector", "ParameterFadeInTimes", indent, align: false)); + AddArray(nodes, indent + 2); + nodes.Add(new TypeTreeNode("float", "data", indent + 2, align: false)); + nodes.Add(new TypeTreeNode("vector", "ParameterFadeOutTimes", indent, align: false)); + AddArray(nodes, indent + 2); + nodes.Add(new TypeTreeNode("float", "data", indent + 2, align: false)); + nodes.Add(new TypeTreeNode("float", "MotionLength", indent , align: false)); + } + + public void AddMonoCubismExpressionController(List nodes, int indent) + { + AddPPtr(nodes, "CubismExpressionList", "ExpressionsList", indent); + nodes.Add(new TypeTreeNode("int", "CurrentExpressionIndex", indent, false)); + } + + public void AddMonoCubismExpressionList(List nodes, int indent) + { + nodes.Add(new TypeTreeNode("vector", "CubismExpressionObjects", indent, align: false)); + AddArray(nodes, indent + 2); + AddPPtr(nodes, "CubismExpressionData", "data", indent + 2); + } + + private void AddMonoCubismExpressionParameter(List nodes, string name, int indent) + { + nodes.Add(new TypeTreeNode("SerializableExpressionParameter", name, indent, false)); + AddString(nodes, "Id", indent + 1); + nodes.Add(new TypeTreeNode("float", "Value", indent + 1, false)); + nodes.Add(new TypeTreeNode("int", "Blend", indent + 1, false)); + } + + public void AddMonoCubismExpressionData(List nodes, int indent) + { + AddString(nodes, "Type", indent); + nodes.Add(new TypeTreeNode("float", "FadeInTime", indent, false)); + nodes.Add(new TypeTreeNode("float", "FadeOutTime", indent, false)); + nodes.Add(new TypeTreeNode("SerializableExpressionParameter", "Parameters", indent, align: false)); + AddArray(nodes, indent + 2); + AddMonoCubismExpressionParameter(nodes, "data", indent + 2); + } + #endregion } }