A game framework written with osu! in mind.
at master 278 lines 11 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 osu.Framework.Graphics.Primitives; 6using osu.Framework.Graphics.Textures; 7using osuTK; 8using osu.Framework.Graphics.Shaders; 9using osu.Framework.Allocation; 10using osu.Framework.Layout; 11using osu.Framework.Graphics.OpenGL.Textures; 12 13namespace osu.Framework.Graphics.Sprites 14{ 15 /// <summary> 16 /// A sprite that displays its texture. 17 /// </summary> 18 public class Sprite : Drawable, ITexturedShaderDrawable 19 { 20 public Sprite() 21 { 22 AddLayout(conservativeScreenSpaceDrawQuadBacking); 23 AddLayout(inflationAmountBacking); 24 } 25 26 [BackgroundDependencyLoader] 27 private void load(ShaderManager shaders) 28 { 29 TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE); 30 RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); 31 } 32 33 public IShader TextureShader { get; protected set; } 34 35 public IShader RoundedTextureShader { get; protected set; } 36 37 private RectangleF textureRectangle = new RectangleF(0, 0, 1, 1); 38 39 /// <summary> 40 /// Sub-rectangle of the sprite in which the texture is positioned. 41 /// Can be either relative coordinates (0 to 1) or absolute coordinates, 42 /// depending on <see cref="TextureRelativeSizeAxes"/>. 43 /// </summary> 44 public RectangleF TextureRectangle 45 { 46 get => textureRectangle; 47 set 48 { 49 if (textureRectangle == value) 50 return; 51 52 textureRectangle = value; 53 Invalidate(Invalidation.DrawNode); 54 } 55 } 56 57 private Axes textureRelativeSizeAxes = Axes.Both; 58 59 /// <summary> 60 /// Whether or not the <see cref="TextureRectangle"/> is in relative coordinates 61 /// (0 to 1) or in absolute coordinates. 62 /// </summary> 63 public Axes TextureRelativeSizeAxes 64 { 65 get => textureRelativeSizeAxes; 66 set 67 { 68 if (textureRelativeSizeAxes == value) 69 return; 70 71 textureRelativeSizeAxes = value; 72 Invalidate(Invalidation.DrawNode); 73 } 74 } 75 76 /// <summary> 77 /// Absolutely sized sub-rectangle in which the texture is positioned in the coordinate space of this <see cref="Sprite"/>. 78 /// Based on <see cref="TextureRectangle"/>. 79 /// </summary> 80 public RectangleF DrawTextureRectangle 81 { 82 get 83 { 84 RectangleF result = TextureRectangle; 85 86 if (TextureRelativeSizeAxes != Axes.None) 87 { 88 var drawSize = DrawSize; 89 90 if ((TextureRelativeSizeAxes & Axes.X) > 0) 91 { 92 result.X *= drawSize.X; 93 result.Width *= drawSize.X; 94 } 95 96 if ((TextureRelativeSizeAxes & Axes.Y) > 0) 97 { 98 result.Y *= drawSize.Y; 99 result.Height *= drawSize.Y; 100 } 101 } 102 103 return result; 104 } 105 } 106 107 /// <summary> 108 /// Maximum value that can be set for <see cref="EdgeSmoothness"/> on either axis. 109 /// </summary> 110 public const int MAX_EDGE_SMOOTHNESS = 3; // See https://github.com/ppy/osu-framework/pull/3511#discussion_r421665156 for relevant discussion. 111 112 private Vector2 edgeSmoothness; 113 114 /// <summary> 115 /// Determines over how many pixels of width the border of the sprite is smoothed 116 /// in X and Y direction respectively. 117 /// IMPORTANT: When masking an edge-smoothed sprite some of the smooth transition 118 /// may be masked away. This should be counteracted by setting the MaskingSmoothness 119 /// of the masking container to a slightly larger value than EdgeSmoothness. 120 /// </summary> 121 public Vector2 EdgeSmoothness 122 { 123 get => edgeSmoothness; 124 set 125 { 126 if (edgeSmoothness == value) 127 return; 128 129 if (value.X > MAX_EDGE_SMOOTHNESS || value.Y > MAX_EDGE_SMOOTHNESS) 130 { 131 throw new InvalidOperationException( 132 $"May not smooth more than {MAX_EDGE_SMOOTHNESS} or will leak neighboring textures in atlas. Tried to smooth by ({value.X}, {value.Y})."); 133 } 134 135 edgeSmoothness = value; 136 137 Invalidate(Invalidation.DrawInfo); 138 } 139 } 140 141 protected override DrawNode CreateDrawNode() => new SpriteDrawNode(this); 142 143 private Texture texture; 144 145 /// <summary> 146 /// The texture that this sprite should draw. Any previous texture will be disposed. 147 /// If this sprite's <see cref="Drawable.Size"/> is <see cref="Vector2.Zero"/> (eg if it has not been set previously), the <see cref="Drawable.Size"/> 148 /// of this sprite will be set to the size of the texture. 149 /// <see cref="Drawable.FillAspectRatio"/> is automatically set to the aspect ratio of the given texture or 1 if the texture is null. 150 /// </summary> 151 public virtual Texture Texture 152 { 153 get => texture; 154 set 155 { 156 if (value == texture) 157 return; 158 159 texture?.Dispose(); 160 texture = value; 161 162 float width; 163 float height; 164 165 if ((TextureRelativeSizeAxes & Axes.X) > 0) 166 width = (texture?.Width ?? 1) / TextureRectangle.Width; 167 else 168 width = TextureRectangle.Width; 169 170 if ((TextureRelativeSizeAxes & Axes.Y) > 0) 171 height = (texture?.Height ?? 1) / TextureRectangle.Height; 172 else 173 height = TextureRectangle.Height; 174 175 FillAspectRatio = width / height; 176 177 Invalidate(Invalidation.DrawNode); 178 conservativeScreenSpaceDrawQuadBacking.Invalidate(); 179 180 if (Size == Vector2.Zero) 181 Size = new Vector2(texture?.DisplayWidth ?? 0, texture?.DisplayHeight ?? 0); 182 } 183 } 184 185 public Vector2 InflationAmount => inflationAmountBacking.IsValid ? inflationAmountBacking.Value : (inflationAmountBacking.Value = computeInflationAmount()); 186 187 private readonly LayoutValue<Vector2> inflationAmountBacking = new LayoutValue<Vector2>(Invalidation.DrawInfo); 188 189 private Vector2 computeInflationAmount() 190 { 191 if (EdgeSmoothness == Vector2.Zero) 192 return Vector2.Zero; 193 194 return DrawInfo.MatrixInverse.ExtractScale().Xy * EdgeSmoothness; 195 } 196 197 protected override Quad ComputeScreenSpaceDrawQuad() 198 { 199 if (EdgeSmoothness == Vector2.Zero) 200 return base.ComputeScreenSpaceDrawQuad(); 201 202 return ToScreenSpace(DrawRectangle.Inflate(InflationAmount)); 203 } 204 205 // Matches the invalidation types of Drawable.screenSpaceDrawQuadBacking 206 private readonly LayoutValue<Quad> conservativeScreenSpaceDrawQuadBacking = new LayoutValue<Quad>(Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit | Invalidation.Presence); 207 208 public Quad ConservativeScreenSpaceDrawQuad => conservativeScreenSpaceDrawQuadBacking.IsValid 209 ? conservativeScreenSpaceDrawQuadBacking 210 : conservativeScreenSpaceDrawQuadBacking.Value = ComputeConservativeScreenSpaceDrawQuad(); 211 212 protected virtual Quad ComputeConservativeScreenSpaceDrawQuad() 213 { 214 if (Texture == null || Texture is TextureWhitePixel) 215 { 216 if (EdgeSmoothness == Vector2.Zero) 217 return ScreenSpaceDrawQuad; 218 219 return ToScreenSpace(DrawRectangle); 220 } 221 222 // ====================================================================================================================== 223 // The following commented-out code shrinks the texture by the maximum mip level and is thereby conservative. 224 // Alternatively, which is the un-commented code, one can assume a certain worst-case LOD bias (in this case -1) and shrink 225 // the rectangle in screen space by 0.5 * 2*(LOD_bias) pixels. 226 // ====================================================================================================================== 227 228 // RectangleF texRect = RelativeDrawTextureRectangle; 229 // Vector2 shrinkageAmount = Vector2.Divide(texRect.Size * (1 << TextureGLSingle.MAX_MIPMAP_LEVELS) / 2, Texture.Size); 230 // shrinkageAmount = Vector2.ComponentMin(shrinkageAmount, texRect.Size / 2); 231 // texRect = texRect.Inflate(-shrinkageAmount); 232 // 233 // return ToScreenSpace(texRect * DrawSize); 234 235 Vector3 scale = DrawInfo.MatrixInverse.ExtractScale(); 236 RectangleF rectangle = DrawTextureRectangle; 237 238 // If the texture wraps or is clamped to its edge in some direction, then the entire 239 // sprite is opaque in that direction, hence the texture's opaque rectangle can be 240 // expanded to the full draw dimension of the sprite. 241 if (Texture.WrapModeS == WrapMode.ClampToEdge || Texture.WrapModeS == WrapMode.Repeat) 242 { 243 rectangle.X = 0; 244 rectangle.Width = DrawWidth; 245 } 246 247 if (Texture.WrapModeT == WrapMode.ClampToEdge || Texture.WrapModeT == WrapMode.Repeat) 248 { 249 rectangle.Y = 0; 250 rectangle.Height = DrawHeight; 251 } 252 253 Vector2 shrinkageAmount = Vector2.ComponentMin(scale.Xy, rectangle.Size / 2); 254 255 return ToScreenSpace(rectangle.Inflate(-shrinkageAmount)); 256 } 257 258 public override string ToString() 259 { 260 string result = base.ToString(); 261 if (!string.IsNullOrEmpty(texture?.AssetName)) 262 result += $" tex: {texture.AssetName}"; 263 return result; 264 } 265 266 #region Disposal 267 268 protected override void Dispose(bool isDisposing) 269 { 270 texture?.Dispose(); 271 texture = null; 272 273 base.Dispose(isDisposing); 274 } 275 276 #endregion 277 } 278}