A game framework written with osu! in mind.
at master 219 lines 9.8 kB view raw
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2// See the LICENCE file in the repository root for full licence text. 3 4using System; 5using System.Runtime.CompilerServices; 6using osu.Framework.Graphics.Primitives; 7using osu.Framework.Graphics.Textures; 8using osuTK.Graphics.ES30; 9using SixLabors.ImageSharp.PixelFormats; 10 11namespace osu.Framework.Graphics.OpenGL.Textures 12{ 13 /// <summary> 14 /// A TextureGL which is acting as the backing for an atlas. 15 /// </summary> 16 internal class TextureGLAtlas : TextureGLSingle 17 { 18 /// <summary> 19 /// The amount of padding around each texture in the atlas. 20 /// </summary> 21 private readonly int padding; 22 23 private readonly RectangleI atlasBounds; 24 25 private static readonly Rgba32 initialisation_colour = default; 26 27 public TextureGLAtlas(int width, int height, bool manualMipmaps, All filteringMode = All.Linear, int padding = 0) 28 : base(width, height, manualMipmaps, filteringMode, initialisationColour: initialisation_colour) 29 { 30 this.padding = padding; 31 32 atlasBounds = new RectangleI(0, 0, Width, Height); 33 } 34 35 internal override void SetData(ITextureUpload upload, WrapMode wrapModeS, WrapMode wrapModeT, Opacity? uploadOpacity) 36 { 37 // Can only perform padding when the bounds are a sub-part of the texture 38 RectangleI middleBounds = upload.Bounds; 39 40 if (middleBounds.IsEmpty || middleBounds.Width * middleBounds.Height > upload.Data.Length) 41 { 42 // For a texture atlas, we don't care about opacity, so we avoid 43 // any computations related to it by assuming it to be mixed. 44 base.SetData(upload, wrapModeS, wrapModeT, Opacity.Mixed); 45 return; 46 } 47 48 int actualPadding = padding / (1 << upload.Level); 49 50 var data = upload.Data; 51 52 uploadCornerPadding(data, middleBounds, actualPadding, wrapModeS != WrapMode.None && wrapModeT != WrapMode.None); 53 uploadHorizontalPadding(data, middleBounds, actualPadding, wrapModeS != WrapMode.None); 54 uploadVerticalPadding(data, middleBounds, actualPadding, wrapModeT != WrapMode.None); 55 56 // Upload the middle part of the texture 57 // For a texture atlas, we don't care about opacity, so we avoid 58 // any computations related to it by assuming it to be mixed. 59 base.SetData(upload, wrapModeS, wrapModeT, Opacity.Mixed); 60 } 61 62 private void uploadVerticalPadding(ReadOnlySpan<Rgba32> upload, RectangleI middleBounds, int actualPadding, bool fillOpaque) 63 { 64 RectangleI[] sideBoundsArray = 65 { 66 new RectangleI(middleBounds.X, middleBounds.Y - actualPadding, middleBounds.Width, actualPadding).Intersect(atlasBounds), // Top 67 new RectangleI(middleBounds.X, middleBounds.Y + middleBounds.Height, middleBounds.Width, actualPadding).Intersect(atlasBounds), // Bottom 68 }; 69 70 int[] sideIndices = 71 { 72 0, // Top 73 (middleBounds.Height - 1) * middleBounds.Width, // Bottom 74 }; 75 76 for (int i = 0; i < 2; ++i) 77 { 78 RectangleI sideBounds = sideBoundsArray[i]; 79 80 if (!sideBounds.IsEmpty) 81 { 82 bool allTransparent = true; 83 int index = sideIndices[i]; 84 85 var sideUpload = new MemoryAllocatorTextureUpload(sideBounds.Width, sideBounds.Height) { Bounds = sideBounds }; 86 var data = sideUpload.RawData; 87 88 for (int y = 0; y < sideBounds.Height; ++y) 89 { 90 for (int x = 0; x < sideBounds.Width; ++x) 91 { 92 Rgba32 pixel = upload[index + x]; 93 allTransparent &= checkEdgeRGB(pixel); 94 95 transferBorderPixel(ref data[y * sideBounds.Width + x], pixel, fillOpaque); 96 } 97 } 98 99 // Only upload padding if the border isn't completely transparent. 100 if (!allTransparent) 101 { 102 // For a texture atlas, we don't care about opacity, so we avoid 103 // any computations related to it by assuming it to be mixed. 104 base.SetData(sideUpload, WrapMode.None, WrapMode.None, Opacity.Mixed); 105 } 106 } 107 } 108 } 109 110 private void uploadHorizontalPadding(ReadOnlySpan<Rgba32> upload, RectangleI middleBounds, int actualPadding, bool fillOpaque) 111 { 112 RectangleI[] sideBoundsArray = 113 { 114 new RectangleI(middleBounds.X - actualPadding, middleBounds.Y, actualPadding, middleBounds.Height).Intersect(atlasBounds), // Left 115 new RectangleI(middleBounds.X + middleBounds.Width, middleBounds.Y, actualPadding, middleBounds.Height).Intersect(atlasBounds), // Right 116 }; 117 118 int[] sideIndices = 119 { 120 0, // Left 121 middleBounds.Width - 1, // Right 122 }; 123 124 for (int i = 0; i < 2; ++i) 125 { 126 RectangleI sideBounds = sideBoundsArray[i]; 127 128 if (!sideBounds.IsEmpty) 129 { 130 bool allTransparent = true; 131 int index = sideIndices[i]; 132 133 var sideUpload = new MemoryAllocatorTextureUpload(sideBounds.Width, sideBounds.Height) { Bounds = sideBounds }; 134 var data = sideUpload.RawData; 135 136 int stride = middleBounds.Width; 137 138 for (int y = 0; y < sideBounds.Height; ++y) 139 { 140 for (int x = 0; x < sideBounds.Width; ++x) 141 { 142 Rgba32 pixel = upload[index + y * stride]; 143 144 allTransparent &= checkEdgeRGB(pixel); 145 146 transferBorderPixel(ref data[y * sideBounds.Width + x], pixel, fillOpaque); 147 } 148 } 149 150 // Only upload padding if the border isn't completely transparent. 151 if (!allTransparent) 152 { 153 // For a texture atlas, we don't care about opacity, so we avoid 154 // any computations related to it by assuming it to be mixed. 155 base.SetData(sideUpload, WrapMode.None, WrapMode.None, Opacity.Mixed); 156 } 157 } 158 } 159 } 160 161 private void uploadCornerPadding(ReadOnlySpan<Rgba32> upload, RectangleI middleBounds, int actualPadding, bool fillOpaque) 162 { 163 RectangleI[] cornerBoundsArray = 164 { 165 new RectangleI(middleBounds.X - actualPadding, middleBounds.Y - actualPadding, actualPadding, actualPadding).Intersect(atlasBounds), // TopLeft 166 new RectangleI(middleBounds.X + middleBounds.Width, middleBounds.Y - actualPadding, actualPadding, actualPadding).Intersect(atlasBounds), // TopRight 167 new RectangleI(middleBounds.X - actualPadding, middleBounds.Y + middleBounds.Height, actualPadding, actualPadding).Intersect(atlasBounds), // BottomLeft 168 new RectangleI(middleBounds.X + middleBounds.Width, middleBounds.Y + middleBounds.Height, actualPadding, actualPadding).Intersect(atlasBounds), // BottomRight 169 }; 170 171 int[] cornerIndices = 172 { 173 0, // TopLeft 174 middleBounds.Width - 1, // TopRight 175 (middleBounds.Height - 1) * middleBounds.Width, // BottomLeft 176 (middleBounds.Height - 1) * middleBounds.Width + middleBounds.Width - 1, // BottomRight 177 }; 178 179 for (int i = 0; i < 4; ++i) 180 { 181 RectangleI cornerBounds = cornerBoundsArray[i]; 182 int nCornerPixels = cornerBounds.Width * cornerBounds.Height; 183 Rgba32 pixel = upload[cornerIndices[i]]; 184 185 // Only upload if we have a non-zero size and if the colour isn't already transparent white 186 if (nCornerPixels > 0 && !checkEdgeRGB(pixel)) 187 { 188 var cornerUpload = new MemoryAllocatorTextureUpload(cornerBounds.Width, cornerBounds.Height) { Bounds = cornerBounds }; 189 var data = cornerUpload.RawData; 190 191 for (int j = 0; j < nCornerPixels; ++j) 192 transferBorderPixel(ref data[j], pixel, fillOpaque); 193 194 // For a texture atlas, we don't care about opacity, so we avoid 195 // any computations related to it by assuming it to be mixed. 196 base.SetData(cornerUpload, WrapMode.None, WrapMode.None, Opacity.Mixed); 197 } 198 } 199 } 200 201 [MethodImpl(MethodImplOptions.AggressiveInlining)] 202 private void transferBorderPixel(ref Rgba32 dest, Rgba32 source, bool fillOpaque) 203 { 204 dest.R = source.R; 205 dest.G = source.G; 206 dest.B = source.B; 207 dest.A = fillOpaque ? source.A : (byte)0; 208 } 209 210 /// <summary> 211 /// Check whether the provided upload edge pixel's RGB components match the initialisation colour. 212 /// </summary> 213 [MethodImpl(MethodImplOptions.AggressiveInlining)] 214 private bool checkEdgeRGB(Rgba32 cornerPixel) 215 => cornerPixel.R == initialisation_colour.R 216 && cornerPixel.G == initialisation_colour.G 217 && cornerPixel.B == initialisation_colour.B; 218 } 219}