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: