diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index 39b8044f5..62218f98b 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -151,6 +151,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/rmg/Zone.cpp ${MAIN_LIB_DIR}/rmg/Functions.cpp ${MAIN_LIB_DIR}/rmg/RmgMap.cpp + ${MAIN_LIB_DIR}/rmg/PenroseTiling.cpp ${MAIN_LIB_DIR}/rmg/modificators/Modificator.cpp ${MAIN_LIB_DIR}/rmg/modificators/ObjectManager.cpp ${MAIN_LIB_DIR}/rmg/modificators/ObjectDistributor.cpp @@ -522,6 +523,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/rmg/RmgMap.h ${MAIN_LIB_DIR}/rmg/float3.h ${MAIN_LIB_DIR}/rmg/Functions.h + ${MAIN_LIB_DIR}/rmg/PenroseTiling.h ${MAIN_LIB_DIR}/rmg/modificators/Modificator.h ${MAIN_LIB_DIR}/rmg/modificators/ObjectManager.h ${MAIN_LIB_DIR}/rmg/modificators/ObjectDistributor.h diff --git a/lib/rmg/CZonePlacer.cpp b/lib/rmg/CZonePlacer.cpp index 44e372dbe..6bcc27d64 100644 --- a/lib/rmg/CZonePlacer.cpp +++ b/lib/rmg/CZonePlacer.cpp @@ -20,13 +20,14 @@ #include "RmgMap.h" #include "Zone.h" #include "Functions.h" +#include "PenroseTiling.h" VCMI_LIB_NAMESPACE_BEGIN class CRandomGenerator; CZonePlacer::CZonePlacer(RmgMap & map) - : width(0), height(0), scaleX(0), scaleY(0), mapSize(0), + : width(0), height(0), mapSize(0), gravityConstant(1e-3f), stiffnessConstant(3e-3f), stifness(0), @@ -524,7 +525,7 @@ void CZonePlacer::prepareZones(TZoneMap &zones, TZoneVector &zonesVector, const std::vector prescaler = { 0, 0 }; for (int i = 0; i < 2; i++) - prescaler[i] = std::sqrt((width * height) / (totalSize[i] * 3.14f)); + prescaler[i] = std::sqrt((width * height) / (totalSize[i] * PI_CONSTANT)); mapSize = static_cast(sqrt(width * height)); for(const auto & zone : zones) { @@ -802,18 +803,8 @@ void CZonePlacer::moveOneZone(TZoneMap& zones, TForceVector& totalForces, TDista float CZonePlacer::metric (const int3 &A, const int3 &B) const { - float dx = abs(A.x - B.x) * scaleX; - float dy = abs(A.y - B.y) * scaleY; + return A.dist2dSQ(B); - /* - 1. Normal euclidean distance - 2. Sinus for extra curves - 3. Nonlinear mess for fuzzy edges - */ - - return dx * dx + dy * dy + - 5 * std::sin(dx * dy / 10) + - 25 * std::sin (std::sqrt(A.x * B.x) * (A.y - B.y) / 100 * (scaleX * scaleY)); } void CZonePlacer::assignZones(CRandomGenerator * rand) @@ -823,9 +814,6 @@ void CZonePlacer::assignZones(CRandomGenerator * rand) auto width = map.getMapGenOptions().getWidth(); auto height = map.getMapGenOptions().getHeight(); - //scale to Medium map to ensure smooth results - scaleX = 72.f / width; - scaleY = 72.f / height; auto zones = map.getZones(); vstd::erase_if(zones, [](const std::pair> & pr) @@ -845,7 +833,13 @@ void CZonePlacer::assignZones(CRandomGenerator * rand) return lhs.second / lhs.first->getSize() < rhs.second / rhs.first->getSize(); }; - auto moveZoneToCenterOfMass = [](const std::shared_ptr & zone) -> void + auto simpleCompareByDistance = [](const Dpair & lhs, const Dpair & rhs) -> bool + { + //bigger zones have smaller distance + return lhs.second < rhs.second; + }; + + auto moveZoneToCenterOfMass = [width, height](const std::shared_ptr & zone) -> void { int3 total(0, 0, 0); auto tiles = zone->area().getTiles(); @@ -855,17 +849,17 @@ void CZonePlacer::assignZones(CRandomGenerator * rand) } int size = static_cast(tiles.size()); assert(size); - zone->setPos(int3(total.x / size, total.y / size, total.z / size)); + auto newPos = int3(total.x / size, total.y / size, total.z / size); + zone->setPos(newPos); + zone->setCenter(float3(float(newPos.x) / width, float(newPos.y) / height, newPos.z)); }; int levels = map.levels(); - /* - 1. Create Voronoi diagram - 2. find current center of mass for each zone. Move zone to that center to balance zones sizes - */ + // Find current center of mass for each zone. Move zone to that center to balance zones sizes int3 pos; + for(pos.z = 0; pos.z < levels; pos.z++) { for(pos.x = 0; pos.x < width; pos.x++) @@ -893,11 +887,28 @@ void CZonePlacer::assignZones(CRandomGenerator * rand) moveZoneToCenterOfMass(zone.second); } - //assign actual tiles to each zone using nonlinear norm for fine edges - for(const auto & zone : zones) zone.second->clearTiles(); //now populate them again + // Assign zones to closest Penrose vertex + PenroseTiling penrose; + auto vertices = penrose.generatePenroseTiling(zones.size() / map.levels(), rand); + + std::map, std::set> vertexMapping; + + for (const auto & vertex : vertices) + { + distances.clear(); + for(const auto & zone : zones) + { + distances.emplace_back(zone.second, zone.second->getCenter().dist2dSQ(float3(vertex.x(), vertex.y(), 0))); + } + auto closestZone = boost::min_element(distances, compareByDistance)->first; + + vertexMapping[closestZone].insert(int3(vertex.x() * width, vertex.y() * height, closestZone->getPos().z)); //Closest vertex belongs to zone + } + + //Assign actual tiles to each zone using nonlinear norm for fine edges for (pos.z = 0; pos.z < levels; pos.z++) { for (pos.x = 0; pos.x < width; pos.x++) @@ -905,22 +916,32 @@ void CZonePlacer::assignZones(CRandomGenerator * rand) for (pos.y = 0; pos.y < height; pos.y++) { distances.clear(); - for(const auto & zone : zones) + for(const auto & zoneVertex : vertexMapping) { - if (zone.second->getPos().z == pos.z) - distances.emplace_back(zone.second, metric(pos, zone.second->getPos())); - else - distances.emplace_back(zone.second, std::numeric_limits::max()); + // FIXME: Find closest vertex, not closest zone + auto zone = zoneVertex.first; + for (const auto & vertex : zoneVertex.second) + { + if (zone->getCenter().z == pos.z) + distances.emplace_back(zone, metric(pos, vertex)); + else + distances.emplace_back(zone, std::numeric_limits::max()); + } } - auto zone = boost::min_element(distances, compareByDistance)->first; //closest tile belongs to zone - zone->area().add(pos); - map.setZoneID(pos, zone->getId()); + + auto closestZone = boost::min_element(distances, simpleCompareByDistance)->first; //closest tile belongs to zone + closestZone->area().add(pos); + map.setZoneID(pos, closestZone->getId()); } } } + //set position (town position) to center of mass of irregular zone for(const auto & zone : zones) { + if(zone.second->area().empty()) + throw rmgException("Empty zone after Penrose tiling"); + moveZoneToCenterOfMass(zone.second); //TODO: similiar for islands diff --git a/lib/rmg/CZonePlacer.h b/lib/rmg/CZonePlacer.h index 90b77b8f2..2eaf429ce 100644 --- a/lib/rmg/CZonePlacer.h +++ b/lib/rmg/CZonePlacer.h @@ -54,9 +54,7 @@ private: private: int width; int height; - //metric coeficients - float scaleX; - float scaleY; + //metric coeficient float mapSize; float gravityConstant; diff --git a/lib/rmg/PenroseTiling.cpp b/lib/rmg/PenroseTiling.cpp new file mode 100644 index 000000000..e6ee0e9f4 --- /dev/null +++ b/lib/rmg/PenroseTiling.cpp @@ -0,0 +1,165 @@ +/* + * PenroseTiling.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +// Adapted from https://github.com/mpizzzle/penrose by Michael Percival + +#include "StdInc.h" +#include "PenroseTiling.h" + +VCMI_LIB_NAMESPACE_BEGIN + + +Point2D Point2D::operator * (float scale) const +{ + return Point2D(x() * scale, y() * scale); +} + +Point2D Point2D::operator + (const Point2D& other) const +{ + return Point2D(x() + other.x(), y() + other.y()); +} + +bool Point2D::operator < (const Point2D& other) const +{ + if (x() < other.x()) + { + return true; + } + else + { + return y() < other.y(); + } +} + +Triangle::Triangle(bool t_123, const TIndices & inds): + tiling(t_123), + indices(inds) +{} + +Triangle::~Triangle() +{ + for (auto * triangle : subTriangles) + { + if (triangle) + { + delete triangle; + triangle = nullptr; + } + } +} + +Point2D Point2D::rotated(float radians) const +{ + float cosAngle = cos(radians); + float sinAngle = sin(radians); + + // Apply rotation matrix transformation + float newX = x() * cosAngle - y() * sinAngle; + float newY = x() * sinAngle + y() * cosAngle; + + return Point2D(newX, newY); +} + +void PenroseTiling::split(Triangle& p, std::vector& points, + std::array, 5>& indices, uint32_t depth) +{ + uint32_t s = points.size(); + TIndices& i = p.indices; + + const auto p2 = P2; + + if (depth > 0) + { + if (p.tiling ^ !p2) + { + points.push_back(Point2D((points[i[0]] * (1.0f - PHI) ) + (points[i[2]]) * PHI)); + points.push_back(Point2D((points[i[p2]] * (1.0f - PHI)) + (points[i[!p2]] * PHI))); + + auto * t1 = new Triangle(p2, TIndices({ i[(!p2) + 1], p2 ? i[2] : s, p2 ? s : i[1] })); + auto * t2 = new Triangle(true, TIndices({ p2 ? i[1] : s, s + 1, p2 ? s : i[1] })); + auto * t3 = new Triangle(false, TIndices({ s, s + 1, i[0] })); + + p.subTriangles = { t1, t2, t3 }; + } + else + { + points.push_back(Point2D((points[i[p2 * 2]] * (1.0f - PHI)) + (points[i[!p2]]) * PHI)); + + auto * t1 = new Triangle(true, TIndices({ i[2], s, i[1] })); + auto * t2 = new Triangle(false, TIndices({ i[(!p2) + 1], s, i[0] })); + + p.subTriangles = { t1, t2 }; + } + + for (auto& t : p.subTriangles) + { + if (depth == 1) + { + for (uint32_t k = 0; k < 3; ++k) + { + if (k != (t->tiling ^ !p2 ? 2 : 1)) + { + indices[indices.size() - 1].push_back(t->indices[k]); + indices[indices.size() - 1].push_back(t->indices[((k + 1) % 3)]); + } + } + + indices[t->tiling + (p.tiling ? 0 : 2)].insert(indices[t->tiling + (p.tiling ? 0 : 2)].end(), t->indices.begin(), t->indices.end()); + } + + // Split recursively + split(*t, points, indices, depth - 1); + } + } + + return; +} + +std::set PenroseTiling::generatePenroseTiling(size_t numZones, CRandomGenerator * rand) +{ + float scale = 100.f / (numZones * 1.5f + 20); + float polyAngle = (2 * PI_CONSTANT) / POLY; + + float randomAngle = rand->nextDouble(0, 2 * PI_CONSTANT); + + std::vector points = { Point2D(0.0f, 0.0f), Point2D(0.0f, 1.0f).rotated(randomAngle) }; + std::array, 5> indices; + + for (uint32_t i = 1; i < POLY; ++i) + { + Point2D next = points[i].rotated(polyAngle); + points.push_back(next); + } + + for (auto& p : points) + { + p.x(p.x() * scale * BASE_SIZE); + } + + std::set finalPoints; + + for (uint32_t i = 0; i < POLY; i++) + { + std::array p = { (i % (POLY + 1)) + 1, ((i + 1) % POLY) + 1 }; + + Triangle t(true, TIndices({ 0, p[i & 1], p[!(i & 1)] })); + + split(t, points, indices, DEPTH); + } + + vstd::copy_if(points, vstd::set_inserter(finalPoints), [](const Point2D point) + { + return vstd::isbetween(point.x(), 0.f, 1.0f) && vstd::isbetween(point.y(), 0.f, 1.0f); + }); + + return finalPoints; +} + +VCMI_LIB_NAMESPACE_END \ No newline at end of file diff --git a/lib/rmg/PenroseTiling.h b/lib/rmg/PenroseTiling.h new file mode 100644 index 000000000..bdda4c7c9 --- /dev/null +++ b/lib/rmg/PenroseTiling.h @@ -0,0 +1,71 @@ +/* + * PenroseTiling.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#pragma once + +#include "../GameConstants.h" +#include "../CRandomGenerator.h" +#include +#include + +VCMI_LIB_NAMESPACE_BEGIN + +using namespace boost::geometry; +typedef std::array TIndices; + +const float PI_CONSTANT = 3.141592f; + +class Point2D : public model::d2::point_xy +{ +public: + using point_xy::point_xy; + + Point2D operator * (float scale) const; + Point2D operator + (const Point2D& other) const; + Point2D rotated(float radians) const; + + bool operator < (const Point2D& other) const; +}; + +Point2D rotatePoint(const Point2D& point, double radians, const Point2D& origin); + +class Triangle +{ +public: + ~Triangle(); + + const bool tiling; + TIndices indices; + + std::vector subTriangles; + + Triangle(bool t_123, const TIndices & inds); +}; + +class PenroseTiling +{ + +public: + const float PHI = 1.0 / ((1.0 + std::sqrt(5.0)) / 2); + const uint32_t POLY = 10; // Number of symmetries? + + const float BASE_SIZE = 1.25f; + const uint32_t DEPTH = 7; //Recursion depth + + const bool P2 = false; // Tiling type + + std::set generatePenroseTiling(size_t numZones, CRandomGenerator * rand); + +private: + void split(Triangle& p, std::vector& points, std::array, 5>& indices, uint32_t depth); + +}; + +VCMI_LIB_NAMESPACE_END \ No newline at end of file