#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; namespace OpenRA.Mods.Common.MapGenerator { public static class MatrixUtils { /// /// 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(); } /// /// /// 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 next = seeds.ToList(); while (next.Count != 0) { var current = next; next = new List<(int2, P)>(); 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 from a set of seeds. /// /// /// The output matrix cells will contain either the distance (if reachable) or /// PositiveInfinity. /// /// public static Matrix WalkingDistances(Matrix passable, IEnumerable seeds, float maxDistance) { const float SQRT2 = 1.4142135623730951f; if (maxDistance == float.PositiveInfinity) maxDistance = float.MaxValue; var output = new Matrix(passable.Size).Fill(float.PositiveInfinity); var unprocessed = new PriorityArray(passable.Size.X * passable.Size.Y, float.PositiveInfinity); 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) break; if (distance <= maxDistance && output.ContainsXY(xy)) output[xy] = distance; unprocessed[i] = float.PositiveInfinity; foreach (var (offset, direction) in Direction.Spread8D) { var nextXY = xy + offset; if (!passable.ContainsXY(nextXY)) continue; if (!passable[nextXY]) continue; if (output[nextXY] != float.PositiveInfinity) continue; float nextDistance; if (Direction.IsDiagonal(direction)) nextDistance = distance + SQRT2; else nextDistance = distance + 1; 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[] { (new int2(x, y), holeCount) }, Filler, Direction.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, Direction.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] ? Direction.MU : 0) | (neighborhood[1] != neighborhood[3] ? Direction.MR : 0) | (neighborhood[3] != neighborhood[2] ? Direction.MD : 0) | (neighborhood[2] != neighborhood[0] ? Direction.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 gaussian kernel. /// /// /// This can be applied once, transposed, then applied again to perform a full gaussian blur. /// See . /// /// public static Matrix GaussianKernel1D(int radius, float standardDeviation) { var span = radius * 2 + 1; var kernel = new Matrix(new int2(span, 1)); var dsd2 = 2 * standardDeviation * standardDeviation; var total = 0.0f; for (var x = -radius; x <= radius; x++) { var value = MathF.Exp(-x * x / dsd2); kernel[x + radius] = value; total += value; } // Instead of dividing by sqrt(PI * dsd2), divide by the total. for (var i = 0; i < span; i++) kernel[i] /= total; return kernel; } /// /// Apply an arithmetic convolution of a kernel over an input matrix. /// public static Matrix KernelBlur(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++) { var total = 0.0f; 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; if (!input.ContainsXY(x, y)) continue; total += input[x, y] * kernel[kx, ky]; samples++; } output[cx, cy] = total / samples; } return output; } /// /// Apply a square gaussian blur to a matrix, returning a new matrix. /// public static Matrix GaussianBlur(Matrix input, int radius, float standardDeviation) { var kernel = GaussianKernel1D(radius, standardDeviation); var stage1 = KernelBlur(input, kernel, new int2(radius, 0)); var stage2 = KernelBlur(stage1, kernel.Transpose(), new int2(0, radius)); return stage2; } /// /// 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.0f; 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; var sumOfSquares = 0.0f; 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; sumOfSquares += MathF.Pow(mean - input[x, y], 2); } output[cx, cy] = 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. /// /// /// For example, a threshold of 0.75 means any change requires a 75% /// majority within the kernel. /// /// /// The space outside of the matrix is treated as if the border was /// extended out. /// /// /// Along with the blured 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, float threshold) { if (threshold < 0.5f || threshold > 1.0f) throw new ArgumentException("threshold must between 0.5 and 1.0 inclusive"); var output = new Matrix(input.Size); var changes = 0; // 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); var trueThreshold = (int)MathF.Ceiling(kernelArea * threshold); var falseThreshold = kernelArea - trueThreshold; 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. public static float Interpolate(Matrix matrix, float x, float y) { var xa = (int)MathF.Floor(x); var xb = (int)MathF.Ceiling(x); var ya = (int)MathF.Floor(y); var yb = (int)MathF.Ceiling(y); // "w" for "weight" var xbw = x - xa; var ybw = y - ya; var xaw = 1.0f - xbw; var yaw = 1.0f - ybw; 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; } var naa = matrix[xa, ya]; var nba = matrix[xb, ya]; var nab = matrix[xa, yb]; var nbb = matrix[xb, yb]; return (naa * xaw + nba * xbw) * yaw + (nab * xaw + nbb * xbw) * ybw; } /// /// Finds the (linearly interpolated) value a given fraction through a sorted array. /// public static float ArrayQuantile(float[] array, float quantile) { if (array.Length == 0) throw new ArgumentException("Cannot get quantile of empty array"); var iFloat = quantile * (array.Length - 1); if (iFloat < 0) iFloat = 0; if (iFloat > array.Length - 1) iFloat = array.Length - 1; var iLow = (int)iFloat; if (iLow == iFloat) return array[iLow]; var iHigh = iLow + 1; var weight = iFloat - iLow; return array[iLow] * (1 - weight) + array[iHigh] * weight; } /// /// Uniformally add to or subtract from all matrix cells such that the given quantile, /// fraction, has the given target value. /// public static void CalibrateQuantileInPlace(Matrix matrix, float target, float fraction) { var sorted = (float[])matrix.Data.Clone(); Array.Sort(sorted); var adjustment = target - ArrayQuantile(sorted, fraction); for (var i = 0; i < matrix.Data.Length; i++) matrix[i] += adjustment; } /// /// 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, Direction.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 MapUtils.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. /// /// 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 = Direction.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"); } } int? FillChirality(int2 point, int prop) { if (prop == FirstPassSentinel) return chirality[point]; if (chirality[point] != 0) return null; chirality[point] = prop; return prop; } FloodFill(size, seeds, FillChirality, Direction.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, int 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, float smoothingThreshold, int minimumThickness, bool bias) { var maxSpan = Math.Max(input.Size.X, input.Size.Y); var matrix = input; (matrix, _) = BooleanBlur(matrix, terrainSmoothing, 0.5f); 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); 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, center: new float2(x, y), radius: minimumThickness * 2, 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 Chebychev 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 = matrix[fromPos]; foreach (var (offset, d) in Direction.Spread8D) { if ((fromDm & (1 << d)) == 0) continue; var dr = Direction.Reverse(d); var toPos = new int2(cx + offset.X, cy + offset.Y); if (matrix.ContainsXY(toPos) && (matrix[toPos] & (1 << dr)) != 0) continue; matrix[fromPos] = (byte)(output[fromPos] & ~(1 << d)); } } } 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 = input[cx, cy]; if (Direction.Count(dm) > 2) { output[cx, cy] = 0; foreach (var (offset, d) in Direction.Spread8D) { var xy = new int2(cx + offset.X, cy + offset.Y); if (!input.ContainsXY(xy)) continue; var dr = Direction.Reverse(d); output[xy] = (byte)(output[xy] & ~(1 << dr)); } } } 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 bidrectional 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, int reverseDm) { var points = new List(); bool AddPoint() { points.Add(xy); var dm = links[xy] & ~reverseDm; links[xy] = 0; foreach (var (offset, d) in Direction.Spread8D) if ((dm & (1 << d)) != 0) { xy += offset; reverseDm = 1 << Direction.Reverse(d); 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 (Direction.FromMask(links[sx, sy]) != 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 = 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 Direction.Spread8D) if ((links[point] & (1 << d)) != 0) 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 ??= new List(); 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. The /// action is called with the int2 position and the squared distance to the circle's /// center. 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 position is <= radius from center. /// Coordinates outside of the Matrix are ignored. /// /// public static void OverCircle( Matrix matrix, float2 center, float radius, 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 = (int)MathF.Floor(center.X - radius); minY = (int)MathF.Floor(center.Y - radius); maxX = (int)MathF.Ceiling(center.X + radius); maxY = (int)MathF.Ceiling(center.Y + radius); 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 = radius * radius; for (var y = minY; y <= maxY; y++) for (var x = minX; x <= maxX; x++) { var rx = x - center.X; var ry = y - center.Y; var thisRadiusSquared = rx * rx + ry * ry; if (thisRadiusSquared <= radiusSquared != outside) action(new int2(x, y), thisRadiusSquared); } } } }