A game framework written with osu! in mind.
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}