diff --git a/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs b/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs index d47ba808ec6..b327cc93851 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs @@ -4,6 +4,7 @@ using System.Numerics; using OpenToolkit.Graphics.OpenGL4; using Robust.Client.ResourceManagement; +using Robust.Shared.Collections; using Robust.Shared.Enums; using Robust.Shared.GameObjects; using Robust.Shared.Graphics; @@ -157,6 +158,16 @@ private void _drawGrids(Viewport viewport, Box2 worldAABB, Box2Rotated worldBoun GL.DrawElements(GetQuadGLPrimitiveType(), datum.EdgeCount * GetQuadBatchIndexCount(), DrawElementsType.UnsignedShort, 0); CheckGlError(); } + + if (_drawTileEdges && datum.InteriorEdgeCount > 0) + { + BindVertexArray(datum.InteriorEdgeVAO); + CheckGlError(); + + _debugStats.LastGLDrawCalls += 1; + GL.DrawElements(GetQuadGLPrimitiveType(), datum.InteriorEdgeCount * GetQuadBatchIndexCount(), DrawElementsType.UnsignedShort, 0); + CheckGlError(); + } } requiresFlush = false; @@ -279,6 +290,14 @@ private void _updateChunkMesh(Entity grid, MapChunk chunk, Map } private void _updateChunkEdges(Entity grid, MapChunk chunk, MapChunkData datum) + { + _updateChunkExteriorEdges(grid, chunk, datum); + _updateChunkInteriorEdges(grid, chunk, datum); + + datum.EdgeDirty = false; + } + + private void _updateChunkExteriorEdges(Entity grid, MapChunk chunk, MapChunkData datum) { // Need a buffer that can potentially store all neighbor tiles Span indexBuffer = EnsureSize(ref _chunkMeshBuilderIndexBuffer, _indicesPerChunk(chunk) * 8); @@ -315,7 +334,7 @@ private void _updateChunkEdges(Entity grid, MapChunk chunk, Ma continue; // If it's the same tile then no edge to be drawn. - if (tile.TypeId == neighborTile.TypeId || neighborDef.EdgeSprites.Count == 0) + if (tile.TypeId == neighborTile.TypeId || neighborDef.EdgeSprites.Count == 0 || neighborDef.BordersMode != TileBordersMode.Exterior8Patch) continue; // If neighbor is a lower or same priority then us then don't draw on our tile. @@ -348,7 +367,209 @@ private void _updateChunkEdges(Entity grid, MapChunk chunk, Ma datum.EdgeVBO.Reallocate(vertSlice); datum.EdgeCount = i; - datum.EdgeDirty = false; + } + + private void _updateTileInterior8Patch(Entity grid, int gridX, int gridY, Tile tile, Span indexBuffer, Span vertexBuffer, ref int i) + { + var maps = _entityManager.System(); + + // Edge render + for (var nx = -1; nx <= 1; nx++) + { + for (var ny = -1; ny <= 1; ny++) + { + if (nx == 0 && ny == 0) + continue; + + var isCorner = nx != 0 && ny != 0; + + var neighborIndices = new Vector2i(gridX + nx, gridY + ny); + var xNeighborIndices = new Vector2i(gridX + nx, gridY); + var yNeighborIndices = new Vector2i(gridX, gridY + ny); + + var neighborIsSame = maps.TryGetTile(grid.Comp, neighborIndices, out var neighborTile) && neighborTile.TypeId == tile.TypeId; + var xNeighborIsSame = maps.TryGetTile(grid.Comp, xNeighborIndices, out var xNeighborTile) && xNeighborTile.TypeId == tile.TypeId; + var yNeighborIsSame = maps.TryGetTile(grid.Comp, yNeighborIndices, out var yNeighborTile) && yNeighborTile.TypeId == tile.TypeId; + + switch (isCorner) + { + case false when neighborIsSame: + case true when xNeighborIsSame && yNeighborIsSame && neighborIsSame: + continue; + } + + var direction = new Vector2i(nx, ny).AsDirection(); + var regionMaybe = _tileDefinitionManager.TileAtlasRegion(tile.TypeId, direction); + + if (regionMaybe == null) + continue; + + var region = regionMaybe[0]; + WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region, 0); + i += 1; + } + } + } + + private void _updateTileInterior4Of16Patch(Entity grid, int gridX, int gridY, Tile tile, Span indexBuffer, Span vertexBuffer, ref int i) + { + var maps = _entityManager.System(); + + var index = new Vector2i(gridX, gridY); + + var north = !maps.TryGetTile(grid.Comp, index + new Vector2i(0, 1), out var northTile) || northTile.TypeId != tile.TypeId; + var northEast = !maps.TryGetTile(grid.Comp, index + new Vector2i(1, 1), out var northEastTile) || northEastTile.TypeId != tile.TypeId; + var east = !maps.TryGetTile(grid.Comp, index + new Vector2i(1, 0), out var eastTile) || eastTile.TypeId != tile.TypeId; + var southEast = !maps.TryGetTile(grid.Comp, index + new Vector2i(1, -1), out var southEastTile) || southEastTile.TypeId != tile.TypeId; + var south = !maps.TryGetTile(grid.Comp, index + new Vector2i(0, -1), out var southTile) || southTile.TypeId != tile.TypeId; + var southWest = !maps.TryGetTile(grid.Comp, index + new Vector2i(-1, -1), out var southWestTile) || southWestTile.TypeId != tile.TypeId; + var west = !maps.TryGetTile(grid.Comp, index + new Vector2i(-1, 0), out var westTile) || westTile.TypeId != tile.TypeId; + var northWest = !maps.TryGetTile(grid.Comp, index + new Vector2i(-1, 1), out var northWestTile) || northWestTile.TypeId != tile.TypeId; + + var edges = new ValueList(); + + // the full tile gets highest priority + if (north && east && west && south) + { + north = northEast = east = southEast = south = southWest = west = northWest = false; + edges.Add(Interior4Of16Edge.Full); + } + // followed by the three-sideds + if (north && east && south) + { + north = northEast = east = southEast = south = southWest = northWest = false; + edges.Add(Interior4Of16Edge.SideNorthEastSouth); + } + if (north && east && west) + { + north = northEast = east = southEast = southWest = west = northWest = false; + edges.Add(Interior4Of16Edge.SideNorthEastWest); + } + if (east && south && west) + { + northEast = east = southEast = south = southWest = west = northWest = false; + edges.Add(Interior4Of16Edge.SideEastSouthWest); + } + if (north && south && west) + { + north = northEast = southEast = south = southWest = west = northWest = false; + edges.Add(Interior4Of16Edge.SideNorthSouthWest); + } + // and then the two-sideds + if (north && east) + { + north = northEast = east = southEast = northWest = false; + edges.Add(Interior4Of16Edge.SideNorthEast); + } + if (south && east) + { + northEast = east = southEast = south = southWest = false; + edges.Add(Interior4Of16Edge.SideSouthEast); + } + if (north && west) + { + north = northEast = southWest = west = northWest = false; + edges.Add(Interior4Of16Edge.SideNorthWest); + } + if (south && west) + { + southEast = south = southWest = west = northWest = false; + edges.Add(Interior4Of16Edge.SideSouthWest); + } + // then we do the one-sideds + if (north) + { + north = northEast = northWest = false; + edges.Add(Interior4Of16Edge.SideNorth); + } + if (east) + { + northEast = east = southEast = false; + edges.Add(Interior4Of16Edge.SideEast); + } + if (south) + { + southEast = south = southWest = false; + edges.Add(Interior4Of16Edge.SideSouth); + } + if (west) + { + southWest = west = northWest = false; + edges.Add(Interior4Of16Edge.SideWest); + } + // finally we have the corner pieces + if (northEast) + edges.Add(Interior4Of16Edge.CornerNorthEast); + + if (southEast) + edges.Add(Interior4Of16Edge.CornerSouthEast); + + if (northWest) + edges.Add(Interior4Of16Edge.CornerNorthWest); + + if (southWest) + edges.Add(Interior4Of16Edge.CornerSouthWest); + + foreach (var edge in edges) + { + var regionMaybe = _tileDefinitionManager.TileAtlasRegion(tile.TypeId, edge); + + if (regionMaybe == null) + continue; + + var region = regionMaybe[0]; + WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region, 0); + i += 1; + } + } + + private void _updateChunkInteriorEdges(Entity grid, MapChunk chunk, MapChunkData datum) + { + // Need a buffer that can potentially store all neighbor tiles + Span indexBuffer = EnsureSize(ref _chunkMeshBuilderIndexBuffer, _indicesPerChunk(chunk) * 8); + Span vertexBuffer = EnsureSize(ref _chunkMeshBuilderVertexBuffer, _verticesPerChunk(chunk) * 8); + + var i = 0; + var chunkSize = grid.Comp.ChunkSize; + var chunkOriginScaled = chunk.Indices * chunkSize; + + for (ushort x = 0; x < chunkSize; x++) + { + for (ushort y = 0; y < chunkSize; y++) + { + var gridX = x + chunkOriginScaled.X; + var gridY = y + chunkOriginScaled.Y; + var tile = chunk.GetTile(x, y); + + if (!_tileDefinitionManager.TryGetDefinition(tile.TypeId, out var tileDef)) + continue; + + switch (tileDef.BordersMode) + { + case TileBordersMode.Interior8Patch: + _updateTileInterior8Patch(grid, gridX, gridY, tile, indexBuffer, vertexBuffer, ref i); + break; + case TileBordersMode.Interior4Of16: + _updateTileInterior4Of16Patch(grid, gridX, gridY, tile, indexBuffer, vertexBuffer, ref i); + break; + default: + continue; + } + } + } + + // We don't save the edge buffers back because we might need to re-use it if a neighbor chunk updates. + var indexSlice = indexBuffer[..(i * GetQuadBatchIndexCount())]; + var vertSlice = vertexBuffer[..(i * 4)]; + + GL.BindVertexArray(datum.InteriorEdgeVAO); + CheckGlError(); + datum.InteriorEdgeEBO.Use(); + datum.InteriorEdgeVBO.Use(); + datum.InteriorEdgeEBO.Reallocate(indexSlice); + datum.InteriorEdgeVBO.Reallocate(vertSlice); + + datum.InteriorEdgeCount = i; } private unsafe void _initChunkBuffers(Entity grid, MapChunk chunk, MapChunkData datum) @@ -389,7 +610,7 @@ private unsafe void _initChunkBuffers(Entity grid, MapChunk ch var edgeEbo = new GLBuffer(this, BufferTarget.ElementArrayBuffer, BufferUsageHint.DynamicDraw, eboSize * 8, $"Grid {grid.Owner} chunk {chunk.Indices} EdgeEBO"); - ObjectLabelMaybe(ObjectLabelIdentifier.VertexArray, vao, $"Grid {grid.Owner} chunk {chunk.Indices} EdgeVAO"); + ObjectLabelMaybe(ObjectLabelIdentifier.VertexArray, edgeVao, $"Grid {grid.Owner} chunk {chunk.Indices} EdgeVAO"); SetupVAOLayout(); CheckGlError(); @@ -399,6 +620,27 @@ private unsafe void _initChunkBuffers(Entity grid, MapChunk ch datum.EdgeEBO = edgeEbo; datum.EdgeVBO = edgeVbo; datum.EdgeVAO = edgeVao; + + // InteriorEdgeVAO + var interiorEdgeVao = GenVertexArray(); + BindVertexArray(interiorEdgeVao); + CheckGlError(); + + var interiorEdgeVbo = new GLBuffer(this, BufferTarget.ArrayBuffer, BufferUsageHint.DynamicDraw, + vboSize * 8, $"Grid {grid.Owner} chunk {chunk.Indices} InteriorEdgeVBO"); + var interiorEdgeEbo = new GLBuffer(this, BufferTarget.ElementArrayBuffer, BufferUsageHint.DynamicDraw, + eboSize * 8, $"Grid {grid.Owner} chunk {chunk.Indices} InteriorEdgeEBO"); + + ObjectLabelMaybe(ObjectLabelIdentifier.VertexArray, interiorEdgeVao, $"Grid {grid.Owner} chunk {chunk.Indices} InteriorEdgeVAO"); + SetupVAOLayout(); + CheckGlError(); + + interiorEdgeVbo.Use(); + interiorEdgeEbo.Use(); + + datum.InteriorEdgeEBO = interiorEdgeEbo; + datum.InteriorEdgeVBO = interiorEdgeVbo; + datum.InteriorEdgeVAO = interiorEdgeVao; } private void DeleteChunk(MapChunkData data) @@ -523,6 +765,11 @@ private sealed class MapChunkData public GLBuffer EBO = default!; public int TileCount; + public uint InteriorEdgeVAO; + public GLBuffer InteriorEdgeVBO = default!; + public GLBuffer InteriorEdgeEBO = default!; + public int InteriorEdgeCount; + public uint EdgeVAO; public GLBuffer EdgeVBO = default!; public GLBuffer EdgeEBO = default!; diff --git a/Robust.Client/Map/ClydeTileDefinitionManager.cs b/Robust.Client/Map/ClydeTileDefinitionManager.cs index 2199b867d58..affd552e672 100644 --- a/Robust.Client/Map/ClydeTileDefinitionManager.cs +++ b/Robust.Client/Map/ClydeTileDefinitionManager.cs @@ -33,6 +33,7 @@ internal sealed partial class ClydeTileDefinitionManager : TileDefinitionManager public Texture TileTextureAtlas => _tileTextureAtlas ?? Texture.Transparent; private FrozenDictionary<(int Id, Direction Direction), Box2[]> _tileRegions = FrozenDictionary<(int Id, Direction Direction), Box2[]>.Empty; + private FrozenDictionary<(int Id, Interior4Of16Edge Edge), Box2[]> _edgeRegions = FrozenDictionary<(int Id, Interior4Of16Edge Edge), Box2[]>.Empty; public Box2 ErrorTileRegion { get; private set; } @@ -60,6 +61,18 @@ internal sealed partial class ClydeTileDefinitionManager : TileDefinitionManager return null; } + /// + public Box2[]? TileAtlasRegion(int tileType, Interior4Of16Edge edge) + { + // ReSharper disable once CanSimplifyDictionaryTryGetValueWithGetValueOrDefault + if (_edgeRegions.TryGetValue((tileType, edge), out var region)) + { + return region; + } + + return null; + } + public override void Initialize() { base.Initialize(); @@ -95,6 +108,7 @@ internal void _genTextureAtlas() { var sw = RStopwatch.StartNew(); var tileRegs = new Dictionary<(int Id, Direction Direction), Box2[]>(); + var edgeRegs = new Dictionary<(int Id, Interior4Of16Edge Edge), Box2[]>(); _tileTextureAtlas = null; var defList = TileDefs.Where(t => t.Sprite != null).ToList(); @@ -105,7 +119,7 @@ internal void _genTextureAtlas() const int tileSize = EyeManager.PixelsPerMeter; - var tileCount = defList.Select(x => x.Variants + x.EdgeSprites.Count).Sum() + 1; + var tileCount = defList.Select(x => x.Variants + x.EdgeSprites.Count + x.Interior4Of16EdgeSprites.Count).Sum() + 1; var dimensionX = (int)Math.Ceiling(Math.Sqrt(tileCount)); var dimensionY = (int)Math.Ceiling((float)tileCount / dimensionX); @@ -174,9 +188,6 @@ internal void _genTextureAtlas() tileRegs.Add((def.TileId, Direction.Invalid), regionList); // Edges - if (def.EdgeSprites.Count <= 0) - continue; - foreach (var direction in DirectionExtensions.AllDirections) { if (!def.EdgeSprites.TryGetValue(direction, out var edge)) @@ -223,7 +234,7 @@ internal void _genTextureAtlas() break; } - if (angle != Angle.Zero) + if (angle != Angle.Zero && def.BordersMode == TileBordersMode.Exterior8Patch) { image.Mutate(o => o.Rotate((float)-angle.Degrees)); } @@ -241,9 +252,37 @@ internal void _genTextureAtlas() tileRegs.Add((def.TileId, direction), edgeList); BumpColumn(ref row, ref column, dimensionX); } + + foreach (var (edge, res) in def.Interior4Of16EdgeSprites) + { + using (var stream = _manager.ContentFileRead(res)) + { + image = Image.Load(stream); + } + + if (image.Width != tileSize || image.Height != tileSize) + { + throw new NotSupportedException( + $"Unable to load {path}, due to being unable to use tile textures with a dimension other than {tileSize}x{tileSize}."); + } + + var point = new Vector2i(column * tileSize, row * tileSize); + var box = new UIBox2i(0, 0, tileSize, tileSize); + image.Blit(box, sheet, point); + + // If you ever need edge variants then you could just bump this. + var edgeList = new Box2[1]; + edgeList[0] = Box2.FromDimensions( + point.X / w, (h - point.Y - EyeManager.PixelsPerMeter) / h, + tileSize / w, tileSize / h); + + edgeRegs.Add((def.TileId, edge), edgeList); + BumpColumn(ref row, ref column, dimensionX); + } } _tileRegions = tileRegs.ToFrozenDictionary(); + _edgeRegions = edgeRegs.ToFrozenDictionary(); _tileTextureAtlas = Texture.LoadFromImage(sheet, "Tile Atlas"); _sawmill.Debug($"Tile atlas took {sw.Elapsed} to build"); } diff --git a/Robust.Client/Map/IClydeTileDefinitionManager.cs b/Robust.Client/Map/IClydeTileDefinitionManager.cs index 99f083c11d7..5358f709fa7 100644 --- a/Robust.Client/Map/IClydeTileDefinitionManager.cs +++ b/Robust.Client/Map/IClydeTileDefinitionManager.cs @@ -36,5 +36,12 @@ internal interface IClydeTileDefinitionManager : ITileDefinitionManager /// /// If null, do not draw the tile at all. public Box2[]? TileAtlasRegion(int tileType, Direction direction); + + /// + /// Gets the region inside the texture atlas to use to draw a tile type. + /// Also handles interior edge sprites. + /// + /// If null, do not draw the tile at all. + public Box2[]? TileAtlasRegion(int tileType, Interior4Of16Edge edge); } } diff --git a/Robust.Shared.IntegrationTests/EntitySerialization/TestComponents.cs b/Robust.Shared.IntegrationTests/EntitySerialization/TestComponents.cs index 04a751eff03..332dcd40177 100644 --- a/Robust.Shared.IntegrationTests/EntitySerialization/TestComponents.cs +++ b/Robust.Shared.IntegrationTests/EntitySerialization/TestComponents.cs @@ -55,6 +55,7 @@ internal sealed partial class TileDef : ITileDefinition public string ID { get; private set; } = default!; public ResPath? Sprite => null; public Dictionary EdgeSprites => new(); + public Dictionary Interior4Of16EdgeSprites => new(); public int EdgeSpritePriority => 0; public float Friction => 0; public byte Variants => 0; diff --git a/Robust.Shared/Map/ITileDefinition.cs b/Robust.Shared/Map/ITileDefinition.cs index 357ed7734de..ff3c93c004d 100644 --- a/Robust.Shared/Map/ITileDefinition.cs +++ b/Robust.Shared/Map/ITileDefinition.cs @@ -5,6 +5,52 @@ namespace Robust.Shared.Map { + /// + /// Determines the placement and selection of edge sprites for tiles + /// + public enum TileBordersMode : byte + { + /// + /// Display any number of 8 border image files at boundaries with other tile types + /// outside the bounds of this tile + /// + Exterior8Patch, + /// + /// Display any number of 8 border image files at boundaries with other tile types + /// inside the bounds of this tile + /// + Interior8Patch, + /// + /// Tiles display up to 4 of 16 border image files at boundaries with other tile types + /// inside the bounds of this tile + /// + Interior4Of16, + } + + /// + /// Key for edge sprites to be used in the interior 4-of-16 mode + /// + public enum Interior4Of16Edge : byte + { + Full, + SideNorthEast, + SideNorthWest, + SideSouthEast, + SideSouthWest, + SideNorthEastSouth, + SideNorthEastWest, + SideEastSouthWest, + SideNorthSouthWest, + SideNorth, + SideEast, + SideSouth, + SideWest, + CornerNorthEast, + CornerSouthEast, + CornerNorthWest, + CornerSouthWest, + } + /// /// The definition (template) for a grid tile. /// @@ -33,6 +79,16 @@ public interface ITileDefinition : IPrototype /// Dictionary EdgeSprites { get; } + /// + /// Possible sprites to use if we're neighbouring another tile in the 4-of-16 mode + /// + Dictionary Interior4Of16EdgeSprites { get; } + + /// + /// If the edge sprites should be drawn on the interior of the tile rather than the exterior + /// + TileBordersMode BordersMode => TileBordersMode.Exterior8Patch; + /// /// When drawing adjacent tiles that both specify edge sprites, the one with the higher priority /// is always solely drawn.