mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-07-17 23:39:18 +02:00
Snap to ground || New terrain system || Terrain sculping || Mesh simplification
This commit is contained in:
@@ -167,6 +167,7 @@ set(SOURCES
|
||||
"application/driveruipanels.cpp"
|
||||
"input/editorkeyboardinput.cpp"
|
||||
"editor/editorSettings.cpp"
|
||||
"editor/editorTerrain.cpp"
|
||||
"application/editormode.cpp"
|
||||
"input/editormouseinput.cpp"
|
||||
"application/editoruilayer.cpp"
|
||||
|
||||
@@ -21,6 +21,8 @@ http://mozilla.org/MPL/2.0/.
|
||||
#include "Console.h"
|
||||
#include "rendering/renderer.h"
|
||||
#include "model/AnimModel.h"
|
||||
#include "model/Model3d.h"
|
||||
#include "utilities/Float3d.h"
|
||||
#include "scene/scene.h"
|
||||
|
||||
|
||||
@@ -29,8 +31,10 @@ http://mozilla.org/MPL/2.0/.
|
||||
#include "utilities/Logs.h"
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
// Static member initialization
|
||||
@@ -54,6 +58,65 @@ namespace
|
||||
return state == GLFW_PRESS;
|
||||
}
|
||||
|
||||
// tests whether the vertical line through (Px,Pz) passes over triangle abc; if so returns the
|
||||
// surface height at that point through OutY. used by the "snap to ground" (END) feature.
|
||||
inline bool triangle_height_at(glm::dvec3 const &a, glm::dvec3 const &b, glm::dvec3 const &c,
|
||||
double const Px, double const Pz, double &OutY)
|
||||
{
|
||||
double const ux = b.x - a.x, uz = b.z - a.z;
|
||||
double const vx = c.x - a.x, vz = c.z - a.z;
|
||||
double const wx = Px - a.x, wz = Pz - a.z;
|
||||
double const den = ux * vz - vx * uz;
|
||||
if (std::abs(den) < 1e-9)
|
||||
return false; // degenerate or vertical triangle, no defined height
|
||||
double const s = (wx * vz - vx * wz) / den;
|
||||
double const t = (ux * wz - wx * uz) / den;
|
||||
if (s < 0.0 || t < 0.0 || (s + t) > 1.0)
|
||||
return false;
|
||||
OutY = a.y + s * (b.y - a.y) + t * (c.y - a.y);
|
||||
return true;
|
||||
}
|
||||
|
||||
using world_triangle = std::array<glm::dvec3, 3>;
|
||||
|
||||
// walks a model's submodel tree (mirroring the renderer's transform chain) and appends every
|
||||
// mesh triangle, in world space, to Out. siblings are iterated to avoid deep recursion.
|
||||
void gather_submodel_triangles(TSubModel *Submodel, glm::dmat4 const &M, std::vector<world_triangle> &Out)
|
||||
{
|
||||
for (TSubModel *sub = Submodel; sub != nullptr; sub = sub->Next)
|
||||
{
|
||||
glm::dmat4 mlocal = M;
|
||||
if ((sub->iFlags & 0xC000) && (sub->GetMatrix() != nullptr))
|
||||
mlocal = M * glm::dmat4(glm::make_mat4(sub->GetMatrix()->readArray()));
|
||||
|
||||
if (sub->eType < TP_ROTATOR) // a drawable mesh, not a rotator/light/etc.
|
||||
{
|
||||
auto const handle = sub->m_geometry.handle;
|
||||
if (handle.bank != 0 || handle.chunk != 0)
|
||||
{
|
||||
auto const &verts = GfxRenderer->Vertices(handle);
|
||||
auto const &indices = GfxRenderer->Indices(handle);
|
||||
auto const to_world = [&](gfx::basic_vertex const &v) {
|
||||
return glm::dvec3(mlocal * glm::dvec4(glm::dvec3(v.position), 1.0));
|
||||
};
|
||||
if (false == indices.empty())
|
||||
{
|
||||
for (std::size_t i = 0; i + 2 < indices.size(); i += 3)
|
||||
Out.push_back({to_world(verts[indices[i]]), to_world(verts[indices[i + 1]]), to_world(verts[indices[i + 2]])});
|
||||
}
|
||||
else
|
||||
{
|
||||
for (std::size_t i = 0; i + 2 < verts.size(); i += 3)
|
||||
Out.push_back({to_world(verts[i]), to_world(verts[i + 1]), to_world(verts[i + 2])});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sub->Child != nullptr)
|
||||
gather_submodel_triangles(sub->Child, mlocal, Out); // children inherit this matrix
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool editor_mode::editormode_input::init()
|
||||
@@ -104,18 +167,132 @@ void editor_mode::start_focus(scene::basic_node *node, double duration)
|
||||
if (!node)
|
||||
return;
|
||||
|
||||
glm::dvec3 const center = node->location();
|
||||
|
||||
// distance that frames the object's bounding sphere within the vertical FOV, with some margin
|
||||
double const radius = std::max(1.0, static_cast<double>(node->radius()));
|
||||
double const fovy = glm::radians(static_cast<double>(Global.FieldOfView) / std::max(0.01, static_cast<double>(Global.ZoomFactor)));
|
||||
double distance = (radius / std::tan(fovy * 0.5)) * 1.6;
|
||||
distance = std::clamp(distance, radius * 1.5, static_cast<double>(kMaxPlacementDistance));
|
||||
|
||||
// keep the camera on the side it currently views from, so the move turns toward the object
|
||||
// rather than flying around it; fall back to a pleasant 3/4 direction when sitting on top of it
|
||||
glm::dvec3 dir = Camera.Pos - center;
|
||||
double const len = glm::length(dir);
|
||||
dir = (len > 1e-3) ? dir / len : glm::normalize(glm::dvec3(1.0, 0.5, 1.0));
|
||||
|
||||
m_focus_start_pos = Camera.Pos;
|
||||
m_focus_start_angle = Camera.Angle;
|
||||
m_focus_target_pos = center + dir * distance;
|
||||
|
||||
// target orientation looks from the target position straight at the object
|
||||
glm::dvec3 look = center - m_focus_target_pos;
|
||||
double const looklen = glm::length(look);
|
||||
if (looklen > 1e-6)
|
||||
look /= looklen;
|
||||
m_focus_target_angle = glm::vec3(
|
||||
static_cast<float>(std::asin(glm::clamp(look.y, -1.0, 1.0))), // pitch
|
||||
static_cast<float>(std::atan2(-look.x, -look.z)), // yaw
|
||||
0.0f); // roll
|
||||
|
||||
m_focus_active = true;
|
||||
m_focus_time = 0.0;
|
||||
m_focus_duration = duration;
|
||||
}
|
||||
|
||||
m_focus_start_pos = Camera.Pos;
|
||||
m_focus_start_angle = Camera.LookAt;
|
||||
void editor_mode::snap_to_ground(scene::basic_node *node)
|
||||
{
|
||||
if (!node || !simulation::Region)
|
||||
return;
|
||||
|
||||
m_focus_target_pos = node->location();
|
||||
glm::dvec3 const origin = node->location();
|
||||
if (!simulation::Region->point_inside(origin))
|
||||
return;
|
||||
|
||||
glm::dvec3 dir = m_focus_target_pos - m_focus_start_pos;
|
||||
double dist = glm::length(dir);
|
||||
m_focus_target_angle = m_focus_target_pos + glm::dvec3(10.0, 3.0, 10.0);
|
||||
// small tolerance so a node already resting on a surface still snaps cleanly to it
|
||||
double const epsilon = 0.05;
|
||||
double bestY = -std::numeric_limits<double>::max();
|
||||
bool found = false;
|
||||
|
||||
// record the highest surface that is at or below the node's current position at its (x,z)
|
||||
auto consider_triangle = [&](glm::dvec3 const &a, glm::dvec3 const &b, glm::dvec3 const &c) {
|
||||
double y;
|
||||
if (triangle_height_at(a, b, c, origin.x, origin.z, y) && y <= origin.y + epsilon && y > bestY)
|
||||
{
|
||||
bestY = y;
|
||||
found = true;
|
||||
}
|
||||
};
|
||||
|
||||
auto consider_shapes = [&](std::vector<scene::shape_node> const &shapes) {
|
||||
for (auto const &shape : shapes)
|
||||
{
|
||||
// quick reject: skip shapes whose bounding circle doesn't cover our (x,z) column
|
||||
auto const &sdata = shape.data();
|
||||
double const sdx = origin.x - sdata.area.center.x;
|
||||
double const sdz = origin.z - sdata.area.center.z;
|
||||
if (sdx * sdx + sdz * sdz > static_cast<double>(sdata.area.radius) * sdata.area.radius)
|
||||
continue;
|
||||
|
||||
auto const &verts = sdata.vertices;
|
||||
for (std::size_t i = 0; i + 2 < verts.size(); i += 3)
|
||||
consider_triangle(verts[i].position, verts[i + 1].position, verts[i + 2].position);
|
||||
}
|
||||
};
|
||||
|
||||
scene::basic_section &sec = simulation::Region->section(origin);
|
||||
// section level holds the large opaque geometry, including legacy terrain
|
||||
consider_shapes(sec.m_shapes);
|
||||
|
||||
// scan a 3x3 neighbourhood of cells for smaller geometry and other model instances below us
|
||||
for (int dz = -1; dz <= 1; ++dz)
|
||||
for (int dx = -1; dx <= 1; ++dx)
|
||||
{
|
||||
scene::basic_cell &cell = sec.cell(origin, glm::ivec2(dx, dz));
|
||||
consider_shapes(cell.m_shapesopaque);
|
||||
consider_shapes(cell.m_shapestranslucent);
|
||||
|
||||
// other instances are approximated by their bounding sphere, so a node can rest on top of them
|
||||
for (auto *inst : cell.m_instancesopaque)
|
||||
{
|
||||
if (!inst || inst == node)
|
||||
continue;
|
||||
glm::dvec3 const ic = inst->location();
|
||||
double const r = static_cast<double>(inst->radius());
|
||||
double const idx = origin.x - ic.x, idz = origin.z - ic.z;
|
||||
double const horiz2 = idx * idx + idz * idz;
|
||||
if (horiz2 < r * r)
|
||||
{
|
||||
double const ytop = ic.y + std::sqrt(r * r - horiz2);
|
||||
if (ytop <= origin.y + epsilon && ytop > bestY)
|
||||
{
|
||||
bestY = ytop;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// editable terrain patches keep their heightmap on the CPU, so query them directly
|
||||
for (auto const &terrain : m_terrains)
|
||||
{
|
||||
if (!terrain || !terrain->contains(origin.x, origin.z))
|
||||
continue;
|
||||
double const y = terrain->height_at(origin.x, origin.z);
|
||||
if (y <= origin.y + epsilon && y > bestY)
|
||||
{
|
||||
bestY = y;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
return;
|
||||
|
||||
push_snapshot(node, EditorSnapshot::Action::Move);
|
||||
glm::dvec3 target = origin;
|
||||
target.y = bestY;
|
||||
m_editor.translate(node, target, true); // true == apply the computed Y (free vertical move)
|
||||
}
|
||||
|
||||
void editor_mode::handle_brush_mouse_hold(int Action, int Button)
|
||||
@@ -439,6 +616,16 @@ bool editor_mode::update()
|
||||
|
||||
auto const deltarealtime = Timer::GetDeltaRenderTime();
|
||||
|
||||
// reconcile camera fly-mode with the real right-button state. ImGui is always fed the button
|
||||
// events (even when it captures the mouse), so io.MouseDown[1] is authoritative; if a release
|
||||
// was swallowed by an ImGui window while flying, force the editor out of fly-mode here so the
|
||||
// camera doesn't get stuck spinning with a hidden cursor.
|
||||
if (!ImGui::GetIO().MouseDown[1] && m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS)
|
||||
{
|
||||
m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT, GLFW_RELEASE);
|
||||
Application.set_cursor(GLFW_CURSOR_NORMAL);
|
||||
}
|
||||
|
||||
// fixed step render time routines (50 Hz)
|
||||
fTime50Hz += deltarealtime; // accumulate even when paused to keep frame reads stable
|
||||
while (fTime50Hz >= 1.0 / 50.0)
|
||||
@@ -481,6 +668,10 @@ bool editor_mode::update()
|
||||
|
||||
simulation::is_ready = true;
|
||||
|
||||
// continuous terrain sculpting while the left mouse button is held in sculpt mode
|
||||
if (m_terrain_sculpt && mouseHold)
|
||||
handle_terrain_sculpt(deltarealtime);
|
||||
|
||||
// --- ImGuizmo: in-viewport transform gizmo for the selected node ---
|
||||
render_gizmo();
|
||||
|
||||
@@ -514,12 +705,257 @@ void editor_mode::render_settings()
|
||||
ImGui::Separator();
|
||||
ImGui::Checkbox("Transform gizmo (ImGuizmo)", &m_gizmo_enabled);
|
||||
|
||||
render_terrain_ui();
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void editor_mode::render_terrain_ui()
|
||||
{
|
||||
ImGui::Separator();
|
||||
ImGui::TextUnformatted("Terrain");
|
||||
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
ImGui::InputInt("Grid cells", &m_terrain_cells);
|
||||
m_terrain_cells = std::clamp(m_terrain_cells, 1, 512);
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
ImGui::InputFloat("Cell size (m)", &m_terrain_cellsize);
|
||||
if (m_terrain_cellsize < 0.1f)
|
||||
m_terrain_cellsize = 0.1f;
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
ImGui::InputFloat("Base height (m)", &m_terrain_baseheight);
|
||||
ImGui::SetNextItemWidth(200.0f);
|
||||
ImGui::InputText("Texture (optional)", m_terrain_texture, IM_ARRAYSIZE(m_terrain_texture));
|
||||
|
||||
if (ImGui::Button("Create flat terrain"))
|
||||
{
|
||||
// centre the new patch horizontally on the camera, flat at the requested base height
|
||||
glm::dvec3 const center(Camera.Pos.x, static_cast<double>(m_terrain_baseheight), Camera.Pos.z);
|
||||
auto terrain = std::make_unique<editor_terrain>();
|
||||
if (terrain->create(center, m_terrain_cells, m_terrain_cellsize, std::string(m_terrain_texture)))
|
||||
m_terrains.push_back(std::move(terrain));
|
||||
else
|
||||
WriteLog("Editor: failed to create terrain", logtype::generic);
|
||||
}
|
||||
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
ImGui::InputInt("Chunks / side", &m_terrain_chunks);
|
||||
m_terrain_chunks = std::clamp(m_terrain_chunks, 1, 32);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Create chunked terrain"))
|
||||
create_chunked_terrain();
|
||||
ImGui::TextDisabled("total %d x %d m, %d chunks",
|
||||
static_cast<int>(m_terrain_chunks * m_terrain_cells * m_terrain_cellsize),
|
||||
static_cast<int>(m_terrain_chunks * m_terrain_cells * m_terrain_cellsize),
|
||||
m_terrain_chunks * m_terrain_chunks);
|
||||
|
||||
ImGui::Text("Patches: %zu", m_terrains.size());
|
||||
|
||||
// capture: sample the selected model's geometry into an editable patch and remove the original
|
||||
if (dynamic_cast<TAnimModel *>(m_node) != nullptr)
|
||||
{
|
||||
if (ImGui::Button("Capture selected model as terrain"))
|
||||
capture_terrain();
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::TextDisabled("Capture: select a model instance first");
|
||||
}
|
||||
|
||||
if (!m_terrains.empty())
|
||||
{
|
||||
ImGui::Separator();
|
||||
ImGui::Checkbox("Sculpt mode (LMB raise / Shift = lower)", &m_terrain_sculpt);
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
ImGui::InputFloat("Brush radius", &m_terrain_brush_radius);
|
||||
if (m_terrain_brush_radius < 0.5f)
|
||||
m_terrain_brush_radius = 0.5f;
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
ImGui::InputFloat("Brush strength", &m_terrain_brush_strength);
|
||||
|
||||
// one-shot nudge of the most recent patch at its centre, handy for a quick test
|
||||
auto &terrain = m_terrains.back();
|
||||
glm::dvec3 const c = terrain->centre();
|
||||
if (ImGui::Button("Raise centre"))
|
||||
terrain->sculpt(c.x, c.z, m_terrain_brush_radius, m_terrain_brush_strength);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Lower centre"))
|
||||
terrain->sculpt(c.x, c.z, m_terrain_brush_radius, -m_terrain_brush_strength);
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::TextUnformatted("Optimize (mesh simplification, all patches)");
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
ImGui::InputFloat("Flatness tol (m)", &m_terrain_simplify_error);
|
||||
if (m_terrain_simplify_error < 0.01f)
|
||||
m_terrain_simplify_error = 0.01f;
|
||||
if (ImGui::Button("Optimize all"))
|
||||
for (auto &t : m_terrains)
|
||||
if (t)
|
||||
t->optimize(m_terrain_simplify_error);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Full-res all"))
|
||||
for (auto &t : m_terrains)
|
||||
if (t)
|
||||
t->unoptimize();
|
||||
|
||||
std::size_t tris = 0, full = 0;
|
||||
for (auto &t : m_terrains)
|
||||
if (t)
|
||||
{
|
||||
tris += t->triangles();
|
||||
full += t->full_triangles();
|
||||
}
|
||||
ImGui::Text("Triangles: %zu / %zu", tris, full);
|
||||
}
|
||||
}
|
||||
|
||||
editor_terrain *editor_mode::terrain_at(double X, double Z)
|
||||
{
|
||||
for (auto &terrain : m_terrains)
|
||||
if (terrain && terrain->contains(X, Z))
|
||||
return terrain.get();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void editor_mode::create_chunked_terrain()
|
||||
{
|
||||
int const chunks = std::clamp(m_terrain_chunks, 1, 32);
|
||||
int const cells = std::clamp(m_terrain_cells, 1, 256);
|
||||
float const cellsize = std::max(0.1f, m_terrain_cellsize);
|
||||
|
||||
double const chunkextent = static_cast<double>(cells) * cellsize;
|
||||
double const half = 0.5 * chunks * chunkextent;
|
||||
double const x0 = Camera.Pos.x - half; // corner of the whole field
|
||||
double const z0 = Camera.Pos.z - half;
|
||||
|
||||
int created = 0;
|
||||
for (int cz = 0; cz < chunks; ++cz)
|
||||
for (int cx = 0; cx < chunks; ++cx)
|
||||
{
|
||||
// adjacent chunks share edges exactly (aligned grids, identical world coords),
|
||||
// so a world-space brush keeps the seams consistent
|
||||
glm::dvec3 const center(
|
||||
x0 + (cx + 0.5) * chunkextent,
|
||||
static_cast<double>(m_terrain_baseheight),
|
||||
z0 + (cz + 0.5) * chunkextent);
|
||||
auto terrain = std::make_unique<editor_terrain>();
|
||||
if (terrain->create(center, cells, cellsize, std::string(m_terrain_texture)))
|
||||
{
|
||||
m_terrains.push_back(std::move(terrain));
|
||||
++created;
|
||||
}
|
||||
}
|
||||
|
||||
WriteLog("Editor: created chunked terrain with " + std::to_string(created) + " chunks", logtype::generic);
|
||||
}
|
||||
|
||||
void editor_mode::handle_terrain_sculpt(double Deltatime)
|
||||
{
|
||||
// world point under the cursor (Mouse_Position is camera-relative, like the brush placement uses)
|
||||
glm::dvec3 const world = Camera.Pos + GfxRenderer->Mouse_Position();
|
||||
// only sculpt when the cursor is actually over terrain (avoids editing on a stale depth read)
|
||||
if (terrain_at(world.x, world.z) == nullptr)
|
||||
return;
|
||||
|
||||
double const rate = m_terrain_brush_strength * Deltatime; // metres applied this frame
|
||||
double const signedrate = (Global.shiftState ? -rate : rate);
|
||||
// apply to every chunk the brush touches; each patch clips to its own bounds, so a stroke
|
||||
// crossing a chunk boundary edits both and shared-edge vertices stay in sync
|
||||
for (auto &terrain : m_terrains)
|
||||
if (terrain)
|
||||
terrain->sculpt(world.x, world.z, m_terrain_brush_radius, signedrate);
|
||||
}
|
||||
|
||||
void editor_mode::capture_terrain()
|
||||
{
|
||||
TAnimModel *model = dynamic_cast<TAnimModel *>(m_node);
|
||||
if (model == nullptr || model->pModel == nullptr)
|
||||
{
|
||||
WriteLog("Editor: select a model instance to capture as terrain", logtype::generic);
|
||||
return;
|
||||
}
|
||||
|
||||
// instance world transform, matching the renderer: translate * rotateY * rotateX * rotateZ * scale
|
||||
glm::dmat4 rootm(1.0);
|
||||
rootm = glm::translate(rootm, model->location());
|
||||
glm::vec3 const angles = model->Angles();
|
||||
if (angles.y != 0.0f) rootm = glm::rotate(rootm, glm::radians(static_cast<double>(angles.y)), glm::dvec3(0.0, 1.0, 0.0));
|
||||
if (angles.x != 0.0f) rootm = glm::rotate(rootm, glm::radians(static_cast<double>(angles.x)), glm::dvec3(1.0, 0.0, 0.0));
|
||||
if (angles.z != 0.0f) rootm = glm::rotate(rootm, glm::radians(static_cast<double>(angles.z)), glm::dvec3(0.0, 0.0, 1.0));
|
||||
glm::vec3 const scale = model->Scale();
|
||||
rootm = glm::scale(rootm, glm::dvec3(scale));
|
||||
|
||||
std::vector<world_triangle> tris;
|
||||
gather_submodel_triangles(model->pModel->Root, rootm, tris);
|
||||
if (tris.empty())
|
||||
{
|
||||
WriteLog("Editor: selected model has no readable geometry to capture", logtype::generic);
|
||||
return;
|
||||
}
|
||||
|
||||
// horizontal bounds of the captured geometry
|
||||
glm::dvec3 lo(std::numeric_limits<double>::max());
|
||||
glm::dvec3 hi(-std::numeric_limits<double>::max());
|
||||
for (auto const &t : tris)
|
||||
for (auto const &p : t)
|
||||
{
|
||||
lo.x = std::min(lo.x, p.x); lo.y = std::min(lo.y, p.y); lo.z = std::min(lo.z, p.z);
|
||||
hi.x = std::max(hi.x, p.x); hi.y = std::max(hi.y, p.y); hi.z = std::max(hi.z, p.z);
|
||||
}
|
||||
|
||||
glm::dvec3 const center((lo.x + hi.x) * 0.5, lo.y, (lo.z + hi.z) * 0.5);
|
||||
double const extent = std::max(hi.x - lo.x, hi.z - lo.z);
|
||||
int const cells = std::max(1, m_terrain_cells);
|
||||
float const cellsize = static_cast<float>(std::max(0.1, extent / cells));
|
||||
|
||||
// sampler: highest captured triangle at (x,z)
|
||||
auto const sampler = [&tris](double X, double Z, double &OutY) -> bool {
|
||||
double best = -std::numeric_limits<double>::max();
|
||||
bool found = false;
|
||||
for (auto const &t : tris)
|
||||
{
|
||||
double const minx = std::min({t[0].x, t[1].x, t[2].x});
|
||||
double const maxx = std::max({t[0].x, t[1].x, t[2].x});
|
||||
double const minz = std::min({t[0].z, t[1].z, t[2].z});
|
||||
double const maxz = std::max({t[0].z, t[1].z, t[2].z});
|
||||
if (X < minx || X > maxx || Z < minz || Z > maxz)
|
||||
continue;
|
||||
double y;
|
||||
if (triangle_height_at(t[0], t[1], t[2], X, Z, y) && (!found || y > best))
|
||||
{
|
||||
best = y;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (found)
|
||||
OutY = best;
|
||||
return found;
|
||||
};
|
||||
|
||||
auto terrain = std::make_unique<editor_terrain>();
|
||||
if (!terrain->create(center, cells, cellsize, std::string(m_terrain_texture), sampler))
|
||||
{
|
||||
WriteLog("Editor: terrain capture failed", logtype::generic);
|
||||
return;
|
||||
}
|
||||
m_terrains.push_back(std::move(terrain));
|
||||
|
||||
// remove the original instance (recorded as a deletion so it can be undone)
|
||||
std::string as_text;
|
||||
model->export_as_text(as_text);
|
||||
push_snapshot(model, EditorSnapshot::Action::Delete, as_text);
|
||||
nullify_history_pointers(model);
|
||||
remove_from_hierarchy(model);
|
||||
m_node = nullptr;
|
||||
m_dragging = false;
|
||||
ui()->set_node(nullptr);
|
||||
simulation::State.delete_model(model);
|
||||
}
|
||||
|
||||
void editor_mode::render_gizmo()
|
||||
{
|
||||
if (!m_gizmo_enabled)
|
||||
// the transform gizmo is suppressed while sculpting terrain, so the brush owns the mouse
|
||||
if (!m_gizmo_enabled || m_terrain_sculpt)
|
||||
{
|
||||
m_gizmo_using = false;
|
||||
return;
|
||||
@@ -651,8 +1087,10 @@ void editor_mode::render_gizmo()
|
||||
|
||||
void editor_mode::update_camera(double const Deltatime)
|
||||
{
|
||||
// account for keyboard-driven motion
|
||||
// if focus animation active, interpolate camera toward target
|
||||
Camera.Update();
|
||||
|
||||
// focus animation runs after Camera.Update() so it overrides any residual velocity/rotation;
|
||||
// it smoothly drives both position and orientation toward the framed object
|
||||
if (m_focus_active)
|
||||
{
|
||||
m_focus_time += Deltatime;
|
||||
@@ -660,15 +1098,24 @@ void editor_mode::update_camera(double const Deltatime)
|
||||
if (t >= 1.0)
|
||||
t = 1.0;
|
||||
// smoothstep easing
|
||||
double s = t * t * (3.0 - 2.0 * t);
|
||||
Camera.Pos = glm::mix(m_focus_start_pos, m_focus_target_pos, s);
|
||||
Camera.LookAt = glm::mix(m_focus_start_angle, m_focus_target_angle, s);
|
||||
float const s = static_cast<float>(t * t * (3.0 - 2.0 * t));
|
||||
|
||||
Camera.Pos = glm::mix(m_focus_start_pos, m_focus_target_pos, static_cast<double>(s));
|
||||
|
||||
// interpolate angles, taking the shortest path around the yaw wrap-around
|
||||
constexpr float TWO_PI = 6.283185307179586f;
|
||||
float const dyaw = std::remainder(m_focus_target_angle.y - m_focus_start_angle.y, TWO_PI);
|
||||
Camera.Angle.x = m_focus_start_angle.x + (m_focus_target_angle.x - m_focus_start_angle.x) * s;
|
||||
Camera.Angle.y = m_focus_start_angle.y + dyaw * s;
|
||||
Camera.Angle.z = m_focus_start_angle.z + (m_focus_target_angle.z - m_focus_start_angle.z) * s;
|
||||
|
||||
// suppress any residual fly velocity so it doesn't fight the animation
|
||||
Camera.Velocity = glm::dvec3(0.0);
|
||||
|
||||
if (t >= 1.0)
|
||||
m_focus_active = false;
|
||||
}
|
||||
|
||||
Camera.Update();
|
||||
|
||||
// reset window state (will be set again if UI requires it)
|
||||
Global.CabWindowOpen = false;
|
||||
|
||||
@@ -838,6 +1285,16 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in
|
||||
}
|
||||
break;
|
||||
|
||||
case GLFW_KEY_END:
|
||||
if (is_press(Action) && m_node)
|
||||
{
|
||||
// Unreal-style "snap to floor": drop the selected node onto the surface below it.
|
||||
// works against triangle geometry (shape_node terrain / opaque shapes); once a proper
|
||||
// editable terrain mesh exists, dropping onto it works without further changes here.
|
||||
snap_to_ground(m_node);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -859,6 +1316,14 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const
|
||||
return;
|
||||
}
|
||||
|
||||
// in terrain sculpt mode the left button paints the terrain instead of picking nodes
|
||||
if (m_terrain_sculpt && Button == GLFW_MOUSE_BUTTON_LEFT)
|
||||
{
|
||||
mouseHold = is_press(Action);
|
||||
m_input.mouse.button(Button, Action);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Button == GLFW_MOUSE_BUTTON_LEFT)
|
||||
{
|
||||
|
||||
|
||||
@@ -15,6 +15,9 @@ http://mozilla.org/MPL/2.0/.
|
||||
#include "vehicle/Camera.h"
|
||||
#include "scene/sceneeditor.h"
|
||||
#include "scene/scenenode.h"
|
||||
#include "editor/editorTerrain.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
class editor_mode : public application_mode
|
||||
{
|
||||
@@ -136,6 +139,30 @@ class editor_mode : public application_mode
|
||||
// focus camera smoothly on specified node
|
||||
void start_focus(scene::basic_node *node, double duration = 0.6);
|
||||
|
||||
// drops the node straight down onto the nearest surface below (terrain or another object)
|
||||
void snap_to_ground(scene::basic_node *node);
|
||||
|
||||
// editable terrain patches created in the editor
|
||||
void render_terrain_ui();
|
||||
// creates a large terrain as a grid of adjacent chunks (each its own editable patch)
|
||||
void create_chunked_terrain();
|
||||
// raises/lowers terrain under the cursor while the left mouse button is held in sculpt mode
|
||||
void handle_terrain_sculpt(double Deltatime);
|
||||
// returns the terrain patch (if any) whose footprint covers the given world point
|
||||
editor_terrain *terrain_at(double X, double Z);
|
||||
// samples the selected model instance's geometry into a new editable terrain patch, then removes it
|
||||
void capture_terrain();
|
||||
std::vector<std::unique_ptr<editor_terrain>> m_terrains;
|
||||
bool m_terrain_sculpt{false}; // when true, LMB sculpts terrain instead of picking
|
||||
int m_terrain_cells{32}; // grid resolution (quads per side)
|
||||
int m_terrain_chunks{4}; // chunks per side for a chunked terrain
|
||||
float m_terrain_cellsize{2.0f}; // metres per quad
|
||||
float m_terrain_baseheight{0.0f}; // flat starting height
|
||||
float m_terrain_brush_radius{12.0f};
|
||||
float m_terrain_brush_strength{4.0f}; // metres per second while held (one-shot for the buttons)
|
||||
float m_terrain_simplify_error{0.5f}; // flatness tolerance (m) for mesh simplification
|
||||
char m_terrain_texture[128]{""}; // optional ground texture name
|
||||
|
||||
// hierarchy management
|
||||
void add_to_hierarchy(scene::basic_node *node);
|
||||
void remove_from_hierarchy(scene::basic_node *node);
|
||||
|
||||
315
editor/editorTerrain.cpp
Normal file
315
editor/editorTerrain.cpp
Normal file
@@ -0,0 +1,315 @@
|
||||
/*
|
||||
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 <glad/glad.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
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<double>(Cells) * CellSize;
|
||||
m_x0 = Center.x - half;
|
||||
m_z0 = Center.z - half;
|
||||
m_heights.assign(static_cast<std::size_t>(Cells + 1) * (Cells + 1), static_cast<float>(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<double>(ix) * CellSize;
|
||||
double const vz = m_z0 + static_cast<double>(iz) * CellSize;
|
||||
double y;
|
||||
if (Sampler(vx, vz, y))
|
||||
m_heights[index(ix, iz)] = static_cast<float>(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<world_vertex> 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<float>(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<double>(Ix) * m_cellsize,
|
||||
static_cast<double>(m_heights[index(Ix, Iz)]),
|
||||
m_z0 + static_cast<double>(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<float>((xr - xl)) * m_cellsize;
|
||||
float const dz = static_cast<float>((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<float>(Ix), static_cast<float>(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<world_vertex> &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<double>(m_heights[index(ix, iz)]) - interp) > Error)
|
||||
return false;
|
||||
}
|
||||
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
|
||||
{
|
||||
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);
|
||||
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);
|
||||
if (splitx)
|
||||
emit_block(xm, Z0, X1, zm, Error, Out);
|
||||
if (splitz)
|
||||
emit_block(X0, zm, xm, Z1, Error, Out);
|
||||
if (splitx && splitz)
|
||||
emit_block(xm, zm, X1, Z1, Error, Out);
|
||||
}
|
||||
|
||||
void editor_terrain::build_vertices(std::vector<world_vertex> &Out, bool Simplify) const
|
||||
{
|
||||
Out.clear();
|
||||
Out.reserve(static_cast<std::size_t>(m_cells) * m_cells * 6);
|
||||
|
||||
if (Simplify)
|
||||
{
|
||||
emit_block(0, 0, m_cells, m_cells, m_simplify_error, 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<world_vertex> 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);
|
||||
regenerate(true);
|
||||
}
|
||||
|
||||
void editor_terrain::unoptimize()
|
||||
{
|
||||
m_simplify = false;
|
||||
regenerate(false);
|
||||
}
|
||||
|
||||
bool editor_terrain::contains(double X, double Z) const
|
||||
{
|
||||
double const x1 = m_x0 + static_cast<double>(m_cells) * m_cellsize;
|
||||
double const z1 = m_z0 + static_cast<double>(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
|
||||
{
|
||||
double const fx = (X - m_x0) / m_cellsize;
|
||||
double const fz = (Z - m_z0) / m_cellsize;
|
||||
int ix = static_cast<int>(std::floor(fx));
|
||||
int iz = static_cast<int>(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<double>(ix) * m_cellsize;
|
||||
double const vz = m_z0 + static_cast<double>(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<float>(Strength * falloff);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
// sculpting edits the full-resolution mesh (fixed vertex count => fast in-place update);
|
||||
// the user can re-run optimize() afterwards to simplify again
|
||||
m_simplify = false;
|
||||
regenerate(false);
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
glm::dvec3 editor_terrain::centre() const
|
||||
{
|
||||
double const c = 0.5 * static_cast<double>(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);
|
||||
}
|
||||
97
editor/editorTerrain.hpp
Normal file
97
editor/editorTerrain.hpp
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
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/.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include "utilities/Classes.h" // material_handle
|
||||
#include "interfaces/ITexture.h" // null_handle
|
||||
#include "rendering/geometrybank.h" // gfx::geometry_handle / geometrybank_handle
|
||||
|
||||
namespace scene { class basic_section; }
|
||||
|
||||
// Editor-owned, editable terrain patch.
|
||||
//
|
||||
// The engine's scene::shape_node drops its CPU-side vertices the moment it uploads them to the GPU,
|
||||
// so it can't be edited or raycast after load. This class keeps the authoritative, editable data
|
||||
// (a regular grid heightmap) on the CPU, generates a shape_node purely for rendering, and answers
|
||||
// height/raycast queries directly from the heightmap (fast and exact). Sculpting updates the
|
||||
// heightmap and pushes the new vertex positions into the shape's existing geometry chunk.
|
||||
class editor_terrain
|
||||
{
|
||||
public:
|
||||
editor_terrain() = default;
|
||||
|
||||
// builds an NxN-cell grid centred on Center, each quad CellSize metres across, using the
|
||||
// material fetched from TextureName (empty => untextured). when Sampler is supplied it provides
|
||||
// the starting height at each grid vertex (returns false to fall back to Center.y) - used to
|
||||
// capture existing terrain. returns false on failure.
|
||||
using height_sampler = std::function<bool(double X, double Z, double &OutY)>;
|
||||
bool create(glm::dvec3 const &Center, int Cells, float CellSize, std::string const &TextureName,
|
||||
height_sampler const &Sampler = {});
|
||||
|
||||
// true if (X,Z) lies within the terrain's horizontal footprint
|
||||
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;
|
||||
|
||||
// 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);
|
||||
|
||||
// rebuilds the rendered mesh, collapsing regions flatter than ErrorMetres into larger quads
|
||||
// (adaptive quadtree). the editable heightmap is untouched, so sculpting/raycast stay exact.
|
||||
void optimize(float ErrorMetres);
|
||||
// rebuilds the rendered mesh at full resolution (undoes optimize)
|
||||
void unoptimize();
|
||||
|
||||
// horizontal centre and extent, handy for the UI / camera framing
|
||||
glm::dvec3 centre() const;
|
||||
float extent() const { return m_cells * m_cellsize; }
|
||||
bool valid() const { return m_cells > 0; }
|
||||
bool optimized() const { return m_simplify; }
|
||||
// rendered triangle count (drops after optimize)
|
||||
std::size_t triangles() const { return m_vertexcount / 3; }
|
||||
// full-resolution triangle count, for reference
|
||||
std::size_t full_triangles() const { return static_cast<std::size_t>(m_cells) * m_cells * 2; }
|
||||
|
||||
private:
|
||||
int index(int Ix, int Iz) const { return Iz * (m_cells + 1) + Ix; }
|
||||
glm::dvec3 vertex_position(int Ix, int Iz) const;
|
||||
glm::vec3 vertex_normal(int Ix, int Iz) const;
|
||||
world_vertex make_vertex(int Ix, int Iz) const;
|
||||
// fills Out with the GL_TRIANGLES world-space vertex list; Simplify enables adaptive merging
|
||||
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 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)
|
||||
void regenerate(bool Simplify);
|
||||
|
||||
int m_cells{0}; // quads per side; (m_cells+1)^2 grid vertices
|
||||
float m_cellsize{1.0f}; // metres per quad
|
||||
double m_x0{0.0}, m_z0{0.0}; // world position of grid corner (ix=0, iz=0)
|
||||
std::vector<float> m_heights; // per-vertex world Y, row-major (m_cells+1)^2
|
||||
|
||||
material_handle m_material{null_handle};
|
||||
gfx::geometrybank_handle m_bank{0, 0}; // geometry bank owning the rendered chunk
|
||||
gfx::geometry_handle m_geometry{0, 0}; // rendered chunk
|
||||
std::size_t m_vertexcount{0}; // current chunk's vertex count (for Replace vs recreate)
|
||||
glm::dvec3 m_origin{0.0}; // origin the GPU vertices are stored relative to
|
||||
scene::basic_section *m_section{nullptr}; // section holding the shape, for handle swaps
|
||||
|
||||
bool m_simplify{false}; // whether the rendered mesh is currently simplified
|
||||
float m_simplify_error{0.5f}; // flatness tolerance used by optimize()
|
||||
};
|
||||
@@ -423,6 +423,32 @@ shape_node::convert( TSubModel const *Submodel ) {
|
||||
return *this;
|
||||
}
|
||||
|
||||
// builds an opaque, always-visible shape from a world-space GL_TRIANGLES vertex list
|
||||
shape_node &
|
||||
shape_node::make_terrain( material_handle const Material, std::vector<world_vertex> Vertices, glm::dvec3 const Origin ) {
|
||||
|
||||
m_data.material = Material;
|
||||
m_data.translucent = false;
|
||||
m_data.visible = true;
|
||||
m_data.rangesquared_min = 0.0;
|
||||
m_data.rangesquared_max = std::numeric_limits<double>::max();
|
||||
m_data.origin = Origin;
|
||||
m_data.vertices = std::move( Vertices );
|
||||
|
||||
// bounding area from the supplied geometry
|
||||
m_data.area.center = glm::dvec3( 0.0 );
|
||||
if( false == m_data.vertices.empty() ) {
|
||||
for( auto const &vertex : m_data.vertices ) {
|
||||
m_data.area.center += vertex.position;
|
||||
}
|
||||
m_data.area.center /= static_cast<double>( m_data.vertices.size() );
|
||||
}
|
||||
invalidate_radius();
|
||||
radius(); // force recompute now, while vertices are still present
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
// adds content of provided node to already enclosed geometry. returns: true if merge could be performed
|
||||
bool
|
||||
shape_node::merge( shape_node &Shape ) {
|
||||
|
||||
@@ -120,6 +120,10 @@ public:
|
||||
// imports data from provided submodel
|
||||
shape_node &
|
||||
convert( TSubModel const *Submodel );
|
||||
// builds an opaque, always-visible shape from a world-space GL_TRIANGLES vertex list, stored
|
||||
// relative to Origin. used by the editor's terrain to own editable geometry it can re-upload.
|
||||
shape_node &
|
||||
make_terrain( material_handle const Material, std::vector<world_vertex> Vertices, glm::dvec3 const Origin );
|
||||
// adds content of provided node to already enclosed geometry. returns: true if merge could be performed
|
||||
bool
|
||||
merge( shape_node &Shape );
|
||||
@@ -138,6 +142,9 @@ public:
|
||||
// set origin point
|
||||
void
|
||||
origin( glm::dvec3 Origin );
|
||||
// replaces the renderable geometry handle (used by the editor when it re-uploads terrain geometry)
|
||||
void
|
||||
geometry( gfx::geometry_handle const &Handle );
|
||||
// data access
|
||||
shapenode_data const &
|
||||
data() const;
|
||||
@@ -163,6 +170,12 @@ void
|
||||
shape_node::origin( glm::dvec3 Origin ) {
|
||||
m_data.origin = Origin;
|
||||
}
|
||||
// replaces the renderable geometry handle
|
||||
inline
|
||||
void
|
||||
shape_node::geometry( gfx::geometry_handle const &Handle ) {
|
||||
m_data.geometry = Handle;
|
||||
}
|
||||
// data access
|
||||
inline
|
||||
shape_node::shapenode_data const &
|
||||
|
||||
Reference in New Issue
Block a user