[AK][GUI] Add support for portrait sprites

This commit is contained in:
VaDiM 2023-08-22 06:06:40 +03:00
parent 3d7d51b54f
commit 572e3bf0d6
11 changed files with 293 additions and 69 deletions

View File

@ -3,6 +3,7 @@ namespace AssetStudio
{ {
public enum ClassIDType public enum ClassIDType
{ {
AkPortraitSprite = -2,
UnknownType = -1, UnknownType = -1,
Object = 0, Object = 0,
GameObject = 1, GameObject = 1,

View File

@ -182,6 +182,13 @@ namespace AssetStudio
public float width; public float width;
public float height; 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) public Rectf(BinaryReader reader)
{ {
x = reader.ReadSingle(); x = reader.ReadSingle();

View File

@ -360,7 +360,7 @@ namespace AssetStudioGUI
break; 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) switch (e.KeyCode)
{ {
@ -430,6 +430,7 @@ namespace AssetStudioGUI
switch (lastSelectedItem.Type) switch (lastSelectedItem.Type)
{ {
case ClassIDType.Texture2D: case ClassIDType.Texture2D:
case ClassIDType.AkPortraitSprite:
case ClassIDType.Sprite: case ClassIDType.Sprite:
{ {
if (enablePreview.Checked && imageTexture != null) if (enablePreview.Checked && imageTexture != null)
@ -798,42 +799,45 @@ namespace AssetStudioGUI
return; return;
try try
{ {
switch (assetItem.Asset) switch (assetItem.Type)
{ {
case Texture2D m_Texture2D: case ClassIDType.Texture2D:
PreviewTexture2D(assetItem, m_Texture2D); PreviewTexture2D(assetItem, assetItem.Asset as Texture2D);
break; break;
case AudioClip m_AudioClip: case ClassIDType.AudioClip:
PreviewAudioClip(assetItem, m_AudioClip); PreviewAudioClip(assetItem, assetItem.Asset as AudioClip);
break; break;
case Shader m_Shader: case ClassIDType.Shader:
PreviewShader(m_Shader); PreviewShader(assetItem.Asset as Shader);
break; break;
case TextAsset m_TextAsset: case ClassIDType.TextAsset:
PreviewTextAsset(m_TextAsset); PreviewTextAsset(assetItem.Asset as TextAsset);
break; break;
case MonoBehaviour m_MonoBehaviour: case ClassIDType.MonoBehaviour:
PreviewMonoBehaviour(m_MonoBehaviour); PreviewMonoBehaviour(assetItem.Asset as MonoBehaviour);
break; break;
case Font m_Font: case ClassIDType.Font:
PreviewFont(m_Font); PreviewFont(assetItem.Asset as Font);
break; break;
case Mesh m_Mesh: case ClassIDType.Mesh:
PreviewMesh(m_Mesh); PreviewMesh(assetItem.Asset as Mesh);
break; break;
case VideoClip m_VideoClip: case ClassIDType.VideoClip:
PreviewVideoClip(assetItem, m_VideoClip); PreviewVideoClip(assetItem, assetItem.Asset as VideoClip);
break; break;
case MovieTexture _: case ClassIDType.MovieTexture:
StatusStripUpdate("Only supported export."); StatusStripUpdate("Only supported export.");
break; break;
case Sprite m_Sprite: case ClassIDType.Sprite:
PreviewSprite(assetItem, m_Sprite); PreviewSprite(assetItem, assetItem.Asset as Sprite);
break; break;
case Animator _: case ClassIDType.AkPortraitSprite:
PreviewAkPortraitSprite(assetItem);
break;
case ClassIDType.Animator:
StatusStripUpdate("Can be exported to FBX file."); StatusStripUpdate("Can be exported to FBX file.");
break; break;
case AnimationClip _: case ClassIDType.AnimationClip:
StatusStripUpdate("Can be exported with Animator or Objects"); StatusStripUpdate("Can be exported with Animator or Objects");
break; break;
default: 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) private void PreviewTexture(DirectBitmap bitmap)
{ {
imageTexture?.Dispose(); imageTexture?.Dispose();
@ -1449,19 +1472,20 @@ namespace AssetStudioGUI
var assetItem = (AssetItem)assetListView.Items[i]; var assetItem = (AssetItem)assetListView.Items[i];
if (assetItem.Type == ClassIDType.Sprite) if (assetItem.Type == ClassIDType.Sprite)
{ {
var sprite = (Sprite)assetItem.Asset; var m_Sprite = (Sprite)assetItem.Asset;
if (akFixFaceSpriteNamesToolStripMenuItem.Checked) if (akFixFaceSpriteNamesToolStripMenuItem.Checked)
{ {
if ((sprite.m_Name.Length < 3 && sprite.m_Name.All(char.IsDigit)) //not grouped ("spriteIndex") var groupedPattern = new Regex(@"^\d{1,2}\$\d{1,2}$"); // "spriteIndex$groupIndex"
|| (sprite.m_Name.Length < 5 && sprite.m_Name.Contains('$') && sprite.m_Name.Split('$')[0].All(char.IsDigit))) //grouped ("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); 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.useExternalAlpha = akUseExternalAlphaToolStripMenuItem.Checked;
Properties.Settings.Default.Save(); Properties.Settings.Default.Save();
if (lastSelectedItem?.Asset.type == ClassIDType.Sprite) if (lastSelectedItem?.Type == ClassIDType.Sprite)
{ {
StatusStripUpdate(""); StatusStripUpdate("");
PreviewAsset(lastSelectedItem); PreviewAsset(lastSelectedItem);

View File

@ -1,11 +1,14 @@
using AssetStudio; using Arknights.PortraitSpriteMono;
using AssetStudio;
using AssetStudioGUI; using AssetStudioGUI;
using AssetStudioGUI.Properties; using AssetStudioGUI.Properties;
using Newtonsoft.Json;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.ImageSharp.Processing.Processors.Transforms;
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
namespace Arknights 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) 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<Bgra32> tex; Image<Bgra32> tex = null;
Image<Bgra32> alphaTex; Image<Bgra32> alphaTex = null;
if (avgSprite != null && avgSprite.IsHubParsed) if (avgSprite != null && avgSprite.IsHubParsed)
{ {
@ -70,22 +73,14 @@ namespace Arknights
} }
else 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); alphaTex = CutImage(m_AlphaTexture2D.ConvertToImage(false), m_Sprite.m_RD.textureRect, m_Sprite.m_RD.downscaleMultiplier);
} }
switch (spriteMaskMode) return ImageRender(tex, alphaTex, 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;
}
} }
else if (m_Sprite.m_RD.texture.TryGet(out m_Texture2D)) else if (m_Sprite.m_RD.texture.TryGet(out m_Texture2D))
{ {
@ -95,6 +90,83 @@ namespace Arknights
return null; return null;
} }
public static Image<Bgra32> AkGetImage(this PortraitSprite portraitSprite, SpriteMaskMode spriteMaskMode = SpriteMaskMode.On)
{
if (portraitSprite.Texture != null && portraitSprite.AlphaTexture != null)
{
Image<Bgra32> tex = null;
Image<Bgra32> 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<PortraitSprite> GeneratePortraits(AssetItem asset)
{
var portraits = new List<PortraitSprite>();
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<PortraitSpriteConfig>(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<Bgra32> ImageRender(Image<Bgra32> tex, Image<Bgra32> 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) private static IResampler GetResampler(bool isPreview)
{ {
IResampler resampler; IResampler resampler;
@ -169,29 +241,31 @@ namespace Arknights
} }
} }
private static Image<Bgra32> CutImage(Image<Bgra32> originalImage, Rectf textureRect, float downscaleMultiplier) private static Image<Bgra32> CutImage(Image<Bgra32> originalImage, Rectf textureRect, float downscaleMultiplier, bool rotate = false)
{ {
if (originalImage != null) 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 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 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; return null;

View File

@ -1,4 +1,4 @@
using Arknights.AvgCharHub; using Arknights.AvgCharHubMono;
using AssetStudio; using AssetStudio;
using AssetStudioGUI; using AssetStudioGUI;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;

View File

@ -1,6 +1,6 @@
using AssetStudio; using AssetStudio;
namespace Arknights.AvgCharHub namespace Arknights.AvgCharHubMono
{ {
internal class AvgAssetIDs internal class AvgAssetIDs
{ {

View File

@ -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;
}
}
}

View File

@ -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; }
}
}

View File

@ -1,5 +1,6 @@
using System.Windows.Forms; using System.Windows.Forms;
using AssetStudio; using AssetStudio;
using Arknights;
namespace AssetStudioGUI namespace AssetStudioGUI
{ {
@ -15,6 +16,7 @@ namespace AssetStudioGUI
public string InfoText; public string InfoText;
public string UniqueID; public string UniqueID;
public GameObjectTreeNode TreeNode; public GameObjectTreeNode TreeNode;
public PortraitSprite AkPortraitSprite;
public AssetItem(Object asset) public AssetItem(Object asset)
{ {
@ -26,6 +28,19 @@ namespace AssetStudioGUI
FullSize = asset.byteSize; 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() public void SetSubItems()
{ {
SubItems.AddRange(new[] SubItems.AddRange(new[]

View File

@ -247,7 +247,7 @@ namespace AssetStudioGUI
var alias = ""; var alias = "";
var m_Sprite = (Sprite)item.Asset; var m_Sprite = (Sprite)item.Asset;
var spriteMaskMode = Properties.Settings.Default.exportSpriteWithMask ? SpriteMaskMode.Export : SpriteMaskMode.Off; 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 isCharAvgSprite = item.Container.Contains("avg/characters");
var isCharArt = item.Container.Contains("arts/characters"); var isCharArt = item.Container.Contains("arts/characters");
@ -296,8 +296,32 @@ namespace AssetStudioGUI
return false; 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) public static bool ExportRawFile(AssetItem item, string exportPath)
{ {
if (item.Asset == null)
return false;
if (!TryExportFile(exportPath, item, ".dat", out var exportFullPath)) if (!TryExportFile(exportPath, item, ".dat", out var exportFullPath))
return false; return false;
File.WriteAllBytes(exportFullPath, item.Asset.GetRawData()); File.WriteAllBytes(exportFullPath, item.Asset.GetRawData());
@ -375,6 +399,8 @@ namespace AssetStudioGUI
public static bool ExportDumpFile(AssetItem item, string exportPath) public static bool ExportDumpFile(AssetItem item, string exportPath)
{ {
if (item.Asset == null)
return false;
if (!TryExportFile(exportPath, item, ".txt", out var exportFullPath)) if (!TryExportFile(exportPath, item, ".txt", out var exportFullPath))
return false; return false;
var str = item.Asset.Dump(); var str = item.Asset.Dump();
@ -415,6 +441,8 @@ namespace AssetStudioGUI
return ExportMovieTexture(item, exportPath); return ExportMovieTexture(item, exportPath);
case ClassIDType.Sprite: case ClassIDType.Sprite:
return ExportSprite(item, exportPath); return ExportSprite(item, exportPath);
case ClassIDType.AkPortraitSprite:
return ExportPortraitSprite(item, exportPath);
case ClassIDType.Animator: case ClassIDType.Animator:
return ExportAnimator(item, exportPath); return ExportAnimator(item, exportPath);
case ClassIDType.AnimationClip: case ClassIDType.AnimationClip:

View File

@ -263,8 +263,18 @@ namespace AssetStudioGUI
{ {
if (pptr.TryGet(out var obj)) if (pptr.TryGet(out var obj))
{ {
objectAssetItemDic[obj].Container = container; var asset = objectAssetItemDic[obj];
asset.Container = container;
allContainers[obj] = 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) foreach (var tmp in exportableAssets)
@ -726,7 +736,7 @@ namespace AssetStudioGUI
public static string DumpAsset(Object obj) public static string DumpAsset(Object obj)
{ {
var str = obj.Dump(); var str = obj?.Dump();
if (str == null && obj is MonoBehaviour m_MonoBehaviour) if (str == null && obj is MonoBehaviour m_MonoBehaviour)
{ {
var type = MonoBehaviourToTypeTree(m_MonoBehaviour); var type = MonoBehaviourToTypeTree(m_MonoBehaviour);