// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Runtime.CompilerServices; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Textures; using osuTK.Graphics.ES30; using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.Graphics.OpenGL.Textures { /// /// A TextureGL which is acting as the backing for an atlas. /// internal class TextureGLAtlas : TextureGLSingle { /// /// The amount of padding around each texture in the atlas. /// private readonly int padding; private readonly RectangleI atlasBounds; private static readonly Rgba32 initialisation_colour = default; public TextureGLAtlas(int width, int height, bool manualMipmaps, All filteringMode = All.Linear, int padding = 0) : base(width, height, manualMipmaps, filteringMode, initialisationColour: initialisation_colour) { this.padding = padding; atlasBounds = new RectangleI(0, 0, Width, Height); } internal override void SetData(ITextureUpload upload, WrapMode wrapModeS, WrapMode wrapModeT, Opacity? uploadOpacity) { // Can only perform padding when the bounds are a sub-part of the texture RectangleI middleBounds = upload.Bounds; if (middleBounds.IsEmpty || middleBounds.Width * middleBounds.Height > upload.Data.Length) { // For a texture atlas, we don't care about opacity, so we avoid // any computations related to it by assuming it to be mixed. base.SetData(upload, wrapModeS, wrapModeT, Opacity.Mixed); return; } int actualPadding = padding / (1 << upload.Level); var data = upload.Data; uploadCornerPadding(data, middleBounds, actualPadding, wrapModeS != WrapMode.None && wrapModeT != WrapMode.None); uploadHorizontalPadding(data, middleBounds, actualPadding, wrapModeS != WrapMode.None); uploadVerticalPadding(data, middleBounds, actualPadding, wrapModeT != WrapMode.None); // Upload the middle part of the texture // For a texture atlas, we don't care about opacity, so we avoid // any computations related to it by assuming it to be mixed. base.SetData(upload, wrapModeS, wrapModeT, Opacity.Mixed); } private void uploadVerticalPadding(ReadOnlySpan upload, RectangleI middleBounds, int actualPadding, bool fillOpaque) { RectangleI[] sideBoundsArray = { new RectangleI(middleBounds.X, middleBounds.Y - actualPadding, middleBounds.Width, actualPadding).Intersect(atlasBounds), // Top new RectangleI(middleBounds.X, middleBounds.Y + middleBounds.Height, middleBounds.Width, actualPadding).Intersect(atlasBounds), // Bottom }; int[] sideIndices = { 0, // Top (middleBounds.Height - 1) * middleBounds.Width, // Bottom }; for (int i = 0; i < 2; ++i) { RectangleI sideBounds = sideBoundsArray[i]; if (!sideBounds.IsEmpty) { bool allTransparent = true; int index = sideIndices[i]; var sideUpload = new MemoryAllocatorTextureUpload(sideBounds.Width, sideBounds.Height) { Bounds = sideBounds }; var data = sideUpload.RawData; for (int y = 0; y < sideBounds.Height; ++y) { for (int x = 0; x < sideBounds.Width; ++x) { Rgba32 pixel = upload[index + x]; allTransparent &= checkEdgeRGB(pixel); transferBorderPixel(ref data[y * sideBounds.Width + x], pixel, fillOpaque); } } // Only upload padding if the border isn't completely transparent. if (!allTransparent) { // For a texture atlas, we don't care about opacity, so we avoid // any computations related to it by assuming it to be mixed. base.SetData(sideUpload, WrapMode.None, WrapMode.None, Opacity.Mixed); } } } } private void uploadHorizontalPadding(ReadOnlySpan upload, RectangleI middleBounds, int actualPadding, bool fillOpaque) { RectangleI[] sideBoundsArray = { new RectangleI(middleBounds.X - actualPadding, middleBounds.Y, actualPadding, middleBounds.Height).Intersect(atlasBounds), // Left new RectangleI(middleBounds.X + middleBounds.Width, middleBounds.Y, actualPadding, middleBounds.Height).Intersect(atlasBounds), // Right }; int[] sideIndices = { 0, // Left middleBounds.Width - 1, // Right }; for (int i = 0; i < 2; ++i) { RectangleI sideBounds = sideBoundsArray[i]; if (!sideBounds.IsEmpty) { bool allTransparent = true; int index = sideIndices[i]; var sideUpload = new MemoryAllocatorTextureUpload(sideBounds.Width, sideBounds.Height) { Bounds = sideBounds }; var data = sideUpload.RawData; int stride = middleBounds.Width; for (int y = 0; y < sideBounds.Height; ++y) { for (int x = 0; x < sideBounds.Width; ++x) { Rgba32 pixel = upload[index + y * stride]; allTransparent &= checkEdgeRGB(pixel); transferBorderPixel(ref data[y * sideBounds.Width + x], pixel, fillOpaque); } } // Only upload padding if the border isn't completely transparent. if (!allTransparent) { // For a texture atlas, we don't care about opacity, so we avoid // any computations related to it by assuming it to be mixed. base.SetData(sideUpload, WrapMode.None, WrapMode.None, Opacity.Mixed); } } } } private void uploadCornerPadding(ReadOnlySpan upload, RectangleI middleBounds, int actualPadding, bool fillOpaque) { RectangleI[] cornerBoundsArray = { new RectangleI(middleBounds.X - actualPadding, middleBounds.Y - actualPadding, actualPadding, actualPadding).Intersect(atlasBounds), // TopLeft new RectangleI(middleBounds.X + middleBounds.Width, middleBounds.Y - actualPadding, actualPadding, actualPadding).Intersect(atlasBounds), // TopRight new RectangleI(middleBounds.X - actualPadding, middleBounds.Y + middleBounds.Height, actualPadding, actualPadding).Intersect(atlasBounds), // BottomLeft new RectangleI(middleBounds.X + middleBounds.Width, middleBounds.Y + middleBounds.Height, actualPadding, actualPadding).Intersect(atlasBounds), // BottomRight }; int[] cornerIndices = { 0, // TopLeft middleBounds.Width - 1, // TopRight (middleBounds.Height - 1) * middleBounds.Width, // BottomLeft (middleBounds.Height - 1) * middleBounds.Width + middleBounds.Width - 1, // BottomRight }; for (int i = 0; i < 4; ++i) { RectangleI cornerBounds = cornerBoundsArray[i]; int nCornerPixels = cornerBounds.Width * cornerBounds.Height; Rgba32 pixel = upload[cornerIndices[i]]; // Only upload if we have a non-zero size and if the colour isn't already transparent white if (nCornerPixels > 0 && !checkEdgeRGB(pixel)) { var cornerUpload = new MemoryAllocatorTextureUpload(cornerBounds.Width, cornerBounds.Height) { Bounds = cornerBounds }; var data = cornerUpload.RawData; for (int j = 0; j < nCornerPixels; ++j) transferBorderPixel(ref data[j], pixel, fillOpaque); // For a texture atlas, we don't care about opacity, so we avoid // any computations related to it by assuming it to be mixed. base.SetData(cornerUpload, WrapMode.None, WrapMode.None, Opacity.Mixed); } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void transferBorderPixel(ref Rgba32 dest, Rgba32 source, bool fillOpaque) { dest.R = source.R; dest.G = source.G; dest.B = source.B; dest.A = fillOpaque ? source.A : (byte)0; } /// /// Check whether the provided upload edge pixel's RGB components match the initialisation colour. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool checkEdgeRGB(Rgba32 cornerPixel) => cornerPixel.R == initialisation_colour.R && cornerPixel.G == initialisation_colour.G && cornerPixel.B == initialisation_colour.B; } }