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.Collections.Generic;
6using System.Numerics;
7using osu.Framework.Graphics.OpenGL.Textures;
8using osuTK.Graphics.ES30;
9using osu.Framework.Graphics.Sprites;
10using osu.Framework.Graphics.Primitives;
11using osu.Framework.Logging;
12using SixLabors.ImageSharp;
13using SixLabors.ImageSharp.PixelFormats;
14
15namespace osu.Framework.Graphics.Textures
16{
17 public class TextureAtlas
18 {
19 // We are adding an extra padding on top of the padding required by
20 // mipmap blending in order to support smooth edges without antialiasing which requires
21 // inflating texture rectangles.
22 internal const int PADDING = (1 << TextureGLSingle.MAX_MIPMAP_LEVELS) * Sprite.MAX_EDGE_SMOOTHNESS;
23 internal const int WHITE_PIXEL_SIZE = 1;
24
25 private readonly List<RectangleI> subTextureBounds = new List<RectangleI>();
26 internal TextureGLSingle AtlasTexture;
27
28 private readonly int atlasWidth;
29 private readonly int atlasHeight;
30
31 private int maxFittableWidth => atlasWidth - PADDING * 2;
32 private int maxFittableHeight => atlasHeight - PADDING * 2;
33
34 private Vector2I currentPosition;
35
36 internal TextureWhitePixel WhitePixel
37 {
38 get
39 {
40 if (AtlasTexture == null)
41 Reset();
42
43 return new TextureWhitePixel(new TextureGLAtlasWhite(AtlasTexture));
44 }
45 }
46
47 private readonly bool manualMipmaps;
48 private readonly All filteringMode;
49 private readonly object textureRetrievalLock = new object();
50
51 public TextureAtlas(int width, int height, bool manualMipmaps = false, All filteringMode = All.Linear)
52 {
53 atlasWidth = width;
54 atlasHeight = height;
55 this.manualMipmaps = manualMipmaps;
56 this.filteringMode = filteringMode;
57 }
58
59 private int exceedCount;
60
61 public void Reset()
62 {
63 subTextureBounds.Clear();
64 currentPosition = Vector2I.Zero;
65
66 // We pass PADDING/2 as opposed to PADDING such that the padded region of each individual texture
67 // occupies half of the padded space.
68 AtlasTexture = new TextureGLAtlas(atlasWidth, atlasHeight, manualMipmaps, filteringMode, PADDING / 2);
69
70 RectangleI bounds = new RectangleI(0, 0, WHITE_PIXEL_SIZE, WHITE_PIXEL_SIZE);
71 subTextureBounds.Add(bounds);
72
73 using (var whiteTex = new TextureGLSub(bounds, AtlasTexture, WrapMode.Repeat, WrapMode.Repeat))
74 // Generate white padding as if WhitePixel was wrapped, even though it isn't
75 whiteTex.SetData(new TextureUpload(new Image<Rgba32>(SixLabors.ImageSharp.Configuration.Default, whiteTex.Width, whiteTex.Height, new Rgba32(Vector4.One))));
76
77 currentPosition = new Vector2I(PADDING + WHITE_PIXEL_SIZE, PADDING);
78 }
79
80 /// <summary>
81 /// Add (allocate) a new texture in the atlas.
82 /// </summary>
83 /// <param name="width">The width of the requested texture.</param>
84 /// <param name="height">The height of the requested texture.</param>
85 /// <param name="wrapModeS">The horizontal wrap mode of the texture.</param>
86 /// <param name="wrapModeT">The vertical wrap mode of the texture.</param>
87 /// <returns>A texture, or null if the requested size exceeds the atlas' bounds.</returns>
88 internal TextureGL Add(int width, int height, WrapMode wrapModeS = WrapMode.None, WrapMode wrapModeT = WrapMode.None)
89 {
90 if (!canFitEmptyTextureAtlas(width, height)) return null;
91
92 lock (textureRetrievalLock)
93 {
94 Vector2I position = findPosition(width, height);
95 RectangleI bounds = new RectangleI(position.X, position.Y, width, height);
96 subTextureBounds.Add(bounds);
97
98 return new TextureGLSub(bounds, AtlasTexture, wrapModeS, wrapModeT);
99 }
100 }
101
102 /// <summary>
103 /// Whether or not a texture of the given width and height could be placed into a completely empty texture atlas
104 /// </summary>
105 /// <param name="width">The width of the texture.</param>
106 /// <param name="height">The height of the texture.</param>
107 /// <returns>True if the texture could fit an empty texture atlas, false if it could not</returns>
108 private bool canFitEmptyTextureAtlas(int width, int height)
109 {
110 // exceeds bounds in one direction
111 if (width > maxFittableWidth || height > maxFittableHeight)
112 return false;
113
114 // exceeds bounds in both directions (in this one, we have to account for the white pixel)
115 if (width + WHITE_PIXEL_SIZE > maxFittableWidth && height + WHITE_PIXEL_SIZE > maxFittableHeight)
116 return false;
117
118 return true;
119 }
120
121 /// <summary>
122 /// Locates a position in the current texture atlas for a new texture of the given size, or
123 /// creates a new texture atlas if there is not enough space in the current one.
124 /// </summary>
125 /// <param name="width">The width of the requested texture.</param>
126 /// <param name="height">The height of the requested texture.</param>
127 /// <returns>The position within the texture atlas to place the new texture.</returns>
128 private Vector2I findPosition(int width, int height)
129 {
130 if (AtlasTexture == null)
131 {
132 Logger.Log($"TextureAtlas initialised ({atlasWidth}x{atlasHeight})", LoggingTarget.Performance);
133 Reset();
134 }
135
136 if (currentPosition.Y + height + PADDING > atlasHeight)
137 {
138 Logger.Log($"TextureAtlas size exceeded {++exceedCount} time(s); generating new texture ({atlasWidth}x{atlasHeight})", LoggingTarget.Performance);
139 Reset();
140 }
141
142 if (currentPosition.X + width + PADDING > atlasWidth)
143 {
144 int maxY = 0;
145
146 foreach (RectangleI bounds in subTextureBounds)
147 maxY = Math.Max(maxY, bounds.Bottom + PADDING);
148
149 subTextureBounds.Clear();
150 currentPosition = new Vector2I(PADDING, maxY);
151
152 return findPosition(width, height);
153 }
154
155 var result = currentPosition;
156 currentPosition.X += width + PADDING;
157
158 return result;
159 }
160 }
161}