A game framework written with osu! in mind.
at master 161 lines 6.7 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.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}