diff --git a/OpenRA.Game/Graphics/SpriteRenderer.cs b/OpenRA.Game/Graphics/SpriteRenderer.cs index b5ec0c2e60..356819eb2f 100644 --- a/OpenRA.Game/Graphics/SpriteRenderer.cs +++ b/OpenRA.Game/Graphics/SpriteRenderer.cs @@ -8,6 +8,7 @@ */ #endregion +using System; using System.Drawing; namespace OpenRA.Graphics @@ -49,6 +50,17 @@ namespace OpenRA.Graphics } } + void SetRenderStateForSprite(Sprite s) + { + renderer.CurrentBatchRenderer = this; + + if (s.Sheet != currentSheet || s.BlendMode != currentBlend || nv + 4 > renderer.TempBufferSize) + Flush(); + + currentBlend = s.BlendMode; + currentSheet = s.Sheet; + } + public void DrawSprite(Sprite s, float2 location, PaletteReference pal) { DrawSprite(s, location, pal.TextureIndex, s.Size); @@ -61,19 +73,7 @@ namespace OpenRA.Graphics void DrawSprite(Sprite s, float2 location, float paletteTextureIndex, float2 size) { - renderer.CurrentBatchRenderer = this; - - if (s.Sheet != currentSheet) - Flush(); - - if (s.BlendMode != currentBlend) - Flush(); - - if (nv + 4 > renderer.TempBufferSize) - Flush(); - - currentBlend = s.BlendMode; - currentSheet = s.Sheet; + SetRenderStateForSprite(s); Util.FastCreateQuad(vertices, location + s.FractionalOffset * size, s, paletteTextureIndex, nv, size); nv += 4; } @@ -91,23 +91,18 @@ namespace OpenRA.Graphics public void DrawSprite(Sprite s, float2 a, float2 b, float2 c, float2 d) { - renderer.CurrentBatchRenderer = this; - - if (s.Sheet != currentSheet) - Flush(); - - if (s.BlendMode != currentBlend) - Flush(); - - if (nv + 4 > renderer.TempBufferSize) - Flush(); - - currentSheet = s.Sheet; - currentBlend = s.BlendMode; + SetRenderStateForSprite(s); Util.FastCreateQuad(vertices, a, b, c, d, s, 0, nv); nv += 4; } + public void DrawSprite(Sprite s, Vertex[] sourceVertices, int offset) + { + SetRenderStateForSprite(s); + Array.Copy(sourceVertices, offset, vertices, nv, 4); + nv += 4; + } + public void DrawVertexBuffer(IVertexBuffer buffer, int start, int length, PrimitiveType type, Sheet sheet) { shader.SetTexture("DiffuseTexture", sheet.GetTexture()); diff --git a/OpenRA.Mods.Common/Traits/World/ShroudRenderer.cs b/OpenRA.Mods.Common/Traits/World/ShroudRenderer.cs index 0251d4dd21..787e1f73d3 100644 --- a/OpenRA.Mods.Common/Traits/World/ShroudRenderer.cs +++ b/OpenRA.Mods.Common/Traits/World/ShroudRenderer.cs @@ -65,38 +65,35 @@ namespace OpenRA.Mods.Common.Traits All = Top | Right | Bottom | Left } - struct ShroudTile + struct TileInfo { public readonly float2 ScreenPosition; public readonly byte Variant; - public Sprite Fog; - public Sprite Shroud; - - public ShroudTile(float2 screenPosition, byte variant) + public TileInfo(float2 screenPosition, byte variant) { ScreenPosition = screenPosition; Variant = variant; - - Fog = null; - Shroud = null; } } readonly ShroudRendererInfo info; - readonly Sprite[] shroudSprites, fogSprites; - readonly byte[] spriteMap; - readonly CellLayer tiles; - readonly byte variantStride; readonly Map map; readonly Edges notVisibleEdges; + readonly byte variantStride; + readonly byte[] edgesToSpriteIndexOffset; - bool clearedForNullShroud; - int lastShroudHash; - CellRegion updatedRegion; + readonly CellLayer tileInfos; + readonly CellLayer shroudDirty; + readonly Vertex[] fogVertices, shroudVertices; + readonly Sprite[] fogSprites, shroudSprites; + readonly CellLayer fogSpriteLayer, shroudSpriteLayer; PaletteReference fogPalette, shroudPalette; + Shroud currentShroud; + bool mapBorderShroudIsCached; + public ShroudRenderer(World world, ShroudRendererInfo info) { if (info.ShroudVariants.Length != info.FogVariants.Length) @@ -114,7 +111,13 @@ namespace OpenRA.Mods.Common.Traits this.info = info; map = world.Map; - tiles = new CellLayer(map); + tileInfos = new CellLayer(map); + shroudDirty = new CellLayer(map); + 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; @@ -141,16 +144,31 @@ namespace OpenRA.Mods.Common.Traits } // Mapping of shrouded directions -> sprite index - spriteMap = new byte[(byte)(info.UseExtendedIndex ? Edges.All : Edges.AllCorners) + 1]; + edgesToSpriteIndexOffset = new byte[(byte)(info.UseExtendedIndex ? Edges.All : Edges.AllCorners) + 1]; for (var i = 0; i < info.Index.Length; i++) - spriteMap[info.Index[i]] = (byte)i; + edgesToSpriteIndexOffset[info.Index[i]] = (byte)i; if (info.OverrideFullShroud != null) - spriteMap[info.OverrideShroudIndex] = (byte)(variantStride - 1); + edgesToSpriteIndexOffset[info.OverrideShroudIndex] = (byte)(variantStride - 1); notVisibleEdges = info.UseExtendedIndex ? Edges.AllSides : Edges.AllCorners; } + public void WorldLoaded(World w, WorldRenderer wr) + { + // Initialize tile cache + // Adds a 1-cell border around the border to cover any sprites peeking outside the map + foreach (var uv in CellRegion.Expand(w.Map.Cells, 1).MapCoords) + { + var screen = wr.ScreenPosition(w.Map.CenterOfCell(uv.ToCPos(map))); + var variant = (byte)Game.CosmeticRandom.Next(info.ShroudVariants.Length); + tileInfos[uv] = new TileInfo(screen, variant); + } + + fogPalette = wr.Palette(info.FogPalette); + shroudPalette = wr.Palette(info.ShroudPalette); + } + Edges GetEdges(MPos uv, Func isVisible) { if (!isVisible(uv)) @@ -158,7 +176,7 @@ namespace OpenRA.Mods.Common.Traits var cell = uv.ToCPos(map); - // If a side is shrouded then we also count the corners + // If a side is shrouded then we also count the corners. var edge = Edges.None; if (!isVisible((cell + new CVec(0, -1)).ToMPos(map))) edge |= Edges.Top; if (!isVisible((cell + new CVec(1, 0)).ToMPos(map))) edge |= Edges.Right; @@ -180,125 +198,171 @@ namespace OpenRA.Mods.Common.Traits return info.UseExtendedIndex ? edge ^ ucorner : edge & Edges.AllCorners; } - Edges GetObserverEdges(CPos p) + public void RenderShroud(WorldRenderer wr, Shroud shroud) { - var u = Edges.None; - if (!map.Contains(p + new CVec(0, -1))) u |= Edges.Top; - if (!map.Contains(p + new CVec(1, 0))) u |= Edges.Right; - if (!map.Contains(p + new CVec(0, 1))) u |= Edges.Bottom; - if (!map.Contains(p + new CVec(-1, 0))) u |= Edges.Left; - - var ucorner = u & Edges.AllCorners; - if (!map.Contains(p + new CVec(-1, -1))) u |= Edges.TopLeft; - if (!map.Contains(p + new CVec(1, -1))) u |= Edges.TopRight; - if (!map.Contains(p + new CVec(1, 1))) u |= Edges.BottomRight; - if (!map.Contains(p + new CVec(-1, 1))) u |= Edges.BottomLeft; - - return info.UseExtendedIndex ? u ^ ucorner : u & Edges.AllCorners; + Update(shroud); + Render(wr.Viewport.VisibleCells); } - public void WorldLoaded(World w, WorldRenderer wr) + void Update(Shroud newShroud) { - // Initialize tile cache - // Adds a 1-cell border around the border to cover any sprites peeking outside the map - foreach (var uv in CellRegion.Expand(w.Map.Cells, 1).MapCoords) + if (currentShroud != newShroud) { - var screen = wr.ScreenPosition(w.Map.CenterOfCell(uv.ToCPos(map))); - var variant = (byte)Game.CosmeticRandom.Next(info.ShroudVariants.Length); - tiles[uv] = new ShroudTile(screen, variant); + if (currentShroud != null) + currentShroud.CellEntryChanged -= MarkCellAndNeighborsDirty; - // Set the cells outside the border so they don't need to be touched again - if (!map.Contains(uv)) + if (newShroud != null) { - var shroudTile = tiles[uv]; - shroudTile.Shroud = GetTile(shroudSprites, notVisibleEdges, variant); - tiles[uv] = shroudTile; + shroudDirty.Clear(true); + newShroud.CellEntryChanged += MarkCellAndNeighborsDirty; } + + currentShroud = newShroud; } - fogPalette = wr.Palette(info.FogPalette); - shroudPalette = wr.Palette(info.ShroudPalette); + if (currentShroud != null) + { + mapBorderShroudIsCached = false; + } + else if (!mapBorderShroudIsCached) + { + mapBorderShroudIsCached = true; + CacheMapBorderShroud(); + } } - Sprite GetTile(Sprite[] sprites, Edges edges, int variant) + void MarkCellAndNeighborsDirty(CPos cell) + { + // Mark this cell and its 8 neighbors 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! + shroudDirty[cell + new CVec(-1, -1)] = true; + shroudDirty[cell + new CVec(0, -1)] = true; + shroudDirty[cell + new CVec(1, -1)] = true; + shroudDirty[cell + new CVec(-1, 0)] = true; + shroudDirty[cell] = true; + shroudDirty[cell + new CVec(1, 0)] = true; + shroudDirty[cell + new CVec(-1, 1)] = true; + shroudDirty[cell + new CVec(0, 1)] = true; + shroudDirty[cell + new CVec(1, 1)] = true; + } + + 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 CellRegion.Expand(map.Cells, 1).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) + { + var renderRegion = CellRegion.Expand(visibleRegion, 1).MapCoords; + + if (currentShroud == null) + { + RenderMapBorderShroud(renderRegion); + return; + } + + RenderPlayerShroud(visibleRegion, renderRegion); + } + + void RenderMapBorderShroud(CellRegion.MapCoordsRegion renderRegion) + { + // 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 renderRegion) + { + var offset = VertexArrayOffset(uv); + RenderCachedTile(shroudSpriteLayer[uv], shroudVertices, offset); + RenderCachedTile(fogSpriteLayer[uv], fogVertices, offset); + } + } + + void RenderPlayerShroud(CellRegion visibleRegion, CellRegion.MapCoordsRegion renderRegion) + { + // 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 renderRegion) + { + 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); + } + else + { + RenderCachedTile(shroudSpriteLayer[uv], shroudVertices, offset); + RenderCachedTile(fogSpriteLayer[uv], fogVertices, offset); + } + } + } + + 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) + { + 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); + } + + spriteLayer[uv] = sprite; + return sprite; + } + + Sprite GetSprite(Sprite[] sprites, Edges edges, int variant) { if (edges == Edges.None) return null; - return sprites[variant * variantStride + spriteMap[(byte)edges]]; - } - - void Update(Shroud shroud, CellRegion region) - { - if (shroud != null) - { - // If the current shroud hasn't changed and we have already updated the specified area, we don't need to do anything. - if (lastShroudHash == shroud.Hash && !clearedForNullShroud && updatedRegion != null && updatedRegion.Contains(region)) - return; - - lastShroudHash = shroud.Hash; - clearedForNullShroud = false; - updatedRegion = region; - UpdateShroud(shroud); - } - else if (!clearedForNullShroud) - { - // We need to clear any applied shroud. - clearedForNullShroud = true; - updatedRegion = new CellRegion(map.TileShape, new CPos(0, 0), new CPos(-1, -1)); - UpdateNullShroud(); - } - } - - void UpdateShroud(Shroud shroud) - { - var visibleUnderShroud = shroud.IsExploredTest(updatedRegion); - var visibleUnderFog = shroud.IsVisibleTest(updatedRegion); - foreach (var uv in updatedRegion.MapCoords) - { - var shrouded = GetEdges(uv, visibleUnderShroud); - var fogged = GetEdges(uv, visibleUnderFog); - var shroudTile = tiles[uv]; - var variant = shroudTile.Variant; - shroudTile.Shroud = GetTile(shroudSprites, shrouded, variant); - shroudTile.Fog = GetTile(fogSprites, fogged, variant); - tiles[uv] = shroudTile; - } - } - - void UpdateNullShroud() - { - foreach (var cell in map.Cells) - { - var edges = GetObserverEdges(cell); - var shroudTile = tiles[cell]; - var variant = shroudTile.Variant; - shroudTile.Shroud = GetTile(shroudSprites, edges, variant); - shroudTile.Fog = GetTile(fogSprites, edges, variant); - tiles[cell] = shroudTile; - } - } - - public void RenderShroud(WorldRenderer wr, Shroud shroud) - { - Update(shroud, wr.Viewport.VisibleCells); - - foreach (var uv in CellRegion.Expand(wr.Viewport.VisibleCells, 1).MapCoords) - { - var t = tiles[uv]; - - if (t.Shroud != null) - { - var pos = t.ScreenPosition - 0.5f * t.Shroud.Size; - Game.Renderer.WorldSpriteRenderer.DrawSprite(t.Shroud, pos, shroudPalette); - } - - if (t.Fog != null) - { - var pos = t.ScreenPosition - 0.5f * t.Fog.Size; - Game.Renderer.WorldSpriteRenderer.DrawSprite(t.Fog, pos, fogPalette); - } - } + return sprites[variant * variantStride + edgesToSpriteIndexOffset[(byte)edges]]; } } }