diff --git a/OpenRA.Game/Graphics/Animation.cs b/OpenRA.Game/Graphics/Animation.cs
index afd5feb260..525d7434fd 100644
--- a/OpenRA.Game/Graphics/Animation.cs
+++ b/OpenRA.Game/Graphics/Animation.cs
@@ -50,37 +50,41 @@ namespace OpenRA.Graphics
}
public int CurrentFrame => backwards ? CurrentSequence.Length - frame - 1 : frame;
+
public Sprite Image => CurrentSequence.GetSprite(CurrentFrame, facingFunc());
public IRenderable[] Render(WPos pos, in WVec offset, int zOffset, PaletteReference palette)
{
var tintModifiers = CurrentSequence.IgnoreWorldTint ? TintModifiers.IgnoreWorldTint : TintModifiers.None;
var alpha = CurrentSequence.GetAlpha(CurrentFrame);
- var imageRenderable = new SpriteRenderable(Image, pos, offset, CurrentSequence.ZOffset + zOffset, palette, CurrentSequence.Scale, alpha, float3.Ones, tintModifiers, IsDecoration);
+ var (image, rotation) = CurrentSequence.GetSpriteWithRotation(CurrentFrame, facingFunc());
+ var imageRenderable = new SpriteRenderable(image, pos, offset, CurrentSequence.ZOffset + zOffset, palette, CurrentSequence.Scale, alpha, float3.Ones, tintModifiers, IsDecoration,
+ rotation);
if (CurrentSequence.ShadowStart >= 0)
{
var shadow = CurrentSequence.GetShadow(CurrentFrame, facingFunc());
- var shadowRenderable = new SpriteRenderable(shadow, pos, offset, CurrentSequence.ShadowZOffset + zOffset, palette, CurrentSequence.Scale, 1f, float3.Ones, tintModifiers, true);
+ var shadowRenderable = new SpriteRenderable(shadow, pos, offset, CurrentSequence.ShadowZOffset + zOffset, palette, CurrentSequence.Scale, 1f, float3.Ones, tintModifiers,
+ true, rotation);
return new IRenderable[] { shadowRenderable, imageRenderable };
}
return new IRenderable[] { imageRenderable };
}
- public IRenderable[] RenderUI(WorldRenderer wr, int2 pos, in WVec offset, int zOffset, PaletteReference palette, float scale = 1f)
+ public IRenderable[] RenderUI(WorldRenderer wr, int2 pos, in WVec offset, int zOffset, PaletteReference palette, float scale = 1f, float rotation = 0f)
{
scale *= CurrentSequence.Scale;
var screenOffset = (scale * wr.ScreenVectorComponents(offset)).XY.ToInt2();
var imagePos = pos + screenOffset - new int2((int)(scale * Image.Size.X / 2), (int)(scale * Image.Size.Y / 2));
var alpha = CurrentSequence.GetAlpha(CurrentFrame);
- var imageRenderable = new UISpriteRenderable(Image, WPos.Zero + offset, imagePos, CurrentSequence.ZOffset + zOffset, palette, scale, alpha);
+ var imageRenderable = new UISpriteRenderable(Image, WPos.Zero + offset, imagePos, CurrentSequence.ZOffset + zOffset, palette, scale, alpha, rotation);
if (CurrentSequence.ShadowStart >= 0)
{
var shadow = CurrentSequence.GetShadow(CurrentFrame, facingFunc());
var shadowPos = pos - new int2((int)(scale * shadow.Size.X / 2), (int)(scale * shadow.Size.Y / 2));
- var shadowRenderable = new UISpriteRenderable(shadow, WPos.Zero + offset, shadowPos, CurrentSequence.ShadowZOffset + zOffset, palette, scale);
+ var shadowRenderable = new UISpriteRenderable(shadow, WPos.Zero + offset, shadowPos, CurrentSequence.ShadowZOffset + zOffset, palette, scale, 1f, rotation);
return new IRenderable[] { shadowRenderable, imageRenderable };
}
diff --git a/OpenRA.Game/Graphics/RgbaSpriteRenderer.cs b/OpenRA.Game/Graphics/RgbaSpriteRenderer.cs
index d9d85cb714..f64e0f28ab 100644
--- a/OpenRA.Game/Graphics/RgbaSpriteRenderer.cs
+++ b/OpenRA.Game/Graphics/RgbaSpriteRenderer.cs
@@ -22,28 +22,28 @@ namespace OpenRA.Graphics
this.parent = parent;
}
- public void DrawSprite(Sprite s, in float3 location, in float3 scale)
+ public void DrawSprite(Sprite s, in float3 location, in float3 scale, float rotation = 0f)
{
if (s.Channel != TextureChannel.RGBA)
throw new InvalidOperationException("DrawRGBASprite requires a RGBA sprite.");
- parent.DrawSprite(s, 0, location, scale);
+ parent.DrawSprite(s, 0, location, scale, rotation);
}
- public void DrawSprite(Sprite s, in float3 location, float scale = 1f)
+ public void DrawSprite(Sprite s, in float3 location, float scale = 1f, float rotation = 0f)
{
if (s.Channel != TextureChannel.RGBA)
throw new InvalidOperationException("DrawRGBASprite requires a RGBA sprite.");
- parent.DrawSprite(s, 0, location, scale);
+ parent.DrawSprite(s, 0, location, scale, rotation);
}
- public void DrawSprite(Sprite s, in float3 location, float scale, in float3 tint, float alpha)
+ public void DrawSprite(Sprite s, in float3 location, float scale, in float3 tint, float alpha, float rotation = 0f)
{
if (s.Channel != TextureChannel.RGBA)
throw new InvalidOperationException("DrawRGBASprite requires a RGBA sprite.");
- parent.DrawSprite(s, 0, location, scale, tint, alpha);
+ parent.DrawSprite(s, 0, location, scale, tint, alpha, rotation);
}
public void DrawSprite(Sprite s, in float3 a, in float3 b, in float3 c, in float3 d, in float3 tint, float alpha)
diff --git a/OpenRA.Game/Graphics/SequenceProvider.cs b/OpenRA.Game/Graphics/SequenceProvider.cs
index 0cb62988cd..04ec9a0c4c 100644
--- a/OpenRA.Game/Graphics/SequenceProvider.cs
+++ b/OpenRA.Game/Graphics/SequenceProvider.cs
@@ -26,6 +26,7 @@ namespace OpenRA.Graphics
int Length { get; }
int Stride { get; }
int Facings { get; }
+ int InterpolatedFacings { get; }
int Tick { get; }
int ZOffset { get; }
int ShadowStart { get; }
@@ -37,6 +38,7 @@ namespace OpenRA.Graphics
Sprite GetSprite(int frame);
Sprite GetSprite(int frame, WAngle facing);
+ (Sprite, WAngle) GetSpriteWithRotation(int frame, WAngle facing);
Sprite GetShadow(int frame, WAngle facing);
float GetAlpha(int frame);
}
diff --git a/OpenRA.Game/Graphics/SpriteRenderable.cs b/OpenRA.Game/Graphics/SpriteRenderable.cs
index 5b2e96b7c1..b4cde89162 100644
--- a/OpenRA.Game/Graphics/SpriteRenderable.cs
+++ b/OpenRA.Game/Graphics/SpriteRenderable.cs
@@ -25,12 +25,14 @@ namespace OpenRA.Graphics
readonly int zOffset;
readonly PaletteReference palette;
readonly float scale;
+ readonly WAngle rotation = WAngle.Zero;
readonly float3 tint;
readonly TintModifiers tintModifiers;
readonly float alpha;
readonly bool isDecoration;
- public SpriteRenderable(Sprite sprite, WPos pos, WVec offset, int zOffset, PaletteReference palette, float scale, float alpha, float3 tint, TintModifiers tintModifiers, bool isDecoration)
+ public SpriteRenderable(Sprite sprite, WPos pos, WVec offset, int zOffset, PaletteReference palette, float scale, float alpha,
+ float3 tint, TintModifiers tintModifiers, bool isDecoration, WAngle rotation)
{
this.sprite = sprite;
this.pos = pos;
@@ -38,6 +40,7 @@ namespace OpenRA.Graphics
this.zOffset = zOffset;
this.palette = palette;
this.scale = scale;
+ this.rotation = rotation;
this.tint = tint;
this.isDecoration = isDecoration;
this.tintModifiers = tintModifiers;
@@ -50,6 +53,10 @@ namespace OpenRA.Graphics
this.palette = null;
}
+ public SpriteRenderable(Sprite sprite, WPos pos, WVec offset, int zOffset, PaletteReference palette, float scale, float alpha,
+ float3 tint, TintModifiers tintModifiers, bool isDecoration)
+ : this(sprite, pos, offset, zOffset, palette, scale, alpha, tint, tintModifiers, isDecoration, WAngle.Zero) { }
+
public WPos Pos => pos + offset;
public WVec Offset => offset;
public PaletteReference Palette => palette;
@@ -60,19 +67,34 @@ namespace OpenRA.Graphics
public float3 Tint => tint;
public TintModifiers TintModifiers => tintModifiers;
- public IPalettedRenderable WithPalette(PaletteReference newPalette) { return new SpriteRenderable(sprite, pos, offset, zOffset, newPalette, scale, alpha, tint, tintModifiers, isDecoration); }
- public IRenderable WithZOffset(int newOffset) { return new SpriteRenderable(sprite, pos, offset, newOffset, palette, scale, alpha, tint, tintModifiers, isDecoration); }
- public IRenderable OffsetBy(in WVec vec) { return new SpriteRenderable(sprite, pos + vec, offset, zOffset, palette, scale, alpha, tint, tintModifiers, isDecoration); }
- public IRenderable AsDecoration() { return new SpriteRenderable(sprite, pos, offset, zOffset, palette, scale, alpha, tint, tintModifiers, true); }
+ public IPalettedRenderable WithPalette(PaletteReference newPalette)
+ {
+ return new SpriteRenderable(sprite, pos, offset, zOffset, newPalette, scale, alpha, tint, tintModifiers, isDecoration, rotation);
+ }
+
+ public IRenderable WithZOffset(int newOffset)
+ {
+ return new SpriteRenderable(sprite, pos, offset, newOffset, palette, scale, alpha, tint, tintModifiers, isDecoration, rotation);
+ }
+
+ public IRenderable OffsetBy(in WVec vec)
+ {
+ return new SpriteRenderable(sprite, pos + vec, offset, zOffset, palette, scale, alpha, tint, tintModifiers, isDecoration, rotation);
+ }
+
+ public IRenderable AsDecoration()
+ {
+ return new SpriteRenderable(sprite, pos, offset, zOffset, palette, scale, alpha, tint, tintModifiers, true, rotation);
+ }
public IModifyableRenderable WithAlpha(float newAlpha)
{
- return new SpriteRenderable(sprite, pos, offset, zOffset, palette, scale, newAlpha, tint, tintModifiers, isDecoration);
+ return new SpriteRenderable(sprite, pos, offset, zOffset, palette, scale, newAlpha, tint, tintModifiers, isDecoration, rotation);
}
public IModifyableRenderable WithTint(in float3 newTint, TintModifiers newTintModifiers)
{
- return new SpriteRenderable(sprite, pos, offset, zOffset, palette, scale, alpha, newTint, newTintModifiers, isDecoration);
+ return new SpriteRenderable(sprite, pos, offset, zOffset, palette, scale, alpha, newTint, newTintModifiers, isDecoration, rotation);
}
float3 ScreenPosition(WorldRenderer wr)
@@ -94,7 +116,7 @@ namespace OpenRA.Graphics
if ((tintModifiers & TintModifiers.ReplaceColor) != 0)
a *= -1;
- wsr.DrawSprite(sprite, palette, ScreenPosition(wr), scale, t, a);
+ wsr.DrawSprite(sprite, palette, ScreenPosition(wr), scale, t, a, rotation.RendererRadians());
}
public void RenderDebugGeometry(WorldRenderer wr)
@@ -102,13 +124,16 @@ namespace OpenRA.Graphics
var pos = ScreenPosition(wr) + sprite.Offset;
var tl = wr.Viewport.WorldToViewPx(pos);
var br = wr.Viewport.WorldToViewPx(pos + sprite.Size);
- Game.Renderer.RgbaColorRenderer.DrawRect(tl, br, 1, Color.Red);
+ if (rotation == WAngle.Zero)
+ Game.Renderer.RgbaColorRenderer.DrawRect(tl, br, 1, Color.Red);
+ else
+ Game.Renderer.RgbaColorRenderer.DrawPolygon(Util.RotateQuad(tl, br - tl, rotation.RendererRadians()), 1, Color.Red);
}
public Rectangle ScreenBounds(WorldRenderer wr)
{
var screenOffset = ScreenPosition(wr) + sprite.Offset;
- return new Rectangle((int)screenOffset.X, (int)screenOffset.Y, (int)sprite.Size.X, (int)sprite.Size.Y);
+ return Util.BoundingRectangle(screenOffset, sprite.Size, rotation.RendererRadians());
}
}
}
diff --git a/OpenRA.Game/Graphics/SpriteRenderer.cs b/OpenRA.Game/Graphics/SpriteRenderer.cs
index 7e88635a36..02b222566a 100644
--- a/OpenRA.Game/Graphics/SpriteRenderer.cs
+++ b/OpenRA.Game/Graphics/SpriteRenderer.cs
@@ -126,35 +126,40 @@ namespace OpenRA.Graphics
return pal.TextureIndex;
}
- internal void DrawSprite(Sprite s, float paletteTextureIndex, in float3 location, in float3 scale)
+ internal void DrawSprite(Sprite s, float paletteTextureIndex, in float3 location, in float3 scale, float rotation = 0f)
{
var samplers = SetRenderStateForSprite(s);
- Util.FastCreateQuad(vertices, location + scale * s.Offset, s, samplers, paletteTextureIndex, nv, scale * s.Size, float3.Ones, 1f);
+ Util.FastCreateQuad(vertices, location + scale * s.Offset, s, samplers, paletteTextureIndex, nv, scale * s.Size, float3.Ones,
+ 1f, rotation);
nv += 6;
}
- internal void DrawSprite(Sprite s, float paletteTextureIndex, in float3 location, float scale)
+ internal void DrawSprite(Sprite s, float paletteTextureIndex, in float3 location, float scale, float rotation = 0f)
{
var samplers = SetRenderStateForSprite(s);
- Util.FastCreateQuad(vertices, location + scale * s.Offset, s, samplers, paletteTextureIndex, nv, scale * s.Size, float3.Ones, 1f);
+ Util.FastCreateQuad(vertices, location + scale * s.Offset, s, samplers, paletteTextureIndex, nv, scale * s.Size, float3.Ones,
+ 1f, rotation);
nv += 6;
}
- public void DrawSprite(Sprite s, PaletteReference pal, in float3 location, float scale = 1f)
+ public void DrawSprite(Sprite s, PaletteReference pal, in float3 location, float scale = 1f, float rotation = 0f)
{
- DrawSprite(s, ResolveTextureIndex(s, pal), location, scale);
+ DrawSprite(s, ResolveTextureIndex(s, pal), location, scale, rotation);
}
- internal void DrawSprite(Sprite s, float paletteTextureIndex, in float3 location, float scale, in float3 tint, float alpha)
+ internal void DrawSprite(Sprite s, float paletteTextureIndex, in float3 location, float scale, in float3 tint, float alpha,
+ float rotation = 0f)
{
var samplers = SetRenderStateForSprite(s);
- Util.FastCreateQuad(vertices, location + scale * s.Offset, s, samplers, paletteTextureIndex, nv, scale * s.Size, tint, alpha);
+ Util.FastCreateQuad(vertices, location + scale * s.Offset, s, samplers, paletteTextureIndex, nv, scale * s.Size, tint, alpha,
+ rotation);
nv += 6;
}
- public void DrawSprite(Sprite s, PaletteReference pal, in float3 location, float scale, in float3 tint, float alpha)
+ public void DrawSprite(Sprite s, PaletteReference pal, in float3 location, float scale, in float3 tint, float alpha,
+ float rotation = 0f)
{
- DrawSprite(s, ResolveTextureIndex(s, pal), location, scale, tint, alpha);
+ DrawSprite(s, ResolveTextureIndex(s, pal), location, scale, tint, alpha, rotation);
}
internal void DrawSprite(Sprite s, float paletteTextureIndex, in float3 a, in float3 b, in float3 c, in float3 d, in float3 tint, float alpha)
diff --git a/OpenRA.Game/Graphics/UISpriteRenderable.cs b/OpenRA.Game/Graphics/UISpriteRenderable.cs
index ad5e69b84c..d591b9ad9c 100644
--- a/OpenRA.Game/Graphics/UISpriteRenderable.cs
+++ b/OpenRA.Game/Graphics/UISpriteRenderable.cs
@@ -22,8 +22,9 @@ namespace OpenRA.Graphics
readonly PaletteReference palette;
readonly float scale;
readonly float alpha;
+ readonly float rotation = 0f;
- public UISpriteRenderable(Sprite sprite, WPos effectiveWorldPos, int2 screenPos, int zOffset, PaletteReference palette, float scale = 1f, float alpha = 1f)
+ public UISpriteRenderable(Sprite sprite, WPos effectiveWorldPos, int2 screenPos, int zOffset, PaletteReference palette, float scale = 1f, float alpha = 1f, float rotation = 0f)
{
this.sprite = sprite;
this.effectiveWorldPos = effectiveWorldPos;
@@ -32,6 +33,7 @@ namespace OpenRA.Graphics
this.palette = palette;
this.scale = scale;
this.alpha = alpha;
+ this.rotation = rotation;
// PERF: Remove useless palette assignments for RGBA sprites
// HACK: This is working around the fact that palettes are defined on traits rather than sequences
@@ -48,7 +50,7 @@ namespace OpenRA.Graphics
public PaletteReference Palette => palette;
public int ZOffset => zOffset;
- public IPalettedRenderable WithPalette(PaletteReference newPalette) { return new UISpriteRenderable(sprite, effectiveWorldPos, screenPos, zOffset, newPalette, scale, alpha); }
+ public IPalettedRenderable WithPalette(PaletteReference newPalette) { return new UISpriteRenderable(sprite, effectiveWorldPos, screenPos, zOffset, newPalette, scale, alpha, rotation); }
public IRenderable WithZOffset(int newOffset) { return this; }
public IRenderable OffsetBy(in WVec vec) { return this; }
public IRenderable AsDecoration() { return this; }
@@ -56,19 +58,22 @@ namespace OpenRA.Graphics
public IFinalizedRenderable PrepareRender(WorldRenderer wr) { return this; }
public void Render(WorldRenderer wr)
{
- Game.Renderer.SpriteRenderer.DrawSprite(sprite, palette, screenPos, scale, float3.Ones, alpha);
+ Game.Renderer.SpriteRenderer.DrawSprite(sprite, palette, screenPos, scale, float3.Ones, alpha, rotation);
}
public void RenderDebugGeometry(WorldRenderer wr)
{
var offset = screenPos + sprite.Offset.XY;
- Game.Renderer.RgbaColorRenderer.DrawRect(offset, offset + sprite.Size.XY, 1, Color.Red);
+ if (rotation == 0f)
+ Game.Renderer.RgbaColorRenderer.DrawRect(offset, offset + sprite.Size.XY, 1, Color.Red);
+ else
+ Game.Renderer.RgbaColorRenderer.DrawPolygon(Util.RotateQuad(offset, sprite.Size, rotation), 1, Color.Red);
}
public Rectangle ScreenBounds(WorldRenderer wr)
{
var offset = screenPos + sprite.Offset;
- return new Rectangle((int)offset.X, (int)offset.Y, (int)sprite.Size.X, (int)sprite.Size.Y);
+ return Util.BoundingRectangle(offset, sprite.Size, rotation);
}
}
}
diff --git a/OpenRA.Game/Graphics/Util.cs b/OpenRA.Game/Graphics/Util.cs
index 1285e33dd6..eb524aced6 100644
--- a/OpenRA.Game/Graphics/Util.cs
+++ b/OpenRA.Game/Graphics/Util.cs
@@ -20,12 +20,44 @@ namespace OpenRA.Graphics
// yes, our channel order is nuts.
static readonly int[] ChannelMasks = { 2, 1, 0, 3 };
- public static void FastCreateQuad(Vertex[] vertices, in float3 o, Sprite r, int2 samplers, float paletteTextureIndex, int nv, in float3 size, in float3 tint, float alpha)
+ public static void FastCreateQuad(Vertex[] vertices, in float3 o, Sprite r, int2 samplers, float paletteTextureIndex, int nv,
+ in float3 size, in float3 tint, float alpha, float rotation = 0f)
{
- var b = new float3(o.X + size.X, o.Y, o.Z);
- var c = new float3(o.X + size.X, o.Y + size.Y, o.Z + size.Z);
- var d = new float3(o.X, o.Y + size.Y, o.Z + size.Z);
- FastCreateQuad(vertices, o, b, c, d, r, samplers, paletteTextureIndex, tint, alpha, nv);
+ float3 a, b, c, d;
+
+ // Rotate sprite if rotation angle is not equal to 0
+ if (rotation != 0f)
+ {
+ var center = o + 0.5f * size;
+ var angleSin = (float)Math.Sin(-rotation);
+ var angleCos = (float)Math.Cos(-rotation);
+
+ // Rotated offset for +/- x with +/- y
+ var ra = 0.5f * new float3(
+ size.X * angleCos - size.Y * angleSin,
+ size.X * angleSin + size.Y * angleCos,
+ (size.X * angleSin + size.Y * angleCos) * size.Z / size.Y);
+
+ // Rotated offset for +/- x with -/+ y
+ var rb = 0.5f * new float3(
+ size.X * angleCos + size.Y * angleSin,
+ size.X * angleSin - size.Y * angleCos,
+ (size.X * angleSin - size.Y * angleCos) * size.Z / size.Y);
+
+ a = center - ra;
+ b = center + rb;
+ c = center + ra;
+ d = center - rb;
+ }
+ else
+ {
+ a = o;
+ b = new float3(o.X + size.X, o.Y, o.Z);
+ c = new float3(o.X + size.X, o.Y + size.Y, o.Z + size.Z);
+ d = new float3(o.X, o.Y + size.Y, o.Z + size.Z);
+ }
+
+ FastCreateQuad(vertices, a, b, c, d, r, samplers, paletteTextureIndex, tint, alpha, nv);
}
public static void FastCreateQuad(Vertex[] vertices,
@@ -191,6 +223,69 @@ namespace OpenRA.Graphics
}
}
+ /// Rotates a quad about its center in the x-y plane.
+ /// The top left vertex of the quad
+ /// A float3 containing the X, Y, and Z lengths of the quad
+ /// The number of radians to rotate by
+ /// An array of four vertices representing the rotated quad (top-left, top-right, bottom-right, bottom-left)
+ public static float3[] RotateQuad(float3 tl, float3 size, float rotation)
+ {
+ var center = tl + 0.5f * size;
+ var angleSin = (float)Math.Sin(-rotation);
+ var angleCos = (float)Math.Cos(-rotation);
+
+ // Rotated offset for +/- x with +/- y
+ var ra = 0.5f * new float3(
+ size.X * angleCos - size.Y * angleSin,
+ size.X * angleSin + size.Y * angleCos,
+ (size.X * angleSin + size.Y * angleCos) * size.Z / size.Y);
+
+ // Rotated offset for +/- x with -/+ y
+ var rb = 0.5f * new float3(
+ size.X * angleCos + size.Y * angleSin,
+ size.X * angleSin - size.Y * angleCos,
+ (size.X * angleSin - size.Y * angleCos) * size.Z / size.Y);
+
+ return new float3[]
+ {
+ center - ra,
+ center + rb,
+ center + ra,
+ center - rb
+ };
+ }
+
+ ///
+ /// Returns the bounds of an object. Used for determining which objects need to be rendered on screen, and which do not.
+ ///
+ /// The top left vertex of the object
+ /// A float 3 containing the X, Y, and Z lengths of the object
+ /// The angle to rotate the object by (use 0f if there is no rotation)
+ public static Rectangle BoundingRectangle(float3 offset, float3 size, float rotation)
+ {
+ if (rotation == 0f)
+ return new Rectangle((int)offset.X, (int)offset.Y, (int)size.X, (int)size.Y);
+
+ var rotatedQuad = Util.RotateQuad(offset, size, rotation);
+ var minX = rotatedQuad[0].X;
+ var maxX = rotatedQuad[0].X;
+ var minY = rotatedQuad[0].Y;
+ var maxY = rotatedQuad[0].Y;
+ for (var i = 1; i < rotatedQuad.Length; i++)
+ {
+ minX = Math.Min(rotatedQuad[i].X, minX);
+ maxX = Math.Max(rotatedQuad[i].X, maxX);
+ minY = Math.Min(rotatedQuad[i].Y, minY);
+ maxY = Math.Max(rotatedQuad[i].Y, maxY);
+ }
+
+ return new Rectangle(
+ (int)minX,
+ (int)minY,
+ (int)Math.Ceiling(maxX) - (int)minX,
+ (int)Math.Ceiling(maxY) - (int)minY);
+ }
+
public static Color PremultiplyAlpha(Color c)
{
if (c.A == byte.MaxValue)
diff --git a/OpenRA.Mods.Common/Graphics/DefaultSpriteSequence.cs b/OpenRA.Mods.Common/Graphics/DefaultSpriteSequence.cs
index 244cce8dbc..3bb47e71a7 100644
--- a/OpenRA.Mods.Common/Graphics/DefaultSpriteSequence.cs
+++ b/OpenRA.Mods.Common/Graphics/DefaultSpriteSequence.cs
@@ -108,6 +108,7 @@ namespace OpenRA.Mods.Common.Graphics
int ISpriteSequence.Length => throw exception;
int ISpriteSequence.Stride => throw exception;
int ISpriteSequence.Facings => throw exception;
+ int ISpriteSequence.InterpolatedFacings => throw exception;
int ISpriteSequence.Tick => throw exception;
int ISpriteSequence.ZOffset => throw exception;
int ISpriteSequence.ShadowStart => throw exception;
@@ -118,6 +119,7 @@ namespace OpenRA.Mods.Common.Graphics
float ISpriteSequence.Scale => throw exception;
Sprite ISpriteSequence.GetSprite(int frame) { throw exception; }
Sprite ISpriteSequence.GetSprite(int frame, WAngle facing) { throw exception; }
+ (Sprite, WAngle) ISpriteSequence.GetSpriteWithRotation(int frame, WAngle facing) { throw exception; }
Sprite ISpriteSequence.GetShadow(int frame, WAngle facing) { throw exception; }
float ISpriteSequence.GetAlpha(int frame) { throw exception; }
}
@@ -167,6 +169,11 @@ namespace OpenRA.Mods.Common.Graphics
int ISpriteSequence.Facings => facings;
protected int facings;
+ [Desc("The amount of directions the unit faces. Use negative values to rotate counter-clockwise.")]
+ static readonly SpriteSequenceField InterpolatedFacings = new SpriteSequenceField(nameof(InterpolatedFacings), 1);
+ int ISpriteSequence.InterpolatedFacings => interpolatedFacings;
+ protected int interpolatedFacings;
+
[Desc("Time (in milliseconds at default game speed) to wait until playing the next frame in the animation.")]
static readonly SpriteSequenceField Tick = new SpriteSequenceField(nameof(Tick), 40);
int ISpriteSequence.Tick => tick;
@@ -305,6 +312,11 @@ namespace OpenRA.Mods.Common.Graphics
var zRamp = LoadField(d, ZRamp);
facings = LoadField(d, Facings);
+ interpolatedFacings = LoadField(d, nameof(InterpolatedFacings), -1);
+ if (interpolatedFacings != -1 && (interpolatedFacings <= 1 || interpolatedFacings <= Math.Abs(facings) || interpolatedFacings > 1024
+ || !Exts.IsPowerOf2(interpolatedFacings)))
+ throw new YamlException($"InterpolatedFacings must be greater than Facings, within the range of 2 to 1024, and a power of 2.");
+
if (facings < 0)
{
reverseFacings = true;
@@ -531,6 +543,17 @@ namespace OpenRA.Mods.Common.Graphics
return GetSprite(start, frame, facing);
}
+ public (Sprite, WAngle) GetSpriteWithRotation(int frame, WAngle facing)
+ {
+ var rotation = WAngle.Zero;
+
+ // Note: Error checking is not done here as it is done on load
+ if (interpolatedFacings != -1)
+ rotation = Util.GetInterpolatedFacing(facing, Math.Abs(facings), interpolatedFacings);
+
+ return (GetSprite(start, frame, facing), rotation);
+ }
+
public Sprite GetShadow(int frame, WAngle facing)
{
return shadowStart >= 0 ? GetSprite(shadowStart, frame, facing) : null;
diff --git a/OpenRA.Mods.Common/Traits/QuantizeFacingsFromSequence.cs b/OpenRA.Mods.Common/Traits/QuantizeFacingsFromSequence.cs
index 8d5b7fddcd..cef4a45b14 100644
--- a/OpenRA.Mods.Common/Traits/QuantizeFacingsFromSequence.cs
+++ b/OpenRA.Mods.Common/Traits/QuantizeFacingsFromSequence.cs
@@ -29,7 +29,8 @@ namespace OpenRA.Mods.Common.Traits
throw new InvalidOperationException("Actor " + ai.Name + " is missing sequence to quantize facings from.");
var rsi = ai.TraitInfo();
- return sequenceProvider.GetSequence(rsi.GetImage(ai, race), Sequence).Facings;
+ var seq = sequenceProvider.GetSequence(rsi.GetImage(ai, race), Sequence);
+ return seq.InterpolatedFacings == -1 ? seq.Facings : seq.InterpolatedFacings;
}
public override object Create(ActorInitializer init) { return new QuantizeFacingsFromSequence(this); }
diff --git a/OpenRA.Mods.Common/Util.cs b/OpenRA.Mods.Common/Util.cs
index 35525044d3..fa27d6a46a 100644
--- a/OpenRA.Mods.Common/Util.cs
+++ b/OpenRA.Mods.Common/Util.cs
@@ -67,11 +67,28 @@ namespace OpenRA.Mods.Common
///
public static int IndexFacing(WAngle facing, int numFrames)
{
+ // 1024 here is the max angle, so we divide the max angle by the total number of facings (numFrames)
var step = 1024 / numFrames;
var a = (facing.Angle + step / 2) & 1023;
return a / step;
}
+ ///
+ /// Returns the remainder angle after rounding to the nearest whole step / facing
+ ///
+ public static WAngle AngleDiffToStep(WAngle facing, int numFrames)
+ {
+ var step = 1024 / numFrames;
+ var a = (facing.Angle + step / 2) & 1023;
+ return new WAngle(a % step - step / 2);
+ }
+
+ public static WAngle GetInterpolatedFacing(WAngle facing, int facings, int interpolatedFacings)
+ {
+ var step = 1024 / interpolatedFacings;
+ return new WAngle(AngleDiffToStep(facing, facings).Angle / step * step);
+ }
+
/// Rounds the given facing value to the nearest quantized step.
public static WAngle QuantizeFacing(WAngle facing, int steps)
{
diff --git a/mods/ra/sequences/aircraft.yaml b/mods/ra/sequences/aircraft.yaml
index 508ef76077..871cf3d815 100644
--- a/mods/ra/sequences/aircraft.yaml
+++ b/mods/ra/sequences/aircraft.yaml
@@ -1,11 +1,13 @@
mig:
idle:
Facings: 16
+ InterpolatedFacings: 64
icon: migicon
yak:
idle:
Facings: 16
+ InterpolatedFacings: 64
muzzle: minigun
Length: 6
Facings: 8
@@ -66,10 +68,12 @@ tran2husk:
u2:
idle:
Facings: 16
+ InterpolatedFacings: 64
badr:
idle:
Facings: 16
+ InterpolatedFacings: 64
mh60:
idle: