The render bounds for an actor now include the area covered by bibs, shadows, and any other widgets. In many cases this area is much larger than we really want to consider for tooltips and mouse selection. An optional Margin is added to Selectable to support cases like infantry, where we want the mouse area of the actor to be larger than the drawn selection box.
321 lines
10 KiB
C#
321 lines
10 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright 2007-2017 The OpenRA Developers (see AUTHORS)
|
|
* This file is part of OpenRA, which is free software. It is made
|
|
* available to you under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation, either version 3 of
|
|
* the License, or (at your option) any later version. For more
|
|
* information, see COPYING.
|
|
*/
|
|
#endregion
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Drawing;
|
|
using System.Linq;
|
|
using OpenRA.Effects;
|
|
using OpenRA.Traits;
|
|
|
|
namespace OpenRA.Graphics
|
|
{
|
|
public sealed class WorldRenderer : IDisposable
|
|
{
|
|
public static readonly Func<IRenderable, int> RenderableScreenZPositionComparisonKey =
|
|
r => ZPosition(r.Pos, r.ZOffset);
|
|
|
|
public readonly Size TileSize;
|
|
public readonly int TileScale;
|
|
public readonly World World;
|
|
public readonly Theater Theater;
|
|
public Viewport Viewport { get; private set; }
|
|
|
|
public event Action PaletteInvalidated = null;
|
|
|
|
readonly HardwarePalette palette = new HardwarePalette();
|
|
readonly Dictionary<string, PaletteReference> palettes = new Dictionary<string, PaletteReference>();
|
|
readonly TerrainRenderer terrainRenderer;
|
|
readonly Lazy<DebugVisualizations> debugVis;
|
|
readonly Func<string, PaletteReference> createPaletteReference;
|
|
readonly bool enableDepthBuffer;
|
|
|
|
internal WorldRenderer(ModData modData, World world)
|
|
{
|
|
World = world;
|
|
TileSize = World.Map.Grid.TileSize;
|
|
TileScale = World.Map.Grid.Type == MapGridType.RectangularIsometric ? 1448 : 1024;
|
|
Viewport = new Viewport(this, world.Map);
|
|
|
|
createPaletteReference = CreatePaletteReference;
|
|
|
|
var mapGrid = modData.Manifest.Get<MapGrid>();
|
|
enableDepthBuffer = mapGrid.EnableDepthBuffer;
|
|
|
|
foreach (var pal in world.TraitDict.ActorsWithTrait<ILoadsPalettes>())
|
|
pal.Trait.LoadPalettes(this);
|
|
|
|
foreach (var p in world.Players)
|
|
UpdatePalettesForPlayer(p.InternalName, p.Color, false);
|
|
|
|
palette.Initialize();
|
|
|
|
Theater = new Theater(world.Map.Rules.TileSet);
|
|
terrainRenderer = new TerrainRenderer(world, this);
|
|
|
|
debugVis = Exts.Lazy(() => world.WorldActor.TraitOrDefault<DebugVisualizations>());
|
|
}
|
|
|
|
public void UpdatePalettesForPlayer(string internalName, HSLColor color, bool replaceExisting)
|
|
{
|
|
foreach (var pal in World.WorldActor.TraitsImplementing<ILoadsPlayerPalettes>())
|
|
pal.LoadPlayerPalettes(this, internalName, color, replaceExisting);
|
|
}
|
|
|
|
PaletteReference CreatePaletteReference(string name)
|
|
{
|
|
var pal = palette.GetPalette(name);
|
|
return new PaletteReference(name, palette.GetPaletteIndex(name), pal, palette);
|
|
}
|
|
|
|
public PaletteReference Palette(string name) { return palettes.GetOrAdd(name, createPaletteReference); }
|
|
public void AddPalette(string name, ImmutablePalette pal, bool allowModifiers = false, bool allowOverwrite = false)
|
|
{
|
|
if (allowOverwrite && palette.Contains(name))
|
|
ReplacePalette(name, pal);
|
|
else
|
|
{
|
|
var oldHeight = palette.Height;
|
|
palette.AddPalette(name, pal, allowModifiers);
|
|
|
|
if (oldHeight != palette.Height && PaletteInvalidated != null)
|
|
PaletteInvalidated();
|
|
}
|
|
}
|
|
|
|
public void ReplacePalette(string name, IPalette pal)
|
|
{
|
|
palette.ReplacePalette(name, pal);
|
|
|
|
// Update cached PlayerReference if one exists
|
|
if (palettes.ContainsKey(name))
|
|
palettes[name].Palette = pal;
|
|
}
|
|
|
|
List<IFinalizedRenderable> GenerateRenderables()
|
|
{
|
|
var actors = World.ScreenMap.RenderableActorsInBox(Viewport.TopLeft, Viewport.BottomRight).Append(World.WorldActor);
|
|
if (World.RenderPlayer != null)
|
|
actors = actors.Append(World.RenderPlayer.PlayerActor);
|
|
|
|
var worldRenderables = actors.SelectMany(a => a.Render(this));
|
|
if (World.OrderGenerator != null)
|
|
worldRenderables = worldRenderables.Concat(World.OrderGenerator.Render(this, World));
|
|
|
|
// Unpartitioned effects
|
|
worldRenderables = worldRenderables.Concat(World.UnpartitionedEffects.SelectMany(e => e.Render(this)));
|
|
|
|
// Partitioned, currently on-screen effects
|
|
var effectRenderables = World.ScreenMap.RenderableEffectsInBox(Viewport.TopLeft, Viewport.BottomRight);
|
|
worldRenderables = worldRenderables.Concat(effectRenderables.SelectMany(e => e.Render(this)));
|
|
|
|
worldRenderables = worldRenderables.OrderBy(RenderableScreenZPositionComparisonKey);
|
|
|
|
Game.Renderer.WorldModelRenderer.BeginFrame();
|
|
var renderables = worldRenderables.Select(r => r.PrepareRender(this)).ToList();
|
|
Game.Renderer.WorldModelRenderer.EndFrame();
|
|
|
|
return renderables;
|
|
}
|
|
|
|
public void Draw()
|
|
{
|
|
if (World.WorldActor.Disposed)
|
|
return;
|
|
|
|
if (debugVis.Value != null)
|
|
{
|
|
Game.Renderer.WorldSpriteRenderer.SetDepthPreviewEnabled(debugVis.Value.DepthBuffer);
|
|
Game.Renderer.WorldRgbaSpriteRenderer.SetDepthPreviewEnabled(debugVis.Value.DepthBuffer);
|
|
Game.Renderer.WorldRgbaColorRenderer.SetDepthPreviewEnabled(debugVis.Value.DepthBuffer);
|
|
}
|
|
|
|
RefreshPalette();
|
|
|
|
var renderables = GenerateRenderables();
|
|
var bounds = Viewport.GetScissorBounds(World.Type != WorldType.Editor);
|
|
Game.Renderer.EnableScissor(bounds);
|
|
|
|
if (enableDepthBuffer)
|
|
Game.Renderer.Device.EnableDepthBuffer();
|
|
|
|
terrainRenderer.Draw(this, Viewport);
|
|
Game.Renderer.Flush();
|
|
|
|
for (var i = 0; i < renderables.Count; i++)
|
|
renderables[i].Render(this);
|
|
|
|
if (enableDepthBuffer)
|
|
Game.Renderer.ClearDepthBuffer();
|
|
|
|
foreach (var a in World.ActorsWithTrait<IRenderAboveWorld>())
|
|
if (a.Actor.IsInWorld && !a.Actor.Disposed)
|
|
a.Trait.RenderAboveWorld(a.Actor, this);
|
|
|
|
var renderShroud = World.RenderPlayer != null ? World.RenderPlayer.Shroud : null;
|
|
|
|
if (enableDepthBuffer)
|
|
Game.Renderer.ClearDepthBuffer();
|
|
|
|
foreach (var a in World.ActorsWithTrait<IRenderShroud>())
|
|
a.Trait.RenderShroud(renderShroud, this);
|
|
|
|
if (enableDepthBuffer)
|
|
Game.Renderer.Device.DisableDepthBuffer();
|
|
|
|
Game.Renderer.DisableScissor();
|
|
|
|
var aboveShroud = World.ActorsWithTrait<IRenderAboveShroud>().Where(a => a.Actor.IsInWorld && !a.Actor.Disposed)
|
|
.SelectMany(a => a.Trait.RenderAboveShroud(a.Actor, this));
|
|
|
|
var aboveShroudSelected = World.Selection.Actors.Where(a => !a.Disposed)
|
|
.SelectMany(a => a.TraitsImplementing<IRenderAboveShroudWhenSelected>()
|
|
.SelectMany(t => t.RenderAboveShroud(a, this)));
|
|
|
|
var aboveShroudEffects = World.Effects.Select(e => e as IEffectAboveShroud)
|
|
.Where(e => e != null)
|
|
.SelectMany(e => e.RenderAboveShroud(this));
|
|
|
|
var aboveShroudOrderGenerator = SpriteRenderable.None;
|
|
if (World.OrderGenerator != null)
|
|
aboveShroudOrderGenerator = World.OrderGenerator.RenderAboveShroud(this, World);
|
|
|
|
Game.Renderer.WorldModelRenderer.BeginFrame();
|
|
var finalOverlayRenderables = aboveShroud
|
|
.Concat(aboveShroudSelected)
|
|
.Concat(aboveShroudEffects)
|
|
.Concat(aboveShroudOrderGenerator)
|
|
.Select(r => r.PrepareRender(this))
|
|
.ToList();
|
|
Game.Renderer.WorldModelRenderer.EndFrame();
|
|
|
|
// HACK: Keep old grouping behaviour
|
|
foreach (var g in finalOverlayRenderables.GroupBy(prs => prs.GetType()))
|
|
foreach (var r in g)
|
|
r.Render(this);
|
|
|
|
if (debugVis.Value != null && debugVis.Value.RenderGeometry)
|
|
{
|
|
for (var i = 0; i < renderables.Count; i++)
|
|
renderables[i].RenderDebugGeometry(this);
|
|
|
|
foreach (var g in finalOverlayRenderables.GroupBy(prs => prs.GetType()))
|
|
foreach (var r in g)
|
|
r.RenderDebugGeometry(this);
|
|
}
|
|
|
|
if (debugVis.Value != null && debugVis.Value.ScreenMap)
|
|
{
|
|
foreach (var r in World.ScreenMap.RenderBounds(World.RenderPlayer))
|
|
Game.Renderer.WorldRgbaColorRenderer.DrawRect(
|
|
new float3(r.Left, r.Top, r.Bottom),
|
|
new float3(r.Right, r.Bottom, r.Bottom),
|
|
1 / Viewport.Zoom, Color.MediumSpringGreen);
|
|
|
|
foreach (var r in World.ScreenMap.MouseBounds(World.RenderPlayer))
|
|
Game.Renderer.WorldRgbaColorRenderer.DrawRect(
|
|
new float3(r.Left, r.Top, r.Bottom),
|
|
new float3(r.Right, r.Bottom, r.Bottom),
|
|
1 / Viewport.Zoom, Color.OrangeRed);
|
|
}
|
|
|
|
Game.Renderer.Flush();
|
|
}
|
|
|
|
public void RefreshPalette()
|
|
{
|
|
palette.ApplyModifiers(World.WorldActor.TraitsImplementing<IPaletteModifier>());
|
|
Game.Renderer.SetPalette(palette);
|
|
}
|
|
|
|
// Conversion between world and screen coordinates
|
|
public float2 ScreenPosition(WPos pos)
|
|
{
|
|
return new float2((float)TileSize.Width * pos.X / TileScale, (float)TileSize.Height * (pos.Y - pos.Z) / TileScale);
|
|
}
|
|
|
|
public float3 Screen3DPosition(WPos pos)
|
|
{
|
|
var z = ZPosition(pos, 0) * (float)TileSize.Height / TileScale;
|
|
return new float3((float)TileSize.Width * pos.X / TileScale, (float)TileSize.Height * (pos.Y - pos.Z) / TileScale, z);
|
|
}
|
|
|
|
public int2 ScreenPxPosition(WPos pos)
|
|
{
|
|
// Round to nearest pixel
|
|
var px = ScreenPosition(pos);
|
|
return new int2((int)Math.Round(px.X), (int)Math.Round(px.Y));
|
|
}
|
|
|
|
public float3 Screen3DPxPosition(WPos pos)
|
|
{
|
|
// Round to nearest pixel
|
|
var px = Screen3DPosition(pos);
|
|
return new float3((float)Math.Round(px.X), (float)Math.Round(px.Y), px.Z);
|
|
}
|
|
|
|
// For scaling vectors to pixel sizes in the model renderer
|
|
public float3 ScreenVectorComponents(WVec vec)
|
|
{
|
|
return new float3(
|
|
(float)TileSize.Width * vec.X / TileScale,
|
|
(float)TileSize.Height * (vec.Y - vec.Z) / TileScale,
|
|
(float)TileSize.Height * vec.Z / TileScale);
|
|
}
|
|
|
|
// For scaling vectors to pixel sizes in the model renderer
|
|
public float[] ScreenVector(WVec vec)
|
|
{
|
|
var xyz = ScreenVectorComponents(vec);
|
|
return new[] { xyz.X, xyz.Y, xyz.Z, 1f };
|
|
}
|
|
|
|
public int2 ScreenPxOffset(WVec vec)
|
|
{
|
|
// Round to nearest pixel
|
|
var xyz = ScreenVectorComponents(vec);
|
|
return new int2((int)Math.Round(xyz.X), (int)Math.Round(xyz.Y));
|
|
}
|
|
|
|
public float ScreenZPosition(WPos pos, int offset)
|
|
{
|
|
return ZPosition(pos, offset) * (float)TileSize.Height / TileScale;
|
|
}
|
|
|
|
static int ZPosition(WPos pos, int offset)
|
|
{
|
|
return pos.Y + pos.Z + offset;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a position in the world that is projected to the given screen position.
|
|
/// There are many possible world positions, and the returned value chooses the value with no elevation.
|
|
/// </summary>
|
|
public WPos ProjectedPosition(int2 screenPx)
|
|
{
|
|
return new WPos(TileScale * screenPx.X / TileSize.Width, TileScale * screenPx.Y / TileSize.Height, 0);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
// HACK: Disposing the world from here violates ownership
|
|
// but the WorldRenderer lifetime matches the disposal
|
|
// behavior we want for the world, and the root object setup
|
|
// is so horrible that doing it properly would be a giant mess.
|
|
World.Dispose();
|
|
|
|
palette.Dispose();
|
|
Theater.Dispose();
|
|
terrainRenderer.Dispose();
|
|
}
|
|
}
|
|
}
|