private static void DrawTileHighlights(Canvas canvas, ICmpTilemapRenderer renderer, Point2 origin, IReadOnlyGrid<bool> highlight, ColorRgba fillTint, ColorRgba outlineTint, TileHighlightMode mode, List<Vector2[]> outlineCache = null) { if (highlight.Width == 0 || highlight.Height == 0) return; // Generate strippled line texture if not available yet if (strippledLineTex == null) { PixelData pixels = new PixelData(8, 1); for (int i = 0; i < pixels.Width / 2; i++) pixels[i, 0] = ColorRgba.White; for (int i = pixels.Width / 2; i < pixels.Width; i++) pixels[i, 0] = ColorRgba.TransparentWhite; using (Pixmap pixmap = new Pixmap(pixels)) { strippledLineTex = new Texture(pixmap, TextureSizeMode.Default, TextureMagFilter.Nearest, TextureMinFilter.Nearest, TextureWrapMode.Repeat, TextureWrapMode.Repeat, TexturePixelFormat.Rgba); } } BatchInfo defaultMaterial = new BatchInfo(DrawTechnique.Alpha, canvas.State.Material.MainColor); BatchInfo strippleMaterial = new BatchInfo(DrawTechnique.Alpha, canvas.State.Material.MainColor, strippledLineTex); bool uncertain = (mode & TileHighlightMode.Uncertain) != 0; bool selection = (mode & TileHighlightMode.Selection) != 0; Component component = renderer as Component; Transform transform = component.GameObj.Transform; Tilemap tilemap = renderer.ActiveTilemap; Tileset tileset = tilemap != null ? tilemap.Tileset.Res : null; Vector2 tileSize = tileset != null ? tileset.TileSize : Tileset.DefaultTileSize; Rect localRect = renderer.LocalTilemapRect; // Determine the object's local coordinate system (rotated, scaled) in world space Vector2 worldAxisX = Vector2.UnitX; Vector2 worldAxisY = Vector2.UnitY; MathF.TransformCoord(ref worldAxisX.X, ref worldAxisX.Y, transform.Angle, transform.Scale); MathF.TransformCoord(ref worldAxisY.X, ref worldAxisY.Y, transform.Angle, transform.Scale); Vector2 localOriginPos = tileSize * origin; Vector2 worldOriginPos = localOriginPos.X * worldAxisX + localOriginPos.Y * worldAxisY; canvas.PushState(); { // Configure the canvas so our shapes are properly rotated and scaled canvas.State.TransformHandle = -localRect.TopLeft; canvas.State.TransformAngle = transform.Angle; canvas.State.TransformScale = new Vector2(transform.Scale); // Fill all highlighted tiles that are currently visible { canvas.State.SetMaterial(defaultMaterial); canvas.State.ColorTint = fillTint * ColorRgba.White.WithAlpha(selection ? 0.2f : 0.375f); // Determine tile visibility Vector2 worldTilemapOriginPos = localRect.TopLeft; MathF.TransformCoord(ref worldTilemapOriginPos.X, ref worldTilemapOriginPos.Y, transform.Angle, transform.Scale); TilemapCulling.TileInput cullingIn = new TilemapCulling.TileInput { // Remember: All these transform values are in world space TilemapPos = transform.Pos + new Vector3(worldTilemapOriginPos) + new Vector3(worldOriginPos), TilemapScale = transform.Scale, TilemapAngle = transform.Angle, TileCount = new Point2(highlight.Width, highlight.Height), TileSize = tileSize }; TilemapCulling.TileOutput cullingOut = TilemapCulling.GetVisibleTileRect(canvas.DrawDevice, cullingIn); int renderedTileCount = cullingOut.VisibleTileCount.X * cullingOut.VisibleTileCount.Y; // Draw all visible highlighted tiles { Point2 tileGridPos = cullingOut.VisibleTileStart; Vector2 renderStartPos = worldOriginPos + tileGridPos.X * tileSize.X * worldAxisX + tileGridPos.Y * tileSize.Y * worldAxisY;; Vector2 renderPos = renderStartPos; Vector2 tileXStep = worldAxisX * tileSize.X; Vector2 tileYStep = worldAxisY * tileSize.Y; int lineMergeCount = 0; int totalRects = 0; for (int tileIndex = 0; tileIndex < renderedTileCount; tileIndex++) { bool current = highlight[tileGridPos.X, tileGridPos.Y]; if (current) { // Try to merge consecutive rects in the same line to reduce drawcalls / CPU load bool hasNext = (tileGridPos.X + 1 < highlight.Width) && ((tileGridPos.X + 1 - cullingOut.VisibleTileStart.X) < cullingOut.VisibleTileCount.X); bool next = hasNext ? highlight[tileGridPos.X + 1, tileGridPos.Y] : false; if (next) { lineMergeCount++; } else { totalRects++; canvas.FillRect( transform.Pos.X + renderPos.X - lineMergeCount * tileXStep.X, transform.Pos.Y + renderPos.Y - lineMergeCount * tileXStep.Y, transform.Pos.Z, tileSize.X * (1 + lineMergeCount), tileSize.Y); lineMergeCount = 0; } } tileGridPos.X++; renderPos += tileXStep; if ((tileGridPos.X - cullingOut.VisibleTileStart.X) >= cullingOut.VisibleTileCount.X) { tileGridPos.X = cullingOut.VisibleTileStart.X; tileGridPos.Y++; renderPos = renderStartPos; renderPos += tileYStep * (tileGridPos.Y - cullingOut.VisibleTileStart.Y); } } } } // Draw highlight area outlines, unless flagged as uncertain if (!uncertain) { // Determine the outlines of individual highlighted tile patches if (outlineCache == null) outlineCache = new List<Vector2[]>(); if (outlineCache.Count == 0) { GetTileAreaOutlines(highlight, tileSize, ref outlineCache); } // Draw outlines around all highlighted tile patches canvas.State.SetMaterial(selection ? strippleMaterial : defaultMaterial); canvas.State.ColorTint = outlineTint; foreach (Vector2[] outline in outlineCache) { // For strippled-line display, determine total length of outline if (selection) { float totalLength = 0.0f; for (int i = 1; i < outline.Length; i++) { totalLength += (outline[i - 1] - outline[i]).Length; } canvas.State.TextureCoordinateRect = new Rect(totalLength / strippledLineTex.PixelWidth, 1.0f); } // Draw the outline canvas.DrawPolygon( outline, transform.Pos.X + worldOriginPos.X, transform.Pos.Y + worldOriginPos.Y, transform.Pos.Z); } } // If this is an uncertain highlight, i.e. not actually reflecting the represented action, // draw a gizmo to indicate this for the user. if (uncertain) { Vector2 highlightSize = new Vector2(highlight.Width * tileSize.X, highlight.Height * tileSize.Y); Vector2 highlightCenter = highlightSize * 0.5f; Vector3 circlePos = transform.Pos + new Vector3(worldOriginPos + worldAxisX * highlightCenter + worldAxisY * highlightCenter); float circleRadius = MathF.Min(tileSize.X, tileSize.Y) * 0.2f; canvas.State.SetMaterial(defaultMaterial); canvas.State.ColorTint = outlineTint; canvas.FillCircle( circlePos.X, circlePos.Y, circlePos.Z, circleRadius); } } canvas.PopState(); }
public override void Draw(IDrawDevice device) { // Determine basic working data Tilemap tilemap = this.ActiveTilemap; Tileset tileset = tilemap != null ? tilemap.Tileset.Res : null; Point2 tileCount = tilemap != null ? tilemap.Size : new Point2(1, 1); Vector2 tileSize = tileset != null ? tileset.TileSize : Tileset.DefaultTileSize; // Early-out, if insufficient if (tilemap == null) return; if (tileset == null) return; // Determine the total size and origin of the rendered Tilemap Vector2 renderTotalSize = tileCount * tileSize; Vector2 renderOrigin = Vector2.Zero; this.origin.ApplyTo(ref renderOrigin, ref renderTotalSize); MathF.TransformCoord(ref renderOrigin.X, ref renderOrigin.Y, this.GameObj.Transform.Angle, this.GameObj.Transform.Scale); // Determine Tile visibility TilemapCulling.TileInput cullingIn = new TilemapCulling.TileInput { // Remember: All these transform values are in world space TilemapPos = this.GameObj.Transform.Pos + new Vector3(renderOrigin), TilemapScale = this.GameObj.Transform.Scale, TilemapAngle = this.GameObj.Transform.Angle, TileCount = tileCount, TileSize = tileSize }; TilemapCulling.TileOutput cullingOut = TilemapCulling.GetVisibleTileRect(device, cullingIn); int renderedTileCount = cullingOut.VisibleTileCount.X * cullingOut.VisibleTileCount.Y; // Determine rendering parameters Material material = (tileset != null ? tileset.RenderMaterial : null) ?? Material.Checkerboard.Res; ColorRgba mainColor = material.MainColor * this.colorTint; // Reserve the required space for vertex data in our locally cached buffer if (this.vertices == null) this.vertices = new RawList<VertexC1P3T2>(); this.vertices.Count = renderedTileCount * 4; VertexC1P3T2[] vertexData = this.vertices.Data; // Determine and adjust data for Z offset generation float depthPerTile = -cullingIn.TileSize.Y * cullingIn.TilemapScale * this.tileDepthScale; if (this.tileDepthMode == TileDepthOffsetMode.Flat) depthPerTile = 0.0f; float originDepthOffset = Rect.Align(this.origin, 0, 0, 0, tileCount.Y * depthPerTile).Y; if (this.tileDepthMode == TileDepthOffsetMode.World) originDepthOffset += (this.GameObj.Transform.Pos.Y / (float)tileSize.Y) * depthPerTile; cullingOut.RenderOriginView.Z += this.offset + this.tileDepthOffset * depthPerTile + originDepthOffset; // Prepare vertex generation data Vector2 tileXStep = cullingOut.XAxisView * cullingIn.TileSize.X; Vector2 tileYStep = cullingOut.YAxisView * cullingIn.TileSize.Y; Vector3 renderPos = cullingOut.RenderOriginView; Point2 tileGridPos = cullingOut.VisibleTileStart; // Prepare vertex data array for batch-submitting IReadOnlyGrid<Tile> tiles = tilemap.Tiles; TileInfo[] tileData = tileset.TileData.Data; int submittedTileCount = 0; int vertexBaseIndex = 0; for (int tileIndex = 0; tileIndex < renderedTileCount; tileIndex++) { Tile tile = tiles[tileGridPos.X, tileGridPos.Y]; if (tile.Index < tileData.Length) { Rect uvRect = tileData[tile.Index].TexCoord0; bool visualEmpty = tileData[tile.Index].IsVisuallyEmpty; int tileBaseOffset = tileData[tile.Index].DepthOffset; float localDepthOffset = (tile.DepthOffset + tileBaseOffset) * depthPerTile; if (!visualEmpty) { vertexData[vertexBaseIndex + 0].Pos.X = renderPos.X; vertexData[vertexBaseIndex + 0].Pos.Y = renderPos.Y; vertexData[vertexBaseIndex + 0].Pos.Z = renderPos.Z + localDepthOffset; vertexData[vertexBaseIndex + 0].TexCoord.X = uvRect.X; vertexData[vertexBaseIndex + 0].TexCoord.Y = uvRect.Y; vertexData[vertexBaseIndex + 0].Color = mainColor; vertexData[vertexBaseIndex + 1].Pos.X = renderPos.X + tileYStep.X; vertexData[vertexBaseIndex + 1].Pos.Y = renderPos.Y + tileYStep.Y; vertexData[vertexBaseIndex + 1].Pos.Z = renderPos.Z + localDepthOffset + depthPerTile; vertexData[vertexBaseIndex + 1].TexCoord.X = uvRect.X; vertexData[vertexBaseIndex + 1].TexCoord.Y = uvRect.Y + uvRect.H; vertexData[vertexBaseIndex + 1].Color = mainColor; vertexData[vertexBaseIndex + 2].Pos.X = renderPos.X + tileXStep.X + tileYStep.X; vertexData[vertexBaseIndex + 2].Pos.Y = renderPos.Y + tileXStep.Y + tileYStep.Y; vertexData[vertexBaseIndex + 2].Pos.Z = renderPos.Z + localDepthOffset + depthPerTile; vertexData[vertexBaseIndex + 2].TexCoord.X = uvRect.X + uvRect.W; vertexData[vertexBaseIndex + 2].TexCoord.Y = uvRect.Y + uvRect.H; vertexData[vertexBaseIndex + 2].Color = mainColor; vertexData[vertexBaseIndex + 3].Pos.X = renderPos.X + tileXStep.X; vertexData[vertexBaseIndex + 3].Pos.Y = renderPos.Y + tileXStep.Y; vertexData[vertexBaseIndex + 3].Pos.Z = renderPos.Z + localDepthOffset; vertexData[vertexBaseIndex + 3].TexCoord.X = uvRect.X + uvRect.W; vertexData[vertexBaseIndex + 3].TexCoord.Y = uvRect.Y; vertexData[vertexBaseIndex + 3].Color = mainColor; bool vertical = tileData[tile.Index].IsVertical; if (vertical) { vertexData[vertexBaseIndex + 0].Pos.Z += depthPerTile; vertexData[vertexBaseIndex + 3].Pos.Z += depthPerTile; } submittedTileCount++; vertexBaseIndex += 4; } } tileGridPos.X++; renderPos.X += tileXStep.X; renderPos.Y += tileXStep.Y; if ((tileGridPos.X - cullingOut.VisibleTileStart.X) >= cullingOut.VisibleTileCount.X) { tileGridPos.X = cullingOut.VisibleTileStart.X; tileGridPos.Y++; renderPos = cullingOut.RenderOriginView; renderPos.X += tileYStep.X * (tileGridPos.Y - cullingOut.VisibleTileStart.Y); renderPos.Y += tileYStep.Y * (tileGridPos.Y - cullingOut.VisibleTileStart.Y); renderPos.Z += tileGridPos.Y * depthPerTile; } } // Submit all the vertices as one draw batch device.AddVertices( material, VertexMode.Quads, vertexData, submittedTileCount * 4); Profile.AddToStat(@"Duality\Stats\Render\Tilemaps\NumTiles", renderedTileCount); Profile.AddToStat(@"Duality\Stats\Render\Tilemaps\NumVertices", submittedTileCount * 4); }
public override void Draw(IDrawDevice device) { // Determine basic working data Tilemap tilemap = this.ActiveTilemap; Tileset tileset = tilemap != null ? tilemap.Tileset.Res : null; Point2 tileCount = tilemap != null ? tilemap.Size : new Point2(1, 1); Vector2 tileSize = tileset != null ? tileset.TileSize : Tileset.DefaultTileSize; // Early-out, if insufficient if (tilemap == null) { return; } if (tileset == null) { return; } // Determine the total size and origin of the rendered Tilemap Vector2 renderTotalSize = tileCount * tileSize; Vector2 renderOrigin = Vector2.Zero; this.origin.ApplyTo(ref renderOrigin, ref renderTotalSize); MathF.TransformCoord(ref renderOrigin.X, ref renderOrigin.Y, this.GameObj.Transform.Angle, this.GameObj.Transform.Scale); // Determine Tile visibility TilemapCulling.TileInput cullingIn = new TilemapCulling.TileInput { // Remember: All these transform values are in world space TilemapPos = this.GameObj.Transform.Pos + new Vector3(renderOrigin), TilemapScale = this.GameObj.Transform.Scale, TilemapAngle = this.GameObj.Transform.Angle, TileCount = tileCount, TileSize = tileSize }; TilemapCulling.TileOutput cullingOut = TilemapCulling.GetVisibleTileRect(device, cullingIn); int renderedTileCount = cullingOut.VisibleTileCount.X * cullingOut.VisibleTileCount.Y; // Determine rendering parameters Material material = (tileset != null ? tileset.RenderMaterial : null) ?? Material.Checkerboard.Res; ColorRgba mainColor = material.MainColor * this.colorTint; // Reserve the required space for vertex data in our locally cached buffer if (this.vertices == null) { this.vertices = new RawList <VertexC1P3T2>(); } this.vertices.Count = renderedTileCount * 4; VertexC1P3T2[] vertexData = this.vertices.Data; // Determine and adjust data for Z offset generation float depthPerTile = -cullingIn.TileSize.Y * cullingIn.TilemapScale * this.tileDepthScale; if (this.tileDepthMode == TileDepthOffsetMode.Flat) { depthPerTile = 0.0f; } float originDepthOffset = Rect.Align(this.origin, 0, 0, 0, tileCount.Y * depthPerTile).Y; if (this.tileDepthMode == TileDepthOffsetMode.World) { originDepthOffset += (this.GameObj.Transform.Pos.Y / (float)tileSize.Y) * depthPerTile; } cullingOut.RenderOriginView.Z += this.offset + this.tileDepthOffset * depthPerTile + originDepthOffset; // Prepare vertex generation data Vector2 tileXStep = cullingOut.XAxisView * cullingIn.TileSize.X; Vector2 tileYStep = cullingOut.YAxisView * cullingIn.TileSize.Y; Vector3 renderPos = cullingOut.RenderOriginView; Point2 tileGridPos = cullingOut.VisibleTileStart; // Prepare vertex data array for batch-submitting IReadOnlyGrid <Tile> tiles = tilemap.Tiles; TileInfo[] tileData = tileset.TileData.Data; int submittedTileCount = 0; int vertexBaseIndex = 0; for (int tileIndex = 0; tileIndex < renderedTileCount; tileIndex++) { Tile tile = tiles[tileGridPos.X, tileGridPos.Y]; if (tile.Index < tileData.Length) { Rect uvRect = tileData[tile.Index].TexCoord0; bool visualEmpty = tileData[tile.Index].IsVisuallyEmpty; int tileBaseOffset = tileData[tile.Index].DepthOffset; float localDepthOffset = (tile.DepthOffset + tileBaseOffset) * depthPerTile; if (!visualEmpty) { vertexData[vertexBaseIndex + 0].Pos.X = renderPos.X; vertexData[vertexBaseIndex + 0].Pos.Y = renderPos.Y; vertexData[vertexBaseIndex + 0].Pos.Z = renderPos.Z + localDepthOffset; vertexData[vertexBaseIndex + 0].TexCoord.X = uvRect.X; vertexData[vertexBaseIndex + 0].TexCoord.Y = uvRect.Y; vertexData[vertexBaseIndex + 0].Color = mainColor; vertexData[vertexBaseIndex + 1].Pos.X = renderPos.X + tileYStep.X; vertexData[vertexBaseIndex + 1].Pos.Y = renderPos.Y + tileYStep.Y; vertexData[vertexBaseIndex + 1].Pos.Z = renderPos.Z + localDepthOffset + depthPerTile; vertexData[vertexBaseIndex + 1].TexCoord.X = uvRect.X; vertexData[vertexBaseIndex + 1].TexCoord.Y = uvRect.Y + uvRect.H; vertexData[vertexBaseIndex + 1].Color = mainColor; vertexData[vertexBaseIndex + 2].Pos.X = renderPos.X + tileXStep.X + tileYStep.X; vertexData[vertexBaseIndex + 2].Pos.Y = renderPos.Y + tileXStep.Y + tileYStep.Y; vertexData[vertexBaseIndex + 2].Pos.Z = renderPos.Z + localDepthOffset + depthPerTile; vertexData[vertexBaseIndex + 2].TexCoord.X = uvRect.X + uvRect.W; vertexData[vertexBaseIndex + 2].TexCoord.Y = uvRect.Y + uvRect.H; vertexData[vertexBaseIndex + 2].Color = mainColor; vertexData[vertexBaseIndex + 3].Pos.X = renderPos.X + tileXStep.X; vertexData[vertexBaseIndex + 3].Pos.Y = renderPos.Y + tileXStep.Y; vertexData[vertexBaseIndex + 3].Pos.Z = renderPos.Z + localDepthOffset; vertexData[vertexBaseIndex + 3].TexCoord.X = uvRect.X + uvRect.W; vertexData[vertexBaseIndex + 3].TexCoord.Y = uvRect.Y; vertexData[vertexBaseIndex + 3].Color = mainColor; bool vertical = tileData[tile.Index].IsVertical; if (vertical) { vertexData[vertexBaseIndex + 0].Pos.Z += depthPerTile; vertexData[vertexBaseIndex + 3].Pos.Z += depthPerTile; } submittedTileCount++; vertexBaseIndex += 4; } } tileGridPos.X++; renderPos.X += tileXStep.X; renderPos.Y += tileXStep.Y; if ((tileGridPos.X - cullingOut.VisibleTileStart.X) >= cullingOut.VisibleTileCount.X) { tileGridPos.X = cullingOut.VisibleTileStart.X; tileGridPos.Y++; renderPos = cullingOut.RenderOriginView; renderPos.X += tileYStep.X * (tileGridPos.Y - cullingOut.VisibleTileStart.Y); renderPos.Y += tileYStep.Y * (tileGridPos.Y - cullingOut.VisibleTileStart.Y); renderPos.Z += tileGridPos.Y * depthPerTile; } } // Submit all the vertices as one draw batch device.AddVertices( material, VertexMode.Quads, vertexData, submittedTileCount * 4); Profile.AddToStat(@"Duality\Stats\Render\Tilemaps\NumTiles", renderedTileCount); Profile.AddToStat(@"Duality\Stats\Render\Tilemaps\NumVertices", submittedTileCount * 4); }