Rework multi-resolution sprite handling:

- Sprite.Bounds now refers to rectangles in the source image.
  Use this when copying pixels, etc.
- Sprite.Size now refers to sizes in effective pixel coordinates.
  Use this when rendering.
- Sheet.DPIScale has been removed.
- "Density" term is introduced to refer to the number of artwork
  pixels per effective pixel.
This commit is contained in:
Paul Chote
2020-02-11 21:29:15 +00:00
committed by abcdefg30
parent c0ece00c4b
commit de4a7cecf0
15 changed files with 106 additions and 91 deletions

View File

@@ -54,10 +54,10 @@ namespace OpenRA.Graphics
public static IReadOnlyDictionary<string, Collection> Collections { get; private set; }
static Dictionary<string, Collection> collections;
static Dictionary<string, Sheet> cachedSheets;
static Dictionary<string, Pair<Sheet, int>> cachedSheets;
static Dictionary<string, Dictionary<string, Sprite>> cachedSprites;
static Dictionary<string, Sprite[]> cachedPanelSprites;
static Dictionary<Collection, Sheet> cachedCollectionSheets;
static Dictionary<Collection, Pair<Sheet, int>> cachedCollectionSheets;
static IReadOnlyFileSystem fileSystem;
static float dpiScale = 1;
@@ -72,10 +72,10 @@ namespace OpenRA.Graphics
fileSystem = modData.DefaultFileSystem;
collections = new Dictionary<string, Collection>();
cachedSheets = new Dictionary<string, Sheet>();
cachedSheets = new Dictionary<string, Pair<Sheet, int>>();
cachedSprites = new Dictionary<string, Dictionary<string, Sprite>>();
cachedPanelSprites = new Dictionary<string, Sprite[]>();
cachedCollectionSheets = new Dictionary<Collection, Sheet>();
cachedCollectionSheets = new Dictionary<Collection, Pair<Sheet, int>>();
Collections = new ReadOnlyDictionary<string, Collection>(collections);
@@ -91,7 +91,7 @@ namespace OpenRA.Graphics
{
if (cachedSheets != null)
foreach (var sheet in cachedSheets.Values)
sheet.Dispose();
sheet.First.Dispose();
collections = null;
cachedSheets = null;
@@ -108,47 +108,43 @@ namespace OpenRA.Graphics
collections.Add(name, FieldLoader.Load<Collection>(yaml));
}
static Sheet SheetForCollection(Collection c)
static Pair<Sheet, int> SheetForCollection(Collection c)
{
Sheet sheet;
Pair<Sheet, int> sheetDensity;
// Outer cache avoids recalculating image names
if (!cachedCollectionSheets.TryGetValue(c, out sheet))
if (!cachedCollectionSheets.TryGetValue(c, out sheetDensity))
{
string sheetImage;
float sheetScale;
var image = c.Image;
var density = 1;
if (dpiScale > 2 && !string.IsNullOrEmpty(c.Image3x))
{
sheetImage = c.Image3x;
sheetScale = 3;
image = c.Image3x;
density = 3;
}
else if (dpiScale > 1 && !string.IsNullOrEmpty(c.Image2x))
{
sheetImage = c.Image2x;
sheetScale = 2;
}
else
{
sheetImage = c.Image;
sheetScale = 1;
image = c.Image2x;
density = 2;
}
// Inner cache makes sure we share sheets between collections
if (!cachedSheets.TryGetValue(sheetImage, out sheet))
if (!cachedSheets.TryGetValue(image, out sheetDensity))
{
using (var stream = fileSystem.Open(sheetImage))
Sheet sheet;
using (var stream = fileSystem.Open(image))
sheet = new Sheet(SheetType.BGRA, stream);
sheet.GetTexture().ScaleFilter = TextureScaleFilter.Linear;
sheet.DPIScale = sheetScale;
cachedSheets.Add(sheetImage, sheet);
sheetDensity = Pair.New(sheet, density);
cachedSheets.Add(image, sheetDensity);
}
cachedCollectionSheets.Add(c, sheet);
cachedCollectionSheets.Add(c, sheetDensity);
}
return sheet;
return sheetDensity;
}
public static Sprite GetImage(string collectionName, string imageName)
@@ -174,14 +170,14 @@ namespace OpenRA.Graphics
return null;
// Cache the sprite
var sheet = SheetForCollection(collection);
var sheetDensity = SheetForCollection(collection);
if (cachedCollection == null)
{
cachedCollection = new Dictionary<string, Sprite>();
cachedSprites.Add(collectionName, cachedCollection);
}
var image = new Sprite(sheet, mi, TextureChannel.RGBA);
var image = new Sprite(sheetDensity.First, sheetDensity.Second * mi, TextureChannel.RGBA, 1f / sheetDensity.Second);
cachedCollection.Add(imageName, image);
return image;
@@ -214,7 +210,7 @@ namespace OpenRA.Graphics
}
// Cache the sprites
var sheet = SheetForCollection(collection);
var sheetDensity = SheetForCollection(collection);
var pr = collection.PanelRegion;
var ps = collection.PanelSides;
@@ -231,7 +227,7 @@ namespace OpenRA.Graphics
Pair.New(PanelSides.Bottom | PanelSides.Right, new Rectangle(pr[0] + pr[2] + pr[4], pr[1] + pr[3] + pr[5], pr[6], pr[7]))
};
sprites = sides.Select(x => ps.HasSide(x.First) ? new Sprite(sheet, x.Second, TextureChannel.RGBA) : null)
sprites = sides.Select(x => ps.HasSide(x.First) ? new Sprite(sheetDensity.First, sheetDensity.Second * x.Second, TextureChannel.RGBA, 1f / sheetDensity.Second) : null)
.ToArray();
}
else

View File

@@ -25,7 +25,6 @@ namespace OpenRA.Graphics
public readonly Size Size;
public readonly SheetType Type;
public float DPIScale = 1f;
public byte[] GetData()
{

View File

@@ -88,9 +88,9 @@ namespace OpenRA.Graphics
return rect;
}
public Sprite Add(Png src)
public Sprite Add(Png src, float scale = 1f)
{
var rect = Allocate(new Size(src.Width, src.Height));
var rect = Allocate(new Size(src.Width, src.Height), scale);
Util.FastCopyIntoSprite(rect, src);
current.CommitBufferedData();
return rect;
@@ -114,8 +114,8 @@ namespace OpenRA.Graphics
return (TextureChannel)nextChannel;
}
public Sprite Allocate(Size imageSize) { return Allocate(imageSize, 0, float3.Zero); }
public Sprite Allocate(Size imageSize, float zRamp, float3 spriteOffset)
public Sprite Allocate(Size imageSize, float scale = 1f) { return Allocate(imageSize, 0, float3.Zero, scale); }
public Sprite Allocate(Size imageSize, float zRamp, float3 spriteOffset, float scale = 1f)
{
if (imageSize.Width + p.X + margin > current.Size.Width)
{
@@ -143,7 +143,7 @@ namespace OpenRA.Graphics
p = int2.Zero;
}
var rect = new Sprite(current, new Rectangle(p.X + margin, p.Y + margin, imageSize.Width, imageSize.Height), zRamp, spriteOffset, channel, BlendMode.Alpha);
var rect = new Sprite(current, new Rectangle(p.X + margin, p.Y + margin, imageSize.Width, imageSize.Height), zRamp, spriteOffset, channel, BlendMode.Alpha, scale);
p += new int2(imageSize.Width + margin, 0);
return rect;

View File

@@ -26,25 +26,25 @@ namespace OpenRA.Graphics
public readonly float3 FractionalOffset;
public readonly float Top, Left, Bottom, Right;
public Sprite(Sheet sheet, Rectangle bounds, TextureChannel channel)
: this(sheet, bounds, 0, float2.Zero, channel) { }
public Sprite(Sheet sheet, Rectangle bounds, TextureChannel channel, float scale = 1)
: this(sheet, bounds, 0, float2.Zero, channel, BlendMode.Alpha, scale) { }
public Sprite(Sheet sheet, Rectangle bounds, float zRamp, float3 offset, TextureChannel channel, BlendMode blendMode = BlendMode.Alpha)
public Sprite(Sheet sheet, Rectangle bounds, float zRamp, float3 offset, TextureChannel channel, BlendMode blendMode = BlendMode.Alpha, float scale = 1f)
{
Sheet = sheet;
Bounds = bounds;
Offset = offset;
ZRamp = zRamp;
Channel = channel;
Size = new float3(bounds.Size.Width, bounds.Size.Height, bounds.Size.Height * zRamp);
Size = scale * new float3(bounds.Size.Width, bounds.Size.Height, bounds.Size.Height * zRamp);
BlendMode = blendMode;
FractionalOffset = Size.Z != 0 ? offset / Size :
new float3(offset.X / Size.X, offset.Y / Size.Y, 0);
Left = (float)Math.Min(bounds.Left, bounds.Right) * sheet.DPIScale / sheet.Size.Width;
Top = (float)Math.Min(bounds.Top, bounds.Bottom) * sheet.DPIScale / sheet.Size.Height;
Right = (float)Math.Max(bounds.Left, bounds.Right) * sheet.DPIScale / sheet.Size.Width;
Bottom = (float)Math.Max(bounds.Top, bounds.Bottom) * sheet.DPIScale / sheet.Size.Height;
Left = (float)Math.Min(bounds.Left, bounds.Right) / sheet.Size.Width;
Top = (float)Math.Min(bounds.Top, bounds.Bottom) / sheet.Size.Height;
Right = (float)Math.Max(bounds.Left, bounds.Right) / sheet.Size.Width;
Bottom = (float)Math.Max(bounds.Top, bounds.Bottom) / sheet.Size.Height;
}
}
@@ -61,10 +61,10 @@ namespace OpenRA.Graphics
SecondarySheet = secondarySheet;
SecondaryBounds = secondaryBounds;
SecondaryChannel = secondaryChannel;
SecondaryLeft = (float)Math.Min(secondaryBounds.Left, secondaryBounds.Right) * secondarySheet.DPIScale / s.Sheet.Size.Width;
SecondaryTop = (float)Math.Min(secondaryBounds.Top, secondaryBounds.Bottom) * secondarySheet.DPIScale / s.Sheet.Size.Height;
SecondaryRight = (float)Math.Max(secondaryBounds.Left, secondaryBounds.Right) * secondarySheet.DPIScale / s.Sheet.Size.Width;
SecondaryBottom = (float)Math.Max(secondaryBounds.Top, secondaryBounds.Bottom) * secondarySheet.DPIScale / s.Sheet.Size.Height;
SecondaryLeft = (float)Math.Min(secondaryBounds.Left, secondaryBounds.Right) / s.Sheet.Size.Width;
SecondaryTop = (float)Math.Min(secondaryBounds.Top, secondaryBounds.Bottom) / s.Sheet.Size.Height;
SecondaryRight = (float)Math.Max(secondaryBounds.Left, secondaryBounds.Right) / s.Sheet.Size.Width;
SecondaryBottom = (float)Math.Max(secondaryBounds.Top, secondaryBounds.Bottom) / s.Sheet.Size.Height;
}
}

View File

@@ -117,6 +117,8 @@ namespace OpenRA.Primitives
return rect == Intersect(this, rect);
}
public static Rectangle operator *(int a, Rectangle b) { return new Rectangle(a * b.X, a * b.Y, a * b.Width, a * b.Height); }
public override string ToString()
{
return string.Format("{{X={0},Y={1},Width={2},Height={3}}}", X, Y, Width, Height);

View File

@@ -31,6 +31,7 @@ namespace OpenRA.Mods.Cnc
float2 loadingPos, versionPos;
Sheet lastSheet;
int lastDensity;
Size lastResolution;
IReadOnlyDictionary<string, SpriteFont> lastFonts;
@@ -41,31 +42,32 @@ namespace OpenRA.Mods.Cnc
versionText = modData.Manifest.Metadata.Version;
}
public override void DisplayInner(Renderer r, Sheet s)
public override void DisplayInner(Renderer r, Sheet s, int density)
{
if (s != lastSheet)
if (s != lastSheet || density != lastDensity)
{
lastSheet = s;
lastDensity = density;
border = new[]
{
new Sprite(s, new Rectangle(129, 129, 32, 32), TextureChannel.RGBA),
new Sprite(s, new Rectangle(161, 129, 62, 32), TextureChannel.RGBA),
new Sprite(s, new Rectangle(223, 129, 32, 32), TextureChannel.RGBA),
new Sprite(s, new Rectangle(129, 161, 32, 62), TextureChannel.RGBA),
CreateSprite(s, density, new Rectangle(129, 129, 32, 32)),
CreateSprite(s, density, new Rectangle(161, 129, 62, 32)),
CreateSprite(s, density, new Rectangle(223, 129, 32, 32)),
CreateSprite(s, density, new Rectangle(129, 161, 32, 62)),
null,
new Sprite(s, new Rectangle(223, 161, 32, 62), TextureChannel.RGBA),
new Sprite(s, new Rectangle(129, 223, 32, 32), TextureChannel.RGBA),
new Sprite(s, new Rectangle(161, 223, 62, 32), TextureChannel.RGBA),
new Sprite(s, new Rectangle(223, 223, 32, 32), TextureChannel.RGBA)
CreateSprite(s, density, new Rectangle(223, 161, 32, 62)),
CreateSprite(s, density, new Rectangle(129, 223, 32, 32)),
CreateSprite(s, density, new Rectangle(161, 223, 62, 32)),
CreateSprite(s, density, new Rectangle(223, 223, 32, 32))
};
nodLogo = new Sprite(s, new Rectangle(0, 256, 256, 256), TextureChannel.RGBA);
gdiLogo = new Sprite(s, new Rectangle(256, 256, 256, 256), TextureChannel.RGBA);
evaLogo = new Sprite(s, new Rectangle(769, 320, 128, 64), TextureChannel.RGBA);
nodLogo = CreateSprite(s, density, new Rectangle(0, 256, 256, 256));
gdiLogo = CreateSprite(s, density, new Rectangle(256, 256, 256, 256));
evaLogo = CreateSprite(s, density, new Rectangle(769, 320, 128, 64));
brightBlock = new Sprite(s, new Rectangle(777, 385, 16, 35), TextureChannel.RGBA);
dimBlock = new Sprite(s, new Rectangle(794, 385, 16, 35), TextureChannel.RGBA);
brightBlock = CreateSprite(s, density, new Rectangle(777, 385, 16, 35));
dimBlock = CreateSprite(s, density, new Rectangle(794, 385, 16, 35));
}
if (r.Resolution != lastResolution)

View File

@@ -24,6 +24,7 @@ namespace OpenRA.Mods.Common.LoadScreens
Sprite stripe, logo;
Sheet lastSheet;
int lastDensity;
Size lastResolution;
string[] messages = { "Loading..." };
@@ -36,13 +37,14 @@ namespace OpenRA.Mods.Common.LoadScreens
messages = info["Text"].Split(',');
}
public override void DisplayInner(Renderer r, Sheet s)
public override void DisplayInner(Renderer r, Sheet s, int density)
{
if (s != lastSheet)
if (s != lastSheet || density != lastDensity)
{
lastSheet = s;
logo = new Sprite(s, new Rectangle(0, 0, 256, 256), TextureChannel.RGBA);
stripe = new Sprite(s, new Rectangle(258, 0, 253, 256), TextureChannel.RGBA);
lastDensity = density;
logo = CreateSprite(s, density, new Rectangle(0, 0, 256, 256));
stripe = CreateSprite(s, density, new Rectangle(258, 0, 253, 256));
}
if (r.Resolution != lastResolution)

View File

@@ -25,14 +25,16 @@ namespace OpenRA.Mods.Common.LoadScreens
Rectangle bounds;
Sheet lastSheet;
int lastDensity;
Size lastResolution;
public override void DisplayInner(Renderer r, Sheet s)
public override void DisplayInner(Renderer r, Sheet s, int density)
{
if (s != lastSheet)
if (s != lastSheet || density != lastDensity)
{
lastSheet = s;
sprite = new Sprite(s, new Rectangle(0, 0, 1024, 480), TextureChannel.RGBA);
lastDensity = density;
sprite = CreateSprite(s, density, new Rectangle(0, 0, 1024, 480));
}
if (r.Resolution != lastResolution)

View File

@@ -12,6 +12,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using OpenRA.Graphics;
using OpenRA.Primitives;
namespace OpenRA.Mods.Common.LoadScreens
{
@@ -21,7 +22,9 @@ namespace OpenRA.Mods.Common.LoadScreens
protected Dictionary<string, string> Info { get; private set; }
float dpiScale = 1;
Sheet sheet;
int density;
public override void Init(ModData modData, Dictionary<string, string> info)
{
@@ -29,7 +32,7 @@ namespace OpenRA.Mods.Common.LoadScreens
Info = info;
}
public abstract void DisplayInner(Renderer r, Sheet s);
public abstract void DisplayInner(Renderer r, Sheet s, int density);
public override void Display()
{
@@ -58,33 +61,37 @@ namespace OpenRA.Mods.Common.LoadScreens
if (sheet == null && Info.ContainsKey("Image"))
{
var key = "Image";
float sheetScale = 1;
density = 1;
if (dpiScale > 2 && Info.ContainsKey("Image3x"))
{
key = "Image3x";
sheetScale = 3;
density = 3;
}
else if (dpiScale > 1 && Info.ContainsKey("Image2x"))
{
key = "Image2x";
sheetScale = 2;
density = 2;
}
using (var stream = ModData.DefaultFileSystem.Open(Info[key]))
{
sheet = new Sheet(SheetType.BGRA, stream);
sheet.GetTexture().ScaleFilter = TextureScaleFilter.Linear;
sheet.DPIScale = sheetScale;
}
}
Game.Renderer.BeginUI();
DisplayInner(Game.Renderer, sheet);
DisplayInner(Game.Renderer, sheet, density);
Game.Renderer.EndFrame(new NullInputHandler());
lastUpdate.Restart();
}
protected static Sprite CreateSprite(Sheet s, int density, Rectangle rect)
{
return new Sprite(s, density * rect, TextureChannel.RGBA, 1f / density);
}
protected override void Dispose(bool disposing)
{
if (disposing && sheet != null)

View File

@@ -164,8 +164,8 @@ namespace OpenRA.Mods.Common.Widgets
Game.Renderer.RgbaSpriteRenderer.DrawSprite(mixerSprite, RenderOrigin, new float2(RenderBounds.Size));
var sprite = ChromeProvider.GetImage("lobby-bits", "colorpicker");
var pos = RenderOrigin + PxFromValue() - new int2(sprite.Bounds.Width, sprite.Bounds.Height) / 2;
WidgetUtils.FillEllipseWithColor(new Rectangle(pos.X + 1, pos.Y + 1, sprite.Bounds.Width - 2, sprite.Bounds.Height - 2), Color);
var pos = RenderOrigin + PxFromValue() - new int2((int)sprite.Size.X, (int)sprite.Size.Y) / 2;
WidgetUtils.FillEllipseWithColor(new Rectangle(pos.X + 1, pos.Y + 1, (int)sprite.Size.X - 2, (int)sprite.Size.Y - 2), Color);
Game.Renderer.RgbaSpriteRenderer.DrawSprite(sprite, pos);
}

View File

@@ -48,10 +48,10 @@ namespace OpenRA.Mods.Common.Widgets
var image = ChromeProvider.GetImage("scrollbar", IsDisabled() ? "down_pressed" : "down_arrow");
var rb = RenderBounds;
WidgetUtils.DrawRGBA(image, stateOffset + new float2((rb.Right - (rb.Height + image.Bounds.Width) / 2), rb.Top + (rb.Height - image.Bounds.Height) / 2));
WidgetUtils.DrawRGBA(image, stateOffset + new float2(rb.Right - (rb.Height + image.Size.X) / 2, rb.Top + (rb.Height - image.Size.Y) / 2));
var separator = ChromeProvider.GetImage(SeparatorCollection, SeparatorImage);
WidgetUtils.DrawRGBA(separator, new float2(-3, 0) + new float2(rb.Right - rb.Height + 4, rb.Top + (rb.Height - separator.Bounds.Height) / 2));
WidgetUtils.DrawRGBA(separator, stateOffset + new float2(-3, 0) + new float2(rb.Right - rb.Height + 4, rb.Top + (rb.Height - separator.Size.Y) / 2));
}
public override Widget Clone() { return new DropDownButtonWidget(this); }

View File

@@ -47,7 +47,7 @@ namespace OpenRA.Mods.Common.Widgets
Game.Renderer.RgbaSpriteRenderer.DrawSprite(hueSprite, ro, new float2(rb.Size));
var sprite = ChromeProvider.GetImage("lobby-bits", "huepicker");
var pos = RenderOrigin + new int2(PxFromValue(Value).Clamp(0, rb.Width - 1) - sprite.Bounds.Width / 2, (rb.Height - sprite.Bounds.Height) / 2);
var pos = RenderOrigin + new int2(PxFromValue(Value).Clamp(0, rb.Width - 1) - (int)sprite.Size.X / 2, (rb.Height - (int)sprite.Size.Y) / 2);
Game.Renderer.RgbaSpriteRenderer.DrawSprite(sprite, pos);
}
}

View File

@@ -252,7 +252,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
if (!orderManager.LocalClient.IsObserver && orderManager.LocalClient.State == Session.ClientState.Ready)
return;
var spawnSize = new float2(ChromeProvider.GetImage("lobby-bits", "spawn-unclaimed").Bounds.Size);
var spawnSize = ChromeProvider.GetImage("lobby-bits", "spawn-unclaimed").Size.XY;
var selectedSpawn = preview.SpawnPoints
.Select((sp, i) => Pair.New(mapPreview.ConvertToPreview(sp, preview.GridType), i))
.Where(a => ((a.First - mi.Location).ToFloat2() / spawnSize * 2).LengthSquared <= 1)

View File

@@ -191,10 +191,10 @@ namespace OpenRA.Mods.Common.Widgets
var owned = colors.ContainsKey(p);
var pos = ConvertToPreview(p, gridType);
var sprite = owned ? spawnClaimed : spawnUnclaimed;
var offset = new int2(sprite.Bounds.Width, sprite.Bounds.Height) / 2;
var offset = sprite.Size.XY.ToInt2() / 2;
if (owned)
WidgetUtils.FillEllipseWithColor(new Rectangle(pos.X - offset.X + 1, pos.Y - offset.Y + 1, sprite.Bounds.Width - 2, sprite.Bounds.Height - 2), colors[p]);
WidgetUtils.FillEllipseWithColor(new Rectangle(pos.X - offset.X + 1, pos.Y - offset.Y + 1, (int)sprite.Size.X - 2, (int)sprite.Size.Y - 2), colors[p]);
Game.Renderer.RgbaSpriteRenderer.DrawSprite(sprite, pos - offset);
var number = Convert.ToChar('A' + spawnPoints.IndexOf(p)).ToString();

View File

@@ -47,23 +47,28 @@ namespace OpenRA.Mods.Common.Widgets
public static void FillRectWithSprite(Rectangle r, Sprite s)
{
for (var x = r.Left; x < r.Right; x += (int)s.Size.X)
for (var y = r.Top; y < r.Bottom; y += (int)s.Size.Y)
var scale = s.Size.X / s.Bounds.Width;
for (var x = (float)r.Left; x < r.Right; x += s.Size.X)
{
for (var y = (float)r.Top; y < r.Bottom; y += s.Size.Y)
{
var ss = s;
var left = new int2(r.Right - x, r.Bottom - y);
if (left.X < (int)s.Size.X || left.Y < (int)s.Size.Y)
var dx = r.Right - x;
var dy = r.Bottom - y;
if (dx < s.Size.X || dy < s.Size.Y)
{
var rr = new Rectangle(s.Bounds.Left,
var rr = new Rectangle(
s.Bounds.Left,
s.Bounds.Top,
Math.Min(left.X, (int)s.Size.X),
Math.Min(left.Y, (int)s.Size.Y));
ss = new Sprite(s.Sheet, rr, s.Channel);
Math.Min(s.Bounds.Width, (int)(dx / scale)),
Math.Min(s.Bounds.Height, (int)(dy / scale)));
ss = new Sprite(s.Sheet, rr, s.Channel, scale);
}
DrawRGBA(ss, new float2(x, y));
}
}
}
public static void FillRectWithColor(Rectangle r, Color c)
{