Rework shroud rendering using TerrainSpriteLayer.
This commit is contained in:
@@ -340,7 +340,7 @@ namespace OpenRA.Traits
|
|||||||
return IsVisible(uv);
|
return IsVisible(uv);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IsVisible(MPos uv)
|
public bool IsVisible(MPos uv)
|
||||||
{
|
{
|
||||||
if (!map.Contains(uv))
|
if (!map.Contains(uv))
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -10,7 +10,12 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Drawing.Imaging;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using OpenRA.Graphics;
|
using OpenRA.Graphics;
|
||||||
|
using OpenRA.Primitives;
|
||||||
using OpenRA.Traits;
|
using OpenRA.Traits;
|
||||||
|
|
||||||
namespace OpenRA.Mods.Common.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 object Create(ActorInitializer init) { return new ShroudRenderer(init.World, this); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ShroudRenderer : IRenderShroud, IWorldLoaded
|
public sealed class ShroudRenderer : IRenderShroud, IWorldLoaded, INotifyActorDisposing
|
||||||
{
|
{
|
||||||
[Flags]
|
[Flags]
|
||||||
enum Edges : byte
|
enum Edges : byte
|
||||||
@@ -85,17 +90,13 @@ namespace OpenRA.Mods.Common.Traits
|
|||||||
readonly byte[] edgesToSpriteIndexOffset;
|
readonly byte[] edgesToSpriteIndexOffset;
|
||||||
|
|
||||||
readonly CellLayer<TileInfo> tileInfos;
|
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 Sprite[] fogSprites, shroudSprites;
|
||||||
readonly CellLayer<Sprite> fogSpriteLayer, shroudSpriteLayer;
|
readonly HashSet<CPos> cellsDirty = new HashSet<CPos>();
|
||||||
PaletteReference fogPalette, shroudPalette;
|
readonly HashSet<CPos> cellsAndNeighborsDirty = new HashSet<CPos>();
|
||||||
|
|
||||||
Shroud currentShroud;
|
Shroud currentShroud;
|
||||||
bool mapBorderShroudIsCached;
|
Func<MPos, bool> visibleUnderShroud, visibleUnderFog;
|
||||||
|
TerrainSpriteLayer shroudLayer, fogLayer;
|
||||||
|
|
||||||
public ShroudRenderer(World world, ShroudRendererInfo info)
|
public ShroudRenderer(World world, ShroudRendererInfo info)
|
||||||
{
|
{
|
||||||
@@ -115,14 +116,6 @@ namespace OpenRA.Mods.Common.Traits
|
|||||||
map = world.Map;
|
map = world.Map;
|
||||||
|
|
||||||
tileInfos = new CellLayer<TileInfo>(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
|
// Load sprite variants
|
||||||
var variantCount = info.ShroudVariants.Length;
|
var variantCount = info.ShroudVariants.Length;
|
||||||
@@ -170,14 +163,28 @@ namespace OpenRA.Mods.Common.Traits
|
|||||||
tileInfos[uv] = new TileInfo(screen, variant);
|
tileInfos[uv] = new TileInfo(screen, variant);
|
||||||
}
|
}
|
||||||
|
|
||||||
fogPalette = wr.Palette(info.FogPalette);
|
DirtyCells(map.AllCells);
|
||||||
shroudPalette = wr.Palette(info.ShroudPalette);
|
visibleUnderShroud = map.Contains;
|
||||||
|
visibleUnderFog = map.Contains;
|
||||||
|
|
||||||
wr.PaletteInvalidated += () =>
|
var shroudSheet = shroudSprites[0].Sheet;
|
||||||
{
|
if (shroudSprites.Any(s => s.Sheet != shroudSheet))
|
||||||
mapBorderShroudIsCached = false;
|
throw new InvalidDataException("Shroud sprites span multiple sheets. Try loading their sequences earlier.");
|
||||||
MarkCellsDirty(map.AllCells);
|
|
||||||
};
|
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)
|
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;
|
return info.UseExtendedIndex ? edge ^ ucorner : edge & Edges.AllCorners;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DirtyCells(IEnumerable<CPos> cells)
|
||||||
|
{
|
||||||
|
cellsDirty.UnionWith(cells);
|
||||||
|
}
|
||||||
|
|
||||||
public void RenderShroud(WorldRenderer wr, Shroud shroud)
|
public void RenderShroud(WorldRenderer wr, Shroud shroud)
|
||||||
{
|
{
|
||||||
Update(shroud);
|
if (currentShroud != shroud)
|
||||||
Render(wr.Viewport.VisibleCells);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Update(Shroud newShroud)
|
|
||||||
{
|
|
||||||
if (currentShroud != newShroud)
|
|
||||||
{
|
{
|
||||||
if (currentShroud != null)
|
if (currentShroud != null)
|
||||||
currentShroud.CellsChanged -= MarkCellsDirty;
|
currentShroud.CellsChanged -= DirtyCells;
|
||||||
|
|
||||||
if (newShroud != null)
|
if (shroud != null)
|
||||||
{
|
{
|
||||||
shroudDirty.Clear(true);
|
shroud.CellsChanged += DirtyCells;
|
||||||
newShroud.CellsChanged += MarkCellsDirty;
|
|
||||||
}
|
|
||||||
|
|
||||||
cellsDirty.Clear();
|
// Needs the anonymous function to ensure the correct overload is chosen
|
||||||
cellsAndNeighborsDirty.Clear();
|
visibleUnderShroud = uv => currentShroud.IsExplored(uv);
|
||||||
|
visibleUnderFog = uv => currentShroud.IsVisible(uv);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
RenderCachedTile(shroudSpriteLayer[uv], shroudVertices, offset);
|
visibleUnderShroud = map.Contains;
|
||||||
RenderCachedTile(fogSpriteLayer[uv], fogVertices, offset);
|
visibleUnderFog = map.Contains;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentShroud = shroud;
|
||||||
|
DirtyCells(map.CellsInsideBounds);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
int VertexArrayOffset(MPos uv)
|
// 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.
|
||||||
return 4 * (uv.V * map.MapSize.X + uv.U);
|
foreach (var cell in cellsDirty)
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
var size = sprite.Size;
|
cellsAndNeighborsDirty.Add(cell);
|
||||||
var location = tileInfo.ScreenPosition - 0.5f * size;
|
foreach (var direction in CVec.Directions)
|
||||||
OpenRA.Graphics.Util.FastCreateQuad(
|
cellsAndNeighborsDirty.Add(cell + direction);
|
||||||
vertices, location + sprite.FractionalOffset * size,
|
|
||||||
sprite, palette.TextureIndex, offset, size);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
spriteLayer[uv] = sprite;
|
foreach (var cell in cellsAndNeighborsDirty)
|
||||||
return sprite;
|
{
|
||||||
|
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)
|
Sprite GetSprite(Sprite[] sprites, Edges edges, int variant)
|
||||||
@@ -389,5 +290,16 @@ namespace OpenRA.Mods.Common.Traits
|
|||||||
|
|
||||||
return sprites[variant * variantStride + edgesToSpriteIndexOffset[(byte)edges]];
|
return sprites[variant * variantStride + edgesToSpriteIndexOffset[(byte)edges]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool disposed;
|
||||||
|
public void Disposing(Actor self)
|
||||||
|
{
|
||||||
|
if (disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
shroudLayer.Dispose();
|
||||||
|
fogLayer.Dispose();
|
||||||
|
disposed = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user