diff --git a/OpenRA.Game/Game.cs b/OpenRA.Game/Game.cs index f1b9e2eec5..74b19e44dd 100644 --- a/OpenRA.Game/Game.cs +++ b/OpenRA.Game/Game.cs @@ -442,6 +442,7 @@ namespace OpenRA PerfHistory.Items["render_world"].HasNormalTick = false; PerfHistory.Items["render_widgets"].HasNormalTick = false; PerfHistory.Items["render_flip"].HasNormalTick = false; + PerfHistory.Items["terrain_lighting"].HasNormalTick = false; JoinLocal(); @@ -706,6 +707,7 @@ namespace OpenRA PerfHistory.Items["render_world"].Tick(); PerfHistory.Items["render_widgets"].Tick(); PerfHistory.Items["render_flip"].Tick(); + PerfHistory.Items["terrain_lighting"].Tick(); } static void Loop() diff --git a/OpenRA.Game/Graphics/SpriteRenderable.cs b/OpenRA.Game/Graphics/SpriteRenderable.cs index 6f228860bd..135553c7d9 100644 --- a/OpenRA.Game/Graphics/SpriteRenderable.cs +++ b/OpenRA.Game/Graphics/SpriteRenderable.cs @@ -75,7 +75,13 @@ namespace OpenRA.Graphics if (ignoreWorldTint) wsr.DrawSprite(sprite, ScreenPosition(wr), palette, scale * sprite.Size); else - wsr.DrawSpriteWithTint(sprite, ScreenPosition(wr), palette, scale * sprite.Size, tint); + { + var t = tint; + if (wr.TerrainLighting != null) + t *= wr.TerrainLighting.TintAt(pos); + + wsr.DrawSpriteWithTint(sprite, ScreenPosition(wr), palette, scale * sprite.Size, t); + } } public void RenderDebugGeometry(WorldRenderer wr) diff --git a/OpenRA.Game/Graphics/TerrainSpriteLayer.cs b/OpenRA.Game/Graphics/TerrainSpriteLayer.cs index a739b61bb3..47344a36f4 100644 --- a/OpenRA.Game/Graphics/TerrainSpriteLayer.cs +++ b/OpenRA.Game/Graphics/TerrainSpriteLayer.cs @@ -18,6 +18,8 @@ namespace OpenRA.Graphics { public sealed class TerrainSpriteLayer : IDisposable { + static readonly int[] CornerVertexMap = { 0, 1, 2, 2, 3, 0 }; + public readonly Sheet Sheet; public readonly BlendMode BlendMode; @@ -25,6 +27,7 @@ namespace OpenRA.Graphics readonly IVertexBuffer vertexBuffer; readonly Vertex[] vertices; + readonly bool[] ignoreTint; readonly HashSet dirtyRows = new HashSet(); readonly int rowStride; readonly bool restrictToBounds; @@ -50,6 +53,12 @@ namespace OpenRA.Graphics emptySprite = new Sprite(sheet, Rectangle.Empty, TextureChannel.Alpha); wr.PaletteInvalidated += UpdatePaletteIndices; + + if (wr.TerrainLighting != null) + { + ignoreTint = new bool[rowStride * map.MapSize.Y]; + wr.TerrainLighting.CellChanged += UpdateTint; + } } void UpdatePaletteIndices() @@ -59,7 +68,7 @@ namespace OpenRA.Graphics for (var i = 0; i < vertices.Length; i++) { var v = vertices[i]; - vertices[i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, palette.TextureIndex, v.C, new float3(v.R, v.G, v.B)); + vertices[i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, palette.TextureIndex, v.C, v.R, v.G, v.B); } for (var row = 0; row < map.MapSize.Y; row++) @@ -68,15 +77,15 @@ namespace OpenRA.Graphics public void Clear(CPos cell) { - Update(cell, null); + Update(cell, null, true); } public void Update(CPos cell, ISpriteSequence sequence, int frame) { - Update(cell, sequence.GetSprite(frame)); + Update(cell, sequence.GetSprite(frame), sequence.IgnoreWorldTint); } - public void Update(CPos cell, Sprite sprite) + public void Update(CPos cell, Sprite sprite, bool ignoreTint) { var xyz = float3.Zero; if (sprite != null) @@ -85,10 +94,50 @@ namespace OpenRA.Graphics xyz = worldRenderer.Screen3DPosition(cellOrigin) + sprite.Offset - 0.5f * sprite.Size; } - Update(cell.ToMPos(map.Grid.Type), sprite, xyz); + Update(cell.ToMPos(map.Grid.Type), sprite, xyz, ignoreTint); } - public void Update(MPos uv, Sprite sprite, float3 pos) + void UpdateTint(MPos uv) + { + var offset = rowStride * uv.V + 6 * uv.U; + if (ignoreTint[offset]) + { + var noTint = float3.Ones; + for (var i = 0; i < 6; i++) + { + var v = vertices[offset + i]; + vertices[offset + i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, palette.TextureIndex, v.C, noTint); + } + + return; + } + + // Allow the terrain tint to vary linearly across the cell to smooth out the staircase effect + // This is done by sampling the lighting the corners of the sprite, even though those pixels are + // transparent for isometric tiles + var tl = worldRenderer.TerrainLighting; + var pos = map.CenterOfCell(uv.ToCPos(map)); + var step = map.Grid.Type == MapGridType.RectangularIsometric ? 724 : 512; + var weights = new[] + { + tl.TintAt(pos + new WVec(-step, -step, 0)), + tl.TintAt(pos + new WVec(step, -step, 0)), + tl.TintAt(pos + new WVec(step, step, 0)), + tl.TintAt(pos + new WVec(-step, step, 0)) + }; + + // Apply tint directly to the underlying vertices + // This saves us from having to re-query the sprite information, which has not changed + for (var i = 0; i < 6; i++) + { + var v = vertices[offset + i]; + vertices[offset + i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, palette.TextureIndex, v.C, weights[CornerVertexMap[i]]); + } + + dirtyRows.Add(uv.V); + } + + public void Update(MPos uv, Sprite sprite, float3 pos, bool ignoreTint) { if (sprite != null) { @@ -108,6 +157,12 @@ namespace OpenRA.Graphics var offset = rowStride * uv.V + 6 * uv.U; Util.FastCreateQuad(vertices, pos, sprite, int2.Zero, palette.TextureIndex, offset, sprite.Size, float3.Ones); + if (worldRenderer.TerrainLighting != null) + { + this.ignoreTint[offset] = ignoreTint; + UpdateTint(uv); + } + dirtyRows.Add(uv.V); } @@ -149,6 +204,9 @@ namespace OpenRA.Graphics public void Dispose() { worldRenderer.PaletteInvalidated -= UpdatePaletteIndices; + if (worldRenderer.TerrainLighting != null) + worldRenderer.TerrainLighting.CellChanged -= UpdateTint; + vertexBuffer.Dispose(); } } diff --git a/OpenRA.Game/Graphics/WorldRenderer.cs b/OpenRA.Game/Graphics/WorldRenderer.cs index 5769666ce5..ab2a56dd53 100644 --- a/OpenRA.Game/Graphics/WorldRenderer.cs +++ b/OpenRA.Game/Graphics/WorldRenderer.cs @@ -28,6 +28,7 @@ namespace OpenRA.Graphics public readonly World World; public readonly Theater Theater; public Viewport Viewport { get; private set; } + public readonly ITerrainLighting TerrainLighting; public event Action PaletteInvalidated = null; @@ -68,6 +69,7 @@ namespace OpenRA.Graphics palette.Initialize(); Theater = new Theater(world.Map.Rules.TileSet); + TerrainLighting = world.WorldActor.TraitOrDefault(); terrainRenderer = world.WorldActor.TraitOrDefault(); debugVis = Exts.Lazy(() => world.WorldActor.TraitOrDefault()); diff --git a/OpenRA.Game/Traits/TraitsInterfaces.cs b/OpenRA.Game/Traits/TraitsInterfaces.cs index a01eafc669..1c11fba579 100644 --- a/OpenRA.Game/Traits/TraitsInterfaces.cs +++ b/OpenRA.Game/Traits/TraitsInterfaces.cs @@ -394,6 +394,13 @@ namespace OpenRA.Traits [RequireExplicitImplementation] public interface IRenderTerrain { void RenderTerrain(WorldRenderer wr, Viewport viewport); } + [RequireExplicitImplementation] + public interface ITerrainLighting + { + event Action CellChanged; + float3 TintAt(WPos pos); + } + public interface IRenderAboveShroud { IEnumerable RenderAboveShroud(Actor self, WorldRenderer wr); diff --git a/OpenRA.Mods.Common/Graphics/ModelRenderable.cs b/OpenRA.Mods.Common/Graphics/ModelRenderable.cs index 9a4eaf56ab..b0a98d5657 100644 --- a/OpenRA.Mods.Common/Graphics/ModelRenderable.cs +++ b/OpenRA.Mods.Common/Graphics/ModelRenderable.cs @@ -144,9 +144,12 @@ namespace OpenRA.Mods.Common.Graphics var sd = shadowOrigin + psb[3]; var wrsr = Game.Renderer.WorldRgbaSpriteRenderer; - var ti = model.tint; - wrsr.DrawSpriteWithTint(renderProxy.ShadowSprite, sa, sb, sc, sd, ti); - wrsr.DrawSpriteWithTint(renderProxy.Sprite, pxOrigin - 0.5f * renderProxy.Sprite.Size, renderProxy.Sprite.Size, ti); + var t = model.tint; + if (wr.TerrainLighting != null) + t *= wr.TerrainLighting.TintAt(model.pos); + + wrsr.DrawSpriteWithTint(renderProxy.ShadowSprite, sa, sb, sc, sd, t); + wrsr.DrawSpriteWithTint(renderProxy.Sprite, pxOrigin - 0.5f * renderProxy.Sprite.Size, renderProxy.Sprite.Size, t); } public void RenderDebugGeometry(WorldRenderer wr) diff --git a/OpenRA.Mods.Common/Traits/TerrainLightSource.cs b/OpenRA.Mods.Common/Traits/TerrainLightSource.cs new file mode 100644 index 0000000000..90a1119ab3 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/TerrainLightSource.cs @@ -0,0 +1,67 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 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 OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("Adds a localized circular light centered on the actor to the world's TerrainLightSource trait.")] + public class TerrainLightSourceInfo : TraitInfo, INotifyEditorPlacementInfo, IRulesetLoaded, ILobbyCustomRulesIgnore + { + public readonly WDist Range = WDist.FromCells(10); + public readonly float Intensity = 0; + public readonly float RedTint = 0; + public readonly float GreenTint = 0; + public readonly float BlueTint = 0; + + object INotifyEditorPlacementInfo.AddedToEditor(EditorActorPreview preview, World editorWorld) + { + var tint = new float3(RedTint, GreenTint, BlueTint); + return editorWorld.WorldActor.Trait().AddLightSource(preview.CenterPosition, Range, Intensity, tint); + } + + void INotifyEditorPlacementInfo.RemovedFromEditor(EditorActorPreview preview, World editorWorld, object data) + { + editorWorld.WorldActor.Trait().RemoveLightSource((int)data); + } + + public void RulesetLoaded(Ruleset rules, ActorInfo ai) + { + if (!rules.Actors["world"].HasTraitInfo()) + throw new YamlException("TerrainLightSource can only be used with the world TerrainLighting trait."); + } + + public override object Create(ActorInitializer init) { return new TerrainLightSource(init.Self, this); } + } + + public sealed class TerrainLightSource : INotifyAddedToWorld, INotifyRemovedFromWorld + { + readonly TerrainLightSourceInfo info; + readonly TerrainLighting terrainLighting; + int lightingToken = -1; + + public TerrainLightSource(Actor self, TerrainLightSourceInfo info) + { + this.info = info; + terrainLighting = self.World.WorldActor.Trait(); + } + + void INotifyAddedToWorld.AddedToWorld(Actor self) + { + lightingToken = terrainLighting.AddLightSource(self.CenterPosition, info.Range, info.Intensity, new float3(info.RedTint, info.GreenTint, info.BlueTint)); + } + + void INotifyRemovedFromWorld.RemovedFromWorld(Actor self) + { + terrainLighting.RemoveLightSource(lightingToken); + } + } +} diff --git a/OpenRA.Mods.Common/Traits/TerrainLighting.cs b/OpenRA.Mods.Common/Traits/TerrainLighting.cs new file mode 100644 index 0000000000..016f02e6c7 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/TerrainLighting.cs @@ -0,0 +1,141 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 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 OpenRA.Primitives; +using OpenRA.Support; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("Add to the world actor to apply a global lighting tint and allow actors using the TerrainLightSource to add localised lighting.")] + public class TerrainLightingInfo : TraitInfo, ILobbyCustomRulesIgnore + { + public readonly float Intensity = 1; + public readonly float HeightStep = 0; + public readonly float RedTint = 1; + public readonly float GreenTint = 1; + public readonly float BlueTint = 1; + + [Desc("Size of light source partition bins (cells)")] + public readonly int BinSize = 10; + + public override object Create(ActorInitializer init) { return new TerrainLighting(init.World, this); } + } + + public sealed class TerrainLighting : ITerrainLighting + { + class LightSource + { + public readonly WPos Pos; + public readonly CPos Cell; + public readonly WDist Range; + public readonly float Intensity; + public readonly float3 Tint; + + public LightSource(WPos pos, CPos cell, WDist range, float intensity, float3 tint) + { + Pos = pos; + Cell = cell; + Range = range; + Intensity = intensity; + Tint = tint; + } + } + + readonly TerrainLightingInfo info; + readonly Map map; + readonly Dictionary lightSources = new Dictionary(); + readonly SpatiallyPartitioned partitionedLightSources; + readonly float3 globalTint; + int nextLightSourceToken = 1; + + public event Action CellChanged = null; + + public TerrainLighting(World world, TerrainLightingInfo info) + { + this.info = info; + map = world.Map; + globalTint = new float3(info.RedTint, info.GreenTint, info.BlueTint); + + var cellSize = map.Grid.Type == MapGridType.RectangularIsometric ? 1448 : 1024; + partitionedLightSources = new SpatiallyPartitioned( + (map.MapSize.X + 1) * cellSize, + (map.MapSize.Y + 1) * cellSize, + info.BinSize * cellSize); + } + + Rectangle Bounds(LightSource source) + { + var c = source.Pos; + var r = source.Range.Length; + return new Rectangle(c.X - r, c.Y - r, 2 * r, 2 * r); + } + + public int AddLightSource(WPos pos, WDist range, float intensity, float3 tint) + { + var token = nextLightSourceToken++; + var source = new LightSource(pos, map.CellContaining(pos), range, intensity, tint); + var bounds = Bounds(source); + lightSources.Add(token, source); + partitionedLightSources.Add(source, bounds); + + if (CellChanged != null) + foreach (var c in map.FindTilesInCircle(source.Cell, (source.Range.Length + 1023) / 1024)) + CellChanged(c.ToMPos(map)); + + return token; + } + + public void RemoveLightSource(int token) + { + LightSource source; + if (!lightSources.TryGetValue(token, out source)) + return; + + lightSources.Remove(token); + partitionedLightSources.Remove(source); + if (CellChanged != null) + foreach (var c in map.FindTilesInCircle(source.Cell, (source.Range.Length + 1023) / 1024)) + CellChanged(c.ToMPos(map)); + } + + float3 ITerrainLighting.TintAt(WPos pos) + { + using (new PerfSample("terrain_lighting")) + { + var uv = map.CellContaining(pos).ToMPos(map); + var tint = globalTint; + if (!map.Height.Contains(uv)) + return tint; + + var intensity = info.Intensity + info.HeightStep * map.Height[uv]; + if (lightSources.Count > 0) + { + foreach (var source in partitionedLightSources.At(new int2(pos.X, pos.Y))) + { + var range = source.Range.Length; + var distance = (source.Pos - pos).Length; + if (distance > range) + continue; + + var falloff = (range - distance) * 1f / range; + intensity += falloff * source.Intensity; + tint += falloff * source.Tint; + } + } + + return intensity * tint; + } + } + } +} diff --git a/OpenRA.Mods.Common/Traits/World/ShroudRenderer.cs b/OpenRA.Mods.Common/Traits/World/ShroudRenderer.cs index ea79e49fe8..deb7816fec 100644 --- a/OpenRA.Mods.Common/Traits/World/ShroudRenderer.cs +++ b/OpenRA.Mods.Common/Traits/World/ShroudRenderer.cs @@ -292,8 +292,8 @@ namespace OpenRA.Mods.Common.Traits if (fogSprite != null) fogPos += fogSprite.Offset - 0.5f * fogSprite.Size; - shroudLayer.Update(uv, shroudSprite, shroudPos); - fogLayer.Update(uv, fogSprite, fogPos); + shroudLayer.Update(uv, shroudSprite, shroudPos, true); + fogLayer.Update(uv, fogSprite, fogPos, true); } } diff --git a/OpenRA.Mods.Common/Traits/World/TerrainRenderer.cs b/OpenRA.Mods.Common/Traits/World/TerrainRenderer.cs index b19b359e7e..64e0c4c607 100644 --- a/OpenRA.Mods.Common/Traits/World/TerrainRenderer.cs +++ b/OpenRA.Mods.Common/Traits/World/TerrainRenderer.cs @@ -59,7 +59,7 @@ namespace OpenRA.Mods.Common.Traits var sprite = theater.TileSprite(tile); foreach (var kv in spriteLayers) - kv.Value.Update(cell, palette == kv.Key ? sprite : null); + kv.Value.Update(cell, palette == kv.Key ? sprite : null, false); } void IRenderTerrain.RenderTerrain(WorldRenderer wr, Viewport viewport) diff --git a/OpenRA.Mods.D2k/Traits/World/BuildableTerrainLayer.cs b/OpenRA.Mods.D2k/Traits/World/BuildableTerrainLayer.cs index e904307507..f26756f135 100644 --- a/OpenRA.Mods.D2k/Traits/World/BuildableTerrainLayer.cs +++ b/OpenRA.Mods.D2k/Traits/World/BuildableTerrainLayer.cs @@ -97,7 +97,7 @@ namespace OpenRA.Mods.D2k.Traits // Terrain tiles define their origin at the topleft var s = theater.TileSprite(tile.Value); var ss = new Sprite(s.Sheet, s.Bounds, s.ZRamp, float2.Zero, s.Channel, s.BlendMode); - render.Update(kv.Key, ss); + render.Update(kv.Key, ss, false); } else render.Clear(kv.Key);