From 1a8a3eca4ddf1a7b74e9609c9500b2bcc0e8cb35 Mon Sep 17 00:00:00 2001 From: Gustas <37534529+PunkPun@users.noreply.github.com> Date: Thu, 28 Aug 2025 16:32:27 +0300 Subject: [PATCH] Add smooth scrolling --- OpenRA.Game/Game.cs | 2 +- OpenRA.Game/Graphics/Viewport.cs | 15 ++++---- OpenRA.Game/Primitives/float2.cs | 15 ++++++++ OpenRA.Game/Renderer.cs | 37 ++++++++++++------- .../Traits/World/WeatherOverlay.cs | 2 +- .../Widgets/Logic/PerfDebugLogic.cs | 2 +- 6 files changed, 48 insertions(+), 25 deletions(-) diff --git a/OpenRA.Game/Game.cs b/OpenRA.Game/Game.cs index ee9d56ce30..7fcac5fe7a 100644 --- a/OpenRA.Game/Game.cs +++ b/OpenRA.Game/Game.cs @@ -724,7 +724,7 @@ namespace OpenRA // Use worldRenderer.World instead of OrderManager.World to avoid a rendering mismatch while processing orders if (worldRenderer != null && !worldRenderer.World.IsLoadingGameSave) { - Renderer.BeginWorld(worldRenderer.Viewport.Rectangle); + Renderer.BeginWorld(worldRenderer.Viewport.CenterLocation, worldRenderer.Viewport.ViewportSize); Sound.SetListenerPosition(worldRenderer.Viewport.CenterPosition); using (new PerfSample("render_world")) worldRenderer.Draw(); diff --git a/OpenRA.Game/Graphics/Viewport.cs b/OpenRA.Game/Graphics/Viewport.cs index 7ca7619f3e..f71a27c639 100644 --- a/OpenRA.Game/Graphics/Viewport.cs +++ b/OpenRA.Game/Graphics/Viewport.cs @@ -49,14 +49,13 @@ namespace OpenRA.Graphics readonly Size tileSize; // Viewport geometry (world-px) - public int2 CenterLocation { get; private set; } + public float2 CenterLocation { get; private set; } - public WPos CenterPosition => worldRenderer.ProjectedPosition(CenterLocation); + public WPos CenterPosition => worldRenderer.ProjectedPosition(CenterLocation.ToInt2()); - public Rectangle Rectangle => new(TopLeft, new Size(viewportSize.X, viewportSize.Y)); - public int2 TopLeft => CenterLocation - viewportSize / 2; - public int2 BottomRight => CenterLocation + viewportSize / 2; - int2 viewportSize; + public int2 TopLeft => CenterLocation.ToInt2() - ViewportSize.ToInt2() / 2; + public int2 BottomRight => CenterLocation.ToInt2() + ViewportSize.ToInt2() / 2; + public Size ViewportSize { get; private set; } ProjectedCellRegion cells; bool cellsDirty = true; @@ -79,7 +78,7 @@ namespace OpenRA.Graphics private set { zoom = value; - viewportSize = (1f / zoom * new float2(Game.Renderer.NativeResolution)).ToInt2(); + ViewportSize = Size.FromInt2((1f / zoom * new float2(Game.Renderer.NativeResolution)).ToInt2()); cellsDirty = true; allCellsDirty = true; } @@ -341,7 +340,7 @@ namespace OpenRA.Graphics public void Scroll(float2 delta, bool ignoreBorders) { // Convert scroll delta from world-px to viewport-px - CenterLocation += (1f / Zoom * delta).ToInt2(); + CenterLocation += 1f / Zoom * delta; cellsDirty = true; allCellsDirty = true; diff --git a/OpenRA.Game/Primitives/float2.cs b/OpenRA.Game/Primitives/float2.cs index 1a26d9be6f..b8cb394a4e 100644 --- a/OpenRA.Game/Primitives/float2.cs +++ b/OpenRA.Game/Primitives/float2.cs @@ -93,6 +93,21 @@ namespace OpenRA public static float2 Max(float2 a, float2 b) { return new float2(Math.Max(a.X, b.X), Math.Max(a.Y, b.Y)); } public static float2 Min(float2 a, float2 b) { return new float2(Math.Min(a.X, b.X), Math.Min(a.Y, b.Y)); } + public float2 Clamp(Rectangle r) + { + var x = X; + var y = Y; + if (r.Left > X) + x = r.Left; + if (r.Top > Y) + y = r.Top; + if (r.Right < X) + x = r.Right; + if (r.Bottom < Y) + y = r.Bottom; + return new float2(x, y); + } + public float LengthSquared => X * X + Y * Y; public float Length => (float)Math.Sqrt(LengthSquared); } diff --git a/OpenRA.Game/Renderer.cs b/OpenRA.Game/Renderer.cs index ffb18a7a15..1994d7fa56 100644 --- a/OpenRA.Game/Renderer.cs +++ b/OpenRA.Game/Renderer.cs @@ -77,7 +77,8 @@ namespace OpenRA Size lastBufferSize = new(-1, -1); - Rectangle lastWorldViewport = Rectangle.Empty; + Rectangle lastWorldViewport; + float2 lastViewportLocation; ITexture currentPaletteTexture; int currentPaletteHeight = 0; IBatchRenderer currentBatchRenderer; @@ -233,7 +234,7 @@ namespace OpenRA lastMaximumViewportSize = size; } - public void BeginWorld(Rectangle worldViewport) + public void BeginWorld(float2 viewportLocation, Size viewportSize) { if (renderType != RenderType.None) throw new InvalidOperationException($"BeginWorld called with renderType = {renderType}, expected RenderType.None."); @@ -243,28 +244,34 @@ namespace OpenRA if (worldSheet == null) throw new InvalidOperationException("BeginWorld called before SetMaximumViewportSize has been set."); - if (worldSprite == null || worldViewport.Size != lastWorldViewportSize) + var centerLocation = viewportLocation.ToInt2(); + if (worldSprite == null || viewportSize != lastWorldViewportSize || viewportLocation != lastViewportLocation) { + lastViewportLocation = viewportLocation; + lastWorldViewportSize = viewportSize; + // Downscale world rendering if needed to fit within the framebuffer - var vw = worldViewport.Size.Width; - var vh = worldViewport.Size.Height; + var vw = viewportSize.Width; + var vh = viewportSize.Height; var bw = worldSheet.Size.Width; var bh = worldSheet.Size.Height; WorldDownscaleFactor = 1; while (vw / WorldDownscaleFactor > bw || vh / WorldDownscaleFactor > bh) WorldDownscaleFactor++; - var s = new Size(vw / WorldDownscaleFactor, vh / WorldDownscaleFactor); - worldSprite = new Sprite(worldSheet, new Rectangle(int2.Zero, s), TextureChannel.RGBA); - lastWorldViewportSize = worldViewport.Size; + // We need to add 1 to scroll in order to handle interpixel 0-0.99 fractionalOffset. + var s = new Size(vw / WorldDownscaleFactor + 1, vh / WorldDownscaleFactor + 1); + var fractionalOffset = centerLocation - viewportLocation; + worldSprite = new Sprite(worldSheet, new Rectangle(int2.Zero, s), 0, fractionalOffset, TextureChannel.RGBA); } worldBuffer.Bind(); - - if (lastWorldViewport != worldViewport) + var rect = new Rectangle(centerLocation, viewportSize); + if (lastWorldViewport != rect) { - WorldSpriteRenderer.SetViewportParams(worldSheet.Size, WorldDownscaleFactor, depthMargin, worldViewport.Location); - lastWorldViewport = worldViewport; + var topLeft = centerLocation - viewportSize.ToInt2() / 2; + WorldSpriteRenderer.SetViewportParams(worldSheet.Size, WorldDownscaleFactor, depthMargin, topLeft); + lastWorldViewport = rect; } renderType = RenderType.World; @@ -282,9 +289,11 @@ namespace OpenRA screenBuffer.Bind(); var scale = Window.EffectiveWindowScale; + + // We added 1 to worldSprite now we need to subtract. var bufferScale = new float3( - (int)(screenSprite.Bounds.Width / scale) / worldSprite.Size.X, - (int)(-screenSprite.Bounds.Height / scale) / worldSprite.Size.Y, + (int)(screenSprite.Bounds.Width / scale) / (worldSprite.Size.X - 1), + (int)(-screenSprite.Bounds.Height / scale) / (worldSprite.Size.Y - 1), 1f); SpriteRenderer.EnablePixelArtScaling(true); diff --git a/OpenRA.Mods.Common/Traits/World/WeatherOverlay.cs b/OpenRA.Mods.Common/Traits/World/WeatherOverlay.cs index 1812c1b2f3..b92a509f6f 100644 --- a/OpenRA.Mods.Common/Traits/World/WeatherOverlay.cs +++ b/OpenRA.Mods.Common/Traits/World/WeatherOverlay.cs @@ -240,7 +240,7 @@ namespace OpenRA.Mods.Common.Traits void IRenderAboveWorld.RenderAboveWorld(Actor self, WorldRenderer wr) { - var center = wr.Viewport.CenterLocation; + var center = wr.Viewport.CenterLocation.ToInt2(); var viewport = new Rectangle(center - new int2(viewportSize) / 2, viewportSize); var wcr = Game.Renderer.WorldRgbaColorRenderer; diff --git a/OpenRA.Mods.Common/Widgets/Logic/PerfDebugLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/PerfDebugLogic.cs index bd3f993d02..7683c6e9b9 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/PerfDebugLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/PerfDebugLogic.cs @@ -47,7 +47,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic var fps = (newestFrame - oldestFrame) / (newestTime - oldestTime).TotalSeconds; var wfbSize = Game.Renderer.WorldFrameBufferSize; - var viewportSize = worldRenderer.Viewport.Rectangle.Size; + var viewportSize = worldRenderer.Viewport.ViewportSize; return $"FPS: {fps:0}\nTick {Game.LocalTick} @ {PerfHistory.Items["tick_time"].Average(Game.Settings.Debug.Samples):F1} ms\n" + $"Render {Game.RenderFrame} @ {PerfHistory.Items["render"].Average(Game.Settings.Debug.Samples):F1} ms\n" + $"Batches: {PerfHistory.Items["batches"].LastValue}\n" +