From cac7ec39f4b0de79dc647ad84980775d3037747c Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Sun, 31 May 2015 22:40:12 +0100 Subject: [PATCH] Rework shroud rendering using TerrainSpriteLayer. --- OpenRA.Game/Traits/World/Shroud.cs | 2 +- .../Traits/World/ShroudRenderer.cs | 270 ++++++------------ 2 files changed, 92 insertions(+), 180 deletions(-) diff --git a/OpenRA.Game/Traits/World/Shroud.cs b/OpenRA.Game/Traits/World/Shroud.cs index b2469772c4..ac7ddfbbc4 100644 --- a/OpenRA.Game/Traits/World/Shroud.cs +++ b/OpenRA.Game/Traits/World/Shroud.cs @@ -340,7 +340,7 @@ namespace OpenRA.Traits return IsVisible(uv); } - bool IsVisible(MPos uv) + public bool IsVisible(MPos uv) { if (!map.Contains(uv)) return false; diff --git a/OpenRA.Mods.Common/Traits/World/ShroudRenderer.cs b/OpenRA.Mods.Common/Traits/World/ShroudRenderer.cs index 6330f814dc..fdd0fc1111 100644 --- a/OpenRA.Mods.Common/Traits/World/ShroudRenderer.cs +++ b/OpenRA.Mods.Common/Traits/World/ShroudRenderer.cs @@ -10,7 +10,12 @@ using System; using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; using OpenRA.Graphics; +using OpenRA.Primitives; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits @@ -43,7 +48,7 @@ namespace OpenRA.Mods.Common.Traits public object Create(ActorInitializer init) { return new ShroudRenderer(init.World, this); } } - public class ShroudRenderer : IRenderShroud, IWorldLoaded + public sealed class ShroudRenderer : IRenderShroud, IWorldLoaded, INotifyActorDisposing { [Flags] enum Edges : byte @@ -85,17 +90,13 @@ namespace OpenRA.Mods.Common.Traits readonly byte[] edgesToSpriteIndexOffset; readonly CellLayer tileInfos; - readonly CellLayer shroudDirty; - readonly HashSet cellsDirty; - readonly HashSet cellsAndNeighborsDirty; - - readonly Vertex[] fogVertices, shroudVertices; readonly Sprite[] fogSprites, shroudSprites; - readonly CellLayer fogSpriteLayer, shroudSpriteLayer; - PaletteReference fogPalette, shroudPalette; + readonly HashSet cellsDirty = new HashSet(); + readonly HashSet cellsAndNeighborsDirty = new HashSet(); Shroud currentShroud; - bool mapBorderShroudIsCached; + Func visibleUnderShroud, visibleUnderFog; + TerrainSpriteLayer shroudLayer, fogLayer; public ShroudRenderer(World world, ShroudRendererInfo info) { @@ -115,14 +116,6 @@ namespace OpenRA.Mods.Common.Traits map = world.Map; tileInfos = new CellLayer(map); - shroudDirty = new CellLayer(map); - cellsDirty = new HashSet(); - cellsAndNeighborsDirty = new HashSet(); - var verticesLength = map.MapSize.X * map.MapSize.Y * 4; - fogVertices = new Vertex[verticesLength]; - shroudVertices = new Vertex[verticesLength]; - fogSpriteLayer = new CellLayer(map); - shroudSpriteLayer = new CellLayer(map); // Load sprite variants var variantCount = info.ShroudVariants.Length; @@ -170,14 +163,28 @@ namespace OpenRA.Mods.Common.Traits tileInfos[uv] = new TileInfo(screen, variant); } - fogPalette = wr.Palette(info.FogPalette); - shroudPalette = wr.Palette(info.ShroudPalette); + DirtyCells(map.AllCells); + visibleUnderShroud = map.Contains; + visibleUnderFog = map.Contains; - wr.PaletteInvalidated += () => - { - mapBorderShroudIsCached = false; - MarkCellsDirty(map.AllCells); - }; + var shroudSheet = shroudSprites[0].Sheet; + if (shroudSprites.Any(s => s.Sheet != shroudSheet)) + throw new InvalidDataException("Shroud sprites span multiple sheets. Try loading their sequences earlier."); + + var shroudBlend = shroudSprites[0].BlendMode; + if (shroudSprites.Any(s => s.BlendMode != shroudBlend)) + throw new InvalidDataException("Shroud sprites must all use the same blend mode."); + + var fogSheet = fogSprites[0].Sheet; + if (fogSprites.Any(s => s.Sheet != fogSheet)) + throw new InvalidDataException("Fog sprites span multiple sheets. Try loading their sequences earlier."); + + var fogBlend = fogSprites[0].BlendMode; + if (fogSprites.Any(s => s.BlendMode != fogBlend)) + throw new InvalidDataException("Fog sprites must all use the same blend mode."); + + shroudLayer = new TerrainSpriteLayer(w, wr, shroudSheet, shroudBlend, wr.Palette(info.ShroudPalette)); + fogLayer = new TerrainSpriteLayer(w, wr, fogSheet, fogBlend, wr.Palette(info.FogPalette)); } Edges GetEdges(MPos uv, Func isVisible) @@ -209,177 +216,71 @@ namespace OpenRA.Mods.Common.Traits return info.UseExtendedIndex ? edge ^ ucorner : edge & Edges.AllCorners; } + void DirtyCells(IEnumerable cells) + { + cellsDirty.UnionWith(cells); + } + public void RenderShroud(WorldRenderer wr, Shroud shroud) { - Update(shroud); - Render(wr.Viewport.VisibleCells); - } - - void Update(Shroud newShroud) - { - if (currentShroud != newShroud) + if (currentShroud != shroud) { if (currentShroud != null) - currentShroud.CellsChanged -= MarkCellsDirty; + currentShroud.CellsChanged -= DirtyCells; - if (newShroud != null) + if (shroud != null) { - shroudDirty.Clear(true); - newShroud.CellsChanged += MarkCellsDirty; - } + shroud.CellsChanged += DirtyCells; - cellsDirty.Clear(); - cellsAndNeighborsDirty.Clear(); - - currentShroud = newShroud; - } - - if (currentShroud != null) - { - mapBorderShroudIsCached = false; - - // We need to mark newly dirtied areas of the shroud. - // Expand the dirty area to cover the neighboring cells, since shroud is affected by neighboring cells. - foreach (var cell in cellsDirty) - { - cellsAndNeighborsDirty.Add(cell); - foreach (var direction in CVec.Directions) - cellsAndNeighborsDirty.Add(cell + direction); - } - - foreach (var cell in cellsAndNeighborsDirty) - shroudDirty[cell] = true; - - cellsDirty.Clear(); - cellsAndNeighborsDirty.Clear(); - } - else if (!mapBorderShroudIsCached) - { - mapBorderShroudIsCached = true; - CacheMapBorderShroud(); - } - } - - void MarkCellsDirty(IEnumerable cellsChanged) - { - // Mark changed cells as being out of date. - // We don't want to do anything more than this for several performance reasons: - // - If the cells remain off-screen for a long time, they may change several times before we next view - // them, so calculating their new vertices is wasted effort since we may recalculate them again before we - // even get a chance to render them. - // - Cells tend to be invalidated in groups (imagine as a unit moves, it advances a wave of sight and - // leaves a trail of fog filling in behind). If we recalculated a cell and its neighbors when the first - // cell in a group changed, many cells would be recalculated again when the second cell, right next to the - // first, is updated. In fact we might do on the order of 3x the work we needed to! - cellsDirty.UnionWith(cellsChanged); - } - - void CacheMapBorderShroud() - { - // Cache the whole of the map border shroud ahead of time, since it never changes. - Func mapContains = map.Contains; - foreach (var uv in map.AllCells.MapCoords) - { - var offset = VertexArrayOffset(uv); - var edges = GetEdges(uv, mapContains); - var tileInfo = tileInfos[uv]; - CacheTile(uv, offset, edges, tileInfo, shroudSprites, shroudVertices, shroudPalette, shroudSpriteLayer); - CacheTile(uv, offset, edges, tileInfo, fogSprites, fogVertices, fogPalette, fogSpriteLayer); - } - } - - void Render(CellRegion visibleRegion) - { - // Due to diamond tile staggering, we need to expand the cordon to get full shroud coverage. - if (map.TileShape == TileShape.Diamond) - visibleRegion = CellRegion.Expand(visibleRegion, 1); - - if (currentShroud == null) - RenderMapBorderShroud(visibleRegion); - else - RenderPlayerShroud(visibleRegion); - } - - void RenderMapBorderShroud(CellRegion visibleRegion) - { - // The map border shroud only affects the map border. If none of the visible cells are on the border, then - // we don't need to render anything and can bail early for performance. - if (CellRegion.Expand(map.CellsInsideBounds, -1).Contains(visibleRegion)) - return; - - // Render the shroud that just encroaches at the map border. This shroud is always fully cached, so we can - // just render straight from the cache. - foreach (var uv in visibleRegion.MapCoords) - { - var offset = VertexArrayOffset(uv); - RenderCachedTile(shroudSpriteLayer[uv], shroudVertices, offset); - RenderCachedTile(fogSpriteLayer[uv], fogVertices, offset); - } - } - - void RenderPlayerShroud(CellRegion visibleRegion) - { - // Render the shroud by drawing the appropriate tile over each cell that is visible on-screen. - // For performance we keep a cache tiles we have drawn previously so we don't have to recalculate the - // vertices for tiles every frame, since this is costly. - // Any shroud marked as dirty has either never been calculated, or has changed since we last drew that - // tile. We will calculate the vertices for that tile and cache them before drawing it. - // Any shroud that is not marked as dirty means our cached tile is still correct - we can just draw the - // cached vertices. - var visibleUnderShroud = currentShroud.IsExploredTest(visibleRegion); - var visibleUnderFog = currentShroud.IsVisibleTest(visibleRegion); - foreach (var uv in visibleRegion.MapCoords) - { - var offset = VertexArrayOffset(uv); - if (shroudDirty[uv]) - { - shroudDirty[uv] = false; - RenderDirtyTile(uv, offset, visibleUnderShroud, shroudSprites, shroudVertices, shroudPalette, shroudSpriteLayer); - RenderDirtyTile(uv, offset, visibleUnderFog, fogSprites, fogVertices, fogPalette, fogSpriteLayer); + // Needs the anonymous function to ensure the correct overload is chosen + visibleUnderShroud = uv => currentShroud.IsExplored(uv); + visibleUnderFog = uv => currentShroud.IsVisible(uv); } else { - RenderCachedTile(shroudSpriteLayer[uv], shroudVertices, offset); - RenderCachedTile(fogSpriteLayer[uv], fogVertices, offset); + visibleUnderShroud = map.Contains; + visibleUnderFog = map.Contains; } + + currentShroud = shroud; + DirtyCells(map.CellsInsideBounds); } - } - int VertexArrayOffset(MPos uv) - { - return 4 * (uv.V * map.MapSize.X + uv.U); - } - - void RenderDirtyTile(MPos uv, int offset, Func isVisible, - Sprite[] sprites, Vertex[] vertices, PaletteReference palette, CellLayer spriteLayer) - { - var tile = tileInfos[uv]; - var edges = GetEdges(uv, isVisible); - var sprite = CacheTile(uv, offset, edges, tile, sprites, vertices, palette, spriteLayer); - RenderCachedTile(sprite, vertices, offset); - } - - void RenderCachedTile(Sprite sprite, Vertex[] vertices, int offset) - { - if (sprite != null) - Game.Renderer.WorldSpriteRenderer.DrawSprite(sprite, vertices, offset); - } - - Sprite CacheTile(MPos uv, int offset, Edges edges, TileInfo tileInfo, - Sprite[] sprites, Vertex[] vertices, PaletteReference palette, CellLayer spriteLayer) - { - var sprite = GetSprite(sprites, edges, tileInfo.Variant); - if (sprite != null) + // We need to update newly dirtied areas of the shroud. + // Expand the dirty area to cover the neighboring cells, since shroud is affected by neighboring cells. + foreach (var cell in cellsDirty) { - var size = sprite.Size; - var location = tileInfo.ScreenPosition - 0.5f * size; - OpenRA.Graphics.Util.FastCreateQuad( - vertices, location + sprite.FractionalOffset * size, - sprite, palette.TextureIndex, offset, size); + cellsAndNeighborsDirty.Add(cell); + foreach (var direction in CVec.Directions) + cellsAndNeighborsDirty.Add(cell + direction); } - spriteLayer[uv] = sprite; - return sprite; + foreach (var cell in cellsAndNeighborsDirty) + { + var uv = cell.ToMPos(map.TileShape); + if (!map.Contains(uv)) + continue; + + var tileInfo = tileInfos[uv]; + var shroudSprite = GetSprite(shroudSprites, GetEdges(uv, visibleUnderShroud), tileInfo.Variant); + var shroudPos = tileInfo.ScreenPosition; + if (shroudSprite != null) + shroudPos += shroudSprite.Offset - 0.5f * shroudSprite.Size; + + var fogSprite = GetSprite(fogSprites, GetEdges(uv, visibleUnderFog), tileInfo.Variant); + var fogPos = tileInfo.ScreenPosition; + if (fogSprite != null) + fogPos += fogSprite.Offset - 0.5f * fogSprite.Size; + + shroudLayer.Update(uv, shroudSprite, shroudPos); + fogLayer.Update(uv, fogSprite, fogPos); + } + + cellsDirty.Clear(); + cellsAndNeighborsDirty.Clear(); + + fogLayer.Draw(wr.Viewport); + shroudLayer.Draw(wr.Viewport); } Sprite GetSprite(Sprite[] sprites, Edges edges, int variant) @@ -389,5 +290,16 @@ namespace OpenRA.Mods.Common.Traits return sprites[variant * variantStride + edgesToSpriteIndexOffset[(byte)edges]]; } + + bool disposed; + public void Disposing(Actor self) + { + if (disposed) + return; + + shroudLayer.Dispose(); + fogLayer.Dispose(); + disposed = true; + } } }