1
0
forked from mirrors/0ad

Implement SmoothingPainter for random maps, fixes #5027.

This allows only specific regions of the map to be smoothened,
especially important on imported digital elevation models.
It uses the Inverse Distance Weighting / Shepard's method as mentioned
by Imarok and formerly implemented in the Pyrenean Sierra map by wraitii
in a796800bb1.

Supersedes the globalSmoothHeightmap function in FeXoRs heightmap
library, refs #3764.
Drop the heightmap argument to be consistent with the other painters.
If painting on arbitrary heightmaps is wished, the createArea mechanism,
all Placers, Painters, Constraints and Areas can and should support
that.

Update the HeightmapPainter from 6319647795 to not break if
TILE_CENTERED_HEIGHT_MAP is enabled (i.e. numVertices = numTiles), refs
#5018.
Use that mode on Mediterranean and Red Sea.
Drop the disabling of bicubic interpolation in the HeightmapPainter
instead of extending it to this feature.

Inevitable smoothing performance improvement for Belgian Uplands (from
45 to 15 seconds per call), even if it implies a somewhat different
outcome, refs #5011.

This was SVN commit r21175.
This commit is contained in:
elexis
2018-02-11 14:56:55 +00:00
parent 9501292661
commit 02fe3ef3e3
18 changed files with 160 additions and 145 deletions
@@ -19,7 +19,6 @@
Engine.LoadLibrary("rmgen");
Engine.LoadLibrary("rmgen2");
Engine.LoadLibrary("rmbiome");
Engine.LoadLibrary("heightmap");
setBiome("generic/desert");
setLandBiome();
@@ -93,7 +92,9 @@ createArea(
Engine.SetProgress(20);
g_Map.log("Smoothing heightmap");
globalSmoothHeightmap(scaleByMapSize(0.1, 0.5));
createArea(
new MapBoundsPlacer(),
new SmoothingPainter(1, scaleByMapSize(0.1, 0.5), 1));
Engine.SetProgress(25);
g_Map.log("Marking water");
@@ -6,27 +6,21 @@ const tPrimary = ["temp_grass", "temp_grass_b", "temp_grass_c", "temp_grass_d",
"temp_grass_long_b", "temp_grass_clovers_2", "temp_grass_mossy", "temp_grass_plants"];
const heightLand = 0;
var g_Map = new RandomMap(heightLand, tPrimary);
var numPlayers = getNumPlayers();
var mapSize = g_Map.getSize();
var mapCenter = g_Map.getCenter();
// Set target min and max height depending on map size to make average stepness the same on all map sizes
var heightRange = {"min": MIN_HEIGHT * mapSize / 8192, "max": MAX_HEIGHT * mapSize / 8192};
// Set average water coverage
var averageWaterCoverage = 1/3; // NOTE: Since errosion is not predictable actual water coverage might differ much with the same value
if (mapSize < 200) // Sink the waterlevel on tiny maps to ensure enough space
averageWaterCoverage = 2/3 * averageWaterCoverage;
// Since erosion is not predictable, actual water coverage can differ much with the same value
var averageWaterCoverage = scaleByMapSize(1/5, 1/3);
var heightSeaGround = -MIN_HEIGHT + heightRange.min + averageWaterCoverage * (heightRange.max - heightRange.min);
var heightSeaGroundAdjusted = heightSeaGround + MIN_HEIGHT;
setWaterHeight(heightSeaGround);
// Prepare terrain texture by height placement
var textueByHeight = [];
// Deep water
@@ -94,8 +88,9 @@ while (true)
// More cycles yield bigger structures
g_Map.log("Smoothing map");
for (let i = 0; i < 50 + mapSize/4; ++i)
globalSmoothHeightmap();
createArea(
new MapBoundsPlacer(),
new SmoothingPainter(2, 1, 20));
g_Map.log("Rescaling map");
rescaleHeightmap(heightRange.min, heightRange.max, g_Map.height);
@@ -209,32 +209,6 @@ function setBaseTerrainDiamondSquare(minHeight = MIN_HEIGHT, maxHeight = MAX_HEI
heightmap[x][y] = newHeightmap[x + shift[0]][y + shift[1]];
}
/**
* Smoothens the entire map
* @param {float} [strength=0.8] - How strong the smooth effect should be: 0 means no effect at all, 1 means quite strong, higher values might cause interferences, better apply it multiple times
* @param {array} [heightmap=g_Map.height] - The heightmap to be smoothed
* @param {array} [smoothMap=[[1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1], [1, -1]]] - Array of offsets discribing the neighborhood tiles to smooth the height of a tile to
*/
function globalSmoothHeightmap(strength = 0.8, heightmap = g_Map.height, smoothMap = [[1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1], [1, -1]])
{
let referenceHeightmap = clone(heightmap);
let max_x = heightmap.length;
let max_y = heightmap[0].length;
for (let x = 0; x < max_x; ++x)
{
for (let y = 0; y < max_y; ++y)
{
for (let i = 0; i < smoothMap.length; ++i)
{
let mapX = x + smoothMap[i][0];
let mapY = y + smoothMap[i][1];
if (mapX >= 0 && mapX < max_x && mapY >= 0 && mapY < max_y)
heightmap[x][y] += strength / smoothMap.length * (referenceHeightmap[mapX][mapY] - referenceHeightmap[x][y]);
}
}
}
}
/**
* Pushes a rectangular area towards a given height smoothing it into the original terrain
* @note The window function to determine the smooth is not exactly a gaussian to ensure smooth edges
@@ -1,7 +1,6 @@
Engine.LoadLibrary("rmgen");
Engine.LoadLibrary("rmgen2");
Engine.LoadLibrary("rmbiome");
Engine.LoadLibrary("heightmap");
const g_InitialMineDistance = 14;
const g_InitialTrees = 50;
@@ -203,8 +202,9 @@ createAreas(
Engine.SetProgress(70);
g_Map.log("Smoothing heightmap");
for (let i = 0; i < 5; ++i)
globalSmoothHeightmap();
createArea(
new MapBoundsPlacer(),
new SmoothingPainter(1, 0.8, 5));
// repaint clLand to compensate for smoothing
unPaintTileClassBasedOnHeight(-10, 10, 3, clLand);
@@ -249,8 +249,10 @@ createAreas(
scaleByMapSize(4, 13)
);
for (let i = 0; i < 3; ++i)
globalSmoothHeightmap();
g_Map.log("Smoothing heightmap");
createArea(
new MapBoundsPlacer(),
new SmoothingPainter(0.8, 1, 3));
createStragglerTrees(
[oTree1, oTree2, oTree4, oTree3],
@@ -18,7 +18,6 @@
Engine.LoadLibrary("rmgen");
Engine.LoadLibrary("rmgen2");
Engine.LoadLibrary("rmbiome");
Engine.LoadLibrary("heightmap");
setBiome("generic/mediterranean");
@@ -76,7 +75,9 @@ createArea(
Engine.SetProgress(20);
g_Map.log("Smoothing heightmap");
globalSmoothHeightmap(scaleByMapSize(0.1, 0.2));
createArea(
new MapBoundsPlacer(),
new SmoothingPainter(1, scaleByMapSize(0.1, 0.2), 1));
Engine.SetProgress(25);
g_Map.log("Marking water");
@@ -19,7 +19,8 @@
Engine.LoadLibrary("rmgen");
Engine.LoadLibrary("rmgen2");
Engine.LoadLibrary("rmbiome");
Engine.LoadLibrary("heightmap");
TILE_CENTERED_HEIGHT_MAP = true;
const tWater = "medit_sand_wet";
const tSnowedRocks = ["alpine_cliff_b", "alpine_cliff_snow"];
@@ -92,7 +93,9 @@ createArea(
Engine.SetProgress(20);
g_Map.log("Smoothing heightmap");
globalSmoothHeightmap(scaleByMapSize(0.3, 0.8));
createArea(
new MapBoundsPlacer(),
new SmoothingPainter(1, scaleByMapSize(0.3, 0.8), 1));
Engine.SetProgress(25);
g_Map.log("Marking water");
@@ -19,7 +19,6 @@
Engine.LoadLibrary("rmgen");
Engine.LoadLibrary("rmgen2");
Engine.LoadLibrary("rmbiome");
Engine.LoadLibrary("heightmap");
setBiome("generic/savanna");
@@ -94,7 +93,9 @@ g_Map.LoadHeightmapImage("ngorongoro.png", 0, heightMax);
Engine.SetProgress(15);
g_Map.log("Smoothing heightmap");
globalSmoothHeightmap(scaleByMapSize(0.1, 0.5));
createArea(
new MapBoundsPlacer(),
new SmoothingPainter(1, scaleByMapSize(0.1, 0.5), 1));
Engine.SetProgress(25);
g_Map.log("Marking land");
@@ -19,7 +19,6 @@
Engine.LoadLibrary("rmgen");
Engine.LoadLibrary("rmgen2");
Engine.LoadLibrary("rmbiome");
Engine.LoadLibrary("heightmap");
setBiome("generic/mediterranean");
@@ -97,7 +96,9 @@ createArea(
Engine.SetProgress(20);
g_Map.log("Smoothing heightmap");
globalSmoothHeightmap(0.8);
createArea(
new MapBoundsPlacer(),
new SmoothingPainter(1, 0.8, 1));
Engine.SetProgress(25);
g_Map.log("Marking water");
@@ -242,20 +242,6 @@ function createPyreneans()
}
}
g_Map.log("Smoothing pyreneans");
for (let ix = 1; ix < mapSize - 1; ++ix)
for (let iz = 1; iz < mapSize - 1; ++iz)
{
let position = new Vector2D(ix, iz);
if (g_Map.validHeight(position) && clPyrenneans.countMembersInRadius(position, 1))
{
let height = g_Map.getHeight(position);
let index = 1 / (1 + Math.max(0, height / 7));
g_Map.setHeight(position, height * (1 - index) + g_Map.getAverageHeight(position) * index);
}
}
Engine.SetProgress(48);
g_Map.log("Creating passages");
var passageLocation = 0.35;
var passageVec = mountainDirection.perpendicular().mult(passageLength);
@@ -278,18 +264,11 @@ for (let passLoc of [passageLocation, 1 - passageLocation])
}
Engine.SetProgress(50);
g_Map.log("Smoothing the mountains");
for (let ix = 1; ix < mapSize - 1; ++ix)
for (let iz = 1; iz < mapSize - 1; ++iz)
{
let position = new Vector2D(ix, iz);
if (g_Map.inMapBounds(position) && clPyrenneans.countMembersInRadius(position, 1))
{
let heightNeighbor = g_Map.getAverageHeight(position);
let index = 1 / (1 + Math.max(0, (g_Map.getHeight(position) - 10) / 7));
g_Map.setHeight(position, g_Map.getHeight(position) * (1 - index) + heightNeighbor * index);
}
}
g_Map.log("Smoothing the pyreneans");
createArea(
new MapBoundsPlacer(),
new SmoothingPainter(1, 0.3, 1),
new NearTileClassConstraint(clPyrenneans, 1));
g_Map.log("Creating oceans");
for (let ocean of distributePointsOnCircle(2, oceanAngle, fractionToTiles(0.48), mapCenter)[0])
@@ -301,27 +280,10 @@ for (let ocean of distributePointsOnCircle(2, oceanAngle, fractionToTiles(0.48),
]);
g_Map.log("Smoothing around the water");
var smoothDist = 5;
for (let ix = 1; ix < mapSize - 1; ++ix)
for (let iz = 1; iz < mapSize - 1; ++iz)
{
let position = new Vector2D(ix, iz);
if (!g_Map.inMapBounds(position) || !clWater.countMembersInRadius(position, smoothDist))
continue;
let averageHeight = 0;
let todivide = 0;
for (let xx = -smoothDist; xx <= smoothDist; ++xx)
for (let yy = -smoothDist; yy <= smoothDist; ++yy)
{
let smoothPos = Vector2D.add(position, new Vector2D(xx, yy));
if (g_Map.inMapBounds(smoothPos) && (xx != 0 || yy != 0))
{
averageHeight += g_Map.getHeight(smoothPos) / (Math.abs(xx) + Math.abs(yy));
todivide += 1 / (Math.abs(xx) + Math.abs(yy));
}
}
g_Map.setHeight(position, (averageHeight + 2 * g_Map.getHeight(position)) / (todivide + 2));
}
createArea(
new MapBoundsPlacer(),
new SmoothingPainter(5, 0.9, 1),
new NearTileClassConstraint(clWater, 5));
Engine.SetProgress(55);
g_Map.log("Creating hills");
@@ -19,7 +19,6 @@
Engine.LoadLibrary("rmgen");
Engine.LoadLibrary("rmgen2");
Engine.LoadLibrary("rmbiome");
Engine.LoadLibrary("heightmap");
setBiome("generic/alpine");
@@ -69,7 +68,9 @@ g_Map.LoadHeightmapImage("ratumacos.png", -3, 20);
Engine.SetProgress(15);
g_Map.log("Smoothing heightmap");
globalSmoothHeightmap(0.1);
createArea(
new MapBoundsPlacer(),
new SmoothingPainter(1, 0.1, 1));
Engine.SetProgress(25);
g_Map.log("Creating shallows...");
@@ -18,7 +18,8 @@
Engine.LoadLibrary("rmgen");
Engine.LoadLibrary("rmgen2");
Engine.LoadLibrary("rmbiome");
Engine.LoadLibrary("heightmap");
TILE_CENTERED_HEIGHT_MAP = true;
setBiome("generic/desert");
@@ -73,7 +74,9 @@ createArea(
Engine.SetProgress(20);
g_Map.log("Smoothing heightmap");
globalSmoothHeightmap(scaleByMapSize(0.1, 0.5));
createArea(
new MapBoundsPlacer(),
new SmoothingPainter(1, scaleByMapSize(0.1, 0.5), 1));
Engine.SetProgress(25);
g_Map.log("Marking water");
@@ -119,7 +122,7 @@ if (!isNomad())
let playerBases = placeRandom(
sortAllPlayers(),
[
avoidClasses(g_TileClasses.mountain, 5),
avoidClasses(g_TileClasses.mountain, scaleByMapSize(5, 10)),
stayClasses(g_TileClasses.land, defaultPlayerBaseRadius())
]);
@@ -1,6 +1,10 @@
const TERRAIN_SEPARATOR = "|";
const SEA_LEVEL = 20.0;
const HEIGHT_UNITS_PER_METRE = 92;
/**
* Number of impassable, unexplorable tiles at the map border.
*/
const MAP_BORDER_WIDTH = 3;
const g_DamageTypes = new DamageTypes();
@@ -1,4 +1,20 @@
const g_TileVertices = deepfreeze([new Vector2D(0, 0), new Vector2D(1, 0), new Vector2D(0, 1), new Vector2D(1, 1)]);
const g_TileVertices = deepfreeze([
new Vector2D(0, 0),
new Vector2D(0, 1),
new Vector2D(1, 0),
new Vector2D(1, 1)
]);
const g_AdjacentCoordinates = deepfreeze([
new Vector2D(1, 0),
new Vector2D(1, 1),
new Vector2D(0, 1),
new Vector2D(-1, 1),
new Vector2D(-1, 0),
new Vector2D(-1, -1),
new Vector2D(0, -1),
new Vector2D(1, -1)
]);
function diskArea(radius)
{
@@ -106,6 +106,72 @@ LayeredPainter.prototype.paint = function(area)
});
};
/**
* Applies smoothing to the given area using Inverse-Distance-Weighting / Shepard's method.
*
* @param {Number} size - Determines the number of neighboring heights to interpolate. The area is a square with the length twice this size.
* @param {Number} strength - Between 0 (no effect) and 1 (only neighbor heights count). This parameter has the lowest performance impact.
* @param {Number} iterations - How often the process should be repeated. Typically 1. Can be used to gain even more smoothing.
*/
function SmoothingPainter(size, strength, iterations)
{
if (size <= 0)
throw new Error("Invalid size: " + size);
if (strength <= 0 || strength > 1)
throw new Error("Invalid strength: " + strength);
if (iterations <= 0)
throw new Error("Invalid iterations: " + iterations);
this.size = size;
this.strength = strength;
this.iterations = iterations;
}
SmoothingPainter.prototype.paint = function(area)
{
let brushPoints = getPointsInBoundingBox(getBoundingBox(
new Array(2).fill(0).map((zero, i) => new Vector2D(1, 1).mult(this.size).floor().mult(i ? 1 : -1))));
for (let i = 0; i < this.iterations; ++i)
{
let heightmap = clone(g_Map.height);
// Additional complexity to process all 4 vertices of each tile, i.e the last row too
let seen = new Array(heightmap.length).fill(0).map(zero => new Uint8Array(heightmap.length).fill(0));
for (let point of area.points)
for (let tileVertex of g_TileVertices)
{
let vertex = Vector2D.add(point, tileVertex);
if (!g_Map.validHeight(vertex) || seen[vertex.x][vertex.y])
continue;
seen[vertex.x][vertex.y] = 1;
let sumWeightedHeights = 0;
let sumWeights = 0;
for (let brushPoint of brushPoints)
{
let position = Vector2D.add(vertex, brushPoint);
let distance = Math.abs(brushPoint.x) + Math.abs(brushPoint.y);
if (!distance || !g_Map.validHeight(position))
continue;
sumWeightedHeights += g_Map.getHeight(position) / distance;
sumWeights += 1 / distance;
}
g_Map.setHeight(
vertex,
this.strength * sumWeightedHeights / sumWeights +
(1 - this.strength) * g_Map.getHeight(vertex));
}
}
};
/**
* Sets the given height in the given Area.
*/
@@ -120,7 +186,7 @@ ElevationPainter.prototype.paint = function(area)
for (let vertex of g_TileVertices)
{
let position = Vector2D.add(point, vertex);
if (g_Map.inMapBounds(position))
if (g_Map.validHeight(position))
g_Map.setHeight(position, this.elevation);
}
};
@@ -337,7 +403,7 @@ function TerrainTextureArrayPainter(textureIDs, textureNames)
{
this.textureIDs = textureIDs;
this.textureNames = textureNames;
};
}
TerrainTextureArrayPainter.prototype.paint = function(area)
{
@@ -353,29 +419,21 @@ TerrainTextureArrayPainter.prototype.paint = function(area)
/**
* Copies the given heightmap to the given area.
* Scales the horizontal plane proportionally and optionally uses bicubic interpolation.
* Scales the horizontal plane proportionally and applies bicubic interpolation.
* The heightrange is either scaled proportionally or mapped to the given heightrange.
*
* @param {Uint16Array} heightmap - One dimensional array of vertex heights.
* @param {Number} [normalMinHeight] - The minimum height the elevation grid of 320 tiles would have.
* @param {Number} [normalMaxHeight] - The maximum height the elevation grid of 320 tiles would have.
*/
function HeightmapPainter(heightmap, bicubicInterpolation, normalMinHeight = undefined, normalMaxHeight = undefined)
function HeightmapPainter(heightmap, normalMinHeight = undefined, normalMaxHeight = undefined)
{
this.heightmap = heightmap;
this.bicubicInterpolation = bicubicInterpolation;
this.verticesPerSide = Math.sqrt(heightmap.length);
this.normalMinHeight = normalMinHeight;
this.normalMaxHeight = normalMaxHeight;
};
HeightmapPainter.prototype.paint = function(area)
{
if (this.bicubicInterpolation)
this.paintBicubic(area);
else
this.paintNearest(area);
};
}
HeightmapPainter.prototype.getScale = function()
{
@@ -393,17 +451,7 @@ HeightmapPainter.prototype.scaleHeight = function(height)
return minHeight + (maxHeight - minHeight) * height / 0xFFFF;
};
HeightmapPainter.prototype.paintNearest = function(area)
{
let scale = this.getScale();
for (let point of area.points)
{
let sourcePos = Vector2D.mult(point, scale).floor();
g_Map.setHeight(point, scaleHeight(this.heightmap[sourcePos.y * this.verticesPerSide + sourcePos.x]));
}
};
HeightmapPainter.prototype.paintBicubic = function(area)
HeightmapPainter.prototype.paint = function(area)
{
let scale = this.getScale();
let leftBottom = new Vector2D(0, 0);
@@ -412,16 +460,17 @@ HeightmapPainter.prototype.paintBicubic = function(area)
let brushCenter = new Vector2D(1, 1);
// Additional complexity to process all 4 vertices of each tile, i.e the last row too
let seen = new Array(g_Map.getSize() + 1).fill(0).map(zero => new Uint8Array(g_Map.getSize() + 1).fill(false));
let seen = new Array(g_Map.height.length).fill(0).map(zero => new Uint8Array(g_Map.height.length).fill(0));
for (let point of area.points)
for (let vertex of g_TileVertices)
{
let vertexPos = Vector2D.add(point, vertex);
if (seen[vertexPos.x][vertexPos.y])
if (!g_Map.validHeight(vertexPos) || seen[vertexPos.x][vertexPos.y])
continue;
seen[vertexPos.x][vertexPos.y] = true;
seen[vertexPos.x][vertexPos.y] = 1;
let sourcePos = Vector2D.mult(vertexPos, scale);
let sourceTilePos = sourcePos.clone().floor();
@@ -78,7 +78,7 @@ RandomMap.prototype.LoadMapTerrain = function(filename)
g_Map.log("Loading terrain file " + filename);
let mapTerrain = Engine.LoadMapTerrain("maps/random/" + filename + ".pmp");
let heightmapPainter = new HeightmapPainter(mapTerrain.height, true);
let heightmapPainter = new HeightmapPainter(mapTerrain.height);
createArea(
new MapBoundsPlacer(),
@@ -99,7 +99,7 @@ RandomMap.prototype.LoadHeightmapImage = function(filename, normalMinHeight, nor
{
g_Map.log("Loading heightmap " + filename);
let heightmapPainter = new HeightmapPainter(Engine.LoadHeightmapImage("maps/random/" + filename), true, normalMinHeight, normalMaxHeight);
let heightmapPainter = new HeightmapPainter(Engine.LoadHeightmapImage("maps/random/" + filename), normalMinHeight, normalMaxHeight);
createArea(
new MapBoundsPlacer(),
@@ -374,14 +374,12 @@ RandomMap.prototype.getAdjacentPoints = function(position)
{
let adjacentPositions = [];
for (let x = -1; x <= 1; ++x)
for (let z = -1; z <= 1; ++z)
if (x || z )
{
let adjacentPos = Vector2D.add(position, new Vector2D(x, z)).round();
if (this.inMapBounds(adjacentPos))
adjacentPositions.push(adjacentPos);
}
for (let adjacentCoordinate of g_AdjacentCoordinates)
{
let adjacentPos = Vector2D.add(position, adjacentCoordinate).round();
if (this.inMapBounds(adjacentPos))
adjacentPositions.push(adjacentPos);
}
return adjacentPositions;
};
@@ -116,8 +116,10 @@ var initialReliefmap = [[heightRange.max, heightRange.max, heightRange.max], [he
setBaseTerrainDiamondSquare(heightRange.min, heightRange.max, initialReliefmap);
for (var i = 0; i < 5; i++)
globalSmoothHeightmap();
g_Map.log("Smoothing map");
createArea(
new MapBoundsPlacer(),
new SmoothingPainter(1, 0.8, 5));
rescaleHeightmap(heightRange.min, heightRange.max);
@@ -1,5 +1,4 @@
Engine.LoadLibrary("rmgen");
Engine.LoadLibrary("heightmap");
const tMainTerrain = "alpine_snow_a";
const tTier1Terrain = "snow rough";
@@ -469,7 +469,10 @@ setBaseTerrainDiamondSquare(heightRange.min, heightRange.max, initialHeightmap,
// Apply simple erosion
for (let i = 0; i < 5; ++i)
splashErodeMap(0.1);
globalSmoothHeightmap();
createArea(
new MapBoundsPlacer(),
new SmoothingPainter(1, 0.8, 1));
// Final rescale
rescaleHeightmap(heightRange.min, heightRange.max);