From 572e3bf0d65f01f80d227d3587866b7c0b75a4c7 Mon Sep 17 00:00:00 2001 From: VaDiM Date: Tue, 22 Aug 2023 06:06:40 +0300 Subject: [PATCH] [AK][GUI] Add support for portrait sprites --- AssetStudio/ClassIDType.cs | 1 + AssetStudio/Classes/Sprite.cs | 7 + AssetStudioGUI/AssetStudioGUIForm.cs | 84 +++++++---- .../Components/Arknights/AkSpriteHelper.cs | 142 +++++++++++++----- .../Components/Arknights/AvgSprite.cs | 2 +- .../Components/Arknights/AvgSpriteConfig.cs | 2 +- .../Components/Arknights/PortraitSprite.cs | 24 +++ .../Arknights/PortraitSpriteConfig.cs | 41 +++++ AssetStudioGUI/Components/AssetItem.cs | 15 ++ AssetStudioGUI/Exporter.cs | 30 +++- AssetStudioGUI/Studio.cs | 14 +- 11 files changed, 293 insertions(+), 69 deletions(-) create mode 100644 AssetStudioGUI/Components/Arknights/PortraitSprite.cs create mode 100644 AssetStudioGUI/Components/Arknights/PortraitSpriteConfig.cs diff --git a/AssetStudio/ClassIDType.cs b/AssetStudio/ClassIDType.cs index f827cae..ce12631 100644 --- a/AssetStudio/ClassIDType.cs +++ b/AssetStudio/ClassIDType.cs @@ -3,6 +3,7 @@ namespace AssetStudio { public enum ClassIDType { + AkPortraitSprite = -2, UnknownType = -1, Object = 0, GameObject = 1, diff --git a/AssetStudio/Classes/Sprite.cs b/AssetStudio/Classes/Sprite.cs index 034c02b..28f7803 100644 --- a/AssetStudio/Classes/Sprite.cs +++ b/AssetStudio/Classes/Sprite.cs @@ -182,6 +182,13 @@ namespace AssetStudio public float width; public float height; + public Rectf(float x, float y, float w, float h) { + this.x = x; + this.y = y; + width = w; + height = h; + } + public Rectf(BinaryReader reader) { x = reader.ReadSingle(); diff --git a/AssetStudioGUI/AssetStudioGUIForm.cs b/AssetStudioGUI/AssetStudioGUIForm.cs index 13f7a9c..d9753ae 100644 --- a/AssetStudioGUI/AssetStudioGUIForm.cs +++ b/AssetStudioGUI/AssetStudioGUIForm.cs @@ -360,7 +360,7 @@ namespace AssetStudioGUI break; } } - else if (lastSelectedItem?.Type == ClassIDType.Sprite && !((Sprite)lastSelectedItem.Asset).m_RD.alphaTexture.IsNull) + else if ((lastSelectedItem?.Type == ClassIDType.Sprite && !((Sprite)lastSelectedItem.Asset).m_RD.alphaTexture.IsNull) || lastSelectedItem?.Type == ClassIDType.AkPortraitSprite) { switch (e.KeyCode) { @@ -430,6 +430,7 @@ namespace AssetStudioGUI switch (lastSelectedItem.Type) { case ClassIDType.Texture2D: + case ClassIDType.AkPortraitSprite: case ClassIDType.Sprite: { if (enablePreview.Checked && imageTexture != null) @@ -798,42 +799,45 @@ namespace AssetStudioGUI return; try { - switch (assetItem.Asset) + switch (assetItem.Type) { - case Texture2D m_Texture2D: - PreviewTexture2D(assetItem, m_Texture2D); + case ClassIDType.Texture2D: + PreviewTexture2D(assetItem, assetItem.Asset as Texture2D); break; - case AudioClip m_AudioClip: - PreviewAudioClip(assetItem, m_AudioClip); + case ClassIDType.AudioClip: + PreviewAudioClip(assetItem, assetItem.Asset as AudioClip); break; - case Shader m_Shader: - PreviewShader(m_Shader); + case ClassIDType.Shader: + PreviewShader(assetItem.Asset as Shader); break; - case TextAsset m_TextAsset: - PreviewTextAsset(m_TextAsset); + case ClassIDType.TextAsset: + PreviewTextAsset(assetItem.Asset as TextAsset); break; - case MonoBehaviour m_MonoBehaviour: - PreviewMonoBehaviour(m_MonoBehaviour); + case ClassIDType.MonoBehaviour: + PreviewMonoBehaviour(assetItem.Asset as MonoBehaviour); break; - case Font m_Font: - PreviewFont(m_Font); + case ClassIDType.Font: + PreviewFont(assetItem.Asset as Font); break; - case Mesh m_Mesh: - PreviewMesh(m_Mesh); + case ClassIDType.Mesh: + PreviewMesh(assetItem.Asset as Mesh); break; - case VideoClip m_VideoClip: - PreviewVideoClip(assetItem, m_VideoClip); + case ClassIDType.VideoClip: + PreviewVideoClip(assetItem, assetItem.Asset as VideoClip); break; - case MovieTexture _: + case ClassIDType.MovieTexture: StatusStripUpdate("Only supported export."); break; - case Sprite m_Sprite: - PreviewSprite(assetItem, m_Sprite); + case ClassIDType.Sprite: + PreviewSprite(assetItem, assetItem.Asset as Sprite); break; - case Animator _: + case ClassIDType.AkPortraitSprite: + PreviewAkPortraitSprite(assetItem); + break; + case ClassIDType.Animator: StatusStripUpdate("Can be exported to FBX file."); break; - case AnimationClip _: + case ClassIDType.AnimationClip: StatusStripUpdate("Can be exported with Animator or Objects"); break; default: @@ -1344,6 +1348,25 @@ namespace AssetStudioGUI } } + private void PreviewAkPortraitSprite(AssetItem assetItem) + { + var image = assetItem.AkPortraitSprite.AkGetImage(spriteMaskMode: spriteMaskVisibleMode); + if (image != null) + { + var bitmap = new DirectBitmap(image); + image.Dispose(); + assetItem.InfoText = $"Width: {bitmap.Width}\nHeight: {bitmap.Height}\n"; + assetItem.InfoText += $"Alpha mask: {spriteMaskVisibleMode}"; + PreviewTexture(bitmap); + + StatusStripUpdate("'Ctrl'+'A' - Enable/Disable alpha mask usage. 'Ctrl'+'M' - Show alpha mask only."); + } + else + { + StatusStripUpdate("Unsupported sprite for preview."); + } + } + private void PreviewTexture(DirectBitmap bitmap) { imageTexture?.Dispose(); @@ -1449,19 +1472,20 @@ namespace AssetStudioGUI var assetItem = (AssetItem)assetListView.Items[i]; if (assetItem.Type == ClassIDType.Sprite) { - var sprite = (Sprite)assetItem.Asset; + var m_Sprite = (Sprite)assetItem.Asset; if (akFixFaceSpriteNamesToolStripMenuItem.Checked) { - if ((sprite.m_Name.Length < 3 && sprite.m_Name.All(char.IsDigit)) //not grouped ("spriteIndex") - || (sprite.m_Name.Length < 5 && sprite.m_Name.Contains('$') && sprite.m_Name.Split('$')[0].All(char.IsDigit))) //grouped ("spriteIndex$groupIndex") + var groupedPattern = new Regex(@"^\d{1,2}\$\d{1,2}$"); // "spriteIndex$groupIndex" + var notGroupedPattern = new Regex(@"^\d{1,2}$"); // "spriteIndex" + if (groupedPattern.IsMatch(m_Sprite.m_Name) || notGroupedPattern.IsMatch(m_Sprite.m_Name)) { var fullName = Path.GetFileNameWithoutExtension(assetItem.Container); - assetItem.Text = $"{fullName}#{sprite.m_Name}"; + assetItem.Text = $"{fullName}#{m_Sprite.m_Name}"; } } - else if (assetItem.Text != sprite.m_Name) + else if (assetItem.Text != m_Sprite.m_Name) { - assetItem.Text = sprite.m_Name; + assetItem.Text = m_Sprite.m_Name; } } } @@ -1968,7 +1992,7 @@ namespace AssetStudioGUI Properties.Settings.Default.useExternalAlpha = akUseExternalAlphaToolStripMenuItem.Checked; Properties.Settings.Default.Save(); - if (lastSelectedItem?.Asset.type == ClassIDType.Sprite) + if (lastSelectedItem?.Type == ClassIDType.Sprite) { StatusStripUpdate(""); PreviewAsset(lastSelectedItem); diff --git a/AssetStudioGUI/Components/Arknights/AkSpriteHelper.cs b/AssetStudioGUI/Components/Arknights/AkSpriteHelper.cs index 8d00b81..a7989ff 100644 --- a/AssetStudioGUI/Components/Arknights/AkSpriteHelper.cs +++ b/AssetStudioGUI/Components/Arknights/AkSpriteHelper.cs @@ -1,11 +1,14 @@ -using AssetStudio; +using Arknights.PortraitSpriteMono; +using AssetStudio; using AssetStudioGUI; using AssetStudioGUI.Properties; +using Newtonsoft.Json; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Transforms; using System; +using System.Collections.Generic; using System.IO; namespace Arknights @@ -44,8 +47,8 @@ namespace Arknights { if (m_Sprite.m_RD.texture.TryGet(out var m_Texture2D) && m_Sprite.m_RD.alphaTexture.TryGet(out var m_AlphaTexture2D) && spriteMaskMode != SpriteMaskMode.Off) { - Image tex; - Image alphaTex; + Image tex = null; + Image alphaTex = null; if (avgSprite != null && avgSprite.IsHubParsed) { @@ -70,22 +73,14 @@ namespace Arknights } else { - tex = CutImage(m_Texture2D.ConvertToImage(false), m_Sprite.m_RD.textureRect, m_Sprite.m_RD.downscaleMultiplier); + if (spriteMaskMode != SpriteMaskMode.MaskOnly) + { + tex = CutImage(m_Texture2D.ConvertToImage(false), m_Sprite.m_RD.textureRect, m_Sprite.m_RD.downscaleMultiplier); + } alphaTex = CutImage(m_AlphaTexture2D.ConvertToImage(false), m_Sprite.m_RD.textureRect, m_Sprite.m_RD.downscaleMultiplier); } - switch (spriteMaskMode) - { - case SpriteMaskMode.On: - tex.ApplyRGBMask(alphaTex, isPreview: true); - return tex; - case SpriteMaskMode.Export: - tex.ApplyRGBMask(alphaTex); - return tex; - case SpriteMaskMode.MaskOnly: - tex.Dispose(); - return alphaTex; - } + return ImageRender(tex, alphaTex, spriteMaskMode); } else if (m_Sprite.m_RD.texture.TryGet(out m_Texture2D)) { @@ -95,6 +90,83 @@ namespace Arknights return null; } + public static Image AkGetImage(this PortraitSprite portraitSprite, SpriteMaskMode spriteMaskMode = SpriteMaskMode.On) + { + if (portraitSprite.Texture != null && portraitSprite.AlphaTexture != null) + { + Image tex = null; + Image alphaTex = null; + + if (spriteMaskMode != SpriteMaskMode.MaskOnly) + { + tex = CutImage(portraitSprite.Texture.ConvertToImage(false), portraitSprite.TextureRect, portraitSprite.DownscaleMultiplier, portraitSprite.Rotate); + } + if (spriteMaskMode != SpriteMaskMode.Off) + { + alphaTex = CutImage(portraitSprite.AlphaTexture.ConvertToImage(false), portraitSprite.TextureRect, portraitSprite.DownscaleMultiplier, portraitSprite.Rotate); + } + + return ImageRender(tex, alphaTex, spriteMaskMode); + } + + return null; + } + + public static List GeneratePortraits(AssetItem asset) + { + var portraits = new List(); + + var portraitsDict = ((MonoBehaviour)asset.Asset).ToType(); + if (portraitsDict == null) + { + Logger.Warning("Portraits MonoBehaviour is not readable."); + return portraits; + } + var portraitsJson = JsonConvert.SerializeObject(portraitsDict); + var portraitsData = JsonConvert.DeserializeObject(portraitsJson); + + var atlasTex = (Texture2D)Studio.exportableAssets.Find(x => x.m_PathID == portraitsData._atlas.Texture.m_PathID).Asset; + var atlasAlpha = (Texture2D)Studio.exportableAssets.Find(x => x.m_PathID == portraitsData._atlas.Alpha.m_PathID).Asset; + + foreach (var portraitData in portraitsData._sprites) + { + var portraitSprite = new PortraitSprite() + { + Name = portraitData.Name, + AssetsFile = atlasTex.assetsFile, + Container = asset.Container, + Texture = atlasTex, + AlphaTexture = atlasAlpha, + TextureRect = new Rectf(portraitData.Rect.X, portraitData.Rect.Y, portraitData.Rect.W, portraitData.Rect.H), + Rotate = portraitData.Rotate, + }; + portraits.Add(portraitSprite); + } + + return portraits; + } + + private static Image ImageRender(Image tex, Image alpha, SpriteMaskMode maskMode) + { + switch (maskMode) + { + case SpriteMaskMode.On: + tex.ApplyRGBMask(alpha, isPreview: true); + return tex; + case SpriteMaskMode.Off: + alpha?.Dispose(); + return tex; + case SpriteMaskMode.MaskOnly: + tex?.Dispose(); + return alpha; + case SpriteMaskMode.Export: + tex.ApplyRGBMask(alpha); + return tex; + } + + return null; + } + private static IResampler GetResampler(bool isPreview) { IResampler resampler; @@ -169,29 +241,31 @@ namespace Arknights } } - private static Image CutImage(Image originalImage, Rectf textureRect, float downscaleMultiplier) + private static Image CutImage(Image originalImage, Rectf textureRect, float downscaleMultiplier, bool rotate = false) { if (originalImage != null) { - using (originalImage) + if (downscaleMultiplier > 0f && downscaleMultiplier != 1f) { - if (downscaleMultiplier > 0f && downscaleMultiplier != 1f) - { - var newSize = (Size)(originalImage.Size() / downscaleMultiplier); - originalImage.Mutate(x => x.Resize(newSize, KnownResamplers.Lanczos3, compand: true)); - } - var rectX = (int)Math.Floor(textureRect.x); - var rectY = (int)Math.Floor(textureRect.y); - var rectRight = (int)Math.Ceiling(textureRect.x + textureRect.width); - var rectBottom = (int)Math.Ceiling(textureRect.y + textureRect.height); - rectRight = Math.Min(rectRight, originalImage.Width); - rectBottom = Math.Min(rectBottom, originalImage.Height); - var rect = new Rectangle(rectX, rectY, rectRight - rectX, rectBottom - rectY); - var spriteImage = originalImage.Clone(x => x.Crop(rect)); - spriteImage.Mutate(x => x.Flip(FlipMode.Vertical)); - - return spriteImage; + var newSize = (Size)(originalImage.Size() / downscaleMultiplier); + originalImage.Mutate(x => x.Resize(newSize, KnownResamplers.Lanczos3, compand: true)); } + var rectX = (int)Math.Floor(textureRect.x); + var rectY = (int)Math.Floor(textureRect.y); + var rectRight = (int)Math.Ceiling(textureRect.x + textureRect.width); + var rectBottom = (int)Math.Ceiling(textureRect.y + textureRect.height); + rectRight = Math.Min(rectRight, originalImage.Width); + rectBottom = Math.Min(rectBottom, originalImage.Height); + var rect = new Rectangle(rectX, rectY, rectRight - rectX, rectBottom - rectY); + var spriteImage = originalImage.Clone(x => x.Crop(rect)); + originalImage.Dispose(); + if (rotate) + { + spriteImage.Mutate(x => x.Rotate(RotateMode.Rotate270)); + } + spriteImage.Mutate(x => x.Flip(FlipMode.Vertical)); + + return spriteImage; } return null; diff --git a/AssetStudioGUI/Components/Arknights/AvgSprite.cs b/AssetStudioGUI/Components/Arknights/AvgSprite.cs index 44d046a..2a7e97d 100644 --- a/AssetStudioGUI/Components/Arknights/AvgSprite.cs +++ b/AssetStudioGUI/Components/Arknights/AvgSprite.cs @@ -1,4 +1,4 @@ -using Arknights.AvgCharHub; +using Arknights.AvgCharHubMono; using AssetStudio; using AssetStudioGUI; using SixLabors.ImageSharp; diff --git a/AssetStudioGUI/Components/Arknights/AvgSpriteConfig.cs b/AssetStudioGUI/Components/Arknights/AvgSpriteConfig.cs index d834743..7db3f6e 100644 --- a/AssetStudioGUI/Components/Arknights/AvgSpriteConfig.cs +++ b/AssetStudioGUI/Components/Arknights/AvgSpriteConfig.cs @@ -1,6 +1,6 @@ using AssetStudio; -namespace Arknights.AvgCharHub +namespace Arknights.AvgCharHubMono { internal class AvgAssetIDs { diff --git a/AssetStudioGUI/Components/Arknights/PortraitSprite.cs b/AssetStudioGUI/Components/Arknights/PortraitSprite.cs new file mode 100644 index 0000000..84973b8 --- /dev/null +++ b/AssetStudioGUI/Components/Arknights/PortraitSprite.cs @@ -0,0 +1,24 @@ +using AssetStudio; + + +namespace Arknights +{ + internal class PortraitSprite + { + public string Name { get; set; } + public ClassIDType Type { get; } + public SerializedFile AssetsFile { get; set; } + public string Container { get; set; } + public Texture2D Texture { get; set; } + public Texture2D AlphaTexture { get; set; } + public Rectf TextureRect { get; set; } + public bool Rotate { get; set; } + public float DownscaleMultiplier { get; } + + public PortraitSprite() + { + Type = ClassIDType.AkPortraitSprite; + DownscaleMultiplier = 1f; + } + } +} diff --git a/AssetStudioGUI/Components/Arknights/PortraitSpriteConfig.cs b/AssetStudioGUI/Components/Arknights/PortraitSpriteConfig.cs new file mode 100644 index 0000000..f59e118 --- /dev/null +++ b/AssetStudioGUI/Components/Arknights/PortraitSpriteConfig.cs @@ -0,0 +1,41 @@ +namespace Arknights.PortraitSpriteMono +{ + internal class PortraitRect + { + public float X { get; set; } + public float Y { get; set; } + public float W { get; set; } + public float H { get; set; } + } + + internal class AtlasSprite + { + public string Name { get; set; } + public string Guid { get; set; } + public int Atlas { get; set; } + public PortraitRect Rect { get; set; } + public bool Rotate { get; set; } + } + + internal class TextureIDs + { + public int m_FileID { get; set; } + public long m_PathID { get; set; } + } + + internal class AtlasInfo + { + public int Index { get; set; } + public TextureIDs Texture { get; set; } + public TextureIDs Alpha { get; set; } + public int Size { get; set; } + } + + internal class PortraitSpriteConfig + { + public string m_Name { get; set; } + public AtlasSprite[] _sprites { get; set; } + public AtlasInfo _atlas { get; set; } + public int _index { get; set; } + } +} diff --git a/AssetStudioGUI/Components/AssetItem.cs b/AssetStudioGUI/Components/AssetItem.cs index 05f4ff9..7d749fa 100644 --- a/AssetStudioGUI/Components/AssetItem.cs +++ b/AssetStudioGUI/Components/AssetItem.cs @@ -1,5 +1,6 @@ using System.Windows.Forms; using AssetStudio; +using Arknights; namespace AssetStudioGUI { @@ -15,6 +16,7 @@ namespace AssetStudioGUI public string InfoText; public string UniqueID; public GameObjectTreeNode TreeNode; + public PortraitSprite AkPortraitSprite; public AssetItem(Object asset) { @@ -26,6 +28,19 @@ namespace AssetStudioGUI FullSize = asset.byteSize; } + public AssetItem(PortraitSprite akPortraitSprite) + { + Asset = null; + SourceFile = akPortraitSprite.AssetsFile; + Container = akPortraitSprite.Container; + Type = akPortraitSprite.Type; + TypeString = Type.ToString(); + Text = akPortraitSprite.Name; + m_PathID = -1; + FullSize = (long)(akPortraitSprite.TextureRect.width * akPortraitSprite.TextureRect.height * 4); + AkPortraitSprite = akPortraitSprite; + } + public void SetSubItems() { SubItems.AddRange(new[] diff --git a/AssetStudioGUI/Exporter.cs b/AssetStudioGUI/Exporter.cs index 18ff32f..01294d0 100644 --- a/AssetStudioGUI/Exporter.cs +++ b/AssetStudioGUI/Exporter.cs @@ -247,7 +247,7 @@ namespace AssetStudioGUI var alias = ""; var m_Sprite = (Sprite)item.Asset; var spriteMaskMode = Properties.Settings.Default.exportSpriteWithMask ? SpriteMaskMode.Export : SpriteMaskMode.Off; - var type = Properties.Settings.Default.convertType; + var type = Properties.Settings.Default.convertType; var isCharAvgSprite = item.Container.Contains("avg/characters"); var isCharArt = item.Container.Contains("arts/characters"); @@ -296,8 +296,32 @@ namespace AssetStudioGUI return false; } + public static bool ExportPortraitSprite(AssetItem item, string exportPath) + { + var type = Properties.Settings.Default.convertType; + var spriteMaskMode = Properties.Settings.Default.exportSpriteWithMask ? SpriteMaskMode.Export : SpriteMaskMode.Off; + if (!TryExportFile(exportPath, item, "." + type.ToString().ToLower(), out var exportFullPath)) + return false; + + var image = item.AkPortraitSprite.AkGetImage(spriteMaskMode: spriteMaskMode); + if (image != null) + { + using (image) + { + using (var file = File.OpenWrite(exportFullPath)) + { + image.WriteToStream(file, type); + } + return true; + } + } + return false; + } + public static bool ExportRawFile(AssetItem item, string exportPath) { + if (item.Asset == null) + return false; if (!TryExportFile(exportPath, item, ".dat", out var exportFullPath)) return false; File.WriteAllBytes(exportFullPath, item.Asset.GetRawData()); @@ -375,6 +399,8 @@ namespace AssetStudioGUI public static bool ExportDumpFile(AssetItem item, string exportPath) { + if (item.Asset == null) + return false; if (!TryExportFile(exportPath, item, ".txt", out var exportFullPath)) return false; var str = item.Asset.Dump(); @@ -415,6 +441,8 @@ namespace AssetStudioGUI return ExportMovieTexture(item, exportPath); case ClassIDType.Sprite: return ExportSprite(item, exportPath); + case ClassIDType.AkPortraitSprite: + return ExportPortraitSprite(item, exportPath); case ClassIDType.Animator: return ExportAnimator(item, exportPath); case ClassIDType.AnimationClip: diff --git a/AssetStudioGUI/Studio.cs b/AssetStudioGUI/Studio.cs index 6d55393..95621c6 100644 --- a/AssetStudioGUI/Studio.cs +++ b/AssetStudioGUI/Studio.cs @@ -263,8 +263,18 @@ namespace AssetStudioGUI { if (pptr.TryGet(out var obj)) { - objectAssetItemDic[obj].Container = container; + var asset = objectAssetItemDic[obj]; + asset.Container = container; allContainers[obj] = container; + + if (asset.Type == ClassIDType.MonoBehaviour && container.Contains("/arts/charportraits/portraits")) + { + var portraitsList = Arknights.AkSpriteHelper.GeneratePortraits(asset); + foreach (var portrait in portraitsList) + { + exportableAssets.Add(new AssetItem(portrait)); + } + } } } foreach (var tmp in exportableAssets) @@ -726,7 +736,7 @@ namespace AssetStudioGUI public static string DumpAsset(Object obj) { - var str = obj.Dump(); + var str = obj?.Dump(); if (str == null && obj is MonoBehaviour m_MonoBehaviour) { var type = MonoBehaviourToTypeTree(m_MonoBehaviour);