Rework shroud rendering using TerrainSpriteLayer.

This commit is contained in:
Paul Chote
2015-05-31 22:40:12 +01:00
parent 9882a6e34d
commit cac7ec39f4
2 changed files with 92 additions and 180 deletions

View File

@@ -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;

View File

@@ -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<TileInfo> tileInfos;
readonly CellLayer<bool> shroudDirty;
readonly HashSet<CPos> cellsDirty;
readonly HashSet<CPos> cellsAndNeighborsDirty;
readonly Vertex[] fogVertices, shroudVertices;
readonly Sprite[] fogSprites, shroudSprites;
readonly CellLayer<Sprite> fogSpriteLayer, shroudSpriteLayer;
PaletteReference fogPalette, shroudPalette;
readonly HashSet<CPos> cellsDirty = new HashSet<CPos>();
readonly HashSet<CPos> cellsAndNeighborsDirty = new HashSet<CPos>();
Shroud currentShroud;
bool mapBorderShroudIsCached;
Func<MPos, bool> 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<TileInfo>(map);
shroudDirty = new CellLayer<bool>(map);
cellsDirty = new HashSet<CPos>();
cellsAndNeighborsDirty = new HashSet<CPos>();
var verticesLength = map.MapSize.X * map.MapSize.Y * 4;
fogVertices = new Vertex[verticesLength];
shroudVertices = new Vertex[verticesLength];
fogSpriteLayer = new CellLayer<Sprite>(map);
shroudSpriteLayer = new CellLayer<Sprite>(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<MPos, bool> isVisible)
@@ -209,177 +216,71 @@ namespace OpenRA.Mods.Common.Traits
return info.UseExtendedIndex ? edge ^ ucorner : edge & Edges.AllCorners;
}
void DirtyCells(IEnumerable<CPos> 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<CPos> 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<MPos, bool> 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<MPos, bool> isVisible,
Sprite[] sprites, Vertex[] vertices, PaletteReference palette, CellLayer<Sprite> 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<Sprite> 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;
}
}
}