diff --git a/AssetStudio/7zip/Common/CRC.cs b/AssetStudio/7zip/Common/CRC.cs index d03fcec..f18d641 100644 --- a/AssetStudio/7zip/Common/CRC.cs +++ b/AssetStudio/7zip/Common/CRC.cs @@ -22,26 +22,26 @@ namespace SevenZip } } - uint _value = 0xFFFFFFFF; + private uint _value = 0xFFFFFFFF; public void Init() { _value = 0xFFFFFFFF; } public void UpdateByte(byte b) { - _value = Table[(((byte)(_value)) ^ b)] ^ (_value >> 8); + _value = Table[(byte)_value ^ b] ^ (_value >> 8); } public void Update(byte[] data, uint offset, uint size) { for (uint i = 0; i < size; i++) - _value = Table[(((byte)(_value)) ^ data[offset + i])] ^ (_value >> 8); + _value = Table[(byte)_value ^ data[offset + i]] ^ (_value >> 8); } public uint GetDigest() { return _value ^ 0xFFFFFFFF; } - static uint CalculateDigest(byte[] data, uint offset, uint size) + public static uint CalculateDigest(byte[] data, uint offset, uint size) { - CRC crc = new CRC(); + var crc = new CRC(); // crc.Init(); crc.Update(data, offset, size); return crc.GetDigest(); @@ -49,7 +49,7 @@ namespace SevenZip static bool VerifyDigest(uint digest, byte[] data, uint offset, uint size) { - return (CalculateDigest(data, offset, size) == digest); + return CalculateDigest(data, offset, size) == digest; } } } diff --git a/AssetStudio/CubismModel.cs b/AssetStudio/CubismModel.cs new file mode 100644 index 0000000..49b2ee5 --- /dev/null +++ b/AssetStudio/CubismModel.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Text; +using static AssetStudio.EndianSpanReader; + +namespace AssetStudio +{ + public enum CubismSDKVersion : byte + { + V30 = 1, + V33, + V40, + V42, + V50 + } + + 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; } + public byte[] ModelData { get; } + private static bool IsBigEndian { get; set; } + + public CubismModel(MonoBehaviour moc) + { + var reader = moc.reader; + reader.Reset(); + reader.Position += 28; //PPtr m_GameObject, m_Enabled, PPtr + reader.ReadAlignedString(); //m_Name + var 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); + CentralPosX = ToSingle(ModelData, canvasInfoOffset + 4); + CentralPosY = ToSingle(ModelData, canvasInfoOffset + 8); + CanvasWidth = ToSingle(ModelData, canvasInfoOffset + 12); + CanvasHeight = ToSingle(ModelData, canvasInfoOffset + 16); + + //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); + } + + 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) //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); + } + } + + public void Dispose() + { + Dispose(true); + } + } +} diff --git a/AssetStudio/EndianSpanReader.cs b/AssetStudio/EndianSpanReader.cs index 1538a86..9a946a9 100644 --- a/AssetStudio/EndianSpanReader.cs +++ b/AssetStudio/EndianSpanReader.cs @@ -12,6 +12,13 @@ namespace AssetStudio : BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(start)); } + public static uint SpanToUint16(Span data, int start, bool isBigEndian) + { + return isBigEndian + ? BinaryPrimitives.ReadUInt16BigEndian(data.Slice(start)) + : BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(start)); + } + public static long SpanToInt64(Span data, int start, bool isBigEndian) { return isBigEndian diff --git a/AssetStudioCLI/Options/CLIOptions.cs b/AssetStudioCLI/Options/CLIOptions.cs index 3fd4810..48c2486 100644 --- a/AssetStudioCLI/Options/CLIOptions.cs +++ b/AssetStudioCLI/Options/CLIOptions.cs @@ -748,7 +748,11 @@ namespace AssetStudioCLI.Options o_l2dMotionMode.Value = CubismLive2DExtractor.Live2DMotionMode.MonoBehaviour; break; case "animationclip": - o_l2dMotionMode.Value = CubismLive2DExtractor.Live2DMotionMode.AnimationClip; + case "animationclipv2": + o_l2dMotionMode.Value = CubismLive2DExtractor.Live2DMotionMode.AnimationClipV2; + break; + case "animationclipv1": + o_l2dMotionMode.Value = CubismLive2DExtractor.Live2DMotionMode.AnimationClipV1; break; default: Console.WriteLine($"{"Error".Color(brightRed)} during parsing [{option}] option. Unsupported Live2D motion mode: [{value.Color(brightRed)}].\n"); diff --git a/AssetStudioCLI/Studio.cs b/AssetStudioCLI/Studio.cs index e095f40..b75c8a9 100644 --- a/AssetStudioCLI/Studio.cs +++ b/AssetStudioCLI/Studio.cs @@ -1,12 +1,12 @@ using AssetStudio; using AssetStudioCLI.Options; +using CubismLive2DExtractor; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml.Linq; using static AssetStudioCLI.Exporter; -using static CubismLive2DExtractor.Live2DExtractor; using Ansi = AssetStudio.ColorConsole; namespace AssetStudioCLI @@ -17,6 +17,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(); private static Dictionary containers = new Dictionary(); static Studio() @@ -125,14 +126,16 @@ namespace AssetStudioCLI assetItem.Text = m_Shader.m_ParsedForm?.m_Name ?? m_Shader.m_Name; break; case MonoBehaviour m_MonoBehaviour: - if (m_MonoBehaviour.m_Name == "" && m_MonoBehaviour.m_Script.TryGet(out var m_Script)) + var assetName = m_MonoBehaviour.m_Name; + if (m_MonoBehaviour.m_Script.TryGet(out var m_Script)) { - assetItem.Text = m_Script.m_ClassName; - } - else - { - assetItem.Text = m_MonoBehaviour.m_Name; + assetName = assetName == "" ? m_Script.m_ClassName : assetName; + if (m_Script.m_ClassName == "CubismMoc") + { + cubismMocList.Add(m_MonoBehaviour); + } } + assetItem.Text = assetName; break; case GameObject m_GameObject: assetItem.Text = m_GameObject.m_Name; @@ -620,64 +623,57 @@ namespace AssetStudioCLI public static void ExportLive2D() { var baseDestPath = Path.Combine(CLIOptions.o_outputFolder.Value, "Live2DOutput"); - var useFullContainerPath = false; + var useFullContainerPath = true; + var mocPathList = new List(); + var basePathSet = new HashSet(); var motionMode = CLIOptions.o_l2dMotionMode.Value; var forceBezier = CLIOptions.f_l2dForceBezier.Value; - Progress.Reset(); - Logger.Info($"Searching for Live2D files..."); - - var cubismMocs = parsedAssetsList.Where(x => - { - if (x.Type == ClassIDType.MonoBehaviour) - { - ((MonoBehaviour)x.Asset).m_Script.TryGet(out var m_Script); - return m_Script?.m_ClassName == "CubismMoc"; - } - return false; - }).Select(x => x.Asset).ToArray(); - - if (cubismMocs.Length == 0) + if (cubismMocList.Count == 0) { Logger.Default.Log(LoggerEvent.Info, "Live2D Cubism models were not found.", ignoreLevel: true); return; } - if (cubismMocs.Length > 1) + + Progress.Reset(); + Logger.Info($"Searching for Live2D files..."); + + foreach (var mocMonoBehaviour in cubismMocList) { - var basePathSet = cubismMocs.Select(x => - { - var pathLen = containers.TryGetValue(x, out var itemContainer) ? itemContainer.LastIndexOf("/") : 0; - pathLen = pathLen < 0 ? containers[x].Length : pathLen; - return itemContainer?.Substring(0, pathLen); - }).ToHashSet(); + if (!containers.TryGetValue(mocMonoBehaviour, out var fullContainerPath)) + continue; - if (basePathSet.All(x => x == null)) - { - Logger.Error($"Live2D Cubism export error: Cannot find any model related files."); - return; - } - - if (basePathSet.Count != cubismMocs.Length) - { - useFullContainerPath = true; - Logger.Debug($"useFullContainerPath: {useFullContainerPath}"); - } + var pathSepIndex = fullContainerPath.LastIndexOf('/'); + var basePath = pathSepIndex > 0 + ? fullContainerPath.Substring(0, pathSepIndex) + : fullContainerPath; + basePathSet.Add(basePath); + mocPathList.Add(fullContainerPath); } - var basePathList = cubismMocs.Select(x => + if (mocPathList.Count == 0) { - containers.TryGetValue(x, out var container); - container = useFullContainerPath - ? container - : container?.Substring(0, container.LastIndexOf("/")); - return container; - }).Where(x => x != null).ToList(); + 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.ToLookup( - x => basePathList.Find(b => x.Value.Contains(b) && x.Value.Split('/').Any(y => y == b.Substring(b.LastIndexOf("/") + 1))), + 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 == "") + { + Logger.Warning("Specifying the assembly folder may be needed for proper extraction"); + } + var totalModelCount = lookup.LongCount(x => x.Key != null); Logger.Info($"Found {totalModelCount} model(s)."); var modelCounter = 0; @@ -691,11 +687,16 @@ namespace AssetStudioCLI 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 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; - ExtractLive2D(assets, destPath, modelName, assemblyLoader, motionMode, forceBezier); + var modelExtractor = new Live2DExtractor(assets); + modelExtractor.ExtractCubismModel(destPath, modelName, motionMode, assemblyLoader, forceBezier); modelCounter++; } catch (Exception ex) diff --git a/AssetStudioGUI/AssetStudioGUIForm.Designer.cs b/AssetStudioGUI/AssetStudioGUIForm.Designer.cs index 38adb76..718732b 100644 --- a/AssetStudioGUI/AssetStudioGUIForm.Designer.cs +++ b/AssetStudioGUI/AssetStudioGUIForm.Designer.cs @@ -68,7 +68,12 @@ this.toolStripMenuItem8 = new System.Windows.Forms.ToolStripMenuItem(); this.toolStripMenuItem9 = new System.Windows.Forms.ToolStripMenuItem(); this.toolStripSeparator6 = new System.Windows.Forms.ToolStripSeparator(); - this.allLive2DModelsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.live2DCubismModelsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.allL2DModelsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.selectedL2DModelsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.l2DModelWithFadeListToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.l2DModelWithFadeMotionsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.l2DModelWithClipsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator(); this.toolStripMenuItem10 = new System.Windows.Forms.ToolStripMenuItem(); this.toolStripMenuItem11 = new System.Windows.Forms.ToolStripMenuItem(); @@ -140,8 +145,12 @@ this.contextMenuStrip1 = new System.Windows.Forms.ContextMenuStrip(this.components); this.copyToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.exportSelectedAssetsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.exportAnimatorwithselectedAnimationClipMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.dumpSelectedAssetsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.exportAnimatorWithSelectedAnimationClipMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.exportAsLive2DModelToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.exportL2DWithFadeLstToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.exportL2DWithFadeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.exportL2DWithClipsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.goToSceneHierarchyToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.showOriginalFileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.menuStrip1.SuspendLayout(); @@ -327,7 +336,7 @@ this.exportAllObjectssplitToolStripMenuItem1.Name = "exportAllObjectssplitToolStripMenuItem1"; this.exportAllObjectssplitToolStripMenuItem1.Size = new System.Drawing.Size(382, 22); this.exportAllObjectssplitToolStripMenuItem1.Text = "Export all objects (split)"; - this.exportAllObjectssplitToolStripMenuItem1.Click += new System.EventHandler(this.exportAllObjectssplitToolStripMenuItem1_Click); + this.exportAllObjectssplitToolStripMenuItem1.Click += new System.EventHandler(this.exportAllObjectsSplitToolStripMenuItem1_Click); // // exportSelectedObjectsToolStripMenuItem // @@ -341,7 +350,7 @@ this.exportSelectedObjectsWithAnimationClipToolStripMenuItem.Name = "exportSelectedObjectsWithAnimationClipToolStripMenuItem"; this.exportSelectedObjectsWithAnimationClipToolStripMenuItem.Size = new System.Drawing.Size(382, 22); this.exportSelectedObjectsWithAnimationClipToolStripMenuItem.Text = "Export selected objects (split) + selected AnimationClips"; - this.exportSelectedObjectsWithAnimationClipToolStripMenuItem.Click += new System.EventHandler(this.exportObjectswithAnimationClipMenuItem_Click); + this.exportSelectedObjectsWithAnimationClipToolStripMenuItem.Click += new System.EventHandler(this.exportObjectsWithAnimationClipMenuItem_Click); // // toolStripSeparator1 // @@ -353,14 +362,14 @@ this.exportSelectedObjectsmergeToolStripMenuItem.Name = "exportSelectedObjectsmergeToolStripMenuItem"; this.exportSelectedObjectsmergeToolStripMenuItem.Size = new System.Drawing.Size(382, 22); this.exportSelectedObjectsmergeToolStripMenuItem.Text = "Export selected objects (merge)"; - this.exportSelectedObjectsmergeToolStripMenuItem.Click += new System.EventHandler(this.exportSelectedObjectsmergeToolStripMenuItem_Click); + this.exportSelectedObjectsmergeToolStripMenuItem.Click += new System.EventHandler(this.exportSelectedObjectsMergeToolStripMenuItem_Click); // // exportSelectedObjectsmergeWithAnimationClipToolStripMenuItem // this.exportSelectedObjectsmergeWithAnimationClipToolStripMenuItem.Name = "exportSelectedObjectsmergeWithAnimationClipToolStripMenuItem"; this.exportSelectedObjectsmergeWithAnimationClipToolStripMenuItem.Size = new System.Drawing.Size(382, 22); this.exportSelectedObjectsmergeWithAnimationClipToolStripMenuItem.Text = "Export selected objects (merge) + selected AnimationClips"; - this.exportSelectedObjectsmergeWithAnimationClipToolStripMenuItem.Click += new System.EventHandler(this.exportSelectedObjectsmergeWithAnimationClipToolStripMenuItem_Click); + this.exportSelectedObjectsmergeWithAnimationClipToolStripMenuItem.Click += new System.EventHandler(this.exportSelectedObjectsMergeWithAnimationClipToolStripMenuItem_Click); // // exportToolStripMenuItem // @@ -374,7 +383,7 @@ this.toolStripMenuItem2, this.toolStripMenuItem3, this.toolStripSeparator6, - this.allLive2DModelsToolStripMenuItem, + this.live2DCubismModelsToolStripMenuItem, this.toolStripSeparator2, this.toolStripMenuItem10}); this.exportToolStripMenuItem.Name = "exportToolStripMenuItem"; @@ -412,7 +421,7 @@ this.exportAnimatorWithSelectedAnimationClipToolStripMenuItem.Name = "exportAnimatorWithSelectedAnimationClipToolStripMenuItem"; this.exportAnimatorWithSelectedAnimationClipToolStripMenuItem.Size = new System.Drawing.Size(266, 22); this.exportAnimatorWithSelectedAnimationClipToolStripMenuItem.Text = "Animator + selected AnimationClips"; - this.exportAnimatorWithSelectedAnimationClipToolStripMenuItem.Click += new System.EventHandler(this.exportAnimatorwithAnimationClipMenuItem_Click); + this.exportAnimatorWithSelectedAnimationClipToolStripMenuItem.Click += new System.EventHandler(this.exportAnimatorWithAnimationClipMenuItem_Click); // // toolStripSeparator4 // @@ -486,12 +495,52 @@ this.toolStripSeparator6.Name = "toolStripSeparator6"; this.toolStripSeparator6.Size = new System.Drawing.Size(263, 6); // - // allLive2DModelsToolStripMenuItem + // live2DCubismModelsToolStripMenuItem // - this.allLive2DModelsToolStripMenuItem.Name = "allLive2DModelsToolStripMenuItem"; - this.allLive2DModelsToolStripMenuItem.Size = new System.Drawing.Size(266, 22); - this.allLive2DModelsToolStripMenuItem.Text = "Live2D Cubism models"; - this.allLive2DModelsToolStripMenuItem.Click += new System.EventHandler(this.allLive2DModelsToolStripMenuItem_Click); + this.live2DCubismModelsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.allL2DModelsToolStripMenuItem, + this.selectedL2DModelsToolStripMenuItem, + this.l2DModelWithFadeListToolStripMenuItem, + this.l2DModelWithFadeMotionsToolStripMenuItem, + this.l2DModelWithClipsToolStripMenuItem}); + this.live2DCubismModelsToolStripMenuItem.Name = "live2DCubismModelsToolStripMenuItem"; + this.live2DCubismModelsToolStripMenuItem.Size = new System.Drawing.Size(266, 22); + this.live2DCubismModelsToolStripMenuItem.Text = "Live2D Cubism models"; + // + // allL2DModelsToolStripMenuItem + // + this.allL2DModelsToolStripMenuItem.Name = "allL2DModelsToolStripMenuItem"; + this.allL2DModelsToolStripMenuItem.Size = new System.Drawing.Size(292, 22); + this.allL2DModelsToolStripMenuItem.Text = "All models"; + this.allL2DModelsToolStripMenuItem.Click += new System.EventHandler(this.exportAllL2D_Click); + // + // selectedL2DModelsToolStripMenuItem + // + this.selectedL2DModelsToolStripMenuItem.Name = "selectedL2DModelsToolStripMenuItem"; + this.selectedL2DModelsToolStripMenuItem.Size = new System.Drawing.Size(292, 22); + this.selectedL2DModelsToolStripMenuItem.Text = "Selected models"; + this.selectedL2DModelsToolStripMenuItem.Click += new System.EventHandler(this.exportSelectedL2D_Click); + // + // l2DModelWithFadeListToolStripMenuItem + // + this.l2DModelWithFadeListToolStripMenuItem.Name = "l2DModelWithFadeListToolStripMenuItem"; + this.l2DModelWithFadeListToolStripMenuItem.Size = new System.Drawing.Size(292, 22); + this.l2DModelWithFadeListToolStripMenuItem.Text = "Model + selected Fade Motion List"; + this.l2DModelWithFadeListToolStripMenuItem.Click += new System.EventHandler(this.exportSelectedL2DWithFadeList_Click); + // + // l2DModelWithFadeMotionsToolStripMenuItem + // + this.l2DModelWithFadeMotionsToolStripMenuItem.Name = "l2DModelWithFadeMotionsToolStripMenuItem"; + this.l2DModelWithFadeMotionsToolStripMenuItem.Size = new System.Drawing.Size(292, 22); + this.l2DModelWithFadeMotionsToolStripMenuItem.Text = "Model + selected Fade motions"; + this.l2DModelWithFadeMotionsToolStripMenuItem.Click += new System.EventHandler(this.exportSelectedL2DWithFadeMotions_Click); + // + // l2DModelWithClipsToolStripMenuItem + // + this.l2DModelWithClipsToolStripMenuItem.Name = "l2DModelWithClipsToolStripMenuItem"; + this.l2DModelWithClipsToolStripMenuItem.Size = new System.Drawing.Size(292, 22); + this.l2DModelWithClipsToolStripMenuItem.Text = "Model + selected AnimationClip motions"; + this.l2DModelWithClipsToolStripMenuItem.Click += new System.EventHandler(this.exportSelectedL2DWithClips_Click); // // toolStripSeparator2 // @@ -1226,46 +1275,82 @@ this.contextMenuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { this.copyToolStripMenuItem, this.exportSelectedAssetsToolStripMenuItem, - this.exportAnimatorwithselectedAnimationClipMenuItem, this.dumpSelectedAssetsToolStripMenuItem, + this.exportAnimatorWithSelectedAnimationClipMenuItem, + this.exportAsLive2DModelToolStripMenuItem, + this.exportL2DWithFadeLstToolStripMenuItem, + this.exportL2DWithFadeToolStripMenuItem, + this.exportL2DWithClipsToolStripMenuItem, this.goToSceneHierarchyToolStripMenuItem, this.showOriginalFileToolStripMenuItem}); this.contextMenuStrip1.Name = "contextMenuStrip1"; - this.contextMenuStrip1.Size = new System.Drawing.Size(304, 136); + this.contextMenuStrip1.Size = new System.Drawing.Size(332, 246); // // copyToolStripMenuItem // this.copyToolStripMenuItem.Name = "copyToolStripMenuItem"; - this.copyToolStripMenuItem.Size = new System.Drawing.Size(303, 22); + this.copyToolStripMenuItem.Size = new System.Drawing.Size(331, 22); this.copyToolStripMenuItem.Text = "Copy text"; this.copyToolStripMenuItem.Click += new System.EventHandler(this.copyToolStripMenuItem_Click); // // exportSelectedAssetsToolStripMenuItem // this.exportSelectedAssetsToolStripMenuItem.Name = "exportSelectedAssetsToolStripMenuItem"; - this.exportSelectedAssetsToolStripMenuItem.Size = new System.Drawing.Size(303, 22); + this.exportSelectedAssetsToolStripMenuItem.Size = new System.Drawing.Size(331, 22); this.exportSelectedAssetsToolStripMenuItem.Text = "Export selected assets"; this.exportSelectedAssetsToolStripMenuItem.Click += new System.EventHandler(this.exportSelectedAssetsToolStripMenuItem_Click); // - // exportAnimatorwithselectedAnimationClipMenuItem - // - this.exportAnimatorwithselectedAnimationClipMenuItem.Name = "exportAnimatorwithselectedAnimationClipMenuItem"; - this.exportAnimatorwithselectedAnimationClipMenuItem.Size = new System.Drawing.Size(303, 22); - this.exportAnimatorwithselectedAnimationClipMenuItem.Text = "Export Animator + selected AnimationClips"; - this.exportAnimatorwithselectedAnimationClipMenuItem.Visible = false; - this.exportAnimatorwithselectedAnimationClipMenuItem.Click += new System.EventHandler(this.exportAnimatorwithAnimationClipMenuItem_Click); - // // dumpSelectedAssetsToolStripMenuItem // this.dumpSelectedAssetsToolStripMenuItem.Name = "dumpSelectedAssetsToolStripMenuItem"; - this.dumpSelectedAssetsToolStripMenuItem.Size = new System.Drawing.Size(303, 22); + this.dumpSelectedAssetsToolStripMenuItem.Size = new System.Drawing.Size(331, 22); this.dumpSelectedAssetsToolStripMenuItem.Text = "Dump selected assets"; this.dumpSelectedAssetsToolStripMenuItem.Click += new System.EventHandler(this.dumpSelectedAssetsToolStripMenuItem_Click); // + // exportAnimatorWithSelectedAnimationClipMenuItem + // + this.exportAnimatorWithSelectedAnimationClipMenuItem.Name = "exportAnimatorWithSelectedAnimationClipMenuItem"; + this.exportAnimatorWithSelectedAnimationClipMenuItem.Size = new System.Drawing.Size(331, 22); + this.exportAnimatorWithSelectedAnimationClipMenuItem.Text = "Export Animator + selected AnimationClips"; + this.exportAnimatorWithSelectedAnimationClipMenuItem.Visible = false; + this.exportAnimatorWithSelectedAnimationClipMenuItem.Click += new System.EventHandler(this.exportAnimatorWithAnimationClipMenuItem_Click); + // + // exportAsLive2DModelToolStripMenuItem + // + this.exportAsLive2DModelToolStripMenuItem.Name = "exportAsLive2DModelToolStripMenuItem"; + this.exportAsLive2DModelToolStripMenuItem.Size = new System.Drawing.Size(331, 22); + this.exportAsLive2DModelToolStripMenuItem.Text = "Export as Live2D model(s)"; + this.exportAsLive2DModelToolStripMenuItem.Visible = false; + this.exportAsLive2DModelToolStripMenuItem.Click += new System.EventHandler(this.exportSelectedL2D_Click); + // + // exportL2DWithFadeLstToolStripMenuItem + // + this.exportL2DWithFadeLstToolStripMenuItem.Name = "exportL2DWithFadeLstToolStripMenuItem"; + this.exportL2DWithFadeLstToolStripMenuItem.Size = new System.Drawing.Size(331, 22); + this.exportL2DWithFadeLstToolStripMenuItem.Text = "Export Live2D model + selected Fade Motion List"; + this.exportL2DWithFadeLstToolStripMenuItem.Visible = false; + this.exportL2DWithFadeLstToolStripMenuItem.Click += new System.EventHandler(this.exportSelectedL2DWithFadeList_Click); + // + // exportL2DWithFadeToolStripMenuItem + // + this.exportL2DWithFadeToolStripMenuItem.Name = "exportL2DWithFadeToolStripMenuItem"; + this.exportL2DWithFadeToolStripMenuItem.Size = new System.Drawing.Size(331, 22); + this.exportL2DWithFadeToolStripMenuItem.Text = "Export Live2D model + selected Fade motions"; + this.exportL2DWithFadeToolStripMenuItem.Visible = false; + this.exportL2DWithFadeToolStripMenuItem.Click += new System.EventHandler(this.exportSelectedL2DWithFadeMotions_Click); + // + // exportL2DWithClipsToolStripMenuItem + // + this.exportL2DWithClipsToolStripMenuItem.Name = "exportL2DWithClipsToolStripMenuItem"; + this.exportL2DWithClipsToolStripMenuItem.Size = new System.Drawing.Size(331, 22); + this.exportL2DWithClipsToolStripMenuItem.Text = "Export Live2D model + selected AnimationClips"; + this.exportL2DWithClipsToolStripMenuItem.Visible = false; + this.exportL2DWithClipsToolStripMenuItem.Click += new System.EventHandler(this.exportSelectedL2DWithClips_Click); + // // goToSceneHierarchyToolStripMenuItem // this.goToSceneHierarchyToolStripMenuItem.Name = "goToSceneHierarchyToolStripMenuItem"; - this.goToSceneHierarchyToolStripMenuItem.Size = new System.Drawing.Size(303, 22); + this.goToSceneHierarchyToolStripMenuItem.Size = new System.Drawing.Size(331, 22); this.goToSceneHierarchyToolStripMenuItem.Text = "Go to scene hierarchy"; this.goToSceneHierarchyToolStripMenuItem.Visible = false; this.goToSceneHierarchyToolStripMenuItem.Click += new System.EventHandler(this.goToSceneHierarchyToolStripMenuItem_Click); @@ -1273,7 +1358,7 @@ // showOriginalFileToolStripMenuItem // this.showOriginalFileToolStripMenuItem.Name = "showOriginalFileToolStripMenuItem"; - this.showOriginalFileToolStripMenuItem.Size = new System.Drawing.Size(303, 22); + this.showOriginalFileToolStripMenuItem.Size = new System.Drawing.Size(331, 22); this.showOriginalFileToolStripMenuItem.Text = "Show original file"; this.showOriginalFileToolStripMenuItem.Visible = false; this.showOriginalFileToolStripMenuItem.Click += new System.EventHandler(this.showOriginalFileToolStripMenuItem_Click); @@ -1387,7 +1472,7 @@ private OpenTK.GLControl glControl1; private System.Windows.Forms.ContextMenuStrip contextMenuStrip1; private System.Windows.Forms.ToolStripMenuItem showOriginalFileToolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem exportAnimatorwithselectedAnimationClipMenuItem; + private System.Windows.Forms.ToolStripMenuItem exportAnimatorWithSelectedAnimationClipMenuItem; private System.Windows.Forms.ToolStripMenuItem exportSelectedAssetsToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem filterTypeToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem allToolStripMenuItem; @@ -1437,13 +1522,22 @@ private System.Windows.Forms.ComboBox listSearchHistory; private System.Windows.Forms.RichTextBox listSearch; private System.Windows.Forms.ToolStripSeparator toolStripSeparator6; - private System.Windows.Forms.ToolStripMenuItem allLive2DModelsToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem showRelatedAssetsToolStripMenuItem; private System.Windows.Forms.ToolStripSeparator toolStripSeparator7; private System.Windows.Forms.ListView assetListView; private System.Windows.Forms.ToolStripMenuItem showConsoleToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem writeLogToFileToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem buildTreeStructureToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem exportL2DWithClipsToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem exportAsLive2DModelToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem live2DCubismModelsToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem allL2DModelsToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem selectedL2DModelsToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem l2DModelWithClipsToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem l2DModelWithFadeMotionsToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem exportL2DWithFadeToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem l2DModelWithFadeListToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem exportL2DWithFadeLstToolStripMenuItem; } } diff --git a/AssetStudioGUI/AssetStudioGUIForm.cs b/AssetStudioGUI/AssetStudioGUIForm.cs index 4f89887..91829f0 100644 --- a/AssetStudioGUI/AssetStudioGUIForm.cs +++ b/AssetStudioGUI/AssetStudioGUIForm.cs @@ -822,7 +822,16 @@ namespace AssetStudioGUI PreviewTextAsset(assetItem.Asset as TextAsset); break; case ClassIDType.MonoBehaviour: - PreviewMonoBehaviour(assetItem.Asset as MonoBehaviour); + var m_MonoBehaviour = (MonoBehaviour)assetItem.Asset; + if (m_MonoBehaviour.m_Script.TryGet(out var m_Script)) + { + if (m_Script.m_ClassName == "CubismMoc") + { + PreviewMoc(assetItem, m_MonoBehaviour); + break; + } + } + PreviewMonoBehaviour(m_MonoBehaviour); break; case ClassIDType.Font: PreviewFont(assetItem.Asset as Font); @@ -1083,6 +1092,27 @@ namespace AssetStudioGUI PreviewText(str); } + private void PreviewMoc(AssetItem assetItem, MonoBehaviour m_MonoBehaviour) + { + using (var cubismModel = new CubismModel(m_MonoBehaviour)) + { + var sb = new StringBuilder(); + sb.AppendLine($"SDK Version: {cubismModel.VersionDescription}"); + if (cubismModel.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}"); + } + assetItem.InfoText = sb.ToString(); + } + StatusStripUpdate("Can be exported as Live2D Cubism model."); + } + private void PreviewFont(Font m_Font) { if (m_Font.m_FontData != null) @@ -1374,6 +1404,7 @@ namespace AssetStudioGUI classesListView.Groups.Clear(); selectedAnimationAssetsList.Clear(); selectedIndicesPrevList.Clear(); + cubismMocList.Clear(); previewPanel.BackgroundImage = Properties.Resources.preview; imageTexture?.Dispose(); imageTexture = null; @@ -1417,7 +1448,11 @@ namespace AssetStudioGUI { goToSceneHierarchyToolStripMenuItem.Visible = false; showOriginalFileToolStripMenuItem.Visible = false; - exportAnimatorwithselectedAnimationClipMenuItem.Visible = false; + exportAnimatorWithSelectedAnimationClipMenuItem.Visible = false; + exportAsLive2DModelToolStripMenuItem.Visible = false; + exportL2DWithFadeLstToolStripMenuItem.Visible = false; + exportL2DWithFadeToolStripMenuItem.Visible = false; + exportL2DWithClipsToolStripMenuItem.Visible = false; if (assetListView.SelectedIndices.Count == 1) { @@ -1427,10 +1462,42 @@ namespace AssetStudioGUI if (assetListView.SelectedIndices.Count >= 1) { var selectedAssets = GetSelectedAssets(); - if (selectedAssets.Any(x => x.Type == ClassIDType.Animator) && selectedAssets.Any(x => x.Type == ClassIDType.AnimationClip)) + + var selectedTypes = (SelectedAssetType)0; + foreach (var asset in selectedAssets) { - exportAnimatorwithselectedAnimationClipMenuItem.Visible = true; + switch (asset.Asset) + { + case MonoBehaviour m_MonoBehaviour: + if (Studio.cubismMocList.Count > 0 && m_MonoBehaviour.m_Script.TryGet(out var m_Script)) + { + if (m_Script.m_ClassName == "CubismMoc") + { + selectedTypes |= SelectedAssetType.MonoBehaviourMoc; + } + else if (m_Script.m_ClassName == "CubismFadeMotionData") + { + selectedTypes |= SelectedAssetType.MonoBehaviourFade; + } + else if (m_Script.m_ClassName == "CubismFadeMotionList") + { + selectedTypes |= SelectedAssetType.MonoBehaviourFadeLst; + } + } + break; + case AnimationClip _: + selectedTypes |= SelectedAssetType.AnimationClip; + break; + case Animator _: + selectedTypes |= SelectedAssetType.Animator; + break; + } } + exportAnimatorWithSelectedAnimationClipMenuItem.Visible = (selectedTypes & SelectedAssetType.Animator) !=0 && (selectedTypes & SelectedAssetType.AnimationClip) != 0; + exportAsLive2DModelToolStripMenuItem.Visible = (selectedTypes & SelectedAssetType.MonoBehaviourMoc) != 0; + exportL2DWithFadeLstToolStripMenuItem.Visible = (selectedTypes & SelectedAssetType.MonoBehaviourMoc) !=0 && (selectedTypes & SelectedAssetType.MonoBehaviourFadeLst) != 0; + exportL2DWithFadeToolStripMenuItem.Visible = (selectedTypes & SelectedAssetType.MonoBehaviourMoc) != 0 && (selectedTypes & SelectedAssetType.MonoBehaviourFade) !=0; + exportL2DWithClipsToolStripMenuItem.Visible = (selectedTypes & SelectedAssetType.MonoBehaviourMoc) !=0 && (selectedTypes & SelectedAssetType.AnimationClip) != 0; } var selectedElement = assetListView.HitTest(new Point(e.X, e.Y)); @@ -1458,13 +1525,13 @@ namespace AssetStudioGUI private void showOriginalFileToolStripMenuItem_Click(object sender, EventArgs e) { - var selectasset = (AssetItem)assetListView.Items[assetListView.SelectedIndices[0]]; - var args = $"/select, \"{selectasset.SourceFile.originalPath ?? selectasset.SourceFile.fullName}\""; + var selectAsset = (AssetItem)assetListView.Items[assetListView.SelectedIndices[0]]; + var args = $"/select, \"{selectAsset.SourceFile.originalPath ?? selectAsset.SourceFile.fullName}\""; var pfi = new ProcessStartInfo("explorer.exe", args); Process.Start(pfi); } - private void exportAnimatorwithAnimationClipMenuItem_Click(object sender, EventArgs e) + private void exportAnimatorWithAnimationClipMenuItem_Click(object sender, EventArgs e) { AssetItem animator = null; var selectedAssets = GetSelectedAssets(); @@ -1494,7 +1561,7 @@ namespace AssetStudioGUI ExportObjects(false); } - private void exportObjectswithAnimationClipMenuItem_Click(object sender, EventArgs e) + private void exportObjectsWithAnimationClipMenuItem_Click(object sender, EventArgs e) { ExportObjects(true); } @@ -1527,12 +1594,12 @@ namespace AssetStudioGUI } } - private void exportSelectedObjectsmergeToolStripMenuItem_Click(object sender, EventArgs e) + private void exportSelectedObjectsMergeToolStripMenuItem_Click(object sender, EventArgs e) { ExportMergeObjects(false); } - private void exportSelectedObjectsmergeWithAnimationClipToolStripMenuItem_Click(object sender, EventArgs e) + private void exportSelectedObjectsMergeWithAnimationClipToolStripMenuItem_Click(object sender, EventArgs e) { ExportMergeObjects(true); } @@ -1575,10 +1642,10 @@ namespace AssetStudioGUI private void goToSceneHierarchyToolStripMenuItem_Click(object sender, EventArgs e) { - var selectasset = (AssetItem)assetListView.Items[assetListView.SelectedIndices[0]]; - if (selectasset.TreeNode != null) + var selectAsset = (AssetItem)assetListView.Items[assetListView.SelectedIndices[0]]; + if (selectAsset.TreeNode != null) { - sceneTreeView.SelectedNode = selectasset.TreeNode; + sceneTreeView.SelectedNode = selectAsset.TreeNode; tabControl1.SelectedTab = tabPage1; } } @@ -1643,7 +1710,7 @@ namespace AssetStudioGUI ExportAssetsList(ExportFilter.Filtered); } - private void exportAllObjectssplitToolStripMenuItem1_Click(object sender, EventArgs e) + private void exportAllObjectsSplitToolStripMenuItem1_Click(object sender, EventArgs e) { if (sceneTreeView.Nodes.Count > 0) { @@ -1938,42 +2005,6 @@ namespace AssetStudioGUI listSearch.SelectionStart = listSearch.Text.Length; } - private void allLive2DModelsToolStripMenuItem_Click(object sender, EventArgs e) - { - if (exportableAssets.Count > 0) - { - var cubismMocs = exportableAssets.Where(x => - { - if (x.Type == ClassIDType.MonoBehaviour) - { - ((MonoBehaviour)x.Asset).m_Script.TryGet(out var m_Script); - return m_Script?.m_ClassName == "CubismMoc"; - } - return false; - }).Select(x => x.Asset).ToArray(); - if (cubismMocs.Length == 0) - { - Logger.Info("Live2D Cubism models were not found."); - return; - } - - var saveFolderDialog = new OpenFolderDialog(); - saveFolderDialog.InitialFolder = saveDirectoryBackup; - if (saveFolderDialog.ShowDialog(this) == DialogResult.OK) - { - timer.Stop(); - saveDirectoryBackup = saveFolderDialog.Folder; - Progress.Reset(); - BeginInvoke(new Action(() => { progressBar1.Style = ProgressBarStyle.Marquee; })); - Studio.ExportLive2D(cubismMocs, saveFolderDialog.Folder); - } - } - else - { - Logger.Info("No exportable assets loaded"); - } - } - private void selectRelatedAsset(object sender, EventArgs e) { var selectedItem = (ToolStripMenuItem)sender; @@ -2077,6 +2108,138 @@ namespace AssetStudioGUI Properties.Settings.Default.Save(); } + private void exportAllL2D_Click(object sender, EventArgs e) + { + if (exportableAssets.Count > 0) + { + if (Studio.cubismMocList.Count == 0) + { + Logger.Info("Live2D Cubism models were not found."); + return; + } + Live2DExporter(); + } + else + { + Logger.Info("No exportable assets loaded"); + } + } + + private void exportSelectedL2D_Click(object sender, EventArgs e) + { + ExportSelectedL2DModels(ExportL2DFilter.Selected); + } + + private void exportSelectedL2DWithClips_Click(object sender, EventArgs e) + { + ExportSelectedL2DModels(ExportL2DFilter.SelectedWithClips); + } + + private void exportSelectedL2DWithFadeMotions_Click(object sender, EventArgs e) + { + ExportSelectedL2DModels(ExportL2DFilter.SelectedWithFade); + } + + private void exportSelectedL2DWithFadeList_Click(object sender, EventArgs e) + { + ExportSelectedL2DModels(ExportL2DFilter.SelectedWithFadeList); + } + + private void ExportSelectedL2DModels(ExportL2DFilter l2dExportMode) + { + if (exportableAssets.Count == 0) + { + Logger.Info("No exportable assets loaded"); + return; + } + if (Studio.cubismMocList.Count == 0) + { + Logger.Info("Live2D Cubism models were not found."); + return; + } + var selectedAssets = GetSelectedAssets(); + if (selectedAssets.Count == 0) + return; + + MonoBehaviour selectedFadeLst = null; + var selectedMocs = new List(); + var selectedFadeMotions = new List(); + var selectedClips = new List(); + foreach (var assetItem in selectedAssets) + { + if (assetItem.Asset is MonoBehaviour m_MonoBehaviour && m_MonoBehaviour.m_Script.TryGet(out var m_Script)) + { + if (m_Script.m_ClassName == "CubismMoc") + { + selectedMocs.Add(m_MonoBehaviour); + } + else if (m_Script.m_ClassName == "CubismFadeMotionData") + { + selectedFadeMotions.Add(m_MonoBehaviour); + } + else if (m_Script.m_ClassName == "CubismFadeMotionList") + { + selectedFadeLst = m_MonoBehaviour; + } + } + else if (assetItem.Asset is AnimationClip m_AnimationClip) + { + selectedClips.Add(m_AnimationClip); + } + } + if (selectedMocs.Count == 0) + { + Logger.Info("Live2D Cubism models were not selected."); + return; + } + + switch (l2dExportMode) + { + case ExportL2DFilter.Selected: + Live2DExporter(selectedMocs); + break; + case ExportL2DFilter.SelectedWithFadeList: + if (selectedFadeLst == null) + { + Logger.Info("Fade Motion List was not selected."); + return; + } + Live2DExporter(selectedMocs, selFadeLst: selectedFadeLst); + break; + case ExportL2DFilter.SelectedWithFade: + if (selectedFadeMotions.Count == 0) + { + Logger.Info("No Fade motions were selected."); + return; + } + Live2DExporter(selectedMocs, selFadeMotions: selectedFadeMotions); + break; + case ExportL2DFilter.SelectedWithClips: + if (selectedClips.Count == 0) + { + Logger.Info("No AnimationClips were selected."); + return; + } + Live2DExporter(selectedMocs, selectedClips); + break; + } + } + + private void Live2DExporter(List selMocs = null, List selClipMotions = null, List selFadeMotions = null, MonoBehaviour selFadeLst = null) + { + var saveFolderDialog = new OpenFolderDialog(); + saveFolderDialog.InitialFolder = saveDirectoryBackup; + if (saveFolderDialog.ShowDialog(this) == DialogResult.OK) + { + timer.Stop(); + saveDirectoryBackup = saveFolderDialog.Folder; + Progress.Reset(); + BeginInvoke(new Action(() => { progressBar1.Style = ProgressBarStyle.Marquee; })); + + Studio.ExportLive2D(saveFolderDialog.Folder, selMocs, selClipMotions, selFadeMotions, selFadeLst); + } + } + #region FMOD private void FMODinit() { diff --git a/AssetStudioGUI/AssetStudioGUIForm.resx b/AssetStudioGUI/AssetStudioGUIForm.resx index 095950b..dcce5cf 100644 --- a/AssetStudioGUI/AssetStudioGUIForm.resx +++ b/AssetStudioGUI/AssetStudioGUIForm.resx @@ -120,6 +120,9 @@ 312, 17 + + 432, 17 + abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWYZ 1234567890.:,;'\"(!?)+-*/= @@ -138,9 +141,6 @@ The quick brown fox jumps over the lazy dog. 1234567890 The quick brown fox jumps over the lazy dog. 1234567890 - - 432, 17 - 775, 21 diff --git a/AssetStudioGUI/ExportOptions.Designer.cs b/AssetStudioGUI/ExportOptions.Designer.cs index 8941fe2..c446b82 100644 --- a/AssetStudioGUI/ExportOptions.Designer.cs +++ b/AssetStudioGUI/ExportOptions.Designer.cs @@ -305,7 +305,7 @@ // // l2dAnimationClipRadioButton // - this.l2dAnimationClipRadioButton.AccessibleName = "AnimationClip"; + this.l2dAnimationClipRadioButton.AccessibleName = "AnimationClipV2"; this.l2dAnimationClipRadioButton.AutoSize = true; this.l2dAnimationClipRadioButton.Location = new System.Drawing.Point(172, 5); this.l2dAnimationClipRadioButton.Name = "l2dAnimationClipRadioButton"; diff --git a/AssetStudioGUI/Studio.cs b/AssetStudioGUI/Studio.cs index 27e70c8..543f136 100644 --- a/AssetStudioGUI/Studio.cs +++ b/AssetStudioGUI/Studio.cs @@ -1,4 +1,5 @@ using AssetStudio; +using CubismLive2DExtractor; using System; using System.Collections.Generic; using System.Diagnostics; @@ -9,7 +10,6 @@ using System.Threading; using System.Windows.Forms; using System.Xml.Linq; using static AssetStudioGUI.Exporter; -using static CubismLive2DExtractor.Live2DExtractor; using Object = AssetStudio.Object; namespace AssetStudioGUI @@ -28,6 +28,15 @@ namespace AssetStudioGUI Filtered } + internal enum ExportL2DFilter + { + All, + Selected, + SelectedWithFadeList, + SelectedWithFade, + SelectedWithClips, + } + internal enum ExportListType { XML @@ -49,13 +58,24 @@ namespace AssetStudioGUI RegexContainer, } + [Flags] + internal enum SelectedAssetType + { + Animator = 0x01, + AnimationClip = 0x02, + MonoBehaviourMoc = 0x04, + MonoBehaviourFade = 0x08, + MonoBehaviourFadeLst = 0x10 + } + internal static class Studio { public static AssetsManager assetsManager = new AssetsManager(); public static AssemblyLoader assemblyLoader = new AssemblyLoader(); public static List exportableAssets = new List(); public static List visibleAssets = new List(); - private static Dictionary allContainers = new Dictionary(); + public static List cubismMocList = new List(); + private static Dictionary l2dResourceContainers = new Dictionary(); internal static Action StatusStripUpdate = x => { }; public static int ExtractFolder(string path, string savePath) @@ -158,7 +178,7 @@ namespace AssetStudioGUI var objectCount = assetsManager.assetsFileList.Sum(x => x.Objects.Count); var objectAssetItemDic = new Dictionary(objectCount); var containers = new List<(PPtr, string)>(); - allContainers.Clear(); + l2dResourceContainers.Clear(); var i = 0; Progress.Reset(); foreach (var assetsFile in assetsManager.assetsFileList) @@ -218,14 +238,16 @@ namespace AssetStudioGUI exportable = true; break; case MonoBehaviour m_MonoBehaviour: - if (m_MonoBehaviour.m_Name == "" && m_MonoBehaviour.m_Script.TryGet(out var m_Script)) + var assetName = m_MonoBehaviour.m_Name; + if (m_MonoBehaviour.m_Script.TryGet(out var m_Script)) { - assetItem.Text = m_Script.m_ClassName; - } - else - { - assetItem.Text = m_MonoBehaviour.m_Name; + assetName = assetName == "" ? m_Script.m_ClassName : assetName; + if (m_Script.m_ClassName == "CubismMoc") + { + cubismMocList.Add(m_MonoBehaviour); + } } + assetItem.Text = assetName; exportable = true; break; case PlayerSettings m_PlayerSettings: @@ -276,7 +298,15 @@ namespace AssetStudioGUI if (pptr.TryGet(out var obj)) { objectAssetItemDic[obj].Container = container; - allContainers[obj] = container; + switch (obj) + { + case AnimationClip _: + case GameObject _: + case Texture2D _: + case MonoBehaviour _: + l2dResourceContainers[obj] = container; + break; + } } } foreach (var tmp in exportableAssets) @@ -725,6 +755,12 @@ namespace AssetStudioGUI } public static TypeTree MonoBehaviourToTypeTree(MonoBehaviour m_MonoBehaviour) + { + SelectAssemblyFolder(); + return m_MonoBehaviour.ConvertToTypeTree(assemblyLoader); + } + + private static void SelectAssemblyFolder() { if (!assemblyLoader.Loaded) { @@ -739,7 +775,6 @@ namespace AssetStudioGUI assemblyLoader.Loaded = true; } } - return m_MonoBehaviour.ConvertToTypeTree(assemblyLoader); } public static string DumpAsset(Object obj) @@ -760,54 +795,64 @@ namespace AssetStudioGUI Process.Start(info); } - public static void ExportLive2D(Object[] cubismMocs, string exportPath) + 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 motionMode = Properties.Settings.Default.l2dMotionMode; var forceBezier = Properties.Settings.Default.l2dForceBezier; + var mocList = selMocs ?? cubismMocList; + var motionMode = Properties.Settings.Default.l2dMotionMode; + if (selClipMotions != null) + motionMode = Live2DMotionMode.AnimationClipV2; + else if (selFadeMotions != null || selFadeLst != null) + motionMode = Live2DMotionMode.MonoBehaviour; ThreadPool.QueueUserWorkItem(state => { Logger.Info($"Searching for Live2D files..."); - var useFullContainerPath = false; - if (cubismMocs.Length > 1) + var mocPathDict = new Dictionary(); + var mocPathList = new List(); + foreach (var mocMonoBehaviour in cubismMocList) { - var basePathSet = cubismMocs.Select(x => - { - var pathLen = allContainers.TryGetValue(x, out var itemContainer) ? itemContainer.LastIndexOf("/") : 0; - pathLen = pathLen < 0 ? allContainers[x].Length : pathLen; - return itemContainer?.Substring(0, pathLen); - }).ToHashSet(); + if (!l2dResourceContainers.TryGetValue(mocMonoBehaviour, out var fullContainerPath)) + continue; - if (basePathSet.All(x => x == null)) - { - Logger.Error($"Live2D Cubism export error\r\nCannot find any model related files"); - StatusStripUpdate("Live2D export canceled"); - Progress.Reset(); - return; - } - - if (basePathSet.Count != cubismMocs.Length) - { - useFullContainerPath = true; - } + var pathSepIndex = fullContainerPath.LastIndexOf('/'); + var basePath = pathSepIndex > 0 + ? fullContainerPath.Substring(0, pathSepIndex) + : fullContainerPath; + mocPathDict.Add(mocMonoBehaviour, (fullContainerPath, basePath)); + } + 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 basePathList = cubismMocs.Select(x => + var basePathSet = mocPathDict.Values.Select(x => x.Item2).ToHashSet(); + var useFullContainerPath = mocPathDict.Count != basePathSet.Count; + foreach (var moc in mocList) { - allContainers.TryGetValue(x, out var container); - container = useFullContainerPath - ? container - : container?.Substring(0, container.LastIndexOf("/")); - return container; - }).Where(x => x != null).ToList(); + var mocPath = useFullContainerPath + ? mocPathDict[moc].Item1 //fullContainerPath + : mocPathDict[moc].Item2; //basePath + mocPathList.Add(mocPath); + } + mocPathDict.Clear(); - var lookup = allContainers.ToLookup( - x => basePathList.Find(b => x.Value.Contains(b) && x.Value.Split('/').Any(y => y == b.Substring(b.LastIndexOf("/") + 1))), + var lookup = l2dResourceContainers.ToLookup( + x => mocPathList.Find(b => x.Value.Contains(b) && x.Value.Split('/').Any(y => y == b.Substring(b.LastIndexOf("/") + 1))), x => x.Key ); + if (mocList[0].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 modelCounter = 0; foreach (var assets in lookup) @@ -820,11 +865,16 @@ namespace AssetStudioGUI 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 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; - ExtractLive2D(assets, destPath, modelName, assemblyLoader, motionMode, forceBezier); + var modelExtractor = new Live2DExtractor(assets, selClipMotions, selFadeMotions, selFadeLst); + modelExtractor.ExtractCubismModel(destPath, modelName, motionMode, assemblyLoader, forceBezier); modelCounter++; } catch (Exception ex) diff --git a/AssetStudioUtility/CubismLive2DExtractor/CubismFadeMotion.cs b/AssetStudioUtility/CubismLive2DExtractor/CubismFadeMotion.cs index f14b35a..2817a37 100644 --- a/AssetStudioUtility/CubismLive2DExtractor/CubismFadeMotion.cs +++ b/AssetStudioUtility/CubismLive2DExtractor/CubismFadeMotion.cs @@ -2,7 +2,7 @@ namespace CubismLive2DExtractor { - public class AnimationCurve + public sealed class AnimationCurve { public CubismKeyframeData[] m_Curve { get; set; } public int m_PreInfinity { get; set; } @@ -10,7 +10,7 @@ namespace CubismLive2DExtractor public int m_RotationOrder { get; set; } } - public class CubismFadeMotion + public sealed class CubismFadeMotion { public string m_Name { get; set; } public string MotionName { get; set; } diff --git a/AssetStudioUtility/CubismLive2DExtractor/CubismMotion3Converter.cs b/AssetStudioUtility/CubismLive2DExtractor/CubismMotion3Converter.cs index 07b9092..1c738fe 100644 --- a/AssetStudioUtility/CubismLive2DExtractor/CubismMotion3Converter.cs +++ b/AssetStudioUtility/CubismLive2DExtractor/CubismMotion3Converter.cs @@ -10,14 +10,21 @@ namespace CubismLive2DExtractor private Dictionary bonePathHash = new Dictionary(); public List AnimationList { get; protected set; } = new List(); - public CubismMotion3Converter(GameObject rootGameObject, AnimationClip[] animationClips) + public CubismMotion3Converter(GameObject rootGameObject, List animationClips) { var rootTransform = GetTransform(rootGameObject); CreateBonePathHash(rootTransform); ConvertAnimations(animationClips); } - private void ConvertAnimations(AnimationClip[] animationClips) + public CubismMotion3Converter(List animationClips, HashSet partIds, HashSet parameterIds) + { + CreateBonePathHash(partIds, pathType: "Parts/"); + CreateBonePathHash(parameterIds, pathType: "Parameters/"); + ConvertAnimations(animationClips); + } + + private void ConvertAnimations(List animationClips) { foreach (var animationClip in animationClips) { @@ -160,21 +167,30 @@ namespace CubismLive2DExtractor return null; } + private void CreateBonePathHash(HashSet ids, string pathType) + { + foreach (var id in ids) + { + var name = pathType + id;; + bonePathHash[GetCRC(name)] = name; + int index; + while ((index = name.IndexOf("/", StringComparison.Ordinal)) >= 0) + { + name = name.Substring(index + 1); + bonePathHash[GetCRC(name)] = name; + } + } + } + private void CreateBonePathHash(Transform m_Transform) { var name = GetTransformPath(m_Transform); - var crc = new SevenZip.CRC(); - var bytes = Encoding.UTF8.GetBytes(name); - crc.Update(bytes, 0, (uint)bytes.Length); - bonePathHash[crc.GetDigest()] = name; + bonePathHash[GetCRC(name)] = name; int index; while ((index = name.IndexOf("/", StringComparison.Ordinal)) >= 0) { name = name.Substring(index + 1); - crc = new SevenZip.CRC(); - bytes = Encoding.UTF8.GetBytes(name); - crc.Update(bytes, 0, (uint)bytes.Length); - bonePathHash[crc.GetDigest()] = name; + bonePathHash[GetCRC(name)] = name; } foreach (var pptr in m_Transform.m_Children) { @@ -183,7 +199,13 @@ namespace CubismLive2DExtractor } } - private string GetTransformPath(Transform transform) + private static uint GetCRC(string name) + { + var bytes = Encoding.UTF8.GetBytes(name); + return SevenZip.CRC.CalculateDigest(bytes, 0, (uint)bytes.Length); + } + + private static string GetTransformPath(Transform transform) { transform.m_GameObject.TryGet(out var m_GameObject); if (transform.m_Father.TryGet(out var father)) diff --git a/AssetStudioUtility/CubismLive2DExtractor/CubismObjectList.cs b/AssetStudioUtility/CubismLive2DExtractor/CubismObjectList.cs new file mode 100644 index 0000000..0604e86 --- /dev/null +++ b/AssetStudioUtility/CubismLive2DExtractor/CubismObjectList.cs @@ -0,0 +1,62 @@ +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 new file mode 100644 index 0000000..912707e --- /dev/null +++ b/AssetStudioUtility/CubismLive2DExtractor/CubismParsers.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Specialized; +using System.Linq; +using AssetStudio; +using Newtonsoft.Json; + +namespace CubismLive2DExtractor +{ + public static class CubismParsers + { + public enum CubismMonoBehaviourType + { + FadeMotionList, + FadeMotion, + Expression, + Physics, + } + + public static string ParsePhysics(OrderedDictionary physicsDict) + { + var cubismPhysicsRig = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(physicsDict))._rig; + + var physicsSettings = new CubismPhysics3Json.SerializablePhysicsSettings[cubismPhysicsRig.SubRigs.Length]; + for (int i = 0; i < physicsSettings.Length; i++) + { + var subRigs = cubismPhysicsRig.SubRigs[i]; + physicsSettings[i] = new CubismPhysics3Json.SerializablePhysicsSettings + { + Id = $"PhysicsSetting{i + 1}", + Input = new CubismPhysics3Json.SerializableInput[subRigs.Input.Length], + Output = new CubismPhysics3Json.SerializableOutput[subRigs.Output.Length], + Vertices = new CubismPhysics3Json.SerializableVertex[subRigs.Particles.Length], + Normalization = new CubismPhysics3Json.SerializableNormalization + { + Position = new CubismPhysics3Json.SerializableNormalizationValue + { + Minimum = subRigs.Normalization.Position.Minimum, + Default = subRigs.Normalization.Position.Default, + Maximum = subRigs.Normalization.Position.Maximum + }, + Angle = new CubismPhysics3Json.SerializableNormalizationValue + { + Minimum = subRigs.Normalization.Angle.Minimum, + Default = subRigs.Normalization.Angle.Default, + Maximum = subRigs.Normalization.Angle.Maximum + } + } + }; + for (int j = 0; j < subRigs.Input.Length; j++) + { + var input = subRigs.Input[j]; + physicsSettings[i].Input[j] = new CubismPhysics3Json.SerializableInput + { + Source = new CubismPhysics3Json.SerializableParameter + { + Target = "Parameter", //同名GameObject父节点的名称 + Id = input.SourceId + }, + Weight = input.Weight, + Type = Enum.GetName(typeof(CubismPhysicsSourceComponent), input.SourceComponent), + Reflect = input.IsInverted + }; + } + for (int j = 0; j < subRigs.Output.Length; j++) + { + var output = subRigs.Output[j]; + physicsSettings[i].Output[j] = new CubismPhysics3Json.SerializableOutput + { + Destination = new CubismPhysics3Json.SerializableParameter + { + Target = "Parameter", //同名GameObject父节点的名称 + Id = output.DestinationId + }, + VertexIndex = output.ParticleIndex, + Scale = output.AngleScale, + Weight = output.Weight, + Type = Enum.GetName(typeof(CubismPhysicsSourceComponent), output.SourceComponent), + Reflect = output.IsInverted + }; + } + for (int j = 0; j < subRigs.Particles.Length; j++) + { + var particles = subRigs.Particles[j]; + physicsSettings[i].Vertices[j] = new CubismPhysics3Json.SerializableVertex + { + Position = particles.InitialPosition, + Mobility = particles.Mobility, + Delay = particles.Delay, + Acceleration = particles.Acceleration, + Radius = particles.Radius + }; + } + } + var physicsDictionary = new CubismPhysics3Json.SerializablePhysicsDictionary[physicsSettings.Length]; + for (int i = 0; i < physicsSettings.Length; i++) + { + physicsDictionary[i] = new CubismPhysics3Json.SerializablePhysicsDictionary + { + Id = $"PhysicsSetting{i + 1}", + Name = $"Dummy{i + 1}" + }; + } + var physicsJson = new CubismPhysics3Json + { + Version = 3, + Meta = new CubismPhysics3Json.SerializableMeta + { + PhysicsSettingCount = cubismPhysicsRig.SubRigs.Length, + TotalInputCount = cubismPhysicsRig.SubRigs.Sum(x => x.Input.Length), + TotalOutputCount = cubismPhysicsRig.SubRigs.Sum(x => x.Output.Length), + VertexCount = cubismPhysicsRig.SubRigs.Sum(x => x.Particles.Length), + EffectiveForces = new CubismPhysics3Json.SerializableEffectiveForces + { + Gravity = cubismPhysicsRig.Gravity, + Wind = cubismPhysicsRig.Wind + }, + PhysicsDictionary = physicsDictionary + }, + PhysicsSettings = physicsSettings + }; + return JsonConvert.SerializeObject(physicsJson, Formatting.Indented, new MyJsonConverter2()); + } + + public static OrderedDictionary ParseMonoBehaviour(MonoBehaviour m_MonoBehaviour, CubismMonoBehaviourType cubismMonoBehaviourType, AssemblyLoader assemblyLoader) + { + var orderedDict = m_MonoBehaviour.ToType(); + if (orderedDict != null) + return orderedDict; + + var fieldName = ""; + var m_Type = m_MonoBehaviour.ConvertToTypeTree(assemblyLoader); + switch (cubismMonoBehaviourType) + { + case CubismMonoBehaviourType.FadeMotionList: + fieldName = "cubismfademotionobjects"; + break; + case CubismMonoBehaviourType.FadeMotion: + fieldName = "parameterids"; + break; + case CubismMonoBehaviourType.Expression: + fieldName = "parameters"; + break; + case CubismMonoBehaviourType.Physics: + fieldName = "_rig"; + break; + } + if (m_Type.m_Nodes.FindIndex(x => x.m_Name.ToLower() == fieldName) < 0) + { + m_MonoBehaviour.m_Script.TryGet(out var m_MonoScript); + var assetName = m_MonoBehaviour.m_Name != "" ? m_MonoBehaviour.m_Name : m_MonoScript.m_ClassName; + Logger.Warning($"{cubismMonoBehaviourType} asset \"{assetName}\" is not readable"); + return null; + } + orderedDict = m_MonoBehaviour.ToType(m_Type); + + return orderedDict; + } + } +} diff --git a/AssetStudioUtility/CubismLive2DExtractor/Live2DExtractor.cs b/AssetStudioUtility/CubismLive2DExtractor/Live2DExtractor.cs index 2ecf7c5..1681209 100644 --- a/AssetStudioUtility/CubismLive2DExtractor/Live2DExtractor.cs +++ b/AssetStudioUtility/CubismLive2DExtractor/Live2DExtractor.cs @@ -7,34 +7,43 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using AssetStudio; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using static CubismLive2DExtractor.CubismParsers; namespace CubismLive2DExtractor { - public static class Live2DExtractor + public sealed class Live2DExtractor { - public static void ExtractLive2D(IGrouping assets, string destPath, string modelName, AssemblyLoader assemblyLoader, Live2DMotionMode motionMode, bool forceBezier = false) - { - var destTexturePath = Path.Combine(destPath, "textures") + Path.DirectorySeparatorChar; - var destMotionPath = Path.Combine(destPath, "motions") + Path.DirectorySeparatorChar; - var destExpressionPath = Path.Combine(destPath, "expressions") + Path.DirectorySeparatorChar; - Directory.CreateDirectory(destPath); - Directory.CreateDirectory(destTexturePath); + private List Expressions { 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; } - var expressionList = new List(); - var fadeMotionList = new List(); - var gameObjects = new List(); - var animationClips = new List(); - - var textures = new SortedSet(); - var eyeBlinkParameters = new HashSet(); - var lipSyncParameters = new HashSet(); - var parameterNames = new HashSet(); - var partNames = new HashSet(); - MonoBehaviour physics = null; + public Live2DExtractor(IGrouping 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(); + ParameterNames = new HashSet(); + PartNames = new HashSet(); + FadeMotionLst = inFadeMotionLst; + Logger.Info("Sorting model assets.."); foreach (var asset in assets) { switch (asset) @@ -45,210 +54,255 @@ namespace CubismLive2DExtractor switch (m_Script.m_ClassName) { case "CubismMoc": - File.WriteAllBytes($"{destPath}{modelName}.moc3", ParseMoc(m_MonoBehaviour)); //moc + MocMono = m_MonoBehaviour; break; case "CubismPhysicsController": - physics = physics ?? m_MonoBehaviour; + PhysicsMono = m_MonoBehaviour; break; case "CubismExpressionData": - expressionList.Add(m_MonoBehaviour); + Expressions.Add(m_MonoBehaviour); break; case "CubismFadeMotionData": - fadeMotionList.Add(m_MonoBehaviour); + if (inFadeMotions == null && inFadeMotionLst == null) + { + FadeMotions.Add(m_MonoBehaviour); + } + break; + case "CubismFadeMotionList": + if (inFadeMotions == null && inFadeMotionLst == null) + { + FadeMotionLst = m_MonoBehaviour; + } break; case "CubismEyeBlinkParameter": if (m_MonoBehaviour.m_GameObject.TryGet(out var blinkGameObject)) { - eyeBlinkParameters.Add(blinkGameObject.m_Name); + EyeBlinkParameters.Add(blinkGameObject.m_Name); } break; case "CubismMouthParameter": if (m_MonoBehaviour.m_GameObject.TryGet(out var mouthGameObject)) { - lipSyncParameters.Add(mouthGameObject.m_Name); + LipSyncParameters.Add(mouthGameObject.m_Name); } break; case "CubismParameter": if (m_MonoBehaviour.m_GameObject.TryGet(out var paramGameObject)) { - parameterNames.Add(paramGameObject.m_Name); + ParameterNames.Add(paramGameObject.m_Name); } break; case "CubismPart": if (m_MonoBehaviour.m_GameObject.TryGet(out var partGameObject)) { - partNames.Add(partGameObject.m_Name); + PartNames.Add(partGameObject.m_Name); } break; } } break; - case Texture2D m_Texture2D: - using (var image = m_Texture2D.ConvertToImage(flip: true)) + case AnimationClip m_AnimationClip: + if (inClipMotions == null) { - using (var file = File.OpenWrite($"{destTexturePath}{m_Texture2D.m_Name}.png")) - { - image.WriteToStream(file, ImageFormat.Png); - } - textures.Add($"textures/{m_Texture2D.m_Name}.png"); //texture + AnimationClips.Add(m_AnimationClip); } break; case GameObject m_GameObject: - gameObjects.Add(m_GameObject); + GameObjects.Add(m_GameObject); break; - case AnimationClip m_AnimationClip: - animationClips.Add(m_AnimationClip); + case Texture2D m_Texture2D: + Texture2Ds.Add(m_Texture2D); break; } } + } - if (textures.Count == 0) + public void ExtractCubismModel(string destPath, string modelName, Live2DMotionMode motionMode, AssemblyLoader assemblyLoader, bool forceBezier = false) + { + Directory.CreateDirectory(destPath); + + #region moc3 + using (var cubismModel = new CubismModel(MocMono)) { - Logger.Warning($"No textures found for \"{modelName}\" model."); + var sb = new StringBuilder(); + sb.AppendLine("Model Stats:"); + sb.AppendLine($"SDK Version: {cubismModel.VersionDescription}"); + if (cubismModel.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}"); + Logger.Debug(sb.ToString()); + + ParameterNames = cubismModel.ParamNames; + PartNames = cubismModel.PartNames; + } + File.WriteAllBytes($"{destPath}{modelName}.moc3", cubismModel.ModelData); + } + #endregion + + #region textures + var textures = new SortedSet(); + var destTexturePath = Path.Combine(destPath, "textures") + Path.DirectorySeparatorChar; + + if (Texture2Ds.Count == 0) + { + Logger.Warning($"No textures found for \"{modelName}\" model"); + } + else + { + Directory.CreateDirectory(destTexturePath); } - //physics - if (physics != null) + foreach (var texture2D in Texture2Ds) { - try + using (var image = texture2D.ConvertToImage(flip: true)) { - var buff = ParsePhysics(physics, assemblyLoader); - File.WriteAllText($"{destPath}{modelName}.physics3.json", buff); - } - catch (Exception e) - { - Logger.Warning($"Error in parsing physics data: {e.Message}"); - physics = null; + using (var file = File.OpenWrite($"{destTexturePath}{texture2D.m_Name}.png")) + { + image.WriteToStream(file, ImageFormat.Png); + } + textures.Add($"textures/{texture2D.m_Name}.png"); } } + #endregion - //motion + #region physics3.json + 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); + } + catch (Exception e) + { + Logger.Warning($"Error in parsing physics data: {e.Message}"); + PhysicsMono = null; + } + } + else + { + PhysicsMono = null; + } + } + #endregion + + #region motion3.json var motions = new SortedDictionary(); + var destMotionPath = Path.Combine(destPath, "motions") + Path.DirectorySeparatorChar; - if (motionMode == Live2DMotionMode.MonoBehaviour && fadeMotionList.Count > 0) //motion from MonoBehaviour + if (motionMode == Live2DMotionMode.MonoBehaviour && FadeMotionLst != null) //Fade motions from Fade Motion List { Logger.Debug("Motion export method: MonoBehaviour (Fade motion)"); - Directory.CreateDirectory(destMotionPath); - foreach (var fadeMotionMono in fadeMotionList) + var fadeMotionLstDict = ParseMonoBehaviour(FadeMotionLst, CubismMonoBehaviourType.FadeMotionList, assemblyLoader); + if (fadeMotionLstDict != null) { - var fadeMotionObj = fadeMotionMono.ToType(); - if (fadeMotionObj == null) + CubismObjectList.AssetsFile = FadeMotionLst.assetsFile; + var fadeMotionAssetList = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(fadeMotionLstDict)).GetFadeMotionAssetList(); + if (fadeMotionAssetList?.Count > 0) { - var m_Type = fadeMotionMono.ConvertToTypeTree(assemblyLoader); - fadeMotionObj = fadeMotionMono.ToType(m_Type); - if (fadeMotionObj == null) - { - Logger.Warning($"Fade motion \"{fadeMotionMono.m_Name}\" is not readable."); - continue; - } + FadeMotions = fadeMotionAssetList; + Logger.Debug($"\"{FadeMotionLst.m_Name}\": found {fadeMotionAssetList.Count} motion(s)"); } - var fadeMotion = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(fadeMotionObj)); - if (fadeMotion.ParameterIds.Length == 0) - continue; - - var motionJson = new CubismMotion3Json(fadeMotion, parameterNames, partNames, forceBezier); - - var animName = Path.GetFileNameWithoutExtension(fadeMotion.m_Name); - if (motions.ContainsKey(animName)) - { - animName = $"{animName}_{fadeMotion.GetHashCode()}"; - - if (motions.ContainsKey(animName)) - continue; - } - var motionPath = new JObject(new JProperty("File", $"motions/{animName}.motion3.json")); - motions.Add(animName, new JArray(motionPath)); - File.WriteAllText($"{destMotionPath}{animName}.motion3.json", JsonConvert.SerializeObject(motionJson, Formatting.Indented, new MyJsonConverter())); } } - if (motions.Count == 0 && gameObjects.Count > 0) //motion from AnimationClip + + if (motionMode == Live2DMotionMode.MonoBehaviour && FadeMotions.Count > 0) //motion from MonoBehaviour { + ExportFadeMotions(destMotionPath, assemblyLoader, forceBezier, motions); + } + + if (motions.Count == 0) //motion from AnimationClip + { + CubismMotion3Converter converter = null; var exportMethod = "AnimationClip"; + if (motionMode != Live2DMotionMode.AnimationClipV1) //AnimationClipV2 + { + exportMethod += "V2"; + converter = new CubismMotion3Converter(AnimationClips, PartNames, ParameterNames); + } + else if (GameObjects.Count > 0) //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); + } + if (motionMode == Live2DMotionMode.MonoBehaviour) { - exportMethod = fadeMotionList.Count > 0 - ? exportMethod + " (unable to export motion using Fade motion method)" + exportMethod = FadeMotions.Count > 0 + ? exportMethod + " (unable to export motions using Fade motion method)" : exportMethod + " (no Fade motions found)"; } Logger.Debug($"Motion export method: {exportMethod}"); - 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); - var converter = new CubismMotion3Converter(rootGameObject, animationClips.ToArray()); - if (converter.AnimationList.Count > 0) - { - Directory.CreateDirectory(destMotionPath); - } - foreach (var animation in converter.AnimationList) - { - var motionJson = new CubismMotion3Json(animation, forceBezier); - var animName = animation.Name; - if (motions.ContainsKey(animName)) - { - animName = $"{animName}_{animation.GetHashCode()}"; - - if (motions.ContainsKey(animName)) - continue; - } - var motionPath = new JObject(new JProperty("File", $"motions/{animName}.motion3.json")); - motions.Add(animName, new JArray(motionPath)); - File.WriteAllText($"{destMotionPath}{animName}.motion3.json", JsonConvert.SerializeObject(motionJson, Formatting.Indented, new MyJsonConverter())); - } + ExportClipMotions(destMotionPath, converter, forceBezier, motions); } + if (motions.Count == 0) { - Logger.Warning($"No motions found for \"{modelName}\" model."); + Logger.Warning($"No exportable motions found for \"{modelName}\" model"); } + else + { + Logger.Debug($"Exported {motions.Count} motion(s)"); + } + #endregion - //expression + #region exp3.json var expressions = new JArray(); - if (expressionList.Count > 0) + var destExpressionPath = Path.Combine(destPath, "expressions") + Path.DirectorySeparatorChar; + + if (Expressions.Count > 0) { Directory.CreateDirectory(destExpressionPath); } - foreach (var monoBehaviour in expressionList) + foreach (var monoBehaviour in Expressions) { var expressionName = monoBehaviour.m_Name.Replace(".exp3", ""); - var expressionObj = monoBehaviour.ToType(); - if (expressionObj == null) - { - var m_Type = monoBehaviour.ConvertToTypeTree(assemblyLoader); - expressionObj = monoBehaviour.ToType(m_Type); - if (expressionObj == null) - { - Logger.Warning($"Expression \"{expressionName}\" is not readable."); - continue; - } - } - var expression = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(expressionObj)); + var expressionDict = ParseMonoBehaviour(monoBehaviour, CubismMonoBehaviourType.Expression, assemblyLoader); + if (expressionDict == null) + continue; + + var expression = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(expressionDict)); expressions.Add(new JObject - { - { "Name", expressionName }, - { "File", $"expressions/{expressionName}.exp3.json" } - }); + { + { "Name", expressionName }, + { "File", $"expressions/{expressionName}.exp3.json" } + }); File.WriteAllText($"{destExpressionPath}{expressionName}.exp3.json", JsonConvert.SerializeObject(expression, Formatting.Indented)); } + #endregion - //group + #region model3.json var groups = new List(); //Try looking for group IDs among the parameter names manually - if (eyeBlinkParameters.Count == 0) + if (EyeBlinkParameters.Count == 0) { - eyeBlinkParameters = parameterNames.Where(x => + EyeBlinkParameters = ParameterNames.Where(x => x.ToLower().Contains("eye") && x.ToLower().Contains("open") && (x.ToLower().Contains('l') || x.ToLower().Contains('r')) ).ToHashSet(); } - if (lipSyncParameters.Count == 0) + if (LipSyncParameters.Count == 0) { - lipSyncParameters = parameterNames.Where(x => + LipSyncParameters = ParameterNames.Where(x => x.ToLower().Contains("mouth") && x.ToLower().Contains("open") && x.ToLower().Contains('y') @@ -259,16 +313,15 @@ namespace CubismLive2DExtractor { Target = "Parameter", Name = "EyeBlink", - Ids = eyeBlinkParameters.ToArray() + Ids = EyeBlinkParameters.ToArray() }); groups.Add(new CubismModel3Json.SerializableGroup { Target = "Parameter", Name = "LipSync", - Ids = lipSyncParameters.ToArray() + Ids = LipSyncParameters.ToArray() }); - - //model + var model3 = new CubismModel3Json { Version = 3, @@ -277,140 +330,74 @@ namespace CubismLive2DExtractor { Moc = $"{modelName}.moc3", Textures = textures.ToArray(), + Physics = PhysicsMono == null ? null : $"{modelName}.physics3.json", Motions = JObject.FromObject(motions), Expressions = expressions, }, Groups = groups.ToArray() }; - if (physics != null) - { - model3.FileReferences.Physics = $"{modelName}.physics3.json"; - } File.WriteAllText($"{destPath}{modelName}.model3.json", JsonConvert.SerializeObject(model3, Formatting.Indented)); + #endregion } - private static string ParsePhysics(MonoBehaviour physics, AssemblyLoader assemblyLoader) + private void ExportFadeMotions(string destMotionPath, AssemblyLoader assemblyLoader, bool forceBezier, SortedDictionary motions) { - var physicsObj = physics.ToType(); - if (physicsObj == null) + Directory.CreateDirectory(destMotionPath); + foreach (var fadeMotionMono in FadeMotions) { - var m_Type = physics.ConvertToTypeTree(assemblyLoader); - physicsObj = physics.ToType(m_Type); - if (physicsObj == null) - { - throw new Exception("MonoBehaviour is not readable."); - } - } - var cubismPhysicsRig = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(physicsObj))._rig; + var fadeMotionDict = ParseMonoBehaviour(fadeMotionMono, CubismMonoBehaviourType.FadeMotion, assemblyLoader); + if (fadeMotionDict == null) + continue; + + var fadeMotion = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(fadeMotionDict)); + if (fadeMotion.ParameterIds.Length == 0) + continue; - var physicsSettings = new CubismPhysics3Json.SerializablePhysicsSettings[cubismPhysicsRig.SubRigs.Length]; - for (int i = 0; i < physicsSettings.Length; i++) - { - var subRigs = cubismPhysicsRig.SubRigs[i]; - physicsSettings[i] = new CubismPhysics3Json.SerializablePhysicsSettings + var motionJson = new CubismMotion3Json(fadeMotion, ParameterNames, PartNames, forceBezier); + + var animName = Path.GetFileNameWithoutExtension(fadeMotion.m_Name); + if (motions.ContainsKey(animName)) { - Id = $"PhysicsSetting{i + 1}", - Input = new CubismPhysics3Json.SerializableInput[subRigs.Input.Length], - Output = new CubismPhysics3Json.SerializableOutput[subRigs.Output.Length], - Vertices = new CubismPhysics3Json.SerializableVertex[subRigs.Particles.Length], - Normalization = new CubismPhysics3Json.SerializableNormalization - { - Position = new CubismPhysics3Json.SerializableNormalizationValue - { - Minimum = subRigs.Normalization.Position.Minimum, - Default = subRigs.Normalization.Position.Default, - Maximum = subRigs.Normalization.Position.Maximum - }, - Angle = new CubismPhysics3Json.SerializableNormalizationValue - { - Minimum = subRigs.Normalization.Angle.Minimum, - Default = subRigs.Normalization.Angle.Default, - Maximum = subRigs.Normalization.Angle.Maximum - } - } - }; - for (int j = 0; j < subRigs.Input.Length; j++) - { - var input = subRigs.Input[j]; - physicsSettings[i].Input[j] = new CubismPhysics3Json.SerializableInput - { - Source = new CubismPhysics3Json.SerializableParameter - { - Target = "Parameter", //同名GameObject父节点的名称 - Id = input.SourceId - }, - Weight = input.Weight, - Type = Enum.GetName(typeof(CubismPhysicsSourceComponent), input.SourceComponent), - Reflect = input.IsInverted - }; - } - for (int j = 0; j < subRigs.Output.Length; j++) - { - var output = subRigs.Output[j]; - physicsSettings[i].Output[j] = new CubismPhysics3Json.SerializableOutput - { - Destination = new CubismPhysics3Json.SerializableParameter - { - Target = "Parameter", //同名GameObject父节点的名称 - Id = output.DestinationId - }, - VertexIndex = output.ParticleIndex, - Scale = output.AngleScale, - Weight = output.Weight, - Type = Enum.GetName(typeof(CubismPhysicsSourceComponent), output.SourceComponent), - Reflect = output.IsInverted - }; - } - for (int j = 0; j < subRigs.Particles.Length; j++) - { - var particles = subRigs.Particles[j]; - physicsSettings[i].Vertices[j] = new CubismPhysics3Json.SerializableVertex - { - Position = particles.InitialPosition, - Mobility = particles.Mobility, - Delay = particles.Delay, - Acceleration = particles.Acceleration, - Radius = particles.Radius - }; + animName = $"{animName}_{fadeMotion.GetHashCode()}"; + if (motions.ContainsKey(animName)) + continue; } + var motionPath = new JObject(new JProperty("File", $"motions/{animName}.motion3.json")); + motions.Add(animName, new JArray(motionPath)); + File.WriteAllText($"{destMotionPath}{animName}.motion3.json", JsonConvert.SerializeObject(motionJson, Formatting.Indented, new MyJsonConverter())); } - var physicsDictionary = new CubismPhysics3Json.SerializablePhysicsDictionary[physicsSettings.Length]; - for (int i = 0; i < physicsSettings.Length; i++) - { - physicsDictionary[i] = new CubismPhysics3Json.SerializablePhysicsDictionary - { - Id = $"PhysicsSetting{i + 1}", - Name = $"Dummy{i + 1}" - }; - } - var physicsJson = new CubismPhysics3Json - { - Version = 3, - Meta = new CubismPhysics3Json.SerializableMeta - { - PhysicsSettingCount = cubismPhysicsRig.SubRigs.Length, - TotalInputCount = cubismPhysicsRig.SubRigs.Sum(x => x.Input.Length), - TotalOutputCount = cubismPhysicsRig.SubRigs.Sum(x => x.Output.Length), - VertexCount = cubismPhysicsRig.SubRigs.Sum(x => x.Particles.Length), - EffectiveForces = new CubismPhysics3Json.SerializableEffectiveForces - { - Gravity = cubismPhysicsRig.Gravity, - Wind = cubismPhysicsRig.Wind - }, - PhysicsDictionary = physicsDictionary - }, - PhysicsSettings = physicsSettings - }; - return JsonConvert.SerializeObject(physicsJson, Formatting.Indented, new MyJsonConverter2()); } - private static byte[] ParseMoc(MonoBehaviour moc) + private static void ExportClipMotions(string destMotionPath, CubismMotion3Converter converter, bool forceBezier, SortedDictionary motions) { - var reader = moc.reader; - reader.Reset(); - reader.Position += 28; //PPtr m_GameObject, m_Enabled, PPtr - reader.ReadAlignedString(); //m_Name - return reader.ReadBytes(reader.ReadInt32()); + if (converter == null) + return; + + if (converter.AnimationList.Count > 0) + { + Directory.CreateDirectory(destMotionPath); + } + foreach (var animation in converter.AnimationList) + { + var animName = animation.Name; + if (animation.TrackList.Count == 0) + { + Logger.Warning($"Motion \"{animName}\" is empty. Export skipped"); + continue; + } + var motionJson = new CubismMotion3Json(animation, forceBezier); + + if (motions.ContainsKey(animName)) + { + animName = $"{animName}_{animation.GetHashCode()}"; + + if (motions.ContainsKey(animName)) + continue; + } + var motionPath = new JObject(new JProperty("File", $"motions/{animName}.motion3.json")); + motions.Add(animName, new JArray(motionPath)); + File.WriteAllText($"{destMotionPath}{animName}.motion3.json", JsonConvert.SerializeObject(motionJson, Formatting.Indented, new MyJsonConverter())); + } } } } diff --git a/AssetStudioUtility/CubismLive2DExtractor/Live2DMotionMode.cs b/AssetStudioUtility/CubismLive2DExtractor/Live2DMotionMode.cs index 60c3acc..3e87f8d 100644 --- a/AssetStudioUtility/CubismLive2DExtractor/Live2DMotionMode.cs +++ b/AssetStudioUtility/CubismLive2DExtractor/Live2DMotionMode.cs @@ -3,6 +3,7 @@ public enum Live2DMotionMode { MonoBehaviour, - AnimationClip + AnimationClipV1, + AnimationClipV2, } } diff --git a/AssetStudioUtility/ModelConverter.cs b/AssetStudioUtility/ModelConverter.cs index 6dce124..059dc48 100644 --- a/AssetStudioUtility/ModelConverter.cs +++ b/AssetStudioUtility/ModelConverter.cs @@ -1012,18 +1012,16 @@ namespace AssetStudio private void CreateBonePathHash(Transform m_Transform) { var name = GetTransformPathByFather(m_Transform); - var crc = new SevenZip.CRC(); var bytes = Encoding.UTF8.GetBytes(name); - crc.Update(bytes, 0, (uint)bytes.Length); - bonePathHash[crc.GetDigest()] = name; + var crc = SevenZip.CRC.CalculateDigest(bytes, 0, (uint)bytes.Length); + bonePathHash[crc] = name; int index; while ((index = name.IndexOf("/", StringComparison.Ordinal)) >= 0) { name = name.Substring(index + 1); - crc = new SevenZip.CRC(); bytes = Encoding.UTF8.GetBytes(name); - crc.Update(bytes, 0, (uint)bytes.Length); - bonePathHash[crc.GetDigest()] = name; + crc = SevenZip.CRC.CalculateDigest(bytes, 0, (uint)bytes.Length); + bonePathHash[crc] = name; } foreach (var pptr in m_Transform.m_Children) {