Remove floating points from map generation

Removes all use of floating point from the RaMapGenerator map generator
and its dependencies.

Floating point behavior is potentially non-portable across client
hardware. Removing them should make the map generation logic
consistent even across clients with different floating point hardware
or compiler behavior. This may be useful for sync-safe multiplayer map
generation where clients independently generate the map from settings.

Most previously fractional public-facing settings are now represented
as numbers out of 1000, with some exceptions using 1000000. Most
internal logic which relies on fixed-point concepts now uses 1024ths,
though some floating point mechanisms have been replaced with
alternative discrete approximations (e.g. gaussian to binomial).
This commit is contained in:
Ashley Newson
2025-01-26 16:45:46 +00:00
committed by Gustas Kažukauskas
parent 04e9cef38e
commit 037326024b
17 changed files with 847 additions and 827 deletions

View File

@@ -268,14 +268,14 @@ namespace OpenRA.Mods.Common.MapGenerator
}
/// <summary>
/// Uniformally add to or subtract from all matrix cells such that the given quantile,
/// fraction, has the given target value.
/// Uniformally add to or subtract from all cells such that count out of every outOf cells,
/// are no greater than the given target value.
/// </summary>
public static void CalibrateQuantileInPlace(CellLayer<float> cellLayer, float target, float fraction)
public static void CalibrateQuantileInPlace(CellLayer<int> cellLayer, int target, int count, int outOf)
{
var sorted = Entries(cellLayer);
Array.Sort(sorted);
var adjustment = target - MatrixUtils.ArrayQuantile(sorted, fraction);
var adjustment = target - sorted[(sorted.Length - 1) * count / outOf];
foreach (var mpos in cellLayer.CellRegion.MapCoords)
cellLayer[mpos] += adjustment;
}
@@ -394,12 +394,15 @@ namespace OpenRA.Mods.Common.MapGenerator
FromMatrix(output, roominess);
}
/// <summary>Wrapper around MatrixUtils.WalkingDistance in CPos space.</summary>
/// <summary>
/// Wrapper around MatrixUtils.WalkingDistance in CPos space.
/// Returns world distances (1024ths).
/// </summary>
public static void WalkingDistances(
CellLayer<float> distances,
CellLayer<int> distances,
CellLayer<bool> passable,
IEnumerable<CPos> seeds,
float maxDistance)
int maxDistance)
{
var passableMatrix = ToMatrix(passable, false);
var cellBounds = CellBounds(passable);
@@ -441,7 +444,7 @@ namespace OpenRA.Mods.Common.MapGenerator
/// Pick a random MPos position in a CellLayer where each cell is a
/// selection weight.
/// </summary>
public static MPos PickWeighted(CellLayer<float> weights, MersenneTwister random)
public static MPos PickWeighted(CellLayer<int> weights, MersenneTwister random)
{
var entries = Entries(weights);
var choice = random.PickWeighted(entries);

View File

@@ -20,6 +20,8 @@ namespace OpenRA.Mods.Common.MapGenerator
{
public static class MatrixUtils
{
public const int MaxBinomialKernelRadius = 10;
/// <summary>
/// Debugging method that prints a matrix to stderr.
/// </summary>
@@ -156,22 +158,20 @@ namespace OpenRA.Mods.Common.MapGenerator
/// <summary>
/// <para>
/// Compute the in-game walking distances from a set of seeds.
/// Compute the in-game walking distances (in 1024ths) from a set of seeds.
/// </para>
/// <para>
/// The output matrix cells will contain either the distance (if reachable) or
/// PositiveInfinity.
/// int.MaxValue.
/// </para>
/// </summary>
public static Matrix<float> WalkingDistances(Matrix<bool> passable, IEnumerable<int2> seeds, float maxDistance)
public static Matrix<int> WalkingDistances(Matrix<bool> passable, IEnumerable<int2> seeds, int maxDistance)
{
const float SQRT2 = 1.4142135623730951f;
const int Diagonal = 1448;
const int Straight = 1024;
if (maxDistance == float.PositiveInfinity)
maxDistance = float.MaxValue;
var output = new Matrix<float>(passable.Size).Fill(float.PositiveInfinity);
var unprocessed = new PriorityArray<float>(passable.Size.X * passable.Size.Y, float.PositiveInfinity);
var output = new Matrix<int>(passable.Size).Fill(int.MaxValue);
var unprocessed = new PriorityArray<int>(passable.Size.X * passable.Size.Y, int.MaxValue);
foreach (var seed in seeds)
unprocessed[passable.Index(seed)] = 0;
@@ -186,7 +186,7 @@ namespace OpenRA.Mods.Common.MapGenerator
if (distance <= maxDistance && output.ContainsXY(xy))
output[xy] = distance;
unprocessed[i] = float.PositiveInfinity;
unprocessed[i] = int.MaxValue;
foreach (var (offset, direction) in Direction.Spread8D)
{
@@ -195,13 +195,13 @@ namespace OpenRA.Mods.Common.MapGenerator
continue;
if (!passable[nextXY])
continue;
if (output[nextXY] != float.PositiveInfinity)
if (output[nextXY] != int.MaxValue)
continue;
float nextDistance;
int nextDistance;
if (Direction.IsDiagonal(direction))
nextDistance = distance + SQRT2;
nextDistance = distance + Diagonal;
else
nextDistance = distance + 1;
nextDistance = distance + Straight;
var nextI = passable.Index(nextXY);
if (nextDistance < unprocessed[nextI])
@@ -390,83 +390,89 @@ namespace OpenRA.Mods.Common.MapGenerator
/// <summary>
/// <para>
/// Create a one-dimensional gaussian kernel.
/// Create a one-dimensional binomial kernel of size (2 * radius + 1, 1).
/// The total of all kernel cells is 1 &lt;&lt; (radius * 2).
/// </para>
/// <para>
/// This can be applied once, transposed, then applied again to perform a full gaussian blur.
/// See <see cref="GaussianBlur"/>.
/// This can be applied once, transposed, then applied again to perform a full binomial blur.
/// See <see cref="BinomialBlur"/>. Maximum supported radius is MaxBinomialKernelRadius.
/// </para>
/// </summary>
public static Matrix<float> GaussianKernel1D(int radius, float standardDeviation)
static Matrix<long> BinomialKernel1D(int radius)
{
var span = radius * 2 + 1;
var kernel = new Matrix<float>(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;
}
if (radius < 0 || radius > 10)
throw new ArgumentException($"Binomial kernel radius was not in supported range (0 to {MaxBinomialKernelRadius} inclusive).");
// Instead of dividing by sqrt(PI * dsd2), divide by the total.
for (var i = 0; i < span; i++)
kernel[i] /= total;
var span = radius * 2 + 1;
var kernel = new Matrix<long>(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;
}
/// <summary>
/// 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.
/// </summary>
public static Matrix<float> KernelBlur(Matrix<float> input, Matrix<float> kernel, int2 kernelCenter)
public static Matrix<long> KernelFilter(Matrix<long> input, Matrix<long> kernel, int2 kernelCenter)
{
var output = new Matrix<float>(input.Size);
var output = new Matrix<long>(input.Size);
for (var cy = 0; cy < input.Size.Y; cy++)
for (var cx = 0; cx < input.Size.X; cx++)
{
var total = 0.0f;
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;
if (!input.ContainsXY(x, y))
continue;
total += input[x, y] * kernel[kx, ky];
total += input[input.ClampXY(new int2(x, y))] * kernel[kx, ky];
samples++;
}
output[cx, cy] = total / samples;
output[cx, cy] = total;
}
return output;
}
/// <summary>
/// Apply a square gaussian blur to a matrix, returning a new matrix.
/// 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.
/// </summary>
public static Matrix<float> GaussianBlur(Matrix<float> input, int radius, float standardDeviation)
public static Matrix<int> BinomialBlur(Matrix<int> input, int radius)
{
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;
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);
}
/// <summary>
/// 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).
/// </summary>
public static Matrix<float> GridVariance(Matrix<float> input, int radius)
public static Matrix<int> GridVariance(Matrix<int> input, int radius)
{
var output = new Matrix<float>(input.Size + new int2(1, 1));
var output = new Matrix<int>(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 total = 0;
var samples = 0;
for (var ry = -radius; ry < radius; ry++)
for (var rx = -radius; rx < radius; rx++)
@@ -480,7 +486,7 @@ namespace OpenRA.Mods.Common.MapGenerator
}
var mean = total / samples;
var sumOfSquares = 0.0f;
long sumOfSquares = 0;
for (var ry = -radius; ry < radius; ry++)
for (var rx = -radius; rx < radius; rx++)
{
@@ -488,10 +494,11 @@ namespace OpenRA.Mods.Common.MapGenerator
var x = cx + rx;
if (!input.ContainsXY(x, y))
continue;
sumOfSquares += MathF.Pow(mean - input[x, y], 2);
long difference = mean - input[x, y];
sumOfSquares += difference * difference;
}
output[cx, cy] = sumOfSquares / samples;
output[cx, cy] = (int)(sumOfSquares / samples);
}
return output;
@@ -503,8 +510,9 @@ namespace OpenRA.Mods.Common.MapGenerator
/// if the neighborhood is significantly different based on a threshold.
/// </para>
/// <para>
/// For example, a threshold of 0.75 means any change requires a 75%
/// majority within the kernel.
/// 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.
/// </para>
/// <para>
/// The space outside of the matrix is treated as if the border was
@@ -522,21 +530,22 @@ namespace OpenRA.Mods.Common.MapGenerator
/// </para>
/// </summary>
public static (Matrix<bool> Output, int Changes) BooleanBlur(
Matrix<bool> input, int radius, float threshold)
Matrix<bool> input, int radius, int threshold, int thresholdOutOf)
{
if (threshold < 0.5f || threshold > 1.0f)
throw new ArgumentException("threshold must between 0.5 and 1.0 inclusive");
var output = new Matrix<bool>(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<int>(input.Size);
var kernelArea = (2 * radius + 1) * (2 * radius + 1);
var trueThreshold = (int)MathF.Ceiling(kernelArea * threshold);
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<bool>(input.Size);
var changes = 0;
for (var cy = 0; cy < input.Size.Y; cy++)
{
var trueCount = 0;
@@ -642,19 +651,35 @@ namespace OpenRA.Mods.Common.MapGenerator
return (output, changes);
}
/// <summary>Read a linearly interpolated value between the cells of a matrix.</summary>
public static float Interpolate(Matrix<float> matrix, float x, float y)
/// <summary>
/// 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.
/// </summary>
public static int IntegerInterpolate(
Matrix<int> matrix,
int x,
int y,
int xWeight,
int yWeight,
int scale)
{
var xa = (int)MathF.Floor(x);
var xb = (int)MathF.Ceiling(x);
var ya = (int)MathF.Floor(y);
var yb = (int)MathF.Ceiling(y);
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 = x - xa;
var ybw = y - ya;
var xaw = 1.0f - xbw;
var yaw = 1.0f - ybw;
var xbw = xWeight;
var ybw = yWeight;
var xaw = scale - xWeight;
var yaw = scale - yWeight;
if (xa < 0)
{
@@ -678,46 +703,22 @@ namespace OpenRA.Mods.Common.MapGenerator
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;
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);
}
/// <summary>
/// Finds the (linearly interpolated) value a given fraction through a sorted array.
/// Uniformally add to or subtract from all cells such that count out of every outOf cells,
/// are no greater than the given target value.
/// </summary>
public static float ArrayQuantile(float[] array, float quantile)
public static void CalibrateQuantileInPlace(Matrix<int> matrix, int target, int count, int outOf)
{
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;
}
/// <summary>
/// Uniformally add to or subtract from all matrix cells such that the given quantile,
/// fraction, has the given target value.
/// </summary>
public static void CalibrateQuantileInPlace(Matrix<float> matrix, float target, float fraction)
{
var sorted = (float[])matrix.Data.Clone();
var sorted = (int[])matrix.Data.Clone();
Array.Sort(sorted);
var adjustment = target - ArrayQuantile(sorted, fraction);
var adjustment = target - sorted[(sorted.Length - 1) * count / outOf];
for (var i = 0; i < matrix.Data.Length; i++)
matrix[i] += adjustment;
}
@@ -1017,14 +1018,15 @@ namespace OpenRA.Mods.Common.MapGenerator
public static Matrix<bool> BooleanBlotch(
Matrix<bool> input,
int terrainSmoothing,
float smoothingThreshold,
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, 0.5f);
(matrix, _) = BooleanBlur(matrix, terrainSmoothing, 1, 2);
for (var i1 = 0; i1 < /*max passes*/16; i1++)
{
for (var i2 = 0; i2 < maxSpan; i2++)
@@ -1033,7 +1035,7 @@ namespace OpenRA.Mods.Common.MapGenerator
var changesAcc = 0;
for (var r = 1; r <= terrainSmoothing; r++)
{
(matrix, changes) = BooleanBlur(matrix, r, smoothingThreshold);
(matrix, changes) = BooleanBlur(matrix, r, smoothingThreshold, smoothingThresholdOutOf);
changesAcc += changes;
}
@@ -1067,8 +1069,8 @@ namespace OpenRA.Mods.Common.MapGenerator
if (diff[x, y])
OverCircle(
matrix: matrix,
center: new float2(x, y),
radius: minimumThickness * 2,
centerIn1024ths: new int2(x * 1024 + 512, y * 1024 + 512),
radiusIn1024ths: minimumThickness * 2048,
outside: false,
action: (xy, _) => matrix[xy] = bias);
}
@@ -1425,22 +1427,25 @@ namespace OpenRA.Mods.Common.MapGenerator
/// <summary>
/// <para>
/// 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
/// 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.
/// </para>
/// <para>
/// A matrix cell is inside the circle if its position is &lt;= radius from center.
/// A matrix cell is inside the circle if its center is &lt;= radius from center.
/// Coordinates outside of the Matrix are ignored.
/// </para>
/// </summary>
public static void OverCircle<T>(
Matrix<T> matrix,
float2 center,
float radius,
int2 centerIn1024ths,
int radiusIn1024ths,
bool outside,
Action<int2, float> action)
Action<int2, long> action)
{
var size = matrix.Size;
int minX;
@@ -1456,10 +1461,10 @@ namespace OpenRA.Mods.Common.MapGenerator
}
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);
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)
@@ -1470,16 +1475,32 @@ namespace OpenRA.Mods.Common.MapGenerator
maxY = size.Y - 1;
}
var radiusSquared = radius * radius;
var radiusSquared = (long)radiusIn1024ths * radiusIn1024ths;
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;
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);
}
}
/// <summary>
/// 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.
/// </summary>
public static Matrix<int> NormalizeRangeInPlace(Matrix<int> 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;
}
}
}

View File

@@ -24,7 +24,7 @@ namespace OpenRA.Mods.Common.MapGenerator
/// </summary>
public sealed class MultiBrushInfo
{
public readonly float Weight;
public readonly int Weight;
public readonly ImmutableArray<string> Actors;
public readonly TerrainTile? BackingTile;
public readonly ImmutableArray<ushort> Templates;
@@ -33,7 +33,7 @@ namespace OpenRA.Mods.Common.MapGenerator
// Currently doesn't support specifying offsets. Add this capability if/when needed.
public MultiBrushInfo(MiniYaml my)
{
Weight = 1.0f;
Weight = MultiBrush.DefaultWeight;
var actors = new List<string>();
var templates = new List<ushort>();
var tiles = new List<TerrainTile>();
@@ -41,7 +41,7 @@ namespace OpenRA.Mods.Common.MapGenerator
switch (node.Key.Split('@')[0])
{
case "Weight":
if (!Exts.TryParseFloatOrPercentInvariant(node.Value.Value, out Weight))
if (!Exts.TryParseInt32Invariant(node.Value.Value, out Weight))
throw new YamlException($"Invalid MultiBrush Weight `${node.Value.Value}`");
break;
case "Actor":
@@ -89,6 +89,8 @@ namespace OpenRA.Mods.Common.MapGenerator
/// <summary>A super template that can be used to paint both tiles and actors.</summary>
sealed class MultiBrush
{
public const int DefaultWeight = 1000;
public enum Replaceability
{
/// <summary>Area cannot be replaced by a tile or obstructing actor.</summary>
@@ -104,7 +106,7 @@ namespace OpenRA.Mods.Common.MapGenerator
Any = 3,
}
public float Weight;
public int Weight;
readonly List<(CVec, TerrainTile)> tiles;
readonly List<ActorPlan> actorPlans;
CVec[] shape;
@@ -134,7 +136,7 @@ namespace OpenRA.Mods.Common.MapGenerator
/// </summary>
public MultiBrush()
{
Weight = 1.0f;
Weight = DefaultWeight;
tiles = new List<(CVec, TerrainTile)>();
actorPlans = new List<ActorPlan>();
shape = Array.Empty<CVec>();
@@ -264,10 +266,10 @@ namespace OpenRA.Mods.Common.MapGenerator
}
/// <summary>Update the weight.</summary>
public MultiBrush WithWeight(float weight)
public MultiBrush WithWeight(int weight)
{
if (!(weight > 0.0f))
throw new ArgumentException("Weight was not > 0.0");
if (weight <= 0)
throw new ArgumentException("Weight was not > 0");
Weight = weight;
return this;
}
@@ -438,7 +440,7 @@ namespace OpenRA.Mods.Common.MapGenerator
var remainingQuota =
brushArea == 1
? int.MaxValue
: (int)Math.Ceiling(replaceMposes.Count * brushWeightForArea / brushTotalWeight);
: (int)(((long)replaceMposes.Count * brushWeightForArea + brushTotalWeight - 1) / brushTotalWeight);
RefreshIndices();
foreach (var mpos in mposes)
{

View File

@@ -10,21 +10,25 @@
#endregion
using System;
using System.Numerics;
using OpenRA.Support;
namespace OpenRA.Mods.Common.MapGenerator
{
public static class NoiseUtils
{
const int Scale = 1024;
const int ScaledSqrt2 = 1448;
/// <summary>Amplitude proportional to wavelength.</summary>
public static float PinkAmplitude(float wavelength) => wavelength;
public static int PinkAmplitude(int wavelength) => wavelength;
/// <summary>
/// <para>
/// Create noise by combining multiple layers of Perlin noise of halving wavelengths.
/// </para>
/// <para>
/// wavelengthScale defines the largest wavelength as a fraction of the largest dimension of
/// featureSize defines the largest wavelength in 1024ths of a matrix cell.
/// the output.
/// </para>
/// <para>
@@ -32,38 +36,45 @@ namespace OpenRA.Mods.Common.MapGenerator
/// choice.
/// </para>
/// </summary>
public static Matrix<float> FractalNoise(
public static Matrix<int> FractalNoise(
MersenneTwister random,
int2 size,
float featureSize,
Func<float, float> ampFunc)
int featureSize,
Func<int, int> ampFunc)
{
var span = Math.Max(size.X, size.Y);
var wavelengths = new float[(int)Math.Log2(span)];
var wavelengths = new int[BitOperations.Log2((uint)span)];
for (var i = 0; i < wavelengths.Length; i++)
wavelengths[i] = featureSize / (1 << i);
wavelengths[i] = featureSize >> i;
var noise = new Matrix<float>(size);
var noise = new Matrix<int>(size);
foreach (var wavelength in wavelengths)
{
if (wavelength <= 0.5)
if (wavelength <= Scale / 2)
break;
var amps = ampFunc(wavelength);
var subSpan = (int)(span / wavelength) + 2;
var subSpan = span * Scale / wavelength + 2;
var subNoise = PerlinNoise(random, subSpan);
// Offsets should align to grid.
// (The wavelength is divided back out later.)
var offsetX = (int)(random.NextFloat() * wavelength);
var offsetY = (int)(random.NextFloat() * wavelength);
var scaledOffsetX = (int)(random.NextUint() % (wavelength + 1));
var scaledOffsetY = (int)(random.NextUint() % (wavelength + 1));
for (var y = 0; y < size.Y; y++)
for (var x = 0; x < size.X; x++)
{
var scaledMappedX = x * Scale + scaledOffsetX;
var scaledMappedY = y * Scale + scaledOffsetY;
noise[y * size.X + x] +=
amps * MatrixUtils.Interpolate(
amps * MatrixUtils.IntegerInterpolate(
subNoise,
(offsetX + x) / wavelength,
(offsetY + y) / wavelength);
scaledMappedX / wavelength,
scaledMappedY / wavelength,
scaledMappedX % wavelength,
scaledMappedY % wavelength,
wavelength);
}
}
return noise;
@@ -71,25 +82,25 @@ namespace OpenRA.Mods.Common.MapGenerator
/// <summary>
/// 2D Perlin Noise generator without interpolation, producing a span-by-span sized matrix.
/// Output values range from -5792 to +5792.
/// </summary>
public static Matrix<float> PerlinNoise(MersenneTwister random, int span)
public static Matrix<int> PerlinNoise(MersenneTwister random, int span)
{
var noise = new Matrix<float>(span, span);
const float D = 0.25f;
var noise = new Matrix<int>(span, span);
for (var y = 0; y <= span; y++)
for (var x = 0; x <= span; x++)
{
var phase = MathF.Tau * random.NextFloatExclusive();
var vx = MathF.Cos(phase);
var vy = MathF.Sin(phase);
var phase = new WAngle((int)random.NextUint() % 1024);
var vx = phase.Cos();
var vy = phase.Sin();
if (x > 0 && y > 0)
noise[x - 1, y - 1] += vx * -D + vy * -D;
noise[x - 1, y - 1] += -vx + -vy;
if (x < span && y > 0)
noise[x, y - 1] += vx * D + vy * -D;
noise[x, y - 1] += vx + -vy;
if (x > 0 && y < span)
noise[x - 1, y] += vx * -D + vy * D;
noise[x - 1, y] += -vx + vy;
if (x < span && y < span)
noise[x, y] += vx * D + vy * D;
noise[x, y] += vx + vy;
}
return noise;
@@ -105,13 +116,13 @@ namespace OpenRA.Mods.Common.MapGenerator
/// noise with different properties to simple Perlin noise.
/// </para>
/// </summary>
public static Matrix<float> SymmetricFractalNoise(
public static Matrix<int> SymmetricFractalNoise(
MersenneTwister random,
int2 size,
int rotations,
Symmetry.Mirror mirror,
float featureSize,
Func<float, float> ampFunc)
int featureSize,
Func<int, int> ampFunc)
{
if (rotations < 1)
throw new ArgumentException("rotations must be >= 1");
@@ -119,32 +130,37 @@ namespace OpenRA.Mods.Common.MapGenerator
// Need higher resolution due to cropping and rotation artifacts
var templateSpan = Math.Max(size.X, size.Y) * 2 + 2;
var templateSize = new int2(templateSpan, templateSpan);
var templateCenter = new float2(templateSpan - 1, templateSpan - 1) / 2.0f;
var scaledTemplateCenter = new int2(templateSpan - 1, templateSpan - 1) * Scale / 2;
var template = FractalNoise(random, templateSize, featureSize, ampFunc);
var output = new Matrix<float>(size);
var output = new Matrix<int>(size);
var inclusiveOutputSize = size - new int2(1, 1);
var outputMid = new float2(inclusiveOutputSize) / 2.0f;
var scaledOutputMid = inclusiveOutputSize * Scale / 2;
for (var y = 0; y < size.Y; y++)
for (var x = 0; x < size.X; x++)
{
const float Sqrt2 = 1.4142135623730951f;
var outputXy = new float2(x, y);
var outputXyFromCenter = outputXy - outputMid;
var templateXyFromCenter = outputXyFromCenter * Sqrt2;
var templateXy = templateXyFromCenter + templateCenter;
var outputXy = new int2(x, y);
var scaledOutputXy = outputXy * Scale;
var scaledOutputXyFromCenter = scaledOutputXy - scaledOutputMid;
// Apply sqrt2 scaling so that diagonal samples don't alias.
var scaledTemplateXyFromCenter = scaledOutputXyFromCenter * ScaledSqrt2 / Scale;
var scaledTemplateXy = scaledTemplateXyFromCenter + scaledTemplateCenter;
var projections = Symmetry.RotateAndMirrorPointAround(
templateXy, templateCenter, rotations, mirror);
scaledTemplateXy, scaledTemplateCenter, rotations, mirror);
foreach (var projection in projections)
output[x, y] +=
MatrixUtils.Interpolate(
MatrixUtils.IntegerInterpolate(
template,
projection.X,
projection.Y);
projection.X / Scale,
projection.Y / Scale,
projection.X % Scale,
projection.Y % Scale,
Scale);
}
return output;
@@ -156,11 +172,11 @@ namespace OpenRA.Mods.Common.MapGenerator
/// </summary>
public static void SymmetricFractalNoiseIntoCellLayer(
MersenneTwister random,
CellLayer<float> cellLayer,
CellLayer<int> cellLayer,
int rotations,
Symmetry.Mirror mirror,
float featureSize,
Func<float, float> ampFunc)
int featureSize,
Func<int, int> ampFunc)
{
var cellBounds = CellLayerUtils.CellBounds(cellLayer);
var size = new int2(cellBounds.Size.Width, cellBounds.Size.Height);

View File

@@ -44,26 +44,26 @@ namespace OpenRA.Mods.Common.MapGenerator
/// Mirrors a (zero-area) point around a given center.
/// </para>
/// <para>
/// For example, if using a center of (4.0, 4.0) a point at (0.1, 0.1) could be projected
/// to (0.1, 0.1), (0.1, 7.9), (7.9, 0.1), or (7.9, 7.9).
/// For example, if using a center of (40, 40) a point at (1, 1) could be projected
/// to (1, 1), (1, 79), (79, 1), or (79, 79).
/// </para>
/// </summary>
public static float2 MirrorPointAround(Mirror mirror, float2 original, float2 center)
public static int2 MirrorPointAround(Mirror mirror, int2 original, int2 center)
{
switch (mirror)
{
case Mirror.None:
throw new ArgumentException("Mirror.None has no transformed point");
case Mirror.LeftMatchesRight:
return new float2(2.0f * center.X - original.X, original.Y);
return new int2(2 * center.X - original.X, original.Y);
case Mirror.TopLeftMatchesBottomRight:
return new float2(
return new int2(
center.Y - original.Y + center.X,
center.X - original.X + center.Y);
case Mirror.TopMatchesBottom:
return new float2(original.X, 2.0f * center.Y - original.Y);
return new int2(original.X, 2 * center.Y - original.Y);
case Mirror.TopRightMatchesBottomLeft:
return new float2(
return new int2(
center.X + original.Y - center.Y,
center.Y + original.X - center.X);
default:
@@ -73,27 +73,11 @@ namespace OpenRA.Mods.Common.MapGenerator
public static WPos MirrorWPosAround(Mirror mirror, WPos original, WPos center)
{
switch (mirror)
{
case Mirror.None:
throw new ArgumentException("Mirror.None has no transformed point");
case Mirror.LeftMatchesRight:
return new WPos(2 * center.X - original.X, original.Y, original.Z);
case Mirror.TopLeftMatchesBottomRight:
return new WPos(
center.Y - original.Y + center.X,
center.X - original.X + center.Y,
original.Z);
case Mirror.TopMatchesBottom:
return new WPos(original.X, 2 * center.Y - original.Y, original.Z);
case Mirror.TopRightMatchesBottomLeft:
return new WPos(
center.X + original.Y - center.Y,
center.Y + original.X - center.X,
original.Z);
default:
throw new ArgumentException("Bad mirror");
}
var result = MirrorPointAround(
mirror,
new int2(original.X, original.Y),
new int2(center.X, center.Y));
return new WPos(result.X, result.Y, original.Z);
}
/// <summary>
@@ -199,17 +183,17 @@ namespace OpenRA.Mods.Common.MapGenerator
/// functions. This may be slightly imprecise for non-trivial rotations.
/// </para>
/// <para>
/// For example, if using a center of (4.0, 4.0) a point at (0.1, 0.1) could be projected
/// to (0.1, 0.1), (0.1, 7.9), (7.9, 0.1), and (7.9, 7.9).
/// For example, if using a center of (40, 40) a point at (1, 1) could be projected
/// to (1, 1), (1, 79), (79, 1), and (79, 79).
/// </para>
/// </summary>
public static float2[] RotateAndMirrorPointAround(
float2 original,
float2 center,
public static int2[] RotateAndMirrorPointAround(
int2 original,
int2 center,
int rotations,
Mirror mirror)
{
var projections = new float2[RotateAndMirrorProjectionCount(rotations, mirror)];
var projections = new int2[RotateAndMirrorProjectionCount(rotations, mirror)];
var projectionIndex = 0;
for (var rotation = 0; rotation < rotations; rotation++)
@@ -217,12 +201,12 @@ namespace OpenRA.Mods.Common.MapGenerator
// This could be made more accurate using dedicated, higher precision
// rotation count to cos and sin lookup tables.
var wangle = new WAngle(rotation * 1024 / rotations);
var cos = wangle.Cos() / 1024.0f;
var sin = wangle.Sin() / 1024.0f;
long cos = wangle.Cos();
long sin = wangle.Sin();
var relOrig = original - center;
var projX = relOrig.X * cos - relOrig.Y * sin + center.X;
var projY = relOrig.X * sin + relOrig.Y * cos + center.Y;
var projection = new float2(projX, projY);
var projX = (relOrig.X * cos - relOrig.Y * sin) / 1024 + center.X;
var projY = (relOrig.X * sin + relOrig.Y * cos) / 1024 + center.Y;
var projection = new int2((int)projX, (int)projY);
projections[projectionIndex++] = projection;
if (mirror != Mirror.None)
@@ -303,7 +287,7 @@ namespace OpenRA.Mods.Common.MapGenerator
public static bool IsCPosNearCenter<T>(
CPos cpos,
CellLayer<T> cellLayer,
float reservationRadius,
int reservationRadius,
Mirror mirror)
{
CPos[] testPoints;
@@ -313,7 +297,7 @@ namespace OpenRA.Mods.Common.MapGenerator
testPoints = RotateAndMirrorCPos(cpos, cellLayer, 1, mirror);
var separation = (testPoints[1] - testPoints[0]).LengthSquared;
return separation <= reservationRadius * reservationRadius * 4.0f;
return separation <= reservationRadius * reservationRadius * 4;
}
}
}