This change lifts large portions of ExperimentalMapGenerator's logic into a new "Terraformer" class with highly documented methods that can theoretically be used by alternative map generator classes. Some additional refactoring may occur in future, subject to the practical needs of additional map generators. ClearMapGenerator is also simplified. This is not a pure refactor and contains some algorithmic and behavioral changes, as well as few minor bug fixes. Notably: - The logic for obstructing unreachable water has been somewhat replaced. - In CnC, where water is already unplayable, EMG no longer obstructs water. (RA still does, as water is playable there.) - Mountain (cliff) generation is now somewhat more effective. This increases the number of cliffs seen on many presets. Settings should no longer use "Mountains: 1000". - PlayableSpace logic has been consolidated into Terraformer. - PlayableSpace.Playability no longer exists as PartiallyPlayable became redundant. Its uses have been replaced with a simple boolean. Consequently, some defunct code and configuration has been removed. - Adjust MultiBrush replaceability contract painting behavior to be more intuitive. - Fix off-by-one error in map bounds computation. - Fix some usages of mixed up MersenneTwisters.
209 lines
6.2 KiB
C#
209 lines
6.2 KiB
C#
#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.Numerics;
|
|
using OpenRA.Support;
|
|
|
|
namespace OpenRA.Mods.Common.MapGenerator
|
|
{
|
|
public static class NoiseUtils
|
|
{
|
|
const int Scale = 1024;
|
|
const int ScaledSqrt2 = 1448;
|
|
|
|
/// <summary>Amplitude is the same for all wavelengths.</summary>
|
|
public static int WhiteAmplitude(int wavelength) => 1;
|
|
|
|
/// <summary>Amplitude proportional to wavelength.</summary>
|
|
public static int PinkAmplitude(int wavelength) => wavelength;
|
|
|
|
/// <summary>
|
|
/// <code>amplitude = wavelength ** (1 / (2 ** clumpiness))</code>
|
|
/// Setting clumpiness to 0 is equivalent to pink noise.
|
|
/// </summary>
|
|
public static int ClumpinessAmplitude(int wavelength, int clumpiness)
|
|
{
|
|
var amplitude = wavelength;
|
|
for (var i = 0; i < clumpiness; i++)
|
|
amplitude = Exts.ISqrt(amplitude);
|
|
return amplitude;
|
|
}
|
|
|
|
/// <summary>
|
|
/// <para>
|
|
/// Create noise by combining multiple layers of Perlin noise of halving wavelengths.
|
|
/// </para>
|
|
/// <para>
|
|
/// featureSize defines the largest wavelength in 1024ths of a matrix cell.
|
|
/// the output.
|
|
/// </para>
|
|
/// <para>
|
|
/// ampFunc specifies the amplitude of each wavelength. PinkAmplitude is often a suitable
|
|
/// choice.
|
|
/// </para>
|
|
/// </summary>
|
|
public static Matrix<int> FractalNoise(
|
|
MersenneTwister random,
|
|
int2 size,
|
|
int featureSize,
|
|
Func<int, int> ampFunc)
|
|
{
|
|
var span = Math.Max(size.X, size.Y);
|
|
var wavelengths = new int[BitOperations.Log2((uint)span)];
|
|
for (var i = 0; i < wavelengths.Length; i++)
|
|
wavelengths[i] = featureSize >> i;
|
|
|
|
var noise = new Matrix<int>(size);
|
|
foreach (var wavelength in wavelengths)
|
|
{
|
|
if (wavelength <= Scale / 2)
|
|
break;
|
|
|
|
var amps = ampFunc(wavelength);
|
|
var subSpan = span * Scale / wavelength + 2;
|
|
var subNoise = PerlinNoise(random, subSpan);
|
|
|
|
// Offsets should align to grid.
|
|
// (The wavelength is divided back out later.)
|
|
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.IntegerInterpolate(
|
|
subNoise,
|
|
scaledMappedX / wavelength,
|
|
scaledMappedY / wavelength,
|
|
scaledMappedX % wavelength,
|
|
scaledMappedY % wavelength,
|
|
wavelength);
|
|
}
|
|
}
|
|
|
|
return noise;
|
|
}
|
|
|
|
/// <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<int> PerlinNoise(MersenneTwister random, int span)
|
|
{
|
|
var noise = new Matrix<int>(span, span);
|
|
for (var y = 0; y <= span; y++)
|
|
for (var x = 0; x <= span; x++)
|
|
{
|
|
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 + -vy;
|
|
if (x < span && y > 0)
|
|
noise[x, y - 1] += vx + -vy;
|
|
if (x > 0 && y < span)
|
|
noise[x - 1, y] += -vx + vy;
|
|
if (x < span && y < span)
|
|
noise[x, y] += vx + vy;
|
|
}
|
|
|
|
return noise;
|
|
}
|
|
|
|
/// <summary>
|
|
/// <para>
|
|
/// Produce symmetric 2D noise by repeatedly applying some generated Perlin noise under
|
|
/// rotation and mirroring.
|
|
/// </para>
|
|
/// <para>
|
|
/// Note that the combination of multiple noise values with varying correlations creates a
|
|
/// noise with different properties to simple Perlin noise.
|
|
/// </para>
|
|
/// </summary>
|
|
public static Matrix<int> SymmetricFractalNoise(
|
|
MersenneTwister random,
|
|
int2 size,
|
|
int rotations,
|
|
Symmetry.Mirror mirror,
|
|
int featureSize,
|
|
Func<int, int> ampFunc)
|
|
{
|
|
if (rotations < 1)
|
|
throw new ArgumentException("rotations must be >= 1");
|
|
|
|
// 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 scaledTemplateCenter = new int2(templateSpan - 1, templateSpan - 1) * Scale / 2;
|
|
var template = FractalNoise(random, templateSize, featureSize, ampFunc);
|
|
|
|
var output = new Matrix<int>(size);
|
|
|
|
var inclusiveOutputSize = size - new int2(1, 1);
|
|
var scaledOutputMid = inclusiveOutputSize * Scale / 2;
|
|
|
|
for (var y = 0; y < size.Y; y++)
|
|
for (var x = 0; x < size.X; x++)
|
|
{
|
|
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(
|
|
scaledTemplateXy, scaledTemplateCenter, rotations, mirror);
|
|
|
|
foreach (var projection in projections)
|
|
output[x, y] +=
|
|
MatrixUtils.IntegerInterpolate(
|
|
template,
|
|
projection.X / Scale,
|
|
projection.Y / Scale,
|
|
projection.X % Scale,
|
|
projection.Y % Scale,
|
|
Scale);
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Use SymmetricFractalNoise to fill a CellLayer. The noise is aligned to the CPos
|
|
/// coordinate system.
|
|
/// </summary>
|
|
public static void SymmetricFractalNoiseIntoCellLayer(
|
|
MersenneTwister random,
|
|
CellLayer<int> cellLayer,
|
|
int rotations,
|
|
Symmetry.Mirror mirror,
|
|
int featureSize,
|
|
Func<int, int> ampFunc)
|
|
{
|
|
var cellBounds = CellLayerUtils.CellBounds(cellLayer);
|
|
var size = new int2(cellBounds.Size.Width, cellBounds.Size.Height);
|
|
var noise = SymmetricFractalNoise(
|
|
random,
|
|
size,
|
|
rotations,
|
|
mirror,
|
|
featureSize,
|
|
ampFunc);
|
|
CellLayerUtils.FromMatrix(cellLayer, noise);
|
|
}
|
|
}
|
|
}
|