From c2421c3ac2018faf5d47205ee979acec181d9672 Mon Sep 17 00:00:00 2001 From: Dan Goodliffe Date: Sun, 2 Jan 2022 18:04:00 +0000 Subject: Separate geographic data (GeoData) from its visual representation(s) (Terrain) --- application/main.cpp | 6 ++- game/gamestate.h | 3 ++ game/geoData.cpp | 106 +++++++++++++++++++++++++++++++++++++ game/geoData.h | 36 +++++++++++++ game/terrain.cpp | 145 +++++++++++++++++---------------------------------- game/terrain.h | 12 ++--- test/Jamfile.jam | 1 + test/test-geo.cpp | 71 +++++++++++++++++++++++++ 8 files changed, 275 insertions(+), 105 deletions(-) create mode 100644 game/geoData.cpp create mode 100644 game/geoData.h create mode 100644 test/test-geo.cpp diff --git a/application/main.cpp b/application/main.cpp index 128c9c1..eef0ed0 100644 --- a/application/main.cpp +++ b/application/main.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -54,10 +55,13 @@ public: int run() { + geoData = std::make_shared(GeoData::Limits {{-120, -120}, {120, 120}}, 10.F); + geoData->generateRandom(); + Windows windows; windows.create(DISPLAY_WIDTH, DISPLAY_HEIGHT, this); - world.create(); + world.create(geoData); { auto rl = world.create(); diff --git a/game/gamestate.h b/game/gamestate.h index b8dfa61..536c420 100644 --- a/game/gamestate.h +++ b/game/gamestate.h @@ -2,9 +2,11 @@ #define GAMESTATE_H #include +#include #include class WorldObject; +class GeoData; class GameState { public: @@ -14,6 +16,7 @@ public: NO_COPY(GameState); Collection world; + std::shared_ptr geoData; }; extern GameState * gameState; diff --git a/game/geoData.cpp b/game/geoData.cpp new file mode 100644 index 0000000..55c69a1 --- /dev/null +++ b/game/geoData.cpp @@ -0,0 +1,106 @@ +#include "geoData.h" +#include "gfx/image.h" +#include +#include +#include +#include +#include +#include + +GeoData::GeoData(Limits l, float s) : + limit {std::move(l)}, size {(limit.second - limit.first) + 1}, scale {s}, nodes {[this]() { + return (static_cast(size.x * size.y)); + }()} +{ +} + +void +GeoData::generateRandom() +{ + // We acknowledge this is terrible :) + + // Add hills + std::mt19937 gen(std::random_device {}()); + std::uniform_int_distribution<> rxpos(limit.first.x + 2, limit.second.x - 2), + rypos(limit.first.y + 2, limit.second.y - 2); + std::uniform_int_distribution<> rsize(10, 30); + std::uniform_real_distribution rheight(1, 3); + for (int h = 0; h < 500;) { + const glm::ivec2 hpos {rxpos(gen), rypos(gen)}; + const glm::ivec2 hsize {rsize(gen), rsize(gen)}; + if (const auto lim1 = hpos - hsize; lim1.x > limit.first.x && lim1.y > limit.first.y) { + if (const auto lim2 = hpos + hsize; lim2.x < limit.second.x && lim2.y < limit.second.y) { + const auto height = rheight(gen); + const glm::ivec2 hsizesqrd {hsize.x * hsize.x, hsize.y * hsize.y}; + for (auto y = lim1.y; y < lim2.y; y += 1) { + for (auto x = lim1.x; x < lim2.x; x += 1) { + const auto dist {hpos - glm::ivec2 {x, y}}; + const glm::ivec2 distsqrd {dist.x * dist.x, dist.y * dist.y}; + const auto out {rdiv(sq(x - hpos.x), sq(hsize.x)) + rdiv(sq(y - hpos.y), sq(hsize.y))}; + if (out <= 1.0F) { + auto & node {nodes[at({x, y})]}; + const auto m {1.F / (7.F * out - 8.F) + 1.F}; + node.height += height * m; + } + } + } + h += 1; + } + } + } +} + +void +GeoData::loadFromImages(const std::filesystem::path & fileName, float scale_) +{ + const Image map {fileName.c_str(), STBI_grey}; + size = {map.width, map.height}; + limit = {{0, 0}, size - glm::uvec2 {1, 1}}; + const auto points {size.x * size.y}; + scale = scale_; + nodes.resize(points); + + std::transform(map.data.data(), map.data.data() + points, nodes.begin(), [](auto d) { + return Node {(d * 0.1F) - 1.5F}; + }); +} + +unsigned int +GeoData::at(glm::ivec2 coord) const +{ + if (coord.x < limit.first.x || coord.x > limit.second.x || coord.y < limit.first.y || coord.y > limit.second.y) { + throw std::range_error {"Coordinates outside GeoData limits"}; + } + const glm::uvec2 offset = coord - limit.first; + return offset.x + (offset.y * size.x); +} + +unsigned int +GeoData::at(int x, int y) const +{ + return at({x, y}); +} + +GeoData::Limits +GeoData::getLimit() const +{ + return limit; +} + +float +GeoData::getScale() const +{ + return scale; +} + +glm::uvec2 +GeoData::getSize() const +{ + return size; +} + +std::span +GeoData::getNodes() const +{ + return nodes; +} diff --git a/game/geoData.h b/game/geoData.h new file mode 100644 index 0000000..39c149c --- /dev/null +++ b/game/geoData.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include +#include +#include + +class GeoData { +public: + struct Node { + float height {-1.5F}; + }; + + using Limits = std::pair; + + GeoData() = default; + explicit GeoData(Limits limit, float scale = 10.F); + + void generateRandom(); + void loadFromImages(const std::filesystem::path &, float scale); + + [[nodiscard]] unsigned int at(glm::ivec2) const; + [[nodiscard]] unsigned int at(int x, int y) const; + + [[nodiscard]] Limits getLimit() const; + [[nodiscard]] glm::uvec2 getSize() const; + [[nodiscard]] float getScale() const; + [[nodiscard]] std::span getNodes() const; + +protected: + Limits limit {}; // Base grid limits first(x,y) -> second(x,y) + glm::uvec2 size {}; + float scale {1}; + std::vector nodes; +}; diff --git a/game/terrain.cpp b/game/terrain.cpp index 3089f3a..10a6215 100644 --- a/game/terrain.cpp +++ b/game/terrain.cpp @@ -1,5 +1,8 @@ #include "terrain.h" +#include "game/geoData.h" #include "gfx/models/texture.h" +#include +#include #include #include #include @@ -8,116 +11,66 @@ #include #include #include +#include #include #include -#include -#include +#include +#include -Terrain::Terrain() : grass {Texture::cachedTexture.get("grass.png")}, water {Texture::cachedTexture.get("water.png")} +Terrain::Terrain(std::shared_ptr gd) : + geoData {std::move(gd)}, grass {Texture::cachedTexture.get("grass.png")}, water {Texture::cachedTexture.get( + "water.png")} { - constexpr auto size {241}; // Vertices - constexpr auto offset {(size - 1) / 2}; - constexpr auto verticesCount = size * size; - constexpr auto resolution = 10; // Grid size - - std::vector vertices; - vertices.reserve(verticesCount + 4); - vertices.resize(verticesCount, {{}, {}, {}}); - - // Initial coordinates - for (auto y = 0U; y < size; y += 1) { - for (auto x = 0U; x < size; x += 1) { - auto & vertex = vertices[x + (y * size)]; - vertex.pos - = {resolution * (static_cast(x) - offset), resolution * (static_cast(y) - offset), -1.5}; - vertex.normal = up; - vertex.texCoord = {x, y}; - } - } - // Add hills - std::mt19937 gen(std::random_device {}()); - std::uniform_int_distribution<> rpos(2, size - 2); - std::uniform_int_distribution<> rsize(10, 30); - std::uniform_real_distribution rheight(1, 3); - for (int h = 0; h < 500;) { - const glm::ivec2 hpos {rpos(gen), rpos(gen)}; - const glm::ivec2 hsize {rsize(gen), rsize(gen)}; - if (const auto lim1 = hpos - hsize; lim1.x > 0 && lim1.y > 0) { - if (const auto lim2 = hpos + hsize; lim2.x < size && lim2.y < size) { - const auto height = rheight(gen); - const glm::ivec2 hsizesqrd {hsize.x * hsize.x, hsize.y * hsize.y}; - for (auto y = lim1.y; y < lim2.y; y += 1) { - for (auto x = lim1.x; x < lim2.x; x += 1) { - const auto dist {hpos - glm::ivec2 {x, y}}; - const glm::ivec2 distsqrd {dist.x * dist.x, dist.y * dist.y}; - const auto out {rdiv(sq(x - hpos.x), sq(hsize.x)) + rdiv(sq(y - hpos.y), sq(hsize.y))}; - if (out <= 1.0F) { - auto & vertex - = vertices[static_cast(x) + (static_cast(y) * size)]; - const auto m {1.F / (7.F * out - 8.F) + 1.F}; - vertex.pos.z += height * m; - } - } - } - h += 1; - } - } - } - finish(size, size, vertices); + generateMeshes(); } -Terrain::Terrain(const std::string & fileName) : - grass {Texture::cachedTexture.get("grass.png")}, water {Texture::cachedTexture.get("water.png")} +void +Terrain::generateMeshes() { - constexpr auto resolution {100}; - - const Image map {fileName.c_str(), STBI_grey}; - - std::vector vertices; - vertices.reserve((map.width * map.height) + 4); + std::vector indices; + const auto isize = geoData->getSize() - glm::uvec2 {1, 1}; + indices.reserve(static_cast(isize.x * isize.y) * 6); - for (auto y = 0U; y < map.height; y += 1) { - for (auto x = 0U; x < map.width; x += 1) { - vertices.emplace_back(glm::vec3 {resolution * (x - (map.width / 2)), resolution * (y - (map.height / 2)), - (static_cast(map.data[x + (y * map.width)]) * 0.1F) - 1.5F}, - glm::vec2 {(x % 2) / 2.01, (y % 2) / 2.01}, up); + const auto limit = geoData->getLimit(); + // Indices + constexpr std::array indices_offsets {{ + {0, 0}, + {1, 0}, + {1, 1}, + {0, 0}, + {1, 1}, + {0, 1}, + }}; + for (auto y = limit.first.y; y < limit.second.y; y += 1) { + for (auto x = limit.first.x; x < limit.second.x; x += 1) { + std::transform(indices_offsets.begin(), indices_offsets.end(), std::back_inserter(indices), + [this, x, y](const auto off) { + return geoData->at(x + off.x, y + off.y); + }); } } - finish(map.width, map.height, vertices); -} - -void -Terrain::finish(unsigned int width, unsigned int height, std::vector & vertices) -{ - const auto tilesCount = (width - 1) * (height - 1); - const auto trianglesCount = tilesCount * 2; - const auto indicesCount = trianglesCount * 3; - std::vector indices; - indices.reserve(indicesCount + 6); - // Indices - for (auto y = 0U; y < height - 1; y += 1) { - for (auto x = 0U; x < width - 1; x += 1) { - indices.push_back(x + (y * width)); - indices.push_back((x + 1) + (y * width)); - indices.push_back((x + 1) + ((y + 1) * width)); - indices.push_back(x + (y * width)); - indices.push_back((x + 1) + ((y + 1) * width)); - indices.push_back(x + ((y + 1) * width)); + const auto nodes = geoData->getNodes(); + const auto scale = geoData->getScale(); + std::vector vertices; + vertices.reserve(nodes.size()); + // Positions + for (auto y = limit.first.y; y <= limit.second.y; y += 1) { + for (auto x = limit.first.x; x <= limit.second.x; x += 1) { + const glm::vec2 xy {x, y}; + vertices.emplace_back((xy * scale) ^ nodes[geoData->at(x, y)].height, xy, ::up); } } // Normals - auto v = [&vertices](unsigned int width, unsigned int x, unsigned int y) -> Vertex & { - return vertices[x + (y * width)]; - }; - - for (auto y = 1U; y < height - 1; y += 1) { - for (auto x = 1U; x < width - 1; x += 1) { - const auto a = v(width, x - 1, y).pos; - const auto b = v(width, x, y - 1).pos; - const auto c = v(width, x + 1, y).pos; - const auto d = v(width, x, y + 1).pos; - v(width, x, y).normal = -glm::normalize(glm::cross(b - d, a - c)); + const glm::uvec2 size = geoData->getSize(); + for (auto y = limit.first.y + 1; y < limit.second.y; y += 1) { + for (auto x = limit.first.x + 1; x < limit.second.x; x += 1) { + const auto n {geoData->at(x, y)}; + const auto a = vertices[n - 1].pos; + const auto b = vertices[n - size.x].pos; + const auto c = vertices[n + 1].pos; + const auto d = vertices[n + size.x].pos; + vertices[n].normal = -glm::normalize(glm::cross(b - d, a - c)); } } meshes.create(vertices, indices); diff --git a/game/terrain.h b/game/terrain.h index c32f092..4775dff 100644 --- a/game/terrain.h +++ b/game/terrain.h @@ -7,17 +7,14 @@ #include #include #include -#include -#include class Shader; class Texture; -class Vertex; +class GeoData; class Terrain : public WorldObject, public Renderable { public: - Terrain(); - explicit Terrain(const std::string &); + explicit Terrain(std::shared_ptr); void render(const Shader & shader) const override; @@ -25,10 +22,9 @@ public: float waveCycle {0.F}; private: - static constexpr unsigned int NUM_BUFFERS {4}; - - void finish(unsigned int width, unsigned int height, std::vector &); + void generateMeshes(); + std::shared_ptr geoData; Collection meshes; std::shared_ptr grass, water; }; diff --git a/test/Jamfile.jam b/test/Jamfile.jam index e487044..4f5b920 100644 --- a/test/Jamfile.jam +++ b/test/Jamfile.jam @@ -23,6 +23,7 @@ run test-collection.cpp ; run test-obj.cpp ; run test-maths.cpp ; run test-lib.cpp ; +run test-geo.cpp ; run test-network.cpp ; run test-persistence.cpp : -- : [ sequence.insertion-sort [ glob fixtures/json/*.json fixtures/json/bad/*.json ] ] ; run test-text.cpp ; diff --git a/test/test-geo.cpp b/test/test-geo.cpp new file mode 100644 index 0000000..b6f276a --- /dev/null +++ b/test/test-geo.cpp @@ -0,0 +1,71 @@ +#define BOOST_TEST_MODULE test_geo + +#include "test-helpers.hpp" +#include +#include +#include + +#include + +struct TestGeoData : public GeoData { + TestGeoData() : GeoData {{{-10, -5}, {30, 40}}, 5.F} { } +}; + +BOOST_FIXTURE_TEST_CASE(initialize, TestGeoData) +{ + BOOST_CHECK_EQUAL(limit.first, glm::ivec2(-10, -5)); + BOOST_CHECK_EQUAL(limit.second, glm::ivec2(30, 40)); + BOOST_CHECK_EQUAL(scale, 5.F); + BOOST_CHECK_EQUAL(size, glm::uvec2(41, 46)); + BOOST_CHECK_EQUAL(nodes.size(), 1886); + BOOST_CHECK(std::all_of(nodes.begin(), nodes.end(), [](const auto & n) { + return n.height == -1.5F; + })); +} + +BOOST_FIXTURE_TEST_CASE(coords, TestGeoData) +{ + BOOST_CHECK_EQUAL(at(-10, -5), 0); + BOOST_CHECK_EQUAL(at(-9, -5), 1); + BOOST_CHECK_EQUAL(at(0, -5), 10); + BOOST_CHECK_EQUAL(at(30, -5), 40); + BOOST_CHECK_EQUAL(at(30, 40), 1885); +} + +BOOST_FIXTURE_TEST_CASE(coords_bad, TestGeoData) +{ + BOOST_CHECK_THROW(std::ignore = at(-11, -5), std::range_error); + BOOST_CHECK_THROW(std::ignore = at(-10, -6), std::range_error); + BOOST_CHECK_THROW(std::ignore = at(-11, -6), std::range_error); + BOOST_CHECK_THROW(std::ignore = at(31, 40), std::range_error); + BOOST_CHECK_THROW(std::ignore = at(30, 41), std::range_error); + BOOST_CHECK_THROW(std::ignore = at(31, 41), std::range_error); +} + +BOOST_FIXTURE_TEST_CASE(gen_random, TestGeoData) +{ + // Can only really its sanity + generateRandom(); + // Some terrain above sea level + BOOST_CHECK(std::any_of(nodes.begin(), nodes.end(), [](const auto & n) { + return n.height > 0; + })); + // Still an island + for (int x = limit.first.x; x <= limit.second.x; x += 1) { + BOOST_CHECK_EQUAL(nodes[at(x, limit.first.y)].height, -1.5F); + BOOST_CHECK_EQUAL(nodes[at(x, limit.second.y)].height, -1.5F); + } + for (int y = limit.first.y; y <= limit.second.y; y += 1) { + BOOST_CHECK_EQUAL(nodes[at(limit.first.x, y)].height, -1.5F); + BOOST_CHECK_EQUAL(nodes[at(limit.second.x, y)].height, -1.5F); + } +} + +BOOST_FIXTURE_TEST_CASE(load_uk_heightmap, TestGeoData) +{ + loadFromImages(FIXTURESDIR "/height/V0txo.jpg", 100.F); + // Some terrain above sea level + BOOST_CHECK(std::any_of(nodes.begin(), nodes.end(), [](const auto & n) { + return n.height > 0; + })); +} -- cgit v1.2.3