mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-17 03:59:19 +02:00
feat(terrain): grade line fitted to the ground under the route within the limits; editor terrain shaped to the grade line with a formation and rounded slopes, with one step back; scenery ground triangles of chosen materials converted to editor terrain and taken out of the files on save, with a dated backup of each file; crack-free simplified terrain mesh, terrain streamed as far as the scenery is drawn, the nearest chunks first; smoothing brush
This commit is contained in:
@@ -385,6 +385,255 @@ line recognize(std::vector<double> const &Chainages, std::vector<double> const &
|
||||
return result;
|
||||
}
|
||||
|
||||
line fit_ground(std::vector<double> const &Chainages, std::vector<double> const &Ground, context const &Context, line const &Settings, ground_fit const &Options)
|
||||
{
|
||||
line result;
|
||||
result.speed = Settings.speed;
|
||||
result.norms = Settings.norms;
|
||||
result.join_start = Settings.join_start;
|
||||
result.join_end = Settings.join_end;
|
||||
auto const count{std::min(Chainages.size(), Ground.size())};
|
||||
if (count < 2)
|
||||
return result;
|
||||
|
||||
// gaps in the ground bridged straight, past the outermost samples it stays level
|
||||
std::vector<double> ground(Ground.begin(), Ground.begin() + count);
|
||||
std::vector<std::size_t> known;
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
if (std::isfinite(ground[i]))
|
||||
known.push_back(i);
|
||||
if (known.empty())
|
||||
return result;
|
||||
for (std::size_t i = 0, k = 0; i < count; ++i)
|
||||
{
|
||||
if (std::isfinite(ground[i]))
|
||||
continue;
|
||||
while (k + 1 < known.size() && known[k + 1] < i)
|
||||
++k;
|
||||
if (i < known.front())
|
||||
ground[i] = Ground[known.front()];
|
||||
else if (k + 1 >= known.size())
|
||||
ground[i] = Ground[known.back()];
|
||||
else
|
||||
{
|
||||
auto const a{known[k]}, b{known[k + 1]};
|
||||
ground[i] = Ground[a] + (Ground[b] - Ground[a]) * (Chainages[i] - Chainages[a]) / std::max(1e-9, Chainages[b] - Chainages[a]);
|
||||
}
|
||||
}
|
||||
if (Options.smoothing > 1.0)
|
||||
{
|
||||
std::vector<double> sums(count + 1, 0.0);
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
sums[i + 1] = sums[i] + ground[i];
|
||||
std::vector<double> smooth(count);
|
||||
std::size_t low{0}, high{0};
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
{
|
||||
while (Chainages[i] - Chainages[low] > Options.smoothing * 0.5)
|
||||
++low;
|
||||
while (high + 1 < count && Chainages[high + 1] - Chainages[i] <= Options.smoothing * 0.5)
|
||||
++high;
|
||||
high = std::max(high, i);
|
||||
smooth[i] = (sums[high + 1] - sums[low]) / static_cast<double>(high - low + 1);
|
||||
}
|
||||
ground.swap(smooth);
|
||||
}
|
||||
|
||||
bool const start_fixed{Settings.join_start && Context.start_joined};
|
||||
bool const end_fixed{Settings.join_end && Context.end_joined};
|
||||
|
||||
// first guess of the points: the ground simplified to within the tolerance
|
||||
std::vector<std::size_t> knots{0, count - 1};
|
||||
{
|
||||
std::vector<char> keep(count, 0);
|
||||
keep.front() = keep.back() = 1;
|
||||
std::vector<std::pair<std::size_t, std::size_t>> stack{{0, count - 1}};
|
||||
while (false == stack.empty())
|
||||
{
|
||||
auto const [a, b]{stack.back()};
|
||||
stack.pop_back();
|
||||
if (b <= a + 1)
|
||||
continue;
|
||||
auto const run{std::max(1e-9, Chainages[b] - Chainages[a])};
|
||||
double worst{0.0};
|
||||
std::size_t index{a};
|
||||
for (auto i = a + 1; i < b; ++i)
|
||||
{
|
||||
auto const chord{ground[a] + (ground[b] - ground[a]) * (Chainages[i] - Chainages[a]) / run};
|
||||
auto const departure{std::abs(ground[i] - chord)};
|
||||
if (departure > worst)
|
||||
{
|
||||
worst = departure;
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
if (worst > Options.tolerance)
|
||||
{
|
||||
keep[index] = 1;
|
||||
stack.push_back({a, index});
|
||||
stack.push_back({index, b});
|
||||
}
|
||||
}
|
||||
knots.clear();
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
if (keep[i] != 0)
|
||||
knots.push_back(i);
|
||||
}
|
||||
|
||||
auto const segment_limit = [&](double const From, double const To) {
|
||||
auto limit{result.norms.grade_max};
|
||||
for (auto const &zone : Context.switches)
|
||||
if (overlaps(From, To, zone))
|
||||
limit = std::min(limit, result.norms.grade_switch_max);
|
||||
return limit / 1000.0;
|
||||
};
|
||||
// elevations of the points with the least squares of the departures from the ground
|
||||
auto const solve = [&](std::vector<std::size_t> const &Knots) {
|
||||
auto const n{Knots.size()};
|
||||
std::vector<double> lower(n, 0.0), diagonal(n, 1e-9), upper(n, 0.0), right(n, 0.0);
|
||||
auto const add = [&](std::size_t const Interval, double const T, double const Value, double const Weight) {
|
||||
diagonal[Interval] += Weight * (1.0 - T) * (1.0 - T);
|
||||
diagonal[Interval + 1] += Weight * T * T;
|
||||
upper[Interval] += Weight * (1.0 - T) * T;
|
||||
lower[Interval + 1] += Weight * (1.0 - T) * T;
|
||||
right[Interval] += Weight * (1.0 - T) * Value;
|
||||
right[Interval + 1] += Weight * T * Value;
|
||||
};
|
||||
auto const interval_of = [&](double const Chainage) {
|
||||
std::size_t k{0};
|
||||
while (k + 2 < n && Chainage >= Chainages[Knots[k + 1]])
|
||||
++k;
|
||||
return k;
|
||||
};
|
||||
for (std::size_t k = 0; k + 1 < n; ++k)
|
||||
{
|
||||
auto const from{Chainages[Knots[k]]};
|
||||
auto const run{std::max(1e-9, Chainages[Knots[k + 1]] - from)};
|
||||
auto const last{k + 2 == n ? Knots[k + 1] : Knots[k + 1] - 1};
|
||||
for (auto i = Knots[k]; i <= last; ++i)
|
||||
add(k, (Chainages[i] - from) / run, ground[i], 1.0);
|
||||
}
|
||||
for (auto const &fixed : Context.fixed)
|
||||
{
|
||||
auto const k{interval_of(fixed.first)};
|
||||
auto const from{Chainages[Knots[k]]};
|
||||
auto const t{std::clamp((fixed.first - from) / std::max(1e-9, Chainages[Knots[k + 1]] - from), 0.0, 1.0)};
|
||||
add(k, t, fixed.second, 1e6);
|
||||
}
|
||||
auto const pin = [&](std::size_t const Index, double const Value) {
|
||||
lower[Index] = upper[Index] = 0.0;
|
||||
diagonal[Index] = 1.0;
|
||||
right[Index] = Value;
|
||||
};
|
||||
if (start_fixed)
|
||||
pin(0, Context.start_elevation);
|
||||
if (end_fixed)
|
||||
pin(n - 1, Context.end_elevation);
|
||||
for (std::size_t k = 1; k < n; ++k)
|
||||
{
|
||||
auto const factor{lower[k] / diagonal[k - 1]};
|
||||
diagonal[k] -= factor * upper[k - 1];
|
||||
right[k] -= factor * right[k - 1];
|
||||
}
|
||||
std::vector<double> z(n);
|
||||
z[n - 1] = right[n - 1] / diagonal[n - 1];
|
||||
for (auto k = n - 1; k-- > 0;)
|
||||
z[k] = (right[k] - upper[k] * z[k + 1]) / diagonal[k];
|
||||
|
||||
// grades kept within the limits, the fixed ends don't move
|
||||
for (int pass = 0; pass < 4; ++pass)
|
||||
{
|
||||
for (std::size_t k = 0; k + 1 < n; ++k)
|
||||
{
|
||||
if (k + 2 == n && end_fixed)
|
||||
continue;
|
||||
auto const run{Chainages[Knots[k + 1]] - Chainages[Knots[k]]};
|
||||
auto const step{segment_limit(Chainages[Knots[k]], Chainages[Knots[k + 1]]) * run};
|
||||
z[k + 1] = std::clamp(z[k + 1], z[k] - step, z[k] + step);
|
||||
}
|
||||
for (auto k = n - 1; k-- > 0;)
|
||||
{
|
||||
if (k == 0 && start_fixed)
|
||||
continue;
|
||||
auto const run{Chainages[Knots[k + 1]] - Chainages[Knots[k]]};
|
||||
auto const step{segment_limit(Chainages[Knots[k]], Chainages[Knots[k + 1]]) * run};
|
||||
z[k] = std::clamp(z[k], z[k + 1] - step, z[k + 1] + step);
|
||||
}
|
||||
}
|
||||
return z;
|
||||
};
|
||||
|
||||
auto const radius{std::ceil(required_radius(result) / 10.0) * 10.0};
|
||||
auto const threshold{result.norms.curve_threshold / 1000.0};
|
||||
std::vector<double> z;
|
||||
for (int iteration = 0; iteration < 1000; ++iteration)
|
||||
{
|
||||
z = solve(knots);
|
||||
auto const n{knots.size()};
|
||||
if (n <= 2)
|
||||
break;
|
||||
auto const chainage = [&](std::size_t const K) { return Chainages[knots[K]]; };
|
||||
auto const slope = [&](std::size_t const K) { return (z[K + 1] - z[K]) / std::max(1e-9, chainage(K + 1) - chainage(K)); };
|
||||
std::vector<double> change(n, 0.0), tangent(n, 0.0);
|
||||
for (std::size_t k = 1; k + 1 < n; ++k)
|
||||
{
|
||||
change[k] = std::abs(slope(k) - slope(k - 1));
|
||||
tangent[k] = change[k] > threshold + 1e-9 ? radius * change[k] * 0.5 : 0.0;
|
||||
}
|
||||
// the curve of a joint starts at the end of the route and takes its whole length
|
||||
if (start_fixed)
|
||||
{
|
||||
change[0] = std::abs(slope(0) - Context.start_grade);
|
||||
tangent[0] = change[0] > threshold + 1e-9 ? radius * change[0] : 0.0;
|
||||
}
|
||||
if (end_fixed)
|
||||
{
|
||||
change[n - 1] = std::abs(slope(n - 2) - Context.end_grade);
|
||||
tangent[n - 1] = change[n - 1] > threshold + 1e-9 ? radius * change[n - 1] : 0.0;
|
||||
}
|
||||
|
||||
std::size_t removed{0};
|
||||
for (std::size_t k = 1; k + 1 < n && removed == 0; ++k)
|
||||
for (auto const &zone : Context.switches)
|
||||
if (overlaps(chainage(k) - tangent[k] - 1.0, chainage(k) + tangent[k] + 1.0, zone))
|
||||
{
|
||||
removed = k;
|
||||
break;
|
||||
}
|
||||
if (removed == 0)
|
||||
{
|
||||
double worst{0.0};
|
||||
for (std::size_t k = 0; k + 1 < n; ++k)
|
||||
{
|
||||
bool const inner{(k > 0 || tangent[0] > 0.0) && (k + 2 < n || tangent[n - 1] > 0.0)};
|
||||
auto const needed{tangent[k] + tangent[k + 1] + (inner ? result.norms.element_min : 0.0)};
|
||||
auto const shortage{needed - (chainage(k + 1) - chainage(k))};
|
||||
if (shortage <= 1e-6 || shortage <= worst)
|
||||
continue;
|
||||
worst = shortage;
|
||||
if (k == 0)
|
||||
removed = 1;
|
||||
else if (k + 2 == n)
|
||||
removed = n - 2;
|
||||
else
|
||||
removed = change[k] <= change[k + 1] ? k : k + 1;
|
||||
}
|
||||
}
|
||||
if (removed == 0)
|
||||
break;
|
||||
knots.erase(knots.begin() + removed);
|
||||
}
|
||||
|
||||
for (std::size_t k = 0; k < knots.size(); ++k)
|
||||
{
|
||||
pvi point;
|
||||
point.chainage = Chainages[knots[k]];
|
||||
point.elevation = z[k];
|
||||
result.points.push_back(point);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<double> breaks(line const &Line)
|
||||
{
|
||||
std::vector<double> result;
|
||||
|
||||
@@ -111,6 +111,15 @@ struct recognition
|
||||
// grade line of existing track, from its elevations and grades sampled along the chainage
|
||||
line recognize(std::vector<double> const &Chainages, std::vector<double> const &Elevations, std::vector<double> const &Grades, recognition const &Options);
|
||||
|
||||
struct ground_fit
|
||||
{
|
||||
double tolerance{1.0}; // m, departure from the ground which doesn't yet need a change of grade
|
||||
double smoothing{20.0}; // m, length over which the ground is averaged
|
||||
};
|
||||
// grade line which follows the ground within the limits of Settings, with the least earthworks.
|
||||
// Ground is NaN where there's none; the joined ends of the route keep the elevation of the adjoining track
|
||||
line fit_ground(std::vector<double> const &Chainages, std::vector<double> const &Ground, context const &Context, line const &Settings, ground_fit const &Options);
|
||||
|
||||
// chainages where the paths have to end so that each of them carries a single grade or a single curve
|
||||
std::vector<double> breaks(line const &Line);
|
||||
|
||||
|
||||
@@ -154,28 +154,72 @@ bool editor_terrain::block_flat(int X0, int Z0, int X1, int Z1, float Error) con
|
||||
return true;
|
||||
}
|
||||
|
||||
// adaptive quadtree: collapse flat blocks into a single quad, otherwise split into four
|
||||
void editor_terrain::emit_block(int X0, int Z0, int X1, int Z1, float Error, std::vector<world_vertex> &Out) const
|
||||
// adaptive quadtree: flat blocks stay whole, the others are split into four
|
||||
void editor_terrain::collect_blocks(int X0, int Z0, int X1, int Z1, float Error, std::vector<std::array<int, 4>> &Out) const
|
||||
{
|
||||
bool const splitx = (X1 - X0) > 1;
|
||||
bool const splitz = (Z1 - Z0) > 1;
|
||||
|
||||
if ((!splitx && !splitz) || block_flat(X0, Z0, X1, Z1, Error))
|
||||
{
|
||||
emit_quad(X0, Z0, X1, Z1, Out);
|
||||
Out.push_back({X0, Z0, X1, Z1});
|
||||
return;
|
||||
}
|
||||
|
||||
int const xm = splitx ? (X0 + X1) / 2 : X1;
|
||||
int const zm = splitz ? (Z0 + Z1) / 2 : Z1;
|
||||
|
||||
emit_block(X0, Z0, xm, zm, Error, Out);
|
||||
collect_blocks(X0, Z0, xm, zm, Error, Out);
|
||||
if (splitx)
|
||||
emit_block(xm, Z0, X1, zm, Error, Out);
|
||||
collect_blocks(xm, Z0, X1, zm, Error, Out);
|
||||
if (splitz)
|
||||
emit_block(X0, zm, xm, Z1, Error, Out);
|
||||
collect_blocks(X0, zm, xm, Z1, Error, Out);
|
||||
if (splitx && splitz)
|
||||
emit_block(xm, zm, X1, Z1, Error, Out);
|
||||
collect_blocks(xm, zm, X1, Z1, Error, Out);
|
||||
}
|
||||
|
||||
void editor_terrain::emit_block(std::array<int, 4> const &Block, std::vector<char> const &Marked, std::vector<world_vertex> &Out) const
|
||||
{
|
||||
int const x0 = Block[0], z0 = Block[1], x1 = Block[2], z1 = Block[3];
|
||||
// the edges walked the way the quads wind
|
||||
std::vector<std::pair<int, int>> ring;
|
||||
for (int z = z0; z < z1; ++z)
|
||||
if (z == z0 || Marked[index(x0, z)] != 0)
|
||||
ring.push_back({x0, z});
|
||||
for (int x = x0; x < x1; ++x)
|
||||
if (x == x0 || Marked[index(x, z1)] != 0)
|
||||
ring.push_back({x, z1});
|
||||
for (int z = z1; z > z0; --z)
|
||||
if (z == z1 || Marked[index(x1, z)] != 0)
|
||||
ring.push_back({x1, z});
|
||||
for (int x = x1; x > x0; --x)
|
||||
if (x == x1 || Marked[index(x, z0)] != 0)
|
||||
ring.push_back({x, z0});
|
||||
if (ring.size() <= 4)
|
||||
{
|
||||
emit_quad(x0, z0, x1, z1, Out);
|
||||
return;
|
||||
}
|
||||
|
||||
world_vertex centre;
|
||||
if ((x1 - x0) % 2 == 0 && (z1 - z0) % 2 == 0)
|
||||
centre = make_vertex((x0 + x1) / 2, (z0 + z1) / 2);
|
||||
else
|
||||
{
|
||||
auto const a = make_vertex(x0, z0), b = make_vertex(x1, z0), c = make_vertex(x0, z1), d = make_vertex(x1, z1);
|
||||
centre = a;
|
||||
centre.position = (a.position + b.position + c.position + d.position) * 0.25;
|
||||
centre.normal = glm::normalize(a.normal + b.normal + c.normal + d.normal);
|
||||
centre.texture = (a.texture + b.texture + c.texture + d.texture) * 0.25f;
|
||||
}
|
||||
for (std::size_t i = 0; i < ring.size(); ++i)
|
||||
{
|
||||
auto const &from = ring[i];
|
||||
auto const &to = ring[(i + 1) % ring.size()];
|
||||
Out.push_back(centre);
|
||||
Out.push_back(make_vertex(from.first, from.second));
|
||||
Out.push_back(make_vertex(to.first, to.second));
|
||||
}
|
||||
}
|
||||
|
||||
void editor_terrain::build_vertices(std::vector<world_vertex> &Out, bool Simplify) const
|
||||
@@ -185,7 +229,16 @@ void editor_terrain::build_vertices(std::vector<world_vertex> &Out, bool Simplif
|
||||
|
||||
if (Simplify)
|
||||
{
|
||||
emit_block(0, 0, m_cells, m_cells, m_simplify_error, Out);
|
||||
std::vector<std::array<int, 4>> blocks;
|
||||
collect_blocks(0, 0, m_cells, m_cells, m_simplify_error, blocks);
|
||||
// the corners of the blocks, and the whole edge of the patch which the neighbouring patch meets in its own way
|
||||
std::vector<char> marked(static_cast<std::size_t>(m_cells + 1) * (m_cells + 1), 0);
|
||||
for (int i = 0; i <= m_cells; ++i)
|
||||
marked[index(i, 0)] = marked[index(i, m_cells)] = marked[index(0, i)] = marked[index(m_cells, i)] = 1;
|
||||
for (auto const &block : blocks)
|
||||
marked[index(block[0], block[1])] = marked[index(block[2], block[1])] = marked[index(block[0], block[3])] = marked[index(block[2], block[3])] = 1;
|
||||
for (auto const &block : blocks)
|
||||
emit_block(block, marked, Out);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -278,6 +331,12 @@ bool editor_terrain::contains(double X, double Z) const
|
||||
|
||||
double editor_terrain::height_at(double X, double Z) const
|
||||
{
|
||||
return height_in(m_heights, X, Z);
|
||||
}
|
||||
|
||||
double editor_terrain::height_in(std::vector<float> const &Heights, double X, double Z) const
|
||||
{
|
||||
auto const &m_heights = Heights;
|
||||
double const fx = (X - m_x0) / m_cellsize;
|
||||
double const fz = (Z - m_z0) / m_cellsize;
|
||||
int ix = static_cast<int>(std::floor(fx));
|
||||
@@ -330,6 +389,62 @@ bool editor_terrain::sculpt(double X, double Z, double Radius, double Strength)
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool editor_terrain::smooth(double X, double Z, double Radius, double Amount, std::function<double(double X, double Z, double Fallback)> const &Ground)
|
||||
{
|
||||
if (!valid() || Radius <= 0.0 || Amount <= 0.0)
|
||||
return false;
|
||||
double const reach = Radius + m_cellsize;
|
||||
if (X + reach < m_x0 || X - reach > m_x0 + m_cells * m_cellsize || Z + reach < m_z0 || Z - reach > m_z0 + m_cells * m_cellsize)
|
||||
return false;
|
||||
|
||||
double const step = m_cellsize;
|
||||
return reshape([&](double const Vx, double const Vz, float &Height) {
|
||||
double const d = std::sqrt((Vx - X) * (Vx - X) + (Vz - Z) * (Vz - Z));
|
||||
if (d > Radius)
|
||||
return false;
|
||||
double const own = Height;
|
||||
// the neighbours a cell away, read from the ground as it was before the stroke, whichever patch holds them
|
||||
double const average = (Ground(Vx - step, Vz, own) + Ground(Vx + step, Vz, own) + Ground(Vx, Vz - step, own) + Ground(Vx, Vz + step, own) + own * 4.0) / 8.0;
|
||||
double const falloff = 0.5 * (std::cos(kPi * d / Radius) + 1.0);
|
||||
double const smoothed = own + (average - own) * std::min(1.0, Amount * falloff);
|
||||
if (std::abs(smoothed - own) < 1e-5)
|
||||
return false;
|
||||
Height = static_cast<float>(smoothed);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
bool editor_terrain::reshape(std::function<bool(double X, double Z, float &Height)> const &Shaper)
|
||||
{
|
||||
if (!valid())
|
||||
return false;
|
||||
|
||||
bool changed = false;
|
||||
for (int iz = 0; iz <= m_cells; ++iz)
|
||||
for (int ix = 0; ix <= m_cells; ++ix)
|
||||
changed |= Shaper(m_x0 + static_cast<double>(ix) * m_cellsize, m_z0 + static_cast<double>(iz) * m_cellsize, m_heights[index(ix, iz)]);
|
||||
|
||||
if (changed)
|
||||
{
|
||||
m_simplify = false;
|
||||
m_dirty = true;
|
||||
m_modified = true;
|
||||
regenerate(false);
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
void editor_terrain::restore(std::vector<float> const &Heights)
|
||||
{
|
||||
if (!valid() || Heights.size() != m_heights.size())
|
||||
return;
|
||||
m_heights = Heights;
|
||||
m_simplify = false;
|
||||
m_dirty = true;
|
||||
m_modified = true;
|
||||
regenerate(false);
|
||||
}
|
||||
|
||||
glm::dvec3 editor_terrain::centre() const
|
||||
{
|
||||
double const c = 0.5 * static_cast<double>(m_cells) * m_cellsize;
|
||||
|
||||
@@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
@@ -44,10 +45,20 @@ class editor_terrain
|
||||
bool contains(double X, double Z) const;
|
||||
// surface height at (X,Z) (bilinear over the covering quad); only valid when contains() is true
|
||||
double height_at(double X, double Z) const;
|
||||
// the same, from heights taken from heights() earlier
|
||||
double height_in(std::vector<float> const &Heights, double X, double Z) const;
|
||||
|
||||
// raises/lowers vertices within Radius of (X,Z) by Strength (metres, signed), with a smooth
|
||||
// falloff; regenerates the rendered geometry (full resolution). returns true if anything changed.
|
||||
bool sculpt(double X, double Z, double Radius, double Strength);
|
||||
// gives every grid vertex to Shaper, with its world position and height; Shaper returns true if it changed the height.
|
||||
// regenerates the rendered geometry (full resolution). returns true if anything changed.
|
||||
bool reshape(std::function<bool(double X, double Z, float &Height)> const &Shaper);
|
||||
// pulls the vertices within Radius of (X,Z) towards the average of their neighbours, Amount (0..1) of the way at the centre,
|
||||
// with a smooth falloff. Ground gives the height before the stroke at a world point, Fallback where there's no terrain
|
||||
bool smooth(double X, double Z, double Radius, double Amount, std::function<double(double X, double Z, double Fallback)> const &Ground);
|
||||
// puts back heights taken from heights() earlier
|
||||
void restore(std::vector<float> const &Heights);
|
||||
|
||||
// rebuilds the rendered mesh, collapsing regions flatter than ErrorMetres into larger quads
|
||||
// (adaptive quadtree). the editable heightmap is untouched, so sculpting/raycast stay exact.
|
||||
@@ -63,6 +74,7 @@ class editor_terrain
|
||||
glm::dvec3 centre() const;
|
||||
float extent() const { return m_cells * m_cellsize; }
|
||||
bool valid() const { return m_cells > 0; }
|
||||
gfx::geometry_handle geometry() const { return m_geometry; }
|
||||
bool optimized() const { return m_simplify; }
|
||||
// set by sculpt() when the mesh changed and is currently full-resolution; cleared by optimize().
|
||||
// used to auto-simplify only the chunks that were actually edited, once a stroke finishes.
|
||||
@@ -88,7 +100,9 @@ class editor_terrain
|
||||
void build_vertices(std::vector<world_vertex> &Out, bool Simplify) const;
|
||||
// adaptive quadtree helpers (used when Simplify is on)
|
||||
bool block_flat(int X0, int Z0, int X1, int Z1, float Error) const;
|
||||
void emit_block(int X0, int Z0, int X1, int Z1, float Error, std::vector<world_vertex> &Out) const;
|
||||
void collect_blocks(int X0, int Z0, int X1, int Z1, float Error, std::vector<std::array<int, 4>> &Out) const;
|
||||
// a block as a fan through the marked grid vertices of its edges, so it meets its smaller neighbours without cracks
|
||||
void emit_block(std::array<int, 4> const &Block, std::vector<char> const &Marked, std::vector<world_vertex> &Out) const;
|
||||
void emit_quad(int X0, int Z0, int X1, int Z1, std::vector<world_vertex> &Out) const;
|
||||
// rebuilds and re-uploads the rendered geometry (Replace when the count is unchanged, otherwise
|
||||
// a fresh chunk whose handle is swapped into the shape)
|
||||
|
||||
@@ -10,7 +10,10 @@ http://mozilla.org/MPL/2.0/.
|
||||
#include "stdafx.h"
|
||||
#include "editor/editorTerrainStreamer.hpp"
|
||||
|
||||
#include "utilities/Globals.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
@@ -133,13 +136,25 @@ void terrain_streamer::update(glm::dvec3 const &CameraPos)
|
||||
return;
|
||||
|
||||
chunk_key const camera = key_at(CameraPos.x, CameraPos.z);
|
||||
// the terrain reaches as far as the rest of the scenery is drawn
|
||||
int const radius = std::max(m_radius, std::min(48, static_cast<int>(std::ceil(Global.BaseDrawRange / std::max(1.0f, chunk_world_size())))));
|
||||
|
||||
// load any missing chunk inside the radius (one build per frame keeps the hitch small)
|
||||
bool built = false;
|
||||
for (int dz = -m_radius; dz <= m_radius && !built; ++dz)
|
||||
for (int dx = -m_radius; dx <= m_radius && !built; ++dx)
|
||||
// missing chunks inside the radius, the nearest first, as many as fit in a few milliseconds of the frame
|
||||
if (m_ring_radius != radius)
|
||||
{
|
||||
m_ring.clear();
|
||||
for (int dz = -radius; dz <= radius; ++dz)
|
||||
for (int dx = -radius; dx <= radius; ++dx)
|
||||
m_ring.push_back({dx, dz});
|
||||
std::sort(m_ring.begin(), m_ring.end(), [](chunk_key const &A, chunk_key const &B) { return A.first * A.first + A.second * A.second < B.first * B.first + B.second * B.second; });
|
||||
m_ring_radius = radius;
|
||||
}
|
||||
auto const started = std::chrono::steady_clock::now();
|
||||
for (auto const &offset : m_ring)
|
||||
{
|
||||
chunk_key const key{camera.first + dx, camera.second + dz};
|
||||
if (std::chrono::steady_clock::now() - started > std::chrono::milliseconds(4))
|
||||
break;
|
||||
chunk_key const key{camera.first + offset.first, camera.second + offset.second};
|
||||
if (m_chunks.count(key))
|
||||
continue;
|
||||
|
||||
@@ -169,7 +184,6 @@ void terrain_streamer::update(glm::dvec3 const &CameraPos)
|
||||
if (m_auto_optimize)
|
||||
terrain->optimize(m_simplify_error);
|
||||
m_chunks.emplace(key, std::move(terrain));
|
||||
built = true; // amortise: at most one new chunk per frame
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +192,7 @@ void terrain_streamer::update(glm::dvec3 const &CameraPos)
|
||||
{
|
||||
int const dx = it->first.first - camera.first;
|
||||
int const dz = it->first.second - camera.second;
|
||||
if (std::abs(dx) > m_radius || std::abs(dz) > m_radius)
|
||||
if (std::abs(dx) > radius || std::abs(dz) > radius)
|
||||
{
|
||||
if (it->second)
|
||||
{
|
||||
|
||||
@@ -77,6 +77,7 @@ class terrain_streamer
|
||||
int cells() const { return m_cells; }
|
||||
float cellsize() const { return m_cellsize; }
|
||||
int radius() const { return m_radius; }
|
||||
std::string const &texture() const { return m_texture; }
|
||||
|
||||
// writes every resident, edited chunk to disk (without unloading) - used on save
|
||||
void flush();
|
||||
@@ -108,6 +109,9 @@ class terrain_streamer
|
||||
|
||||
std::map<chunk_key, std::unique_ptr<editor_terrain>> m_chunks;
|
||||
std::map<chunk_key, std::uint8_t> m_known; // cached per-chunk flag bits
|
||||
// offsets of the chunks around the camera, the nearest first, for the radius in effect
|
||||
std::vector<chunk_key> m_ring;
|
||||
int m_ring_radius{-1};
|
||||
};
|
||||
|
||||
// single, simulation-level streamer instance shared by the editor (authoring) and the scenery
|
||||
|
||||
Reference in New Issue
Block a user