From 8562545138ff3bbf66b2aa92b4a96e8318db96f4 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen Date: Mon, 30 Dec 2024 14:46:47 +0100 Subject: [PATCH 01/10] Move MapGen and MapHelpers units into map_generator folder --- src/openrct2-ui/windows/MapGen.cpp | 2 +- src/openrct2/libopenrct2.vcxproj | 8 ++-- .../world/{ => map_generator}/MapGen.cpp | 44 +++++++++---------- .../world/{ => map_generator}/MapGen.h | 4 +- .../world/{ => map_generator}/MapHelpers.cpp | 6 +-- .../world/{ => map_generator}/MapHelpers.h | 2 +- 6 files changed, 33 insertions(+), 33 deletions(-) rename src/openrct2/world/{ => map_generator}/MapGen.cpp (97%) rename src/openrct2/world/{ => map_generator}/MapGen.h (95%) rename src/openrct2/world/{ => map_generator}/MapHelpers.cpp (99%) rename src/openrct2/world/{ => map_generator}/MapHelpers.h (96%) diff --git a/src/openrct2-ui/windows/MapGen.cpp b/src/openrct2-ui/windows/MapGen.cpp index 353fcfbff6..52b1272d9d 100644 --- a/src/openrct2-ui/windows/MapGen.cpp +++ b/src/openrct2-ui/windows/MapGen.cpp @@ -25,7 +25,7 @@ #include #include #include -#include +#include namespace OpenRCT2::Ui::Windows { diff --git a/src/openrct2/libopenrct2.vcxproj b/src/openrct2/libopenrct2.vcxproj index 44f08b3617..a7ad8410df 100644 --- a/src/openrct2/libopenrct2.vcxproj +++ b/src/openrct2/libopenrct2.vcxproj @@ -616,8 +616,6 @@ - - @@ -626,6 +624,8 @@ + + @@ -1115,13 +1115,13 @@ - - + + diff --git a/src/openrct2/world/MapGen.cpp b/src/openrct2/world/map_generator/MapGen.cpp similarity index 97% rename from src/openrct2/world/MapGen.cpp rename to src/openrct2/world/map_generator/MapGen.cpp index 1c35ea05ba..2f1eaa7c80 100644 --- a/src/openrct2/world/MapGen.cpp +++ b/src/openrct2/world/map_generator/MapGen.cpp @@ -9,29 +9,29 @@ #include "MapGen.h" -#include "../Context.h" -#include "../Diagnostic.h" -#include "../Game.h" -#include "../GameState.h" -#include "../core/Guard.hpp" -#include "../core/Imaging.h" -#include "../core/String.hpp" -#include "../localisation/StringIds.h" -#include "../object/ObjectEntryManager.h" -#include "../object/ObjectList.h" -#include "../object/ObjectManager.h" -#include "../object/SmallSceneryEntry.h" -#include "../object/TerrainEdgeObject.h" -#include "../object/TerrainSurfaceObject.h" -#include "../platform/Platform.h" -#include "../util/Util.h" -#include "../world/tile_element/Slope.h" -#include "../world/tile_element/SmallSceneryElement.h" -#include "../world/tile_element/SurfaceElement.h" -#include "../world/tile_element/TileElement.h" -#include "Map.h" +#include "../../Context.h" +#include "../../Diagnostic.h" +#include "../../Game.h" +#include "../../GameState.h" +#include "../../core/Guard.hpp" +#include "../../core/Imaging.h" +#include "../../core/String.hpp" +#include "../../localisation/StringIds.h" +#include "../../object/ObjectEntryManager.h" +#include "../../object/ObjectList.h" +#include "../../object/ObjectManager.h" +#include "../../object/SmallSceneryEntry.h" +#include "../../object/TerrainEdgeObject.h" +#include "../../object/TerrainSurfaceObject.h" +#include "../../platform/Platform.h" +#include "../../util/Util.h" +#include "../../world/tile_element/Slope.h" +#include "../../world/tile_element/SmallSceneryElement.h" +#include "../../world/tile_element/SurfaceElement.h" +#include "../../world/tile_element/TileElement.h" +#include "../Map.h" +#include "../Scenery.h" #include "MapHelpers.h" -#include "Scenery.h" #include #include diff --git a/src/openrct2/world/MapGen.h b/src/openrct2/world/map_generator/MapGen.h similarity index 95% rename from src/openrct2/world/MapGen.h rename to src/openrct2/world/map_generator/MapGen.h index 2df619823a..aa3af57937 100644 --- a/src/openrct2/world/MapGen.h +++ b/src/openrct2/world/map_generator/MapGen.h @@ -9,8 +9,8 @@ #pragma once -#include "../core/StringTypes.h" -#include "Location.hpp" +#include "../../core/StringTypes.h" +#include "../Location.hpp" enum class MapGenAlgorithm : uint8_t { diff --git a/src/openrct2/world/MapHelpers.cpp b/src/openrct2/world/map_generator/MapHelpers.cpp similarity index 99% rename from src/openrct2/world/MapHelpers.cpp rename to src/openrct2/world/map_generator/MapHelpers.cpp index 42a2e43871..94efbe03b2 100644 --- a/src/openrct2/world/MapHelpers.cpp +++ b/src/openrct2/world/map_generator/MapHelpers.cpp @@ -9,9 +9,9 @@ #include "MapHelpers.h" -#include "../world/tile_element/Slope.h" -#include "../world/tile_element/SurfaceElement.h" -#include "Map.h" +#include "../../world/tile_element/Slope.h" +#include "../../world/tile_element/SurfaceElement.h" +#include "../Map.h" #include diff --git a/src/openrct2/world/MapHelpers.h b/src/openrct2/world/map_generator/MapHelpers.h similarity index 96% rename from src/openrct2/world/MapHelpers.h rename to src/openrct2/world/map_generator/MapHelpers.h index bd9fa0ea12..8b2e50a3e9 100644 --- a/src/openrct2/world/MapHelpers.h +++ b/src/openrct2/world/map_generator/MapHelpers.h @@ -9,7 +9,7 @@ #pragma once -#include "Location.hpp" +#include "../Location.hpp" enum { From c5508bcf1d0d0fc337460b7ec03be66e47537f9f Mon Sep 17 00:00:00 2001 From: Aaron van Geffen Date: Mon, 30 Dec 2024 15:11:40 +0100 Subject: [PATCH 02/10] Split off PngTerrainGenerator, SimplexNoise units --- src/openrct2-ui/windows/MapGen.cpp | 1 + src/openrct2/libopenrct2.vcxproj | 4 + src/openrct2/world/map_generator/MapGen.cpp | 403 +----------------- src/openrct2/world/map_generator/MapGen.h | 6 +- .../map_generator/PngTerrainGenerator.cpp | 265 ++++++++++++ .../world/map_generator/PngTerrainGenerator.h | 18 + .../world/map_generator/SimplexNoise.cpp | 146 +++++++ .../world/map_generator/SimplexNoise.h | 15 + 8 files changed, 461 insertions(+), 397 deletions(-) create mode 100644 src/openrct2/world/map_generator/PngTerrainGenerator.cpp create mode 100644 src/openrct2/world/map_generator/PngTerrainGenerator.h create mode 100644 src/openrct2/world/map_generator/SimplexNoise.cpp create mode 100644 src/openrct2/world/map_generator/SimplexNoise.h diff --git a/src/openrct2-ui/windows/MapGen.cpp b/src/openrct2-ui/windows/MapGen.cpp index 52b1272d9d..69abcbf936 100644 --- a/src/openrct2-ui/windows/MapGen.cpp +++ b/src/openrct2-ui/windows/MapGen.cpp @@ -26,6 +26,7 @@ #include #include #include +#include namespace OpenRCT2::Ui::Windows { diff --git a/src/openrct2/libopenrct2.vcxproj b/src/openrct2/libopenrct2.vcxproj index a7ad8410df..e1dadc931c 100644 --- a/src/openrct2/libopenrct2.vcxproj +++ b/src/openrct2/libopenrct2.vcxproj @@ -626,6 +626,8 @@ + + @@ -1122,6 +1124,8 @@ + + diff --git a/src/openrct2/world/map_generator/MapGen.cpp b/src/openrct2/world/map_generator/MapGen.cpp index 2f1eaa7c80..b8fd0f5500 100644 --- a/src/openrct2/world/map_generator/MapGen.cpp +++ b/src/openrct2/world/map_generator/MapGen.cpp @@ -14,9 +14,7 @@ #include "../../Game.h" #include "../../GameState.h" #include "../../core/Guard.hpp" -#include "../../core/Imaging.h" #include "../../core/String.hpp" -#include "../../localisation/StringIds.h" #include "../../object/ObjectEntryManager.h" #include "../../object/ObjectList.h" #include "../../object/ObjectManager.h" @@ -25,32 +23,20 @@ #include "../../object/TerrainSurfaceObject.h" #include "../../platform/Platform.h" #include "../../util/Util.h" -#include "../../world/tile_element/Slope.h" -#include "../../world/tile_element/SmallSceneryElement.h" -#include "../../world/tile_element/SurfaceElement.h" -#include "../../world/tile_element/TileElement.h" #include "../Map.h" #include "../Scenery.h" +#include "../tile_element/Slope.h" +#include "../tile_element/SmallSceneryElement.h" +#include "../tile_element/SurfaceElement.h" +#include "../tile_element/TileElement.h" #include "MapHelpers.h" +#include "PngTerrainGenerator.h" +#include "SimplexNoise.h" -#include -#include -#include #include using namespace OpenRCT2; -#pragma region Height map struct - -static struct -{ - uint32_t width = 0; - uint32_t height = 0; - std::vector mono_bitmap; -} _heightMapData; - -#pragma endregion Height map struct - #pragma region Random objects static constexpr const char* GrassTrees[] = { @@ -97,7 +83,6 @@ static constexpr std::string_view BaseTerrain[] = { static void MapGenGenerateBlank(MapGenSettings* settings); static void MapGenGenerateSimplex(MapGenSettings* settings); -static void MapGenGenerateFromHeightmapImage(MapGenSettings* settings); static void MapGenPlaceTrees(MapGenSettings* settings); static void MapGenAddBeaches(MapGenSettings* settings); @@ -133,7 +118,6 @@ static void MapGenSetWaterLevel(int32_t waterLevel); static void MapGenSmoothHeight(int32_t iterations); static void MapGenSetHeight(MapGenSettings* settings); -static float FractalNoise(int32_t x, int32_t y, float frequency, int32_t octaves, float lacunarity, float persistence); static void MapGenSimplex(MapGenSettings* settings); static TileCoordsXY _heightSize; @@ -153,7 +137,7 @@ static void SetHeight(int32_t x, int32_t y, int32_t height) _height[x + y * _heightSize.x] = height; } -static ObjectEntryIndex MapGenSurfaceTextureId(MapGenSettings* settings) +ObjectEntryIndex MapGenSurfaceTextureId(MapGenSettings* settings) { auto& objectManager = OpenRCT2::GetContext()->GetObjectManager(); @@ -178,7 +162,7 @@ static ObjectEntryIndex MapGenSurfaceTextureId(MapGenSettings* settings) return surfaceTextureId; } -static ObjectEntryIndex MapGenEdgeTextureId(MapGenSettings* settings, ObjectEntryIndex surfaceTextureId) +ObjectEntryIndex MapGenEdgeTextureId(MapGenSettings* settings, ObjectEntryIndex surfaceTextureId) { auto& objectManager = OpenRCT2::GetContext()->GetObjectManager(); @@ -557,141 +541,6 @@ static void MapGenSetHeight(MapGenSettings* settings) } } -#pragma region Noise - -/** - * Simplex Noise Algorithm with Fractional Brownian Motion - * Based on: - * - https://code.google.com/p/simplexnoise/ - * - https://code.google.com/p/fractalterraingeneration/wiki/Fractional_Brownian_Motion - */ - -static float Generate(float x, float y); -static int32_t FastFloor(float x); -static float Grad(int32_t hash, float x, float y); - -static uint8_t perm[512]; - -static void NoiseRand() -{ - for (auto& i : perm) - { - i = UtilRand() & 0xFF; - } -} - -static float FractalNoise(int32_t x, int32_t y, float frequency, int32_t octaves, float lacunarity, float persistence) -{ - float total = 0.0f; - float amplitude = persistence; - for (int32_t i = 0; i < octaves; i++) - { - total += Generate(x * frequency, y * frequency) * amplitude; - frequency *= lacunarity; - amplitude *= persistence; - } - return total; -} - -static float Generate(float x, float y) -{ - const float F2 = 0.366025403f; // F2 = 0.5*(sqrt(3.0)-1.0) - const float G2 = 0.211324865f; // G2 = (3.0-sqrt(3.0))/6.0 - - float n0, n1, n2; // Noise contributions from the three corners - - // Skew the input space to determine which simplex cell we're in - float s = (x + y) * F2; // Hairy factor for 2D - float xs = x + s; - float ys = y + s; - int32_t i = FastFloor(xs); - int32_t j = FastFloor(ys); - - float t = static_cast(i + j) * G2; - float X0 = i - t; // Unskew the cell origin back to (x,y) space - float Y0 = j - t; - float x0 = x - X0; // The x,y distances from the cell origin - float y0 = y - Y0; - - // For the 2D case, the simplex shape is an equilateral triangle. - // Determine which simplex we are in. - int32_t i1, j1; // Offsets for second (middle) corner of simplex in (i,j) coords - if (x0 > y0) - { - i1 = 1; - j1 = 0; - } // lower triangle, XY order: (0,0)->(1,0)->(1,1) - else - { - i1 = 0; - j1 = 1; - } // upper triangle, YX order: (0,0)->(0,1)->(1,1) - - // A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and - // a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where - // c = (3-sqrt(3))/6 - - float x1 = x0 - i1 + G2; // Offsets for middle corner in (x,y) unskewed coords - float y1 = y0 - j1 + G2; - float x2 = x0 - 1.0f + 2.0f * G2; // Offsets for last corner in (x,y) unskewed coords - float y2 = y0 - 1.0f + 2.0f * G2; - - // Wrap the integer indices at 256, to avoid indexing perm[] out of bounds - int32_t ii = i % 256; - int32_t jj = j % 256; - - // Calculate the contribution from the three corners - float t0 = 0.5f - x0 * x0 - y0 * y0; - if (t0 < 0.0f) - { - n0 = 0.0f; - } - else - { - t0 *= t0; - n0 = t0 * t0 * Grad(perm[ii + perm[jj]], x0, y0); - } - - float t1 = 0.5f - x1 * x1 - y1 * y1; - if (t1 < 0.0f) - { - n1 = 0.0f; - } - else - { - t1 *= t1; - n1 = t1 * t1 * Grad(perm[ii + i1 + perm[jj + j1]], x1, y1); - } - - float t2 = 0.5f - x2 * x2 - y2 * y2; - if (t2 < 0.0f) - { - n2 = 0.0f; - } - else - { - t2 *= t2; - n2 = t2 * t2 * Grad(perm[ii + 1 + perm[jj + 1]], x2, y2); - } - - // Add contributions from each corner to get the final noise value. - // The result is scaled to return values in the interval [-1,1]. - return 40.0f * (n0 + n1 + n2); // TODO: The scale factor is preliminary! -} - -static int32_t FastFloor(float x) -{ - return (x > 0) ? (static_cast(x)) : ((static_cast(x)) - 1); -} - -static float Grad(int32_t hash, float x, float y) -{ - int32_t h = hash & 7; // Convert low 3 bits of hash code - float u = h < 4 ? x : y; // into 8 simple gradient directions, - float v = h < 4 ? y : x; // and compute the dot product with (x,y). - return ((h & 1) != 0 ? -u : u) + ((h & 2) != 0 ? -2.0f * v : 2.0f * v); -} - static void MapGenSimplex(MapGenSettings* settings) { float freq = settings->simplex_base_freq / 100.0f * (1.0f / _heightSize.x); @@ -712,239 +561,3 @@ static void MapGenSimplex(MapGenSettings* settings) } } } - -#pragma endregion - -#pragma region Heightmap - -/** - * Return the tile coordinate that matches the given pixel of a heightmap - */ -static TileCoordsXY MapgenHeightmapCoordToTileCoordsXY(uint32_t x, uint32_t y) -{ - // The height map does not include the empty tiles around the map, so we add 1. - return TileCoordsXY(static_cast(y + 1), static_cast(x + 1)); -} - -bool MapGenLoadHeightmapImage(const utf8* path) -{ - auto format = Imaging::GetImageFormatFromPath(path); - if (format == IMAGE_FORMAT::PNG) - { - // Promote to 32-bit - format = IMAGE_FORMAT::PNG_32; - } - - try - { - auto image = Imaging::ReadFromFile(path, format); - auto width = std::min(image.Width, kMaximumMapSizePractical); - auto height = std::min(image.Height, kMaximumMapSizePractical); - if (width != image.Width || height != image.Height) - { - ContextShowError(STR_HEIGHT_MAP_ERROR, STR_ERROR_HEIGHT_MAP_TOO_BIG, {}); - } - - // Allocate memory for the height map values, one byte pixel - _heightMapData.mono_bitmap.resize(width * height); - _heightMapData.width = width; - _heightMapData.height = height; - - // Copy average RGB value to mono bitmap - constexpr auto numChannels = 4; - const auto pitch = image.Stride; - const auto pixels = image.Pixels.data(); - for (uint32_t x = 0; x < _heightMapData.width; x++) - { - for (uint32_t y = 0; y < _heightMapData.height; y++) - { - const auto red = pixels[x * numChannels + y * pitch]; - const auto green = pixels[x * numChannels + y * pitch + 1]; - const auto blue = pixels[x * numChannels + y * pitch + 2]; - _heightMapData.mono_bitmap[x + y * _heightMapData.width] = (red + green + blue) / 3; - } - } - return true; - } - catch (const std::exception& e) - { - switch (format) - { - case IMAGE_FORMAT::BITMAP: - ContextShowError(STR_HEIGHT_MAP_ERROR, STR_ERROR_READING_BITMAP, {}); - break; - case IMAGE_FORMAT::PNG_32: - ContextShowError(STR_HEIGHT_MAP_ERROR, STR_ERROR_READING_PNG, {}); - break; - default: - LOG_ERROR("Unable to load height map image: %s", e.what()); - break; - } - return false; - } -} - -/** - * Frees the memory used to store the selected height map - */ -void MapGenUnloadHeightmapImage() -{ - _heightMapData.mono_bitmap.clear(); - _heightMapData.width = 0; - _heightMapData.height = 0; -} - -/** - * Applies box blur to the surface N times - */ -static void MapGenSmoothHeightmap(std::vector& src, int32_t strength) -{ - // Create buffer to store one channel - std::vector dest(src.size()); - - for (int32_t i = 0; i < strength; i++) - { - // Calculate box blur value to all pixels of the surface - for (uint32_t y = 0; y < _heightMapData.height; y++) - { - for (uint32_t x = 0; x < _heightMapData.width; x++) - { - uint32_t heightSum = 0; - - // Loop over neighbour pixels, all of them have the same weight - for (int8_t offsetX = -1; offsetX <= 1; offsetX++) - { - for (int8_t offsetY = -1; offsetY <= 1; offsetY++) - { - // Clamp x and y so they stay within the image - // This assumes the height map is not tiled, and increases the weight of the edges - const int32_t readX = std::clamp(x + offsetX, 0, _heightMapData.width - 1); - const int32_t readY = std::clamp(y + offsetY, 0, _heightMapData.height - 1); - heightSum += src[readX + readY * _heightMapData.width]; - } - } - - // Take average - dest[x + y * _heightMapData.width] = heightSum / 9; - } - } - - // Now apply the blur to the source pixels - for (uint32_t y = 0; y < _heightMapData.height; y++) - { - for (uint32_t x = 0; x < _heightMapData.width; x++) - { - src[x + y * _heightMapData.width] = dest[x + y * _heightMapData.width]; - } - } - } -} - -static void MapGenGenerateFromHeightmapImage(MapGenSettings* settings) -{ - Guard::Assert(!_heightMapData.mono_bitmap.empty(), "No height map loaded"); - Guard::Assert(settings->heightmapHigh != settings->heightmapLow, "Low and high setting cannot be the same"); - - // Make a copy of the original height map that we can edit - auto dest = _heightMapData.mono_bitmap; - - // Get technical map size, +2 for the black tiles around the map - auto maxWidth = static_cast(_heightMapData.width + 2); - auto maxHeight = static_cast(_heightMapData.height + 2); - MapInit({ maxHeight, maxWidth }); - - if (settings->smooth_height_map) - { - MapGenSmoothHeightmap(dest, settings->smooth_strength); - } - - uint8_t maxValue = 255; - uint8_t minValue = 0; - - if (settings->normalize_height) - { - // Get highest and lowest pixel value - maxValue = 0; - minValue = 0xff; - for (uint32_t y = 0; y < _heightMapData.height; y++) - { - for (uint32_t x = 0; x < _heightMapData.width; x++) - { - uint8_t value = dest[x + y * _heightMapData.width]; - maxValue = std::max(maxValue, value); - minValue = std::min(minValue, value); - } - } - - if (minValue == maxValue) - { - ContextShowError(STR_HEIGHT_MAP_ERROR, STR_ERROR_CANNOT_NORMALIZE, {}); - return; - } - } - - Guard::Assert(maxValue > minValue, "Input range is invalid"); - Guard::Assert(settings->heightmapHigh > settings->heightmapLow, "Output range is invalid"); - - const auto surfaceTextureId = MapGenSurfaceTextureId(settings); - const auto edgeTextureId = MapGenEdgeTextureId(settings, surfaceTextureId); - - const uint8_t rangeIn = maxValue - minValue; - const uint8_t rangeOut = (settings->heightmapHigh - settings->heightmapLow) * 2; - - for (uint32_t y = 0; y < _heightMapData.height; y++) - { - for (uint32_t x = 0; x < _heightMapData.width; x++) - { - // The x and y axis are flipped in the world, so this uses y for x and x for y. - auto tileCoords = MapgenHeightmapCoordToTileCoordsXY(x, y); - auto* const surfaceElement = MapGetSurfaceElementAt(tileCoords); - if (surfaceElement == nullptr) - continue; - - // Read value from bitmap, and convert its range - uint8_t value = dest[x + y * _heightMapData.width]; - value = static_cast(static_cast(value - minValue) / rangeIn * rangeOut) - + (settings->heightmapLow * 2); - surfaceElement->BaseHeight = value; - - // Floor to even number - surfaceElement->BaseHeight /= 2; - surfaceElement->BaseHeight *= 2; - surfaceElement->ClearanceHeight = surfaceElement->BaseHeight; - - // Set textures - surfaceElement->SetSurfaceObjectIndex(surfaceTextureId); - surfaceElement->SetEdgeObjectIndex(edgeTextureId); - - // Set water level - if (surfaceElement->BaseHeight < settings->waterLevel) - { - surfaceElement->SetWaterHeight(settings->waterLevel * kCoordsZStep); - } - } - } - - // Smooth tile edges - if (settings->smoothTileEdges) - { - // Keep smoothing the entire map until no tiles are changed anymore - while (true) - { - uint32_t numTilesChanged = 0; - for (uint32_t y = 0; y < _heightMapData.height; y++) - { - for (uint32_t x = 0; x < _heightMapData.width; x++) - { - auto tileCoords = MapgenHeightmapCoordToTileCoordsXY(x, y); - numTilesChanged += TileSmooth(tileCoords); - } - } - - if (numTilesChanged == 0) - break; - } - } -} - -#pragma endregion diff --git a/src/openrct2/world/map_generator/MapGen.h b/src/openrct2/world/map_generator/MapGen.h index aa3af57937..cc10be6468 100644 --- a/src/openrct2/world/map_generator/MapGen.h +++ b/src/openrct2/world/map_generator/MapGen.h @@ -10,6 +10,7 @@ #pragma once #include "../../core/StringTypes.h" +#include "../../object/ObjectTypes.h" #include "../Location.hpp" enum class MapGenAlgorithm : uint8_t @@ -49,5 +50,6 @@ struct MapGenSettings }; void MapGenGenerate(MapGenSettings* settings); -bool MapGenLoadHeightmapImage(const utf8* path); -void MapGenUnloadHeightmapImage(); + +ObjectEntryIndex MapGenSurfaceTextureId(MapGenSettings* settings); +ObjectEntryIndex MapGenEdgeTextureId(MapGenSettings* settings, ObjectEntryIndex surfaceTextureId); diff --git a/src/openrct2/world/map_generator/PngTerrainGenerator.cpp b/src/openrct2/world/map_generator/PngTerrainGenerator.cpp new file mode 100644 index 0000000000..e7bf284b52 --- /dev/null +++ b/src/openrct2/world/map_generator/PngTerrainGenerator.cpp @@ -0,0 +1,265 @@ +/***************************************************************************** + * Copyright (c) 2014-2025 OpenRCT2 developers + * + * For a complete list of all authors, please refer to contributors.md + * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 + * + * OpenRCT2 is licensed under the GNU General Public License version 3. + *****************************************************************************/ + +#include "PngTerrainGenerator.h" + +#include "../../Context.h" +#include "../../Diagnostic.h" +#include "../../core/Imaging.h" +#include "../../localisation/Formatter.h" +#include "../../localisation/StringIds.h" +#include "../Map.h" +#include "../tile_element/SurfaceElement.h" +#include "MapGen.h" +#include "MapHelpers.h" + +#include + +using namespace OpenRCT2; + +#pragma region Height map struct + +static struct +{ + uint32_t width = 0; + uint32_t height = 0; + std::vector mono_bitmap; +} _heightMapData; + +#pragma endregion Height map struct + +/** + * Return the tile coordinate that matches the given pixel of a heightmap + */ +static TileCoordsXY MapgenHeightmapCoordToTileCoordsXY(uint32_t x, uint32_t y) +{ + // The height map does not include the empty tiles around the map, so we add 1. + return TileCoordsXY(static_cast(y + 1), static_cast(x + 1)); +} + +bool MapGenLoadHeightmapImage(const utf8* path) +{ + auto format = Imaging::GetImageFormatFromPath(path); + if (format == IMAGE_FORMAT::PNG) + { + // Promote to 32-bit + format = IMAGE_FORMAT::PNG_32; + } + + try + { + auto image = Imaging::ReadFromFile(path, format); + auto width = std::min(image.Width, kMaximumMapSizePractical); + auto height = std::min(image.Height, kMaximumMapSizePractical); + if (width != image.Width || height != image.Height) + { + ContextShowError(STR_HEIGHT_MAP_ERROR, STR_ERROR_HEIGHT_MAP_TOO_BIG, {}); + } + + // Allocate memory for the height map values, one byte pixel + _heightMapData.mono_bitmap.resize(width * height); + _heightMapData.width = width; + _heightMapData.height = height; + + // Copy average RGB value to mono bitmap + constexpr auto numChannels = 4; + const auto pitch = image.Stride; + const auto pixels = image.Pixels.data(); + for (uint32_t x = 0; x < _heightMapData.width; x++) + { + for (uint32_t y = 0; y < _heightMapData.height; y++) + { + const auto red = pixels[x * numChannels + y * pitch]; + const auto green = pixels[x * numChannels + y * pitch + 1]; + const auto blue = pixels[x * numChannels + y * pitch + 2]; + _heightMapData.mono_bitmap[x + y * _heightMapData.width] = (red + green + blue) / 3; + } + } + return true; + } + catch (const std::exception& e) + { + switch (format) + { + case IMAGE_FORMAT::BITMAP: + ContextShowError(STR_HEIGHT_MAP_ERROR, STR_ERROR_READING_BITMAP, {}); + break; + case IMAGE_FORMAT::PNG_32: + ContextShowError(STR_HEIGHT_MAP_ERROR, STR_ERROR_READING_PNG, {}); + break; + default: + LOG_ERROR("Unable to load height map image: %s", e.what()); + break; + } + return false; + } +} + +/** + * Frees the memory used to store the selected height map + */ +void MapGenUnloadHeightmapImage() +{ + _heightMapData.mono_bitmap.clear(); + _heightMapData.width = 0; + _heightMapData.height = 0; +} + +/** + * Applies box blur to the surface N times + */ +static void MapGenSmoothHeightmap(std::vector& src, int32_t strength) +{ + // Create buffer to store one channel + std::vector dest(src.size()); + + for (int32_t i = 0; i < strength; i++) + { + // Calculate box blur value to all pixels of the surface + for (uint32_t y = 0; y < _heightMapData.height; y++) + { + for (uint32_t x = 0; x < _heightMapData.width; x++) + { + uint32_t heightSum = 0; + + // Loop over neighbour pixels, all of them have the same weight + for (int8_t offsetX = -1; offsetX <= 1; offsetX++) + { + for (int8_t offsetY = -1; offsetY <= 1; offsetY++) + { + // Clamp x and y so they stay within the image + // This assumes the height map is not tiled, and increases the weight of the edges + const int32_t readX = std::clamp(x + offsetX, 0, _heightMapData.width - 1); + const int32_t readY = std::clamp(y + offsetY, 0, _heightMapData.height - 1); + heightSum += src[readX + readY * _heightMapData.width]; + } + } + + // Take average + dest[x + y * _heightMapData.width] = heightSum / 9; + } + } + + // Now apply the blur to the source pixels + for (uint32_t y = 0; y < _heightMapData.height; y++) + { + for (uint32_t x = 0; x < _heightMapData.width; x++) + { + src[x + y * _heightMapData.width] = dest[x + y * _heightMapData.width]; + } + } + } +} + +void MapGenGenerateFromHeightmapImage(MapGenSettings* settings) +{ + Guard::Assert(!_heightMapData.mono_bitmap.empty(), "No height map loaded"); + Guard::Assert(settings->heightmapHigh != settings->heightmapLow, "Low and high setting cannot be the same"); + + // Make a copy of the original height map that we can edit + auto dest = _heightMapData.mono_bitmap; + + // Get technical map size, +2 for the black tiles around the map + auto maxWidth = static_cast(_heightMapData.width + 2); + auto maxHeight = static_cast(_heightMapData.height + 2); + MapInit({ maxHeight, maxWidth }); + + if (settings->smooth_height_map) + { + MapGenSmoothHeightmap(dest, settings->smooth_strength); + } + + uint8_t maxValue = 255; + uint8_t minValue = 0; + + if (settings->normalize_height) + { + // Get highest and lowest pixel value + maxValue = 0; + minValue = 0xff; + for (uint32_t y = 0; y < _heightMapData.height; y++) + { + for (uint32_t x = 0; x < _heightMapData.width; x++) + { + uint8_t value = dest[x + y * _heightMapData.width]; + maxValue = std::max(maxValue, value); + minValue = std::min(minValue, value); + } + } + + if (minValue == maxValue) + { + ContextShowError(STR_HEIGHT_MAP_ERROR, STR_ERROR_CANNOT_NORMALIZE, {}); + return; + } + } + + Guard::Assert(maxValue > minValue, "Input range is invalid"); + Guard::Assert(settings->heightmapHigh > settings->heightmapLow, "Output range is invalid"); + + const auto surfaceTextureId = MapGenSurfaceTextureId(settings); + const auto edgeTextureId = MapGenEdgeTextureId(settings, surfaceTextureId); + + const uint8_t rangeIn = maxValue - minValue; + const uint8_t rangeOut = (settings->heightmapHigh - settings->heightmapLow) * 2; + + for (uint32_t y = 0; y < _heightMapData.height; y++) + { + for (uint32_t x = 0; x < _heightMapData.width; x++) + { + // The x and y axis are flipped in the world, so this uses y for x and x for y. + auto tileCoords = MapgenHeightmapCoordToTileCoordsXY(x, y); + auto* const surfaceElement = MapGetSurfaceElementAt(tileCoords); + if (surfaceElement == nullptr) + continue; + + // Read value from bitmap, and convert its range + uint8_t value = dest[x + y * _heightMapData.width]; + value = static_cast(static_cast(value - minValue) / rangeIn * rangeOut) + + (settings->heightmapLow * 2); + surfaceElement->BaseHeight = value; + + // Floor to even number + surfaceElement->BaseHeight /= 2; + surfaceElement->BaseHeight *= 2; + surfaceElement->ClearanceHeight = surfaceElement->BaseHeight; + + // Set textures + surfaceElement->SetSurfaceObjectIndex(surfaceTextureId); + surfaceElement->SetEdgeObjectIndex(edgeTextureId); + + // Set water level + if (surfaceElement->BaseHeight < settings->waterLevel) + { + surfaceElement->SetWaterHeight(settings->waterLevel * kCoordsZStep); + } + } + } + + // Smooth tile edges + if (settings->smoothTileEdges) + { + // Keep smoothing the entire map until no tiles are changed anymore + while (true) + { + uint32_t numTilesChanged = 0; + for (uint32_t y = 0; y < _heightMapData.height; y++) + { + for (uint32_t x = 0; x < _heightMapData.width; x++) + { + auto tileCoords = MapgenHeightmapCoordToTileCoordsXY(x, y); + numTilesChanged += TileSmooth(tileCoords); + } + } + + if (numTilesChanged == 0) + break; + } + } +} diff --git a/src/openrct2/world/map_generator/PngTerrainGenerator.h b/src/openrct2/world/map_generator/PngTerrainGenerator.h new file mode 100644 index 0000000000..c61f28648b --- /dev/null +++ b/src/openrct2/world/map_generator/PngTerrainGenerator.h @@ -0,0 +1,18 @@ +/***************************************************************************** + * Copyright (c) 2014-2025 OpenRCT2 developers + * + * For a complete list of all authors, please refer to contributors.md + * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 + * + * OpenRCT2 is licensed under the GNU General Public License version 3. + *****************************************************************************/ + +#pragma once + +#include "../../core/StringTypes.h" + +struct MapGenSettings; + +bool MapGenLoadHeightmapImage(const utf8* path); +void MapGenUnloadHeightmapImage(); +void MapGenGenerateFromHeightmapImage(MapGenSettings* settings); diff --git a/src/openrct2/world/map_generator/SimplexNoise.cpp b/src/openrct2/world/map_generator/SimplexNoise.cpp new file mode 100644 index 0000000000..b85edcaaf7 --- /dev/null +++ b/src/openrct2/world/map_generator/SimplexNoise.cpp @@ -0,0 +1,146 @@ +/***************************************************************************** + * Copyright (c) 2014-2025 OpenRCT2 developers + * + * For a complete list of all authors, please refer to contributors.md + * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 + * + * OpenRCT2 is licensed under the GNU General Public License version 3. + *****************************************************************************/ + +#include "SimplexNoise.h" + +#include "../../util/Util.h" +#include "MapGen.h" + +/** + * Simplex Noise Algorithm with Fractional Brownian Motion + * Based on: + * - https://code.google.com/p/simplexnoise/ + * - https://code.google.com/p/fractalterraingeneration/wiki/Fractional_Brownian_Motion + */ + +static float Generate(float x, float y); +static int32_t FastFloor(float x); +static float Grad(int32_t hash, float x, float y); + +static uint8_t perm[512]; + +void NoiseRand() +{ + for (auto& i : perm) + { + i = UtilRand() & 0xFF; + } +} + +float FractalNoise(int32_t x, int32_t y, float frequency, int32_t octaves, float lacunarity, float persistence) +{ + float total = 0.0f; + float amplitude = persistence; + for (int32_t i = 0; i < octaves; i++) + { + total += Generate(x * frequency, y * frequency) * amplitude; + frequency *= lacunarity; + amplitude *= persistence; + } + return total; +} + +static float Generate(float x, float y) +{ + const float F2 = 0.366025403f; // F2 = 0.5*(sqrt(3.0)-1.0) + const float G2 = 0.211324865f; // G2 = (3.0-sqrt(3.0))/6.0 + + float n0, n1, n2; // Noise contributions from the three corners + + // Skew the input space to determine which simplex cell we're in + float s = (x + y) * F2; // Hairy factor for 2D + float xs = x + s; + float ys = y + s; + int32_t i = FastFloor(xs); + int32_t j = FastFloor(ys); + + float t = static_cast(i + j) * G2; + float X0 = i - t; // Unskew the cell origin back to (x,y) space + float Y0 = j - t; + float x0 = x - X0; // The x,y distances from the cell origin + float y0 = y - Y0; + + // For the 2D case, the simplex shape is an equilateral triangle. + // Determine which simplex we are in. + int32_t i1, j1; // Offsets for second (middle) corner of simplex in (i,j) coords + if (x0 > y0) + { + i1 = 1; + j1 = 0; + } // lower triangle, XY order: (0,0)->(1,0)->(1,1) + else + { + i1 = 0; + j1 = 1; + } // upper triangle, YX order: (0,0)->(0,1)->(1,1) + + // A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and + // a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where + // c = (3-sqrt(3))/6 + + float x1 = x0 - i1 + G2; // Offsets for middle corner in (x,y) unskewed coords + float y1 = y0 - j1 + G2; + float x2 = x0 - 1.0f + 2.0f * G2; // Offsets for last corner in (x,y) unskewed coords + float y2 = y0 - 1.0f + 2.0f * G2; + + // Wrap the integer indices at 256, to avoid indexing perm[] out of bounds + int32_t ii = i % 256; + int32_t jj = j % 256; + + // Calculate the contribution from the three corners + float t0 = 0.5f - x0 * x0 - y0 * y0; + if (t0 < 0.0f) + { + n0 = 0.0f; + } + else + { + t0 *= t0; + n0 = t0 * t0 * Grad(perm[ii + perm[jj]], x0, y0); + } + + float t1 = 0.5f - x1 * x1 - y1 * y1; + if (t1 < 0.0f) + { + n1 = 0.0f; + } + else + { + t1 *= t1; + n1 = t1 * t1 * Grad(perm[ii + i1 + perm[jj + j1]], x1, y1); + } + + float t2 = 0.5f - x2 * x2 - y2 * y2; + if (t2 < 0.0f) + { + n2 = 0.0f; + } + else + { + t2 *= t2; + n2 = t2 * t2 * Grad(perm[ii + 1 + perm[jj + 1]], x2, y2); + } + + // Add contributions from each corner to get the final noise value. + // The result is scaled to return values in the interval [-1,1]. + return 40.0f * (n0 + n1 + n2); // TODO: The scale factor is preliminary! +} + +static int32_t FastFloor(float x) +{ + return (x > 0) ? (static_cast(x)) : ((static_cast(x)) - 1); +} + +static float Grad(int32_t hash, float x, float y) +{ + int32_t h = hash & 7; // Convert low 3 bits of hash code + float u = h < 4 ? x : y; // into 8 simple gradient directions, + float v = h < 4 ? y : x; // and compute the dot product with (x,y). + return ((h & 1) != 0 ? -u : u) + ((h & 2) != 0 ? -2.0f * v : 2.0f * v); +} diff --git a/src/openrct2/world/map_generator/SimplexNoise.h b/src/openrct2/world/map_generator/SimplexNoise.h new file mode 100644 index 0000000000..a21a42762a --- /dev/null +++ b/src/openrct2/world/map_generator/SimplexNoise.h @@ -0,0 +1,15 @@ +/***************************************************************************** + * Copyright (c) 2014-2025 OpenRCT2 developers + * + * For a complete list of all authors, please refer to contributors.md + * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 + * + * OpenRCT2 is licensed under the GNU General Public License version 3. + *****************************************************************************/ + +#pragma once + +#include + +void NoiseRand(); +float FractalNoise(int32_t x, int32_t y, float frequency, int32_t octaves, float lacunarity, float persistence); From dffbb655ff75a8e16bbc9b92a21f0bab78c2d429 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen Date: Mon, 30 Dec 2024 15:25:30 +0100 Subject: [PATCH 03/10] Move TreePlacement into its own unit as well --- src/openrct2/libopenrct2.vcxproj | 2 + src/openrct2/world/map_generator/MapGen.cpp | 203 +--------------- .../world/map_generator/TreePlacement.cpp | 225 ++++++++++++++++++ .../world/map_generator/TreePlacement.h | 12 + 4 files changed, 240 insertions(+), 202 deletions(-) create mode 100644 src/openrct2/world/map_generator/TreePlacement.cpp create mode 100644 src/openrct2/world/map_generator/TreePlacement.h diff --git a/src/openrct2/libopenrct2.vcxproj b/src/openrct2/libopenrct2.vcxproj index e1dadc931c..781ff6427e 100644 --- a/src/openrct2/libopenrct2.vcxproj +++ b/src/openrct2/libopenrct2.vcxproj @@ -628,6 +628,7 @@ + @@ -1126,6 +1127,7 @@ + diff --git a/src/openrct2/world/map_generator/MapGen.cpp b/src/openrct2/world/map_generator/MapGen.cpp index b8fd0f5500..353d9a3a85 100644 --- a/src/openrct2/world/map_generator/MapGen.cpp +++ b/src/openrct2/world/map_generator/MapGen.cpp @@ -26,55 +26,17 @@ #include "../Map.h" #include "../Scenery.h" #include "../tile_element/Slope.h" -#include "../tile_element/SmallSceneryElement.h" #include "../tile_element/SurfaceElement.h" #include "../tile_element/TileElement.h" #include "MapHelpers.h" #include "PngTerrainGenerator.h" #include "SimplexNoise.h" +#include "TreePlacement.h" #include using namespace OpenRCT2; -#pragma region Random objects - -static constexpr const char* GrassTrees[] = { - // Dark - "rct2.scenery_small.tcf", // Caucasian Fir Tree - "rct2.scenery_small.trf", // Red Fir Tree - "rct2.scenery_small.trf2", // Red Fir Tree - "rct2.scenery_small.tsp", // Scots Pine Tree - "rct2.scenery_small.tmzp", // Montezuma Pine Tree - "rct2.scenery_small.tap", // Aleppo Pine Tree - "rct2.scenery_small.tcrp", // Corsican Pine Tree - "rct2.scenery_small.tbp", // Black Poplar Tree - - // Light - "rct2.scenery_small.tcl", // Cedar of Lebanon Tree - "rct2.scenery_small.tel", // European Larch Tree -}; - -static constexpr const char* DesertTrees[] = { - "rct2.scenery_small.tmp", // Monkey-Puzzle Tree - "rct2.scenery_small.thl", // Honey Locust Tree - "rct2.scenery_small.th1", // Canary Palm Tree - "rct2.scenery_small.th2", // Palm Tree - "rct2.scenery_small.tpm", // Palm Tree - "rct2.scenery_small.tropt1", // Tree - "rct2.scenery_small.tbc", // Cactus - "rct2.scenery_small.tsc", // Cactus -}; - -static constexpr const char* SnowTrees[] = { - "rct2.scenery_small.tcfs", // Snow-covered Caucasian Fir Tree - "rct2.scenery_small.tnss", // Snow-covered Norway Spruce Tree - "rct2.scenery_small.trf3", // Snow-covered Red Fir Tree - "rct2.scenery_small.trfs", // Snow-covered Red Fir Tree -}; - -#pragma endregion - // Randomly chosen base terrains. We rarely want a whole map made out of chequerboard or rock. static constexpr std::string_view BaseTerrain[] = { "rct2.terrain_surface.grass", "rct2.terrain_surface.sand", "rct2.terrain_surface.sand_brown", @@ -84,7 +46,6 @@ static constexpr std::string_view BaseTerrain[] = { static void MapGenGenerateBlank(MapGenSettings* settings); static void MapGenGenerateSimplex(MapGenSettings* settings); -static void MapGenPlaceTrees(MapGenSettings* settings); static void MapGenAddBeaches(MapGenSettings* settings); void MapGenGenerate(MapGenSettings* settings) @@ -281,168 +242,6 @@ static void MapGenAddBeaches(MapGenSettings* settings) } } -static void MapGenPlaceTree(ObjectEntryIndex type, const CoordsXY& loc) -{ - auto* sceneryEntry = OpenRCT2::ObjectManager::GetObjectEntry(type); - if (sceneryEntry == nullptr) - { - return; - } - - int32_t surfaceZ = TileElementHeight(loc.ToTileCentre()); - - auto* sceneryElement = TileElementInsert({ loc, surfaceZ }, 0b1111); - Guard::Assert(sceneryElement != nullptr); - - sceneryElement->SetClearanceZ(surfaceZ + sceneryEntry->height); - sceneryElement->SetDirection(UtilRand() & 3); - sceneryElement->SetEntryIndex(type); - sceneryElement->SetAge(0); - sceneryElement->SetPrimaryColour(COLOUR_YELLOW); -} - -static bool MapGenSurfaceTakesGrassTrees(const TerrainSurfaceObject& surface) -{ - const auto& id = surface.GetIdentifier(); - return id == "rct2.terrain_surface.grass" || id == "rct2.terrain_surface.grass_clumps" || id == "rct2.terrain_surface.dirt"; -} - -static bool MapGenSurfaceTakesSandTrees(const TerrainSurfaceObject& surface) -{ - const auto& id = surface.GetIdentifier(); - return id == "rct2.terrain_surface.sand" || id == "rct2.terrain_surface.sand_brown" - || id == "rct2.terrain_surface.sand_red"; -} - -static bool MapGenSurfaceTakesSnowTrees(const TerrainSurfaceObject& surface) -{ - const auto& id = surface.GetIdentifier(); - return id == "rct2.terrain_surface.ice"; -} - -template -static bool TryFindTreeInList(std::string_view id, const T& treeList) -{ - for (size_t j = 0; j < std::size(treeList); j++) - { - if (treeList[j] == id) - return true; - } - return false; -} - -/** - * Randomly places a selection of preset trees on the map. Picks the right tree for the terrain it is placing it on. - */ -static void MapGenPlaceTrees(MapGenSettings* settings) -{ - std::vector grassTreeIds; - std::vector desertTreeIds; - std::vector snowTreeIds; - - for (auto i = 0u; i < getObjectEntryGroupCount(ObjectType::SmallScenery); i++) - { - auto* sceneryEntry = OpenRCT2::ObjectManager::GetObjectEntry(i); - auto entry = ObjectEntryGetObject(ObjectType::SmallScenery, i); - - if (sceneryEntry == nullptr) - continue; - - if (TryFindTreeInList(entry->GetIdentifier(), GrassTrees)) - { - grassTreeIds.push_back(i); - } - else if (TryFindTreeInList(entry->GetIdentifier(), DesertTrees)) - { - desertTreeIds.push_back(i); - } - else if (TryFindTreeInList(entry->GetIdentifier(), SnowTrees)) - { - snowTreeIds.push_back(i); - } - } - - // Place trees - float treeToLandRatio = static_cast(settings->treeToLandRatio) / 100.0f; - - auto& gameState = GetGameState(); - for (int32_t y = 1; y < gameState.MapSize.y - 1; y++) - { - for (int32_t x = 1; x < gameState.MapSize.x - 1; x++) - { - auto pos = CoordsXY{ x, y } * kCoordsXYStep; - auto* surfaceElement = MapGetSurfaceElementAt(pos); - if (surfaceElement == nullptr) - continue; - - // Don't place on water - if (surfaceElement->GetWaterHeight() > 0) - continue; - - if (settings->minTreeAltitude > surfaceElement->BaseHeight - || settings->maxTreeAltitude < surfaceElement->BaseHeight) - continue; - - // On sand surfaces, give the tile a score based on nearby water, to be used to determine whether to spawn - // vegetation - float oasisScore = 0.0f; - ObjectEntryIndex treeObjectEntryIndex = OBJECT_ENTRY_INDEX_NULL; - const auto& surfaceStyleObject = *TerrainSurfaceObject::GetById(surfaceElement->GetSurfaceObjectIndex()); - if (MapGenSurfaceTakesSandTrees(surfaceStyleObject)) - { - oasisScore = -0.5f; - constexpr auto maxOasisDistance = 4; - for (int32_t offsetY = -maxOasisDistance; offsetY <= maxOasisDistance; offsetY++) - { - for (int32_t offsetX = -maxOasisDistance; offsetX <= maxOasisDistance; offsetX++) - { - // Get map coord, clamped to the edges - const auto offset = CoordsXY{ offsetX * kCoordsXYStep, offsetY * kCoordsXYStep }; - auto neighbourPos = pos + offset; - neighbourPos.x = std::clamp(neighbourPos.x, kCoordsXYStep, kCoordsXYStep * (gameState.MapSize.x - 1)); - neighbourPos.y = std::clamp(neighbourPos.y, kCoordsXYStep, kCoordsXYStep * (gameState.MapSize.y - 1)); - - const auto neighboutSurface = MapGetSurfaceElementAt(neighbourPos); - if (neighboutSurface != nullptr && neighboutSurface->GetWaterHeight() > 0) - { - float distance = std::sqrt(offsetX * offsetX + offsetY * offsetY); - oasisScore += 0.5f / (maxOasisDistance * distance); - } - } - } - } - - // Use tree:land ratio except when near an oasis - constexpr static auto randModulo = 0xFFFF; - if (static_cast(UtilRand() & randModulo) / randModulo > std::max(treeToLandRatio, oasisScore)) - continue; - - // Use fractal noise to group tiles that are likely to spawn trees together - float noiseValue = FractalNoise(x, y, 0.025f, 2, 2.0f, 0.65f); - // Reduces the range to rarely stray further than 0.5 from the mean. - float noiseOffset = UtilRandNormalDistributed() * 0.25f; - if (noiseValue + oasisScore < noiseOffset) - continue; - - if (!grassTreeIds.empty() && MapGenSurfaceTakesGrassTrees(surfaceStyleObject)) - { - treeObjectEntryIndex = grassTreeIds[UtilRand() % grassTreeIds.size()]; - } - else if (!desertTreeIds.empty() && MapGenSurfaceTakesSandTrees(surfaceStyleObject)) - { - treeObjectEntryIndex = desertTreeIds[UtilRand() % desertTreeIds.size()]; - } - else if (!snowTreeIds.empty() && MapGenSurfaceTakesSnowTrees(surfaceStyleObject)) - { - treeObjectEntryIndex = snowTreeIds[UtilRand() % snowTreeIds.size()]; - } - - if (treeObjectEntryIndex != OBJECT_ENTRY_INDEX_NULL) - MapGenPlaceTree(treeObjectEntryIndex, pos); - } - } -} - /** * Sets each tile's water level to the specified water level if underneath that water level. */ diff --git a/src/openrct2/world/map_generator/TreePlacement.cpp b/src/openrct2/world/map_generator/TreePlacement.cpp new file mode 100644 index 0000000000..5b209b3f1c --- /dev/null +++ b/src/openrct2/world/map_generator/TreePlacement.cpp @@ -0,0 +1,225 @@ +/***************************************************************************** + * Copyright (c) 2014-2025 OpenRCT2 developers + * + * For a complete list of all authors, please refer to contributors.md + * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 + * + * OpenRCT2 is licensed under the GNU General Public License version 3. + *****************************************************************************/ + +#include "TreePlacement.h" + +#include "../../GameState.h" +#include "../../core/Guard.hpp" +#include "../../object/ObjectEntryManager.h" +#include "../../object/ObjectList.h" +#include "../../object/ObjectManager.h" +#include "../../object/SmallSceneryEntry.h" +#include "../../object/TerrainSurfaceObject.h" +#include "../../util/Util.h" +#include "../Map.h" +#include "../tile_element/SmallSceneryElement.h" +#include "../tile_element/SurfaceElement.h" +#include "../tile_element/TileElement.h" +#include "MapGen.h" +#include "SimplexNoise.h" + +#include + +using namespace OpenRCT2; + +static constexpr const char* kGrassTrees[] = { + // Dark + "rct2.scenery_small.tcf", // Caucasian Fir Tree + "rct2.scenery_small.trf", // Red Fir Tree + "rct2.scenery_small.trf2", // Red Fir Tree + "rct2.scenery_small.tsp", // Scots Pine Tree + "rct2.scenery_small.tmzp", // Montezuma Pine Tree + "rct2.scenery_small.tap", // Aleppo Pine Tree + "rct2.scenery_small.tcrp", // Corsican Pine Tree + "rct2.scenery_small.tbp", // Black Poplar Tree + + // Light + "rct2.scenery_small.tcl", // Cedar of Lebanon Tree + "rct2.scenery_small.tel", // European Larch Tree +}; + +static constexpr const char* kDesertTrees[] = { + "rct2.scenery_small.tmp", // Monkey-Puzzle Tree + "rct2.scenery_small.thl", // Honey Locust Tree + "rct2.scenery_small.th1", // Canary Palm Tree + "rct2.scenery_small.th2", // Palm Tree + "rct2.scenery_small.tpm", // Palm Tree + "rct2.scenery_small.tropt1", // Tree + "rct2.scenery_small.tbc", // Cactus + "rct2.scenery_small.tsc", // Cactus +}; + +static constexpr const char* kSnowTrees[] = { + "rct2.scenery_small.tcfs", // Snow-covered Caucasian Fir Tree + "rct2.scenery_small.tnss", // Snow-covered Norway Spruce Tree + "rct2.scenery_small.trf3", // Snow-covered Red Fir Tree + "rct2.scenery_small.trfs", // Snow-covered Red Fir Tree +}; + +static void MapGenPlaceTree(ObjectEntryIndex type, const CoordsXY& loc) +{ + auto* sceneryEntry = ObjectManager::GetObjectEntry(type); + if (sceneryEntry == nullptr) + { + return; + } + + int32_t surfaceZ = TileElementHeight(loc.ToTileCentre()); + + auto* sceneryElement = TileElementInsert({ loc, surfaceZ }, 0b1111); + Guard::Assert(sceneryElement != nullptr); + + sceneryElement->SetClearanceZ(surfaceZ + sceneryEntry->height); + sceneryElement->SetDirection(UtilRand() & 3); + sceneryElement->SetEntryIndex(type); + sceneryElement->SetAge(0); + sceneryElement->SetPrimaryColour(COLOUR_YELLOW); +} + +static bool MapGenSurfaceTakesGrassTrees(const TerrainSurfaceObject& surface) +{ + const auto& id = surface.GetIdentifier(); + return id == "rct2.terrain_surface.grass" || id == "rct2.terrain_surface.grass_clumps" || id == "rct2.terrain_surface.dirt"; +} + +static bool MapGenSurfaceTakesSandTrees(const TerrainSurfaceObject& surface) +{ + const auto& id = surface.GetIdentifier(); + return id == "rct2.terrain_surface.sand" || id == "rct2.terrain_surface.sand_brown" + || id == "rct2.terrain_surface.sand_red"; +} + +static bool MapGenSurfaceTakesSnowTrees(const TerrainSurfaceObject& surface) +{ + const auto& id = surface.GetIdentifier(); + return id == "rct2.terrain_surface.ice"; +} + +template +static bool TryFindTreeInList(std::string_view id, const T& treeList) +{ + for (size_t j = 0; j < std::size(treeList); j++) + { + if (treeList[j] == id) + return true; + } + return false; +} + +/** + * Randomly places a selection of preset trees on the map. Picks the right tree for the terrain it is placing it on. + */ +void MapGenPlaceTrees(MapGenSettings* settings) +{ + std::vector grassTreeIds; + std::vector desertTreeIds; + std::vector snowTreeIds; + + for (auto i = 0u; i < getObjectEntryGroupCount(ObjectType::SmallScenery); i++) + { + auto* sceneryEntry = OpenRCT2::ObjectManager::GetObjectEntry(i); + auto entry = ObjectEntryGetObject(ObjectType::SmallScenery, i); + + if (sceneryEntry == nullptr) + continue; + + if (TryFindTreeInList(entry->GetIdentifier(), kGrassTrees)) + { + grassTreeIds.push_back(i); + } + else if (TryFindTreeInList(entry->GetIdentifier(), kDesertTrees)) + { + desertTreeIds.push_back(i); + } + else if (TryFindTreeInList(entry->GetIdentifier(), kSnowTrees)) + { + snowTreeIds.push_back(i); + } + } + + // Place trees + float treeToLandRatio = static_cast(settings->treeToLandRatio) / 100.0f; + + auto& gameState = GetGameState(); + for (int32_t y = 1; y < gameState.MapSize.y - 1; y++) + { + for (int32_t x = 1; x < gameState.MapSize.x - 1; x++) + { + auto pos = CoordsXY{ x, y } * kCoordsXYStep; + auto* surfaceElement = MapGetSurfaceElementAt(pos); + if (surfaceElement == nullptr) + continue; + + // Don't place on water + if (surfaceElement->GetWaterHeight() > 0) + continue; + + if (settings->minTreeAltitude > surfaceElement->BaseHeight + || settings->maxTreeAltitude < surfaceElement->BaseHeight) + continue; + + // On sand surfaces, give the tile a score based on nearby water, to be used to determine whether to spawn + // vegetation + float oasisScore = 0.0f; + ObjectEntryIndex treeObjectEntryIndex = OBJECT_ENTRY_INDEX_NULL; + const auto& surfaceStyleObject = *TerrainSurfaceObject::GetById(surfaceElement->GetSurfaceObjectIndex()); + if (MapGenSurfaceTakesSandTrees(surfaceStyleObject)) + { + oasisScore = -0.5f; + constexpr auto maxOasisDistance = 4; + for (int32_t offsetY = -maxOasisDistance; offsetY <= maxOasisDistance; offsetY++) + { + for (int32_t offsetX = -maxOasisDistance; offsetX <= maxOasisDistance; offsetX++) + { + // Get map coord, clamped to the edges + const auto offset = CoordsXY{ offsetX * kCoordsXYStep, offsetY * kCoordsXYStep }; + auto neighbourPos = pos + offset; + neighbourPos.x = std::clamp(neighbourPos.x, kCoordsXYStep, kCoordsXYStep * (gameState.MapSize.x - 1)); + neighbourPos.y = std::clamp(neighbourPos.y, kCoordsXYStep, kCoordsXYStep * (gameState.MapSize.y - 1)); + + const auto neighboutSurface = MapGetSurfaceElementAt(neighbourPos); + if (neighboutSurface != nullptr && neighboutSurface->GetWaterHeight() > 0) + { + float distance = std::sqrt(offsetX * offsetX + offsetY * offsetY); + oasisScore += 0.5f / (maxOasisDistance * distance); + } + } + } + } + + // Use tree:land ratio except when near an oasis + constexpr static auto randModulo = 0xFFFF; + if (static_cast(UtilRand() & randModulo) / randModulo > std::max(treeToLandRatio, oasisScore)) + continue; + + // Use fractal noise to group tiles that are likely to spawn trees together + float noiseValue = FractalNoise(x, y, 0.025f, 2, 2.0f, 0.65f); + // Reduces the range to rarely stray further than 0.5 from the mean. + float noiseOffset = UtilRandNormalDistributed() * 0.25f; + if (noiseValue + oasisScore < noiseOffset) + continue; + + if (!grassTreeIds.empty() && MapGenSurfaceTakesGrassTrees(surfaceStyleObject)) + { + treeObjectEntryIndex = grassTreeIds[UtilRand() % grassTreeIds.size()]; + } + else if (!desertTreeIds.empty() && MapGenSurfaceTakesSandTrees(surfaceStyleObject)) + { + treeObjectEntryIndex = desertTreeIds[UtilRand() % desertTreeIds.size()]; + } + else if (!snowTreeIds.empty() && MapGenSurfaceTakesSnowTrees(surfaceStyleObject)) + { + treeObjectEntryIndex = snowTreeIds[UtilRand() % snowTreeIds.size()]; + } + + if (treeObjectEntryIndex != OBJECT_ENTRY_INDEX_NULL) + MapGenPlaceTree(treeObjectEntryIndex, pos); + } + } +} diff --git a/src/openrct2/world/map_generator/TreePlacement.h b/src/openrct2/world/map_generator/TreePlacement.h new file mode 100644 index 0000000000..c8e4fe9745 --- /dev/null +++ b/src/openrct2/world/map_generator/TreePlacement.h @@ -0,0 +1,12 @@ +/***************************************************************************** + * Copyright (c) 2014-2025 OpenRCT2 developers + * + * For a complete list of all authors, please refer to contributors.md + * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 + * + * OpenRCT2 is licensed under the GNU General Public License version 3. + *****************************************************************************/ + +struct MapGenSettings; + +void MapGenPlaceTrees(MapGenSettings* settings); From bffeb8125890f5e3d9b227b6abc518da52a3a19f Mon Sep 17 00:00:00 2001 From: Aaron van Geffen Date: Mon, 30 Dec 2024 15:27:05 +0100 Subject: [PATCH 04/10] Remove some unnecessary includes --- src/openrct2/world/map_generator/MapGen.cpp | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/openrct2/world/map_generator/MapGen.cpp b/src/openrct2/world/map_generator/MapGen.cpp index 353d9a3a85..3d50b41250 100644 --- a/src/openrct2/world/map_generator/MapGen.cpp +++ b/src/openrct2/world/map_generator/MapGen.cpp @@ -10,24 +10,13 @@ #include "MapGen.h" #include "../../Context.h" -#include "../../Diagnostic.h" -#include "../../Game.h" #include "../../GameState.h" -#include "../../core/Guard.hpp" -#include "../../core/String.hpp" -#include "../../object/ObjectEntryManager.h" -#include "../../object/ObjectList.h" #include "../../object/ObjectManager.h" -#include "../../object/SmallSceneryEntry.h" #include "../../object/TerrainEdgeObject.h" #include "../../object/TerrainSurfaceObject.h" -#include "../../platform/Platform.h" #include "../../util/Util.h" -#include "../Map.h" -#include "../Scenery.h" #include "../tile_element/Slope.h" #include "../tile_element/SurfaceElement.h" -#include "../tile_element/TileElement.h" #include "MapHelpers.h" #include "PngTerrainGenerator.h" #include "SimplexNoise.h" From 57984db60aa52b9f3bdd41f06687f8f6dd92377c Mon Sep 17 00:00:00 2001 From: Aaron van Geffen Date: Mon, 30 Dec 2024 15:53:42 +0100 Subject: [PATCH 05/10] Move SurfaceSelection into its own unit as well --- src/openrct2/libopenrct2.vcxproj | 2 + src/openrct2/world/map_generator/MapGen.cpp | 78 +------------- src/openrct2/world/map_generator/MapGen.h | 5 - .../map_generator/PngTerrainGenerator.cpp | 1 + .../world/map_generator/SurfaceSelection.cpp | 101 ++++++++++++++++++ .../world/map_generator/SurfaceSelection.h | 18 ++++ 6 files changed, 125 insertions(+), 80 deletions(-) create mode 100644 src/openrct2/world/map_generator/SurfaceSelection.cpp create mode 100644 src/openrct2/world/map_generator/SurfaceSelection.h diff --git a/src/openrct2/libopenrct2.vcxproj b/src/openrct2/libopenrct2.vcxproj index 781ff6427e..c78270d037 100644 --- a/src/openrct2/libopenrct2.vcxproj +++ b/src/openrct2/libopenrct2.vcxproj @@ -628,6 +628,7 @@ + @@ -1127,6 +1128,7 @@ + diff --git a/src/openrct2/world/map_generator/MapGen.cpp b/src/openrct2/world/map_generator/MapGen.cpp index 3d50b41250..f256932df4 100644 --- a/src/openrct2/world/map_generator/MapGen.cpp +++ b/src/openrct2/world/map_generator/MapGen.cpp @@ -12,26 +12,19 @@ #include "../../Context.h" #include "../../GameState.h" #include "../../object/ObjectManager.h" -#include "../../object/TerrainEdgeObject.h" -#include "../../object/TerrainSurfaceObject.h" #include "../../util/Util.h" #include "../tile_element/Slope.h" #include "../tile_element/SurfaceElement.h" #include "MapHelpers.h" #include "PngTerrainGenerator.h" #include "SimplexNoise.h" +#include "SurfaceSelection.h" #include "TreePlacement.h" #include using namespace OpenRCT2; -// Randomly chosen base terrains. We rarely want a whole map made out of chequerboard or rock. -static constexpr std::string_view BaseTerrain[] = { - "rct2.terrain_surface.grass", "rct2.terrain_surface.sand", "rct2.terrain_surface.sand_brown", - "rct2.terrain_surface.dirt", "rct2.terrain_surface.ice", -}; - static void MapGenGenerateBlank(MapGenSettings* settings); static void MapGenGenerateSimplex(MapGenSettings* settings); @@ -87,60 +80,6 @@ static void SetHeight(int32_t x, int32_t y, int32_t height) _height[x + y * _heightSize.x] = height; } -ObjectEntryIndex MapGenSurfaceTextureId(MapGenSettings* settings) -{ - auto& objectManager = OpenRCT2::GetContext()->GetObjectManager(); - - const auto selectedFloor = TerrainSurfaceObject::GetById(settings->landTexture); - std::string_view surfaceTexture = selectedFloor != nullptr ? selectedFloor->GetIdentifier() : ""; - - if (surfaceTexture.empty()) - { - std::vector availableTerrains; - std::copy_if(std::begin(BaseTerrain), std::end(BaseTerrain), std::back_inserter(availableTerrains), [&](auto terrain) { - return objectManager.GetLoadedObject(ObjectEntryDescriptor(terrain)) != nullptr; - }); - - if (availableTerrains.empty()) - // Fall back to the first available surface texture that is available in the park - surfaceTexture = TerrainSurfaceObject::GetById(0)->GetIdentifier(); - else - surfaceTexture = availableTerrains[UtilRand() % availableTerrains.size()]; - } - - auto surfaceTextureId = objectManager.GetLoadedObjectEntryIndex(ObjectEntryDescriptor(surfaceTexture)); - return surfaceTextureId; -} - -ObjectEntryIndex MapGenEdgeTextureId(MapGenSettings* settings, ObjectEntryIndex surfaceTextureId) -{ - auto& objectManager = OpenRCT2::GetContext()->GetObjectManager(); - - const auto selectedEdge = TerrainEdgeObject::GetById(settings->edgeTexture); - std::string_view edgeTexture = selectedEdge != nullptr ? selectedEdge->GetIdentifier() : ""; - - if (edgeTexture.empty()) - { - auto surfaceObject = objectManager.GetLoadedObject(ObjectType::TerrainSurface, surfaceTextureId); - auto surfaceTexture = surfaceObject->GetIdentifier(); - - // Base edge type on surface type - if (surfaceTexture == "rct2.terrain_surface.dirt") - edgeTexture = "rct2.terrain_edge.wood_red"; - else if (surfaceTexture == "rct2.terrain_surface.ice") - edgeTexture = "rct2.terrain_edge.ice"; - else - edgeTexture = "rct2.terrain_edge.rock"; - - // Fall back to the first available edge texture that is available in the park - if (objectManager.GetLoadedObject(ObjectEntryDescriptor(edgeTexture)) == nullptr) - edgeTexture = TerrainEdgeObject::GetById(0)->GetIdentifier(); - } - - auto edgeTextureId = objectManager.GetLoadedObjectEntryIndex(ObjectEntryDescriptor(edgeTexture)); - return edgeTextureId; -} - static void MapGenResetSurfaces(MapGenSettings* settings) { MapClearAllElements(); @@ -202,21 +141,10 @@ static void MapGenGenerateSimplex(MapGenSettings* settings) static void MapGenAddBeaches(MapGenSettings* settings) { - auto& objectManager = OpenRCT2::GetContext()->GetObjectManager(); - - // Figure out what beach texture to use - std::vector availableBeachTextures; - if (objectManager.GetLoadedObject(ObjectEntryDescriptor("rct2.terrain_surface.sand")) != nullptr) - availableBeachTextures.push_back("rct2.terrain_surface.sand"); - if (objectManager.GetLoadedObject(ObjectEntryDescriptor("rct2.terrain_surface.sand_brown")) != nullptr) - availableBeachTextures.push_back("rct2.terrain_surface.sand_brown"); - - if (availableBeachTextures.empty()) + auto beachTextureId = MapGenBeachTextureId(); + if (beachTextureId == OBJECT_ENTRY_INDEX_NULL) return; - std::string_view beachTexture = availableBeachTextures[UtilRand() % availableBeachTextures.size()]; - auto beachTextureId = objectManager.GetLoadedObjectEntryIndex(ObjectEntryDescriptor(beachTexture)); - // Add sandy beaches const auto& mapSize = settings->mapSize; for (auto y = 1; y < mapSize.y - 1; y++) diff --git a/src/openrct2/world/map_generator/MapGen.h b/src/openrct2/world/map_generator/MapGen.h index cc10be6468..5d45c52aed 100644 --- a/src/openrct2/world/map_generator/MapGen.h +++ b/src/openrct2/world/map_generator/MapGen.h @@ -9,8 +9,6 @@ #pragma once -#include "../../core/StringTypes.h" -#include "../../object/ObjectTypes.h" #include "../Location.hpp" enum class MapGenAlgorithm : uint8_t @@ -50,6 +48,3 @@ struct MapGenSettings }; void MapGenGenerate(MapGenSettings* settings); - -ObjectEntryIndex MapGenSurfaceTextureId(MapGenSettings* settings); -ObjectEntryIndex MapGenEdgeTextureId(MapGenSettings* settings, ObjectEntryIndex surfaceTextureId); diff --git a/src/openrct2/world/map_generator/PngTerrainGenerator.cpp b/src/openrct2/world/map_generator/PngTerrainGenerator.cpp index e7bf284b52..763e459094 100644 --- a/src/openrct2/world/map_generator/PngTerrainGenerator.cpp +++ b/src/openrct2/world/map_generator/PngTerrainGenerator.cpp @@ -18,6 +18,7 @@ #include "../tile_element/SurfaceElement.h" #include "MapGen.h" #include "MapHelpers.h" +#include "SurfaceSelection.h" #include diff --git a/src/openrct2/world/map_generator/SurfaceSelection.cpp b/src/openrct2/world/map_generator/SurfaceSelection.cpp new file mode 100644 index 0000000000..b6e4f59964 --- /dev/null +++ b/src/openrct2/world/map_generator/SurfaceSelection.cpp @@ -0,0 +1,101 @@ +/***************************************************************************** + * Copyright (c) 2014-2025 OpenRCT2 developers + * + * For a complete list of all authors, please refer to contributors.md + * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 + * + * OpenRCT2 is licensed under the GNU General Public License version 3. + *****************************************************************************/ + +#include "SurfaceSelection.h" + +#include "../../Context.h" +#include "../../object/ObjectEntryManager.h" +#include "../../object/ObjectList.h" +#include "../../object/ObjectManager.h" +#include "../../object/TerrainEdgeObject.h" +#include "../../object/TerrainSurfaceObject.h" +#include "../../util/Util.h" +#include "MapGen.h" + +#include + +using namespace OpenRCT2; + +// Randomly chosen base terrains. We rarely want a whole map made out of chequerboard or rock. +static constexpr std::string_view kBaseTerrain[] = { + "rct2.terrain_surface.grass", "rct2.terrain_surface.sand", "rct2.terrain_surface.sand_brown", + "rct2.terrain_surface.dirt", "rct2.terrain_surface.ice", +}; + +ObjectEntryIndex MapGenSurfaceTextureId(MapGenSettings* settings) +{ + auto& objectManager = GetContext()->GetObjectManager(); + + const auto selectedFloor = TerrainSurfaceObject::GetById(settings->landTexture); + std::string_view surfaceTexture = selectedFloor != nullptr ? selectedFloor->GetIdentifier() : ""; + + if (surfaceTexture.empty()) + { + std::vector availableTerrains; + std::copy_if( + std::begin(kBaseTerrain), std::end(kBaseTerrain), std::back_inserter(availableTerrains), + [&](auto terrain) { return objectManager.GetLoadedObject(ObjectEntryDescriptor(terrain)) != nullptr; }); + + if (availableTerrains.empty()) + // Fall back to the first available surface texture that is available in the park + surfaceTexture = TerrainSurfaceObject::GetById(0)->GetIdentifier(); + else + surfaceTexture = availableTerrains[UtilRand() % availableTerrains.size()]; + } + + auto surfaceTextureId = objectManager.GetLoadedObjectEntryIndex(ObjectEntryDescriptor(surfaceTexture)); + return surfaceTextureId; +} + +ObjectEntryIndex MapGenEdgeTextureId(MapGenSettings* settings, ObjectEntryIndex surfaceTextureId) +{ + auto& objectManager = GetContext()->GetObjectManager(); + + const auto selectedEdge = TerrainEdgeObject::GetById(settings->edgeTexture); + std::string_view edgeTexture = selectedEdge != nullptr ? selectedEdge->GetIdentifier() : ""; + + if (edgeTexture.empty()) + { + auto surfaceObject = objectManager.GetLoadedObject(ObjectType::TerrainSurface, surfaceTextureId); + auto surfaceTexture = surfaceObject->GetIdentifier(); + + // Base edge type on surface type + if (surfaceTexture == "rct2.terrain_surface.dirt") + edgeTexture = "rct2.terrain_edge.wood_red"; + else if (surfaceTexture == "rct2.terrain_surface.ice") + edgeTexture = "rct2.terrain_edge.ice"; + else + edgeTexture = "rct2.terrain_edge.rock"; + + // Fall back to the first available edge texture that is available in the park + if (objectManager.GetLoadedObject(ObjectEntryDescriptor(edgeTexture)) == nullptr) + edgeTexture = TerrainEdgeObject::GetById(0)->GetIdentifier(); + } + + auto edgeTextureId = objectManager.GetLoadedObjectEntryIndex(ObjectEntryDescriptor(edgeTexture)); + return edgeTextureId; +} + +ObjectEntryIndex MapGenBeachTextureId() +{ + auto& objectManager = GetContext()->GetObjectManager(); + + // Figure out what beach texture to use + std::vector availableBeachTextures; + if (objectManager.GetLoadedObject(ObjectEntryDescriptor("rct2.terrain_surface.sand")) != nullptr) + availableBeachTextures.push_back("rct2.terrain_surface.sand"); + if (objectManager.GetLoadedObject(ObjectEntryDescriptor("rct2.terrain_surface.sand_brown")) != nullptr) + availableBeachTextures.push_back("rct2.terrain_surface.sand_brown"); + + if (availableBeachTextures.empty()) + return OBJECT_ENTRY_INDEX_NULL; + + std::string_view beachTexture = availableBeachTextures[UtilRand() % availableBeachTextures.size()]; + return objectManager.GetLoadedObjectEntryIndex(ObjectEntryDescriptor(beachTexture)); +} diff --git a/src/openrct2/world/map_generator/SurfaceSelection.h b/src/openrct2/world/map_generator/SurfaceSelection.h new file mode 100644 index 0000000000..945ec5f120 --- /dev/null +++ b/src/openrct2/world/map_generator/SurfaceSelection.h @@ -0,0 +1,18 @@ +/***************************************************************************** + * Copyright (c) 2014-2025 OpenRCT2 developers + * + * For a complete list of all authors, please refer to contributors.md + * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 + * + * OpenRCT2 is licensed under the GNU General Public License version 3. + *****************************************************************************/ + +#pragma once + +#include "../../object/ObjectTypes.h" + +struct MapGenSettings; + +ObjectEntryIndex MapGenSurfaceTextureId(MapGenSettings* settings); +ObjectEntryIndex MapGenEdgeTextureId(MapGenSettings* settings, ObjectEntryIndex surfaceTextureId); +ObjectEntryIndex MapGenBeachTextureId(); From 8cf9142b870fbd63465098738b73ef61284284a7 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen Date: Mon, 30 Dec 2024 16:21:22 +0100 Subject: [PATCH 06/10] Encapsulate map generator in World::MapGenerator namespace --- src/openrct2-ui/windows/MapGen.cpp | 50 +- src/openrct2/world/map_generator/MapGen.cpp | 451 +++++++-------- src/openrct2/world/map_generator/MapGen.h | 65 ++- .../world/map_generator/MapHelpers.cpp | 530 +++++++++--------- src/openrct2/world/map_generator/MapHelpers.h | 19 +- .../map_generator/PngTerrainGenerator.cpp | 403 +++++++------ .../world/map_generator/PngTerrainGenerator.h | 11 +- .../world/map_generator/SimplexNoise.cpp | 245 ++++---- .../world/map_generator/SimplexNoise.h | 7 +- .../world/map_generator/SurfaceSelection.cpp | 131 ++--- .../world/map_generator/SurfaceSelection.h | 11 +- .../world/map_generator/TreePlacement.cpp | 354 ++++++------ .../world/map_generator/TreePlacement.h | 7 +- 13 files changed, 1159 insertions(+), 1125 deletions(-) diff --git a/src/openrct2-ui/windows/MapGen.cpp b/src/openrct2-ui/windows/MapGen.cpp index 69abcbf936..0727377b26 100644 --- a/src/openrct2-ui/windows/MapGen.cpp +++ b/src/openrct2-ui/windows/MapGen.cpp @@ -28,6 +28,8 @@ #include #include +using namespace OpenRCT2::World; + namespace OpenRCT2::Ui::Windows { enum @@ -268,7 +270,7 @@ namespace OpenRCT2::Ui::Windows private: ResizeDirection _resizeDirection{ ResizeDirection::Both }; bool _mapWidthAndHeightLinked{ true }; - MapGenSettings _settings{}; + MapGenerator::Settings _settings{}; bool _randomTerrain = true; bool _heightmapLoaded = false; std::string _heightmapFilename{}; @@ -372,17 +374,17 @@ namespace OpenRCT2::Ui::Windows void GenerateMap() { - if (_settings.algorithm == MapGenAlgorithm::heightmapImage && !_heightmapLoaded) + if (_settings.algorithm == MapGenerator::Algorithm::heightmapImage && !_heightmapLoaded) return; - MapGenSettings mapgenSettings = _settings; + MapGenerator::Settings mapgenSettings = _settings; if (_randomTerrain) { mapgenSettings.landTexture = -1; mapgenSettings.edgeTexture = -1; } - MapGenGenerate(&mapgenSettings); + MapGenerator::generate(&mapgenSettings); GfxInvalidateScreen(); } @@ -392,9 +394,9 @@ namespace OpenRCT2::Ui::Windows { SharedMouseUp(widgetIndex); - if (_settings.algorithm == MapGenAlgorithm::simplexNoise) + if (_settings.algorithm == MapGenerator::Algorithm::simplexNoise) SimplexMouseUp(widgetIndex); - else if (_settings.algorithm == MapGenAlgorithm::heightmapImage) + else if (_settings.algorithm == MapGenerator::Algorithm::heightmapImage) HeightmapMouseUp(widgetIndex); switch (widgetIndex) @@ -415,10 +417,10 @@ namespace OpenRCT2::Ui::Windows void BaseMouseDown(WidgetIndex widgetIndex, Widget* widget) { - if (_settings.algorithm == MapGenAlgorithm::simplexNoise) + if (_settings.algorithm == MapGenerator::Algorithm::simplexNoise) SimplexMouseDown(widgetIndex, widget); - else if (_settings.algorithm == MapGenAlgorithm::heightmapImage) + else if (_settings.algorithm == MapGenerator::Algorithm::heightmapImage) HeightmapMouseDown(widgetIndex, widget); switch (widgetIndex) @@ -482,7 +484,7 @@ namespace OpenRCT2::Ui::Windows switch (widgetIndex) { case WIDX_HEIGHTMAP_SOURCE_DROPDOWN: - _settings.algorithm = MapGenAlgorithm(dropdownIndex); + _settings.algorithm = MapGenerator::Algorithm(dropdownIndex); Invalidate(); break; } @@ -490,9 +492,9 @@ namespace OpenRCT2::Ui::Windows void BaseTextInput(WidgetIndex widgetIndex, int32_t value) { - if (_settings.algorithm == MapGenAlgorithm::simplexNoise) + if (_settings.algorithm == MapGenerator::Algorithm::simplexNoise) SimplexTextInput(widgetIndex, value); - else if (_settings.algorithm == MapGenAlgorithm::heightmapImage) + else if (_settings.algorithm == MapGenerator::Algorithm::heightmapImage) HeightmapTextInput(widgetIndex, value); switch (widgetIndex) @@ -523,7 +525,7 @@ namespace OpenRCT2::Ui::Windows SetWidgetPressed(WIDX_MAP_SIZE_LINK, _mapWidthAndHeightLinked); SetWidgetDisabled(WIDX_MAP_SIZE_LINK, _settings.mapSize.x != _settings.mapSize.y); - bool isHeightMapImage = _settings.algorithm == MapGenAlgorithm::heightmapImage; + bool isHeightMapImage = _settings.algorithm == MapGenerator::Algorithm::heightmapImage; SetWidgetDisabled(WIDX_MAP_SIZE_Y, isHeightMapImage); SetWidgetDisabled(WIDX_MAP_SIZE_Y_UP, isHeightMapImage); SetWidgetDisabled(WIDX_MAP_SIZE_Y_DOWN, isHeightMapImage); @@ -552,19 +554,19 @@ namespace OpenRCT2::Ui::Windows auto& sourceWidget = widgets[WIDX_HEIGHTMAP_SOURCE]; switch (_settings.algorithm) { - case MapGenAlgorithm::blank: + case MapGenerator::Algorithm::blank: sourceWidget.text = STR_HEIGHTMAP_FLATLAND; ToggleSimplexWidgets(false); ToggleHeightmapWidgets(false); break; - case MapGenAlgorithm::simplexNoise: + case MapGenerator::Algorithm::simplexNoise: sourceWidget.text = STR_HEIGHTMAP_SIMPLEX_NOISE; ToggleSimplexWidgets(true); ToggleHeightmapWidgets(false); break; - case MapGenAlgorithm::heightmapImage: + case MapGenerator::Algorithm::heightmapImage: sourceWidget.text = STR_HEIGHTMAP_FILE; ToggleSimplexWidgets(false); ToggleHeightmapWidgets(true); @@ -604,10 +606,10 @@ namespace OpenRCT2::Ui::Windows DrawWidgets(dpi); DrawTabImages(dpi); - if (_settings.algorithm == MapGenAlgorithm::simplexNoise) + if (_settings.algorithm == MapGenerator::Algorithm::simplexNoise) SimplexDraw(dpi); - else if (_settings.algorithm == MapGenAlgorithm::heightmapImage) + else if (_settings.algorithm == MapGenerator::Algorithm::heightmapImage) HeightmapDraw(dpi); const auto enabledColour = colours[1]; @@ -754,7 +756,7 @@ namespace OpenRCT2::Ui::Windows SetPressedTab(); - const bool isFlatland = _settings.algorithm == MapGenAlgorithm::blank; + const bool isFlatland = _settings.algorithm == MapGenerator::Algorithm::blank; SetWidgetDisabled(WIDX_TREE_LAND_RATIO, !_settings.trees); SetWidgetDisabled(WIDX_TREE_LAND_RATIO_UP, !_settings.trees); @@ -802,7 +804,7 @@ namespace OpenRCT2::Ui::Windows STR_RIDE_LENGTH_ENTRY, ft, { textColour }); // Maximum tree altitude, label and value - const bool isFlatland = _settings.algorithm == MapGenAlgorithm::blank; + const bool isFlatland = _settings.algorithm == MapGenerator::Algorithm::blank; const auto maxTreeTextColour = _settings.trees && !isFlatland ? enabledColour : disabledColour; DrawTextBasic( @@ -1233,10 +1235,10 @@ namespace OpenRCT2::Ui::Windows SetWidgetEnabled(WIDX_WALL_TEXTURE, !_randomTerrain); // Max land height option is irrelevant for flatland - SetWidgetEnabled(WIDX_HEIGHTMAP_HIGH, _settings.algorithm != MapGenAlgorithm::blank); + SetWidgetEnabled(WIDX_HEIGHTMAP_HIGH, _settings.algorithm != MapGenerator::Algorithm::blank); // Only offer terrain edge smoothing if we don't use flatland terrain - SetWidgetEnabled(WIDX_HEIGHTMAP_SMOOTH_TILE_EDGES, _settings.algorithm != MapGenAlgorithm::blank); + SetWidgetEnabled(WIDX_HEIGHTMAP_SMOOTH_TILE_EDGES, _settings.algorithm != MapGenerator::Algorithm::blank); SetPressedTab(); } @@ -1396,7 +1398,7 @@ namespace OpenRCT2::Ui::Windows void OnClose() override { - MapGenUnloadHeightmapImage(); + MapGenerator::UnloadHeightmapImage(); } void OnMouseUp(WidgetIndex widgetIndex) override @@ -1455,7 +1457,7 @@ namespace OpenRCT2::Ui::Windows void OnPrepareDraw() override { - bool isHeightMapImage = _settings.algorithm == MapGenAlgorithm::heightmapImage; + bool isHeightMapImage = _settings.algorithm == MapGenerator::Algorithm::heightmapImage; SetWidgetDisabled(WIDX_MAP_GENERATE, isHeightMapImage && !_heightmapLoaded); switch (page) @@ -1535,7 +1537,7 @@ namespace OpenRCT2::Ui::Windows { if (result == MODAL_RESULT_OK) { - if (!MapGenLoadHeightmapImage(path)) + if (!MapGenerator::LoadHeightmapImage(path)) { // TODO: Display error popup return; diff --git a/src/openrct2/world/map_generator/MapGen.cpp b/src/openrct2/world/map_generator/MapGen.cpp index f256932df4..164d667084 100644 --- a/src/openrct2/world/map_generator/MapGen.cpp +++ b/src/openrct2/world/map_generator/MapGen.cpp @@ -23,257 +23,258 @@ #include -using namespace OpenRCT2; - -static void MapGenGenerateBlank(MapGenSettings* settings); -static void MapGenGenerateSimplex(MapGenSettings* settings); - -static void MapGenAddBeaches(MapGenSettings* settings); - -void MapGenGenerate(MapGenSettings* settings) +namespace OpenRCT2::World::MapGenerator { - // First, generate the height map - switch (settings->algorithm) + static void generateBlankMap(Settings* settings); + static void generateSimplexMap(Settings* settings); + + static void addBeaches(Settings* settings); + + void generate(Settings* settings) { - case MapGenAlgorithm::blank: - MapGenGenerateBlank(settings); - break; + // First, generate the height map + switch (settings->algorithm) + { + case Algorithm::blank: + generateBlankMap(settings); + break; - case MapGenAlgorithm::simplexNoise: - MapGenGenerateSimplex(settings); - break; + case Algorithm::simplexNoise: + generateSimplexMap(settings); + break; - case MapGenAlgorithm::heightmapImage: - MapGenGenerateFromHeightmapImage(settings); - break; + case Algorithm::heightmapImage: + GenerateFromHeightmapImage(settings); + break; + } + + // Add beaches? + if (settings->beaches) + addBeaches(settings); + + // Place trees? + if (settings->trees) + placeTrees(settings); } - // Add beaches? - if (settings->beaches) - MapGenAddBeaches(settings); + static void setWaterLevel(int32_t waterLevel); + static void smoothHeight(int32_t iterations); + static void setHeight(Settings* settings); - // Place trees? - if (settings->trees) - MapGenPlaceTrees(settings); -} - -static void MapGenSetWaterLevel(int32_t waterLevel); -static void MapGenSmoothHeight(int32_t iterations); -static void MapGenSetHeight(MapGenSettings* settings); - -static void MapGenSimplex(MapGenSettings* settings); - -static TileCoordsXY _heightSize; -static uint8_t* _height; - -static int32_t GetHeight(int32_t x, int32_t y) -{ - if (x >= 0 && y >= 0 && x < _heightSize.x && y < _heightSize.y) - return _height[x + y * _heightSize.x]; - - return 0; -} - -static void SetHeight(int32_t x, int32_t y, int32_t height) -{ - if (x >= 0 && y >= 0 && x < _heightSize.x && y < _heightSize.y) - _height[x + y * _heightSize.x] = height; -} - -static void MapGenResetSurfaces(MapGenSettings* settings) -{ - MapClearAllElements(); - MapInit(settings->mapSize); - - const auto surfaceTextureId = MapGenSurfaceTextureId(settings); - const auto edgeTextureId = MapGenEdgeTextureId(settings, surfaceTextureId); - - for (auto y = 1; y < settings->mapSize.y - 1; y++) + static void resetSurfaces(Settings* settings) { - for (auto x = 1; x < settings->mapSize.x - 1; x++) + MapClearAllElements(); + MapInit(settings->mapSize); + + const auto surfaceTextureId = generateSurfaceTextureId(settings); + const auto edgeTextureId = generateEdgeTextureId(settings, surfaceTextureId); + + for (auto y = 1; y < settings->mapSize.y - 1; y++) { - auto surfaceElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y }); - if (surfaceElement != nullptr) + for (auto x = 1; x < settings->mapSize.x - 1; x++) { - surfaceElement->SetSurfaceObjectIndex(surfaceTextureId); - surfaceElement->SetEdgeObjectIndex(edgeTextureId); - surfaceElement->BaseHeight = settings->heightmapLow; - surfaceElement->ClearanceHeight = settings->heightmapLow; - } - } - } -} - -static void MapGenGenerateBlank(MapGenSettings* settings) -{ - MapGenResetSurfaces(settings); - MapGenSetWaterLevel(settings->waterLevel); -} - -static void MapGenGenerateSimplex(MapGenSettings* settings) -{ - MapGenResetSurfaces(settings); - - // Create the temporary height map and initialise - auto& mapSize = settings->mapSize; - _heightSize = { mapSize.x * 2, mapSize.y * 2 }; - _height = new uint8_t[_heightSize.y * _heightSize.x]; - std::fill_n(_height, _heightSize.y * _heightSize.x, 0x00); - - MapGenSimplex(settings); - MapGenSmoothHeight(2 + (UtilRand() % 6)); - - // Set the game map to the height map - MapGenSetHeight(settings); - delete[] _height; - - if (settings->smoothTileEdges) - { - // Set the tile slopes so that there are no cliffs - while (MapSmooth(1, 1, mapSize.x - 1, mapSize.y - 1)) - { - } - } - - // Add the water - MapGenSetWaterLevel(settings->waterLevel); -} - -static void MapGenAddBeaches(MapGenSettings* settings) -{ - auto beachTextureId = MapGenBeachTextureId(); - if (beachTextureId == OBJECT_ENTRY_INDEX_NULL) - return; - - // Add sandy beaches - const auto& mapSize = settings->mapSize; - for (auto y = 1; y < mapSize.y - 1; y++) - { - for (auto x = 1; x < mapSize.x - 1; x++) - { - auto surfaceElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y }); - - if (surfaceElement != nullptr && surfaceElement->BaseHeight < settings->waterLevel + 6) - surfaceElement->SetSurfaceObjectIndex(beachTextureId); - } - } -} - -/** - * Sets each tile's water level to the specified water level if underneath that water level. - */ -static void MapGenSetWaterLevel(int32_t waterLevel) -{ - auto& gameState = GetGameState(); - for (int32_t y = 1; y < gameState.MapSize.y - 1; y++) - { - for (int32_t x = 1; x < gameState.MapSize.x - 1; x++) - { - auto surfaceElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y }); - if (surfaceElement != nullptr && surfaceElement->BaseHeight < waterLevel) - surfaceElement->SetWaterHeight(waterLevel * kCoordsZStep); - } - } -} - -/** - * Smooths the height map. - */ -static void MapGenSmoothHeight(int32_t iterations) -{ - int32_t i, x, y, xx, yy, avg; - int32_t arraySize = _heightSize.y * _heightSize.x * sizeof(uint8_t); - uint8_t* copyHeight = new uint8_t[arraySize]; - - for (i = 0; i < iterations; i++) - { - std::memcpy(copyHeight, _height, arraySize); - for (y = 1; y < _heightSize.y - 1; y++) - { - for (x = 1; x < _heightSize.x - 1; x++) - { - avg = 0; - for (yy = -1; yy <= 1; yy++) + auto surfaceElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y }); + if (surfaceElement != nullptr) { - for (xx = -1; xx <= 1; xx++) - { - avg += copyHeight[(y + yy) * _heightSize.x + (x + xx)]; - } + surfaceElement->SetSurfaceObjectIndex(surfaceTextureId); + surfaceElement->SetEdgeObjectIndex(edgeTextureId); + surfaceElement->BaseHeight = settings->heightmapLow; + surfaceElement->ClearanceHeight = settings->heightmapLow; } - avg /= 9; - SetHeight(x, y, avg); } } } - delete[] copyHeight; -} - -/** - * Sets the height of the actual game map tiles to the height map. - */ -static void MapGenSetHeight(MapGenSettings* settings) -{ - int32_t x, y, heightX, heightY; - - for (y = 1; y < _heightSize.y / 2 - 1; y++) + static void generateBlankMap(Settings* settings) { - for (x = 1; x < _heightSize.x / 2 - 1; x++) + resetSurfaces(settings); + setWaterLevel(settings->waterLevel); + } + + static TileCoordsXY _heightSize; + static uint8_t* _height; + + static int32_t getHeight(int32_t x, int32_t y) + { + if (x >= 0 && y >= 0 && x < _heightSize.x && y < _heightSize.y) + return _height[x + y * _heightSize.x]; + + return 0; + } + + static void setHeight(int32_t x, int32_t y, int32_t height) + { + if (x >= 0 && y >= 0 && x < _heightSize.x && y < _heightSize.y) + _height[x + y * _heightSize.x] = height; + } + + static void generateSimplexNoise(Settings* settings); + + static void generateSimplexMap(Settings* settings) + { + resetSurfaces(settings); + + // Create the temporary height map and initialise + auto& mapSize = settings->mapSize; + _heightSize = { mapSize.x * 2, mapSize.y * 2 }; + _height = new uint8_t[_heightSize.y * _heightSize.x]; + std::fill_n(_height, _heightSize.y * _heightSize.x, 0x00); + + generateSimplexNoise(settings); + smoothHeight(2 + (UtilRand() % 6)); + + // Set the game map to the height map + setHeight(settings); + delete[] _height; + + if (settings->smoothTileEdges) { - heightX = x * 2; - heightY = y * 2; + // Set the tile slopes so that there are no cliffs + while (MapSmooth(1, 1, mapSize.x - 1, mapSize.y - 1)) + { + } + } - uint8_t q00 = GetHeight(heightX + 0, heightY + 0); - uint8_t q01 = GetHeight(heightX + 0, heightY + 1); - uint8_t q10 = GetHeight(heightX + 1, heightY + 0); - uint8_t q11 = GetHeight(heightX + 1, heightY + 1); + // Add the water + setWaterLevel(settings->waterLevel); + } - uint8_t baseHeight = (q00 + q01 + q10 + q11) / 4; + static void addBeaches(Settings* settings) + { + auto beachTextureId = generateBeachTextureId(); + if (beachTextureId == OBJECT_ENTRY_INDEX_NULL) + return; - auto surfaceElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y }); - if (surfaceElement == nullptr) - continue; - surfaceElement->BaseHeight = std::max(2, baseHeight * 2); + // Add sandy beaches + const auto& mapSize = settings->mapSize; + for (auto y = 1; y < mapSize.y - 1; y++) + { + for (auto x = 1; x < mapSize.x - 1; x++) + { + auto surfaceElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y }); - // If base height is below water level, lower it to create more natural shorelines - if (surfaceElement->BaseHeight >= 4 && surfaceElement->BaseHeight <= settings->waterLevel) - surfaceElement->BaseHeight -= 2; - - surfaceElement->ClearanceHeight = surfaceElement->BaseHeight; - - uint8_t currentSlope = surfaceElement->GetSlope(); - - if (q00 > baseHeight) - currentSlope |= kTileSlopeSCornerUp; - if (q01 > baseHeight) - currentSlope |= kTileSlopeWCornerUp; - if (q10 > baseHeight) - currentSlope |= kTileSlopeECornerUp; - if (q11 > baseHeight) - currentSlope |= kTileSlopeNCornerUp; - - surfaceElement->SetSlope(currentSlope); + if (surfaceElement != nullptr && surfaceElement->BaseHeight < settings->waterLevel + 6) + surfaceElement->SetSurfaceObjectIndex(beachTextureId); + } } } -} -static void MapGenSimplex(MapGenSettings* settings) -{ - float freq = settings->simplex_base_freq / 100.0f * (1.0f / _heightSize.x); - int32_t octaves = settings->simplex_octaves; - - int32_t low = settings->heightmapLow / 2; - int32_t high = settings->heightmapHigh / 2 - low; - - NoiseRand(); - for (int32_t y = 0; y < _heightSize.y; y++) + /** + * Sets each tile's water level to the specified water level if underneath that water level. + */ + static void setWaterLevel(int32_t waterLevel) { - for (int32_t x = 0; x < _heightSize.x; x++) + auto& gameState = GetGameState(); + for (int32_t y = 1; y < gameState.MapSize.y - 1; y++) { - float noiseValue = std::clamp(FractalNoise(x, y, freq, octaves, 2.0f, 0.65f), -1.0f, 1.0f); - float normalisedNoiseValue = (noiseValue + 1.0f) / 2.0f; - - SetHeight(x, y, low + static_cast(normalisedNoiseValue * high)); + for (int32_t x = 1; x < gameState.MapSize.x - 1; x++) + { + auto surfaceElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y }); + if (surfaceElement != nullptr && surfaceElement->BaseHeight < waterLevel) + surfaceElement->SetWaterHeight(waterLevel * kCoordsZStep); + } } } -} + + /** + * Smooths the height map. + */ + static void smoothHeight(int32_t iterations) + { + int32_t i, x, y, xx, yy, avg; + int32_t arraySize = _heightSize.y * _heightSize.x * sizeof(uint8_t); + uint8_t* copyHeight = new uint8_t[arraySize]; + + for (i = 0; i < iterations; i++) + { + std::memcpy(copyHeight, _height, arraySize); + for (y = 1; y < _heightSize.y - 1; y++) + { + for (x = 1; x < _heightSize.x - 1; x++) + { + avg = 0; + for (yy = -1; yy <= 1; yy++) + { + for (xx = -1; xx <= 1; xx++) + { + avg += copyHeight[(y + yy) * _heightSize.x + (x + xx)]; + } + } + avg /= 9; + setHeight(x, y, avg); + } + } + } + + delete[] copyHeight; + } + + /** + * Sets the height of the actual game map tiles to the height map. + */ + static void setHeight(Settings* settings) + { + int32_t x, y, heightX, heightY; + + for (y = 1; y < _heightSize.y / 2 - 1; y++) + { + for (x = 1; x < _heightSize.x / 2 - 1; x++) + { + heightX = x * 2; + heightY = y * 2; + + uint8_t q00 = getHeight(heightX + 0, heightY + 0); + uint8_t q01 = getHeight(heightX + 0, heightY + 1); + uint8_t q10 = getHeight(heightX + 1, heightY + 0); + uint8_t q11 = getHeight(heightX + 1, heightY + 1); + + uint8_t baseHeight = (q00 + q01 + q10 + q11) / 4; + + auto surfaceElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y }); + if (surfaceElement == nullptr) + continue; + surfaceElement->BaseHeight = std::max(2, baseHeight * 2); + + // If base height is below water level, lower it to create more natural shorelines + if (surfaceElement->BaseHeight >= 4 && surfaceElement->BaseHeight <= settings->waterLevel) + surfaceElement->BaseHeight -= 2; + + surfaceElement->ClearanceHeight = surfaceElement->BaseHeight; + + uint8_t currentSlope = surfaceElement->GetSlope(); + + if (q00 > baseHeight) + currentSlope |= kTileSlopeSCornerUp; + if (q01 > baseHeight) + currentSlope |= kTileSlopeWCornerUp; + if (q10 > baseHeight) + currentSlope |= kTileSlopeECornerUp; + if (q11 > baseHeight) + currentSlope |= kTileSlopeNCornerUp; + + surfaceElement->SetSlope(currentSlope); + } + } + } + + static void generateSimplexNoise(Settings* settings) + { + float freq = settings->simplex_base_freq / 100.0f * (1.0f / _heightSize.x); + int32_t octaves = settings->simplex_octaves; + + int32_t low = settings->heightmapLow / 2; + int32_t high = settings->heightmapHigh / 2 - low; + + NoiseRand(); + for (int32_t y = 0; y < _heightSize.y; y++) + { + for (int32_t x = 0; x < _heightSize.x; x++) + { + float noiseValue = std::clamp(FractalNoise(x, y, freq, octaves, 2.0f, 0.65f), -1.0f, 1.0f); + float normalisedNoiseValue = (noiseValue + 1.0f) / 2.0f; + + setHeight(x, y, low + static_cast(normalisedNoiseValue * high)); + } + } + } +} // namespace OpenRCT2::World::MapGenerator diff --git a/src/openrct2/world/map_generator/MapGen.h b/src/openrct2/world/map_generator/MapGen.h index 5d45c52aed..cf1c2476e4 100644 --- a/src/openrct2/world/map_generator/MapGen.h +++ b/src/openrct2/world/map_generator/MapGen.h @@ -11,40 +11,43 @@ #include "../Location.hpp" -enum class MapGenAlgorithm : uint8_t +namespace OpenRCT2::World::MapGenerator { - blank, - simplexNoise, - heightmapImage, -}; + enum class Algorithm : uint8_t + { + blank, + simplexNoise, + heightmapImage, + }; -struct MapGenSettings -{ - // Base - MapGenAlgorithm algorithm = MapGenAlgorithm::blank; - TileCoordsXY mapSize{ 150, 150 }; - int32_t waterLevel = 6; - int32_t landTexture = 0; - int32_t edgeTexture = 0; - int32_t heightmapLow = 14; - int32_t heightmapHigh = 60; - bool smoothTileEdges = true; + struct Settings + { + // Base + Algorithm algorithm = Algorithm::blank; + TileCoordsXY mapSize{ 150, 150 }; + int32_t waterLevel = 6; + int32_t landTexture = 0; + int32_t edgeTexture = 0; + int32_t heightmapLow = 14; + int32_t heightmapHigh = 60; + bool smoothTileEdges = true; - // Features (e.g. tree, rivers, lakes etc.) - bool trees = true; - int32_t treeToLandRatio = 25; - int32_t minTreeAltitude = 10; - int32_t maxTreeAltitude = 50; - bool beaches = true; + // Features (e.g. tree, rivers, lakes etc.) + bool trees = true; + int32_t treeToLandRatio = 25; + int32_t minTreeAltitude = 10; + int32_t maxTreeAltitude = 50; + bool beaches = true; - // Simplex Noise Parameters - int32_t simplex_base_freq = 175; - int32_t simplex_octaves = 6; + // Simplex Noise Parameters + int32_t simplex_base_freq = 175; + int32_t simplex_octaves = 6; - // Height map settings - bool smooth_height_map = true; - uint32_t smooth_strength = 1; - bool normalize_height = true; -}; + // Height map settings + bool smooth_height_map = true; + uint32_t smooth_strength = 1; + bool normalize_height = true; + }; -void MapGenGenerate(MapGenSettings* settings); + void generate(Settings* settings); +} // namespace OpenRCT2::World::MapGenerator diff --git a/src/openrct2/world/map_generator/MapHelpers.cpp b/src/openrct2/world/map_generator/MapHelpers.cpp index 94efbe03b2..f106418bce 100644 --- a/src/openrct2/world/map_generator/MapHelpers.cpp +++ b/src/openrct2/world/map_generator/MapHelpers.cpp @@ -15,297 +15,305 @@ #include -static uint8_t GetBaseHeightOrZero(int32_t x, int32_t y) +namespace OpenRCT2::World::MapGenerator { - auto surfaceElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y }); - return surfaceElement != nullptr ? surfaceElement->BaseHeight : 0; -} - -/** - * Not perfect, this still leaves some particular tiles unsmoothed. - */ -int32_t MapSmooth(int32_t l, int32_t t, int32_t r, int32_t b) -{ - int32_t x, y, count, doubleCorner, raisedLand = 0; - uint8_t highest, cornerHeights[4]; - for (y = t; y < b; y++) + static uint8_t GetBaseHeightOrZero(int32_t x, int32_t y) { - for (x = l; x < r; x++) + auto surfaceElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y }); + return surfaceElement != nullptr ? surfaceElement->BaseHeight : 0; + } + + /** + * Not perfect, this still leaves some particular tiles unsmoothed. + */ + int32_t MapSmooth(int32_t l, int32_t t, int32_t r, int32_t b) + { + int32_t x, y, count, doubleCorner, raisedLand = 0; + uint8_t highest, cornerHeights[4]; + for (y = t; y < b; y++) { - auto surfaceElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y }); - if (surfaceElement == nullptr) - continue; - surfaceElement->SetSlope(kTileSlopeFlat); - - // Raise to edge height - 2 - highest = surfaceElement->BaseHeight; - highest = std::max(highest, GetBaseHeightOrZero(x - 1, y + 0)); - highest = std::max(highest, GetBaseHeightOrZero(x + 1, y + 0)); - highest = std::max(highest, GetBaseHeightOrZero(x + 0, y - 1)); - highest = std::max(highest, GetBaseHeightOrZero(x + 0, y + 1)); - if (surfaceElement->BaseHeight < highest - 2) + for (x = l; x < r; x++) { - raisedLand = 1; - surfaceElement->BaseHeight = surfaceElement->ClearanceHeight = highest - 2; - } + auto surfaceElement = MapGetSurfaceElementAt(TileCoordsXY{ x, y }); + if (surfaceElement == nullptr) + continue; + surfaceElement->SetSlope(kTileSlopeFlat); - // Check corners - doubleCorner = -1; - cornerHeights[0] = GetBaseHeightOrZero(x - 1, y - 1); - cornerHeights[1] = GetBaseHeightOrZero(x + 1, y - 1); - cornerHeights[2] = GetBaseHeightOrZero(x + 1, y + 1); - cornerHeights[3] = GetBaseHeightOrZero(x - 1, y + 1); - highest = surfaceElement->BaseHeight; - for (std::size_t i = 0; i < std::size(cornerHeights); i++) - highest = std::max(highest, cornerHeights[i]); - - if (highest >= surfaceElement->BaseHeight + 4) - { - count = 0; - int32_t canCompensate = 1; - for (std::size_t i = 0; i < std::size(cornerHeights); i++) - if (cornerHeights[i] == highest) - { - count++; - - // Check if surrounding corners aren't too high. The current tile - // can't compensate for all the height differences anymore if it has - // the extra height slope. - int32_t highestOnLowestSide; - switch (i) - { - default: - case 0: - highestOnLowestSide = std::max(GetBaseHeightOrZero(x + 1, y), GetBaseHeightOrZero(x, y + 1)); - break; - case 1: - highestOnLowestSide = std::max(GetBaseHeightOrZero(x - 1, y), GetBaseHeightOrZero(x, y + 1)); - break; - case 2: - highestOnLowestSide = std::max(GetBaseHeightOrZero(x - 1, y), GetBaseHeightOrZero(x, y - 1)); - break; - case 3: - highestOnLowestSide = std::max(GetBaseHeightOrZero(x + 1, y), GetBaseHeightOrZero(x, y - 1)); - break; - } - - if (highestOnLowestSide > surfaceElement->BaseHeight) - { - surfaceElement->BaseHeight = surfaceElement->ClearanceHeight = highestOnLowestSide; - raisedLand = 1; - canCompensate = 0; - } - } - - if (count == 1 && canCompensate) + // Raise to edge height - 2 + highest = surfaceElement->BaseHeight; + highest = std::max(highest, GetBaseHeightOrZero(x - 1, y + 0)); + highest = std::max(highest, GetBaseHeightOrZero(x + 1, y + 0)); + highest = std::max(highest, GetBaseHeightOrZero(x + 0, y - 1)); + highest = std::max(highest, GetBaseHeightOrZero(x + 0, y + 1)); + if (surfaceElement->BaseHeight < highest - 2) { - if (surfaceElement->BaseHeight < highest - 4) + raisedLand = 1; + surfaceElement->BaseHeight = surfaceElement->ClearanceHeight = highest - 2; + } + + // Check corners + doubleCorner = -1; + cornerHeights[0] = GetBaseHeightOrZero(x - 1, y - 1); + cornerHeights[1] = GetBaseHeightOrZero(x + 1, y - 1); + cornerHeights[2] = GetBaseHeightOrZero(x + 1, y + 1); + cornerHeights[3] = GetBaseHeightOrZero(x - 1, y + 1); + highest = surfaceElement->BaseHeight; + for (std::size_t i = 0; i < std::size(cornerHeights); i++) + highest = std::max(highest, cornerHeights[i]); + + if (highest >= surfaceElement->BaseHeight + 4) + { + count = 0; + int32_t canCompensate = 1; + for (std::size_t i = 0; i < std::size(cornerHeights); i++) + if (cornerHeights[i] == highest) + { + count++; + + // Check if surrounding corners aren't too high. The current tile + // can't compensate for all the height differences anymore if it has + // the extra height slope. + int32_t highestOnLowestSide; + switch (i) + { + default: + case 0: + highestOnLowestSide = std::max( + GetBaseHeightOrZero(x + 1, y), GetBaseHeightOrZero(x, y + 1)); + break; + case 1: + highestOnLowestSide = std::max( + GetBaseHeightOrZero(x - 1, y), GetBaseHeightOrZero(x, y + 1)); + break; + case 2: + highestOnLowestSide = std::max( + GetBaseHeightOrZero(x - 1, y), GetBaseHeightOrZero(x, y - 1)); + break; + case 3: + highestOnLowestSide = std::max( + GetBaseHeightOrZero(x + 1, y), GetBaseHeightOrZero(x, y - 1)); + break; + } + + if (highestOnLowestSide > surfaceElement->BaseHeight) + { + surfaceElement->BaseHeight = surfaceElement->ClearanceHeight = highestOnLowestSide; + raisedLand = 1; + canCompensate = 0; + } + } + + if (count == 1 && canCompensate) { - surfaceElement->BaseHeight = surfaceElement->ClearanceHeight = highest - 4; - raisedLand = 1; + if (surfaceElement->BaseHeight < highest - 4) + { + surfaceElement->BaseHeight = surfaceElement->ClearanceHeight = highest - 4; + raisedLand = 1; + } + if (cornerHeights[0] == highest && cornerHeights[2] <= cornerHeights[0] - 4) + doubleCorner = 0; + else if (cornerHeights[1] == highest && cornerHeights[3] <= cornerHeights[1] - 4) + doubleCorner = 1; + else if (cornerHeights[2] == highest && cornerHeights[0] <= cornerHeights[2] - 4) + doubleCorner = 2; + else if (cornerHeights[3] == highest && cornerHeights[1] <= cornerHeights[3] - 4) + doubleCorner = 3; } - if (cornerHeights[0] == highest && cornerHeights[2] <= cornerHeights[0] - 4) - doubleCorner = 0; - else if (cornerHeights[1] == highest && cornerHeights[3] <= cornerHeights[1] - 4) - doubleCorner = 1; - else if (cornerHeights[2] == highest && cornerHeights[0] <= cornerHeights[2] - 4) - doubleCorner = 2; - else if (cornerHeights[3] == highest && cornerHeights[1] <= cornerHeights[3] - 4) - doubleCorner = 3; + else + { + if (surfaceElement->BaseHeight < highest - 2) + { + surfaceElement->BaseHeight = surfaceElement->ClearanceHeight = highest - 2; + raisedLand = 1; + } + } + } + + if (doubleCorner != -1) + { + uint8_t slope = surfaceElement->GetSlope() | kTileSlopeDiagonalFlag; + switch (doubleCorner) + { + case 0: + slope |= kTileSlopeNCornerDown; + break; + case 1: + slope |= kTileSlopeWCornerDown; + break; + case 2: + slope |= kTileSlopeSCornerDown; + break; + case 3: + slope |= kTileSlopeECornerDown; + break; + } + surfaceElement->SetSlope(slope); } else { - if (surfaceElement->BaseHeight < highest - 2) + uint8_t slope = surfaceElement->GetSlope(); + // Corners + auto surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x + 1, y + 1 }); + if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight) + slope |= kTileSlopeNCornerUp; + + surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x - 1, y + 1 }); + if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight) + slope |= kTileSlopeWCornerUp; + + surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x + 1, y - 1 }); + if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight) + slope |= kTileSlopeECornerUp; + + surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x - 1, y - 1 }); + if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight) + slope |= kTileSlopeSCornerUp; + + // Sides + surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x + 1, y + 0 }); + if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight) + slope |= kTileSlopeNESideUp; + + surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x - 1, y + 0 }); + if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight) + slope |= kTileSlopeSWSideUp; + + surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x + 0, y - 1 }); + if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight) + slope |= kTileSlopeSESideUp; + + surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x + 0, y + 1 }); + if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight) + slope |= kTileSlopeNWSideUp; + + // Raise + if (slope == kTileSlopeRaisedCornersMask) { - surfaceElement->BaseHeight = surfaceElement->ClearanceHeight = highest - 2; - raisedLand = 1; + slope = kTileSlopeFlat; + surfaceElement->BaseHeight = surfaceElement->ClearanceHeight += 2; } + surfaceElement->SetSlope(slope); } } + } - if (doubleCorner != -1) + return raisedLand; + } + + /** + * Raises the corners based on the height offset of neighbour tiles. + * This does not change the base height, unless all corners have been raised. + * @returns 0 if no edits were made, 1 otherwise + */ + int32_t TileSmooth(const TileCoordsXY& tileCoords) + { + auto* const surfaceElement = MapGetSurfaceElementAt(tileCoords); + if (surfaceElement == nullptr) + return 0; + + // +-----+-----+-----+ + // | W | NW | N | + // | 2 | 1 | 0 | + // +-----+-----+-----+ + // | SW | _ | NE | + // | 4 | | 3 | + // +-----+-----+-----+ + // | S | SE | E | + // | 7 | 6 | 5 | + // +-----+-----+-----+ + + union + { + int32_t baseheight[8]; + struct { - uint8_t slope = surfaceElement->GetSlope() | kTileSlopeDiagonalFlag; - switch (doubleCorner) - { - case 0: - slope |= kTileSlopeNCornerDown; - break; - case 1: - slope |= kTileSlopeWCornerDown; - break; - case 2: - slope |= kTileSlopeSCornerDown; - break; - case 3: - slope |= kTileSlopeECornerDown; - break; - } - surfaceElement->SetSlope(slope); - } - else + int32_t N; + int32_t NW; + int32_t W; + int32_t NE; + int32_t SW; + int32_t E; + int32_t SE; + int32_t S; + }; + } neighbourHeightOffset = {}; + + // Find the neighbour base heights + for (int32_t index = 0, y_offset = -1; y_offset <= 1; y_offset++) + { + for (int32_t x_offset = -1; x_offset <= 1; x_offset++) { - uint8_t slope = surfaceElement->GetSlope(); - // Corners - auto surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x + 1, y + 1 }); - if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight) - slope |= kTileSlopeNCornerUp; + // Skip self + if (y_offset == 0 && x_offset == 0) + continue; - surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x - 1, y + 1 }); - if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight) - slope |= kTileSlopeWCornerUp; + // Get neighbour height. If the element is not valid (outside of map) assume the same height + auto* neighbourSurfaceElement = MapGetSurfaceElementAt(tileCoords + TileCoordsXY{ x_offset, y_offset }); + neighbourHeightOffset.baseheight[index] = neighbourSurfaceElement != nullptr + ? neighbourSurfaceElement->BaseHeight + : surfaceElement->BaseHeight; - surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x + 1, y - 1 }); - if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight) - slope |= kTileSlopeECornerUp; + // Make the height relative to the current surface element + neighbourHeightOffset.baseheight[index] -= surfaceElement->BaseHeight; - surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x - 1, y - 1 }); - if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight) - slope |= kTileSlopeSCornerUp; - - // Sides - surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x + 1, y + 0 }); - if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight) - slope |= kTileSlopeNESideUp; - - surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x - 1, y + 0 }); - if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight) - slope |= kTileSlopeSWSideUp; - - surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x + 0, y - 1 }); - if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight) - slope |= kTileSlopeSESideUp; - - surfaceElement2 = MapGetSurfaceElementAt(TileCoordsXY{ x + 0, y + 1 }); - if (surfaceElement2 != nullptr && surfaceElement2->BaseHeight > surfaceElement->BaseHeight) - slope |= kTileSlopeNWSideUp; - - // Raise - if (slope == kTileSlopeRaisedCornersMask) - { - slope = kTileSlopeFlat; - surfaceElement->BaseHeight = surfaceElement->ClearanceHeight += 2; - } - surfaceElement->SetSlope(slope); + index++; } } - } - return raisedLand; -} + // Count number from the three tiles that is currently higher + int8_t thresholdW = std::clamp(neighbourHeightOffset.SW, 0, 1) + std::clamp(neighbourHeightOffset.W, 0, 1) + + std::clamp(neighbourHeightOffset.NW, 0, 1); + int8_t thresholdN = std::clamp(neighbourHeightOffset.NW, 0, 1) + std::clamp(neighbourHeightOffset.N, 0, 1) + + std::clamp(neighbourHeightOffset.NE, 0, 1); + int8_t thresholdE = std::clamp(neighbourHeightOffset.NE, 0, 1) + std::clamp(neighbourHeightOffset.E, 0, 1) + + std::clamp(neighbourHeightOffset.SE, 0, 1); + int8_t thresholdS = std::clamp(neighbourHeightOffset.SE, 0, 1) + std::clamp(neighbourHeightOffset.S, 0, 1) + + std::clamp(neighbourHeightOffset.SW, 0, 1); -/** - * Raises the corners based on the height offset of neighbour tiles. - * This does not change the base height, unless all corners have been raised. - * @returns 0 if no edits were made, 1 otherwise - */ -int32_t TileSmooth(const TileCoordsXY& tileCoords) -{ - auto* const surfaceElement = MapGetSurfaceElementAt(tileCoords); - if (surfaceElement == nullptr) - return 0; + uint8_t slope = kTileSlopeFlat; + slope |= (thresholdW >= 1) ? SLOPE_W_THRESHOLD_FLAGS : 0; + slope |= (thresholdN >= 1) ? SLOPE_N_THRESHOLD_FLAGS : 0; + slope |= (thresholdE >= 1) ? SLOPE_E_THRESHOLD_FLAGS : 0; + slope |= (thresholdS >= 1) ? SLOPE_S_THRESHOLD_FLAGS : 0; - // +-----+-----+-----+ - // | W | NW | N | - // | 2 | 1 | 0 | - // +-----+-----+-----+ - // | SW | _ | NE | - // | 4 | | 3 | - // +-----+-----+-----+ - // | S | SE | E | - // | 7 | 6 | 5 | - // +-----+-----+-----+ - - union - { - int32_t baseheight[8]; - struct + // Set diagonal when three corners (one corner down) have been raised, and the middle one can be raised one more + if ((slope == kTileSlopeWCornerDown && neighbourHeightOffset.W >= 4) + || (slope == kTileSlopeSCornerDown && neighbourHeightOffset.S >= 4) + || (slope == kTileSlopeECornerDown && neighbourHeightOffset.E >= 4) + || (slope == kTileSlopeNCornerDown && neighbourHeightOffset.N >= 4)) { - int32_t N; - int32_t NW; - int32_t W; - int32_t NE; - int32_t SW; - int32_t E; - int32_t SE; - int32_t S; - }; - } neighbourHeightOffset = {}; - - // Find the neighbour base heights - for (int32_t index = 0, y_offset = -1; y_offset <= 1; y_offset++) - { - for (int32_t x_offset = -1; x_offset <= 1; x_offset++) - { - // Skip self - if (y_offset == 0 && x_offset == 0) - continue; - - // Get neighbour height. If the element is not valid (outside of map) assume the same height - auto* neighbourSurfaceElement = MapGetSurfaceElementAt(tileCoords + TileCoordsXY{ x_offset, y_offset }); - neighbourHeightOffset.baseheight[index] = neighbourSurfaceElement != nullptr ? neighbourSurfaceElement->BaseHeight - : surfaceElement->BaseHeight; - - // Make the height relative to the current surface element - neighbourHeightOffset.baseheight[index] -= surfaceElement->BaseHeight; - - index++; + slope |= kTileSlopeDiagonalFlag; } - } - // Count number from the three tiles that is currently higher - int8_t thresholdW = std::clamp(neighbourHeightOffset.SW, 0, 1) + std::clamp(neighbourHeightOffset.W, 0, 1) - + std::clamp(neighbourHeightOffset.NW, 0, 1); - int8_t thresholdN = std::clamp(neighbourHeightOffset.NW, 0, 1) + std::clamp(neighbourHeightOffset.N, 0, 1) - + std::clamp(neighbourHeightOffset.NE, 0, 1); - int8_t thresholdE = std::clamp(neighbourHeightOffset.NE, 0, 1) + std::clamp(neighbourHeightOffset.E, 0, 1) - + std::clamp(neighbourHeightOffset.SE, 0, 1); - int8_t thresholdS = std::clamp(neighbourHeightOffset.SE, 0, 1) + std::clamp(neighbourHeightOffset.S, 0, 1) - + std::clamp(neighbourHeightOffset.SW, 0, 1); - - uint8_t slope = kTileSlopeFlat; - slope |= (thresholdW >= 1) ? SLOPE_W_THRESHOLD_FLAGS : 0; - slope |= (thresholdN >= 1) ? SLOPE_N_THRESHOLD_FLAGS : 0; - slope |= (thresholdE >= 1) ? SLOPE_E_THRESHOLD_FLAGS : 0; - slope |= (thresholdS >= 1) ? SLOPE_S_THRESHOLD_FLAGS : 0; - - // Set diagonal when three corners (one corner down) have been raised, and the middle one can be raised one more - if ((slope == kTileSlopeWCornerDown && neighbourHeightOffset.W >= 4) - || (slope == kTileSlopeSCornerDown && neighbourHeightOffset.S >= 4) - || (slope == kTileSlopeECornerDown && neighbourHeightOffset.E >= 4) - || (slope == kTileSlopeNCornerDown && neighbourHeightOffset.N >= 4)) - { - slope |= kTileSlopeDiagonalFlag; - } - - // Check if the calculated slope is the same already - uint8_t currentSlope = surfaceElement->GetSlope(); - if (currentSlope == slope) - { - return 0; - } - - if ((slope & kTileSlopeRaisedCornersMask) == kTileSlopeRaisedCornersMask) - { - // All corners are raised, raise the entire tile instead. - surfaceElement->SetSlope(kTileSlopeFlat); - surfaceElement->BaseHeight = (surfaceElement->ClearanceHeight += 2); - if (surfaceElement->GetWaterHeight() <= surfaceElement->GetBaseZ()) + // Check if the calculated slope is the same already + uint8_t currentSlope = surfaceElement->GetSlope(); + if (currentSlope == slope) { - surfaceElement->SetWaterHeight(0); + return 0; } - } - else - { - // Apply the slope to this tile - surfaceElement->SetSlope(slope); - // Set correct clearance height - if (slope & kTileSlopeDiagonalFlag) - surfaceElement->ClearanceHeight = surfaceElement->BaseHeight + 4; - else if (slope & kTileSlopeRaisedCornersMask) - surfaceElement->ClearanceHeight = surfaceElement->BaseHeight + 2; - } + if ((slope & kTileSlopeRaisedCornersMask) == kTileSlopeRaisedCornersMask) + { + // All corners are raised, raise the entire tile instead. + surfaceElement->SetSlope(kTileSlopeFlat); + surfaceElement->BaseHeight = (surfaceElement->ClearanceHeight += 2); + if (surfaceElement->GetWaterHeight() <= surfaceElement->GetBaseZ()) + { + surfaceElement->SetWaterHeight(0); + } + } + else + { + // Apply the slope to this tile + surfaceElement->SetSlope(slope); - return 1; -} + // Set correct clearance height + if (slope & kTileSlopeDiagonalFlag) + surfaceElement->ClearanceHeight = surfaceElement->BaseHeight + 4; + else if (slope & kTileSlopeRaisedCornersMask) + surfaceElement->ClearanceHeight = surfaceElement->BaseHeight + 2; + } + + return 1; + } +} // namespace OpenRCT2::World::MapGenerator diff --git a/src/openrct2/world/map_generator/MapHelpers.h b/src/openrct2/world/map_generator/MapHelpers.h index 8b2e50a3e9..d01f060412 100644 --- a/src/openrct2/world/map_generator/MapHelpers.h +++ b/src/openrct2/world/map_generator/MapHelpers.h @@ -11,13 +11,16 @@ #include "../Location.hpp" -enum +namespace OpenRCT2::World::MapGenerator { - SLOPE_S_THRESHOLD_FLAGS = (1 << 0), - SLOPE_W_THRESHOLD_FLAGS = (1 << 1), - SLOPE_N_THRESHOLD_FLAGS = (1 << 2), - SLOPE_E_THRESHOLD_FLAGS = (1 << 3) -}; + enum + { + SLOPE_S_THRESHOLD_FLAGS = (1 << 0), + SLOPE_W_THRESHOLD_FLAGS = (1 << 1), + SLOPE_N_THRESHOLD_FLAGS = (1 << 2), + SLOPE_E_THRESHOLD_FLAGS = (1 << 3) + }; -int32_t MapSmooth(int32_t l, int32_t t, int32_t r, int32_t b); -int32_t TileSmooth(const TileCoordsXY& tileCoords); + int32_t MapSmooth(int32_t l, int32_t t, int32_t r, int32_t b); + int32_t TileSmooth(const TileCoordsXY& tileCoords); +} // namespace OpenRCT2::World::MapGenerator diff --git a/src/openrct2/world/map_generator/PngTerrainGenerator.cpp b/src/openrct2/world/map_generator/PngTerrainGenerator.cpp index 763e459094..5a0dd90fa6 100644 --- a/src/openrct2/world/map_generator/PngTerrainGenerator.cpp +++ b/src/openrct2/world/map_generator/PngTerrainGenerator.cpp @@ -22,245 +22,242 @@ #include -using namespace OpenRCT2; - -#pragma region Height map struct - -static struct +namespace OpenRCT2::World::MapGenerator { - uint32_t width = 0; - uint32_t height = 0; - std::vector mono_bitmap; -} _heightMapData; - -#pragma endregion Height map struct - -/** - * Return the tile coordinate that matches the given pixel of a heightmap - */ -static TileCoordsXY MapgenHeightmapCoordToTileCoordsXY(uint32_t x, uint32_t y) -{ - // The height map does not include the empty tiles around the map, so we add 1. - return TileCoordsXY(static_cast(y + 1), static_cast(x + 1)); -} - -bool MapGenLoadHeightmapImage(const utf8* path) -{ - auto format = Imaging::GetImageFormatFromPath(path); - if (format == IMAGE_FORMAT::PNG) + static struct { - // Promote to 32-bit - format = IMAGE_FORMAT::PNG_32; + uint32_t width = 0; + uint32_t height = 0; + std::vector mono_bitmap; + } _heightMapData; + + /** + * Return the tile coordinate that matches the given pixel of a heightmap + */ + static TileCoordsXY HeightmapCoordToTileCoordsXY(uint32_t x, uint32_t y) + { + // The height map does not include the empty tiles around the map, so we add 1. + return TileCoordsXY(static_cast(y + 1), static_cast(x + 1)); } - try + bool LoadHeightmapImage(const utf8* path) { - auto image = Imaging::ReadFromFile(path, format); - auto width = std::min(image.Width, kMaximumMapSizePractical); - auto height = std::min(image.Height, kMaximumMapSizePractical); - if (width != image.Width || height != image.Height) + auto format = Imaging::GetImageFormatFromPath(path); + if (format == IMAGE_FORMAT::PNG) { - ContextShowError(STR_HEIGHT_MAP_ERROR, STR_ERROR_HEIGHT_MAP_TOO_BIG, {}); + // Promote to 32-bit + format = IMAGE_FORMAT::PNG_32; } - // Allocate memory for the height map values, one byte pixel - _heightMapData.mono_bitmap.resize(width * height); - _heightMapData.width = width; - _heightMapData.height = height; - - // Copy average RGB value to mono bitmap - constexpr auto numChannels = 4; - const auto pitch = image.Stride; - const auto pixels = image.Pixels.data(); - for (uint32_t x = 0; x < _heightMapData.width; x++) + try { - for (uint32_t y = 0; y < _heightMapData.height; y++) + auto image = Imaging::ReadFromFile(path, format); + auto width = std::min(image.Width, kMaximumMapSizePractical); + auto height = std::min(image.Height, kMaximumMapSizePractical); + if (width != image.Width || height != image.Height) { - const auto red = pixels[x * numChannels + y * pitch]; - const auto green = pixels[x * numChannels + y * pitch + 1]; - const auto blue = pixels[x * numChannels + y * pitch + 2]; - _heightMapData.mono_bitmap[x + y * _heightMapData.width] = (red + green + blue) / 3; + ContextShowError(STR_HEIGHT_MAP_ERROR, STR_ERROR_HEIGHT_MAP_TOO_BIG, {}); } - } - return true; - } - catch (const std::exception& e) - { - switch (format) - { - case IMAGE_FORMAT::BITMAP: - ContextShowError(STR_HEIGHT_MAP_ERROR, STR_ERROR_READING_BITMAP, {}); - break; - case IMAGE_FORMAT::PNG_32: - ContextShowError(STR_HEIGHT_MAP_ERROR, STR_ERROR_READING_PNG, {}); - break; - default: - LOG_ERROR("Unable to load height map image: %s", e.what()); - break; - } - return false; - } -} -/** - * Frees the memory used to store the selected height map - */ -void MapGenUnloadHeightmapImage() -{ - _heightMapData.mono_bitmap.clear(); - _heightMapData.width = 0; - _heightMapData.height = 0; -} + // Allocate memory for the height map values, one byte pixel + _heightMapData.mono_bitmap.resize(width * height); + _heightMapData.width = width; + _heightMapData.height = height; -/** - * Applies box blur to the surface N times - */ -static void MapGenSmoothHeightmap(std::vector& src, int32_t strength) -{ - // Create buffer to store one channel - std::vector dest(src.size()); - - for (int32_t i = 0; i < strength; i++) - { - // Calculate box blur value to all pixels of the surface - for (uint32_t y = 0; y < _heightMapData.height; y++) - { + // Copy average RGB value to mono bitmap + constexpr auto numChannels = 4; + const auto pitch = image.Stride; + const auto pixels = image.Pixels.data(); for (uint32_t x = 0; x < _heightMapData.width; x++) { - uint32_t heightSum = 0; - - // Loop over neighbour pixels, all of them have the same weight - for (int8_t offsetX = -1; offsetX <= 1; offsetX++) + for (uint32_t y = 0; y < _heightMapData.height; y++) { - for (int8_t offsetY = -1; offsetY <= 1; offsetY++) - { - // Clamp x and y so they stay within the image - // This assumes the height map is not tiled, and increases the weight of the edges - const int32_t readX = std::clamp(x + offsetX, 0, _heightMapData.width - 1); - const int32_t readY = std::clamp(y + offsetY, 0, _heightMapData.height - 1); - heightSum += src[readX + readY * _heightMapData.width]; - } + const auto red = pixels[x * numChannels + y * pitch]; + const auto green = pixels[x * numChannels + y * pitch + 1]; + const auto blue = pixels[x * numChannels + y * pitch + 2]; + _heightMapData.mono_bitmap[x + y * _heightMapData.width] = (red + green + blue) / 3; } - - // Take average - dest[x + y * _heightMapData.width] = heightSum / 9; } + return true; } - - // Now apply the blur to the source pixels - for (uint32_t y = 0; y < _heightMapData.height; y++) + catch (const std::exception& e) { - for (uint32_t x = 0; x < _heightMapData.width; x++) + switch (format) { - src[x + y * _heightMapData.width] = dest[x + y * _heightMapData.width]; + case IMAGE_FORMAT::BITMAP: + ContextShowError(STR_HEIGHT_MAP_ERROR, STR_ERROR_READING_BITMAP, {}); + break; + case IMAGE_FORMAT::PNG_32: + ContextShowError(STR_HEIGHT_MAP_ERROR, STR_ERROR_READING_PNG, {}); + break; + default: + LOG_ERROR("Unable to load height map image: %s", e.what()); + break; } - } - } -} - -void MapGenGenerateFromHeightmapImage(MapGenSettings* settings) -{ - Guard::Assert(!_heightMapData.mono_bitmap.empty(), "No height map loaded"); - Guard::Assert(settings->heightmapHigh != settings->heightmapLow, "Low and high setting cannot be the same"); - - // Make a copy of the original height map that we can edit - auto dest = _heightMapData.mono_bitmap; - - // Get technical map size, +2 for the black tiles around the map - auto maxWidth = static_cast(_heightMapData.width + 2); - auto maxHeight = static_cast(_heightMapData.height + 2); - MapInit({ maxHeight, maxWidth }); - - if (settings->smooth_height_map) - { - MapGenSmoothHeightmap(dest, settings->smooth_strength); - } - - uint8_t maxValue = 255; - uint8_t minValue = 0; - - if (settings->normalize_height) - { - // Get highest and lowest pixel value - maxValue = 0; - minValue = 0xff; - for (uint32_t y = 0; y < _heightMapData.height; y++) - { - for (uint32_t x = 0; x < _heightMapData.width; x++) - { - uint8_t value = dest[x + y * _heightMapData.width]; - maxValue = std::max(maxValue, value); - minValue = std::min(minValue, value); - } - } - - if (minValue == maxValue) - { - ContextShowError(STR_HEIGHT_MAP_ERROR, STR_ERROR_CANNOT_NORMALIZE, {}); - return; + return false; } } - Guard::Assert(maxValue > minValue, "Input range is invalid"); - Guard::Assert(settings->heightmapHigh > settings->heightmapLow, "Output range is invalid"); - - const auto surfaceTextureId = MapGenSurfaceTextureId(settings); - const auto edgeTextureId = MapGenEdgeTextureId(settings, surfaceTextureId); - - const uint8_t rangeIn = maxValue - minValue; - const uint8_t rangeOut = (settings->heightmapHigh - settings->heightmapLow) * 2; - - for (uint32_t y = 0; y < _heightMapData.height; y++) + /** + * Frees the memory used to store the selected height map + */ + void UnloadHeightmapImage() { - for (uint32_t x = 0; x < _heightMapData.width; x++) - { - // The x and y axis are flipped in the world, so this uses y for x and x for y. - auto tileCoords = MapgenHeightmapCoordToTileCoordsXY(x, y); - auto* const surfaceElement = MapGetSurfaceElementAt(tileCoords); - if (surfaceElement == nullptr) - continue; - - // Read value from bitmap, and convert its range - uint8_t value = dest[x + y * _heightMapData.width]; - value = static_cast(static_cast(value - minValue) / rangeIn * rangeOut) - + (settings->heightmapLow * 2); - surfaceElement->BaseHeight = value; - - // Floor to even number - surfaceElement->BaseHeight /= 2; - surfaceElement->BaseHeight *= 2; - surfaceElement->ClearanceHeight = surfaceElement->BaseHeight; - - // Set textures - surfaceElement->SetSurfaceObjectIndex(surfaceTextureId); - surfaceElement->SetEdgeObjectIndex(edgeTextureId); - - // Set water level - if (surfaceElement->BaseHeight < settings->waterLevel) - { - surfaceElement->SetWaterHeight(settings->waterLevel * kCoordsZStep); - } - } + _heightMapData.mono_bitmap.clear(); + _heightMapData.width = 0; + _heightMapData.height = 0; } - // Smooth tile edges - if (settings->smoothTileEdges) + /** + * Applies box blur to the surface N times + */ + static void SmoothHeightmap(std::vector& src, int32_t strength) { - // Keep smoothing the entire map until no tiles are changed anymore - while (true) + // Create buffer to store one channel + std::vector dest(src.size()); + + for (int32_t i = 0; i < strength; i++) { - uint32_t numTilesChanged = 0; + // Calculate box blur value to all pixels of the surface for (uint32_t y = 0; y < _heightMapData.height; y++) { for (uint32_t x = 0; x < _heightMapData.width; x++) { - auto tileCoords = MapgenHeightmapCoordToTileCoordsXY(x, y); - numTilesChanged += TileSmooth(tileCoords); + uint32_t heightSum = 0; + + // Loop over neighbour pixels, all of them have the same weight + for (int8_t offsetX = -1; offsetX <= 1; offsetX++) + { + for (int8_t offsetY = -1; offsetY <= 1; offsetY++) + { + // Clamp x and y so they stay within the image + // This assumes the height map is not tiled, and increases the weight of the edges + const int32_t readX = std::clamp(x + offsetX, 0, _heightMapData.width - 1); + const int32_t readY = std::clamp(y + offsetY, 0, _heightMapData.height - 1); + heightSum += src[readX + readY * _heightMapData.width]; + } + } + + // Take average + dest[x + y * _heightMapData.width] = heightSum / 9; } } - if (numTilesChanged == 0) - break; + // Now apply the blur to the source pixels + for (uint32_t y = 0; y < _heightMapData.height; y++) + { + for (uint32_t x = 0; x < _heightMapData.width; x++) + { + src[x + y * _heightMapData.width] = dest[x + y * _heightMapData.width]; + } + } } } -} + + void GenerateFromHeightmapImage(Settings* settings) + { + Guard::Assert(!_heightMapData.mono_bitmap.empty(), "No height map loaded"); + Guard::Assert(settings->heightmapHigh != settings->heightmapLow, "Low and high setting cannot be the same"); + + // Make a copy of the original height map that we can edit + auto dest = _heightMapData.mono_bitmap; + + // Get technical map size, +2 for the black tiles around the map + auto maxWidth = static_cast(_heightMapData.width + 2); + auto maxHeight = static_cast(_heightMapData.height + 2); + MapInit({ maxHeight, maxWidth }); + + if (settings->smooth_height_map) + { + SmoothHeightmap(dest, settings->smooth_strength); + } + + uint8_t maxValue = 255; + uint8_t minValue = 0; + + if (settings->normalize_height) + { + // Get highest and lowest pixel value + maxValue = 0; + minValue = 0xff; + for (uint32_t y = 0; y < _heightMapData.height; y++) + { + for (uint32_t x = 0; x < _heightMapData.width; x++) + { + uint8_t value = dest[x + y * _heightMapData.width]; + maxValue = std::max(maxValue, value); + minValue = std::min(minValue, value); + } + } + + if (minValue == maxValue) + { + ContextShowError(STR_HEIGHT_MAP_ERROR, STR_ERROR_CANNOT_NORMALIZE, {}); + return; + } + } + + Guard::Assert(maxValue > minValue, "Input range is invalid"); + Guard::Assert(settings->heightmapHigh > settings->heightmapLow, "Output range is invalid"); + + const auto surfaceTextureId = generateSurfaceTextureId(settings); + const auto edgeTextureId = generateEdgeTextureId(settings, surfaceTextureId); + + const uint8_t rangeIn = maxValue - minValue; + const uint8_t rangeOut = (settings->heightmapHigh - settings->heightmapLow) * 2; + + for (uint32_t y = 0; y < _heightMapData.height; y++) + { + for (uint32_t x = 0; x < _heightMapData.width; x++) + { + // The x and y axis are flipped in the world, so this uses y for x and x for y. + auto tileCoords = HeightmapCoordToTileCoordsXY(x, y); + auto* const surfaceElement = MapGetSurfaceElementAt(tileCoords); + if (surfaceElement == nullptr) + continue; + + // Read value from bitmap, and convert its range + uint8_t value = dest[x + y * _heightMapData.width]; + value = static_cast(static_cast(value - minValue) / rangeIn * rangeOut) + + (settings->heightmapLow * 2); + surfaceElement->BaseHeight = value; + + // Floor to even number + surfaceElement->BaseHeight /= 2; + surfaceElement->BaseHeight *= 2; + surfaceElement->ClearanceHeight = surfaceElement->BaseHeight; + + // Set textures + surfaceElement->SetSurfaceObjectIndex(surfaceTextureId); + surfaceElement->SetEdgeObjectIndex(edgeTextureId); + + // Set water level + if (surfaceElement->BaseHeight < settings->waterLevel) + { + surfaceElement->SetWaterHeight(settings->waterLevel * kCoordsZStep); + } + } + } + + // Smooth tile edges + if (settings->smoothTileEdges) + { + // Keep smoothing the entire map until no tiles are changed anymore + while (true) + { + uint32_t numTilesChanged = 0; + for (uint32_t y = 0; y < _heightMapData.height; y++) + { + for (uint32_t x = 0; x < _heightMapData.width; x++) + { + auto tileCoords = HeightmapCoordToTileCoordsXY(x, y); + numTilesChanged += TileSmooth(tileCoords); + } + } + + if (numTilesChanged == 0) + break; + } + } + } +} // namespace OpenRCT2::World::MapGenerator diff --git a/src/openrct2/world/map_generator/PngTerrainGenerator.h b/src/openrct2/world/map_generator/PngTerrainGenerator.h index c61f28648b..6563d2b9af 100644 --- a/src/openrct2/world/map_generator/PngTerrainGenerator.h +++ b/src/openrct2/world/map_generator/PngTerrainGenerator.h @@ -11,8 +11,11 @@ #include "../../core/StringTypes.h" -struct MapGenSettings; +namespace OpenRCT2::World::MapGenerator +{ + struct Settings; -bool MapGenLoadHeightmapImage(const utf8* path); -void MapGenUnloadHeightmapImage(); -void MapGenGenerateFromHeightmapImage(MapGenSettings* settings); + bool LoadHeightmapImage(const utf8* path); + void UnloadHeightmapImage(); + void GenerateFromHeightmapImage(Settings* settings); +} // namespace OpenRCT2::World::MapGenerator diff --git a/src/openrct2/world/map_generator/SimplexNoise.cpp b/src/openrct2/world/map_generator/SimplexNoise.cpp index b85edcaaf7..6ea77f2e46 100644 --- a/src/openrct2/world/map_generator/SimplexNoise.cpp +++ b/src/openrct2/world/map_generator/SimplexNoise.cpp @@ -12,135 +12,138 @@ #include "../../util/Util.h" #include "MapGen.h" -/** - * Simplex Noise Algorithm with Fractional Brownian Motion - * Based on: - * - https://code.google.com/p/simplexnoise/ - * - https://code.google.com/p/fractalterraingeneration/wiki/Fractional_Brownian_Motion - */ - -static float Generate(float x, float y); -static int32_t FastFloor(float x); -static float Grad(int32_t hash, float x, float y); - -static uint8_t perm[512]; - -void NoiseRand() +namespace OpenRCT2::World::MapGenerator { - for (auto& i : perm) + /** + * Simplex Noise Algorithm with Fractional Brownian Motion + * Based on: + * - https://code.google.com/p/simplexnoise/ + * - https://code.google.com/p/fractalterraingeneration/wiki/Fractional_Brownian_Motion + */ + + static float Generate(float x, float y); + static int32_t FastFloor(float x); + static float Grad(int32_t hash, float x, float y); + + static uint8_t perm[512]; + + void NoiseRand() { - i = UtilRand() & 0xFF; - } -} - -float FractalNoise(int32_t x, int32_t y, float frequency, int32_t octaves, float lacunarity, float persistence) -{ - float total = 0.0f; - float amplitude = persistence; - for (int32_t i = 0; i < octaves; i++) - { - total += Generate(x * frequency, y * frequency) * amplitude; - frequency *= lacunarity; - amplitude *= persistence; - } - return total; -} - -static float Generate(float x, float y) -{ - const float F2 = 0.366025403f; // F2 = 0.5*(sqrt(3.0)-1.0) - const float G2 = 0.211324865f; // G2 = (3.0-sqrt(3.0))/6.0 - - float n0, n1, n2; // Noise contributions from the three corners - - // Skew the input space to determine which simplex cell we're in - float s = (x + y) * F2; // Hairy factor for 2D - float xs = x + s; - float ys = y + s; - int32_t i = FastFloor(xs); - int32_t j = FastFloor(ys); - - float t = static_cast(i + j) * G2; - float X0 = i - t; // Unskew the cell origin back to (x,y) space - float Y0 = j - t; - float x0 = x - X0; // The x,y distances from the cell origin - float y0 = y - Y0; - - // For the 2D case, the simplex shape is an equilateral triangle. - // Determine which simplex we are in. - int32_t i1, j1; // Offsets for second (middle) corner of simplex in (i,j) coords - if (x0 > y0) - { - i1 = 1; - j1 = 0; - } // lower triangle, XY order: (0,0)->(1,0)->(1,1) - else - { - i1 = 0; - j1 = 1; - } // upper triangle, YX order: (0,0)->(0,1)->(1,1) - - // A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and - // a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where - // c = (3-sqrt(3))/6 - - float x1 = x0 - i1 + G2; // Offsets for middle corner in (x,y) unskewed coords - float y1 = y0 - j1 + G2; - float x2 = x0 - 1.0f + 2.0f * G2; // Offsets for last corner in (x,y) unskewed coords - float y2 = y0 - 1.0f + 2.0f * G2; - - // Wrap the integer indices at 256, to avoid indexing perm[] out of bounds - int32_t ii = i % 256; - int32_t jj = j % 256; - - // Calculate the contribution from the three corners - float t0 = 0.5f - x0 * x0 - y0 * y0; - if (t0 < 0.0f) - { - n0 = 0.0f; - } - else - { - t0 *= t0; - n0 = t0 * t0 * Grad(perm[ii + perm[jj]], x0, y0); + for (auto& i : perm) + { + i = UtilRand() & 0xFF; + } } - float t1 = 0.5f - x1 * x1 - y1 * y1; - if (t1 < 0.0f) + float FractalNoise(int32_t x, int32_t y, float frequency, int32_t octaves, float lacunarity, float persistence) { - n1 = 0.0f; - } - else - { - t1 *= t1; - n1 = t1 * t1 * Grad(perm[ii + i1 + perm[jj + j1]], x1, y1); + float total = 0.0f; + float amplitude = persistence; + for (int32_t i = 0; i < octaves; i++) + { + total += Generate(x * frequency, y * frequency) * amplitude; + frequency *= lacunarity; + amplitude *= persistence; + } + return total; } - float t2 = 0.5f - x2 * x2 - y2 * y2; - if (t2 < 0.0f) + static float Generate(float x, float y) { - n2 = 0.0f; - } - else - { - t2 *= t2; - n2 = t2 * t2 * Grad(perm[ii + 1 + perm[jj + 1]], x2, y2); + const float F2 = 0.366025403f; // F2 = 0.5*(sqrt(3.0)-1.0) + const float G2 = 0.211324865f; // G2 = (3.0-sqrt(3.0))/6.0 + + float n0, n1, n2; // Noise contributions from the three corners + + // Skew the input space to determine which simplex cell we're in + float s = (x + y) * F2; // Hairy factor for 2D + float xs = x + s; + float ys = y + s; + int32_t i = FastFloor(xs); + int32_t j = FastFloor(ys); + + float t = static_cast(i + j) * G2; + float X0 = i - t; // Unskew the cell origin back to (x,y) space + float Y0 = j - t; + float x0 = x - X0; // The x,y distances from the cell origin + float y0 = y - Y0; + + // For the 2D case, the simplex shape is an equilateral triangle. + // Determine which simplex we are in. + int32_t i1, j1; // Offsets for second (middle) corner of simplex in (i,j) coords + if (x0 > y0) + { + i1 = 1; + j1 = 0; + } // lower triangle, XY order: (0,0)->(1,0)->(1,1) + else + { + i1 = 0; + j1 = 1; + } // upper triangle, YX order: (0,0)->(0,1)->(1,1) + + // A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and + // a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where + // c = (3-sqrt(3))/6 + + float x1 = x0 - i1 + G2; // Offsets for middle corner in (x,y) unskewed coords + float y1 = y0 - j1 + G2; + float x2 = x0 - 1.0f + 2.0f * G2; // Offsets for last corner in (x,y) unskewed coords + float y2 = y0 - 1.0f + 2.0f * G2; + + // Wrap the integer indices at 256, to avoid indexing perm[] out of bounds + int32_t ii = i % 256; + int32_t jj = j % 256; + + // Calculate the contribution from the three corners + float t0 = 0.5f - x0 * x0 - y0 * y0; + if (t0 < 0.0f) + { + n0 = 0.0f; + } + else + { + t0 *= t0; + n0 = t0 * t0 * Grad(perm[ii + perm[jj]], x0, y0); + } + + float t1 = 0.5f - x1 * x1 - y1 * y1; + if (t1 < 0.0f) + { + n1 = 0.0f; + } + else + { + t1 *= t1; + n1 = t1 * t1 * Grad(perm[ii + i1 + perm[jj + j1]], x1, y1); + } + + float t2 = 0.5f - x2 * x2 - y2 * y2; + if (t2 < 0.0f) + { + n2 = 0.0f; + } + else + { + t2 *= t2; + n2 = t2 * t2 * Grad(perm[ii + 1 + perm[jj + 1]], x2, y2); + } + + // Add contributions from each corner to get the final noise value. + // The result is scaled to return values in the interval [-1,1]. + return 40.0f * (n0 + n1 + n2); // TODO: The scale factor is preliminary! } - // Add contributions from each corner to get the final noise value. - // The result is scaled to return values in the interval [-1,1]. - return 40.0f * (n0 + n1 + n2); // TODO: The scale factor is preliminary! -} + static int32_t FastFloor(float x) + { + return (x > 0) ? (static_cast(x)) : ((static_cast(x)) - 1); + } -static int32_t FastFloor(float x) -{ - return (x > 0) ? (static_cast(x)) : ((static_cast(x)) - 1); -} - -static float Grad(int32_t hash, float x, float y) -{ - int32_t h = hash & 7; // Convert low 3 bits of hash code - float u = h < 4 ? x : y; // into 8 simple gradient directions, - float v = h < 4 ? y : x; // and compute the dot product with (x,y). - return ((h & 1) != 0 ? -u : u) + ((h & 2) != 0 ? -2.0f * v : 2.0f * v); -} + static float Grad(int32_t hash, float x, float y) + { + int32_t h = hash & 7; // Convert low 3 bits of hash code + float u = h < 4 ? x : y; // into 8 simple gradient directions, + float v = h < 4 ? y : x; // and compute the dot product with (x,y). + return ((h & 1) != 0 ? -u : u) + ((h & 2) != 0 ? -2.0f * v : 2.0f * v); + } +} // namespace OpenRCT2::World::MapGenerator diff --git a/src/openrct2/world/map_generator/SimplexNoise.h b/src/openrct2/world/map_generator/SimplexNoise.h index a21a42762a..c46df9bd60 100644 --- a/src/openrct2/world/map_generator/SimplexNoise.h +++ b/src/openrct2/world/map_generator/SimplexNoise.h @@ -11,5 +11,8 @@ #include -void NoiseRand(); -float FractalNoise(int32_t x, int32_t y, float frequency, int32_t octaves, float lacunarity, float persistence); +namespace OpenRCT2::World::MapGenerator +{ + void NoiseRand(); + float FractalNoise(int32_t x, int32_t y, float frequency, int32_t octaves, float lacunarity, float persistence); +} // namespace OpenRCT2::World::MapGenerator diff --git a/src/openrct2/world/map_generator/SurfaceSelection.cpp b/src/openrct2/world/map_generator/SurfaceSelection.cpp index b6e4f59964..1a239849a1 100644 --- a/src/openrct2/world/map_generator/SurfaceSelection.cpp +++ b/src/openrct2/world/map_generator/SurfaceSelection.cpp @@ -20,82 +20,83 @@ #include -using namespace OpenRCT2; - -// Randomly chosen base terrains. We rarely want a whole map made out of chequerboard or rock. -static constexpr std::string_view kBaseTerrain[] = { - "rct2.terrain_surface.grass", "rct2.terrain_surface.sand", "rct2.terrain_surface.sand_brown", - "rct2.terrain_surface.dirt", "rct2.terrain_surface.ice", -}; - -ObjectEntryIndex MapGenSurfaceTextureId(MapGenSettings* settings) +namespace OpenRCT2::World::MapGenerator { - auto& objectManager = GetContext()->GetObjectManager(); + // Randomly chosen base terrains. We rarely want a whole map made out of chequerboard or rock. + static constexpr std::string_view kBaseTerrain[] = { + "rct2.terrain_surface.grass", "rct2.terrain_surface.sand", "rct2.terrain_surface.sand_brown", + "rct2.terrain_surface.dirt", "rct2.terrain_surface.ice", + }; - const auto selectedFloor = TerrainSurfaceObject::GetById(settings->landTexture); - std::string_view surfaceTexture = selectedFloor != nullptr ? selectedFloor->GetIdentifier() : ""; - - if (surfaceTexture.empty()) + ObjectEntryIndex generateSurfaceTextureId(Settings* settings) { - std::vector availableTerrains; - std::copy_if( - std::begin(kBaseTerrain), std::end(kBaseTerrain), std::back_inserter(availableTerrains), - [&](auto terrain) { return objectManager.GetLoadedObject(ObjectEntryDescriptor(terrain)) != nullptr; }); + auto& objectManager = GetContext()->GetObjectManager(); - if (availableTerrains.empty()) - // Fall back to the first available surface texture that is available in the park - surfaceTexture = TerrainSurfaceObject::GetById(0)->GetIdentifier(); - else - surfaceTexture = availableTerrains[UtilRand() % availableTerrains.size()]; + const auto selectedFloor = TerrainSurfaceObject::GetById(settings->landTexture); + std::string_view surfaceTexture = selectedFloor != nullptr ? selectedFloor->GetIdentifier() : ""; + + if (surfaceTexture.empty()) + { + std::vector availableTerrains; + std::copy_if( + std::begin(kBaseTerrain), std::end(kBaseTerrain), std::back_inserter(availableTerrains), + [&](auto terrain) { return objectManager.GetLoadedObject(ObjectEntryDescriptor(terrain)) != nullptr; }); + + if (availableTerrains.empty()) + // Fall back to the first available surface texture that is available in the park + surfaceTexture = TerrainSurfaceObject::GetById(0)->GetIdentifier(); + else + surfaceTexture = availableTerrains[UtilRand() % availableTerrains.size()]; + } + + auto surfaceTextureId = objectManager.GetLoadedObjectEntryIndex(ObjectEntryDescriptor(surfaceTexture)); + return surfaceTextureId; } - auto surfaceTextureId = objectManager.GetLoadedObjectEntryIndex(ObjectEntryDescriptor(surfaceTexture)); - return surfaceTextureId; -} - -ObjectEntryIndex MapGenEdgeTextureId(MapGenSettings* settings, ObjectEntryIndex surfaceTextureId) -{ - auto& objectManager = GetContext()->GetObjectManager(); - - const auto selectedEdge = TerrainEdgeObject::GetById(settings->edgeTexture); - std::string_view edgeTexture = selectedEdge != nullptr ? selectedEdge->GetIdentifier() : ""; - - if (edgeTexture.empty()) + ObjectEntryIndex generateEdgeTextureId(Settings* settings, ObjectEntryIndex surfaceTextureId) { - auto surfaceObject = objectManager.GetLoadedObject(ObjectType::TerrainSurface, surfaceTextureId); - auto surfaceTexture = surfaceObject->GetIdentifier(); + auto& objectManager = GetContext()->GetObjectManager(); - // Base edge type on surface type - if (surfaceTexture == "rct2.terrain_surface.dirt") - edgeTexture = "rct2.terrain_edge.wood_red"; - else if (surfaceTexture == "rct2.terrain_surface.ice") - edgeTexture = "rct2.terrain_edge.ice"; - else - edgeTexture = "rct2.terrain_edge.rock"; + const auto selectedEdge = TerrainEdgeObject::GetById(settings->edgeTexture); + std::string_view edgeTexture = selectedEdge != nullptr ? selectedEdge->GetIdentifier() : ""; - // Fall back to the first available edge texture that is available in the park - if (objectManager.GetLoadedObject(ObjectEntryDescriptor(edgeTexture)) == nullptr) - edgeTexture = TerrainEdgeObject::GetById(0)->GetIdentifier(); + if (edgeTexture.empty()) + { + auto surfaceObject = objectManager.GetLoadedObject(ObjectType::TerrainSurface, surfaceTextureId); + auto surfaceTexture = surfaceObject->GetIdentifier(); + + // Base edge type on surface type + if (surfaceTexture == "rct2.terrain_surface.dirt") + edgeTexture = "rct2.terrain_edge.wood_red"; + else if (surfaceTexture == "rct2.terrain_surface.ice") + edgeTexture = "rct2.terrain_edge.ice"; + else + edgeTexture = "rct2.terrain_edge.rock"; + + // Fall back to the first available edge texture that is available in the park + if (objectManager.GetLoadedObject(ObjectEntryDescriptor(edgeTexture)) == nullptr) + edgeTexture = TerrainEdgeObject::GetById(0)->GetIdentifier(); + } + + auto edgeTextureId = objectManager.GetLoadedObjectEntryIndex(ObjectEntryDescriptor(edgeTexture)); + return edgeTextureId; } - auto edgeTextureId = objectManager.GetLoadedObjectEntryIndex(ObjectEntryDescriptor(edgeTexture)); - return edgeTextureId; -} + ObjectEntryIndex generateBeachTextureId() + { + auto& objectManager = GetContext()->GetObjectManager(); -ObjectEntryIndex MapGenBeachTextureId() -{ - auto& objectManager = GetContext()->GetObjectManager(); + // Figure out what beach texture to use + std::vector availableBeachTextures; + if (objectManager.GetLoadedObject(ObjectEntryDescriptor("rct2.terrain_surface.sand")) != nullptr) + availableBeachTextures.push_back("rct2.terrain_surface.sand"); + if (objectManager.GetLoadedObject(ObjectEntryDescriptor("rct2.terrain_surface.sand_brown")) != nullptr) + availableBeachTextures.push_back("rct2.terrain_surface.sand_brown"); - // Figure out what beach texture to use - std::vector availableBeachTextures; - if (objectManager.GetLoadedObject(ObjectEntryDescriptor("rct2.terrain_surface.sand")) != nullptr) - availableBeachTextures.push_back("rct2.terrain_surface.sand"); - if (objectManager.GetLoadedObject(ObjectEntryDescriptor("rct2.terrain_surface.sand_brown")) != nullptr) - availableBeachTextures.push_back("rct2.terrain_surface.sand_brown"); + if (availableBeachTextures.empty()) + return OBJECT_ENTRY_INDEX_NULL; - if (availableBeachTextures.empty()) - return OBJECT_ENTRY_INDEX_NULL; - - std::string_view beachTexture = availableBeachTextures[UtilRand() % availableBeachTextures.size()]; - return objectManager.GetLoadedObjectEntryIndex(ObjectEntryDescriptor(beachTexture)); -} + std::string_view beachTexture = availableBeachTextures[UtilRand() % availableBeachTextures.size()]; + return objectManager.GetLoadedObjectEntryIndex(ObjectEntryDescriptor(beachTexture)); + } +} // namespace OpenRCT2::World::MapGenerator diff --git a/src/openrct2/world/map_generator/SurfaceSelection.h b/src/openrct2/world/map_generator/SurfaceSelection.h index 945ec5f120..878441022a 100644 --- a/src/openrct2/world/map_generator/SurfaceSelection.h +++ b/src/openrct2/world/map_generator/SurfaceSelection.h @@ -11,8 +11,11 @@ #include "../../object/ObjectTypes.h" -struct MapGenSettings; +namespace OpenRCT2::World::MapGenerator +{ + struct Settings; -ObjectEntryIndex MapGenSurfaceTextureId(MapGenSettings* settings); -ObjectEntryIndex MapGenEdgeTextureId(MapGenSettings* settings, ObjectEntryIndex surfaceTextureId); -ObjectEntryIndex MapGenBeachTextureId(); + ObjectEntryIndex generateSurfaceTextureId(Settings* settings); + ObjectEntryIndex generateEdgeTextureId(Settings* settings, ObjectEntryIndex surfaceTextureId); + ObjectEntryIndex generateBeachTextureId(); +} // namespace OpenRCT2::World::MapGenerator diff --git a/src/openrct2/world/map_generator/TreePlacement.cpp b/src/openrct2/world/map_generator/TreePlacement.cpp index 5b209b3f1c..6a5bded4aa 100644 --- a/src/openrct2/world/map_generator/TreePlacement.cpp +++ b/src/openrct2/world/map_generator/TreePlacement.cpp @@ -26,200 +26,204 @@ #include -using namespace OpenRCT2; - -static constexpr const char* kGrassTrees[] = { - // Dark - "rct2.scenery_small.tcf", // Caucasian Fir Tree - "rct2.scenery_small.trf", // Red Fir Tree - "rct2.scenery_small.trf2", // Red Fir Tree - "rct2.scenery_small.tsp", // Scots Pine Tree - "rct2.scenery_small.tmzp", // Montezuma Pine Tree - "rct2.scenery_small.tap", // Aleppo Pine Tree - "rct2.scenery_small.tcrp", // Corsican Pine Tree - "rct2.scenery_small.tbp", // Black Poplar Tree - - // Light - "rct2.scenery_small.tcl", // Cedar of Lebanon Tree - "rct2.scenery_small.tel", // European Larch Tree -}; - -static constexpr const char* kDesertTrees[] = { - "rct2.scenery_small.tmp", // Monkey-Puzzle Tree - "rct2.scenery_small.thl", // Honey Locust Tree - "rct2.scenery_small.th1", // Canary Palm Tree - "rct2.scenery_small.th2", // Palm Tree - "rct2.scenery_small.tpm", // Palm Tree - "rct2.scenery_small.tropt1", // Tree - "rct2.scenery_small.tbc", // Cactus - "rct2.scenery_small.tsc", // Cactus -}; - -static constexpr const char* kSnowTrees[] = { - "rct2.scenery_small.tcfs", // Snow-covered Caucasian Fir Tree - "rct2.scenery_small.tnss", // Snow-covered Norway Spruce Tree - "rct2.scenery_small.trf3", // Snow-covered Red Fir Tree - "rct2.scenery_small.trfs", // Snow-covered Red Fir Tree -}; - -static void MapGenPlaceTree(ObjectEntryIndex type, const CoordsXY& loc) +namespace OpenRCT2::World::MapGenerator { - auto* sceneryEntry = ObjectManager::GetObjectEntry(type); - if (sceneryEntry == nullptr) + static constexpr const char* kGrassTrees[] = { + // Dark + "rct2.scenery_small.tcf", // Caucasian Fir Tree + "rct2.scenery_small.trf", // Red Fir Tree + "rct2.scenery_small.trf2", // Red Fir Tree + "rct2.scenery_small.tsp", // Scots Pine Tree + "rct2.scenery_small.tmzp", // Montezuma Pine Tree + "rct2.scenery_small.tap", // Aleppo Pine Tree + "rct2.scenery_small.tcrp", // Corsican Pine Tree + "rct2.scenery_small.tbp", // Black Poplar Tree + + // Light + "rct2.scenery_small.tcl", // Cedar of Lebanon Tree + "rct2.scenery_small.tel", // European Larch Tree + }; + + static constexpr const char* kDesertTrees[] = { + "rct2.scenery_small.tmp", // Monkey-Puzzle Tree + "rct2.scenery_small.thl", // Honey Locust Tree + "rct2.scenery_small.th1", // Canary Palm Tree + "rct2.scenery_small.th2", // Palm Tree + "rct2.scenery_small.tpm", // Palm Tree + "rct2.scenery_small.tropt1", // Tree + "rct2.scenery_small.tbc", // Cactus + "rct2.scenery_small.tsc", // Cactus + }; + + static constexpr const char* kSnowTrees[] = { + "rct2.scenery_small.tcfs", // Snow-covered Caucasian Fir Tree + "rct2.scenery_small.tnss", // Snow-covered Norway Spruce Tree + "rct2.scenery_small.trf3", // Snow-covered Red Fir Tree + "rct2.scenery_small.trfs", // Snow-covered Red Fir Tree + }; + + static void placeTree(ObjectEntryIndex type, const CoordsXY& loc) { - return; - } - - int32_t surfaceZ = TileElementHeight(loc.ToTileCentre()); - - auto* sceneryElement = TileElementInsert({ loc, surfaceZ }, 0b1111); - Guard::Assert(sceneryElement != nullptr); - - sceneryElement->SetClearanceZ(surfaceZ + sceneryEntry->height); - sceneryElement->SetDirection(UtilRand() & 3); - sceneryElement->SetEntryIndex(type); - sceneryElement->SetAge(0); - sceneryElement->SetPrimaryColour(COLOUR_YELLOW); -} - -static bool MapGenSurfaceTakesGrassTrees(const TerrainSurfaceObject& surface) -{ - const auto& id = surface.GetIdentifier(); - return id == "rct2.terrain_surface.grass" || id == "rct2.terrain_surface.grass_clumps" || id == "rct2.terrain_surface.dirt"; -} - -static bool MapGenSurfaceTakesSandTrees(const TerrainSurfaceObject& surface) -{ - const auto& id = surface.GetIdentifier(); - return id == "rct2.terrain_surface.sand" || id == "rct2.terrain_surface.sand_brown" - || id == "rct2.terrain_surface.sand_red"; -} - -static bool MapGenSurfaceTakesSnowTrees(const TerrainSurfaceObject& surface) -{ - const auto& id = surface.GetIdentifier(); - return id == "rct2.terrain_surface.ice"; -} - -template -static bool TryFindTreeInList(std::string_view id, const T& treeList) -{ - for (size_t j = 0; j < std::size(treeList); j++) - { - if (treeList[j] == id) - return true; - } - return false; -} - -/** - * Randomly places a selection of preset trees on the map. Picks the right tree for the terrain it is placing it on. - */ -void MapGenPlaceTrees(MapGenSettings* settings) -{ - std::vector grassTreeIds; - std::vector desertTreeIds; - std::vector snowTreeIds; - - for (auto i = 0u; i < getObjectEntryGroupCount(ObjectType::SmallScenery); i++) - { - auto* sceneryEntry = OpenRCT2::ObjectManager::GetObjectEntry(i); - auto entry = ObjectEntryGetObject(ObjectType::SmallScenery, i); - + auto* sceneryEntry = ObjectManager::GetObjectEntry(type); if (sceneryEntry == nullptr) - continue; + { + return; + } - if (TryFindTreeInList(entry->GetIdentifier(), kGrassTrees)) - { - grassTreeIds.push_back(i); - } - else if (TryFindTreeInList(entry->GetIdentifier(), kDesertTrees)) - { - desertTreeIds.push_back(i); - } - else if (TryFindTreeInList(entry->GetIdentifier(), kSnowTrees)) - { - snowTreeIds.push_back(i); - } + int32_t surfaceZ = TileElementHeight(loc.ToTileCentre()); + + auto* sceneryElement = TileElementInsert({ loc, surfaceZ }, 0b1111); + Guard::Assert(sceneryElement != nullptr); + + sceneryElement->SetClearanceZ(surfaceZ + sceneryEntry->height); + sceneryElement->SetDirection(UtilRand() & 3); + sceneryElement->SetEntryIndex(type); + sceneryElement->SetAge(0); + sceneryElement->SetPrimaryColour(COLOUR_YELLOW); } - // Place trees - float treeToLandRatio = static_cast(settings->treeToLandRatio) / 100.0f; - - auto& gameState = GetGameState(); - for (int32_t y = 1; y < gameState.MapSize.y - 1; y++) + static bool surfaceTakesGrassTrees(const TerrainSurfaceObject& surface) { - for (int32_t x = 1; x < gameState.MapSize.x - 1; x++) + const auto& id = surface.GetIdentifier(); + return id == "rct2.terrain_surface.grass" || id == "rct2.terrain_surface.grass_clumps" + || id == "rct2.terrain_surface.dirt"; + } + + static bool surfaceTakesSandTrees(const TerrainSurfaceObject& surface) + { + const auto& id = surface.GetIdentifier(); + return id == "rct2.terrain_surface.sand" || id == "rct2.terrain_surface.sand_brown" + || id == "rct2.terrain_surface.sand_red"; + } + + static bool surfaceTakesSnowTrees(const TerrainSurfaceObject& surface) + { + const auto& id = surface.GetIdentifier(); + return id == "rct2.terrain_surface.ice"; + } + + template + static bool TryFindTreeInList(std::string_view id, const T& treeList) + { + for (size_t j = 0; j < std::size(treeList); j++) { - auto pos = CoordsXY{ x, y } * kCoordsXYStep; - auto* surfaceElement = MapGetSurfaceElementAt(pos); - if (surfaceElement == nullptr) + if (treeList[j] == id) + return true; + } + return false; + } + + /** + * Randomly places a selection of preset trees on the map. Picks the right tree for the terrain it is placing it on. + */ + void placeTrees(Settings* settings) + { + std::vector grassTreeIds; + std::vector desertTreeIds; + std::vector snowTreeIds; + + for (auto i = 0u; i < getObjectEntryGroupCount(ObjectType::SmallScenery); i++) + { + auto* sceneryEntry = OpenRCT2::ObjectManager::GetObjectEntry(i); + auto entry = ObjectEntryGetObject(ObjectType::SmallScenery, i); + + if (sceneryEntry == nullptr) continue; - // Don't place on water - if (surfaceElement->GetWaterHeight() > 0) - continue; - - if (settings->minTreeAltitude > surfaceElement->BaseHeight - || settings->maxTreeAltitude < surfaceElement->BaseHeight) - continue; - - // On sand surfaces, give the tile a score based on nearby water, to be used to determine whether to spawn - // vegetation - float oasisScore = 0.0f; - ObjectEntryIndex treeObjectEntryIndex = OBJECT_ENTRY_INDEX_NULL; - const auto& surfaceStyleObject = *TerrainSurfaceObject::GetById(surfaceElement->GetSurfaceObjectIndex()); - if (MapGenSurfaceTakesSandTrees(surfaceStyleObject)) + if (TryFindTreeInList(entry->GetIdentifier(), kGrassTrees)) { - oasisScore = -0.5f; - constexpr auto maxOasisDistance = 4; - for (int32_t offsetY = -maxOasisDistance; offsetY <= maxOasisDistance; offsetY++) - { - for (int32_t offsetX = -maxOasisDistance; offsetX <= maxOasisDistance; offsetX++) - { - // Get map coord, clamped to the edges - const auto offset = CoordsXY{ offsetX * kCoordsXYStep, offsetY * kCoordsXYStep }; - auto neighbourPos = pos + offset; - neighbourPos.x = std::clamp(neighbourPos.x, kCoordsXYStep, kCoordsXYStep * (gameState.MapSize.x - 1)); - neighbourPos.y = std::clamp(neighbourPos.y, kCoordsXYStep, kCoordsXYStep * (gameState.MapSize.y - 1)); + grassTreeIds.push_back(i); + } + else if (TryFindTreeInList(entry->GetIdentifier(), kDesertTrees)) + { + desertTreeIds.push_back(i); + } + else if (TryFindTreeInList(entry->GetIdentifier(), kSnowTrees)) + { + snowTreeIds.push_back(i); + } + } - const auto neighboutSurface = MapGetSurfaceElementAt(neighbourPos); - if (neighboutSurface != nullptr && neighboutSurface->GetWaterHeight() > 0) + // Place trees + float treeToLandRatio = static_cast(settings->treeToLandRatio) / 100.0f; + + auto& gameState = GetGameState(); + for (int32_t y = 1; y < gameState.MapSize.y - 1; y++) + { + for (int32_t x = 1; x < gameState.MapSize.x - 1; x++) + { + auto pos = CoordsXY{ x, y } * kCoordsXYStep; + auto* surfaceElement = MapGetSurfaceElementAt(pos); + if (surfaceElement == nullptr) + continue; + + // Don't place on water + if (surfaceElement->GetWaterHeight() > 0) + continue; + + if (settings->minTreeAltitude > surfaceElement->BaseHeight + || settings->maxTreeAltitude < surfaceElement->BaseHeight) + continue; + + // On sand surfaces, give the tile a score based on nearby water, to be used to determine whether to spawn + // vegetation + float oasisScore = 0.0f; + ObjectEntryIndex treeObjectEntryIndex = OBJECT_ENTRY_INDEX_NULL; + const auto& surfaceStyleObject = *TerrainSurfaceObject::GetById(surfaceElement->GetSurfaceObjectIndex()); + if (surfaceTakesSandTrees(surfaceStyleObject)) + { + oasisScore = -0.5f; + constexpr auto maxOasisDistance = 4; + for (int32_t offsetY = -maxOasisDistance; offsetY <= maxOasisDistance; offsetY++) + { + for (int32_t offsetX = -maxOasisDistance; offsetX <= maxOasisDistance; offsetX++) { - float distance = std::sqrt(offsetX * offsetX + offsetY * offsetY); - oasisScore += 0.5f / (maxOasisDistance * distance); + // Get map coord, clamped to the edges + const auto offset = CoordsXY{ offsetX * kCoordsXYStep, offsetY * kCoordsXYStep }; + auto neighbourPos = pos + offset; + neighbourPos.x = std::clamp( + neighbourPos.x, kCoordsXYStep, kCoordsXYStep * (gameState.MapSize.x - 1)); + neighbourPos.y = std::clamp( + neighbourPos.y, kCoordsXYStep, kCoordsXYStep * (gameState.MapSize.y - 1)); + + const auto neighboutSurface = MapGetSurfaceElementAt(neighbourPos); + if (neighboutSurface != nullptr && neighboutSurface->GetWaterHeight() > 0) + { + float distance = std::sqrt(offsetX * offsetX + offsetY * offsetY); + oasisScore += 0.5f / (maxOasisDistance * distance); + } } } } - } - // Use tree:land ratio except when near an oasis - constexpr static auto randModulo = 0xFFFF; - if (static_cast(UtilRand() & randModulo) / randModulo > std::max(treeToLandRatio, oasisScore)) - continue; + // Use tree:land ratio except when near an oasis + constexpr static auto randModulo = 0xFFFF; + if (static_cast(UtilRand() & randModulo) / randModulo > std::max(treeToLandRatio, oasisScore)) + continue; - // Use fractal noise to group tiles that are likely to spawn trees together - float noiseValue = FractalNoise(x, y, 0.025f, 2, 2.0f, 0.65f); - // Reduces the range to rarely stray further than 0.5 from the mean. - float noiseOffset = UtilRandNormalDistributed() * 0.25f; - if (noiseValue + oasisScore < noiseOffset) - continue; + // Use fractal noise to group tiles that are likely to spawn trees together + float noiseValue = FractalNoise(x, y, 0.025f, 2, 2.0f, 0.65f); + // Reduces the range to rarely stray further than 0.5 from the mean. + float noiseOffset = UtilRandNormalDistributed() * 0.25f; + if (noiseValue + oasisScore < noiseOffset) + continue; - if (!grassTreeIds.empty() && MapGenSurfaceTakesGrassTrees(surfaceStyleObject)) - { - treeObjectEntryIndex = grassTreeIds[UtilRand() % grassTreeIds.size()]; - } - else if (!desertTreeIds.empty() && MapGenSurfaceTakesSandTrees(surfaceStyleObject)) - { - treeObjectEntryIndex = desertTreeIds[UtilRand() % desertTreeIds.size()]; - } - else if (!snowTreeIds.empty() && MapGenSurfaceTakesSnowTrees(surfaceStyleObject)) - { - treeObjectEntryIndex = snowTreeIds[UtilRand() % snowTreeIds.size()]; - } + if (!grassTreeIds.empty() && surfaceTakesGrassTrees(surfaceStyleObject)) + { + treeObjectEntryIndex = grassTreeIds[UtilRand() % grassTreeIds.size()]; + } + else if (!desertTreeIds.empty() && surfaceTakesSandTrees(surfaceStyleObject)) + { + treeObjectEntryIndex = desertTreeIds[UtilRand() % desertTreeIds.size()]; + } + else if (!snowTreeIds.empty() && surfaceTakesSnowTrees(surfaceStyleObject)) + { + treeObjectEntryIndex = snowTreeIds[UtilRand() % snowTreeIds.size()]; + } - if (treeObjectEntryIndex != OBJECT_ENTRY_INDEX_NULL) - MapGenPlaceTree(treeObjectEntryIndex, pos); + if (treeObjectEntryIndex != OBJECT_ENTRY_INDEX_NULL) + placeTree(treeObjectEntryIndex, pos); + } } } -} +} // namespace OpenRCT2::World::MapGenerator diff --git a/src/openrct2/world/map_generator/TreePlacement.h b/src/openrct2/world/map_generator/TreePlacement.h index c8e4fe9745..80b45555ab 100644 --- a/src/openrct2/world/map_generator/TreePlacement.h +++ b/src/openrct2/world/map_generator/TreePlacement.h @@ -7,6 +7,9 @@ * OpenRCT2 is licensed under the GNU General Public License version 3. *****************************************************************************/ -struct MapGenSettings; +namespace OpenRCT2::World::MapGenerator +{ + struct Settings; -void MapGenPlaceTrees(MapGenSettings* settings); + void placeTrees(Settings* settings); +} // namespace OpenRCT2::World::MapGenerator From 6386307b772ec17ac8c72b629e4a26e087f1e0b6 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen Date: Mon, 30 Dec 2024 16:45:33 +0100 Subject: [PATCH 07/10] Refactor raw height array into HeightMap class --- src/openrct2/libopenrct2.vcxproj | 1 + .../world/map_generator/HeightMap.hpp | 66 ++++++++++++++ src/openrct2/world/map_generator/MapGen.cpp | 91 +++++++------------ 3 files changed, 99 insertions(+), 59 deletions(-) create mode 100644 src/openrct2/world/map_generator/HeightMap.hpp diff --git a/src/openrct2/libopenrct2.vcxproj b/src/openrct2/libopenrct2.vcxproj index c78270d037..26bbb300f0 100644 --- a/src/openrct2/libopenrct2.vcxproj +++ b/src/openrct2/libopenrct2.vcxproj @@ -624,6 +624,7 @@ + diff --git a/src/openrct2/world/map_generator/HeightMap.hpp b/src/openrct2/world/map_generator/HeightMap.hpp new file mode 100644 index 0000000000..1ff4e86189 --- /dev/null +++ b/src/openrct2/world/map_generator/HeightMap.hpp @@ -0,0 +1,66 @@ +/***************************************************************************** + * Copyright (c) 2014-2025 OpenRCT2 developers + * + * For a complete list of all authors, please refer to contributors.md + * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 + * + * OpenRCT2 is licensed under the GNU General Public License version 3. + *****************************************************************************/ + +#pragma once + +#include "../Location.hpp" + +#include +#include +#include +#include + +namespace OpenRCT2::World::MapGenerator +{ + class HeightMap + { + private: + std::vector _height; + + public: + const uint16_t width; + const uint16_t height; + + HeightMap(int32_t tagetWidth, int32_t targetHeight) + : _height(tagetWidth * targetHeight) + , width(tagetWidth) + , height(targetHeight) + { + } + + HeightMap(const HeightMap& heightMap) = default; + + uint8_t& operator[](TileCoordsXY pos) + { + assert(pos.x >= 0 || pos.y >= 0 || pos.x < width || pos.y < height); + return _height[pos.y * width + pos.x]; + } + + const uint8_t& operator[](TileCoordsXY pos) const + { + assert(pos.x >= 0 || pos.y >= 0 || pos.x < width || pos.y < height); + return _height[pos.y * width + pos.x]; + } + + uint8_t* data() + { + return _height.data(); + } + + const uint8_t* data() const + { + return _height.data(); + } + + size_t size() const + { + return _height.size(); + } + }; +} // namespace OpenRCT2::World::MapGenerator diff --git a/src/openrct2/world/map_generator/MapGen.cpp b/src/openrct2/world/map_generator/MapGen.cpp index 164d667084..cf93eff806 100644 --- a/src/openrct2/world/map_generator/MapGen.cpp +++ b/src/openrct2/world/map_generator/MapGen.cpp @@ -15,6 +15,7 @@ #include "../../util/Util.h" #include "../tile_element/Slope.h" #include "../tile_element/SurfaceElement.h" +#include "HeightMap.hpp" #include "MapHelpers.h" #include "PngTerrainGenerator.h" #include "SimplexNoise.h" @@ -58,8 +59,8 @@ namespace OpenRCT2::World::MapGenerator } static void setWaterLevel(int32_t waterLevel); - static void smoothHeight(int32_t iterations); - static void setHeight(Settings* settings); + static void smoothHeightMap(int32_t iterations, HeightMap& heightMap); + static void setMapHeight(Settings* settings, const HeightMap& heightMap); static void resetSurfaces(Settings* settings) { @@ -91,24 +92,7 @@ namespace OpenRCT2::World::MapGenerator setWaterLevel(settings->waterLevel); } - static TileCoordsXY _heightSize; - static uint8_t* _height; - - static int32_t getHeight(int32_t x, int32_t y) - { - if (x >= 0 && y >= 0 && x < _heightSize.x && y < _heightSize.y) - return _height[x + y * _heightSize.x]; - - return 0; - } - - static void setHeight(int32_t x, int32_t y, int32_t height) - { - if (x >= 0 && y >= 0 && x < _heightSize.x && y < _heightSize.y) - _height[x + y * _heightSize.x] = height; - } - - static void generateSimplexNoise(Settings* settings); + static void generateSimplexNoise(Settings* settings, HeightMap& heightMap); static void generateSimplexMap(Settings* settings) { @@ -116,16 +100,13 @@ namespace OpenRCT2::World::MapGenerator // Create the temporary height map and initialise auto& mapSize = settings->mapSize; - _heightSize = { mapSize.x * 2, mapSize.y * 2 }; - _height = new uint8_t[_heightSize.y * _heightSize.x]; - std::fill_n(_height, _heightSize.y * _heightSize.x, 0x00); + auto heightMap = HeightMap(mapSize.x * 2, mapSize.y * 2); - generateSimplexNoise(settings); - smoothHeight(2 + (UtilRand() % 6)); + generateSimplexNoise(settings, heightMap); + smoothHeightMap(2 + (UtilRand() % 6), heightMap); // Set the game map to the height map - setHeight(settings); - delete[] _height; + setMapHeight(settings, heightMap); if (settings->smoothTileEdges) { @@ -179,54 +160,46 @@ namespace OpenRCT2::World::MapGenerator /** * Smooths the height map. */ - static void smoothHeight(int32_t iterations) + static void smoothHeightMap(int32_t iterations, HeightMap& heightMap) { - int32_t i, x, y, xx, yy, avg; - int32_t arraySize = _heightSize.y * _heightSize.x * sizeof(uint8_t); - uint8_t* copyHeight = new uint8_t[arraySize]; - - for (i = 0; i < iterations; i++) + for (auto i = 0; i < iterations; i++) { - std::memcpy(copyHeight, _height, arraySize); - for (y = 1; y < _heightSize.y - 1; y++) + auto copyHeight = heightMap; + for (auto y = 1; y < heightMap.height - 1; y++) { - for (x = 1; x < _heightSize.x - 1; x++) + for (auto x = 1; x < heightMap.width - 1; x++) { - avg = 0; - for (yy = -1; yy <= 1; yy++) + auto avg = 0; + for (auto yy = -1; yy <= 1; yy++) { - for (xx = -1; xx <= 1; xx++) + for (auto xx = -1; xx <= 1; xx++) { - avg += copyHeight[(y + yy) * _heightSize.x + (x + xx)]; + avg += copyHeight[{ y + yy, x + xx }]; } } avg /= 9; - setHeight(x, y, avg); + heightMap[{ x, y }] = avg; } } } - - delete[] copyHeight; } /** * Sets the height of the actual game map tiles to the height map. */ - static void setHeight(Settings* settings) + static void setMapHeight(Settings* settings, const HeightMap& heightMap) { - int32_t x, y, heightX, heightY; - - for (y = 1; y < _heightSize.y / 2 - 1; y++) + for (auto y = 1; y < heightMap.height / 2 - 1; y++) { - for (x = 1; x < _heightSize.x / 2 - 1; x++) + for (auto x = 1; x < heightMap.width / 2 - 1; x++) { - heightX = x * 2; - heightY = y * 2; + auto heightX = x * 2; + auto heightY = y * 2; - uint8_t q00 = getHeight(heightX + 0, heightY + 0); - uint8_t q01 = getHeight(heightX + 0, heightY + 1); - uint8_t q10 = getHeight(heightX + 1, heightY + 0); - uint8_t q11 = getHeight(heightX + 1, heightY + 1); + uint8_t q00 = heightMap[{ heightX + 0, heightY + 0 }]; + uint8_t q01 = heightMap[{ heightX + 0, heightY + 1 }]; + uint8_t q10 = heightMap[{ heightX + 1, heightY + 0 }]; + uint8_t q11 = heightMap[{ heightX + 1, heightY + 1 }]; uint8_t baseHeight = (q00 + q01 + q10 + q11) / 4; @@ -257,23 +230,23 @@ namespace OpenRCT2::World::MapGenerator } } - static void generateSimplexNoise(Settings* settings) + static void generateSimplexNoise(Settings* settings, HeightMap& heightMap) { - float freq = settings->simplex_base_freq / 100.0f * (1.0f / _heightSize.x); + float freq = settings->simplex_base_freq / 100.0f * (1.0f / heightMap.width); int32_t octaves = settings->simplex_octaves; int32_t low = settings->heightmapLow / 2; int32_t high = settings->heightmapHigh / 2 - low; NoiseRand(); - for (int32_t y = 0; y < _heightSize.y; y++) + for (int32_t y = 0; y < heightMap.height; y++) { - for (int32_t x = 0; x < _heightSize.x; x++) + for (int32_t x = 0; x < heightMap.width; x++) { float noiseValue = std::clamp(FractalNoise(x, y, freq, octaves, 2.0f, 0.65f), -1.0f, 1.0f); float normalisedNoiseValue = (noiseValue + 1.0f) / 2.0f; - setHeight(x, y, low + static_cast(normalisedNoiseValue * high)); + heightMap[{ x, y }] = low + static_cast(normalisedNoiseValue * high); } } } From a809f481066c71b6a8e95454497dd038e273bad7 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen Date: Mon, 30 Dec 2024 17:32:16 +0100 Subject: [PATCH 08/10] Move more simplex functions to SimplexNoise unit --- src/openrct2/world/map_generator/MapGen.cpp | 88 +------------------ src/openrct2/world/map_generator/MapGen.h | 6 ++ .../world/map_generator/SimplexNoise.cpp | 78 ++++++++++++++++ .../world/map_generator/SimplexNoise.h | 4 + 4 files changed, 91 insertions(+), 85 deletions(-) diff --git a/src/openrct2/world/map_generator/MapGen.cpp b/src/openrct2/world/map_generator/MapGen.cpp index cf93eff806..0ca7bcf66d 100644 --- a/src/openrct2/world/map_generator/MapGen.cpp +++ b/src/openrct2/world/map_generator/MapGen.cpp @@ -16,7 +16,6 @@ #include "../tile_element/Slope.h" #include "../tile_element/SurfaceElement.h" #include "HeightMap.hpp" -#include "MapHelpers.h" #include "PngTerrainGenerator.h" #include "SimplexNoise.h" #include "SurfaceSelection.h" @@ -27,7 +26,6 @@ namespace OpenRCT2::World::MapGenerator { static void generateBlankMap(Settings* settings); - static void generateSimplexMap(Settings* settings); static void addBeaches(Settings* settings); @@ -58,11 +56,7 @@ namespace OpenRCT2::World::MapGenerator placeTrees(settings); } - static void setWaterLevel(int32_t waterLevel); - static void smoothHeightMap(int32_t iterations, HeightMap& heightMap); - static void setMapHeight(Settings* settings, const HeightMap& heightMap); - - static void resetSurfaces(Settings* settings) + void resetSurfaces(Settings* settings) { MapClearAllElements(); MapInit(settings->mapSize); @@ -92,34 +86,6 @@ namespace OpenRCT2::World::MapGenerator setWaterLevel(settings->waterLevel); } - static void generateSimplexNoise(Settings* settings, HeightMap& heightMap); - - static void generateSimplexMap(Settings* settings) - { - resetSurfaces(settings); - - // Create the temporary height map and initialise - auto& mapSize = settings->mapSize; - auto heightMap = HeightMap(mapSize.x * 2, mapSize.y * 2); - - generateSimplexNoise(settings, heightMap); - smoothHeightMap(2 + (UtilRand() % 6), heightMap); - - // Set the game map to the height map - setMapHeight(settings, heightMap); - - if (settings->smoothTileEdges) - { - // Set the tile slopes so that there are no cliffs - while (MapSmooth(1, 1, mapSize.x - 1, mapSize.y - 1)) - { - } - } - - // Add the water - setWaterLevel(settings->waterLevel); - } - static void addBeaches(Settings* settings) { auto beachTextureId = generateBeachTextureId(); @@ -143,7 +109,7 @@ namespace OpenRCT2::World::MapGenerator /** * Sets each tile's water level to the specified water level if underneath that water level. */ - static void setWaterLevel(int32_t waterLevel) + void setWaterLevel(int32_t waterLevel) { auto& gameState = GetGameState(); for (int32_t y = 1; y < gameState.MapSize.y - 1; y++) @@ -157,37 +123,10 @@ namespace OpenRCT2::World::MapGenerator } } - /** - * Smooths the height map. - */ - static void smoothHeightMap(int32_t iterations, HeightMap& heightMap) - { - for (auto i = 0; i < iterations; i++) - { - auto copyHeight = heightMap; - for (auto y = 1; y < heightMap.height - 1; y++) - { - for (auto x = 1; x < heightMap.width - 1; x++) - { - auto avg = 0; - for (auto yy = -1; yy <= 1; yy++) - { - for (auto xx = -1; xx <= 1; xx++) - { - avg += copyHeight[{ y + yy, x + xx }]; - } - } - avg /= 9; - heightMap[{ x, y }] = avg; - } - } - } - } - /** * Sets the height of the actual game map tiles to the height map. */ - static void setMapHeight(Settings* settings, const HeightMap& heightMap) + void setMapHeight(Settings* settings, const HeightMap& heightMap) { for (auto y = 1; y < heightMap.height / 2 - 1; y++) { @@ -229,25 +168,4 @@ namespace OpenRCT2::World::MapGenerator } } } - - static void generateSimplexNoise(Settings* settings, HeightMap& heightMap) - { - float freq = settings->simplex_base_freq / 100.0f * (1.0f / heightMap.width); - int32_t octaves = settings->simplex_octaves; - - int32_t low = settings->heightmapLow / 2; - int32_t high = settings->heightmapHigh / 2 - low; - - NoiseRand(); - for (int32_t y = 0; y < heightMap.height; y++) - { - for (int32_t x = 0; x < heightMap.width; x++) - { - float noiseValue = std::clamp(FractalNoise(x, y, freq, octaves, 2.0f, 0.65f), -1.0f, 1.0f); - float normalisedNoiseValue = (noiseValue + 1.0f) / 2.0f; - - heightMap[{ x, y }] = low + static_cast(normalisedNoiseValue * high); - } - } - } } // namespace OpenRCT2::World::MapGenerator diff --git a/src/openrct2/world/map_generator/MapGen.h b/src/openrct2/world/map_generator/MapGen.h index cf1c2476e4..431a231217 100644 --- a/src/openrct2/world/map_generator/MapGen.h +++ b/src/openrct2/world/map_generator/MapGen.h @@ -49,5 +49,11 @@ namespace OpenRCT2::World::MapGenerator bool normalize_height = true; }; + class HeightMap; + void generate(Settings* settings); + void resetSurfaces(Settings* settings); + void setWaterLevel(int32_t waterLevel); + void setMapHeight(Settings* settings, const HeightMap& heightMap); + } // namespace OpenRCT2::World::MapGenerator diff --git a/src/openrct2/world/map_generator/SimplexNoise.cpp b/src/openrct2/world/map_generator/SimplexNoise.cpp index 6ea77f2e46..e65273feb9 100644 --- a/src/openrct2/world/map_generator/SimplexNoise.cpp +++ b/src/openrct2/world/map_generator/SimplexNoise.cpp @@ -10,7 +10,11 @@ #include "SimplexNoise.h" #include "../../util/Util.h" +#include "HeightMap.hpp" #include "MapGen.h" +#include "MapHelpers.h" + +#include namespace OpenRCT2::World::MapGenerator { @@ -146,4 +150,78 @@ namespace OpenRCT2::World::MapGenerator float v = h < 4 ? y : x; // and compute the dot product with (x,y). return ((h & 1) != 0 ? -u : u) + ((h & 2) != 0 ? -2.0f * v : 2.0f * v); } + + /** + * Smooths the height map. + */ + static void smoothHeightMap(int32_t iterations, HeightMap& heightMap) + { + for (auto i = 0; i < iterations; i++) + { + auto copyHeight = heightMap; + for (auto y = 1; y < heightMap.height - 1; y++) + { + for (auto x = 1; x < heightMap.width - 1; x++) + { + auto avg = 0; + for (auto yy = -1; yy <= 1; yy++) + { + for (auto xx = -1; xx <= 1; xx++) + { + avg += copyHeight[{ y + yy, x + xx }]; + } + } + avg /= 9; + heightMap[{ x, y }] = avg; + } + } + } + } + + static void generateSimplexNoise(Settings* settings, HeightMap& heightMap) + { + float freq = settings->simplex_base_freq / 100.0f * (1.0f / heightMap.width); + int32_t octaves = settings->simplex_octaves; + + int32_t low = settings->heightmapLow / 2; + int32_t high = settings->heightmapHigh / 2 - low; + + NoiseRand(); + for (int32_t y = 0; y < heightMap.height; y++) + { + for (int32_t x = 0; x < heightMap.width; x++) + { + float noiseValue = std::clamp(FractalNoise(x, y, freq, octaves, 2.0f, 0.65f), -1.0f, 1.0f); + float normalisedNoiseValue = (noiseValue + 1.0f) / 2.0f; + + heightMap[{ x, y }] = low + static_cast(normalisedNoiseValue * high); + } + } + } + + void generateSimplexMap(Settings* settings) + { + resetSurfaces(settings); + + // Create the temporary height map and initialise + auto& mapSize = settings->mapSize; + auto heightMap = HeightMap(mapSize.x * 2, mapSize.y * 2); + + generateSimplexNoise(settings, heightMap); + smoothHeightMap(2 + (UtilRand() % 6), heightMap); + + // Set the game map to the height map + setMapHeight(settings, heightMap); + + if (settings->smoothTileEdges) + { + // Set the tile slopes so that there are no cliffs + while (MapSmooth(1, 1, mapSize.x - 1, mapSize.y - 1)) + { + } + } + + // Add the water + setWaterLevel(settings->waterLevel); + } } // namespace OpenRCT2::World::MapGenerator diff --git a/src/openrct2/world/map_generator/SimplexNoise.h b/src/openrct2/world/map_generator/SimplexNoise.h index c46df9bd60..f8e5a3f515 100644 --- a/src/openrct2/world/map_generator/SimplexNoise.h +++ b/src/openrct2/world/map_generator/SimplexNoise.h @@ -13,6 +13,10 @@ namespace OpenRCT2::World::MapGenerator { + struct Settings; + void NoiseRand(); float FractalNoise(int32_t x, int32_t y, float frequency, int32_t octaves, float lacunarity, float persistence); + + void generateSimplexMap(Settings* settings); } // namespace OpenRCT2::World::MapGenerator From c0505bb16472a993ff2d3666e07a4fbfc8198702 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen Date: Thu, 2 Jan 2025 12:30:51 +0100 Subject: [PATCH 09/10] Fix mapgen window reverting to flatland when selecting heightmap image --- distribution/changelog.txt | 1 + src/openrct2-ui/windows/MapGen.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/distribution/changelog.txt b/distribution/changelog.txt index 638cd4d137..e390b0622b 100644 --- a/distribution/changelog.txt +++ b/distribution/changelog.txt @@ -14,6 +14,7 @@ - Fix: [#23376] Peeps with balloons, hats and umbrellas may leave artifacts on screen. - Fix: [#23486] Object selection minimum requirements can be bypassed with close window hotkey. - Fix: [#23496] Newly spawned vehicles are invisible when spawned while the game is paused. +- Fix: [#23509] Map generator window reverts to flatland after selecting a heightmap image. 0.4.17 (2024-12-08) ------------------------------------------------------------------------ diff --git a/src/openrct2-ui/windows/MapGen.cpp b/src/openrct2-ui/windows/MapGen.cpp index 0727377b26..e8f41533e1 100644 --- a/src/openrct2-ui/windows/MapGen.cpp +++ b/src/openrct2-ui/windows/MapGen.cpp @@ -1546,6 +1546,7 @@ namespace OpenRCT2::Ui::Windows // The window needs to be open while using the map _heightmapLoaded = true; _heightmapFilename = fs::u8path(path).filename().string(); + _settings.algorithm = MapGenerator::Algorithm::heightmapImage; SetPage(WINDOW_MAPGEN_PAGE_BASE); } } From 167daaa1d25189e0cd05ae26a02cece4b2979b29 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen Date: Thu, 2 Jan 2025 12:38:33 +0100 Subject: [PATCH 10/10] Fix #23135: Tree placement has noticable patterns --- distribution/changelog.txt | 1 + src/openrct2/world/map_generator/TreePlacement.cpp | 3 +++ 2 files changed, 4 insertions(+) diff --git a/distribution/changelog.txt b/distribution/changelog.txt index e390b0622b..04b98f1abf 100644 --- a/distribution/changelog.txt +++ b/distribution/changelog.txt @@ -9,6 +9,7 @@ - Change: [#23413] The max number of park entrance objects has been raised to 255. - Fix: [#1122] Trains spawned on a cable lift hill will fall down and crash (original bug). - Fix: [#22742, #22793] In game console does not handle format tokens properly. +- Fix: [#23135] Map generator tree placement has noticable patterns. - Fix: [#23286] Currency formatted incorrectly in the in game console. - Fix: [#23348] Console set commands don't print output properly. - Fix: [#23376] Peeps with balloons, hats and umbrellas may leave artifacts on screen. diff --git a/src/openrct2/world/map_generator/TreePlacement.cpp b/src/openrct2/world/map_generator/TreePlacement.cpp index 6a5bded4aa..8061e50b04 100644 --- a/src/openrct2/world/map_generator/TreePlacement.cpp +++ b/src/openrct2/world/map_generator/TreePlacement.cpp @@ -147,6 +147,9 @@ namespace OpenRCT2::World::MapGenerator // Place trees float treeToLandRatio = static_cast(settings->treeToLandRatio) / 100.0f; + // Randomise simplex noise + NoiseRand(); + auto& gameState = GetGameState(); for (int32_t y = 1; y < gameState.MapSize.y - 1; y++) {