17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-01 09:12:08 +01:00

feat(terrain): holes cut in the heightmap terrain and shader grass painted over it

This commit is contained in:
2026-10-20 01:32:01 +02:00
parent d7ba6926fc
commit f3bdadad66
32 changed files with 2020 additions and 30 deletions

View File

@@ -39,6 +39,8 @@ std::uint32_t constexpr tag_heights{fourcc('H', 'G', 'H', 'T')};
std::uint32_t constexpr tag_base{fourcc('H', 'B', 'A', 'S')};
std::uint32_t constexpr tag_paint{fourcc('S', 'P', 'L', 'T')};
std::uint32_t constexpr tag_adjust{fourcc('H', 'A', 'D', 'J')};
std::uint32_t constexpr tag_holes{fourcc('H', 'O', 'L', 'E')};
std::uint32_t constexpr tag_grass{fourcc('G', 'R', 'A', 'S')};
std::uint32_t constexpr pack_magic{fourcc('T', 'C', 'H', '1')};
std::uint32_t constexpr pack_version{1};
@@ -188,6 +190,11 @@ float valid_spacing(float const Spacing)
return best;
}
int detail_samples(int const Cells)
{
return std::max(default_paint_samples, Cells);
}
chunk_key chunk_at(double const X, double const Z)
{
return {static_cast<int>(std::floor(X / chunk_size)), static_cast<int>(std::floor(Z / chunk_size))};
@@ -429,6 +436,111 @@ void chunk_data::compact_layers()
weights = std::move(kept);
}
float chunk_data::hole_value(double const X, double const Z) const
{
if (false == has_holes())
return 0.f;
auto const low{corner()};
auto const step{chunk_size / holes};
auto const fx{std::clamp((X - low.x) / step, 0.0, static_cast<double>(holes))};
auto const fz{std::clamp((Z - low.y) / step, 0.0, static_cast<double>(holes))};
auto const ix{std::min(static_cast<int>(fx), holes - 1)};
auto const iz{std::min(static_cast<int>(fz), holes - 1)};
auto const tx{fx - ix}, tz{fz - iz};
auto const side{static_cast<std::size_t>(holes + 1)};
auto const at = [&](int const X_, int const Z_) { return hole_mask[static_cast<std::size_t>(Z_) * side + X_] / 255.0; };
return static_cast<float>((at(ix, iz) * (1.0 - tx) + at(ix + 1, iz) * tx) * (1.0 - tz) + (at(ix, iz + 1) * (1.0 - tx) + at(ix + 1, iz + 1) * tx) * tz);
}
bool chunk_data::cut_cells(int const Ix0, int const Iz0, int const Ix1, int const Iz1) const
{
if (false == has_holes() || cells <= 0)
return false;
// every sample the surface of the cells is led from has to be cut, then all of it is
auto const ratio{static_cast<double>(holes) / cells};
auto const range = [&](int const From, int const To) {
return std::make_pair(std::clamp(static_cast<int>(std::floor(From * ratio + 1e-9)), 0, holes), std::clamp(static_cast<int>(std::ceil(To * ratio - 1e-9)), 0, holes));
};
auto const [x0, x1]{range(Ix0, Ix1)};
auto const [z0, z1]{range(Iz0, Iz1)};
auto const side{static_cast<std::size_t>(holes + 1)};
for (int z = z0; z <= z1; ++z)
for (int x = x0; x <= x1; ++x)
if (hole_mask[static_cast<std::size_t>(z) * side + x] < 128)
return false;
return true;
}
void chunk_data::ensure_holes()
{
if (has_holes())
return;
holes = detail_samples(cells);
hole_mask.assign(static_cast<std::size_t>(holes + 1) * (holes + 1), 0);
}
void chunk_data::compact_holes()
{
if (has_holes() && std::any_of(hole_mask.begin(), hole_mask.end(), [](std::uint8_t const Value) { return Value != 0; }))
return;
holes = 0;
hole_mask.clear();
}
std::size_t chunk_data::grass_count() const
{
if (grass <= 0)
return 0;
std::size_t count{0};
while (count < grass_kinds.size() && grass_kinds[count] != no_layer)
++count;
return count;
}
int chunk_data::grass_slot_of(std::uint16_t const Kind) const
{
for (std::size_t slot = 0; slot < grass_count(); ++slot)
if (grass_kinds[slot] == Kind)
return static_cast<int>(slot);
return -1;
}
void chunk_data::ensure_grass()
{
if (grass > 0)
return;
grass = default_paint_samples;
grass_kinds.fill(no_layer);
grass_density.clear();
}
void chunk_data::compact_grass()
{
if (grass <= 0)
return;
auto const samples{static_cast<std::size_t>(grass + 1) * (grass + 1)};
auto const count{grass_count()};
std::vector<std::uint8_t> kept;
std::array<std::uint16_t, max_chunk_grass> keptkinds;
keptkinds.fill(no_layer);
std::size_t used{0};
for (std::size_t slot = 0; slot < count; ++slot)
{
auto const begin{grass_density.begin() + slot * samples};
if (slot * samples + samples > grass_density.size() || std::all_of(begin, begin + samples, [](std::uint8_t const Value) { return Value == 0; }))
continue;
kept.insert(kept.end(), begin, begin + samples);
keptkinds[used++] = grass_kinds[slot];
}
grass_kinds = keptkinds;
grass_density = std::move(kept);
if (used == 0)
{
grass = 0;
grass_density.clear();
}
}
// modifiers
std::pair<glm::dvec2, glm::dvec2> modifier::bounds() const
@@ -541,6 +653,19 @@ std::optional<modifier_shape::effect> modifier_shape::evaluate(double const X, d
// manifest
std::vector<grass_def> const &grass_presets()
{
static std::vector<grass_def> const presets{
// name, root, tip, height, variation, width, density, bend
{"meadow", {0.22f, 0.33f, 0.09f}, {0.58f, 0.68f, 0.27f}, 0.25f, 0.45f, 0.020f, 90.f, 0.30f},
{"lawn", {0.18f, 0.30f, 0.08f}, {0.42f, 0.58f, 0.20f}, 0.07f, 0.30f, 0.012f, 200.f, 0.15f},
{"dry grass", {0.36f, 0.32f, 0.16f}, {0.80f, 0.72f, 0.45f}, 0.30f, 0.45f, 0.016f, 70.f, 0.40f},
{"tall grass", {0.18f, 0.27f, 0.07f}, {0.60f, 0.66f, 0.30f}, 0.70f, 0.35f, 0.022f, 40.f, 0.45f},
{"weeds", {0.16f, 0.25f, 0.08f}, {0.45f, 0.55f, 0.22f}, 0.16f, 0.60f, 0.030f, 30.f, 0.25f},
};
return presets;
}
bool load_manifest(std::string const &Path, manifest &Manifest, std::string *Error)
{
try
@@ -615,6 +740,30 @@ bool load_manifest(std::string const &Path, manifest &Manifest, std::string *Err
if (item.points.size() >= 2)
result.modifiers.push_back(std::move(item));
}
if (auto const node{document["grass"]}; node && node.IsSequence())
for (auto const &entry : node)
{
grass_def kind;
auto const colour = [](YAML::Node const &Node, glm::vec3 &Colour) {
if (Node && Node.IsSequence() && Node.size() >= 3)
Colour = glm::clamp(glm::vec3{Node[0].as<float>(), Node[1].as<float>(), Node[2].as<float>()}, glm::vec3{0.f}, glm::vec3{1.f});
};
if (auto const name{entry["name"]})
kind.name = name.as<std::string>();
colour(entry["base"], kind.base);
colour(entry["tip"], kind.tip);
if (auto const height{entry["height"]})
kind.height = std::max(0.01f, height.as<float>());
if (auto const variation{entry["variation"]})
kind.variation = std::clamp(variation.as<float>(), 0.f, 1.f);
if (auto const width{entry["width"]})
kind.width = std::max(0.001f, width.as<float>());
if (auto const density{entry["density"]})
kind.density = std::max(0.f, density.as<float>());
if (auto const bend{entry["bend"]})
kind.bend = std::clamp(bend.as<float>(), 0.f, 1.f);
result.grass.push_back(std::move(kind));
}
Manifest = std::move(result);
return true;
}
@@ -689,6 +838,24 @@ bool save_manifest(std::string const &Path, manifest const &Manifest, std::strin
}
out << YAML::EndSeq;
}
if (false == Manifest.grass.empty())
{
out << YAML::Key << "grass" << YAML::Value << YAML::BeginSeq;
for (auto const &kind : Manifest.grass)
{
out << YAML::BeginMap;
out << YAML::Key << "name" << YAML::Value << kind.name;
out << YAML::Key << "base" << YAML::Value << YAML::Flow << YAML::BeginSeq << kind.base.r << kind.base.g << kind.base.b << YAML::EndSeq;
out << YAML::Key << "tip" << YAML::Value << YAML::Flow << YAML::BeginSeq << kind.tip.r << kind.tip.g << kind.tip.b << YAML::EndSeq;
out << YAML::Key << "height" << YAML::Value << kind.height;
out << YAML::Key << "variation" << YAML::Value << kind.variation;
out << YAML::Key << "width" << YAML::Value << kind.width;
out << YAML::Key << "density" << YAML::Value << kind.density;
out << YAML::Key << "bend" << YAML::Value << kind.bend;
out << YAML::EndMap;
}
out << YAML::EndSeq;
}
out << YAML::EndMap;
if (false == out.good())
{
@@ -844,7 +1011,7 @@ std::shared_ptr<chunk_data> pack_file::read(chunk_key const &Key, std::string *E
auto chunk{std::make_shared<chunk_data>()};
chunk->key = Key;
slice const *heights{nullptr}, *base{nullptr}, *adjust{nullptr}, *paint{nullptr};
slice const *heights{nullptr}, *base{nullptr}, *adjust{nullptr}, *paint{nullptr}, *holes{nullptr}, *grass{nullptr};
bool header{false};
for (auto const &slice : slices)
{
@@ -871,6 +1038,10 @@ std::shared_ptr<chunk_data> pack_file::read(chunk_key const &Key, std::string *E
adjust = &slice;
else if (slice.tag == tag_paint)
paint = &slice;
else if (slice.tag == tag_holes)
holes = &slice;
else if (slice.tag == tag_grass)
grass = &slice;
// slices of unknown kinds are left for newer versions of the simulator
}
if (false == header || heights == nullptr || chunk->cells <= 0 || chunk->cells > 4096)
@@ -892,6 +1063,34 @@ std::shared_ptr<chunk_data> pack_file::read(chunk_key const &Key, std::string *E
if (paint == nullptr || false == load(*paint, chunk->weights) || chunk->weights.size() != expected)
return fail("damaged paint");
}
if (holes != nullptr)
{
if (false == load(*holes, data))
return fail("damaged holes");
reader input{data.data(), data.size()};
std::uint16_t samples{0};
input.get(samples);
auto const count{static_cast<std::size_t>(samples + 1) * (samples + 1)};
chunk->holes = samples;
chunk->hole_mask.resize(count);
if (samples == 0 || input.left() != count || false == input.get(chunk->hole_mask.data(), count))
return fail("damaged holes");
}
if (grass != nullptr)
{
if (false == load(*grass, data))
return fail("damaged grass");
reader input{data.data(), data.size()};
std::uint16_t samples{0};
input.get(samples);
for (auto &kind : chunk->grass_kinds)
input.get(kind);
chunk->grass = samples;
auto const count{static_cast<std::size_t>(samples + 1) * (samples + 1) * chunk->grass_count()};
chunk->grass_density.resize(count);
if (samples == 0 || input.left() != count || false == input.get(chunk->grass_density.data(), count))
return fail("damaged grass");
}
return chunk;
}
@@ -958,6 +1157,22 @@ bool write_pack(std::string const &Path, pack_key const &Key, std::vector<chunk_
}
if (chunk->paint > 0 && chunk->layer_count() > 1)
add(table, tag_paint, chunk->weights);
if (chunk->has_holes())
{
data.clear();
put<std::uint16_t>(data, static_cast<std::uint16_t>(chunk->holes));
data.insert(data.end(), chunk->hole_mask.begin(), chunk->hole_mask.end());
add(table, tag_holes, data);
}
if (auto const kinds{chunk->grass_count()}; kinds > 0 && chunk->grass_density.size() == static_cast<std::size_t>(chunk->grass + 1) * (chunk->grass + 1) * kinds)
{
data.clear();
put<std::uint16_t>(data, static_cast<std::uint16_t>(chunk->grass));
for (auto const kind : chunk->grass_kinds)
put<std::uint16_t>(data, kind);
data.insert(data.end(), chunk->grass_density.begin(), chunk->grass_density.end());
add(table, tag_grass, data);
}
}
std::vector<std::uint8_t> file;
@@ -1146,6 +1361,42 @@ void build_indices(int const Cells, int const Stride, gfx::index_array &Out)
skirt(grid + 3 * row, [&](gfx::basic_index const K) { return at(n, K); }, false);
}
void cut_indices(chunk_data const &Chunk, int const Stride, gfx::index_array &Indices)
{
if (false == Chunk.has_holes())
return;
auto const cells{Chunk.cells};
auto const stride{std::clamp(Stride, 1, max_stride(cells))};
auto const n{segments(cells, stride)};
auto const grid{static_cast<std::size_t>(n) * n * 6};
if (Indices.size() != grid + static_cast<std::size_t>(n) * 24)
return;
auto const coordinate = [&](int const I) { return std::min(I * stride, cells); };
// the triangles of a cell cut out entirely become empty ones, so the count of the indices stays
std::vector<bool> cut(static_cast<std::size_t>(n) * n, false);
for (int j = 0; j < n; ++j)
for (int i = 0; i < n; ++i)
{
if (false == Chunk.cut_cells(coordinate(i), coordinate(j), coordinate(i + 1), coordinate(j + 1)))
continue;
cut[static_cast<std::size_t>(j) * n + i] = true;
std::fill_n(Indices.begin() + (static_cast<std::size_t>(j) * n + i) * 6, 6, Indices[(static_cast<std::size_t>(j) * n + i) * 6]);
}
// the skirts along the cells cut out would hang in the opening
auto const skirt = [&](std::size_t const Skirt, auto const &Cell) {
for (int k = 0; k < n; ++k)
if (Cell(k))
{
auto const first{grid + (Skirt * n + k) * 6};
std::fill_n(Indices.begin() + first, 6, Indices[first]);
}
};
skirt(0, [&](int const K) { return cut[K]; });
skirt(1, [&](int const K) { return cut[static_cast<std::size_t>(n - 1) * n + K]; });
skirt(2, [&](int const K) { return cut[static_cast<std::size_t>(K) * n]; });
skirt(3, [&](int const K) { return cut[static_cast<std::size_t>(K) * n + n - 1]; });
}
void build_mesh(chunk_data const &Chunk, int const Stride, glm::dvec3 const &Origin, height_source const &Outside, mesh &Out)
{
auto const cells{Chunk.cells};
@@ -1223,6 +1474,9 @@ void build_mesh(chunk_data const &Chunk, int const Stride, glm::dvec3 const &Ori
vertex(cells, coordinate(k), skirtdepth);
build_indices(cells, stride, Out.indices);
Out.cut = Chunk.has_holes();
if (Out.cut)
cut_indices(Chunk, stride, Out.indices);
Out.centre = (lowest + highest) * 0.5;
Out.radius = static_cast<float>(glm::length(highest - lowest) * 0.5 + skirtdepth);
Out.low = lowest - glm::dvec3{0.0, skirtdepth, 0.0};

View File

@@ -35,6 +35,7 @@ double constexpr height_step{0.005}; // metres, quantum of the stored heights
std::size_t constexpr max_chunk_layers{8}; // materials painted over a single chunk
std::uint16_t constexpr no_layer{0xffff};
int constexpr default_paint_samples{250}; // paint samples along the side of a chunk, less one: a sample per metre
std::size_t constexpr max_chunk_grass{4}; // kinds of grass growing on a single chunk
// point spacings a chunk can have, finest first
std::array<float, 4> constexpr spacings{0.5f, 1.f, 2.f, 5.f};
@@ -43,6 +44,9 @@ float constexpr default_spacing{2.f};
int cells_for(float const Spacing);
// the allowed spacing closest to specified one
float valid_spacing(float const Spacing);
// samples along the side of a chunk less one, of the holes and the grass of a chunk with specified cells: a sample a metre,
// or one at every grid point of a finer grid
int detail_samples(int const Cells);
using chunk_key = std::pair<int, int>;
using pack_key = std::pair<int, int>;
@@ -70,8 +74,22 @@ struct chunk_data
int paint{0}; // paint samples along a side, less one. 0: the chunk is covered with its first layer alone
std::array<std::uint16_t, max_chunk_layers> layers; // indices of the materials in the terrain palette, no_layer for unused
std::vector<std::uint8_t> weights; // planar, (paint + 1)^2 per used layer, rows like the heights; weights of a sample add up to 255
// holes cut in the ground, for the portals of tunnels and the like: (holes + 1)^2 samples, rows like the heights, 255 where
// the ground is cut and 0 where it stays. the surface is cut where the value led between the samples is over the half.
// 0: no holes
int holes{0};
std::vector<std::uint8_t> hole_mask;
// grass grown on the chunk by the renderer: the density (0..255) of up to 4 kinds of the grass palette at (grass + 1)^2
// samples, planar per used kind, rows like the heights. the densities of a sample add up to 255 at most. 0: no grass
int grass{0};
std::array<std::uint16_t, max_chunk_grass> grass_kinds; // indices of the kinds in the grass palette, no_layer for unused
std::vector<std::uint8_t> grass_density;
chunk_data() { layers.fill(no_layer); }
chunk_data()
{
layers.fill(no_layer);
grass_kinds.fill(no_layer);
}
int side() const { return cells + 1; }
double spacing() const { return chunk_size / cells; }
@@ -101,6 +119,28 @@ struct chunk_data
void ensure_paint(int const Samples = default_paint_samples);
// drops the layers left without weight anywhere, and the paint samples altogether when a single layer is left
void compact_layers();
// holes
bool has_holes() const { return holes > 0 && hole_mask.size() == static_cast<std::size_t>(holes + 1) * (holes + 1); }
// 0..1, how much the point is cut, led between the samples; the surface is cut where it's over the half
float hole_value(double const X, double const Z) const;
bool hole_at(double const X, double const Z) const { return has_holes() && hole_value(X, Z) > 0.5f; }
// true if the whole of the grid cells from (Ix0, Iz0) to (Ix1, Iz1) (exclusive) is cut out
bool cut_cells(int const Ix0, int const Iz0, int const Ix1, int const Iz1) const;
// gives the chunk a hole mask with nothing cut, if it has none
void ensure_holes();
// drops the hole mask if nothing is cut
void compact_holes();
// grass
// number of kinds of grass in use, the used ones come first
std::size_t grass_count() const;
// grass slot of specified kind of the palette, or -1
int grass_slot_of(std::uint16_t const Kind) const;
// gives the chunk grass samples (with no kind) if it has none yet
void ensure_grass();
// drops the kinds left without density anywhere, and the samples altogether when no grass is left
void compact_grass();
};
using chunk_ptr = std::shared_ptr<chunk_data const>;
@@ -171,6 +211,22 @@ struct layer_def
float rotation{0.f}; // degrees the textures are turned by, counterclockwise seen from above
};
// kind of grass of the grass palette: blades the renderer makes up where the grass is painted
struct grass_def
{
std::string name;
glm::vec3 base{0.22f, 0.33f, 0.09f}; // colour at the root of the blades, sRGB 0..1
glm::vec3 tip{0.58f, 0.68f, 0.27f}; // colour at the tips
float height{0.25f}; // metres, of an average blade
float variation{0.45f}; // share of the height the blades differ by, either way
float width{0.02f}; // metres, of a blade at its root
float density{90.f}; // blades per square metre where the kind is painted fully
float bend{0.3f}; // how far the tips lean over, share of the height
};
// a few kinds to start the palette with
std::vector<grass_def> const &grass_presets();
// description of the terrain, terrain.yaml
struct manifest
{
@@ -181,6 +237,7 @@ struct manifest
float spacing{default_spacing}; // point spacing of new chunks
std::vector<water_body> water;
std::vector<modifier> modifiers;
std::vector<grass_def> grass; // the grass palette
};
bool load_manifest(std::string const &Path, manifest &Manifest, std::string *Error = nullptr);
@@ -202,6 +259,8 @@ bool save_manifest(std::string const &Path, manifest const &Manifest, std::strin
// 'HBAS' heights before the modifiers, as 'HGHT'
// 'HADJ' touch-ups over the modifiers, as 'HGHT' (only with 'HBAS'; older versions skip it and keep the heights)
// 'SPLT' paint weights: uint8 per sample per used layer, planar
// 'HOLE' holes: uint16 samples less one, then uint8 per sample (255: cut)
// 'GRAS' grass: uint16 samples less one, uint16 kinds[4], then uint8 density per sample per used kind, planar
class pack_file
{
public:
@@ -247,12 +306,17 @@ struct mesh
float radius{0.f}; // from the centre of the vertices
glm::dvec3 centre{0.0};
glm::dvec3 low{0.0}, high{0.0}; // world bounds of the grid, the skirts included
// the chunk has holes: the cells cut out are left out of the indices (their triangles made empty, so the count of the
// indices is the same as without the holes), the indices are its own
bool cut{false};
};
// strides the grid of a chunk with specified cells can be drawn with
int max_stride(int const Cells);
void build_mesh(chunk_data const &Chunk, int const Stride, glm::dvec3 const &Origin, height_source const &Outside, mesh &Out);
// indices of the mesh of a chunk with specified cells and stride; the same for every chunk of these
void build_indices(int const Cells, int const Stride, gfx::index_array &Out);
// leaves the cells of the chunk cut out entirely, and the skirts along them, out of the indices made by build_indices()
void cut_indices(chunk_data const &Chunk, int const Stride, gfx::index_array &Indices);
// vertices the mesh of a chunk with specified cells and stride has
std::size_t mesh_vertex_count(int const Cells, int const Stride);