This is a fun one.
My current project is all about rectangles. There’s only two things drawn in it that aren’t simple rectangles: the font and an arrow texture. The rectangles are drawn using a simple code-generated blank texture that is stretched to fit whatever size of rectangle is needed.
I use a lot of RectangleF
instances, as well, where RectangleF
is a home-grown floating point implementation of Rectangle
. Being able to draw those rectangles with sub-pixel precision would be nice.
However, subpixel rendering doesn’t work unless the blank texture has empty edges. And even when there are empty edges, if the texture is drawn stretched (if the rectangle doesn’t exactly match the size of the texture) the effect doesn’t work right.
Here’s a code sample of what I’m talking about:
public class Game1 : Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Texture2D blankTextureFull;
Texture2D blankTextureEmptyEdges;
List<RectangleF> boxes = new();
Color boxColor = new(255, 255, 255);
int emptyEdgeBoxCopyOffset = 250;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
IsFixedTimeStep = false;
graphics.SynchronizeWithVerticalRetrace = true;
}
protected override void LoadContent()
{
const int textureWidth = 50;
const int textureHeight = 50;
spriteBatch = new SpriteBatch(GraphicsDevice);
blankTextureFull = new Texture2D(GraphicsDevice, textureWidth, textureHeight);
blankTextureEmptyEdges = new Texture2D(GraphicsDevice, textureWidth, textureHeight);
Color[] colors = new Color[textureWidth * textureHeight];
for (int i = 0; i < colors.Length; i++)
colors[i] = new Color(255, 255, 255);
blankTextureFull.SetData(colors);
for (int x = 0; x < textureWidth; x++)
{
// Set top and bottom rows to transparent
int index = Convert2DIndexInto1DIndex(x, 0, textureWidth);
colors[index] = new Color(0, 0, 0, 0);
index = Convert2DIndexInto1DIndex(x, textureHeight - 1, textureWidth);
colors[index] = new Color(0, 0, 0, 0);
}
for (int y = 0; y < textureHeight; y++)
{
// Set left and right columns to transparent
int index = Convert2DIndexInto1DIndex(0, y, textureWidth);
colors[index] = new Color(0, 0, 0, 0);
index = Convert2DIndexInto1DIndex(textureWidth - 1, y, textureWidth);
colors[index] = new Color(0, 0, 0, 0);
}
blankTextureEmptyEdges.SetData(colors);
boxes.Add(new RectangleF(0, 0, 50, 50));
boxes.Add(new RectangleF(100, 0, 50, 200));
boxes.Add(new RectangleF(200, 0, 200, 200));
boxes.Add(new RectangleF(450, 0, 25, 25));
base.LoadContent();
int Convert2DIndexInto1DIndex(int x, int y, int rowLength) => y * rowLength + x;
}
protected override void Update(GameTime gameTime)
{
KeyboardState keys = Keyboard.GetState();
if (keys.IsKeyDown(Keys.Escape))
Exit();
float moveSpeed = 1f;
float moveAmount = (float)(moveSpeed * gameTime.ElapsedGameTime.TotalSeconds);
if (keys.IsKeyDown(Keys.Up))
MoveBoxes(0, -moveAmount);
if (keys.IsKeyDown(Keys.Down))
MoveBoxes(0, moveAmount);
if (keys.IsKeyDown(Keys.Left))
MoveBoxes(-moveAmount, 0);
if (keys.IsKeyDown(Keys.Right))
MoveBoxes(moveAmount, 0);
base.Update(gameTime);
void MoveBoxes(float x, float y)
{
for (int i = 0; i < boxes.Count; i++)
{
RectangleF box = boxes[i];
box.X += x;
box.Y += y;
boxes[i] = box;
}
}
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
for (int i = 0; i < boxes.Count; i++)
{
RectangleF box = boxes[i];
DrawRectangle(spriteBatch, blankTextureFull, box, boxColor);
box.Y += emptyEdgeBoxCopyOffset;
DrawRectangle(spriteBatch, blankTextureEmptyEdges, box, boxColor);
}
spriteBatch.End();
base.Draw(gameTime);
}
public static void DrawRectangle(SpriteBatch batch, Texture2D texture, RectangleF rectangle, Color color)
{
if (rectangle.IsEmpty)
return;
Vector2 scale = rectangle.Size / texture.Bounds.Size.ToVector2();
batch.Draw(texture, rectangle.Location, null, color, 0f, Vector2.Zero, scale, SpriteEffects.None, 1f);
}
}
// RectangleF struct, with elements unnecessary to the example removed
public struct RectangleF
{
public float X;
public float Y;
public float Width;
public float Height;
public Vector2 Location
{
get => new(X, Y);
set
{
X = value.X;
Y = value.Y;
}
}
public Vector2 Size
{
get => new(Width, Height);
set
{
Width = value.X;
Height = value.Y;
}
}
public bool IsEmpty => Width == 0 || Height == 0;
public RectangleF(float x, float y, float width, float height)
{
X = x;
Y = y;
Width = width;
Height = height;
}
}
The sample can be run without any content files. Use the keyboard arrow keys to very slowly move the rectangles around.
In this sample, only the bottom left rectangle does what I want. The top row is drawn with a blank texture with no transparent margin, and it behaves just like drawing integer rectangles. The bottom row is drawn with a texture with a transparent margin, but any of the rectangles that are stretched or squashed don’t work right, either having blurry sides or, in the case of the tiny rectangle, even weirder behavior.
My project has a large number of arbitrarily-sized rectangles, so creating a unique blank texture for each one isn’t going to work. Is there a way to get any rectangle of arbitrary size to be drawn like the lower-left one in my sample, without having to create multiple blank textures?