351 lines
11 KiB
C++
351 lines
11 KiB
C++
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
|
|
#include "procgen/GeneratorRegistry.h"
|
|
#include "procgen/Grid2D.h"
|
|
#include "procgen/MeshBuild.h"
|
|
#include "procgen/Params.h"
|
|
#include "procgen/Semantic.h"
|
|
#include "procgen/algorithms/MarchingCubes.h"
|
|
#include "procgen/urban/UrbanGenerator.h"
|
|
#include "procgen/urban/UrbanGeometry.h"
|
|
#include "procgen/urban/UrbanOutput.h"
|
|
#include "procgen/urban/UrbanTypes.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <set>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
using namespace eve::procgen;
|
|
using namespace eve::procgen::urban;
|
|
|
|
namespace {
|
|
|
|
Params makeParams(uint32_t seed, const std::string& land, int targetParcels = 120) {
|
|
Params p;
|
|
p.setSeed(seed);
|
|
p.setString("land", land);
|
|
p.setFloat("landWidth", 100.f);
|
|
p.setFloat("landHeight", 60.f);
|
|
p.setFloat("minParcelArea", 4.f);
|
|
p.setInt("targetParcels", targetParcels);
|
|
return p;
|
|
}
|
|
|
|
bool layoutAllParcelsReachable(const UrbanLayout& l) {
|
|
std::set<std::pair<int, int>> street;
|
|
for (const auto& e : l.streetSegments) {
|
|
const int a = e.first, b = e.second;
|
|
street.insert({std::min(a, b), std::max(a, b)});
|
|
}
|
|
for (const Parcel& parcel : l.parcels) {
|
|
const size_t m = parcel.ring.size();
|
|
bool reachable = false;
|
|
for (size_t i = 0; i < m; ++i) {
|
|
const int a = parcel.ring[i];
|
|
const int b = parcel.ring[(i + 1) % m];
|
|
if (street.count({std::min(a, b), std::max(a, b)})) {
|
|
reachable = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!reachable) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool streetNetworkConnected(const UrbanLayout& l) {
|
|
if (l.streetSegments.empty()) return false;
|
|
std::vector<std::vector<int>> adj(l.corners.size());
|
|
for (const auto& e : l.streetSegments) {
|
|
adj[size_t(e.first)].push_back(e.second);
|
|
adj[size_t(e.second)].push_back(e.first);
|
|
}
|
|
int start = l.streetSegments[0].first;
|
|
std::vector<char> seen(l.corners.size(), 0);
|
|
std::vector<int> q{start};
|
|
seen[size_t(start)] = 1;
|
|
int reached = 0;
|
|
for (size_t qi = 0; qi < q.size(); ++qi) {
|
|
const int v = q[qi];
|
|
++reached;
|
|
for (const int u : adj[size_t(v)]) {
|
|
if (!seen[size_t(u)]) {
|
|
seen[size_t(u)] = 1;
|
|
q.push_back(u);
|
|
}
|
|
}
|
|
}
|
|
for (const auto& e : l.streetSegments) {
|
|
if (!seen[size_t(e.first)] || !seen[size_t(e.second)]) return false;
|
|
}
|
|
return reached >= 2;
|
|
}
|
|
|
|
double layoutCoveredArea(const UrbanLayout& l) {
|
|
double sum = 0.0;
|
|
for (const Parcel& parcel : l.parcels) {
|
|
Polygon poly;
|
|
for (const int c : parcel.ring) poly.push_back(l.corners[size_t(c)]);
|
|
sum += area(poly);
|
|
}
|
|
return sum;
|
|
}
|
|
|
|
int countQuads(const UrbanLayout& l) {
|
|
int n = 0;
|
|
for (const Parcel& parcel : l.parcels) {
|
|
Polygon poly;
|
|
for (const int c : parcel.ring) poly.push_back(l.corners[size_t(c)]);
|
|
if (approximatePolygon(poly).size() == 4) ++n;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
bool meshValid(const MeshBuild& m) {
|
|
if (m.getVertexCount() <= 0 || m.getIndexCount() <= 0) return false;
|
|
for (int i = 0; i < m.getIndexCount(); ++i) {
|
|
if (m.getIndex(i) < 0 || m.getIndex(i) >= m.getVertexCount()) return false;
|
|
}
|
|
for (int i = 0; i < m.getVertexCount(); ++i) {
|
|
if (!std::isfinite(m.getPositionX(i)) || !std::isfinite(m.getPositionY(i)) || !std::isfinite(m.getPositionZ(i)))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("urban.geometry.irregularityMetric") {
|
|
const Polygon square = {{0, 0}, {10, 0}, {10, 10}, {0, 10}};
|
|
const Polygon rect = {{0, 0}, {20, 0}, {20, 5}, {0, 5}};
|
|
const Polygon tri = {{0, 0}, {10, 0}, {0, 10}};
|
|
const double iSquare = shapeIrregularity(square);
|
|
const double iRect = shapeIrregularity(rect);
|
|
const double iTri = shapeIrregularity(tri);
|
|
CHECK(iSquare < 1e-6);
|
|
CHECK(iRect > iSquare);
|
|
CHECK(iTri > iRect);
|
|
}
|
|
|
|
TEST_CASE("urban.geometry.splitRect") {
|
|
const Polygon rect = {{0, 0}, {20, 0}, {20, 10}, {0, 10}};
|
|
const Polyline split = {{10, 0}, {10, 10}};
|
|
BoundaryPosition pa, pb;
|
|
closestPointOnBoundary(rect, split.front(), &pa);
|
|
closestPointOnBoundary(rect, split.back(), &pb);
|
|
Polygon a, b;
|
|
REQUIRE(splitPolygonByPolyline(rect, split, pa, pb, a, b));
|
|
CHECK(std::fabs(area(a) + area(b) - area(rect)) < 1e-6);
|
|
CHECK(area(a) > 0.0);
|
|
CHECK(area(b) > 0.0);
|
|
CHECK(polygonIsSimple(a));
|
|
CHECK(polygonIsSimple(b));
|
|
}
|
|
|
|
TEST_CASE("urban.generator.rect.reachableAndRegular") {
|
|
UrbanOptions opts;
|
|
opts.land = {{0, 0}, {100, 0}, {100, 60}, {0, 60}};
|
|
opts.seed = 42;
|
|
opts.targetParcels = 120;
|
|
UrbanGenerator gen(opts);
|
|
std::string err;
|
|
REQUIRE(gen.generate(&err));
|
|
const UrbanLayout& l = gen.layout();
|
|
REQUIRE(l.parcels.size() >= 32);
|
|
REQUIRE(layoutAllParcelsReachable(l));
|
|
REQUIRE(streetNetworkConnected(l));
|
|
REQUIRE(std::fabs(layoutCoveredArea(l) - area(opts.land)) < 0.03 * area(opts.land));
|
|
REQUIRE(double(countQuads(l)) / double(l.parcels.size()) > 0.5);
|
|
CHECK(l.avgIrregularity < 0.4);
|
|
CHECK(l.streetJunctions >= 0);
|
|
CHECK(l.totalStreetLength > 0.0);
|
|
}
|
|
|
|
TEST_CASE("urban.generator.deterministic") {
|
|
UrbanOptions a;
|
|
a.land = {{0, 0}, {100, 0}, {100, 60}, {0, 60}};
|
|
a.seed = 7;
|
|
a.targetParcels = 64;
|
|
UrbanOptions b = a;
|
|
UrbanGenerator g1(a), g2(b);
|
|
std::string err;
|
|
REQUIRE(g1.generate(&err));
|
|
REQUIRE(g2.generate(&err));
|
|
const UrbanLayout& l1 = g1.layout();
|
|
const UrbanLayout& l2 = g2.layout();
|
|
CHECK_EQ(l1.parcels.size(), l2.parcels.size());
|
|
CHECK_EQ(l1.streets.size(), l2.streets.size());
|
|
REQUIRE(l1.corners.size() == l2.corners.size());
|
|
for (size_t i = 0; i < l1.corners.size(); ++i) CHECK(l1.corners[i] == l2.corners[i]);
|
|
}
|
|
|
|
TEST_CASE("urban.generator.streetPatterns") {
|
|
auto run = [&](int pattern, int boundaryMode) {
|
|
UrbanOptions opts;
|
|
opts.land = {{0, 0}, {100, 0}, {100, 60}, {0, 60}};
|
|
opts.seed = 11;
|
|
opts.targetParcels = 64;
|
|
opts.streetPattern = pattern;
|
|
opts.boundaryStreetMode = boundaryMode;
|
|
UrbanGenerator gen(opts);
|
|
std::string err;
|
|
REQUIRE(gen.generate(&err));
|
|
return gen.layout();
|
|
};
|
|
const UrbanLayout loop = run(1, 0);
|
|
const UrbanLayout tree = run(3, 1);
|
|
CHECK_EQ(loop.streetEnds, 0);
|
|
CHECK(tree.streetEnds >= 1);
|
|
CHECK(layoutAllParcelsReachable(tree));
|
|
CHECK(streetNetworkConnected(tree));
|
|
}
|
|
|
|
TEST_CASE("urban.generator.irregularLand") {
|
|
const std::vector<Polygon> lands = {
|
|
{{0, 0}, {100, 0}, {50, 60}}, // triangle
|
|
{{0, 0}, {100, 0}, {100, 33}, {45, 33}, {45, 60}, {0, 60}}, // L
|
|
};
|
|
for (const Polygon& land : lands) {
|
|
UrbanOptions opts;
|
|
opts.land = land;
|
|
opts.seed = 3;
|
|
opts.targetParcels = 80;
|
|
UrbanGenerator gen(opts);
|
|
std::string err;
|
|
REQUIRE(gen.generate(&err));
|
|
const UrbanLayout& l = gen.layout();
|
|
CHECK(l.parcels.size() >= 16);
|
|
CHECK(layoutAllParcelsReachable(l));
|
|
CHECK(streetNetworkConnected(l));
|
|
}
|
|
}
|
|
|
|
TEST_CASE("urban.generator.weightsChangeLayout") {
|
|
auto run = [&](float lr, float la) {
|
|
UrbanOptions opts;
|
|
opts.land = {{0, 0}, {100, 0}, {100, 60}, {0, 60}};
|
|
opts.seed = 9;
|
|
opts.targetParcels = 64;
|
|
opts.lambdaRegu = lr;
|
|
opts.lambdaAcce = la;
|
|
UrbanGenerator gen(opts);
|
|
std::string err;
|
|
REQUIRE(gen.generate(&err));
|
|
return gen.layout();
|
|
};
|
|
const UrbanLayout regular = run(0.9f, 0.05f);
|
|
const UrbanLayout access = run(0.1f, 0.9f);
|
|
CHECK(regular.corners != access.corners);
|
|
}
|
|
|
|
TEST_CASE("urban.grid.rasterAndSemantics") {
|
|
Params p = makeParams(42, "rect", 64);
|
|
Grid2D grid;
|
|
std::string err;
|
|
REQUIRE(generateUrbanGrid(p, grid, err));
|
|
CHECK(grid.getWidth() > 10);
|
|
CHECK(grid.getHeight() > 10);
|
|
int floor = 0, road = 0, wall = 0;
|
|
std::set<int> details;
|
|
for (int y = 0; y < grid.getHeight(); ++y) {
|
|
for (int x = 0; x < grid.getWidth(); ++x) {
|
|
const int c = grid.getCell(x, y);
|
|
if (c == int(Semantic::Floor))
|
|
++floor;
|
|
else if (c == int(Semantic::Road))
|
|
++road;
|
|
else if (c == int(Semantic::Wall))
|
|
++wall;
|
|
if (grid.getDetail(x, y) > 0) details.insert(grid.getDetail(x, y));
|
|
}
|
|
}
|
|
CHECK(floor > 200);
|
|
CHECK(road > 10);
|
|
CHECK(wall > 0);
|
|
CHECK(details.size() >= 32);
|
|
CHECK(grid.getObjectCount() >= 32);
|
|
CHECK_EQ(grid.getMeta("algorithm", ""), "urban.parcels");
|
|
|
|
// Determinism through the public registry.
|
|
Grid2D g2;
|
|
REQUIRE(generateUrbanGrid(p, g2, err));
|
|
CHECK(g2.cells() == grid.cells());
|
|
GeneratorRegistry::instance().registerBuiltins();
|
|
CHECK(GeneratorRegistry::instance().has("urban.parcels"));
|
|
}
|
|
|
|
TEST_CASE("urban.mesh.flatAndExtruded") {
|
|
Params p = makeParams(42, "rect", 48);
|
|
MeshBuild flat;
|
|
std::string err;
|
|
REQUIRE(generateUrbanMesh(p, flat, err));
|
|
CHECK(meshValid(flat));
|
|
CHECK(flat.getVertexCount() > 100);
|
|
CHECK(flat.getIndexCount() % 3 == 0);
|
|
CHECK_EQ(flat.getMeta("algorithm", ""), "mesh.urban");
|
|
|
|
p.setFloat("extrude", 5.f);
|
|
MeshBuild extruded;
|
|
REQUIRE(generateUrbanMesh(p, extruded, err));
|
|
CHECK(meshValid(extruded));
|
|
CHECK(extruded.getVertexCount() > flat.getVertexCount());
|
|
|
|
MeshBuild again;
|
|
p.setFloat("extrude", 0.f);
|
|
REQUIRE(generateUrbanMesh(p, again, err));
|
|
CHECK(again.positions() == flat.positions());
|
|
CHECK(again.indices() == flat.indices());
|
|
MeshRecipeRegistry::instance().registerBuiltins();
|
|
CHECK(MeshRecipeRegistry::instance().has("mesh.urban"));
|
|
}
|
|
|
|
TEST_CASE("urban.generator.orientation") {
|
|
auto run = [&](int orientation) {
|
|
UrbanOptions opts;
|
|
opts.land = {{0, 0}, {100, 0}, {100, 60}, {0, 60}};
|
|
opts.seed = 5;
|
|
opts.targetParcels = 48;
|
|
opts.orientation = orientation;
|
|
opts.lambdaOrient = 1.0;
|
|
UrbanGenerator gen(opts);
|
|
std::string err;
|
|
REQUIRE(gen.generate(&err));
|
|
return gen.layout();
|
|
};
|
|
const UrbanLayout ew = run(1);
|
|
const UrbanLayout ns = run(2);
|
|
CHECK(ew.corners != ns.corners);
|
|
CHECK(layoutAllParcelsReachable(ew));
|
|
CHECK(layoutAllParcelsReachable(ns));
|
|
}
|
|
|
|
TEST_CASE("urban.generator.optimizeCanBeDisabled") {
|
|
UrbanOptions on;
|
|
on.land = {{0, 0}, {100, 0}, {100, 60}, {0, 60}};
|
|
on.seed = 13;
|
|
on.targetParcels = 64;
|
|
on.optimize = true;
|
|
UrbanOptions off = on;
|
|
off.optimize = false;
|
|
UrbanGenerator g1(on), g2(off);
|
|
std::string err;
|
|
REQUIRE(g1.generate(&err));
|
|
REQUIRE(g2.generate(&err));
|
|
CHECK(layoutAllParcelsReachable(g1.layout()));
|
|
CHECK(layoutAllParcelsReachable(g2.layout()));
|
|
CHECK(streetNetworkConnected(g1.layout()));
|
|
}
|
|
|
|
TEST_CASE("urban.grid.badParams") {
|
|
Params p = makeParams(1, "rect");
|
|
p.setFloat("minParcelArea", 0.f);
|
|
Grid2D grid;
|
|
std::string err;
|
|
CHECK(!generateUrbanGrid(p, grid, err));
|
|
CHECK(!err.empty());
|
|
}
|