#region Copyright & License Information /* * Copyright (c) The OpenRA Developers and Contributors * 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.Collections.Immutable; using System.Globalization; using System.Linq; using OpenRA.Primitives; using OpenRA.Support; namespace OpenRA.Mods.Common.MapGenerator { public static class MatrixUtils { public const int MaxBinomialKernelRadius = 10; public enum DumpAdjustment { /// Make no adjustment. None, /// Normalize the matrix amplitude to the color range. Normalize, /// /// Normalize the matrix amplitude, but uniformally extend away from zero by a small /// amount to help identify the sign of martix values. /// Emphasize, } public enum GraphMode { /// /// The plotted value is the latest sequence touching a cell + 1. /// Identifier, /// /// The plotted value is the (latest) point index in the (latest) sequence touching a /// cell. /// Gradient, /// The plotted value is the count of points touching a cell. Accumulate, } /// /// /// Debugging method that prints a matrix to stderr using color only (not value listing). /// /// /// Orange < -255, -255 <= Red < 0, Black == 0, 0 < Blue <= 255, /// 255 < Cyan. Faint green is used for distance markings. /// /// /// The matrix can optionally be preprocessed for easier visual interpretation using a /// DumpAdjustment. /// /// public static void ColorDump2d( string label, Matrix matrix, DumpAdjustment adjustment = DumpAdjustment.None) { Console.Error.WriteLine($"{label}: {matrix.Size.X} by {matrix.Size.Y}, {matrix.Data.Min()} to {matrix.Data.Max()}"); switch (adjustment) { case DumpAdjustment.Normalize: matrix = NormalizeRangeInPlace(matrix.Clone(), 255); break; case DumpAdjustment.Emphasize: matrix = NormalizeRangeInPlace(matrix.Clone(), 224) .Map(v => v += Math.Sign(v) * 31); break; default: break; } for (var y = 0; y < matrix.Size.Y; y++) { for (var x = 0; x < matrix.Size.X; x++) { var v = matrix[x, y]; int r = 0, g = 0, b = 0; if (v < -255) { r = 255; g = 192; } else if (v < 0) { r = -v; } else if (v == 0) { } else if (v <= 255) { b = v; g = v / 4; } else { // v > 255 b = 255; g = 192; } g += (((x & 4) != (y & 4)) ? 1 : 0) * (((x & 16) != (y & 16)) ? 48 : 32); Console.Error.Write(string.Format(NumberFormatInfo.InvariantInfo, "\u001b[48;2;{0};{1};{2}m ", r, g, b)); } Console.Error.Write("\u001b[0m\n"); } Console.Error.WriteLine(""); Console.Error.Flush(); } public static void ColorDump2d( string label, Matrix matrix) { ColorDump2d(label, matrix.Map(v => v ? 255 : -255)); } /// /// Debugging method that prints a matrix of enum-like values to stderr, where values are /// mapped to one of 27 different colors. Red, green, and blue values represent base-3 /// digits of increasing significance. Unmappable values produce white. A corresponding /// letter of the latin alphabet is also written in the right of cells greater than zero. /// E.g., 21_base10 = 210_base3 = bright blue + medium green + no red, letter U. /// public static void EnumDump2d(string label, Matrix matrix) { Console.Error.WriteLine($"{label}: {matrix.Size.X} by {matrix.Size.Y}, {matrix.Data.Min()} to {matrix.Data.Max()}"); for (var y = 0; y < matrix.Size.Y; y++) { for (var x = 0; x < matrix.Size.X; x++) { var v = matrix[x, y]; if (v < 0 || v > 26) v = 26; var r = 127 * (v / 1 % 3); var g = 127 * (v / 3 % 3); var b = 127 * (v / 9 % 3); var f = (r + g + b <= 127) ? 37 : 30; var c = v > 0 ? (char)(64 + v) : '.'; Console.Error.Write(string.Format(NumberFormatInfo.InvariantInfo, "\u001b[{0};48;2;{1};{2};{3}m {4}", f, r, g, b, c)); // if (v < 0 || v >= 15) // v = 15; // var code = (v < 8 ? 40 : 92) + v; // Console.Error.Write(string.Format(NumberFormatInfo.InvariantInfo, "\u001b[{0}m .", code)); } Console.Error.Write("\u001b[0m\n"); } Console.Error.WriteLine(""); Console.Error.Flush(); } public static void EnumDump2d(string label, Matrix matrix) where T : Enum { EnumDump2d(label, matrix.Map(v => Convert.ToInt32(v, NumberFormatInfo.InvariantInfo))); } /// /// Debugging method that prints a matrix to stderr. /// public static void Dump2d(string label, Matrix matrix) { Console.Error.WriteLine($"{label}:"); for (var y = 0; y < matrix.Size.Y; y++) { for (var x = 0; x < matrix.Size.X; x++) Console.Error.Write(matrix[x, y] ? "\u001b[0;42m .\u001b[m" : "\u001b[m ."); Console.Error.Write("\n"); } Console.Error.WriteLine(""); Console.Error.Flush(); } /// /// Debugging method that prints a matrix to stderr. /// public static void Dump2d(string label, Matrix matrix) { Console.Error.WriteLine($"{label}: {matrix.Size.X} by {matrix.Size.Y}, {matrix.Data.Min()} to {matrix.Data.Max()}"); for (var y = 0; y < matrix.Size.Y; y++) { for (var x = 0; x < matrix.Size.X; x++) { var v = matrix[x, y]; string formatted; if (v > 0) formatted = string.Format(NumberFormatInfo.InvariantInfo, "\u001b[1;42m{0:X8}\u001b[m ", v); else if (v < 0) formatted = string.Format(NumberFormatInfo.InvariantInfo, "\u001b[1;41m{0:X8}\u001b[m ", v); else formatted = "\u001b[m 0 "; Console.Error.Write(formatted); } Console.Error.Write("\n"); } Console.Error.WriteLine(""); Console.Error.Flush(); } /// /// Debugging method that prints a matrix to stderr. /// public static void Dump2d(string label, Matrix matrix) { Console.Error.WriteLine($"{label}: {matrix.Size.X} by {matrix.Size.Y}, {matrix.Data.Min()} to {matrix.Data.Max()}"); for (var y = 0; y < matrix.Size.Y; y++) { for (var x = 0; x < matrix.Size.X; x++) { var v = matrix[x, y]; string formatted; if (v > 0 && v < 0x80) formatted = string.Format(NumberFormatInfo.InvariantInfo, "\u001b[1;42m{0:X2}\u001b[m ", v); else if (v >= 0x80) formatted = string.Format(NumberFormatInfo.InvariantInfo, "\u001b[1;41m{0:X2}\u001b[m ", v); else formatted = "\u001b[m 0 "; Console.Error.Write(formatted); } Console.Error.Write("\n"); } Console.Error.WriteLine(""); Console.Error.Flush(); } /// /// Plot multiple point sequences onto a matrix for debugging visualization. The matrix is /// fit to the shape of all the path. /// public static Matrix GraphPoints( IEnumerable> pointArrays, GraphMode mode = GraphMode.Identifier) { var pointArrayArray = pointArrays.Select(a => a.ToArray()).ToArray(); var allPoints = pointArrayArray.SelectMany(p => p).ToArray(); if (allPoints.Length == 0) return new Matrix(1, 1).Fill(int.MinValue); var topLeft = new int2(allPoints.Min(p => p.X), allPoints.Min(p => p.Y)); var bottomRight = new int2(allPoints.Max(p => p.X), allPoints.Max(p => p.Y)); var size = bottomRight - topLeft + new int2(1, 1); var matrix = new Matrix(size).Fill(mode == GraphMode.Gradient ? -1 : 0); for (var j = 0; j < pointArrayArray.Length; j++) { var pointArray = pointArrayArray[j]; for (var i = 0; i < pointArray.Length; i++) switch (mode) { case GraphMode.Identifier: matrix[pointArray[i] - topLeft] = j + 1; break; case GraphMode.Gradient: matrix[pointArray[i] - topLeft] = i; break; case GraphMode.Accumulate: matrix[pointArray[i] - topLeft]++; break; } } return matrix; } /// /// Plot a point sequence onto a matrix for debugging visualization. The matrix is fit to /// the shape of the path. /// public static Matrix GraphPoints( IEnumerable points, GraphMode mode = GraphMode.Identifier) { return GraphPoints([points], mode); } /// /// /// Perform a generic flood fill starting at seeds [(xy, prop), ...]. /// /// /// For each point being considered for fill, filler(xy, prop) is /// called with the current position (xy) and propagation value (prop). /// filler should return the value to be propagated or null if not to be /// propagated. Propagation happens to all neighbours (offsets) defined /// by spread, regardless of whether they have previously been visited, /// so filler is responsible for terminating propagation by returning /// nulls. Usually, Direction.SPREAD4 or Direction.SPREAD8 /// is appropriate as a spread pattern. /// /// /// filler should capture and manipulate any necessary input and output /// arrays. /// /// /// Each call to filler will have either an equal or greater /// growth/propagation distance from their seed value than all calls /// before it. (You can think of this as them being called in ordered /// growth layers.) /// /// /// Note that filler may be called multiple times for the same spot, /// perhaps with different propagation values. Within the same /// growth/propagation distance, filler will be called from values /// propagated from earlier seeds before values propagated from later /// seeds. /// /// /// filler is not called for positions outside of the bounds defined by /// size EXCEPT for points being processed as seed values. /// /// public static void FloodFill

( int2 size, IEnumerable<(int2 XY, P Prop)> seeds, Func filler, ImmutableArray spread) where P : struct { var current = new List<(int2 XY, P Prop)>(); var next = seeds.ToList(); while (next.Count != 0) { (next, current) = (current, next); next.Clear(); foreach (var (source, prop) in current) { var newProp = filler(source, prop); if (newProp != null) foreach (var offset in spread) { var destination = source + offset; if (destination.X >= 0 && destination.X < size.X && destination.Y >= 0 && destination.Y < size.Y) next.Add((destination, (P)newProp)); } } } } ///

/// /// Compute the in-game walking distances (in 1024ths) from a set of seeds. /// /// /// The output matrix cells will contain either the distance (if reachable) or /// int.MaxValue. /// /// public static Matrix WalkingDistances(Matrix passable, IEnumerable seeds, WDist maxDistance) { const int Diagonal = 1448; const int Straight = 1024; var output = new Matrix(passable.Size).Fill(WDist.MaxValue); var unprocessed = new PriorityArray(passable.Size.X * passable.Size.Y, int.MaxValue); foreach (var seed in seeds) unprocessed[passable.Index(seed)] = 0; while (true) { var i = unprocessed.GetMinIndex(); var distance = unprocessed[i]; var xy = passable.XY(i); if (distance > maxDistance.Length) break; if (distance <= maxDistance.Length && output.ContainsXY(xy)) output[xy] = new WDist(distance); unprocessed[i] = int.MaxValue; foreach (var (offset, direction) in DirectionExts.Spread8D) { var nextXY = xy + offset; if (!passable.ContainsXY(nextXY)) continue; if (!passable[nextXY]) continue; if (output[nextXY] != WDist.MaxValue) continue; int nextDistance; if (direction.IsDiagonal()) nextDistance = distance + Diagonal; else nextDistance = distance + Straight; var nextI = passable.Index(nextXY); if (nextDistance < unprocessed[nextI]) unprocessed[nextI] = nextDistance; } } return output; } /// /// /// Shrinkwraps true space to be as far away from false space as possible, preserving /// topology. The result is a kind of rough Voronoi diagram. /// /// /// If the space matrix has width (w, h), the returned matrix will have width (w + 1, h + 1). /// Each value in the returned matrix is a Direction bitmask describing the border structure /// between the cells of the original space matrix. /// /// outsideSpace specified the space values for cells which are outside the space matrix. /// /// /// public static Matrix DeflateSpace(Matrix space, bool outsideSpace) { var size = space.Size; var holes = new Matrix(size); var holeCount = 0; for (var y = 0; y < space.Size.Y; y++) for (var x = 0; x < space.Size.X; x++) if (!space[x, y] && holes[x, y] == 0) { holeCount++; int? Filler(int2 xy, int holeId) { if (!space[xy] && holes[xy] == 0) { holes[xy] = holeId; return holeId; } else { return null; } } FloodFill(space.Size, [(new int2(x, y), holeCount)], Filler, DirectionExts.Spread4); } const int UNASSIGNED = int.MaxValue; var voronoi = new Matrix(size); var distances = new Matrix(size).Fill(UNASSIGNED); var closestN = new Matrix(size).Fill(UNASSIGNED); var midN = (size.X * size.Y + 1) / 2; var seeds = new List<(int2, (int, int2, int))>(); for (var y = 0; y < size.Y; y++) for (var x = 0; x < size.X; x++) { var xy = new int2(x, y); if (holes[xy] != 0) seeds.Add((xy, (holes[xy], xy, closestN.Index(x, y)))); } if (!outsideSpace) { holeCount++; for (var x = 0; x < size.X; x++) { // Hack: closestN is actually inside, but starting x, y are outside. seeds.Add((new int2(x, 0), (holeCount, new int2(x, -1), closestN.Index(x, 0)))); seeds.Add((new int2(x, size.Y - 1), (holeCount, new int2(x, size.Y), closestN.Index(x, size.Y - 1)))); } for (var y = 0; y < size.Y; y++) { // Hack: closestN is actually inside, but starting x, y are outside. seeds.Add((new int2(0, y), (holeCount, new int2(-1, y), closestN.Index(0, y)))); seeds.Add((new int2(size.X - 1, y), (holeCount, new int2(size.X, y), closestN.Index(size.X - 1, y)))); } } { (int HoleId, int2 StartXY, int StartN)? Filler(int2 xy, (int HoleId, int2 StartXY, int StartN) prop) { var n = closestN.Index(xy); var distance = (xy - prop.StartXY).LengthSquared; if (distance < distances[n]) { voronoi[n] = prop.HoleId; distances[n] = distance; closestN[n] = prop.StartN; return (prop.HoleId, prop.StartXY, prop.StartN); } else if (distance == distances[n]) { if (closestN[n] == prop.StartN) { return null; } else if (n <= midN == prop.StartN < closestN[n]) { // For the first half of the map, lower seed indexes are preferred. // For the second half of the map, higher seed indexes are preferred. voronoi[n] = prop.HoleId; closestN[n] = prop.StartN; return (prop.HoleId, prop.StartXY, prop.StartN); } else { return null; } } else { return null; } } FloodFill(size, seeds, Filler, DirectionExts.Spread4); } var deflatedSize = size + new int2(1, 1); var deflated = new Matrix(deflatedSize); var neighborhood = new int[4]; var scan = new int2[] { new(-1, -1), new(0, -1), new(-1, 0), new(0, 0) }; for (var cy = 0; cy < deflatedSize.Y; cy++) for (var cx = 0; cx < deflatedSize.X; cx++) { for (var neighbor = 0; neighbor < 4; neighbor++) { var x = Math.Clamp(cx + scan[neighbor].X, 0, size.X - 1); var y = Math.Clamp(cy + scan[neighbor].Y, 0, size.Y - 1); neighborhood[neighbor] = voronoi[x, y]; } deflated[cx, cy] = (byte)( (neighborhood[0] != neighborhood[1] ? DirectionMask.MU : 0) | (neighborhood[1] != neighborhood[3] ? DirectionMask.MR : 0) | (neighborhood[3] != neighborhood[2] ? DirectionMask.MD : 0) | (neighborhood[2] != neighborhood[0] ? DirectionMask.ML : 0)); } return deflated; } /// /// Convolute a kernel over a boolean input matrix. /// If dilating, the values specified by the kernel are logically OR-ed. /// If eroding, the values specified by the kernel are logically AND-ed. /// public static Matrix KernelDilateOrErode(Matrix input, Matrix kernel, int2 kernelCenter, bool dilate) { var output = new Matrix(input.Size).Fill(!dilate); for (var cy = 0; cy < input.Size.Y; cy++) for (var cx = 0; cx < input.Size.X; cx++) { void InnerLoop() { for (var ky = 0; ky < kernel.Size.Y; ky++) for (var kx = 0; kx < kernel.Size.X; kx++) { var x = cx + kx - kernelCenter.X; var y = cy + ky - kernelCenter.Y; if (!input.ContainsXY(x, y)) continue; if (kernel[kx, ky] && input[x, y] == dilate) { output[cx, cy] = dilate; return; } } } InnerLoop(); } return output; } /// /// /// Create a one-dimensional binomial kernel of size (2 * radius + 1, 1). /// The total of all kernel cells is 1 << (radius * 2). /// /// /// This can be applied once, transposed, then applied again to perform a full binomial blur. /// See . Maximum supported radius is MaxBinomialKernelRadius. /// /// static Matrix BinomialKernel1D(int radius) { if (radius < 0 || radius > 10) throw new ArgumentException($"Binomial kernel radius was not in supported range (0 to {MaxBinomialKernelRadius} inclusive)."); var span = radius * 2 + 1; var kernel = new Matrix(new int2(span, 1)); var factorials = new long[span]; factorials[0] = 1; for (var i = 1; i < span; i++) factorials[i] = factorials[i - 1] * i; var n = span - 1; for (var k = 0; k < span; k++) kernel[k] = factorials[n] / (factorials[k] * factorials[n - k]); return kernel; } /// /// Apply an arithmetic convolution of a kernel over an input matrix. /// Cells outside the input matrix take the value of the nearest edge/corner cell. /// public static Matrix KernelFilter(Matrix input, Matrix kernel, int2 kernelCenter) { var output = new Matrix(input.Size); for (var cy = 0; cy < input.Size.Y; cy++) for (var cx = 0; cx < input.Size.X; cx++) { long total = 0; var samples = 0; for (var ky = 0; ky < kernel.Size.Y; ky++) for (var kx = 0; kx < kernel.Size.X; kx++) { var x = cx + kx - kernelCenter.X; var y = cy + ky - kernelCenter.Y; total += input[input.ClampXY(new int2(x, y))] * kernel[kx, ky]; samples++; } output[cx, cy] = total; } return output; } /// /// Apply a binomial filter-based blur to a matrix, returning a new matrix. The result is /// somewhat similar to a Gaussian blur. Maximum supported radius is MaxBinomialKernelRadius. /// public static Matrix BinomialBlur(Matrix input, int radius) { var kernel = BinomialKernel1D(radius); var downscale = 2 * radius; var stage1 = KernelFilter(input.Map(v => (long)v), kernel, new int2(radius, 0)); for (var i = 0; i < stage1.Data.Length; i++) stage1[i] >>= downscale; var stage2 = KernelFilter(stage1, kernel.Transpose(), new int2(0, radius)); for (var i = 0; i < stage2.Data.Length; i++) stage2[i] >>= downscale; return stage2.Map(v => (int)v); } /// /// Finds the local variance of points in a grid (using a square sample area). /// Sample areas are centered on data point corners, so output is (size + 1) * (size + 1). /// public static Matrix GridVariance(Matrix input, int radius) { var output = new Matrix(input.Size + new int2(1, 1)); for (var cy = 0; cy < output.Size.Y; cy++) for (var cx = 0; cx < output.Size.X; cx++) { var total = 0; var samples = 0; for (var ry = -radius; ry < radius; ry++) for (var rx = -radius; rx < radius; rx++) { var y = cy + ry; var x = cx + rx; if (!input.ContainsXY(x, y)) continue; total += input[x, y]; samples++; } var mean = total / samples; long sumOfSquares = 0; for (var ry = -radius; ry < radius; ry++) for (var rx = -radius; rx < radius; rx++) { var y = cy + ry; var x = cx + rx; if (!input.ContainsXY(x, y)) continue; long difference = mean - input[x, y]; sumOfSquares += difference * difference; } output[cx, cy] = (int)(sumOfSquares / samples); } return output; } /// /// /// Blur a boolean matrix using a square kernel, only changing the value /// if the neighborhood is significantly different based on a threshold. /// /// /// The threshold / thresholdOutOf is the size of a majority needed to /// change a value. For example, a threshold of 20 / 25 means, 80% of /// cells must agree to change a cell's value. /// /// /// The space outside of the matrix is treated as if the border was /// extended out. /// /// /// Along with the blurred matrix, the number of changes compared to the /// original is returned. /// /// /// Runtime complexity is approximately O(input.Size) for small radii. /// A more precise complexity would be /// O((input.Size.X + radius) * input.Size.Y + /// input.Size.X * (input.Size.Y + radius)). /// /// public static (Matrix Output, int Changes) BooleanBlur( Matrix input, int radius, int threshold, int thresholdOutOf) { // Sum radius-by-1 kernels first in O((size.X + radius) * size.Y) time using a diffing sliding // window, then sum 1-by-radius kernels in O(size.X * (size.Y + radius)) time. var hTrueCounts = new Matrix(input.Size); var kernelArea = (2 * radius + 1) * (2 * radius + 1); if (threshold < 1 || thresholdOutOf < 1 || threshold * 2 < thresholdOutOf) throw new ArgumentException("invalid threshold"); var trueThreshold = (kernelArea * threshold + thresholdOutOf - 1) / thresholdOutOf; var falseThreshold = kernelArea - trueThreshold; var output = new Matrix(input.Size); var changes = 0; for (var cy = 0; cy < input.Size.Y; cy++) { var trueCount = 0; for (var ox = -radius; ox <= radius; ox++) if (input[input.ClampXY(new int2(ox, cy))]) trueCount++; hTrueCounts[0, cy] = trueCount; for (var cx = 1; cx < input.Size.X; cx++) { if (input[input.ClampXY(new int2(cx - radius - 1, cy))]) trueCount--; if (input[input.ClampXY(new int2(cx + radius, cy))]) trueCount++; hTrueCounts[cx, cy] = trueCount; } } void OutputForXY(int x, int y, int trueCount) { var thisInput = input[x, y]; bool thisOutput; if (trueCount <= falseThreshold) thisOutput = false; else if (trueCount >= trueThreshold) thisOutput = true; else thisOutput = thisInput; output[x, y] = thisOutput; if (thisOutput != thisInput) changes++; } for (var cx = 0; cx < input.Size.X; cx++) { var trueCount = 0; for (var oy = -radius; oy <= radius; oy++) trueCount += hTrueCounts[hTrueCounts.ClampXY(new int2(cx, oy))]; OutputForXY(cx, 0, trueCount); for (var cy = 1; cy < input.Size.Y; cy++) { trueCount -= hTrueCounts[hTrueCounts.ClampXY(new int2(cx, cy - radius - 1))]; trueCount += hTrueCounts[hTrueCounts.ClampXY(new int2(cx, cy + radius))]; OutputForXY(cx, cy, trueCount); } } return (output, changes); } /// /// Preserves foreground cells that can be safely covered by a (possibly /// out-of-bound) span-by-span square that doesn't touch any !foreground /// cells, and sets any remaining cells to !foreground. /// public static (Matrix Output, int Changes) RetainThickRegions( Matrix input, bool foreground, int span) { // The time complexity could be improved to O(input.Size) by using // a technique similar to BooleanBlur, but, in practice, this // hasn't needed optimizing yet. var output = new Matrix(input.Size).Fill(!foreground); for (var cy = 1 - span; cy < input.Size.Y; cy++) for (var cx = 1 - span; cx < input.Size.X; cx++) { bool IsRetained() { for (var ry = 0; ry < span; ry++) for (var rx = 0; rx < span; rx++) { var x = cx + rx; var y = cy + ry; if (!input.ContainsXY(x, y)) continue; if (input[x, y] != foreground) return false; } return true; } if (!IsRetained()) continue; for (var ry = 0; ry < span; ry++) for (var rx = 0; rx < span; rx++) { var x = cx + rx; var y = cy + ry; if (!input.ContainsXY(x, y)) continue; output[x, y] = foreground; } } var changes = 0; for (var i = 0; i < input.Data.Length; i++) if (input[i] != output[i]) changes++; return (output, changes); } /// /// Read a linearly interpolated value between the cells of a matrix. xWeight and yWeight /// must be between 0 and scale inclusive and define the interpolation position /// between x and x+1, and y and y+1. /// public static int IntegerInterpolate( Matrix matrix, int x, int y, int xWeight, int yWeight, int scale) { var xa = x; var xb = x + 1; var ya = y; var yb = y + 1; if (scale <= 0) throw new ArgumentException("Interpolation scale was not > 0"); if (xWeight < 0 || yWeight < 0 || xWeight > scale || yWeight > scale) throw new ArgumentException("Interpolation weights were not between 0 and scale inclusive."); // "w" for "weight" var xbw = xWeight; var ybw = yWeight; var xaw = scale - xWeight; var yaw = scale - yWeight; if (xa < 0) { xa = 0; xb = 0; } else if (xb > matrix.Size.X - 1) { xa = matrix.Size.X - 1; xb = matrix.Size.X - 1; } if (ya < 0) { ya = 0; yb = 0; } else if (yb > matrix.Size.Y - 1) { ya = matrix.Size.Y - 1; yb = matrix.Size.Y - 1; } long naa = matrix[xa, ya]; long nba = matrix[xb, ya]; long nab = matrix[xa, yb]; long nbb = matrix[xb, yb]; return (int)(((naa * xaw + nba * xbw) * yaw + (nab * xaw + nbb * xbw) * ybw) / scale / scale); } /// /// Uniformally add to or subtract from all cells such that the quantile (count/outOf) has at the target value. /// For example, (target: 0, count: 25, outOf: 75) where there are 401 cells would mean /// that 100 cells are no greater than 0, 300 cells are no less than 0, and at least 1 cell /// is 0. /// public static void CalibrateQuantileInPlace(Matrix matrix, int target, int count, int outOf) { var sorted = (int[])matrix.Data.Clone(); Array.Sort(sorted); var adjustment = target - sorted[(long)(sorted.Length - 1) * count / outOf]; for (var i = 0; i < matrix.Data.Length; i++) matrix[i] += adjustment; } /// /// Return a boolean matrix where true correlates with the largest values in the input, /// such that the fraction of true cells is at least (but approximately) count/outOf. /// public static Matrix CalibratedBooleanThreshold(Matrix input, int count, int outOf) { if (count <= 0) return new Matrix(input.Size); else if (count >= outOf) return new Matrix(input.Size).Fill(true); var sorted = (int[])input.Data.Clone(); Array.Sort(sorted); var threshold = sorted[(long)sorted.Length * (outOf - count) / outOf]; return input.Map(v => v >= threshold); } /// /// For true cells, gives the Chebyshev distance to the closest false cell. /// For false cells, gives the Chebyshev distance to the closest true cell as a negative. /// outsideValue specifies whether cells outside of the matrix are true or false. /// public static Matrix ChebyshevRoom(Matrix input, bool outsideValue) { var roominess = new Matrix(input.Size); var seeds = new List<(int2, int)>(); // Find true/false boundaries and map boundary for (var cy = 0; cy < input.Size.Y; cy++) for (var cx = 0; cx < input.Size.X; cx++) { var pCount = 0; var nCount = 0; for (var oy = -1; oy <= 1; oy++) for (var ox = -1; ox <= 1; ox++) { var x = cx + ox; var y = cy + oy; if (input.ContainsXY(x, y) ? input[x, y] : outsideValue) pCount++; else nCount++; } if (pCount != 9 && nCount != 9) seeds.Add((new int2(cx, cy), 1)); } if (seeds.Count == 0) { // There were no shores. Use minSpan or -minSpan as appropriate. var minSpan = Math.Min(input.Size.X, input.Size.Y); roominess.Fill(input[0] ? minSpan : -minSpan); return roominess; } int? Filler(int2 xy, int room) { if (!roominess.ContainsXY(xy) || roominess[xy] != 0) return null; roominess[xy] = input[xy] ? room : -room; return room + 1; } FloodFill( roominess.Size, seeds, Filler, DirectionExts.Spread8); return roominess; } /// /// /// Given a set of grid-intersection point arrays, creates a matrix where each cell /// identifies whether the closest points are wrapping around it clockwise or /// counter-clockwise (as defined in MapGenerator.Direction). /// /// /// Positive output values indicate the points are wrapping around it clockwise. /// Negative output values indicate the points are wrapping around it counter-clockwise. /// Outputs can be zero or non-unit magnitude if there are fighting point arrays. /// /// /// If no points are on or close enough to the matrix area, returns null. /// /// public static Matrix PointsChirality(int2 size, IEnumerable pointArrayArray) { const int FirstPassSentinel = int.MinValue; var chirality = new Matrix(size); var seeds = new List<(int2, int)>(); void SeedChirality(int2 point, int value) { if (!chirality.ContainsXY(point)) return; chirality[point] += value; seeds.Add((point, FirstPassSentinel)); } foreach (var pointArray in pointArrayArray) { for (var i = 1; i < pointArray.Length; i++) { var from = pointArray[i - 1]; var to = pointArray[i]; var direction = DirectionExts.FromInt2(to - from); var fx = from.X; var fy = from.Y; switch (direction) { case Direction.R: SeedChirality(new int2(fx, fy), 1); SeedChirality(new int2(fx, fy - 1), -1); break; case Direction.D: SeedChirality(new int2(fx - 1, fy), 1); SeedChirality(new int2(fx, fy), -1); break; case Direction.L: SeedChirality(new int2(fx - 1, fy - 1), 1); SeedChirality(new int2(fx - 1, fy), -1); break; case Direction.U: SeedChirality(new int2(fx, fy - 1), 1); SeedChirality(new int2(fx - 1, fy - 1), -1); break; default: throw new ArgumentException("Unsupported direction for chirality"); } } } if (seeds.Count == 0) return null; int? FillChirality(int2 point, int prop) { if (prop == FirstPassSentinel) return chirality[point]; if (chirality[point] != 0 || prop == 0) return null; chirality[point] = prop; return prop; } FloodFill(size, seeds, FillChirality, DirectionExts.Spread4); return chirality; } /// /// /// Trace the borders between true and false regions of an input matrix, returning an array /// of point sequences. /// /// /// Point sequences follow the borders keeping the true region on the right-hand side as it /// traces forward. Loops have a matching start and end point. /// /// /// If a mask is supplied, only borders between matrix cells in the mask are considered. /// /// public static int2[][] BordersToPoints(Matrix matrix, Matrix mask = null) { if (mask != null && matrix.Size != mask.Size) throw new ArgumentException("matrix and mask did not have same size"); // There is redundant memory/iteration, but I don't care enough. // These are really only the signs of the gradients. var gradientH = new Matrix(matrix.Size); var gradientV = new Matrix(matrix.Size); for (var y = 0; y < matrix.Size.Y; y++) for (var x = 1; x < matrix.Size.X; x++) if (mask == null || (mask[x - 1, y] && mask[x, y])) { var l = matrix[x - 1, y] ? 1 : 0; var r = matrix[x, y] ? 1 : 0; gradientV[x, y] = (sbyte)(r - l); } for (var y = 1; y < matrix.Size.Y; y++) for (var x = 0; x < matrix.Size.X; x++) if (mask == null || (mask[x, y - 1] && mask[x, y])) { var u = matrix[x, y - 1] ? 1 : 0; var d = matrix[x, y] ? 1 : 0; gradientH[x, y] = (sbyte)(d - u); } // Looping paths contain the start/end point twice. var paths = new List(); void TracePath(int sx, int sy, Direction direction) { var points = new List(); var x = sx; var y = sy; points.Add(new int2(x, y)); do { switch (direction) { case Direction.R: gradientH[x, y] = 0; x++; break; case Direction.D: gradientV[x, y] = 0; y++; break; case Direction.L: x--; gradientH[x, y] = 0; break; case Direction.U: y--; gradientV[x, y] = 0; break; default: throw new ArgumentException("direction assertion failed"); } points.Add(new int2(x, y)); var r = gradientH.ContainsXY(x, y) && gradientH[x, y] > 0; var d = gradientV.ContainsXY(x, y) && gradientV[x, y] < 0; var l = gradientH.ContainsXY(x - 1, y) && gradientH[x - 1, y] < 0; var u = gradientV.ContainsXY(x, y - 1) && gradientV[x, y - 1] > 0; if (direction == Direction.R && u) direction = Direction.U; else if (direction == Direction.D && r) direction = Direction.R; else if (direction == Direction.L && d) direction = Direction.D; else if (direction == Direction.U && l) direction = Direction.L; else if (r) direction = Direction.R; else if (d) direction = Direction.D; else if (l) direction = Direction.L; else if (u) direction = Direction.U; else break; // Dead end (not a loop) } while (x != sx || y != sy); paths.Add(points.ToArray()); } // Trace non-loops (from edge of map) for (var x = 1; x < matrix.Size.X; x++) { if (gradientV[x, 0] < 0) TracePath(x, 0, Direction.D); if (gradientV[x, matrix.Size.Y - 1] > 0) TracePath(x, matrix.Size.Y, Direction.U); } for (var y = 1; y < matrix.Size.Y; y++) { if (gradientH[0, y] > 0) TracePath(0, y, Direction.R); if (gradientH[matrix.Size.X - 1, y] < 0) TracePath(matrix.Size.X, y, Direction.L); } // Trace loops for (var y = 0; y < matrix.Size.Y; y++) for (var x = 0; x < matrix.Size.X; x++) { if (gradientH[x, y] > 0) TracePath(x, y, Direction.R); else if (gradientH[x, y] < 0) TracePath(x + 1, y, Direction.L); if (gradientV[x, y] < 0) TracePath(x, y, Direction.D); else if (gradientV[x, y] > 0) TracePath(x, y + 1, Direction.U); } return paths.ToArray(); } /// /// /// Takes an input boolean matrix and performs adjustments to improve the local consistency /// of the true and false regions, making them "blotchy": /// /// /// - Smoothing via thresholded median blurs. /// /// /// - A minimum thickness is enforced for all true/false regions. More formally, eroding /// and then dilating the true or false regions by minimumThickness results in no change. /// /// /// - No grid points connect diagonally-crossing true and false regions. In other words, /// these 2x2 patterns never appear in the output matrix: /// /// 10 01 /// 01 or 10 /// /// /// /// A new matrix is returned. The input is unmodified. /// /// public static Matrix BooleanBlotch( Matrix input, int terrainSmoothing, int smoothingThreshold, int smoothingThresholdOutOf, int minimumThickness, bool bias) { var maxSpan = Math.Max(input.Size.X, input.Size.Y); var matrix = input; (matrix, _) = BooleanBlur(matrix, terrainSmoothing, 1, 2); for (var i1 = 0; i1 < /*max passes*/16; i1++) { for (var i2 = 0; i2 < maxSpan; i2++) { int changes; var changesAcc = 0; for (var r = 1; r <= terrainSmoothing; r++) { (matrix, changes) = BooleanBlur(matrix, r, smoothingThreshold, smoothingThresholdOutOf); changesAcc += changes; } if (changesAcc == 0) break; } { var changesAcc = 0; int changes; (matrix, changes) = RetainThickRegions(matrix, true, minimumThickness); changesAcc += changes; changes = DilateThinRegionsInPlaceFull(matrix, true, minimumThickness); changesAcc += changes; var midFixLandmass = matrix.Clone(); (matrix, changes) = RetainThickRegions(matrix, false, minimumThickness); changesAcc += changes; changes = DilateThinRegionsInPlaceFull(matrix, false, minimumThickness); changesAcc += changes; if (changesAcc == 0) break; if (i1 >= 8 && i1 % 4 == 0) { var diff = Matrix.Zip(midFixLandmass, matrix, (a, b) => a != b); for (var y = 0; y < matrix.Size.Y; y++) for (var x = 0; x < matrix.Size.X; x++) { if (diff[x, y]) OverCircle( matrix: matrix, centerIn1024ths: new int2(x * 1024 + 512, y * 1024 + 512), radiusIn1024ths: minimumThickness * 2048, outside: false, action: (xy, _) => matrix[xy] = bias); } } } } return matrix; } /// /// Repeatedly calls DilateThinRegionsInPlace until no changes are made. /// static int DilateThinRegionsInPlaceFull(Matrix input, bool foreground, int width) { int changes; var changesAcc = 0; do { changes = DilateThinRegionsInPlace(input, foreground, width); changesAcc += changes; } while (changes > 0); return changesAcc; } /// /// /// If foreground true, finds the thinnest true regions and dilates them. /// If foreground false, finds the thinnest false regions and dilates them. /// Each call only dilates thin regions by one cell's thickness on each border. /// /// /// Only regions with a thickness less than width (in Chebyshev distance) are considered. /// /// /// Returns the number of changes made. /// /// static int DilateThinRegionsInPlace(Matrix input, bool foreground, int width) { var sizeMinus1 = input.Size - new int2(1, 1); var cornerMaskSpan = width + 1; // Zero means ignore. var cornerMask = new Matrix(cornerMaskSpan, cornerMaskSpan); for (var y = 0; y < cornerMaskSpan; y++) for (var x = 0; x < cornerMaskSpan; x++) cornerMask[x, y] = 1 + width + width - x - y; cornerMask[0] = 0; // Higher number indicates a thinner area. var thinness = new Matrix(input.Size); void SetThinness(int x, int y, int v) { if (!input.ContainsXY(x, y)) return; if (input[x, y] == foreground) return; thinness[x, y] = Math.Max(v, thinness[x, y]); } for (var cy = 0; cy < input.Size.Y; cy++) for (var cx = 0; cx < input.Size.X; cx++) { if (input[cx, cy] == foreground) continue; // _L_eft _R_ight _U_p _D_own var l = input[Math.Max(cx - 1, 0), cy] == foreground; var r = input[Math.Min(cx + 1, sizeMinus1.X), cy] == foreground; var u = input[cx, Math.Max(cy - 1, 0)] == foreground; var d = input[cx, Math.Min(cy + 1, sizeMinus1.Y)] == foreground; var lu = l && u; var ru = r && u; var ld = l && d; var rd = r && d; for (var ry = 0; ry < cornerMaskSpan; ry++) for (var rx = 0; rx < cornerMaskSpan; rx++) { if (rd) { var x = cx + rx; var y = cy + ry; SetThinness(x, y, cornerMask[rx, ry]); } if (ru) { var x = cx + rx; var y = cy - ry; SetThinness(x, y, cornerMask[rx, ry]); } if (ld) { var x = cx - rx; var y = cy + ry; SetThinness(x, y, cornerMask[rx, ry]); } if (lu) { var x = cx - rx; var y = cy - ry; SetThinness(x, y, cornerMask[rx, ry]); } } } var thinnest = thinness.Data.Max(); if (thinnest == 0) { // No fixes return 0; } var changes = 0; for (var y = 0; y < input.Size.Y; y++) for (var x = 0; x < input.Size.X; x++) if (thinness[x, y] == thinnest) { input[x, y] = foreground; changes++; } // Fixes made, with potentially more that can be done in another pass. return changes; } /// Remove links from a direction map that are not reciprocated. public static void RemoveStubsFromDirectionMapInPlace(Matrix matrix) { var output = matrix.Clone(); for (var cy = 0; cy < matrix.Size.Y; cy++) for (var cx = 0; cx < matrix.Size.X; cx++) { var fromPos = new int2(cx, cy); var fromDm = (DirectionMask)matrix[fromPos]; foreach (var (offset, d) in DirectionExts.Spread8D) { if ((fromDm & d.ToMask()) == DirectionMask.None) continue; var dr = d.Reverse(); var toPos = new int2(cx + offset.X, cy + offset.Y); if (matrix.ContainsXY(toPos) && ((DirectionMask)matrix[toPos] & dr.ToMask()) != DirectionMask.None) continue; matrix[fromPos] = (byte)((DirectionMask)output[fromPos] & ~d.ToMask()); } } } static Matrix RemoveJunctionsFromDirectionMap(Matrix input) { var output = input.Clone(); for (var cy = 0; cy < input.Size.Y; cy++) for (var cx = 0; cx < input.Size.X; cx++) { var dm = (DirectionMask)input[cx, cy]; if (dm.Count() > 2) { output[cx, cy] = 0; foreach (var (offset, d) in DirectionExts.Spread8D) { var xy = new int2(cx + offset.X, cy + offset.Y); if (!input.ContainsXY(xy)) continue; var dr = d.Reverse(); output[xy] = (byte)((DirectionMask)output[xy] & ~dr.ToMask()); } } } return output; } /// /// Traces a matrix of directions into a set of point sequences. Each point sequence is /// traced up to but excluding junction points. Paths are traced in both directions. The /// paths in the direction map must be bidirectional and contain no stubs. /// public static int2[][] DirectionMapToPaths(Matrix input) { var links = RemoveJunctionsFromDirectionMap(input); // Find non-loops, starting at terminals. var pointArrays = new List(); void TracePoints(int2 xy, DirectionMask reverseDm) { var points = new List(); bool AddPoint() { points.Add(xy); var dm = (DirectionMask)links[xy] & ~reverseDm; links[xy] = 0; foreach (var (offset, d) in DirectionExts.Spread8D) if ((dm & d.ToMask()) != DirectionMask.None) { xy += offset; reverseDm = d.Reverse().ToMask(); return true; } return false; } while (AddPoint()) { } pointArrays.Add(points.ToArray()); pointArrays.Add(points.Reverse().ToArray()); } for (var sy = 0; sy < links.Size.Y; sy++) for (var sx = 0; sx < links.Size.X; sx++) if (((DirectionMask)links[sx, sy]).ToDirection() != Direction.None) TracePoints(new int2(sx, sy), 0); // All non-loops have been removed, leaving only loops left. for (var sy = 0; sy < links.Size.Y; sy++) for (var sx = 0; sx < links.Size.X; sx++) if (links[sx, sy] != 0) { // Choose direction with most-significant bit var reverseDm = (DirectionMask)(links[sx, sy] & (links[sx, sy] - 1)); TracePoints(new int2(sx, sy), reverseDm); } return pointArrays.ToArray(); } /// /// Wrapper around DirectionMapToPaths which iteratively prunes stubs and short paths until /// all paths are at least a minimumLength. Paths shorter than mimimumJunctionSeparation /// sever their neighboring junctions instead of fusing them together. /// public static int2[][] DirectionMapToPathsWithPruning( Matrix input, int minimumLength, int minimumJunctionSeparation, bool preserveEdgePaths) { var links = input.Clone(); int2[][] pointArrays; // Iteratively remove paths which are too short and merge remaining ones. while (true) { RemoveStubsFromDirectionMapInPlace(links); pointArrays = DirectionMapToPaths(links); var removeablePointArrays = pointArrays; if (preserveEdgePaths) removeablePointArrays = pointArrays .Where(a => !(links.IsEdge(a[0]) || links.IsEdge(a[^1]))) .ToArray(); if (removeablePointArrays.Length == 0) return pointArrays; var shortest = removeablePointArrays.Min(a => a.Length); if (shortest >= minimumLength) return pointArrays; var toDelete = new List(); foreach (var pointArray in removeablePointArrays) if (pointArray.Length == shortest) foreach (var point in pointArray) { toDelete.Add(point); if (pointArray.Length < minimumJunctionSeparation) foreach (var (offset, d) in DirectionExts.Spread8D) if (((DirectionMask)links[point] & d.ToMask()) != DirectionMask.None) toDelete.Add(point + offset); } foreach (var point in toDelete) links[point] = 0; } } /// /// /// Given a set of point sequences and a stencil mask that defines permitted point positions, /// remove points that are disallowed, splitting or dropping point sequences as needed. /// /// /// The outside of the matrix is considered false (points disallowed). /// /// /// Sequences with fewer than 2 points are dropped. /// /// public static int2[][] MaskPathPoints(IEnumerable pointArrayArray, Matrix mask) { var newPointArrayArray = new List(); foreach (var pointArray in pointArrayArray) { if (pointArray == null || pointArray.Length < 2) continue; var isLoop = pointArray[0] == pointArray[^1]; int firstBad; for (firstBad = 0; firstBad < pointArray.Length; firstBad++) if (!(mask.ContainsXY(pointArray[firstBad]) && mask[pointArray[firstBad]])) break; if (firstBad == pointArray.Length) { // The path is entirely within the mask already. newPointArrayArray.Add(pointArray); continue; } var startAt = isLoop ? firstBad : 0; var wrapAt = isLoop ? pointArray.Length - 1 : pointArray.Length; var i = startAt; List currentPointArray = null; do { if (mask.ContainsXY(pointArray[i]) && mask[pointArray[i]]) { currentPointArray ??= []; currentPointArray.Add(pointArray[i]); } else { if (currentPointArray != null && currentPointArray.Count > 1) newPointArrayArray.Add(currentPointArray.ToArray()); currentPointArray = null; } i++; if (i == wrapAt) i = 0; } while (i != startAt); if (currentPointArray != null && currentPointArray.Count > 1) newPointArrayArray.Add(currentPointArray.ToArray()); } return newPointArrayArray.ToArray(); } /// /// /// Run an action over the inside or outside of a circle of given center and radius, /// measured in 1024ths of a cell. The action is called with the int2 cell position (NOT in /// 1024ths), and the square of the distance-in-1024ths from the cell's center to the /// circle's center. (Square root and divide by 1024 to get the distance in whole cells.) /// (0, 0) is a corner of the matrix, and (512, 512) is the center of the first cell. /// If outside is true, the action is run for cells outside of the circle instead /// of the inside. /// /// /// A matrix cell is inside the circle if its center is <= radius from center. /// Coordinates outside of the Matrix are ignored. /// /// public static void OverCircle( Matrix matrix, int2 centerIn1024ths, int radiusIn1024ths, bool outside, Action action) { var size = matrix.Size; int minX; int minY; int maxX; int maxY; if (outside) { minX = 0; minY = 0; maxX = size.X - 1; maxY = size.Y - 1; } else { minX = (centerIn1024ths.X - radiusIn1024ths) / 1024; minY = (centerIn1024ths.Y - radiusIn1024ths) / 1024; maxX = (centerIn1024ths.X + radiusIn1024ths + 1023) / 1024; maxY = (centerIn1024ths.Y + radiusIn1024ths + 1023) / 1024; if (minX < 0) minX = 0; if (minY < 0) minY = 0; if (maxX >= size.X) maxX = size.X - 1; if (maxY >= size.Y) maxY = size.Y - 1; } var radiusSquared = (long)radiusIn1024ths * radiusIn1024ths; for (var y = minY; y <= maxY; y++) for (var x = minX; x <= maxX; x++) { var rx = x * 1024 + 512 - centerIn1024ths.X; var ry = y * 1024 + 512 - centerIn1024ths.Y; var thisRadiusSquared = (long)rx * rx + (long)ry * ry; if (thisRadiusSquared <= radiusSquared != outside) action(new int2(x, y), thisRadiusSquared); } } /// /// Linearly scales the range of values in a matrix to the given target amplitude. /// Returns the modified input. If the input matrix is all zeros, it is left unmodified. /// public static Matrix NormalizeRangeInPlace(Matrix matrix, int targetAmplitude) { long inputAmplitude = matrix.Data.Max(Math.Abs); if (inputAmplitude == 0) return matrix; for (var i = 0; i < matrix.Data.Length; i++) matrix[i] = (int)((long)matrix[i] * targetAmplitude / inputAmplitude); return matrix; } /// /// Rank all cell values and select the best (greatest compared) value. /// If there are equally good best candidates, choose one at random. /// public static (int2 MPos, T Value) FindRandomBest( Matrix matrix, MersenneTwister random, Comparison comparison) { var candidates = new List(); var best = matrix[new int2(0, 0)]; for (var y = 0; y < matrix.Size.Y; y++) for (var x = 0; x < matrix.Size.X; x++) { var rank = comparison(matrix[x, y], best); if (rank > 0) { best = matrix[x, y]; candidates.Clear(); } if (rank >= 0) candidates.Add(new int2(x, y)); } var choice = candidates[random.Next(candidates.Count)]; return (choice, best); } } }