/* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "stdafx.h" #include "editor/editorTerrain.hpp" #include "scene/scene.h" #include "scene/scenenode.h" #include "simulation/simulation.h" #include "rendering/renderer.h" #include "model/vertex.h" #include #include #include namespace { constexpr double kPi = 3.14159265358979323846; } bool editor_terrain::create(glm::dvec3 const &Center, int Cells, float CellSize, std::string const &TextureName, height_sampler const &Sampler) { if (Cells < 1 || CellSize <= 0.0f || simulation::Region == nullptr) return false; m_cells = Cells; m_cellsize = CellSize; double const half = 0.5 * static_cast(Cells) * CellSize; m_x0 = Center.x - half; m_z0 = Center.z - half; m_heights.assign(static_cast(Cells + 1) * (Cells + 1), static_cast(Center.y)); // optionally seed the grid by sampling whatever geometry is already there (terrain capture) if (Sampler) { for (int iz = 0; iz <= Cells; ++iz) for (int ix = 0; ix <= Cells; ++ix) { double const vx = m_x0 + static_cast(ix) * CellSize; double const vz = m_z0 + static_cast(iz) * CellSize; double y; if (Sampler(vx, vz, y)) m_heights[index(ix, iz)] = static_cast(y); } } m_material = TextureName.empty() ? null_handle : GfxRenderer->Fetch_Material(TextureName); // section-level shapes are rendered relative to the section centre, so that is our geometry origin scene::basic_section &sec = simulation::Region->section(Center); sec.create_geometry(); // ensure existing section geometry is already built (idempotent) m_origin = sec.m_area.center; m_section = &sec; std::vector verts; build_vertices(verts, false); m_vertexcount = verts.size(); scene::shape_node shape; shape.make_terrain(m_material, std::move(verts), m_origin); // upload to a dedicated bank; the renderer resolves draw calls by handle regardless of bank m_bank = GfxRenderer->Create_Bank(); shape.create_geometry(m_bank); // sets the shape's geometry handle, clears its CPU vertices m_geometry = shape.data().geometry; glm::dvec3 const shapecenter = shape.data().area.center; float const shaperadius = shape.radius(); // cached inside make_terrain, vertices already gone sec.m_shapes.emplace_back(std::move(shape)); // extend the section bounds so the new terrain isn't frustum-culled at its edges sec.m_area.radius = std::max( sec.m_area.radius, static_cast(glm::length(sec.m_area.center - shapecenter) + shaperadius)); return true; } glm::dvec3 editor_terrain::vertex_position(int Ix, int Iz) const { return glm::dvec3( m_x0 + static_cast(Ix) * m_cellsize, static_cast(m_heights[index(Ix, Iz)]), m_z0 + static_cast(Iz) * m_cellsize); } glm::vec3 editor_terrain::vertex_normal(int Ix, int Iz) const { // central differences on the heightfield; clamp to edges int const xl = std::max(0, Ix - 1), xr = std::min(m_cells, Ix + 1); int const zl = std::max(0, Iz - 1), zr = std::min(m_cells, Iz + 1); float const hl = m_heights[index(xl, Iz)], hr = m_heights[index(xr, Iz)]; float const hd = m_heights[index(Ix, zl)], hu = m_heights[index(Ix, zr)]; float const dx = static_cast((xr - xl)) * m_cellsize; float const dz = static_cast((zr - zl)) * m_cellsize; glm::vec3 n(-(hr - hl) / (dx > 0.f ? dx : 1.f), 1.0f, -(hu - hd) / (dz > 0.f ? dz : 1.f)); return glm::normalize(n); } world_vertex editor_terrain::make_vertex(int Ix, int Iz) const { world_vertex v; v.position = vertex_position(Ix, Iz); v.normal = vertex_normal(Ix, Iz); v.texture = glm::vec2(static_cast(Ix), static_cast(Iz)); return v; } // emits one quad (two upward-facing triangles) spanning grid corners (X0,Z0)..(X1,Z1) void editor_terrain::emit_quad(int X0, int Z0, int X1, int Z1, std::vector &Out) const { world_vertex const v00 = make_vertex(X0, Z0); world_vertex const v10 = make_vertex(X1, Z0); world_vertex const v01 = make_vertex(X0, Z1); world_vertex const v11 = make_vertex(X1, Z1); Out.push_back(v00); Out.push_back(v01); Out.push_back(v10); Out.push_back(v11); Out.push_back(v10); Out.push_back(v01); } // true if every grid vertex inside the block stays within Error of the bilinear plane of its corners bool editor_terrain::block_flat(int X0, int Z0, int X1, int Z1, float Error) const { float const h00 = m_heights[index(X0, Z0)]; float const h10 = m_heights[index(X1, Z0)]; float const h01 = m_heights[index(X0, Z1)]; float const h11 = m_heights[index(X1, Z1)]; double const wx = X1 - X0, wz = Z1 - Z0; for (int iz = Z0; iz <= Z1; ++iz) for (int ix = X0; ix <= X1; ++ix) { double const tx = (wx > 0.0) ? (ix - X0) / wx : 0.0; double const tz = (wz > 0.0) ? (iz - Z0) / wz : 0.0; double const top = h00 + tx * (h10 - h00); double const bot = h01 + tx * (h11 - h01); double const interp = top + tz * (bot - top); if (std::abs(static_cast(m_heights[index(ix, iz)]) - interp) > Error) return false; } return true; } // 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> &Out) const { bool const splitx = (X1 - X0) > 1; bool const splitz = (Z1 - Z0) > 1; if ((!splitx && !splitz) || block_flat(X0, Z0, X1, Z1, Error)) { Out.push_back({X0, Z0, X1, Z1}); return; } int const xm = splitx ? (X0 + X1) / 2 : X1; int const zm = splitz ? (Z0 + Z1) / 2 : Z1; collect_blocks(X0, Z0, xm, zm, Error, Out); if (splitx) collect_blocks(xm, Z0, X1, zm, Error, Out); if (splitz) collect_blocks(X0, zm, xm, Z1, Error, Out); if (splitx && splitz) collect_blocks(xm, zm, X1, Z1, Error, Out); } void editor_terrain::emit_block(std::array const &Block, std::vector const &Marked, std::vector &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> 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 &Out, bool Simplify) const { Out.clear(); Out.reserve(static_cast(m_cells) * m_cells * 6); if (Simplify) { std::vector> 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 marked(static_cast(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; } for (int iz = 0; iz < m_cells; ++iz) for (int ix = 0; ix < m_cells; ++ix) emit_quad(ix, iz, ix + 1, iz + 1, Out); } void editor_terrain::regenerate(bool Simplify) { if (!valid()) return; std::vector verts; build_vertices(verts, Simplify); gfx::vertex_array gpuverts; gpuverts.reserve(verts.size()); for (auto const &v : verts) gpuverts.emplace_back(gfx::basic_vertex::convert(v, m_origin)); gfx::userdata_array nouserdata; // fast path: same vertex count -> in-place swap into the existing chunk if (gpuverts.size() == m_vertexcount && (m_geometry.bank != 0 || m_geometry.chunk != 0)) { GfxRenderer->Replace(gpuverts, nouserdata, m_geometry, GL_TRIANGLES); return; } // count changed (optimize / un-optimize): upload a fresh chunk and point the shape at it gfx::geometry_handle const newhandle = GfxRenderer->Insert(gpuverts, nouserdata, m_bank, GL_TRIANGLES); if (m_section != nullptr) { for (auto &shape : m_section->m_shapes) { auto const h = shape.data().geometry; if (h.bank == m_geometry.bank && h.chunk == m_geometry.chunk) { shape.geometry(newhandle); break; } } } m_geometry = newhandle; m_vertexcount = gpuverts.size(); } void editor_terrain::optimize(float ErrorMetres) { m_simplify = true; m_simplify_error = (ErrorMetres > 0.0f ? ErrorMetres : 0.01f); m_dirty = false; regenerate(true); } void editor_terrain::unoptimize() { m_simplify = false; regenerate(false); } void editor_terrain::destroy() { if (m_section != nullptr) { // erase the shape whose geometry handle is ours; other shapes keep their handles (so they // keep rendering), and the geometry GC reclaims our now-undrawn chunk's GPU memory for (auto it = m_section->m_shapes.begin(); it != m_section->m_shapes.end(); ++it) { auto const h = it->data().geometry; if (h.bank == m_geometry.bank && h.chunk == m_geometry.chunk) { m_section->m_shapes.erase(it); break; } } } m_section = nullptr; m_geometry = gfx::geometry_handle{0, 0}; m_cells = 0; // mark invalid m_heights.clear(); } bool editor_terrain::contains(double X, double Z) const { double const x1 = m_x0 + static_cast(m_cells) * m_cellsize; double const z1 = m_z0 + static_cast(m_cells) * m_cellsize; return (X >= m_x0 && X <= x1 && Z >= m_z0 && Z <= z1); } double editor_terrain::height_at(double X, double Z) const { return height_in(m_heights, X, Z); } double editor_terrain::height_in(std::vector 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(std::floor(fx)); int iz = static_cast(std::floor(fz)); ix = std::clamp(ix, 0, m_cells - 1); iz = std::clamp(iz, 0, m_cells - 1); double const tx = std::clamp(fx - ix, 0.0, 1.0); double const tz = std::clamp(fz - iz, 0.0, 1.0); double const h00 = m_heights[index(ix, iz)]; double const h10 = m_heights[index(ix + 1, iz)]; double const h01 = m_heights[index(ix, iz + 1)]; double const h11 = m_heights[index(ix + 1, iz + 1)]; // matches the triangulation in build_vertices if (tx + tz <= 1.0) return h00 + tx * (h10 - h00) + tz * (h01 - h00); return h11 + (1.0 - tx) * (h01 - h11) + (1.0 - tz) * (h10 - h11); } bool editor_terrain::sculpt(double X, double Z, double Radius, double Strength) { if (!valid() || Radius <= 0.0) return false; bool changed = false; for (int iz = 0; iz <= m_cells; ++iz) for (int ix = 0; ix <= m_cells; ++ix) { double const vx = m_x0 + static_cast(ix) * m_cellsize; double const vz = m_z0 + static_cast(iz) * m_cellsize; double const d = std::sqrt((vx - X) * (vx - X) + (vz - Z) * (vz - Z)); if (d > Radius) continue; // smooth cosine falloff: full strength at the centre, zero at the rim double const falloff = 0.5 * (std::cos(kPi * d / Radius) + 1.0); m_heights[index(ix, iz)] += static_cast(Strength * falloff); changed = true; } if (changed) { // sculpting edits the full-resolution mesh (fixed vertex count => fast in-place update); // mark dirty so it can be auto-simplified once the stroke finishes, and modified for saving m_simplify = false; m_dirty = true; m_modified = true; regenerate(false); } return changed; } bool editor_terrain::smooth(double X, double Z, double Radius, double Amount, std::function 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(smoothed); return true; }); } bool editor_terrain::reshape(std::function 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(ix) * m_cellsize, m_z0 + static_cast(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 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(m_cells) * m_cellsize; double y = 0.0; if (!m_heights.empty()) y = m_heights[index(m_cells / 2, m_cells / 2)]; return glm::dvec3(m_x0 + c, y, m_z0 + c); }