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 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}