using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing; 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; using System.Numerics; namespace AssetStudio { public enum SpriteMaskMode { Off, On, Preview, MaskOnly, } public class SpriteConverter { 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 texture2D)) { 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 if (m_Sprite.m_RD.texture.TryGet(out var texture2D)) { 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; } if (textureRect == null) return; resultWidth = cutImageWithCanvas ? (int)sprite.m_Rect.width : (int)textureRect.width; resultHeight = cutImageWithCanvas ? (int)sprite.m_Rect.height : (int)textureRect.height; } public bool TryConvertToImage(out Image spriteImage, SpriteMaskMode maskMode) { spriteImage = null; if (m_Texture2D == null) return false; if (m_AlphaTexture2D != null) { var alphaTex = CutSprite(m_AlphaTexture2D); if (maskMode != SpriteMaskMode.MaskOnly) { spriteImage = CutSprite(m_Texture2D); } switch (maskMode) { 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 CutSprite(Texture2D texture2D) { var croppedImage = CutImage(texture2D); if (croppedImage == null) return null; if (!cutImageWithCanvas) return croppedImage; using (croppedImage) { var canvas = new Image(resultWidth, resultHeight); canvas.Mutate(ctx => ctx.DrawImage( croppedImage, new Point((int)textureRectOffset.X, (int)(resultHeight - textureRectOffset.Y - croppedImage.Height)), 1f) ); return canvas; } } private Image CutImage(Texture2D texture2D) { if (!texture2D.TryConvertToImage(false, out var textureImage)) return null; if (downscaleMultiplier > 0f && downscaleMultiplier != 1f) { var width = (int)(texture2D.m_Width / downscaleMultiplier); var height = (int)(texture2D.m_Height / downscaleMultiplier); textureImage.Mutate(x => x.Resize(width, height)); } 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); rectBottom = Math.Min(rectBottom, textureImage.Height); var rect = new Rectangle(rectX, rectY, rectRight - rectX, rectBottom - rectY); var spriteImage = textureImage.Clone(x => x.Crop(rect)); textureImage.Dispose(); if (settingsRaw.packed == 1) { //RotateAndFlip switch (settingsRaw.packingRotation) { case SpritePackingRotation.FlipHorizontal: spriteImage.Mutate(x => x.Flip(FlipMode.Horizontal)); break; case SpritePackingRotation.FlipVertical: spriteImage.Mutate(x => x.Flip(FlipMode.Vertical)); break; case SpritePackingRotation.Rotate180: spriteImage.Mutate(x => x.Rotate(180)); break; case SpritePackingRotation.Rotate90: spriteImage.Mutate(x => x.Rotate(270)); break; } } //Tight if (settingsRaw.packingMode == SpritePackingMode.Tight) { try { 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) { pathBuilder.AddLines(p); pathBuilder.CloseFigure(); } var path = pathBuilder.Build(); var options = new DrawingOptions { GraphicsOptions = new GraphicsOptions { Antialias = false, AlphaCompositionMode = PixelAlphaCompositionMode.DestOut } }; if (triangles.Length < 1024) { var rectP = new RectangularPolygon(0, 0, rect.Width, rect.Height); try { spriteImage.Mutate(x => x.Fill(options, SixLabors.ImageSharp.Color.Red, rectP.Clip(path.Clip()))); spriteImage.Mutate(x => x.Flip(FlipMode.Vertical)); return spriteImage; } catch (ArgumentOutOfRangeException) { // ignored } } using (var mask = new Image(rect.Width, rect.Height, SixLabors.ImageSharp.Color.Black)) { mask.Mutate(x => x.Fill(options, SixLabors.ImageSharp.Color.Red, path)); var brush = new ImageBrush(mask); spriteImage.Mutate(x => x.Fill(options, brush)); spriteImage.Mutate(x => x.Flip(FlipMode.Vertical)); return spriteImage; } } catch (Exception e) { Logger.Warning($"{sprite.m_Name} Unable to render the packed sprite correctly.\n{e}"); } } //Rectangle spriteImage.Mutate(x => x.Flip(FlipMode.Vertical)); return spriteImage; } private static Vector2[][] GetTriangles(SpriteRenderData m_RD) { if (m_RD.vertices != null) //5.6 down { var vertices = m_RD.vertices.Select(x => (Vector2)x.pos).ToArray(); var triangleCount = m_RD.indices.Length / 3; var triangles = new Vector2[triangleCount][]; for (int i = 0; i < triangleCount; i++) { var first = m_RD.indices[i * 3]; var second = m_RD.indices[i * 3 + 1]; var third = m_RD.indices[i * 3 + 2]; var triangle = new[] { vertices[first], vertices[second], vertices[third] }; triangles[i] = triangle; } return triangles; } else //5.6 and up { var triangles = new List(); var m_VertexData = m_RD.m_VertexData; var m_Channel = m_VertexData.m_Channels[0]; //kShaderChannelVertex var m_Stream = m_VertexData.m_Streams[m_Channel.stream]; using (var vertexReader = new BinaryReader(new MemoryStream(m_VertexData.m_DataSize))) { using (var indexReader = new BinaryReader(new MemoryStream(m_RD.m_IndexBuffer))) { foreach (var subMesh in m_RD.m_SubMeshes) { vertexReader.BaseStream.Position = m_Stream.offset + subMesh.firstVertex * m_Stream.stride + m_Channel.offset; var vertices = new Vector2[subMesh.vertexCount]; for (int v = 0; v < subMesh.vertexCount; v++) { vertices[v] = vertexReader.ReadVector3(); vertexReader.BaseStream.Position += m_Stream.stride - 12; } indexReader.BaseStream.Position = subMesh.firstByte; var triangleCount = subMesh.indexCount / 3u; for (int i = 0; i < triangleCount; i++) { var first = indexReader.ReadUInt16() - subMesh.firstVertex; var second = indexReader.ReadUInt16() - subMesh.firstVertex; var third = indexReader.ReadUInt16() - subMesh.firstVertex; var triangle = new[] { vertices[first], vertices[second], vertices[third] }; triangles.Add(triangle); } } } } 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 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); } } }