mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-30 23:40:29 +01:00
Editor terrain: texture browser, undo, conversion from terrain files
- Terrain work mode shows a browser of the textures folder in place of the node bank; textures/materials can be dragged onto the palette entries (replacing the material), the material field and the water material field - Ctrl+Z/Ctrl+Y for heightmap terrain changes: a brush stroke or an operation is one step of the editor history - Ground conversion reads only the files of the terrain directives (.txtf with origin/rotate, or .btf without text), paints the chunks with the materials of the triangles, drops only the shapes of those files - Fix: ground grids converted out-of-range floats to int, so whole-scene queries returned the triangles of a single cell (shifted chunks) - Fix: terrain layers copied from textures already uploaded without their data stayed grey - Fix: ImGui ID conflict between terrain tool radios and section headers - Fix: layers of terrain .txtf files named with the scenery/ prefix and could not be saved - Editor selftest commands for the terrain
This commit is contained in:
@@ -191,6 +191,7 @@ set(SOURCES
|
|||||||
"application/editormodetrackux.cpp"
|
"application/editormodetrackux.cpp"
|
||||||
"application/editormoderoad.cpp"
|
"application/editormoderoad.cpp"
|
||||||
"application/editormodeprofile.cpp"
|
"application/editormodeprofile.cpp"
|
||||||
|
"application/editormodetextures.cpp"
|
||||||
"application/editormodegauge.cpp"
|
"application/editormodegauge.cpp"
|
||||||
"application/editormodeinfra.cpp"
|
"application/editormodeinfra.cpp"
|
||||||
"application/editormodespeed.cpp"
|
"application/editormodespeed.cpp"
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ extern char **environ;
|
|||||||
#include "utilities/Float3d.h"
|
#include "utilities/Float3d.h"
|
||||||
#include "scene/scene.h"
|
#include "scene/scene.h"
|
||||||
#include "scene/scenelayers.h"
|
#include "scene/scenelayers.h"
|
||||||
|
#include "scene/sceneterrain.h"
|
||||||
#include "utilities/parser.h"
|
#include "utilities/parser.h"
|
||||||
#include "utilities/utilities.h"
|
#include "utilities/utilities.h"
|
||||||
|
|
||||||
@@ -213,8 +214,8 @@ namespace
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// highest surface at (X,Z) that isn't above Ceiling. returns: true if there is one
|
// highest surface at (X,Z) that isn't above Ceiling. returns: true if there is one, and which triangle it is in Index if asked
|
||||||
bool height_at(double const X, double const Z, double const Ceiling, double &OutY) const
|
bool height_at(double const X, double const Z, double const Ceiling, double &OutY, std::uint32_t *Index = nullptr) const
|
||||||
{
|
{
|
||||||
bool found = false;
|
bool found = false;
|
||||||
for (auto const idx : m_cells[static_cast<std::size_t>(row(Z)) * m_columns + column(X)])
|
for (auto const idx : m_cells[static_cast<std::size_t>(row(Z)) * m_columns + column(X)])
|
||||||
@@ -225,6 +226,8 @@ namespace
|
|||||||
{
|
{
|
||||||
OutY = y;
|
OutY = y;
|
||||||
found = true;
|
found = true;
|
||||||
|
if (Index != nullptr)
|
||||||
|
*Index = idx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return found;
|
return found;
|
||||||
@@ -233,11 +236,11 @@ namespace
|
|||||||
private:
|
private:
|
||||||
int column(double const X) const
|
int column(double const X) const
|
||||||
{
|
{
|
||||||
return std::clamp(static_cast<int>(std::floor((X - m_min.x) / m_cellsize)), 0, m_columns - 1);
|
return static_cast<int>(std::clamp(std::floor((X - m_min.x) / m_cellsize), 0.0, static_cast<double>(m_columns - 1)));
|
||||||
}
|
}
|
||||||
int row(double const Z) const
|
int row(double const Z) const
|
||||||
{
|
{
|
||||||
return std::clamp(static_cast<int>(std::floor((Z - m_min.y) / m_cellsize)), 0, m_rows - 1);
|
return static_cast<int>(std::clamp(std::floor((Z - m_min.y) / m_cellsize), 0.0, static_cast<double>(m_rows - 1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<world_triangle> m_triangles;
|
std::vector<world_triangle> m_triangles;
|
||||||
@@ -498,6 +501,7 @@ editor_mode::editor_mode() {
|
|||||||
ui()->set_track_options([this]() { render_track_tool_options(); }, [this]() { render_track_selection(); });
|
ui()->set_track_options([this]() { render_track_tool_options(); }, [this]() { render_track_selection(); });
|
||||||
ui()->set_workspace_options(editor_ui::workspace::roads, [this]() { render_road_tool_options(); }, [this]() { render_road_selection(); });
|
ui()->set_workspace_options(editor_ui::workspace::roads, [this]() { render_road_tool_options(); }, [this]() { render_road_selection(); });
|
||||||
ui()->set_workspace_options(editor_ui::workspace::terrain, [this]() { render_terrain_tool_options(); });
|
ui()->set_workspace_options(editor_ui::workspace::terrain, [this]() { render_terrain_tool_options(); });
|
||||||
|
ui()->set_workspace_bank(editor_ui::workspace::terrain, [this]() { render_texture_browser(); }, STR("Textures"));
|
||||||
// a node picked in the scene window is selected as if clicked in the view; a double click flies the camera to it
|
// a node picked in the scene window is selected as if clicked in the view; a double click flies the camera to it
|
||||||
ui()->set_scene_select([this](scene::basic_node *Node, bool const Focus) {
|
ui()->set_scene_select([this](scene::basic_node *Node, bool const Focus) {
|
||||||
if (Focus)
|
if (Focus)
|
||||||
@@ -876,8 +880,50 @@ void editor_mode::nullify_history_pointers(scene::basic_node *node)
|
|||||||
m_array.base = nullptr;
|
m_array.base = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void editor_mode::terrain_history_step()
|
||||||
|
{
|
||||||
|
if (false == m_streamer.active())
|
||||||
|
return;
|
||||||
|
if (m_streamer.recording())
|
||||||
|
{
|
||||||
|
if (terrain_brush() && mouseHold && false == m_history_busy)
|
||||||
|
return; // the stroke goes on
|
||||||
|
auto change{m_streamer.end_change()};
|
||||||
|
if (false == change.empty())
|
||||||
|
{
|
||||||
|
trim_history();
|
||||||
|
EditorSnapshot snap;
|
||||||
|
snap.action = EditorSnapshot::Action::Terrain;
|
||||||
|
snap.node_name = STR("terrain");
|
||||||
|
if (false == change.chunks.empty())
|
||||||
|
{
|
||||||
|
auto const centre{heightmap::chunk_corner(change.chunks.begin()->first) + glm::dvec2{heightmap::chunk_size * 0.5}};
|
||||||
|
snap.position = {centre.x, 0.0, centre.y};
|
||||||
|
}
|
||||||
|
snap.terrain = std::make_shared<terrain_streamer::change const>(std::move(change));
|
||||||
|
m_history.push_back(std::move(snap));
|
||||||
|
g_redo.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_streamer.begin_change();
|
||||||
|
}
|
||||||
|
|
||||||
|
void editor_mode::restore_terrain(EditorSnapshot &Snapshot)
|
||||||
|
{
|
||||||
|
if (Snapshot.terrain == nullptr)
|
||||||
|
return;
|
||||||
|
auto opposite{m_streamer.restore(*Snapshot.terrain)};
|
||||||
|
Snapshot.terrain = std::make_shared<terrain_streamer::change const>(std::move(opposite));
|
||||||
|
forget_ground();
|
||||||
|
m_water_selected = -1;
|
||||||
|
}
|
||||||
|
|
||||||
void editor_mode::undo_last()
|
void editor_mode::undo_last()
|
||||||
{
|
{
|
||||||
|
// a change of the terrain made a moment ago is a step of its own before it's taken back
|
||||||
|
m_history_busy = true;
|
||||||
|
terrain_history_step();
|
||||||
|
m_history_busy = false;
|
||||||
// the changes made together are taken back together, and their models are the selection again
|
// the changes made together are taken back together, and their models are the selection again
|
||||||
std::vector<scene::basic_node *> touched;
|
std::vector<scene::basic_node *> touched;
|
||||||
while (false == m_history.empty())
|
while (false == m_history.empty())
|
||||||
@@ -906,6 +952,13 @@ void editor_mode::undo_one()
|
|||||||
EditorSnapshot snap = m_history.back();
|
EditorSnapshot snap = m_history.back();
|
||||||
m_history.pop_back();
|
m_history.pop_back();
|
||||||
|
|
||||||
|
if (snap.action == EditorSnapshot::Action::Terrain)
|
||||||
|
{
|
||||||
|
restore_terrain(snap);
|
||||||
|
g_redo.push_back(std::move(snap));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (snap.action == EditorSnapshot::Action::Bend)
|
if (snap.action == EditorSnapshot::Action::Bend)
|
||||||
{
|
{
|
||||||
restore_bend(snap, true);
|
restore_bend(snap, true);
|
||||||
@@ -1036,6 +1089,10 @@ void editor_mode::undo_one()
|
|||||||
|
|
||||||
void editor_mode::redo_last()
|
void editor_mode::redo_last()
|
||||||
{
|
{
|
||||||
|
// a change of the terrain made since the undo goes to the history, and the redo steps are dropped with it
|
||||||
|
m_history_busy = true;
|
||||||
|
terrain_history_step();
|
||||||
|
m_history_busy = false;
|
||||||
// the first of the changes made together is on the top, the others follow it
|
// the first of the changes made together is on the top, the others follow it
|
||||||
std::vector<scene::basic_node *> touched;
|
std::vector<scene::basic_node *> touched;
|
||||||
do
|
do
|
||||||
@@ -1060,6 +1117,13 @@ void editor_mode::redo_one()
|
|||||||
EditorSnapshot snap = g_redo.back();
|
EditorSnapshot snap = g_redo.back();
|
||||||
g_redo.pop_back();
|
g_redo.pop_back();
|
||||||
|
|
||||||
|
if (snap.action == EditorSnapshot::Action::Terrain)
|
||||||
|
{
|
||||||
|
restore_terrain(snap);
|
||||||
|
m_history.push_back(std::move(snap));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (snap.action == EditorSnapshot::Action::Bend)
|
if (snap.action == EditorSnapshot::Action::Bend)
|
||||||
{
|
{
|
||||||
restore_bend(snap, false);
|
restore_bend(snap, false);
|
||||||
@@ -1238,6 +1302,7 @@ bool editor_mode::update()
|
|||||||
m_dragging = false;
|
m_dragging = false;
|
||||||
m_input.mouse.button(GLFW_MOUSE_BUTTON_LEFT, GLFW_RELEASE);
|
m_input.mouse.button(GLFW_MOUSE_BUTTON_LEFT, GLFW_RELEASE);
|
||||||
}
|
}
|
||||||
|
terrain_history_step();
|
||||||
|
|
||||||
// fixed step render time routines (50 Hz)
|
// fixed step render time routines (50 Hz)
|
||||||
fTime50Hz += deltarealtime; // accumulate even when paused to keep frame reads stable
|
fTime50Hz += deltarealtime; // accumulate even when paused to keep frame reads stable
|
||||||
@@ -1479,8 +1544,8 @@ namespace
|
|||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// grid points without a height take the height of the nearest point which has one (by steps along the grid)
|
// grid points without a height (or another value) take the one of the nearest point which has it (by steps along the grid)
|
||||||
void fill_from_nearest(std::vector<double> &Heights, std::vector<char> &Found, std::size_t const Side)
|
template <typename Value_> void fill_from_nearest(std::vector<Value_> &Heights, std::vector<char> &Found, std::size_t const Side)
|
||||||
{
|
{
|
||||||
std::deque<std::size_t> queue;
|
std::deque<std::size_t> queue;
|
||||||
for (std::size_t index = 0; index < Found.size(); ++index)
|
for (std::size_t index = 0; index < Found.size(); ++index)
|
||||||
@@ -1530,6 +1595,8 @@ void editor_mode::render_terrain_ui()
|
|||||||
if (ImGui::IsItemHovered())
|
if (ImGui::IsItemHovered())
|
||||||
ImGui::SetTooltip("%s", Tooltip);
|
ImGui::SetTooltip("%s", Tooltip);
|
||||||
};
|
};
|
||||||
|
// the tools are named like the headers below, which would give them the same IDs
|
||||||
|
ImGui::PushID("terrain_tools");
|
||||||
radio(STR_C("Select"), terrain_tool::none, STR_C("LMB picks the models, as in the surroundings"));
|
radio(STR_C("Select"), terrain_tool::none, STR_C("LMB picks the models, as in the surroundings"));
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
radio(STR_C("Chunks"), terrain_tool::chunks, STR_C("LMB adds a chunk shaped as the ground under it, next to the clicked one; Shift+LMB removes the clicked chunk"));
|
radio(STR_C("Chunks"), terrain_tool::chunks, STR_C("LMB adds a chunk shaped as the ground under it, next to the clicked one; Shift+LMB removes the clicked chunk"));
|
||||||
@@ -1542,6 +1609,7 @@ void editor_mode::render_terrain_ui()
|
|||||||
radio(STR_C("Paint"), terrain_tool::paint, STR_C("LMB paints the material chosen in the palette, Shift+LMB the first material of the palette"));
|
radio(STR_C("Paint"), terrain_tool::paint, STR_C("LMB paints the material chosen in the palette, Shift+LMB the first material of the palette"));
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
radio(STR_C("Water"), terrain_tool::water, STR_C("LMB adds a point of the outline of a body of water, Shift+LMB takes the last one back"));
|
radio(STR_C("Water"), terrain_tool::water, STR_C("LMB adds a point of the outline of a body of water, Shift+LMB takes the last one back"));
|
||||||
|
ImGui::PopID();
|
||||||
m_terrain_tool = static_cast<terrain_tool>(tool);
|
m_terrain_tool = static_cast<terrain_tool>(tool);
|
||||||
|
|
||||||
if (ImGui::CollapsingHeader(STR_C("Chunks"), ImGuiTreeNodeFlags_DefaultOpen))
|
if (ImGui::CollapsingHeader(STR_C("Chunks"), ImGuiTreeNodeFlags_DefaultOpen))
|
||||||
@@ -1594,6 +1662,10 @@ void editor_mode::render_terrain_ui()
|
|||||||
auto const label{layers[i].size > 0.f ? format("%zu: %s (%.1f m)", i, layers[i].material.c_str(), layers[i].size) : format("%zu: %s", i, layers[i].material.c_str())};
|
auto const label{layers[i].size > 0.f ? format("%zu: %s (%.1f m)", i, layers[i].material.c_str(), layers[i].size) : format("%zu: %s", i, layers[i].material.c_str())};
|
||||||
if (ImGui::Selectable(label.c_str(), static_cast<int>(i) == m_terrain_layer))
|
if (ImGui::Selectable(label.c_str(), static_cast<int>(i) == m_terrain_layer))
|
||||||
m_terrain_layer = static_cast<int>(i);
|
m_terrain_layer = static_cast<int>(i);
|
||||||
|
// a material dropped on an entry takes its place, on every chunk painted with it
|
||||||
|
std::string dropped;
|
||||||
|
if (material_drop(dropped))
|
||||||
|
m_streamer.layer_material(static_cast<std::uint16_t>(i), dropped);
|
||||||
}
|
}
|
||||||
if (false == layers.empty())
|
if (false == layers.empty())
|
||||||
{
|
{
|
||||||
@@ -1607,10 +1679,12 @@ void editor_mode::render_terrain_ui()
|
|||||||
}
|
}
|
||||||
ImGui::SetNextItemWidth(160.0f);
|
ImGui::SetNextItemWidth(160.0f);
|
||||||
ImGui::InputText(STR_C("Material"), m_terrain_material, IM_ARRAYSIZE(m_terrain_material));
|
ImGui::InputText(STR_C("Material"), m_terrain_material, IM_ARRAYSIZE(m_terrain_material));
|
||||||
|
material_drop(m_terrain_material, IM_ARRAYSIZE(m_terrain_material));
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ImGui::Button(STR_C("Add to the palette")) && m_terrain_material[0] != '\0')
|
if (ImGui::Button(STR_C("Add to the palette")) && m_terrain_material[0] != '\0')
|
||||||
m_terrain_layer = m_streamer.layer(m_terrain_material, m_terrain_material_size);
|
m_terrain_layer = m_streamer.layer(m_terrain_material, m_terrain_material_size);
|
||||||
ImGui::TextDisabled("%s", STR_C("New chunks are covered with the first material. A chunk holds up to 8 of them."));
|
ImGui::TextDisabled("%s", STR_C("New chunks are covered with the first material. A chunk holds up to 8 of them. "
|
||||||
|
"A texture dragged from the browser onto an entry of the palette takes its place."));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ImGui::CollapsingHeader(STR_C("Water"), m_terrain_tool == terrain_tool::water ? ImGuiTreeNodeFlags_DefaultOpen : 0))
|
if (ImGui::CollapsingHeader(STR_C("Water"), m_terrain_tool == terrain_tool::water ? ImGuiTreeNodeFlags_DefaultOpen : 0))
|
||||||
@@ -1623,6 +1697,7 @@ void editor_mode::render_terrain_ui()
|
|||||||
m_water_level_edited = true;
|
m_water_level_edited = true;
|
||||||
ImGui::SetNextItemWidth(160.0f);
|
ImGui::SetNextItemWidth(160.0f);
|
||||||
ImGui::InputText(STR_C("Material##water"), m_water_material, IM_ARRAYSIZE(m_water_material));
|
ImGui::InputText(STR_C("Material##water"), m_water_material, IM_ARRAYSIZE(m_water_material));
|
||||||
|
material_drop(m_water_material, IM_ARRAYSIZE(m_water_material));
|
||||||
if (ImGui::IsItemHovered())
|
if (ImGui::IsItemHovered())
|
||||||
ImGui::SetTooltip(STR_C("Empty: %s, the water material of the terrain"), m_streamer.manifest().water_material.c_str());
|
ImGui::SetTooltip(STR_C("Empty: %s, the water material of the terrain"), m_streamer.manifest().water_material.c_str());
|
||||||
ImGui::BeginDisabled(m_water_points.size() < 3);
|
ImGui::BeginDisabled(m_water_points.size() < 3);
|
||||||
@@ -2254,41 +2329,13 @@ void editor_mode::handle_terrain_click(bool const Shift)
|
|||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
std::uint64_t geometry_key(gfx::geometry_handle const &Geometry)
|
// takes the shapes of specified terrain files and materials out of the scene. returns: the count of the shapes
|
||||||
{
|
std::size_t drop_terrain_shapes(std::set<std::uint16_t> const &Files, std::set<material_handle> const &Materials)
|
||||||
return (static_cast<std::uint64_t>(Geometry.bank) << 32) | Geometry.chunk;
|
|
||||||
}
|
|
||||||
|
|
||||||
// the shapes of the scenery which can be ground: the ones the editor makes itself (its terrain, roads, imagery) left out
|
|
||||||
template <typename Visitor> void visit_scenery_shapes(std::set<std::uint64_t> const &Own, Visitor const &Visit)
|
|
||||||
{
|
|
||||||
auto const scenery = [&](scene::shape_node const &Shape) {
|
|
||||||
auto const &data = Shape.data();
|
|
||||||
return Own.count(geometry_key(data.geometry)) == 0 && false == editor_orthophoto::owns(data.geometry) && false == owned_shapes::owns(data.geometry);
|
|
||||||
};
|
|
||||||
for (std::size_t i = 0; i < sq(scene::EU07_REGIONSIDESECTIONCOUNT); ++i)
|
|
||||||
{
|
|
||||||
auto *section = simulation::Region->get_section(i);
|
|
||||||
if (section == nullptr)
|
|
||||||
continue;
|
|
||||||
for (auto &shape : section->m_shapes)
|
|
||||||
if (scenery(shape))
|
|
||||||
Visit(shape);
|
|
||||||
for (auto &cell : section->m_cells)
|
|
||||||
for (auto &shape : cell.m_shapesopaque)
|
|
||||||
if (scenery(shape))
|
|
||||||
Visit(shape);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// takes the shapes of specified materials out of the scene
|
|
||||||
std::size_t drop_scenery_shapes(std::set<std::uint64_t> const &Own, std::set<material_handle> const &Materials)
|
|
||||||
{
|
{
|
||||||
std::size_t dropped = 0;
|
std::size_t dropped = 0;
|
||||||
auto const dropping = [&](scene::shape_node const &Shape) {
|
auto const dropping = [&](scene::shape_node const &Shape) {
|
||||||
auto const &data = Shape.data();
|
auto const &data = Shape.data();
|
||||||
bool const drop = Materials.count(data.material) != 0 && Own.count(geometry_key(data.geometry)) == 0 && false == editor_orthophoto::owns(data.geometry) &&
|
bool const drop = Files.count(data.terrainfile) != 0 && Materials.count(data.material) != 0;
|
||||||
false == owned_shapes::owns(data.geometry);
|
|
||||||
dropped += drop ? 1 : 0;
|
dropped += drop ? 1 : 0;
|
||||||
return drop;
|
return drop;
|
||||||
};
|
};
|
||||||
@@ -2299,33 +2346,63 @@ namespace
|
|||||||
continue;
|
continue;
|
||||||
section->m_shapes.erase(std::remove_if(section->m_shapes.begin(), section->m_shapes.end(), dropping), section->m_shapes.end());
|
section->m_shapes.erase(std::remove_if(section->m_shapes.begin(), section->m_shapes.end(), dropping), section->m_shapes.end());
|
||||||
for (auto &cell : section->m_cells)
|
for (auto &cell : section->m_cells)
|
||||||
|
{
|
||||||
cell.m_shapesopaque.erase(std::remove_if(cell.m_shapesopaque.begin(), cell.m_shapesopaque.end(), dropping), cell.m_shapesopaque.end());
|
cell.m_shapesopaque.erase(std::remove_if(cell.m_shapesopaque.begin(), cell.m_shapesopaque.end(), dropping), cell.m_shapesopaque.end());
|
||||||
|
cell.m_shapestranslucent.erase(std::remove_if(cell.m_shapestranslucent.begin(), cell.m_shapestranslucent.end(), dropping), cell.m_shapestranslucent.end());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return dropped;
|
return dropped;
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
std::map<material_handle, std::size_t> editor_mode::ground_materials()
|
void editor_mode::load_ground_files()
|
||||||
{
|
{
|
||||||
std::map<material_handle, std::size_t> result;
|
auto &conversion{m_ground_conversion};
|
||||||
if (simulation::Region == nullptr)
|
conversion.files.clear();
|
||||||
return result;
|
conversion.triangles.clear();
|
||||||
std::set<std::uint64_t> own;
|
conversion.materials.clear();
|
||||||
for (auto *terrain : active_terrains())
|
conversion.names.clear();
|
||||||
own.insert(geometry_key(terrain->geometry()));
|
conversion.chosen.clear();
|
||||||
glm::dvec2 const everywhere{std::numeric_limits<double>::max() * 0.5};
|
std::map<std::string, std::uint16_t> lookup;
|
||||||
visit_scenery_shapes(own, [&](scene::shape_node const &Shape) {
|
auto const &references{scene::terrain_file::references()};
|
||||||
std::vector<world_triangle> triangles;
|
for (std::size_t index = 0; index < references.size(); ++index)
|
||||||
gather_shape_triangles(Shape, -everywhere, everywhere, triangles, true);
|
{
|
||||||
if (false == triangles.empty())
|
ground_conversion::source source;
|
||||||
result[Shape.data().material] += triangles.size();
|
source.file = references[index].file;
|
||||||
});
|
source.reference = static_cast<std::uint16_t>(index + 1);
|
||||||
return result;
|
source.first = conversion.triangles.size();
|
||||||
|
source.good = scene::terrain_file::read_triangles(
|
||||||
|
references[index],
|
||||||
|
[&](std::string const &Material, std::array<glm::dvec3, 3> const &Triangle) {
|
||||||
|
auto found{lookup.find(Material)};
|
||||||
|
if (found == lookup.end())
|
||||||
|
{
|
||||||
|
found = lookup.emplace(Material, static_cast<std::uint16_t>(conversion.names.size())).first;
|
||||||
|
conversion.names.push_back(Material);
|
||||||
|
}
|
||||||
|
conversion.triangles.push_back(Triangle);
|
||||||
|
conversion.materials.push_back(found->second);
|
||||||
|
},
|
||||||
|
&source.message, &source.text);
|
||||||
|
if (false == source.good)
|
||||||
|
{
|
||||||
|
// what was read before the failure doesn't count
|
||||||
|
conversion.triangles.resize(source.first);
|
||||||
|
conversion.materials.resize(source.first);
|
||||||
|
source.chosen = false;
|
||||||
|
}
|
||||||
|
source.count = conversion.triangles.size() - source.first;
|
||||||
|
conversion.files.push_back(std::move(source));
|
||||||
|
}
|
||||||
|
// the terrain files hold the ground, all of it is taken unless told otherwise
|
||||||
|
for (std::size_t index = 0; index < conversion.names.size(); ++index)
|
||||||
|
conversion.chosen.insert(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string editor_mode::convert_ground(std::set<material_handle> const &Materials)
|
std::string editor_mode::convert_ground()
|
||||||
{
|
{
|
||||||
if (simulation::Region == nullptr || Materials.empty())
|
auto &conversion{m_ground_conversion};
|
||||||
|
if (simulation::Region == nullptr || conversion.chosen.empty())
|
||||||
return {};
|
return {};
|
||||||
if (false == ensure_terrain())
|
if (false == ensure_terrain())
|
||||||
return STR_C("The heightmap terrain can't be opened");
|
return STR_C("The heightmap terrain can't be opened");
|
||||||
@@ -2333,50 +2410,56 @@ std::string editor_mode::convert_ground(std::set<material_handle> const &Materia
|
|||||||
int const cells = heightmap::cells_for(m_terrain_spacing);
|
int const cells = heightmap::cells_for(m_terrain_spacing);
|
||||||
double const cellsize = size / cells;
|
double const cellsize = size / cells;
|
||||||
|
|
||||||
// the shapes of the heightmap terrain hold no triangles of their own, nothing to leave out
|
// the chosen triangles of the chosen files
|
||||||
std::set<std::uint64_t> const own;
|
|
||||||
std::vector<world_triangle> triangles;
|
std::vector<world_triangle> triangles;
|
||||||
std::vector<material_handle> materials;
|
std::vector<std::uint16_t> materials;
|
||||||
glm::dvec2 const everywhere{std::numeric_limits<double>::max() * 0.5};
|
std::set<std::uint16_t> files;
|
||||||
visit_scenery_shapes(own, [&](scene::shape_node const &Shape) {
|
for (auto const &source : conversion.files)
|
||||||
if (Materials.count(Shape.data().material) == 0)
|
{
|
||||||
return;
|
if (false == source.good || false == source.chosen)
|
||||||
gather_shape_triangles(Shape, -everywhere, everywhere, triangles, true);
|
continue;
|
||||||
materials.resize(triangles.size(), Shape.data().material);
|
files.insert(source.reference);
|
||||||
});
|
for (auto index = source.first; index < source.first + source.count; ++index)
|
||||||
|
{
|
||||||
|
if (conversion.chosen.count(conversion.materials[index]) == 0)
|
||||||
|
continue;
|
||||||
|
triangles.push_back(conversion.triangles[index]);
|
||||||
|
materials.push_back(conversion.materials[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (triangles.empty())
|
if (triangles.empty())
|
||||||
return STR_C("The chosen materials have no triangles");
|
return STR_C("The chosen files and materials have no triangles");
|
||||||
|
|
||||||
// the triangles reaching each chunk
|
// the triangles reaching each chunk
|
||||||
std::map<std::pair<int, int>, std::vector<std::uint32_t>> buckets;
|
std::map<heightmap::chunk_key, std::vector<std::uint32_t>> buckets;
|
||||||
for (std::uint32_t i = 0; i < triangles.size(); ++i)
|
for (std::uint32_t i = 0; i < triangles.size(); ++i)
|
||||||
{
|
{
|
||||||
auto const &t = triangles[i];
|
auto const &t = triangles[i];
|
||||||
int const x0 = static_cast<int>(std::floor(std::min({t[0].x, t[1].x, t[2].x}) / size));
|
// a triangle which only touches the edge of a chunk gives it nothing but the edge, which the chunk on the other side has
|
||||||
int const x1 = static_cast<int>(std::floor(std::max({t[0].x, t[1].x, t[2].x}) / size));
|
auto const margin{1e-3};
|
||||||
int const z0 = static_cast<int>(std::floor(std::min({t[0].z, t[1].z, t[2].z}) / size));
|
auto const low{heightmap::chunk_at(std::min({t[0].x, t[1].x, t[2].x}) + margin, std::min({t[0].z, t[1].z, t[2].z}) + margin)};
|
||||||
int const z1 = static_cast<int>(std::floor(std::max({t[0].z, t[1].z, t[2].z}) / size));
|
auto const high{heightmap::chunk_at(std::max({t[0].x, t[1].x, t[2].x}) - margin, std::max({t[0].z, t[1].z, t[2].z}) - margin)};
|
||||||
for (int x = x0; x <= x1; ++x)
|
for (int x = low.first; x <= high.first; ++x)
|
||||||
for (int z = z0; z <= z1; ++z)
|
for (int z = low.second; z <= high.second; ++z)
|
||||||
buckets[{x, z}].push_back(i);
|
buckets[{x, z}].push_back(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
// the chunks are covered with the material which covers the most; it goes to the palette of the terrain
|
// palette entries of the materials, added as they're needed
|
||||||
std::map<material_handle, double> coverage;
|
std::map<std::uint16_t, std::uint16_t> palette;
|
||||||
for (std::size_t i = 0; i < triangles.size(); ++i)
|
auto const layer_of = [&](std::uint16_t const Material) {
|
||||||
{
|
auto const found{palette.find(Material)};
|
||||||
auto const &t = triangles[i];
|
if (found != palette.end())
|
||||||
coverage[materials[i]] += 0.5 * std::abs((t[1].x - t[0].x) * (t[2].z - t[0].z) - (t[2].x - t[0].x) * (t[1].z - t[0].z));
|
return found->second;
|
||||||
}
|
auto const layer{m_streamer.layer(conversion.names[Material])};
|
||||||
auto const dominant = std::max_element(coverage.begin(), coverage.end(), [](auto const &A, auto const &B) { return A.second < B.second; })->first;
|
palette.emplace(Material, layer);
|
||||||
std::uint16_t layer = 0;
|
return layer;
|
||||||
if (auto const *material = GfxRenderer->Material(dominant))
|
};
|
||||||
layer = m_streamer.layer(material->GetName());
|
|
||||||
|
|
||||||
// the old ground goes first, so what's made in its place isn't taken for it
|
|
||||||
auto const dropped = drop_scenery_shapes(own, Materials);
|
|
||||||
std::size_t made = 0, skipped = 0;
|
std::size_t made = 0, skipped = 0;
|
||||||
std::size_t const side = static_cast<std::size_t>(cells) + 1;
|
std::size_t const side = static_cast<std::size_t>(cells) + 1;
|
||||||
|
auto const samples{heightmap::default_paint_samples};
|
||||||
|
std::size_t const paintside = static_cast<std::size_t>(samples) + 1;
|
||||||
|
double const paintstep = size / samples;
|
||||||
for (auto const &bucket : buckets)
|
for (auto const &bucket : buckets)
|
||||||
{
|
{
|
||||||
auto const &key = bucket.first;
|
auto const &key = bucket.first;
|
||||||
@@ -2385,11 +2468,17 @@ std::string editor_mode::convert_ground(std::set<material_handle> const &Materia
|
|||||||
++skipped;
|
++skipped;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
glm::dvec2 const low{key.first * size, key.second * size};
|
auto const low{heightmap::chunk_corner(key)};
|
||||||
std::vector<world_triangle> local;
|
std::vector<world_triangle> local;
|
||||||
|
std::vector<std::uint16_t> localmaterials;
|
||||||
for (auto const i : bucket.second)
|
for (auto const i : bucket.second)
|
||||||
|
{
|
||||||
local.push_back(triangles[i]);
|
local.push_back(triangles[i]);
|
||||||
|
localmaterials.push_back(materials[i]);
|
||||||
|
}
|
||||||
triangle_grid const ground(std::move(local), low - glm::dvec2{1.0}, low + glm::dvec2{size + 1.0});
|
triangle_grid const ground(std::move(local), low - glm::dvec2{1.0}, low + glm::dvec2{size + 1.0});
|
||||||
|
auto const ceiling{std::numeric_limits<double>::max()};
|
||||||
|
// heights of the grid points; where there's no ground they're taken from the nearest point which has some
|
||||||
std::vector<double> heights(side * side, 0.0);
|
std::vector<double> heights(side * side, 0.0);
|
||||||
std::vector<char> found(side * side, 0);
|
std::vector<char> found(side * side, 0);
|
||||||
bool any = false;
|
bool any = false;
|
||||||
@@ -2397,29 +2486,103 @@ std::string editor_mode::convert_ground(std::set<material_handle> const &Materia
|
|||||||
for (std::size_t ix = 0; ix < side; ++ix)
|
for (std::size_t ix = 0; ix < side; ++ix)
|
||||||
{
|
{
|
||||||
auto const index = iz * side + ix;
|
auto const index = iz * side + ix;
|
||||||
if (ground.height_at(low.x + ix * cellsize, low.y + iz * cellsize, std::numeric_limits<double>::max(), heights[index]))
|
if (ground.height_at(low.x + ix * cellsize, low.y + iz * cellsize, ceiling, heights[index]))
|
||||||
found[index] = 1, any = true;
|
found[index] = 1, any = true;
|
||||||
}
|
}
|
||||||
if (false == any)
|
if (false == any)
|
||||||
continue;
|
continue;
|
||||||
fill_from_nearest(heights, found, side);
|
fill_from_nearest(heights, found, side);
|
||||||
auto const sampler = [&](double X, double Z) {
|
// the material on top at each paint sample
|
||||||
auto const ix = static_cast<std::size_t>(std::clamp<long>(std::lround((X - low.x) / cellsize), 0, cells));
|
std::vector<std::uint16_t> paint(paintside * paintside, 0);
|
||||||
auto const iz = static_cast<std::size_t>(std::clamp<long>(std::lround((Z - low.y) / cellsize), 0, cells));
|
std::vector<char> painted(paintside * paintside, 0);
|
||||||
return static_cast<float>(heights[iz * side + ix]);
|
std::map<std::uint16_t, std::size_t> counts;
|
||||||
};
|
for (std::size_t iz = 0; iz < paintside; ++iz)
|
||||||
if (m_streamer.add_chunk(key.first, key.second, m_terrain_spacing, sampler, layer))
|
for (std::size_t ix = 0; ix < paintside; ++ix)
|
||||||
|
{
|
||||||
|
double height;
|
||||||
|
std::uint32_t triangle;
|
||||||
|
if (false == ground.height_at(low.x + ix * paintstep, low.y + iz * paintstep, ceiling, height, &triangle))
|
||||||
|
continue;
|
||||||
|
auto const index = iz * paintside + ix;
|
||||||
|
paint[index] = localmaterials[triangle];
|
||||||
|
painted[index] = 1;
|
||||||
|
++counts[paint[index]];
|
||||||
|
}
|
||||||
|
if (counts.empty())
|
||||||
|
{
|
||||||
|
// the points of the grid hit the ground, the samples in between didn't; the nearest triangle covers the chunk
|
||||||
|
counts[localmaterials.front()] = 1;
|
||||||
|
std::fill(paint.begin(), paint.end(), localmaterials.front());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
fill_from_nearest(paint, painted, paintside);
|
||||||
|
// a chunk holds up to 8 materials, the ones covering the most; the rest is given to the one covering the most
|
||||||
|
std::vector<std::pair<std::size_t, std::uint16_t>> ranking;
|
||||||
|
for (auto const &count : counts)
|
||||||
|
ranking.emplace_back(count.second, count.first);
|
||||||
|
std::sort(ranking.begin(), ranking.end(), [](auto const &A, auto const &B) { return A.first > B.first; });
|
||||||
|
if (ranking.size() > heightmap::max_chunk_layers)
|
||||||
|
ranking.resize(heightmap::max_chunk_layers);
|
||||||
|
|
||||||
|
auto data{heightmap::chunk_data::make_flat(key, m_terrain_spacing, 0.f)};
|
||||||
|
for (std::size_t index = 0; index < data->heights.size(); ++index)
|
||||||
|
data->heights[index] = static_cast<float>(heights[index]);
|
||||||
|
std::map<std::uint16_t, std::size_t> slots;
|
||||||
|
for (std::size_t slot = 0; slot < ranking.size(); ++slot)
|
||||||
|
{
|
||||||
|
data->layers[slot] = layer_of(ranking[slot].second);
|
||||||
|
slots.emplace(ranking[slot].second, slot);
|
||||||
|
}
|
||||||
|
if (ranking.size() > 1)
|
||||||
|
{
|
||||||
|
auto const plane{paintside * paintside};
|
||||||
|
data->paint = samples;
|
||||||
|
data->weights.assign(plane * ranking.size(), 0);
|
||||||
|
for (std::size_t index = 0; index < plane; ++index)
|
||||||
|
{
|
||||||
|
auto const slot{slots.find(paint[index])};
|
||||||
|
data->weights[(slot != slots.end() ? slot->second : 0) * plane + index] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m_streamer.add_chunk(std::move(data)))
|
||||||
++made;
|
++made;
|
||||||
}
|
}
|
||||||
ground_tiles.clear();
|
forget_ground();
|
||||||
forget_ground_shapes();
|
|
||||||
|
|
||||||
auto const erased = scene::Layers.erase_shapes(Materials);
|
// the converted ground goes from the scene, and from the text files on save
|
||||||
auto summary = format(STR_C("Ground converted: %zu chunks of heightmap terrain made, %zu shapes taken out of the scene, %zu triangle nodes go from the scenery files on save"), made, dropped, erased.first);
|
std::set<material_handle> handles;
|
||||||
|
for (auto const material : conversion.chosen)
|
||||||
|
if (material < conversion.names.size())
|
||||||
|
handles.insert(GfxRenderer->Fetch_Material(conversion.names[material]));
|
||||||
|
auto const dropped = drop_terrain_shapes(files, handles);
|
||||||
|
std::set<scene::layer_handle> layers;
|
||||||
|
std::size_t binary = 0;
|
||||||
|
for (auto const &source : conversion.files)
|
||||||
|
{
|
||||||
|
if (false == source.good || false == source.chosen)
|
||||||
|
continue;
|
||||||
|
if (false == source.text)
|
||||||
|
{
|
||||||
|
++binary;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto const textfile{source.file + ".txtf"};
|
||||||
|
for (std::size_t layer = 1; layer <= scene::Layers.size(); ++layer)
|
||||||
|
{
|
||||||
|
auto name{scene::Layers.layer(static_cast<scene::layer_handle>(layer)).name};
|
||||||
|
replace_slashes(name);
|
||||||
|
if (name.ends_with(textfile))
|
||||||
|
layers.insert(static_cast<scene::layer_handle>(layer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto const erased = scene::Layers.erase_shapes(handles, &layers);
|
||||||
|
auto summary = format(STR_C("Ground converted: %zu chunks of heightmap terrain made, %zu shapes taken out of the scene, %zu triangle nodes go from the terrain files on save"), made, dropped, erased.first);
|
||||||
if (skipped > 0)
|
if (skipped > 0)
|
||||||
summary += format(STR_C(", %zu chunks kept as there was terrain already"), skipped);
|
summary += format(STR_C(", %zu chunks kept as there was terrain already"), skipped);
|
||||||
if (erased.second > 0)
|
if (erased.second > 0)
|
||||||
summary += format(STR_C(", %zu nodes stay as their files can't be rewritten"), erased.second);
|
summary += format(STR_C(", %zu nodes stay as their files can't be rewritten"), erased.second);
|
||||||
|
if (binary > 0)
|
||||||
|
summary += format(STR_C(". %zu of the files are binary (.btf) without their text: their triangles come back with the next load, until the files are taken out of the terrain directive"), binary);
|
||||||
WriteLog("Editor: " + summary, logtype::generic);
|
WriteLog("Editor: " + summary, logtype::generic);
|
||||||
return summary;
|
return summary;
|
||||||
}
|
}
|
||||||
@@ -5261,7 +5424,8 @@ void editor_mode::render_change_history(){
|
|||||||
s.action == EditorSnapshot::Action::Scale ? "SCA" :
|
s.action == EditorSnapshot::Action::Scale ? "SCA" :
|
||||||
s.action == EditorSnapshot::Action::TrackEdit ? "TRK" :
|
s.action == EditorSnapshot::Action::TrackEdit ? "TRK" :
|
||||||
s.action == EditorSnapshot::Action::RoadEdit ? "ROAD" :
|
s.action == EditorSnapshot::Action::RoadEdit ? "ROAD" :
|
||||||
s.action == EditorSnapshot::Action::Array ? "ARR" : "OTH",
|
s.action == EditorSnapshot::Action::Array ? "ARR" :
|
||||||
|
s.action == EditorSnapshot::Action::Terrain ? "TER" : "OTH",
|
||||||
s.node_name.empty() ? "(noname)" : s.node_name.c_str(),
|
s.node_name.empty() ? "(noname)" : s.node_name.c_str(),
|
||||||
s.position.x, s.position.y, s.position.z);
|
s.position.x, s.position.y, s.position.z);
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ class editor_mode : public application_mode, private editor_track::observer
|
|||||||
struct stored_profile;
|
struct stored_profile;
|
||||||
struct EditorSnapshot
|
struct EditorSnapshot
|
||||||
{
|
{
|
||||||
enum class Action { Move, Rotate, Scale, Add, Delete, TrackEdit, Other, RoadEdit, Array, Bend };
|
enum class Action { Move, Rotate, Scale, Add, Delete, TrackEdit, Other, RoadEdit, Array, Bend, Terrain };
|
||||||
// copy of a model an array made
|
// copy of a model an array made
|
||||||
struct array_copy
|
struct array_copy
|
||||||
{
|
{
|
||||||
@@ -170,6 +170,7 @@ class editor_mode : public application_mode, private editor_track::observer
|
|||||||
std::vector<stored_profile> profile_library;
|
std::vector<stored_profile> profile_library;
|
||||||
std::vector<array_copy> copies; // models an array added to the one it was made of
|
std::vector<array_copy> copies; // models an array added to the one it was made of
|
||||||
bool joined{false}; // undone and redone together with the change before it
|
bool joined{false}; // undone and redone together with the change before it
|
||||||
|
std::shared_ptr<terrain_streamer::change const> terrain; // the heightmap terrain from before the change (Action::Terrain)
|
||||||
};
|
};
|
||||||
void push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action = EditorSnapshot::Action::Move, std::string const &Serialized = std::string());
|
void push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action = EditorSnapshot::Action::Move, std::string const &Serialized = std::string());
|
||||||
|
|
||||||
@@ -183,6 +184,12 @@ class editor_mode : public application_mode, private editor_track::observer
|
|||||||
// a single step of the history, see undo_last()
|
// a single step of the history, see undo_last()
|
||||||
void undo_one();
|
void undo_one();
|
||||||
void redo_one();
|
void redo_one();
|
||||||
|
// the changes of the heightmap terrain since the last step become a step of the history: a stroke of the brush,
|
||||||
|
// or what a frame did; the next change is recorded from now on
|
||||||
|
void terrain_history_step();
|
||||||
|
// puts the terrain back as the step recorded it, the step left with the state it replaced
|
||||||
|
void restore_terrain(EditorSnapshot &Snapshot);
|
||||||
|
bool m_history_busy{false}; // undo or redo under way
|
||||||
void handle_brush_mouse_hold(int Action, int Button);
|
void handle_brush_mouse_hold(int Action, int Button);
|
||||||
void apply_rotation_for_new_node(scene::basic_node *node, int rotation_mode, float fixed_rotation_value);
|
void apply_rotation_for_new_node(scene::basic_node *node, int rotation_mode, float fixed_rotation_value);
|
||||||
// members
|
// members
|
||||||
@@ -313,18 +320,59 @@ class editor_mode : public application_mode, private editor_track::observer
|
|||||||
void draw_terrain_overlay();
|
void draw_terrain_overlay();
|
||||||
// forgets what's known about the ground, after the terrain changed
|
// forgets what's known about the ground, after the terrain changed
|
||||||
void forget_ground();
|
void forget_ground();
|
||||||
// the scenery ground: triangles of each material, the ones the editor makes itself left out
|
// conversion of the ground of the terrain files of the scenery (terrain directive, .txtf/.btf) to heightmap terrain
|
||||||
std::map<material_handle, std::size_t> ground_materials();
|
|
||||||
// replaces the triangles of specified materials with chunks of heightmap terrain of their shape; the scenery files lose them on save.
|
|
||||||
// returns: summary for the user
|
|
||||||
std::string convert_ground(std::set<material_handle> const &Materials);
|
|
||||||
struct ground_conversion
|
struct ground_conversion
|
||||||
{
|
{
|
||||||
|
struct source
|
||||||
|
{
|
||||||
|
std::string file; // as the terrain directive names it
|
||||||
|
std::uint16_t reference{0}; // see scene::terrain_file::reference_of()
|
||||||
|
bool text{false}; // read from the text file, rather than the binary one
|
||||||
|
bool good{false};
|
||||||
|
bool chosen{true};
|
||||||
|
std::string message;
|
||||||
|
std::size_t first{0}, count{0}; // its triangles
|
||||||
|
};
|
||||||
bool open{false};
|
bool open{false};
|
||||||
std::map<material_handle, std::size_t> materials;
|
std::vector<source> files;
|
||||||
std::set<material_handle> chosen;
|
std::vector<std::array<glm::dvec3, 3>> triangles;
|
||||||
|
std::vector<std::uint16_t> materials; // of the triangles: index of the name
|
||||||
|
std::vector<std::string> names; // of the materials
|
||||||
|
std::set<std::size_t> chosen; // materials to convert
|
||||||
std::string status;
|
std::string status;
|
||||||
} m_ground_conversion;
|
} m_ground_conversion;
|
||||||
|
// reads the triangles of the terrain files of the scenery for the conversion
|
||||||
|
void load_ground_files();
|
||||||
|
// the folder of the textures as a browser, in the window of the node bank while the terrain is edited: the textures and the
|
||||||
|
// materials can be dragged onto the fields which take a material
|
||||||
|
struct texture_browser
|
||||||
|
{
|
||||||
|
struct item
|
||||||
|
{
|
||||||
|
std::string name; // as the materials are named: relative to the folder of the textures, without the extension
|
||||||
|
std::string label; // the file, without the folder
|
||||||
|
bool material{false}; // there's a .mat file of the name
|
||||||
|
};
|
||||||
|
std::string folder; // relative to the folder of the textures, empty or with a trailing slash
|
||||||
|
std::vector<std::string> folders; // under the current one
|
||||||
|
std::vector<item> items;
|
||||||
|
bool listed{false};
|
||||||
|
char filter[64]{};
|
||||||
|
float size{96.f}; // of the images, pixels
|
||||||
|
std::string selected;
|
||||||
|
std::map<std::string, int> thumbnails; // texture shown for the item of the name, 0: none
|
||||||
|
int loads{0}; // textures read in the frame, a few at most so the browser doesn't stall the editor
|
||||||
|
} m_textures;
|
||||||
|
void render_texture_browser();
|
||||||
|
// texture of the image of the item of the browser, 0 while there's none
|
||||||
|
std::uint64_t texture_thumbnail(texture_browser::item const &Item);
|
||||||
|
// takes a material dragged from the texture browser onto the item drawn last. returns: true if one was dropped, its name in Name
|
||||||
|
static bool material_drop(std::string &Name);
|
||||||
|
// the same, for a text field
|
||||||
|
static bool material_drop(char *Buffer, std::size_t Size);
|
||||||
|
// replaces the chosen triangles of the chosen terrain files with chunks of heightmap terrain of their shape, painted with
|
||||||
|
// their materials; the files lose them on save. returns: summary for the user
|
||||||
|
std::string convert_ground();
|
||||||
void render_ground_conversion();
|
void render_ground_conversion();
|
||||||
// handles a click of the chunk, spacing and water tools (Shift: the other way round)
|
// handles a click of the chunk, spacing and water tools (Shift: the other way round)
|
||||||
void handle_terrain_click(bool const Shift);
|
void handle_terrain_click(bool const Shift);
|
||||||
|
|||||||
@@ -674,54 +674,84 @@ void editor_mode::profile_restore_ground()
|
|||||||
void editor_mode::render_ground_conversion()
|
void editor_mode::render_ground_conversion()
|
||||||
{
|
{
|
||||||
auto &conversion{m_ground_conversion};
|
auto &conversion{m_ground_conversion};
|
||||||
|
ImGui::PushID("ground_conversion");
|
||||||
if (false == conversion.open)
|
if (false == conversion.open)
|
||||||
{
|
{
|
||||||
if (ImGui::Button(STR_C("Convert the scenery ground to heightmap terrain...")))
|
if (ImGui::Button(STR_C("Convert the terrain files to heightmap terrain...")))
|
||||||
{
|
{
|
||||||
conversion.open = true;
|
conversion.open = true;
|
||||||
conversion.materials = ground_materials();
|
load_ground_files();
|
||||||
conversion.chosen.clear();
|
|
||||||
// the material with the most triangles is the ground, most likely
|
|
||||||
auto const most{std::max_element(conversion.materials.begin(), conversion.materials.end(), [](auto const &A, auto const &B) { return A.second < B.second; })};
|
|
||||||
if (most != conversion.materials.end())
|
|
||||||
conversion.chosen.insert(most->first);
|
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered())
|
if (ImGui::IsItemHovered())
|
||||||
ImGui::SetTooltip("%s", STR_C("Replaces the triangles of the chosen materials with heightmap terrain of the same shape,\nwhich can be shaped, sculpted and painted; the scenery files lose these triangles on save"));
|
ImGui::SetTooltip("%s", STR_C("Replaces the triangles of the terrain files of the scenery (terrain directive, .txtf/.btf) with heightmap terrain\n"
|
||||||
|
"of the same shape, painted with their materials; the files lose these triangles on save"));
|
||||||
if (false == conversion.status.empty())
|
if (false == conversion.status.empty())
|
||||||
ImGui::TextWrapped("%s", conversion.status.c_str());
|
ImGui::TextWrapped("%s", conversion.status.c_str());
|
||||||
|
ImGui::PopID();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ImGui::TextWrapped("%s", STR_C("Materials of the scenery triangles: tick the ones which are the ground"));
|
if (conversion.files.empty())
|
||||||
if (conversion.materials.empty())
|
ImGui::TextWrapped("%s", STR_C("The scenery has no terrain files: no terrain directive names a .txtf or .btf file"));
|
||||||
ImGui::TextDisabled("%s", STR_C("The scenery has no triangles"));
|
else
|
||||||
for (auto const &entry : conversion.materials)
|
ImGui::TextUnformatted(STR_C("Terrain files of the scenery:"));
|
||||||
|
for (std::size_t index = 0; index < conversion.files.size(); ++index)
|
||||||
{
|
{
|
||||||
auto const *material{GfxRenderer->Material(entry.first)};
|
auto &source{conversion.files[index]};
|
||||||
auto const name{material != nullptr ? material->GetName() : std::string{"?"}};
|
ImGui::PushID(static_cast<int>(index));
|
||||||
bool chosen{conversion.chosen.count(entry.first) != 0};
|
ImGui::BeginDisabled(false == source.good);
|
||||||
ImGui::PushID(static_cast<int>(entry.first));
|
ImGui::Checkbox(source.file.c_str(), &source.chosen);
|
||||||
if (ImGui::Checkbox(format(STR_C("%s (%zu triangles)"), name.c_str(), entry.second).c_str(), &chosen))
|
ImGui::EndDisabled();
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (source.good)
|
||||||
|
ImGui::TextDisabled(source.text ? STR_C("(text, %zu triangles)") : STR_C("(binary, %zu triangles)"), source.count);
|
||||||
|
else
|
||||||
|
ImGui::TextDisabled("(%s)", source.message.c_str());
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
// the materials of the chosen files
|
||||||
|
std::vector<std::size_t> counts(conversion.names.size(), 0);
|
||||||
|
for (auto const &source : conversion.files)
|
||||||
|
if (source.good && source.chosen)
|
||||||
|
for (auto index = source.first; index < source.first + source.count; ++index)
|
||||||
|
++counts[conversion.materials[index]];
|
||||||
|
if (false == conversion.names.empty())
|
||||||
|
ImGui::TextUnformatted(STR_C("Materials: tick the ones which are the ground"));
|
||||||
|
for (std::size_t index = 0; index < conversion.names.size(); ++index)
|
||||||
|
{
|
||||||
|
if (counts[index] == 0)
|
||||||
|
continue;
|
||||||
|
bool chosen{conversion.chosen.count(index) != 0};
|
||||||
|
ImGui::PushID(static_cast<int>(index) + 0x10000);
|
||||||
|
if (ImGui::Checkbox(format(STR_C("%s (%zu triangles)"), conversion.names[index].c_str(), counts[index]).c_str(), &chosen))
|
||||||
{
|
{
|
||||||
if (chosen)
|
if (chosen)
|
||||||
conversion.chosen.insert(entry.first);
|
conversion.chosen.insert(index);
|
||||||
else
|
else
|
||||||
conversion.chosen.erase(entry.first);
|
conversion.chosen.erase(index);
|
||||||
}
|
}
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
ImGui::TextWrapped("%s", STR_C("The chunks are covered with the material which covers the most, added to the palette of the terrain. Until saved, loading the scenery again takes it all back."));
|
ImGui::TextWrapped("%s", STR_C("The chunks are painted with the materials of the triangles, up to 8 of them per chunk, added to the palette of the terrain. "
|
||||||
if (conversion.chosen.empty())
|
"Until saved, loading the scenery again takes it all back."));
|
||||||
ImGui::TextDisabled("%s", STR_C("Tick at least one material"));
|
auto const ready{std::any_of(conversion.files.begin(), conversion.files.end(), [](auto const &Source) { return Source.good && Source.chosen; }) && false == conversion.chosen.empty()};
|
||||||
else if (ImGui::Button(STR_C("Convert")))
|
ImGui::BeginDisabled(false == ready);
|
||||||
|
if (ImGui::Button(STR_C("Convert")))
|
||||||
{
|
{
|
||||||
conversion.status = convert_ground(conversion.chosen);
|
conversion.status = convert_ground();
|
||||||
conversion.open = false;
|
conversion.open = false;
|
||||||
profile_resample();
|
profile_resample();
|
||||||
}
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ImGui::Button(STR_C("Cancel")))
|
if (ImGui::Button(STR_C("Cancel")))
|
||||||
conversion.open = false;
|
conversion.open = false;
|
||||||
|
if (false == conversion.open)
|
||||||
|
{
|
||||||
|
// the triangles take memory, they're read again the next time
|
||||||
|
conversion.triangles = {};
|
||||||
|
conversion.materials = {};
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
|
|
||||||
void editor_mode::render_profile_earthworks()
|
void editor_mode::render_profile_earthworks()
|
||||||
|
|||||||
@@ -72,6 +72,10 @@ http://mozilla.org/MPL/2.0/.
|
|||||||
// spread <track> <stop 0..2> <reach> logs the paths the spread from the track takes
|
// spread <track> <stop 0..2> <reach> logs the paths the spread from the track takes
|
||||||
// newscenery the editor starts again with the wizard of a new scenery
|
// newscenery the editor starts again with the wizard of a new scenery
|
||||||
// vehicles logs each vehicle: its head, facing, and what its couplers hold
|
// vehicles logs each vehicle: its head, facing, and what its couplers hold
|
||||||
|
// terrainmode the terrain field of work, with its texture browser
|
||||||
|
// terrainsculpt <x> <z> <radius> <metres>, terrainpaint <x> <z> <radius> <palette entry> as a stroke of the brush
|
||||||
|
// terrainprobe <x> <z> logs the height of the heightmap terrain and the materials of the chunk
|
||||||
|
// terrainconvert converts every material of the terrain files of the scenery
|
||||||
// screenshot
|
// screenshot
|
||||||
// save
|
// save
|
||||||
// log <text>
|
// log <text>
|
||||||
@@ -1134,6 +1138,53 @@ void editor_mode::selftest_step()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (command == "terrainmode")
|
||||||
|
{
|
||||||
|
m_terrain_open = true;
|
||||||
|
WriteLog("SELFTEST terrainmode");
|
||||||
|
}
|
||||||
|
else if (command == "terrainsculpt" || command == "terrainpaint")
|
||||||
|
{
|
||||||
|
double x{0.0}, z{0.0}, radius{10.0}, value{1.0};
|
||||||
|
words >> x >> z >> radius >> value;
|
||||||
|
auto const changed{command == "terrainsculpt" ? m_streamer.sculpt(x, z, radius, value) : m_streamer.paint(x, z, radius, 1.0, static_cast<std::uint16_t>(value))};
|
||||||
|
forget_ground();
|
||||||
|
WriteLog(format("SELFTEST %s %.1f %.1f: %s", command.c_str(), x, z, changed ? "changed" : "nothing changed"));
|
||||||
|
}
|
||||||
|
else if (command == "terrainprobe")
|
||||||
|
{
|
||||||
|
double x{0.0}, z{0.0}, height{0.0};
|
||||||
|
words >> x >> z;
|
||||||
|
auto const *chunk{m_streamer.terrain_at(x, z)};
|
||||||
|
std::string materials;
|
||||||
|
if (chunk != nullptr)
|
||||||
|
for (std::size_t slot = 0; slot < std::max<std::size_t>(1, chunk->data().layer_count()); ++slot)
|
||||||
|
{
|
||||||
|
auto const layer{chunk->data().layers[slot]};
|
||||||
|
auto const &palette{m_streamer.manifest().layers};
|
||||||
|
materials += (layer < palette.size() ? palette[layer].material : std::string{"?"}) + " ";
|
||||||
|
}
|
||||||
|
auto const found{m_streamer.height_at(x, z, height)};
|
||||||
|
WriteLog(format("SELFTEST terrainprobe %.1f %.1f: %s %.3f, %zu chunks, materials %s", x, z, found ? "height" : "no terrain", height, m_streamer.resident(), materials.c_str()));
|
||||||
|
}
|
||||||
|
else if (command == "terrainconvert")
|
||||||
|
{
|
||||||
|
load_ground_files();
|
||||||
|
for (auto const &source : m_ground_conversion.files)
|
||||||
|
{
|
||||||
|
glm::dvec3 low{1e30}, high{-1e30};
|
||||||
|
for (auto index = source.first; index < source.first + source.count; ++index)
|
||||||
|
for (auto const &vertex : m_ground_conversion.triangles[index])
|
||||||
|
{
|
||||||
|
low = glm::min(low, vertex);
|
||||||
|
high = glm::max(high, vertex);
|
||||||
|
}
|
||||||
|
WriteLog("SELFTEST terrain file " + source.file + (source.text ? " (text): " : " (binary): ") + source.message +
|
||||||
|
format(", from %.1f %.1f %.1f to %.1f %.1f %.1f", low.x, low.y, low.z, high.x, high.y, high.z));
|
||||||
|
}
|
||||||
|
m_ground_conversion.status = convert_ground();
|
||||||
|
WriteLog("SELFTEST terrainconvert: " + m_ground_conversion.status);
|
||||||
|
}
|
||||||
else if (command == "quit")
|
else if (command == "quit")
|
||||||
{
|
{
|
||||||
if (false == test.quitting)
|
if (false == test.quitting)
|
||||||
|
|||||||
279
application/editormodetextures.cpp
Normal file
279
application/editormodetextures.cpp
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
/*
|
||||||
|
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 "application/editormode.h"
|
||||||
|
#include "rendering/renderer.h"
|
||||||
|
#include "utilities/Globals.h"
|
||||||
|
#include "utilities/utilities.h"
|
||||||
|
#include "utilities/translation.h"
|
||||||
|
#include "imgui/imgui.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// payload of a material dragged from the texture browser: its name, with the terminating zero
|
||||||
|
char const *const material_payload{"EU07_MATERIAL"};
|
||||||
|
|
||||||
|
// files the browser shows: textures, and the materials, which are kept with them
|
||||||
|
bool texture_file(std::string const &Extension)
|
||||||
|
{
|
||||||
|
return Extension == ".dds" || Extension == ".tga" || Extension == ".png" || Extension == ".bmp" || Extension == ".jpg" || Extension == ".ktx" || Extension == ".tex";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string lowercase(std::string Text)
|
||||||
|
{
|
||||||
|
std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char const Character) { return static_cast<char>(std::tolower(Character)); });
|
||||||
|
return Text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the text cut to fit the width, with an ellipsis
|
||||||
|
std::string fitted(std::string const &Text, float const Width)
|
||||||
|
{
|
||||||
|
if (ImGui::CalcTextSize(Text.c_str()).x <= Width)
|
||||||
|
return Text;
|
||||||
|
auto result{Text};
|
||||||
|
while (false == result.empty() && ImGui::CalcTextSize((result + "...").c_str()).x > Width)
|
||||||
|
result.pop_back();
|
||||||
|
return result + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool editor_mode::material_drop(std::string &Name)
|
||||||
|
{
|
||||||
|
if (false == ImGui::BeginDragDropTarget())
|
||||||
|
return false;
|
||||||
|
auto dropped{false};
|
||||||
|
if (auto const *payload{ImGui::AcceptDragDropPayload(material_payload)})
|
||||||
|
{
|
||||||
|
auto const *data{static_cast<char const *>(payload->Data)};
|
||||||
|
Name.assign(data, std::find(data, data + payload->DataSize, '\0'));
|
||||||
|
dropped = false == Name.empty();
|
||||||
|
}
|
||||||
|
ImGui::EndDragDropTarget();
|
||||||
|
return dropped;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool editor_mode::material_drop(char *Buffer, std::size_t const Size)
|
||||||
|
{
|
||||||
|
std::string name;
|
||||||
|
if (Size == 0 || false == material_drop(name))
|
||||||
|
return false;
|
||||||
|
auto const length{std::min(name.size(), Size - 1)};
|
||||||
|
std::memcpy(Buffer, name.data(), length);
|
||||||
|
Buffer[length] = '\0';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint64_t editor_mode::texture_thumbnail(texture_browser::item const &Item)
|
||||||
|
{
|
||||||
|
auto &browser{m_textures};
|
||||||
|
auto lookup{browser.thumbnails.find(Item.name)};
|
||||||
|
if (lookup == browser.thumbnails.end())
|
||||||
|
{
|
||||||
|
// reading a texture takes a moment, a few of them go in a frame
|
||||||
|
if (browser.loads >= 2)
|
||||||
|
return 0;
|
||||||
|
++browser.loads;
|
||||||
|
texture_handle texture{null_handle};
|
||||||
|
if (Item.material)
|
||||||
|
{
|
||||||
|
auto const material{GfxRenderer->Fetch_Material(Item.name)};
|
||||||
|
if (auto const *data{GfxRenderer->Material(material)}; material != null_handle && data != nullptr)
|
||||||
|
texture = data->GetTexture(0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
texture = GfxRenderer->Fetch_Texture(Item.name, true);
|
||||||
|
lookup = browser.thumbnails.emplace(Item.name, texture).first;
|
||||||
|
}
|
||||||
|
if (lookup->second == null_handle)
|
||||||
|
return 0;
|
||||||
|
auto &texture{GfxRenderer->Texture(lookup->second)};
|
||||||
|
if (false == texture.get_is_ready())
|
||||||
|
texture.create();
|
||||||
|
return texture.get_is_ready() ? static_cast<std::uint64_t>(texture.get_id()) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void editor_mode::render_texture_browser()
|
||||||
|
{
|
||||||
|
auto &browser{m_textures};
|
||||||
|
browser.loads = 0;
|
||||||
|
if (false == browser.listed)
|
||||||
|
{
|
||||||
|
browser.listed = true;
|
||||||
|
browser.folders.clear();
|
||||||
|
browser.items.clear();
|
||||||
|
std::map<std::string, texture_browser::item> items;
|
||||||
|
std::error_code error;
|
||||||
|
for (std::filesystem::directory_iterator entry{std::string{paths::textures} + browser.folder, error}, end; false == static_cast<bool>(error) && entry != end; entry.increment(error))
|
||||||
|
{
|
||||||
|
auto const path{entry->path()};
|
||||||
|
if (entry->is_directory(error))
|
||||||
|
{
|
||||||
|
browser.folders.push_back(path.filename().string());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto const extension{lowercase(path.extension().string())};
|
||||||
|
auto const material{extension == ".mat"};
|
||||||
|
if (false == material && false == texture_file(extension))
|
||||||
|
continue;
|
||||||
|
auto const stem{path.stem().string()};
|
||||||
|
auto &item{items[stem]};
|
||||||
|
item.name = browser.folder + stem;
|
||||||
|
item.label = stem;
|
||||||
|
item.material |= material;
|
||||||
|
}
|
||||||
|
for (auto &item : items)
|
||||||
|
browser.items.push_back(std::move(item.second));
|
||||||
|
std::sort(browser.folders.begin(), browser.folders.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
// where the browser is, and the folders under it
|
||||||
|
ImGui::BeginDisabled(browser.folder.empty());
|
||||||
|
if (ImGui::Button(STR_C("Up")))
|
||||||
|
{
|
||||||
|
auto folder{browser.folder};
|
||||||
|
if (false == folder.empty())
|
||||||
|
folder.pop_back();
|
||||||
|
auto const slash{folder.rfind('/')};
|
||||||
|
browser.folder = (slash == std::string::npos ? std::string{} : folder.substr(0, slash + 1));
|
||||||
|
browser.listed = false;
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button(STR_C("Refresh")))
|
||||||
|
{
|
||||||
|
browser.listed = false;
|
||||||
|
browser.thumbnails.clear();
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextUnformatted((std::string{paths::textures} + browser.folder).c_str());
|
||||||
|
ImGui::SetNextItemWidth(std::max(80.0f, ImGui::GetContentRegionAvail().x * 0.5f));
|
||||||
|
ImGui::InputTextWithHint("##texturefilter", STR_C("Filter"), browser.filter, IM_ARRAYSIZE(browser.filter));
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::SetNextItemWidth(std::max(60.0f, ImGui::GetContentRegionAvail().x));
|
||||||
|
ImGui::SliderFloat("##texturesize", &browser.size, 48.0f, 256.0f, "%.0f px");
|
||||||
|
if (false == browser.folders.empty())
|
||||||
|
{
|
||||||
|
// the folders flow along the width of the window
|
||||||
|
auto const right{ImGui::GetCursorScreenPos().x + ImGui::GetContentRegionAvail().x};
|
||||||
|
for (std::size_t index = 0; index < browser.folders.size(); ++index)
|
||||||
|
{
|
||||||
|
auto const label{"[" + browser.folders[index] + "]"};
|
||||||
|
auto const width{ImGui::CalcTextSize(label.c_str()).x + ImGui::GetStyle().FramePadding.x * 2.0f};
|
||||||
|
if (index > 0)
|
||||||
|
{
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::GetCursorScreenPos().x + width > right)
|
||||||
|
ImGui::NewLine();
|
||||||
|
}
|
||||||
|
ImGui::PushID(static_cast<int>(index));
|
||||||
|
if (ImGui::SmallButton(label.c_str()))
|
||||||
|
{
|
||||||
|
browser.folder += browser.folders[index] + "/";
|
||||||
|
browser.listed = false;
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
if (false == browser.listed)
|
||||||
|
{
|
||||||
|
// the list is made anew in the next frame
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
std::vector<texture_browser::item const *> shown;
|
||||||
|
auto const filter{lowercase(browser.filter)};
|
||||||
|
for (auto const &item : browser.items)
|
||||||
|
if (filter.empty() || lowercase(item.label).find(filter) != std::string::npos)
|
||||||
|
shown.push_back(&item);
|
||||||
|
if (shown.empty())
|
||||||
|
{
|
||||||
|
ImGui::TextDisabled("%s", STR_C("No textures here"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ImGui::TextDisabled("%s", STR_C("Drag a texture onto a field of a material; double click: add it to the palette of the terrain"));
|
||||||
|
|
||||||
|
ImGui::BeginChild("##texturecards");
|
||||||
|
auto const &style{ImGui::GetStyle()};
|
||||||
|
auto const side{std::max(16.0f, browser.size * std::max(1.0f, Global.ui_scale))};
|
||||||
|
auto const cellheight{side + ImGui::GetTextLineHeight() + style.ItemInnerSpacing.y};
|
||||||
|
auto const columns{std::max(1, static_cast<int>((ImGui::GetContentRegionAvail().x + style.ItemSpacing.x) / (side + style.ItemSpacing.x)))};
|
||||||
|
auto const rows{(static_cast<int>(shown.size()) + columns - 1) / columns};
|
||||||
|
auto *drawlist{ImGui::GetWindowDrawList()};
|
||||||
|
ImGuiListClipper clipper;
|
||||||
|
clipper.Begin(rows, cellheight + style.ItemSpacing.y);
|
||||||
|
while (clipper.Step())
|
||||||
|
{
|
||||||
|
for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row)
|
||||||
|
{
|
||||||
|
for (int column = 0; column < columns; ++column)
|
||||||
|
{
|
||||||
|
auto const index{static_cast<std::size_t>(row) * columns + column};
|
||||||
|
if (index >= shown.size())
|
||||||
|
break;
|
||||||
|
auto const &item{*shown[index]};
|
||||||
|
if (column > 0)
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::PushID(item.name.c_str());
|
||||||
|
ImGui::BeginGroup();
|
||||||
|
auto const corner{ImGui::GetCursorScreenPos()};
|
||||||
|
ImGui::InvisibleButton("##card", ImVec2(side, cellheight));
|
||||||
|
auto const hovered{ImGui::IsItemHovered()};
|
||||||
|
if (ImGui::IsItemClicked(ImGuiMouseButton_Left))
|
||||||
|
{
|
||||||
|
browser.selected = item.name;
|
||||||
|
std::snprintf(m_terrain_material, IM_ARRAYSIZE(m_terrain_material), "%s", item.name.c_str());
|
||||||
|
}
|
||||||
|
if (hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) && m_streamer.active())
|
||||||
|
m_terrain_layer = m_streamer.layer(item.name, m_terrain_material_size);
|
||||||
|
auto const texture{texture_thumbnail(item)};
|
||||||
|
if (ImGui::BeginDragDropSource())
|
||||||
|
{
|
||||||
|
ImGui::SetDragDropPayload(material_payload, item.name.c_str(), item.name.size() + 1);
|
||||||
|
if (texture != 0)
|
||||||
|
ImGui::Image(static_cast<ImTextureID>(texture), ImVec2(64.0f, 64.0f), ImVec2(0, 1), ImVec2(1, 0));
|
||||||
|
ImGui::TextUnformatted(item.name.c_str());
|
||||||
|
ImGui::EndDragDropSource();
|
||||||
|
}
|
||||||
|
ImVec2 const low{corner.x, corner.y}, high{corner.x + side, corner.y + side};
|
||||||
|
if (texture != 0)
|
||||||
|
drawlist->AddImage(static_cast<ImTextureID>(texture), low, high, ImVec2(0, 1), ImVec2(1, 0));
|
||||||
|
else
|
||||||
|
{
|
||||||
|
drawlist->AddRectFilled(low, high, ImGui::GetColorU32(ImGuiCol_FrameBg));
|
||||||
|
drawlist->AddText(ImVec2(low.x + style.FramePadding.x, low.y + style.FramePadding.y), ImGui::GetColorU32(ImGuiCol_TextDisabled), "...");
|
||||||
|
}
|
||||||
|
if (item.material)
|
||||||
|
drawlist->AddText(ImVec2(low.x + style.FramePadding.x, high.y - ImGui::GetTextLineHeight() - style.FramePadding.y), IM_COL32(255, 220, 120, 255), "mat");
|
||||||
|
if (browser.selected == item.name || hovered)
|
||||||
|
drawlist->AddRect(low, high, ImGui::GetColorU32(browser.selected == item.name ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered), 0.0f, 0, 2.0f);
|
||||||
|
drawlist->AddText(ImVec2(low.x, high.y + style.ItemInnerSpacing.y), ImGui::GetColorU32(ImGuiCol_Text), fitted(item.label, side).c_str());
|
||||||
|
if (hovered)
|
||||||
|
{
|
||||||
|
ImGui::BeginTooltip();
|
||||||
|
ImGui::TextUnformatted(item.name.c_str());
|
||||||
|
ImGui::TextDisabled("%s", item.material ? STR_C("material") : STR_C("texture"));
|
||||||
|
ImGui::EndTooltip();
|
||||||
|
}
|
||||||
|
ImGui::EndGroup();
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
}
|
||||||
@@ -128,6 +128,13 @@ class editor_ui : public ui_layer
|
|||||||
void set_workspace(workspace const Workspace)
|
void set_workspace(workspace const Workspace)
|
||||||
{
|
{
|
||||||
m_workspace = Workspace;
|
m_workspace = Workspace;
|
||||||
|
auto const &bank{m_workspacebank[static_cast<std::size_t>(Workspace)]};
|
||||||
|
m_nodebankpanel.stand_in(bank.first, bank.second);
|
||||||
|
}
|
||||||
|
// content of the window of the node bank while the field of work is chosen, in place of the node bank
|
||||||
|
void set_workspace_bank(workspace const Workspace, std::function<void()> Content, std::string Title)
|
||||||
|
{
|
||||||
|
m_workspacebank[static_cast<std::size_t>(Workspace)] = {std::move(Content), std::move(Title)};
|
||||||
}
|
}
|
||||||
void set_workspace_options(workspace const Workspace, std::function<void()> Tools, std::function<void()> Selection = {})
|
void set_workspace_options(workspace const Workspace, std::function<void()> Tools, std::function<void()> Selection = {})
|
||||||
{
|
{
|
||||||
@@ -205,6 +212,7 @@ class editor_ui : public ui_layer
|
|||||||
workspace m_workspace{workspace::none};
|
workspace m_workspace{workspace::none};
|
||||||
std::array<std::function<void()>, static_cast<std::size_t>(workspace::count_)> m_workspacetools;
|
std::array<std::function<void()>, static_cast<std::size_t>(workspace::count_)> m_workspacetools;
|
||||||
std::array<std::function<void()>, static_cast<std::size_t>(workspace::count_)> m_workspaceselection;
|
std::array<std::function<void()>, static_cast<std::size_t>(workspace::count_)> m_workspaceselection;
|
||||||
|
std::array<std::pair<std::function<void()>, std::string>, static_cast<std::size_t>(workspace::count_)> m_workspacebank;
|
||||||
// scene window: the models of the scenery by layer and group, rebuilt when their number changes
|
// scene window: the models of the scenery by layer and group, rebuilt when their number changes
|
||||||
struct scene_entry
|
struct scene_entry
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -934,6 +934,14 @@ void nodebank_panel::render()
|
|||||||
{
|
{
|
||||||
ImGui::SetNextWindowSizeConstraints(ImVec2S(size_min.x, size_min.y), ImVec2S(size_max.x, size_max.y));
|
ImGui::SetNextWindowSizeConstraints(ImVec2S(size_min.x, size_min.y), ImVec2S(size_max.x, size_max.y));
|
||||||
}
|
}
|
||||||
|
if (m_standin)
|
||||||
|
{
|
||||||
|
// the same window, so it keeps its place in the dock
|
||||||
|
if (ImGui::Begin((m_standintitle + "###" + name()).c_str(), nullptr, flags))
|
||||||
|
m_standin();
|
||||||
|
ImGui::End();
|
||||||
|
return;
|
||||||
|
}
|
||||||
auto const panelname{(title.empty() ? name() : title) + "###" + name()};
|
auto const panelname{(title.empty() ? name() : title) + "###" + name()};
|
||||||
|
|
||||||
if (true == ImGui::Begin(panelname.c_str(), nullptr, flags))
|
if (true == ImGui::Begin(panelname.c_str(), nullptr, flags))
|
||||||
|
|||||||
@@ -156,6 +156,13 @@ class nodebank_panel : public ui_panel
|
|||||||
std::string set_name(model_set_ref const &Ref, char const *Manuallabel) const;
|
std::string set_name(model_set_ref const &Ref, char const *Manuallabel) const;
|
||||||
// shows the set manager window, optionally with specified user set selected
|
// shows the set manager window, optionally with specified user set selected
|
||||||
void open_sets_window(int const Setid = 0);
|
void open_sets_window(int const Setid = 0);
|
||||||
|
// content drawn in the window of the node bank in its place, with its own title (a field of work of the editor uses the window
|
||||||
|
// for its own browser); none: the node bank
|
||||||
|
void stand_in(std::function<void()> Content, std::string Title)
|
||||||
|
{
|
||||||
|
m_standin = std::move(Content);
|
||||||
|
m_standintitle = std::move(Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -183,6 +190,8 @@ class nodebank_panel : public ui_panel
|
|||||||
// true if the node bank selection is a definition of a node, which the hand-made lists and the model sets can take
|
// true if the node bank selection is a definition of a node, which the hand-made lists and the model sets can take
|
||||||
bool node_selected() const;
|
bool node_selected() const;
|
||||||
// members:
|
// members:
|
||||||
|
std::function<void()> m_standin;
|
||||||
|
std::string m_standintitle;
|
||||||
std::vector<std::pair<std::string, std::shared_ptr<std::string>>> m_nodebank;
|
std::vector<std::pair<std::string, std::shared_ptr<std::string>>> m_nodebank;
|
||||||
char m_nodesearch[128];
|
char m_nodesearch[128];
|
||||||
std::shared_ptr<std::string> m_selectedtemplate;
|
std::shared_ptr<std::string> m_selectedtemplate;
|
||||||
@@ -295,4 +304,4 @@ class functions_panel : public ui_panel
|
|||||||
scene::group_handle m_grouphandle{null_handle}; // scene group bound to the panel
|
scene::group_handle m_grouphandle{null_handle}; // scene group bound to the panel
|
||||||
std::string m_groupprefix;
|
std::string m_groupprefix;
|
||||||
std::vector<text_line> m_grouplines;
|
std::vector<text_line> m_grouplines;
|
||||||
};
|
};
|
||||||
@@ -120,13 +120,15 @@ class ground_mesh
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// NOTE: clamped before the conversion: a rectangle asked about can reach far beyond the mesh (to infinity even),
|
||||||
|
// and a float out of the range of int doesn't turn into anything sensible
|
||||||
int column(float const X) const
|
int column(float const X) const
|
||||||
{
|
{
|
||||||
return std::clamp(static_cast<int>(std::floor((X - m_low.x) / m_cellsize)), 0, m_columns - 1);
|
return static_cast<int>(std::clamp(std::floor((X - m_low.x) / m_cellsize), 0.f, static_cast<float>(m_columns - 1)));
|
||||||
}
|
}
|
||||||
int row(float const Z) const
|
int row(float const Z) const
|
||||||
{
|
{
|
||||||
return std::clamp(static_cast<int>(std::floor((Z - m_low.y) / m_cellsize)), 0, m_rows - 1);
|
return static_cast<int>(std::clamp(std::floor((Z - m_low.y) / m_cellsize), 0.f, static_cast<float>(m_rows - 1)));
|
||||||
}
|
}
|
||||||
// cells the bounds of a triangle reach: first column, first row, last column, last row
|
// cells the bounds of a triangle reach: first column, first row, last column, last row
|
||||||
std::array<int, 4> cells(triangle const &Triangle) const
|
std::array<int, 4> cells(triangle const &Triangle) const
|
||||||
|
|||||||
@@ -136,6 +136,8 @@ void terrain_streamer::close()
|
|||||||
m_loading.clear();
|
m_loading.clear();
|
||||||
m_removed.clear();
|
m_removed.clear();
|
||||||
m_palette.clear();
|
m_palette.clear();
|
||||||
|
m_recording = 0;
|
||||||
|
m_change = change();
|
||||||
m_active = false;
|
m_active = false;
|
||||||
m_name.clear();
|
m_name.clear();
|
||||||
m_directory.clear();
|
m_directory.clear();
|
||||||
@@ -159,6 +161,8 @@ void terrain_streamer::reset()
|
|||||||
m_removed.clear();
|
m_removed.clear();
|
||||||
m_palette.clear();
|
m_palette.clear();
|
||||||
clear_water(false);
|
clear_water(false);
|
||||||
|
m_recording = 0;
|
||||||
|
m_change = change();
|
||||||
m_active = false;
|
m_active = false;
|
||||||
m_name.clear();
|
m_name.clear();
|
||||||
m_directory.clear();
|
m_directory.clear();
|
||||||
@@ -898,6 +902,24 @@ bool terrain_streamer::add_chunk(int Cx, int Cz, float Spacing, std::function<fl
|
|||||||
for (int iz = 0; iz <= data->cells; ++iz)
|
for (int iz = 0; iz <= data->cells; ++iz)
|
||||||
for (int ix = 0; ix <= data->cells; ++ix)
|
for (int ix = 0; ix <= data->cells; ++ix)
|
||||||
data->heights[static_cast<std::size_t>(iz) * data->side() + ix] = Height(low.x + ix * step, low.y + iz * step);
|
data->heights[static_cast<std::size_t>(iz) * data->side() + ix] = Height(low.x + ix * step, low.y + iz * step);
|
||||||
|
return add_chunk(std::move(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool terrain_streamer::add_chunk(std::shared_ptr<heightmap::chunk_data> Data)
|
||||||
|
{
|
||||||
|
if (Data == nullptr || false == Data->valid())
|
||||||
|
return false;
|
||||||
|
auto const key{Data->key};
|
||||||
|
if (false == m_active || load_now(key) != nullptr || m_chunks.count(key) > 0)
|
||||||
|
return false;
|
||||||
|
if (false == in_region(glm::dvec2{chunk_centre(key).x, chunk_centre(key).z}))
|
||||||
|
return false;
|
||||||
|
for (auto &layer : Data->layers)
|
||||||
|
if (layer != heightmap::no_layer && layer >= m_manifest.layers.size())
|
||||||
|
layer = 0;
|
||||||
|
auto data{std::move(Data)};
|
||||||
|
auto const low{data->corner()};
|
||||||
|
remember(key, nullptr);
|
||||||
auto chunk{std::make_unique<editor_terrain>(std::move(data))};
|
auto chunk{std::make_unique<editor_terrain>(std::move(data))};
|
||||||
chunk->m_modified = true;
|
chunk->m_modified = true;
|
||||||
auto const [lowest, highest]{std::minmax_element(chunk->heights().begin(), chunk->heights().end())};
|
auto const [lowest, highest]{std::minmax_element(chunk->heights().begin(), chunk->heights().end())};
|
||||||
@@ -915,6 +937,12 @@ bool terrain_streamer::add_chunk(int Cx, int Cz, float Spacing, std::function<fl
|
|||||||
void terrain_streamer::remove_chunk(int Cx, int Cz)
|
void terrain_streamer::remove_chunk(int Cx, int Cz)
|
||||||
{
|
{
|
||||||
chunk_key const key{Cx, Cz};
|
chunk_key const key{Cx, Cz};
|
||||||
|
if (recording())
|
||||||
|
{
|
||||||
|
// the data is needed to bring the chunk back
|
||||||
|
if (auto const *chunk{load_now(key)})
|
||||||
|
remember(*chunk);
|
||||||
|
}
|
||||||
auto const lookup{m_chunks.find(key)};
|
auto const lookup{m_chunks.find(key)};
|
||||||
if (lookup != m_chunks.end())
|
if (lookup != m_chunks.end())
|
||||||
{
|
{
|
||||||
@@ -931,6 +959,7 @@ void terrain_streamer::spacing(int Cx, int Cz, float Spacing)
|
|||||||
auto *chunk{load_now({Cx, Cz})};
|
auto *chunk{load_now({Cx, Cz})};
|
||||||
if (chunk == nullptr || heightmap::cells_for(Spacing) == chunk->cells())
|
if (chunk == nullptr || heightmap::cells_for(Spacing) == chunk->cells())
|
||||||
return;
|
return;
|
||||||
|
remember(*chunk);
|
||||||
chunk->replace(chunk->data().resampled(Spacing), true, false);
|
chunk->replace(chunk->data().resampled(Spacing), true, false);
|
||||||
auto const low{chunk->data().corner()};
|
auto const low{chunk->data().corner()};
|
||||||
apply_modifiers(*chunk, low, low + glm::dvec2{heightmap::chunk_size});
|
apply_modifiers(*chunk, low, low + glm::dvec2{heightmap::chunk_size});
|
||||||
@@ -957,6 +986,7 @@ bool terrain_streamer::reshape(glm::dvec2 const &Min, glm::dvec2 const &Max, std
|
|||||||
shaped |= Shaper(low.x + ix * step, low.y + iz * step, heights[static_cast<std::size_t>(iz) * current.side() + ix]);
|
shaped |= Shaper(low.x + ix * step, low.y + iz * step, heights[static_cast<std::size_t>(iz) * current.side() + ix]);
|
||||||
if (false == shaped)
|
if (false == shaped)
|
||||||
continue;
|
continue;
|
||||||
|
remember(*chunk);
|
||||||
auto &data{chunk->edit(true, false)};
|
auto &data{chunk->edit(true, false)};
|
||||||
(data.base.empty() ? data.heights : data.base) = std::move(heights);
|
(data.base.empty() ? data.heights : data.base) = std::move(heights);
|
||||||
if (false == data.base.empty())
|
if (false == data.base.empty())
|
||||||
@@ -1081,6 +1111,7 @@ bool terrain_streamer::paint(double X, double Z, double Radius, double Strength,
|
|||||||
if (false == painted)
|
if (false == painted)
|
||||||
continue;
|
continue;
|
||||||
data->compact_layers();
|
data->compact_layers();
|
||||||
|
remember(*chunk);
|
||||||
chunk->replace(std::move(data), false, true);
|
chunk->replace(std::move(data), false, true);
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
@@ -1092,6 +1123,7 @@ std::uint16_t terrain_streamer::layer(std::string const &Material, float const S
|
|||||||
for (std::size_t i = 0; i < m_manifest.layers.size(); ++i)
|
for (std::size_t i = 0; i < m_manifest.layers.size(); ++i)
|
||||||
if (m_manifest.layers[i].material == Material && (Size <= 0.f || m_manifest.layers[i].size == Size))
|
if (m_manifest.layers[i].material == Material && (Size <= 0.f || m_manifest.layers[i].size == Size))
|
||||||
return static_cast<std::uint16_t>(i);
|
return static_cast<std::uint16_t>(i);
|
||||||
|
remember_manifest();
|
||||||
m_manifest.layers.push_back({Material, std::max(0.f, Size)});
|
m_manifest.layers.push_back({Material, std::max(0.f, Size)});
|
||||||
m_manifestchanged = true;
|
m_manifestchanged = true;
|
||||||
return static_cast<std::uint16_t>(m_manifest.layers.size() - 1);
|
return static_cast<std::uint16_t>(m_manifest.layers.size() - 1);
|
||||||
@@ -1101,6 +1133,7 @@ void terrain_streamer::layer_size(std::uint16_t const Layer, float const Size)
|
|||||||
{
|
{
|
||||||
if (Layer >= m_manifest.layers.size() || m_manifest.layers[Layer].size == Size)
|
if (Layer >= m_manifest.layers.size() || m_manifest.layers[Layer].size == Size)
|
||||||
return;
|
return;
|
||||||
|
remember_manifest();
|
||||||
m_manifest.layers[Layer].size = std::max(0.f, Size);
|
m_manifest.layers[Layer].size = std::max(0.f, Size);
|
||||||
m_manifestchanged = true;
|
m_manifestchanged = true;
|
||||||
// the materials of the chunks using it are made again
|
// the materials of the chunks using it are made again
|
||||||
@@ -1109,6 +1142,19 @@ void terrain_streamer::layer_size(std::uint16_t const Layer, float const Size)
|
|||||||
entry.second->m_paintedversion = 0;
|
entry.second->m_paintedversion = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void terrain_streamer::layer_material(std::uint16_t const Layer, std::string const &Material)
|
||||||
|
{
|
||||||
|
if (Layer >= m_manifest.layers.size() || Material.empty() || m_manifest.layers[Layer].material == Material)
|
||||||
|
return;
|
||||||
|
remember_manifest();
|
||||||
|
m_manifest.layers[Layer].material = Material;
|
||||||
|
m_manifestchanged = true;
|
||||||
|
m_palette.erase(Layer);
|
||||||
|
for (auto &entry : m_chunks)
|
||||||
|
if (entry.second->data().slot_of(Layer) >= 0 || (Layer == 0 && entry.second->data().layer_count() == 0))
|
||||||
|
entry.second->m_paintedversion = 0;
|
||||||
|
}
|
||||||
|
|
||||||
void terrain_streamer::compress(bool const State)
|
void terrain_streamer::compress(bool const State)
|
||||||
{
|
{
|
||||||
if (m_manifest.compress == State)
|
if (m_manifest.compress == State)
|
||||||
@@ -1128,6 +1174,7 @@ void terrain_streamer::default_spacing(float const Spacing)
|
|||||||
|
|
||||||
void terrain_streamer::water(std::vector<heightmap::water_body> Water)
|
void terrain_streamer::water(std::vector<heightmap::water_body> Water)
|
||||||
{
|
{
|
||||||
|
remember_manifest();
|
||||||
m_manifest.water = std::move(Water);
|
m_manifest.water = std::move(Water);
|
||||||
m_manifestchanged = true;
|
m_manifestchanged = true;
|
||||||
m_waterchanged = true;
|
m_waterchanged = true;
|
||||||
@@ -1155,12 +1202,14 @@ void terrain_streamer::apply_modifiers(editor_terrain &Chunk, glm::dvec2 const &
|
|||||||
// no modifier reaches the chunk (any more): its own heights are what's left
|
// no modifier reaches the chunk (any more): its own heights are what's left
|
||||||
if (false == current.base.empty())
|
if (false == current.base.empty())
|
||||||
{
|
{
|
||||||
|
remember(Chunk);
|
||||||
auto &data{Chunk.edit(true, false)};
|
auto &data{Chunk.edit(true, false)};
|
||||||
data.heights = std::move(data.base);
|
data.heights = std::move(data.base);
|
||||||
data.base.clear();
|
data.base.clear();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
remember(Chunk);
|
||||||
auto &data{Chunk.edit(true, false)};
|
auto &data{Chunk.edit(true, false)};
|
||||||
if (data.base.empty())
|
if (data.base.empty())
|
||||||
data.base = data.heights;
|
data.base = data.heights;
|
||||||
@@ -1187,6 +1236,7 @@ void terrain_streamer::apply_modifiers(glm::dvec2 const &Min, glm::dvec2 const &
|
|||||||
|
|
||||||
void terrain_streamer::modifier(heightmap::modifier Modifier)
|
void terrain_streamer::modifier(heightmap::modifier Modifier)
|
||||||
{
|
{
|
||||||
|
remember_manifest();
|
||||||
auto bounds{Modifier.bounds()};
|
auto bounds{Modifier.bounds()};
|
||||||
auto const existing{std::find_if(m_manifest.modifiers.begin(), m_manifest.modifiers.end(), [&](heightmap::modifier const &Item) { return Item.name == Modifier.name; })};
|
auto const existing{std::find_if(m_manifest.modifiers.begin(), m_manifest.modifiers.end(), [&](heightmap::modifier const &Item) { return Item.name == Modifier.name; })};
|
||||||
if (existing != m_manifest.modifiers.end())
|
if (existing != m_manifest.modifiers.end())
|
||||||
@@ -1208,6 +1258,7 @@ bool terrain_streamer::remove_modifier(std::string const &Name)
|
|||||||
auto const existing{std::find_if(m_manifest.modifiers.begin(), m_manifest.modifiers.end(), [&](heightmap::modifier const &Item) { return Item.name == Name; })};
|
auto const existing{std::find_if(m_manifest.modifiers.begin(), m_manifest.modifiers.end(), [&](heightmap::modifier const &Item) { return Item.name == Name; })};
|
||||||
if (existing == m_manifest.modifiers.end())
|
if (existing == m_manifest.modifiers.end())
|
||||||
return false;
|
return false;
|
||||||
|
remember_manifest();
|
||||||
auto const bounds{existing->bounds()};
|
auto const bounds{existing->bounds()};
|
||||||
m_manifest.modifiers.erase(existing);
|
m_manifest.modifiers.erase(existing);
|
||||||
m_manifestchanged = true;
|
m_manifestchanged = true;
|
||||||
@@ -1222,6 +1273,103 @@ bool terrain_streamer::modified() const
|
|||||||
return std::any_of(m_chunks.begin(), m_chunks.end(), [](auto const &Entry) { return Entry.second->modified(); });
|
return std::any_of(m_chunks.begin(), m_chunks.end(), [](auto const &Entry) { return Entry.second->modified(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// undo
|
||||||
|
|
||||||
|
void terrain_streamer::begin_change()
|
||||||
|
{
|
||||||
|
if (m_recording++ == 0)
|
||||||
|
m_change = change();
|
||||||
|
}
|
||||||
|
|
||||||
|
terrain_streamer::change terrain_streamer::end_change()
|
||||||
|
{
|
||||||
|
if (m_recording == 0)
|
||||||
|
return change();
|
||||||
|
if (--m_recording > 0)
|
||||||
|
return change(); // the outer change goes on
|
||||||
|
change result{std::move(m_change)};
|
||||||
|
m_change = change();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void terrain_streamer::remember(chunk_key const &Key, heightmap::chunk_ptr const &Data)
|
||||||
|
{
|
||||||
|
if (m_recording > 0)
|
||||||
|
m_change.chunks.emplace(Key, Data); // only the first state counts
|
||||||
|
}
|
||||||
|
|
||||||
|
void terrain_streamer::remember_manifest()
|
||||||
|
{
|
||||||
|
if (m_recording == 0 || m_change.manifest)
|
||||||
|
return;
|
||||||
|
m_change.manifest = true;
|
||||||
|
m_change.layers = m_manifest.layers;
|
||||||
|
m_change.water = m_manifest.water;
|
||||||
|
m_change.modifiers = m_manifest.modifiers;
|
||||||
|
}
|
||||||
|
|
||||||
|
terrain_streamer::change terrain_streamer::restore(change const &Change)
|
||||||
|
{
|
||||||
|
change opposite;
|
||||||
|
if (false == m_active)
|
||||||
|
return opposite;
|
||||||
|
for (auto const &[key, data] : Change.chunks)
|
||||||
|
{
|
||||||
|
auto *current{load_now(key)};
|
||||||
|
opposite.chunks.emplace(key, current != nullptr ? current->shared() : nullptr);
|
||||||
|
if (data == nullptr)
|
||||||
|
{
|
||||||
|
if (current != nullptr)
|
||||||
|
{
|
||||||
|
undraw(*current);
|
||||||
|
m_chunks.erase(key);
|
||||||
|
m_removed.insert(key);
|
||||||
|
touch_neighbours(key);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (current != nullptr)
|
||||||
|
{
|
||||||
|
current->replace(data, true, true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auto chunk{std::make_unique<editor_terrain>(data)};
|
||||||
|
chunk->m_modified = true;
|
||||||
|
auto const [low, high]{std::minmax_element(chunk->heights().begin(), chunk->heights().end())};
|
||||||
|
auto const corner{data->corner()};
|
||||||
|
chunk->m_low = {corner.x, *low, corner.y};
|
||||||
|
chunk->m_high = {corner.x + heightmap::chunk_size, *high, corner.y + heightmap::chunk_size};
|
||||||
|
m_chunks.emplace(key, std::move(chunk));
|
||||||
|
m_removed.erase(key);
|
||||||
|
m_loading.erase(key);
|
||||||
|
}
|
||||||
|
touch_neighbours(key);
|
||||||
|
}
|
||||||
|
if (Change.manifest)
|
||||||
|
{
|
||||||
|
opposite.manifest = true;
|
||||||
|
opposite.layers = m_manifest.layers;
|
||||||
|
opposite.water = m_manifest.water;
|
||||||
|
opposite.modifiers = m_manifest.modifiers;
|
||||||
|
auto const samelayers{std::equal(Change.layers.begin(), Change.layers.end(), m_manifest.layers.begin(), m_manifest.layers.end(),
|
||||||
|
[](heightmap::layer_def const &A, heightmap::layer_def const &B) { return A.material == B.material && A.size == B.size; })};
|
||||||
|
m_manifest.layers = Change.layers;
|
||||||
|
m_manifest.modifiers = Change.modifiers;
|
||||||
|
m_manifest.water = Change.water;
|
||||||
|
m_manifestchanged = true;
|
||||||
|
m_waterchanged = true;
|
||||||
|
if (false == samelayers)
|
||||||
|
{
|
||||||
|
// the materials of the chunks are made anew from the palette as it was
|
||||||
|
m_palette.clear();
|
||||||
|
for (auto &entry : m_chunks)
|
||||||
|
entry.second->m_paintedversion = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return opposite;
|
||||||
|
}
|
||||||
|
|
||||||
bool terrain_streamer::save(std::string *Error)
|
bool terrain_streamer::save(std::string *Error)
|
||||||
{
|
{
|
||||||
if (false == m_active)
|
if (false == m_active)
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ class terrain_streamer
|
|||||||
bool add_chunk(int Cx, int Cz, float Spacing, float Height, std::uint16_t Layer = 0);
|
bool add_chunk(int Cx, int Cz, float Spacing, float Height, std::uint16_t Layer = 0);
|
||||||
// a chunk at (Cx, Cz), heights given for each grid point by Height (from its world position)
|
// a chunk at (Cx, Cz), heights given for each grid point by Height (from its world position)
|
||||||
bool add_chunk(int Cx, int Cz, float Spacing, std::function<float(double X, double Z)> const &Height, std::uint16_t Layer = 0);
|
bool add_chunk(int Cx, int Cz, float Spacing, std::function<float(double X, double Z)> const &Height, std::uint16_t Layer = 0);
|
||||||
|
// a chunk made elsewhere, at the place its key says; its layers are indices of the palette
|
||||||
|
bool add_chunk(std::shared_ptr<heightmap::chunk_data> Data);
|
||||||
void remove_chunk(int Cx, int Cz);
|
void remove_chunk(int Cx, int Cz);
|
||||||
// changes the point spacing of the chunk at (Cx, Cz), heights resampled
|
// changes the point spacing of the chunk at (Cx, Cz), heights resampled
|
||||||
void spacing(int Cx, int Cz, float Spacing);
|
void spacing(int Cx, int Cz, float Spacing);
|
||||||
@@ -99,6 +101,8 @@ class terrain_streamer
|
|||||||
// palette entry of the material, added to the palette if needed
|
// palette entry of the material, added to the palette if needed
|
||||||
std::uint16_t layer(std::string const &Material, float Size = 0.f);
|
std::uint16_t layer(std::string const &Material, float Size = 0.f);
|
||||||
void layer_size(std::uint16_t Layer, float Size);
|
void layer_size(std::uint16_t Layer, float Size);
|
||||||
|
// gives the palette entry Layer another material; the chunks painted with it change with it
|
||||||
|
void layer_material(std::uint16_t Layer, std::string const &Material);
|
||||||
void compress(bool State);
|
void compress(bool State);
|
||||||
void default_spacing(float Spacing);
|
void default_spacing(float Spacing);
|
||||||
// bodies of water
|
// bodies of water
|
||||||
@@ -110,6 +114,26 @@ class terrain_streamer
|
|||||||
std::vector<heightmap::modifier> const &modifiers() const { return m_manifest.modifiers; }
|
std::vector<heightmap::modifier> const &modifiers() const { return m_manifest.modifiers; }
|
||||||
// changed since the last save
|
// changed since the last save
|
||||||
bool modified() const;
|
bool modified() const;
|
||||||
|
|
||||||
|
// undo: the state of what a change touched, from before it
|
||||||
|
struct change
|
||||||
|
{
|
||||||
|
std::map<chunk_key, heightmap::chunk_ptr> chunks; // the data the chunks had; null: there was no chunk
|
||||||
|
bool manifest{false}; // the palette, the water and the modifiers below were changed
|
||||||
|
std::vector<heightmap::layer_def> layers;
|
||||||
|
std::vector<heightmap::water_body> water;
|
||||||
|
std::vector<heightmap::modifier> modifiers;
|
||||||
|
|
||||||
|
bool empty() const { return chunks.empty() && false == manifest; }
|
||||||
|
};
|
||||||
|
// from now on the state of what's changed is kept, until end_change(); nested calls join the change in progress
|
||||||
|
void begin_change();
|
||||||
|
// returns: the state from before the changes made since begin_change() (empty if nothing changed)
|
||||||
|
change end_change();
|
||||||
|
bool recording() const { return m_recording > 0; }
|
||||||
|
// puts the terrain back in the state recorded. returns: the state it replaced, to go forward again
|
||||||
|
change restore(change const &Change);
|
||||||
|
|
||||||
// writes the description and the changed pack files. returns: true on success, Error describing the failure
|
// writes the description and the changed pack files. returns: true on success, Error describing the failure
|
||||||
bool save(std::string *Error = nullptr);
|
bool save(std::string *Error = nullptr);
|
||||||
|
|
||||||
@@ -200,6 +224,10 @@ class terrain_streamer
|
|||||||
void build_water();
|
void build_water();
|
||||||
void clear_water(bool const Touch);
|
void clear_water(bool const Touch);
|
||||||
std::string pack_path(heightmap::pack_key const &Key) const;
|
std::string pack_path(heightmap::pack_key const &Key) const;
|
||||||
|
// the state of a chunk (null: no chunk) or of the description is kept for the change in progress, if it's the first time
|
||||||
|
void remember(chunk_key const &Key, heightmap::chunk_ptr const &Data);
|
||||||
|
void remember(editor_terrain const &Chunk) { remember(Chunk.key(), Chunk.shared()); }
|
||||||
|
void remember_manifest();
|
||||||
|
|
||||||
bool m_active{false};
|
bool m_active{false};
|
||||||
std::string m_name;
|
std::string m_name;
|
||||||
@@ -221,6 +249,9 @@ class terrain_streamer
|
|||||||
gfx::geometrybank_handle m_waterbank{0, 0};
|
gfx::geometrybank_handle m_waterbank{0, 0};
|
||||||
bool m_waterchanged{false};
|
bool m_waterchanged{false};
|
||||||
|
|
||||||
|
int m_recording{0};
|
||||||
|
change m_change;
|
||||||
|
|
||||||
glm::dvec3 m_camera{0.0};
|
glm::dvec3 m_camera{0.0};
|
||||||
glm::dvec3 m_velocity{0.0};
|
glm::dvec3 m_velocity{0.0};
|
||||||
double m_cameratime{0.0};
|
double m_cameratime{0.0};
|
||||||
|
|||||||
@@ -329,11 +329,24 @@ bool opengl33_terrain_materials::copy_layer(layer &Layer)
|
|||||||
if (slots[i] < 0 || material.textures[slots[i]] == null_handle)
|
if (slots[i] < 0 || material.textures[slots[i]] == null_handle)
|
||||||
continue;
|
continue;
|
||||||
auto &texture{m_textures.mark_as_used(material.textures[slots[i]])};
|
auto &texture{m_textures.mark_as_used(material.textures[slots[i]])};
|
||||||
if (false == texture.create())
|
if (false == texture.is_ready)
|
||||||
{
|
{
|
||||||
if (++Layer.attempts < copy_attempts)
|
// a texture which is on the card already may have dropped its data after the upload (create() would refuse it then),
|
||||||
return false;
|
// one released by the sweep or never read has to be read first
|
||||||
continue; // waited long enough, the layer goes without it
|
if (texture.data_state == resource_state::none)
|
||||||
|
{
|
||||||
|
texture.reload_on_use = false;
|
||||||
|
texture.load();
|
||||||
|
}
|
||||||
|
if (texture.data_state == resource_state::failed)
|
||||||
|
continue;
|
||||||
|
if (false == texture.create())
|
||||||
|
{
|
||||||
|
if (++Layer.attempts < copy_attempts)
|
||||||
|
return false;
|
||||||
|
WriteLog("heightmap terrain: texture \"" + texture.name + "\" isn't ready, the terrain goes without it");
|
||||||
|
continue; // waited long enough, the layer goes without it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sources[i] = &texture;
|
sources[i] = &texture;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -499,12 +499,12 @@ void node_layers::shape(material_handle const Material, source_span const &Span)
|
|||||||
m_shapes.push_back({current, Span, Material, false});
|
m_shapes.push_back({current, Span, Material, false});
|
||||||
}
|
}
|
||||||
|
|
||||||
std::pair<std::size_t, std::size_t> node_layers::erase_shapes(std::set<material_handle> const &Materials)
|
std::pair<std::size_t, std::size_t> node_layers::erase_shapes(std::set<material_handle> const &Materials, std::set<layer_handle> const *Layers)
|
||||||
{
|
{
|
||||||
std::pair<std::size_t, std::size_t> result{0, 0};
|
std::pair<std::size_t, std::size_t> result{0, 0};
|
||||||
for (auto &shape : m_shapes)
|
for (auto &shape : m_shapes)
|
||||||
{
|
{
|
||||||
if (shape.erased || Materials.count(shape.material) == 0)
|
if (shape.erased || Materials.count(shape.material) == 0 || (Layers != nullptr && Layers->count(shape.layer) == 0))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -267,9 +267,9 @@ class node_layers
|
|||||||
{
|
{
|
||||||
return m_shapes;
|
return m_shapes;
|
||||||
}
|
}
|
||||||
// drops the definitions of the shapes of specified materials from the writable layer files on save.
|
// drops the definitions of the shapes of specified materials from the writable layer files on save; only from specified layers, if given.
|
||||||
// returns: amount of the shapes to drop, and of the ones which stay as their files can't be rewritten
|
// returns: amount of the shapes to drop, and of the ones which stay as their files can't be rewritten
|
||||||
std::pair<std::size_t, std::size_t> erase_shapes(std::set<material_handle> const &Materials);
|
std::pair<std::size_t, std::size_t> erase_shapes(std::set<material_handle> const &Materials, std::set<layer_handle> const *Layers = nullptr);
|
||||||
// indicates the scenario initialization (FirstInit) is being performed. Span: location of the directive.
|
// indicates the scenario initialization (FirstInit) is being performed. Span: location of the directive.
|
||||||
// Infile: the directive comes straight from the layer file being loaded
|
// Infile: the directive comes straight from the layer file being loaded
|
||||||
void initialization(source_span const &Span, bool Infile);
|
void initialization(source_span const &Span, bool Infile);
|
||||||
|
|||||||
@@ -429,7 +429,9 @@ bool
|
|||||||
shape_node::merge( shape_node &Shape ) {
|
shape_node::merge( shape_node &Shape ) {
|
||||||
|
|
||||||
if( m_data.material != Shape.m_data.material
|
if( m_data.material != Shape.m_data.material
|
||||||
|| m_data.lighting != Shape.m_data.lighting ) {
|
|| m_data.lighting != Shape.m_data.lighting
|
||||||
|
|| m_data.terrainfile != Shape.m_data.terrainfile ) {
|
||||||
|
// (shapes of different terrain files are kept apart, so the editor can tell which file the geometry comes from)
|
||||||
// can't merge nodes with different appearance
|
// can't merge nodes with different appearance
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ public:
|
|||||||
gfx::geometry_handle geometry { 0, 0 }; // relative origin-centered chunk of geometry held by gfx renderer
|
gfx::geometry_handle geometry { 0, 0 }; // relative origin-centered chunk of geometry held by gfx renderer
|
||||||
std::vector<world_vertex> vertices; // world space source data of the geometry
|
std::vector<world_vertex> vertices; // world space source data of the geometry
|
||||||
gfx::userdata_array userdata;
|
gfx::userdata_array userdata;
|
||||||
|
// terrain file the shape comes from, see scene::terrain_file::reference_of(); 0: none, or not known (only the editor asks)
|
||||||
|
std::uint16_t terrainfile { 0 };
|
||||||
// methods:
|
// methods:
|
||||||
// sends content of the struct to provided stream
|
// sends content of the struct to provided stream
|
||||||
void
|
void
|
||||||
@@ -152,6 +154,9 @@ public:
|
|||||||
// sets whether the shape is drawn with the translucent geometry (used by generated geometry)
|
// sets whether the shape is drawn with the translucent geometry (used by generated geometry)
|
||||||
void
|
void
|
||||||
translucent( bool const Translucent );
|
translucent( bool const Translucent );
|
||||||
|
// notes the terrain file the shape comes from
|
||||||
|
void
|
||||||
|
source_file( std::uint16_t const Reference ) { m_data.terrainfile = Reference; }
|
||||||
// data access
|
// data access
|
||||||
shapenode_data const &
|
shapenode_data const &
|
||||||
data() const;
|
data() const;
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ http://mozilla.org/MPL/2.0/.
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <sstream>
|
||||||
|
#include <stack>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -75,6 +77,7 @@ struct terrain_source
|
|||||||
};
|
};
|
||||||
std::string file;
|
std::string file;
|
||||||
std::vector<material> materials;
|
std::vector<material> materials;
|
||||||
|
std::uint16_t reference{0}; // see terrain_file::reference_of()
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
@@ -739,6 +742,233 @@ std::string terrain_file::extra(std::string const &Binaryfile)
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<terrain_reference> &terrain_file::references()
|
||||||
|
{
|
||||||
|
static std::vector<terrain_reference> references;
|
||||||
|
return references;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint16_t terrain_file::reference_of(std::string const &File)
|
||||||
|
{
|
||||||
|
auto file{File};
|
||||||
|
replace_slashes(file);
|
||||||
|
auto const &list{references()};
|
||||||
|
for (std::size_t index{0}; index < list.size() && index < std::numeric_limits<std::uint16_t>::max(); ++index)
|
||||||
|
{
|
||||||
|
auto const base{Global.asCurrentSceneryPath + list[index].file};
|
||||||
|
if (file.ends_with(base + ".txtf") || file.ends_with(base + ".btf"))
|
||||||
|
{
|
||||||
|
return static_cast<std::uint16_t>(index + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// world position of a vertex of a shape placed with specified rotation and offset, the way basic_region::insert() does it
|
||||||
|
glm::dvec3 place_vertex(glm::dvec3 Position, glm::vec3 const &Rotation, glm::dvec3 const &Offset)
|
||||||
|
{
|
||||||
|
if (Rotation != glm::vec3(0.f))
|
||||||
|
{
|
||||||
|
auto const rotation{glm::radians(glm::dvec3{Rotation})};
|
||||||
|
Position = glm::rotateZ<double>(Position, rotation.z);
|
||||||
|
Position = glm::rotateX<double>(Position, rotation.x);
|
||||||
|
Position = glm::rotateY<double>(Position, rotation.y);
|
||||||
|
}
|
||||||
|
return Position + Offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool read_text_triangles(std::string const &Text, terrain_reference const &Reference, terrain_file::triangle_visitor const &Visit, std::string &Message)
|
||||||
|
{
|
||||||
|
conversion_scope scope;
|
||||||
|
std::stack<glm::dvec3> offsets;
|
||||||
|
offsets.push(Reference.offset);
|
||||||
|
auto rotation{Reference.rotation};
|
||||||
|
std::size_t count{0};
|
||||||
|
cParser input{Text, cParser::buffer_TEXT};
|
||||||
|
input.expandIncludes = false;
|
||||||
|
auto token{input.getToken<std::string>()};
|
||||||
|
while (false == token.empty())
|
||||||
|
{
|
||||||
|
if (token == "node")
|
||||||
|
{
|
||||||
|
node_data nodedata;
|
||||||
|
input.getTokens(4);
|
||||||
|
input >> nodedata.range_max >> nodedata.range_min >> nodedata.name >> nodedata.type;
|
||||||
|
if (nodedata.type == "triangles" || nodedata.type == "triangle_strip" || nodedata.type == "triangle_fan")
|
||||||
|
{
|
||||||
|
auto const shape{shape_node().import(input, nodedata)};
|
||||||
|
auto const &data{shape.data()};
|
||||||
|
auto const *material{GfxRenderer->Material(data.material)};
|
||||||
|
auto const name{material != nullptr ? material->GetName() : std::string{}};
|
||||||
|
for (std::size_t index{0}; index + 2 < data.vertices.size(); index += 3)
|
||||||
|
{
|
||||||
|
Visit(name, {place_vertex(data.vertices[index].position, rotation, offsets.top()), place_vertex(data.vertices[index + 1].position, rotation, offsets.top()),
|
||||||
|
place_vertex(data.vertices[index + 2].position, rotation, offsets.top())});
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// anything else the file holds (the editor keeps sweeps there) is of no interest here
|
||||||
|
auto const end{"end" + nodedata.type};
|
||||||
|
auto rest{input.getToken<std::string>()};
|
||||||
|
while (false == rest.empty() && rest != end)
|
||||||
|
rest = input.getToken<std::string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (token == "origin")
|
||||||
|
{
|
||||||
|
glm::dvec3 offset;
|
||||||
|
input.getTokens(3);
|
||||||
|
input >> offset.x >> offset.y >> offset.z;
|
||||||
|
offsets.push(offset + offsets.top());
|
||||||
|
}
|
||||||
|
else if (token == "endorigin")
|
||||||
|
{
|
||||||
|
if (offsets.size() > 1)
|
||||||
|
{
|
||||||
|
offsets.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (token == "rotate")
|
||||||
|
{
|
||||||
|
input.getTokens(3);
|
||||||
|
input >> rotation.x >> rotation.y >> rotation.z;
|
||||||
|
}
|
||||||
|
token = input.getToken<std::string>();
|
||||||
|
}
|
||||||
|
Message = std::to_string(count) + " triangle(s)";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool read_binary_triangles(std::string const &Binaryfile, terrain_file::triangle_visitor const &Visit, std::string &Message)
|
||||||
|
{
|
||||||
|
std::string content;
|
||||||
|
if (false == read_file(Binaryfile, content))
|
||||||
|
{
|
||||||
|
Message = "can't read the file";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::istringstream stream{content};
|
||||||
|
file_header header;
|
||||||
|
if (false == read_header(stream, header))
|
||||||
|
{
|
||||||
|
Message = "the file is of either unrecognized type or version";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::vector<std::string> materials;
|
||||||
|
for (std::uint32_t index{0}; index < header.materialcount; ++index)
|
||||||
|
{
|
||||||
|
std::string name;
|
||||||
|
if (false == static_cast<bool>(std::getline(stream, name, '\0')))
|
||||||
|
{
|
||||||
|
Message = "the file is damaged";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
materials.emplace_back(std::move(name));
|
||||||
|
}
|
||||||
|
auto const directorystart{static_cast<std::size_t>(stream.tellg())};
|
||||||
|
std::size_t const entrysize{sizeof(std::uint32_t) + sizeof(std::uint64_t) + sizeof(std::uint32_t) + sizeof(float)};
|
||||||
|
if (directorystart > content.size() || content.size() - directorystart < header.sectioncount * entrysize)
|
||||||
|
{
|
||||||
|
Message = "the file is damaged";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
data_reader directory{content.data() + directorystart, header.sectioncount * entrysize};
|
||||||
|
std::size_t count{0};
|
||||||
|
for (std::uint32_t entry{0}; entry < header.sectioncount; ++entry)
|
||||||
|
{
|
||||||
|
auto const sectionindex{directory.get<std::uint32_t>()};
|
||||||
|
auto const offset{directory.get<std::uint64_t>()};
|
||||||
|
auto const size{directory.get<std::uint32_t>()};
|
||||||
|
directory.get<float>();
|
||||||
|
if (false == directory.good() || offset > content.size() || content.size() - offset < size)
|
||||||
|
{
|
||||||
|
Message = "the file is damaged";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// the vertices are relative to the centre of their section, see attach()
|
||||||
|
auto const column{static_cast<int>(sectionindex % EU07_REGIONSIDESECTIONCOUNT)};
|
||||||
|
auto const row{static_cast<int>(sectionindex / EU07_REGIONSIDESECTIONCOUNT)};
|
||||||
|
auto const centeroffset{-(EU07_REGIONSIDESECTIONCOUNT / 2 * EU07_SECTIONSIZE) + EU07_SECTIONSIZE / 2};
|
||||||
|
glm::dvec3 const centre{static_cast<double>(centeroffset + column * EU07_SECTIONSIZE), 0.0, static_cast<double>(centeroffset + row * EU07_SECTIONSIZE)};
|
||||||
|
data_reader reader{content.data() + offset, size};
|
||||||
|
if (reader.get<std::uint32_t>() != EU07_TERRAINBLOCK)
|
||||||
|
{
|
||||||
|
Message = "the file is damaged";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
reader.get<std::uint32_t>(); // section index
|
||||||
|
auto shapecount{reader.get<std::uint32_t>()};
|
||||||
|
while (shapecount-- > 0 && reader.good())
|
||||||
|
{
|
||||||
|
auto const materialindex{reader.get<std::uint32_t>()};
|
||||||
|
// cell, ranges, colours, centre and radius
|
||||||
|
reader.skip(sizeof(std::uint32_t) + 2 * sizeof(double) + 12 * sizeof(float) + 3 * sizeof(double) + sizeof(float));
|
||||||
|
auto const vertexcount{reader.get<std::uint32_t>()};
|
||||||
|
if (false == reader.good() || false == reader.has(static_cast<std::size_t>(vertexcount) * EU07_TERRAINVERTEXSIZE) || materialindex >= materials.size())
|
||||||
|
{
|
||||||
|
Message = "the file is damaged";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::array<glm::dvec3, 3> triangle;
|
||||||
|
for (std::uint32_t vertex{0}; vertex < vertexcount; ++vertex)
|
||||||
|
{
|
||||||
|
float values[8];
|
||||||
|
reader.get(values, 8);
|
||||||
|
triangle[vertex % 3] = centre + glm::dvec3{values[0], values[1], values[2]};
|
||||||
|
if (vertex % 3 == 2)
|
||||||
|
{
|
||||||
|
Visit(materials[materialindex], triangle);
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (false == reader.good())
|
||||||
|
{
|
||||||
|
Message = "the file is damaged";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message = std::to_string(count) + " triangle(s)";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool terrain_file::read_triangles(terrain_reference const &Reference, triangle_visitor const &Visit, std::string *Message, bool *Text)
|
||||||
|
{
|
||||||
|
auto const textfile{Global.asCurrentSceneryPath + Reference.file + ".txtf"};
|
||||||
|
auto const binaryfile{Global.asCurrentSceneryPath + Reference.file + ".btf"};
|
||||||
|
std::string message;
|
||||||
|
auto result{false};
|
||||||
|
auto const fromtext{FileExists(textfile)};
|
||||||
|
if (fromtext)
|
||||||
|
{
|
||||||
|
std::string text;
|
||||||
|
result = read_file(textfile, text) && read_text_triangles(text, Reference, Visit, message);
|
||||||
|
if (message.empty())
|
||||||
|
message = "can't read the file";
|
||||||
|
}
|
||||||
|
else if (FileExists(binaryfile))
|
||||||
|
{
|
||||||
|
// the binary file holds the geometry where the text alone puts it; it stands in for files placed without offset or rotation only
|
||||||
|
result = read_binary_triangles(binaryfile, Visit, message);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
message = "the file is missing";
|
||||||
|
}
|
||||||
|
if (Message != nullptr)
|
||||||
|
*Message = message;
|
||||||
|
if (Text != nullptr)
|
||||||
|
*Text = fromtext;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// makes content of specified binary terrain file a part of provided region
|
// makes content of specified binary terrain file a part of provided region
|
||||||
bool terrain_file::attach(std::string const &Binaryfile, basic_region &Region, bool Deferred)
|
bool terrain_file::attach(std::string const &Binaryfile, basic_region &Region, bool Deferred)
|
||||||
{
|
{
|
||||||
@@ -751,6 +981,7 @@ bool terrain_file::attach(std::string const &Binaryfile, basic_region &Region, b
|
|||||||
}
|
}
|
||||||
auto source{std::make_shared<terrain_source>()};
|
auto source{std::make_shared<terrain_source>()};
|
||||||
source->file = Binaryfile;
|
source->file = Binaryfile;
|
||||||
|
source->reference = reference_of(Binaryfile);
|
||||||
source->materials.reserve(header.materialcount);
|
source->materials.reserve(header.materialcount);
|
||||||
for (std::uint32_t index{0}; index < header.materialcount; ++index)
|
for (std::uint32_t index{0}; index < header.materialcount; ++index)
|
||||||
{
|
{
|
||||||
@@ -878,6 +1109,7 @@ void terrain_file::load(basic_section &Section, terrain_block const &Block)
|
|||||||
}
|
}
|
||||||
shapedata.material = material.handle;
|
shapedata.material = material.handle;
|
||||||
shapedata.translucent = material.translucent;
|
shapedata.translucent = material.translucent;
|
||||||
|
shapedata.terrainfile = source.reference;
|
||||||
shapedata.vertices.resize(vertexcount);
|
shapedata.vertices.resize(vertexcount);
|
||||||
for (auto &vertex : shapedata.vertices)
|
for (auto &vertex : shapedata.vertices)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,8 +9,12 @@ http://mozilla.org/MPL/2.0/.
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
namespace scene
|
namespace scene
|
||||||
{
|
{
|
||||||
@@ -22,10 +26,20 @@ struct terrain_block;
|
|||||||
// binary terrain file (.btf): static geometry of a text terrain file (.txtf), arranged by the sections
|
// binary terrain file (.btf): static geometry of a text terrain file (.txtf), arranged by the sections
|
||||||
// of the scene so each can be loaded on its own, when it comes into use.
|
// of the scene so each can be loaded on its own, when it comes into use.
|
||||||
// the binary file records length and checksum of the text it was made from, and is made anew when these don't match
|
// the binary file records length and checksum of the text it was made from, and is made anew when these don't match
|
||||||
|
// terrain file named by a terrain directive of the scenery, with the placement it was loaded with
|
||||||
|
struct terrain_reference
|
||||||
|
{
|
||||||
|
std::string file; // name relative to the scenery folder, without extension
|
||||||
|
glm::dvec3 offset{0.0};
|
||||||
|
glm::vec3 rotation{0.f};
|
||||||
|
};
|
||||||
|
|
||||||
class terrain_file
|
class terrain_file
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
// types
|
// types
|
||||||
|
// receives a triangle of a terrain file: name of its material, the vertices in the world
|
||||||
|
using triangle_visitor = std::function<void(std::string const &Material, std::array<glm::dvec3, 3> const &Triangle)>;
|
||||||
// what a scenery is left with for a terrain file it refers to
|
// what a scenery is left with for a terrain file it refers to
|
||||||
enum class state
|
enum class state
|
||||||
{
|
{
|
||||||
@@ -47,6 +61,13 @@ class terrain_file
|
|||||||
// loads specified piece of a binary terrain file into provided section
|
// loads specified piece of a binary terrain file into provided section
|
||||||
static void load(basic_section &Section, terrain_block const &Block);
|
static void load(basic_section &Section, terrain_block const &Block);
|
||||||
static std::string extra(std::string const &Binaryfile);
|
static std::string extra(std::string const &Binaryfile);
|
||||||
|
// terrain files used by the scenery being loaded or shown, in the order the directives named them
|
||||||
|
static std::vector<terrain_reference> &references();
|
||||||
|
// number (counted from 1) of the reference whose text or binary file is the specified file, 0 if there's none
|
||||||
|
static std::uint16_t reference_of(std::string const &File);
|
||||||
|
// goes through the triangles of the terrain file of specified reference, read from the text file if it's there,
|
||||||
|
// otherwise from the binary one. returns: true on success, Message describing what was read or the failure
|
||||||
|
static bool read_triangles(terrain_reference const &Reference, triangle_visitor const &Visit, std::string *Message = nullptr, bool *Text = nullptr);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// converts provided content of a text terrain file to binary format and stores it in specified file. returns: true on success, false otherwise
|
// converts provided content of a text terrain file to binary format and stores it in specified file. returns: true on success, false otherwise
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ state_serializer::deserialize_begin( std::string const &Scenariofile ) {
|
|||||||
// drop any streamed editor terrain from a previously loaded scenery before the old region (and
|
// drop any streamed editor terrain from a previously loaded scenery before the old region (and
|
||||||
// its sections, which those chunks referenced) is destroyed below
|
// its sections, which those chunks referenced) is destroyed below
|
||||||
EditorTerrain.reset();
|
EditorTerrain.reset();
|
||||||
|
scene::terrain_file::references().clear();
|
||||||
|
|
||||||
// TODO: move initialization to separate routine so we can reuse it
|
// TODO: move initialization to separate routine so we can reuse it
|
||||||
SafeDelete( Region );
|
SafeDelete( Region );
|
||||||
@@ -722,6 +723,9 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
|
|||||||
++Scratchpad.binary.geometry_imported;
|
++Scratchpad.binary.geometry_imported;
|
||||||
auto shape { scene::shape_node().import( Input, nodedata ) };
|
auto shape { scene::shape_node().import( Input, nodedata ) };
|
||||||
material = shape.data().material;
|
material = shape.data().material;
|
||||||
|
if( true == Global.editor_session ) {
|
||||||
|
shape.source_file( scene::terrain_file::reference_of( Input.Name() ) );
|
||||||
|
}
|
||||||
simulation::Region->insert(
|
simulation::Region->insert(
|
||||||
std::move( shape ),
|
std::move( shape ),
|
||||||
Scratchpad,
|
Scratchpad,
|
||||||
@@ -1188,6 +1192,11 @@ include_terrain_file( std::string File, std::string &Includes, scene::scratch_da
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
binary.terrain_textfiles.emplace_back( File );
|
binary.terrain_textfiles.emplace_back( File );
|
||||||
|
// the editor converts the triangles of the terrain files to heightmap terrain, it has to know them and where they're placed
|
||||||
|
scene::terrain_file::references().push_back( {
|
||||||
|
File,
|
||||||
|
( Scratchpad.location.offset.empty() ? glm::dvec3( 0.0 ) : Scratchpad.location.offset.top() ),
|
||||||
|
Scratchpad.location.rotation } );
|
||||||
|
|
||||||
auto const textfile { Global.asCurrentSceneryPath + File + ".txtf" };
|
auto const textfile { Global.asCurrentSceneryPath + File + ".txtf" };
|
||||||
auto const binaryfile { Global.asCurrentSceneryPath + File + ".btf" };
|
auto const binaryfile { Global.asCurrentSceneryPath + File + ".btf" };
|
||||||
|
|||||||
@@ -485,6 +485,11 @@ void cParser::startIncludeFromParser(cParser& srcParser, bool ToLower, std::stri
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// layers are named by their files relative to the scenery folder; the terrain files (.txtf) come from includes made up by
|
||||||
|
// the loader, relative to the folder of the simulator
|
||||||
|
auto layername{mPath + includefile};
|
||||||
|
layername = (layername.starts_with(Global.asCurrentSceneryPath) ? layername.substr(Global.asCurrentSceneryPath.size()) : includefile);
|
||||||
|
|
||||||
const bool isTerrain = contains(includefile, "_ter.scm");
|
const bool isTerrain = contains(includefile, "_ter.scm");
|
||||||
if (isTerrain && true == Global.file_binary_terrain_state) {
|
if (isTerrain && true == Global.file_binary_terrain_state) {
|
||||||
WriteLog("SBT found, ignoring: " + includefile);
|
WriteLog("SBT found, ignoring: " + includefile);
|
||||||
@@ -493,7 +498,7 @@ void cParser::startIncludeFromParser(cParser& srcParser, bool ToLower, std::stri
|
|||||||
if (sceneryLayers)
|
if (sceneryLayers)
|
||||||
{
|
{
|
||||||
// the file is still a part of the scenery, even though its content comes from the binary terrain file
|
// the file is still a part of the scenery, even though its content comes from the binary terrain file
|
||||||
scene::Layers.layer(scene::Layers.open(includefile, include_site())).binary = true;
|
scene::Layers.layer(scene::Layers.open(layername, include_site())).binary = true;
|
||||||
scene::Layers.close();
|
scene::Layers.close();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -537,7 +542,7 @@ void cParser::startIncludeFromParser(cParser& srcParser, bool ToLower, std::stri
|
|||||||
{
|
{
|
||||||
auto site{include_site()};
|
auto site{include_site()};
|
||||||
site.parameters = false == mIncludeParser->parameters.empty();
|
site.parameters = false == mIncludeParser->parameters.empty();
|
||||||
scene::Layers.open(includefile, site);
|
scene::Layers.open(layername, site);
|
||||||
mIncludeParser->mLayerFile = true;
|
mIncludeParser->mLayerFile = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user