summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--application/main.cpp6
-rw-r--r--game/gamestate.h3
-rw-r--r--game/geoData.cpp106
-rw-r--r--game/geoData.h36
-rw-r--r--game/terrain.cpp145
-rw-r--r--game/terrain.h12
-rw-r--r--test/Jamfile.jam1
-rw-r--r--test/test-geo.cpp71
8 files changed, 275 insertions, 105 deletions
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 <game/activities/idle.h>
#include <game/activity.h>
#include <game/gamestate.h>
+#include <game/geoData.h>
#include <game/network/link.h>
#include <game/network/rail.h>
#include <game/objective.h>
@@ -54,10 +55,13 @@ public:
int
run()
{
+ geoData = std::make_shared<GeoData>(GeoData::Limits {{-120, -120}, {120, 120}}, 10.F);
+ geoData->generateRandom();
+
Windows windows;
windows.create<GameMainWindow>(DISPLAY_WIDTH, DISPLAY_HEIGHT, this);
- world.create<Terrain>();
+ world.create<Terrain>(geoData);
{
auto rl = world.create<RailLinks>();
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 <collection.hpp>
+#include <memory>
#include <special_members.hpp>
class WorldObject;
+class GeoData;
class GameState {
public:
@@ -14,6 +16,7 @@ public:
NO_COPY(GameState);
Collection<WorldObject> world;
+ std::shared_ptr<GeoData> 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 <algorithm>
+#include <cstddef>
+#include <maths.h>
+#include <random>
+#include <stb/stb_image.h>
+#include <stdexcept>
+
+GeoData::GeoData(Limits l, float s) :
+ limit {std::move(l)}, size {(limit.second - limit.first) + 1}, scale {s}, nodes {[this]() {
+ return (static_cast<std::size_t>(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<float> 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<const GeoData::Node>
+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 <filesystem>
+#include <glm/glm.hpp>
+#include <span>
+#include <utility>
+#include <vector>
+
+class GeoData {
+public:
+ struct Node {
+ float height {-1.5F};
+ };
+
+ using Limits = std::pair<glm::ivec2, glm::ivec2>;
+
+ 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<const Node> getNodes() const;
+
+protected:
+ Limits limit {}; // Base grid limits first(x,y) -> second(x,y)
+ glm::uvec2 size {};
+ float scale {1};
+ std::vector<Node> 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 <algorithm>
+#include <array>
#include <cache.h>
#include <cstddef>
#include <filesystem>
@@ -8,116 +11,66 @@
#include <gfx/models/mesh.h>
#include <gfx/models/vertex.hpp>
#include <glm/glm.hpp>
+#include <iterator>
#include <location.hpp>
#include <maths.h>
-#include <random>
-#include <stb/stb_image.h>
+#include <utility>
+#include <vector>
-Terrain::Terrain() : grass {Texture::cachedTexture.get("grass.png")}, water {Texture::cachedTexture.get("water.png")}
+Terrain::Terrain(std::shared_ptr<GeoData> 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<Vertex> 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<int>(x) - offset), resolution * (static_cast<int>(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<float> 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<std::size_t>(x) + (static_cast<std::size_t>(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<Vertex> vertices;
- vertices.reserve((map.width * map.height) + 4);
+ std::vector<unsigned int> indices;
+ const auto isize = geoData->getSize() - glm::uvec2 {1, 1};
+ indices.reserve(static_cast<std::size_t>(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<float>(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<glm::ivec2, 6> 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<Vertex> & vertices)
-{
- const auto tilesCount = (width - 1) * (height - 1);
- const auto trianglesCount = tilesCount * 2;
- const auto indicesCount = trianglesCount * 3;
- std::vector<unsigned int> 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<Vertex> 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<Mesh>(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 <gfx/models/mesh.h>
#include <gfx/renderable.h>
#include <memory>
-#include <string>
-#include <vector>
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<GeoData>);
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<Vertex> &);
+ void generateMeshes();
+ std::shared_ptr<GeoData> geoData;
Collection<Mesh, false> meshes;
std::shared_ptr<Texture> 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 <boost/test/data/test_case.hpp>
+#include <boost/test/unit_test.hpp>
+#include <stream_support.hpp>
+
+#include <game/geoData.h>
+
+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;
+ }));
+}