Add option to export images in "assetName@hash" name format

- Redesigned spriteHelper & texture2DConverter
- Improved speed of texture2D preview a bit
This commit is contained in:
VaDiM
2026-03-13 13:27:47 +03:00
parent 94090a997a
commit 61d9bc8f15
16 changed files with 611 additions and 407 deletions
+28
View File
@@ -2,6 +2,8 @@
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Tga;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System.IO;
namespace AssetStudio
@@ -48,5 +50,31 @@ namespace AssetStudio
image.WriteToStream(stream, imageFormat);
return stream;
}
public static void ApplyRgbMask(this Image<Bgra32> srcTex, Image<Bgra32> texMask, bool fastResize = false)
{
using (texMask)
{
if (srcTex.Width != texMask.Width || srcTex.Height != texMask.Height)
{
var resampler = fastResize ? KnownResamplers.NearestNeighbor : KnownResamplers.Bicubic;
texMask.Mutate(x => x.Resize(srcTex.Width, srcTex.Height, resampler));
}
srcTex.ProcessPixelRows(texMask, (sourceTex, targetTexMask) =>
{
for (var y = 0; y < texMask.Height; y++)
{
var texRow = sourceTex.GetRowSpan(y);
var maskRow = targetTexMask.GetRowSpan(y);
for (var x = 0; x < maskRow.Length; x++)
{
var grayscale = (byte)((maskRow[x].R + maskRow[x].G + maskRow[x].B) / 3);
texRow[x].A = grayscale;
}
}
});
}
}
}
}
@@ -4,6 +4,7 @@ using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -15,92 +16,133 @@ namespace AssetStudio
{
Off,
On,
Preview,
MaskOnly,
Export
}
public static class SpriteHelper
public class SpriteConverter
{
public static Image<Bgra32> GetImage(this Sprite m_Sprite, SpriteMaskMode spriteMaskMode = SpriteMaskMode.On)
private Sprite sprite;
private Rectf textureRect;
private Vector2 textureRectOffset;
private float downscaleMultiplier;
private SpriteSettings settingsRaw;
private bool cutImageWithCanvas;
private int resultWidth;
private int resultHeight;
public Texture2D m_Texture2D { get; }
public Texture2D m_AlphaTexture2D { get; }
public SpriteConverter(Sprite m_Sprite, bool cutWithCanvas = true)
{
sprite = m_Sprite;
cutImageWithCanvas = cutWithCanvas;
if (m_Sprite.m_SpriteAtlas != null && m_Sprite.m_SpriteAtlas.TryGet(out var m_SpriteAtlas))
{
if (m_SpriteAtlas.m_RenderDataMap.TryGetValue(m_Sprite.m_RenderDataKey, out var spriteAtlasData) && spriteAtlasData.texture.TryGet(out var m_Texture2D))
if (m_SpriteAtlas.m_RenderDataMap.TryGetValue(m_Sprite.m_RenderDataKey, out var spriteAtlasData) && spriteAtlasData.texture.TryGet(out var texture2D))
{
return CutImage(m_Sprite, m_Texture2D, spriteAtlasData.textureRect, spriteAtlasData.textureRectOffset, spriteAtlasData.downscaleMultiplier, spriteAtlasData.settingsRaw);
m_Texture2D = texture2D;
if (spriteAtlasData.alphaTexture.TryGet(out var alphaTexture2D))
m_AlphaTexture2D = alphaTexture2D;
textureRect = spriteAtlasData.textureRect;
textureRectOffset = spriteAtlasData.textureRectOffset;
downscaleMultiplier = spriteAtlasData.downscaleMultiplier;
settingsRaw = spriteAtlasData.settingsRaw;
}
}
else
else if (m_Sprite.m_RD.texture.TryGet(out var texture2D))
{
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 = null;
if (spriteMaskMode != SpriteMaskMode.MaskOnly)
{
tex = CutImageWithCanvas(m_Sprite, m_Texture2D, m_Sprite.m_RD.textureRect, m_Sprite.m_RD.textureRectOffset, m_Sprite.m_RD.downscaleMultiplier, m_Sprite.m_RD.settingsRaw);
}
var alphaTex = CutImageWithCanvas(m_Sprite, m_AlphaTexture2D, m_Sprite.m_RD.textureRect, m_Sprite.m_RD.textureRectOffset, m_Sprite.m_RD.downscaleMultiplier, m_Sprite.m_RD.settingsRaw);
switch (spriteMaskMode)
{
case SpriteMaskMode.On:
tex.ApplyRGBMask(alphaTex, isPreview: true);
return tex;
case SpriteMaskMode.Export:
tex.ApplyRGBMask(alphaTex);
return tex;
case SpriteMaskMode.MaskOnly:
return alphaTex;
}
}
else if (m_Sprite.m_RD.texture.TryGet(out m_Texture2D))
{
return CutImageWithCanvas(m_Sprite, m_Texture2D, m_Sprite.m_RD.textureRect, m_Sprite.m_RD.textureRectOffset, m_Sprite.m_RD.downscaleMultiplier, m_Sprite.m_RD.settingsRaw);
}
m_Texture2D = texture2D;
if (m_Sprite.m_RD.alphaTexture.TryGet(out var alphaTexture2D))
m_AlphaTexture2D = alphaTexture2D;
textureRect = m_Sprite.m_RD.textureRect;
textureRectOffset = m_Sprite.m_RD.textureRectOffset;
downscaleMultiplier = m_Sprite.m_RD.downscaleMultiplier;
settingsRaw = m_Sprite.m_RD.settingsRaw;
}
return null;
if (textureRect == null)
return;
resultWidth = cutImageWithCanvas
? (int)sprite.m_Rect.width
: (int)textureRect.width;
resultHeight = cutImageWithCanvas
? (int)sprite.m_Rect.height
: (int)textureRect.height;
}
private static void ApplyRGBMask(this Image<Bgra32> tex, Image<Bgra32> texMask, bool isPreview = false)
public bool TryConvertToImage(out Image<Bgra32> spriteImage, SpriteMaskMode maskMode)
{
using (texMask)
spriteImage = null;
if (m_Texture2D == null)
return false;
if (m_AlphaTexture2D != null)
{
if (tex.Width != texMask.Width || tex.Height != texMask.Height)
var alphaTex = CutSprite(m_AlphaTexture2D);
if (maskMode != SpriteMaskMode.MaskOnly)
{
var resampler = isPreview ? KnownResamplers.NearestNeighbor : KnownResamplers.Bicubic;
texMask.Mutate(x => x.Resize(tex.Width, tex.Height, resampler));
spriteImage = CutSprite(m_Texture2D);
}
tex.ProcessPixelRows(texMask, (sourceTex, targetTexMask) =>
switch (maskMode)
{
for (int y = 0; y < texMask.Height; y++)
{
var texRow = sourceTex.GetRowSpan(y);
var maskRow = targetTexMask.GetRowSpan(y);
for (int x = 0; x < maskRow.Length; x++)
{
var grayscale = (byte)((maskRow[x].R + maskRow[x].G + maskRow[x].B) / 3);
texRow[x].A = grayscale;
}
}
});
case SpriteMaskMode.MaskOnly:
spriteImage = alphaTex;
break;
case SpriteMaskMode.Preview when alphaTex != null:
spriteImage.ApplyRgbMask(alphaTex, fastResize: true);
break;
case SpriteMaskMode.On when alphaTex != null:
spriteImage.ApplyRgbMask(alphaTex);
break;
}
return spriteImage != null;
}
spriteImage = CutSprite(m_Texture2D);
return spriteImage != null;
}
private Image<Bgra32> CutSprite(Texture2D texture2D)
{
var croppedImage = CutImage(texture2D);
if (croppedImage == null)
return null;
if (!cutImageWithCanvas)
return croppedImage;
using (croppedImage)
{
var canvas = new Image<Bgra32>(resultWidth, resultHeight);
canvas.Mutate(ctx => ctx.DrawImage(
croppedImage,
new Point((int)textureRectOffset.X, (int)(resultHeight - textureRectOffset.Y - croppedImage.Height)),
1f)
);
return canvas;
}
}
private static Image<Bgra32> CutImage(Sprite m_Sprite, Texture2D m_Texture2D, Rectf textureRect, Vector2 textureRectOffset, float downscaleMultiplier, SpriteSettings settingsRaw)
private Image<Bgra32> CutImage(Texture2D texture2D)
{
if (!m_Texture2D.TryConvertToImage(false, out var textureImage))
if (!texture2D.TryConvertToImage(false, out var textureImage))
return null;
if (downscaleMultiplier > 0f && downscaleMultiplier != 1f)
{
var width = (int)(m_Texture2D.m_Width / downscaleMultiplier);
var height = (int)(m_Texture2D.m_Height / downscaleMultiplier);
var width = (int)(texture2D.m_Width / downscaleMultiplier);
var height = (int)(texture2D.m_Height / downscaleMultiplier);
textureImage.Mutate(x => x.Resize(width, height));
}
var rectX = (int)MathF.Floor(textureRect.x);
var rectY = (int)MathF.Floor(textureRect.y);
var rectX = (int)textureRect.x;
var rectY = (int)textureRect.y;
var rectRight = (int)MathF.Ceiling(textureRect.x + textureRect.width);
var rectBottom = (int)MathF.Ceiling(textureRect.y + textureRect.height);
rectRight = Math.Min(rectRight, textureImage.Width);
@@ -133,9 +175,9 @@ namespace AssetStudio
{
try
{
var matrix = Matrix3x2.CreateScale(m_Sprite.m_PixelsToUnits);
matrix *= Matrix3x2.CreateTranslation(m_Sprite.m_Rect.width * m_Sprite.m_Pivot.X - textureRectOffset.X, m_Sprite.m_Rect.height * m_Sprite.m_Pivot.Y - textureRectOffset.Y);
var triangles = GetTriangles(m_Sprite.m_RD);
var matrix = Matrix3x2.CreateScale(sprite.m_PixelsToUnits);
matrix *= Matrix3x2.CreateTranslation(sprite.m_Rect.width * sprite.m_Pivot.X - textureRectOffset.X, sprite.m_Rect.height * sprite.m_Pivot.Y - textureRectOffset.Y);
var triangles = GetTriangles(sprite.m_RD);
var points = triangles.Select(x => x.Select(y => new PointF(y.X, y.Y)));
var pathBuilder = new PathBuilder(matrix);
foreach (var p in points)
@@ -177,7 +219,7 @@ namespace AssetStudio
}
catch (Exception e)
{
Logger.Warning($"{m_Sprite.m_Name} Unable to render the packed sprite correctly.\n{e}");
Logger.Warning($"{sprite.m_Name} Unable to render the packed sprite correctly.\n{e}");
}
}
@@ -186,19 +228,6 @@ namespace AssetStudio
return spriteImage;
}
private static Image<Bgra32> CutImageWithCanvas(Sprite m_Sprite, Texture2D m_Texture2D, Rectf textureRect, Vector2 textureRectOffset, float downscaleMultiplier, SpriteSettings settingsRaw)
{
var cropped = CutImage(m_Sprite, m_Texture2D, textureRect, textureRectOffset, downscaleMultiplier, settingsRaw);
if (cropped == null)
return null;
var canvas = new Image<Bgra32>((int)MathF.Floor(m_Sprite.m_Rect.width), (int)MathF.Floor(m_Sprite.m_Rect.height));
canvas.Mutate(ctx => ctx.DrawImage(cropped, new Point((int)MathF.Floor(textureRectOffset.X), (int)MathF.Floor(m_Sprite.m_Rect.height - textureRectOffset.Y - cropped.Height)), 1f));
cropped.Dispose();
return canvas;
}
private static Vector2[][] GetTriangles(SpriteRenderData m_RD)
{
if (m_RD.vertices != null) //5.6 down
@@ -254,5 +283,40 @@ namespace AssetStudio
return triangles.ToArray();
}
}
public ulong GetSpriteHash()
{
ulong spriteHash = 0;
if (m_Texture2D == null)
return spriteHash;
spriteHash = m_Texture2D.GetImageDataHash();
if (m_AlphaTexture2D != null)
{
spriteHash ^= m_AlphaTexture2D.GetImageDataHash();
}
return (m_Texture2D.m_Width, m_Texture2D.m_Height) == (resultWidth, resultHeight)
? spriteHash
: spriteHash ^ GetRectHash();
}
private ulong GetRectHash()
{
Span<byte> buff = stackalloc byte[40];
BinaryPrimitives.WriteInt32LittleEndian(buff, (int)textureRect.x);
BinaryPrimitives.WriteInt32LittleEndian(buff.Slice(4), (int)textureRect.y);
BinaryPrimitives.WriteInt32LittleEndian(buff.Slice(8), (int)textureRect.width);
BinaryPrimitives.WriteInt32LittleEndian(buff.Slice(12), (int)textureRect.height);
BinaryPrimitives.WriteInt32LittleEndian(buff.Slice(16), (int)textureRectOffset.X);
BinaryPrimitives.WriteInt32LittleEndian(buff.Slice(20), (int)textureRectOffset.Y);
BinaryPrimitives.WriteInt32LittleEndian(buff.Slice(24), (int)sprite.m_Rect.x);
BinaryPrimitives.WriteInt32LittleEndian(buff.Slice(28), (int)sprite.m_Rect.y);
BinaryPrimitives.WriteInt32LittleEndian(buff.Slice(32), (int)sprite.m_Rect.width);
BinaryPrimitives.WriteInt32LittleEndian(buff.Slice(36), (int)sprite.m_Rect.height);
return System.IO.Hashing.XxHash3.HashToUInt64(buff);
}
}
}
+13
View File
@@ -0,0 +1,13 @@
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace AssetStudio
{
public static class SpriteExtensions
{
public static bool TryGetImage(this Sprite m_Sprite, out Image<Bgra32> spriteImage, SpriteMaskMode spriteMaskMode = SpriteMaskMode.On)
{
return new SpriteConverter(m_Sprite).TryConvertToImage(out spriteImage, spriteMaskMode);
}
}
}
File diff suppressed because it is too large Load Diff
+5 -26
View File
@@ -1,8 +1,6 @@
using System;
using System.IO;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System.IO;
namespace AssetStudio
{
@@ -10,32 +8,13 @@ namespace AssetStudio
{
public static bool TryConvertToImage(this Texture2D m_Texture2D, bool flip, out Image<Bgra32> image)
{
image = null;
var converter = new Texture2DConverter(m_Texture2D);
var buff = BigArrayPool<byte>.Shared.Rent(converter.OutputDataSize);
var spanBuff = buff.AsSpan(0, converter.OutputDataSize);
try
{
if (!converter.DecodeTexture2D(buff))
return false;
image = Image.LoadPixelData<Bgra32>(spanBuff, m_Texture2D.m_Width, m_Texture2D.m_Height);
if (flip)
{
image.Mutate(x => x.Flip(FlipMode.Vertical));
}
return true;
}
finally
{
BigArrayPool<byte>.Shared.Return(buff, clearArray: true);
}
return new Texture2DConverter(m_Texture2D).TryConvertToBgra32Image(flip, out image);
}
public static MemoryStream ConvertToStream(this Texture2D m_Texture2D, ImageFormat imageFormat, bool flip)
{
if (!TryConvertToImage(m_Texture2D, flip, out var image))
var converter = new Texture2DConverter(m_Texture2D);
if (!converter.TryConvertToBgra32Image(flip, out var image))
return null;
using (image)