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

Heightmap terrain: .tch chunk packs, streaming with LOD, terrain painting, water and modifiers - terrain/<name>/terrain.yaml + <px>_<pz>.tch packs of 16x16 chunks (250 m, 0.5/1/2/5 m spacing), slices with crc-32 and optional zstd (vendored zstd 1.5.6 in ref/zstd) - heightmap_terrain [name] endheightmap_terrain directive replaces editorterrain - streamer: worker threads, camera + look-ahead loading, geomipmap LOD with skirts, pooled geometry - mat_terrain shader: up to 8 painted layer materials per chunk via texture arrays (opengl33); other renderers fall back to the dominant material - water bodies (outline + level), terrain modifiers (vertical profile shaping kept non-destructive) - F11 Terrain work mode rebuilt: chunks, sculpt, smooth, paint, point spacing, water tools

This commit is contained in:
2026-10-09 21:02:25 +02:00
parent bbccce0e29
commit 35bda3778c
26 changed files with 52888 additions and 1624 deletions

View File

@@ -213,6 +213,7 @@ set(SOURCES
"scene/scenelayers.cpp"
"scene/scenelayerssave.cpp"
"scene/sceneterrain.cpp"
"scene/heightmapterrain.cpp"
"scene/scenemodelentries.cpp"
"simulation/simulationenvironment.cpp"
"simulation/simulationstateserializer.cpp"
@@ -243,6 +244,7 @@ set(SOURCES
"${DEPS_DIR}/glad/src/glad.c"
"${DEPS_DIR}/dds-ktx/src/dds-ktx.c"
"${DEPS_DIR}/zstd/zstd.c"
"gl/shader.cpp"
"gl/vao.cpp"
@@ -333,6 +335,7 @@ if (WITH_OPENGL_MODERN)
"rendering/opengl33skydome.cpp"
"rendering/opengl33precipitation.cpp"
"rendering/opengl33particles.cpp"
"rendering/opengl33terrain.cpp"
"rendering/opengl33renderer.cpp")
set(DEFINITIONS ${DEFINITIONS} "WITH_OPENGL_MODERN")
endif()
@@ -460,6 +463,7 @@ target_include_directories(${PROJECT_NAME} PRIVATE
"gl"
"${DEPS_DIR}/glad/include"
"${DEPS_DIR}/dds-ktx/include"
"${DEPS_DIR}/zstd"
"${CMAKE_BINARY_DIR}/geninc"
"${GLOBAL_INCLUDE_DIR}")
@@ -497,7 +501,7 @@ if(NOT ${CMAKE_VERSION} VERSION_LESS "3.16.0")
if (USE_PCH)
target_precompile_headers(${PROJECT_NAME} PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:${CMAKE_CURRENT_SOURCE_DIR}/stdafx.h>")
set_source_files_properties("/glad/src/glad.c" "ref/dds-ktx/src/dds-ktx.c" "stb/stb_image.c"
set_source_files_properties("/glad/src/glad.c" "ref/dds-ktx/src/dds-ktx.c" "ref/zstd/zstd.c" "stb/stb_image.c"
"imgui/imgui.cpp" "imgui/imgui_demo.cpp" "imgui/imgui_draw.cpp" "imgui/imgui_widgets.cpp" "imgui/imgui_tables.cpp" "imgui/imgui_impl_glfw.cpp" "imgui/ImGuizmo.cpp"
PROPERTIES SKIP_PRECOMPILE_HEADERS TRUE)
endif()
@@ -693,4 +697,4 @@ if(ENABLE_AVX2)
else()
target_compile_options(${PROJECT_NAME} PRIVATE -mavx2)
endif()
endif()
endif()

File diff suppressed because it is too large Load Diff

View File

@@ -282,21 +282,40 @@ class editor_mode : public application_mode, private editor_track::observer
// unless Alone: then the node goes by itself. Joined: undone together with the change before
void snap_to_ground(scene::basic_node *node, bool const Alone = false, bool const Joined = false);
// editable terrain patches created in the editor
// the heightmap terrain of the scenery (terrain/<name>/): chunks, sculpting, painting, water
void render_terrain_ui();
// the terrain as a field of work of its own: the terrain tools and the orthophoto in the tool options window
void render_terrain_tool_options();
// opens or leaves the terrain; leaving it puts the sculpting and the chunk editing away, so the mouse doesn't stay with them
// opens or leaves the terrain; leaving it puts the terrain tools away, so the mouse doesn't stay with them
void terrain_workspace(bool const Open);
// creates a large terrain as a grid of adjacent chunks (each its own editable patch)
void create_chunked_terrain();
// manual grid-aligned chunks: add/remove single chunks for fine control
float chunk_grid_size() const { return m_terrain_cells * m_terrain_cellsize; }
void add_grid_chunk(int Cx, int Cz);
void remove_grid_chunk(int Cx, int Cz);
// opens the heightmap terrain of the scenery for editing, named after the scenery, if the scenery has none yet. returns: true if it's open
bool ensure_terrain();
// adds chunks around the camera, Count along a side, at the height of the ground under each grid point where there's any
void add_chunks_around(int const Count);
// adds a chunk of the terrain shaped as the ground under it; where there's no ground, carried on level with the nearest point
// of it, or at the base height. returns: false if there's a chunk already
bool add_chunk_from_ground(heightmap::chunk_key const &Key, float const Spacing, std::uint16_t const Layer = 0);
// the tool of the terrain the left mouse button works
enum class terrain_tool
{
none, // picks the models, as in the surroundings
chunks, // adds a chunk, Shift: removes it
sculpt, // raises the ground, Shift: lowers it
smooth,
paint, // paints the chosen material, Shift: the first material of the palette
spacing, // gives the clicked chunk the chosen point spacing
water // outlines a body of water
};
terrain_tool m_terrain_tool{terrain_tool::none};
bool terrain_brush() const { return m_terrain_tool == terrain_tool::sculpt || m_terrain_tool == terrain_tool::smooth || m_terrain_tool == terrain_tool::paint; }
bool terrain_clicking() const { return m_terrain_tool == terrain_tool::chunks || m_terrain_tool == terrain_tool::spacing || m_terrain_tool == terrain_tool::water; }
// the brush, the hovered chunk and the outlines of water drawn over the viewport
void draw_terrain_overlay();
// forgets what's known about the ground, after the terrain changed
void forget_ground();
// the scenery ground: triangles of each material, the ones the editor makes itself left out
std::map<material_handle, std::size_t> ground_materials();
// replaces the triangles of specified materials with chunks of editor terrain of their shape; the scenery files lose them on save.
// 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
@@ -307,12 +326,8 @@ class editor_mode : public application_mode, private editor_track::observer
std::string status;
} m_ground_conversion;
void render_ground_conversion();
// handles a click in chunk-edit mode (add a neighbour, or Shift = delete the clicked chunk)
void handle_chunk_edit_click(bool DeleteMode);
// commits authored terrain to disk, enables streaming, and exports the scenery
void save_scene_with_terrain();
// commits authored terrain to disk and enables streaming
void commit_terrain();
// handles a click of the chunk, spacing and water tools (Shift: the other way round)
void handle_terrain_click(bool const Shift);
// writes the changes to the files of scenery opened for editing (Ctrl+S)
bool save();
// places include of specified scenery template at the cursor, in the active layer
@@ -333,37 +348,34 @@ class editor_mode : public application_mode, private editor_track::observer
void render_include_gizmo(glm::mat4 const &View, glm::mat4 const &Projection, glm::dvec3 const &Camerapos);
// exports the scenery in legacy (text) format, with layers hidden in the editor left out of the picture
void export_scenery();
// raises/lowers terrain under the cursor while the left mouse button is held in sculpt mode
void handle_terrain_sculpt(double Deltatime);
// returns the terrain patch (if any) whose footprint covers the given world point
// works the brush of the terrain under the cursor while the left mouse button is held
void handle_terrain_brush(double Deltatime);
// the chunk of the terrain (if any) covering the given world point
editor_terrain *terrain_at(double X, double Z);
// gathers every active terrain patch: manually-created ones plus streamed chunks
// the chunks of the terrain held in memory
std::vector<editor_terrain *> active_terrains();
// samples the selected model instance's geometry into a new editable terrain patch, then removes it
// samples the selected model instance's geometry into the chunks of the terrain it covers, then removes it
void capture_terrain();
std::vector<std::unique_ptr<editor_terrain>> m_terrains;
// grid-aligned manual chunks, keyed by (cx,cz) on the global chunk grid
std::map<std::pair<int, int>, std::unique_ptr<editor_terrain>> m_grid_chunks;
bool m_terrain_sculpt{false}; // when true, LMB sculpts terrain instead of picking
bool m_terrain_brush_smooth{false}; // the brush evens the ground out instead of raising or lowering it
bool m_terrain_open{false}; // the terrain is the field of work, chosen in the toolbar
bool m_chunk_edit{false}; // when true, LMB adds/removes whole chunks
int m_terrain_cells{32}; // grid resolution (quads per side)
int m_terrain_chunks{4}; // chunks per side for a chunked terrain
float m_terrain_cellsize{2.0f}; // metres per quad
float m_terrain_baseheight{0.0f}; // flat starting height
float m_terrain_spacing{2.0f}; // point spacing of the chunks made and of the spacing tool
float m_terrain_baseheight{0.0f}; // height of chunks made where there's no ground
int m_terrain_add_count{4}; // chunks along a side added around the camera
float m_terrain_brush_radius{12.0f};
float m_terrain_brush_strength{4.0f}; // metres per second while held (one-shot for the buttons)
float m_terrain_simplify_error{0.5f}; // flatness tolerance (m) for mesh simplification
bool m_terrain_auto_optimize{false}; // auto-simplify edited chunks after sculpting settles
double m_terrain_idle{0.0}; // seconds since the last sculpt edit (debounce timer)
char m_terrain_texture[128]{""}; // optional ground texture name
float m_terrain_brush_strength{4.0f}; // metres per second while held; for the paint, how fast it covers
int m_terrain_layer{0}; // entry of the palette painted
char m_terrain_material[128]{"grass"}; // material added to the palette
float m_terrain_material_size{0.0f}; // metres its textures repeat at, 0: given by the material
std::vector<glm::dvec3> m_water_points; // outline of a body of water being drawn
float m_water_level{0.0f};
bool m_water_level_edited{false}; // the level was given by hand, the points clicked don't change it
char m_water_material[128]{""};
int m_water_selected{-1};
float m_water_selected_level{0.0f}; // level of the chosen water, while it's edited
bool m_water_level_dragged{false};
std::string m_terrain_status;
// streaming terrain that follows the camera (open-world); the editor shares the single
// simulation-level instance so authored terrain also renders in the driver / other modes
// the heightmap terrain, shared with the scenery loader so it shows in every mode
terrain_streamer &m_streamer{EditorTerrain};
int m_stream_radius{2};
bool m_stream_persist{true}; // save edited chunks to disk and load them back
// geoportal orthophoto layer drawn under the other viewport overlays
void render_orthophoto_ui();
@@ -1573,7 +1585,7 @@ class editor_mode : public application_mode, private editor_track::observer
double origin{0.0}; // chainage of the start of the route
double departure{0.0}; // largest difference between the grade line and the track
profile::ground_fit fit;
// the editor terrain led to the grade line: formation under the track, slopes down or up to the ground
// the heightmap terrain led to the grade line by a modifier: formation under the track, slopes down or up to the ground
struct earthworks
{
bool from_ballast{true};
@@ -1582,11 +1594,11 @@ class editor_mode : public application_mode, private editor_track::observer
double slope{1.5}; // horizontal run of the slopes per metre of height
double reach{60.0}; // m, from the edge of the formation, where the slopes end even if they don't get to the ground
double rounding{1.5}; // m, over which the slopes bend into the formation and into the ground
// heights of the terrain before the last shaping
std::vector<std::pair<editor_terrain *, std::vector<float>>> undo;
bool generate{true};
// chunks of terrain made under the route by the last shaping
std::vector<std::pair<int, int>> generated;
bool generated_streaming{false};
// the modifier of the terrain made by the last shaping
std::string modifier;
} earthworks;
double view_from{0.0};
double view_to{100.0};

View File

@@ -488,44 +488,53 @@ std::pair<int, int> editor_mode::profile_generate_ground(std::vector<std::pair<d
auto &state{m_profile};
auto &works{state.earthworks};
auto const &samples{state.samples};
bool const streaming{m_streamer.active()};
double const size{streaming ? m_streamer.cells() * m_streamer.cellsize() : chunk_grid_size()};
int const cells{streaming ? m_streamer.cells() : std::clamp(m_terrain_cells, 1, 256)};
if (size <= 0.0 || cells <= 0 || samples.empty())
if (samples.empty() || false == ensure_terrain())
return {0, 0};
double const cellsize{size / cells};
std::set<std::pair<int, int>> keys;
auto const size{heightmap::chunk_size};
std::set<heightmap::chunk_key> keys;
for (auto const &sample : samples)
{
auto const &p{sample.position};
for (auto x = static_cast<int>(std::floor((p.x - Reach) / size)); x <= static_cast<int>(std::floor((p.x + Reach) / size)); ++x)
for (auto z = static_cast<int>(std::floor((p.z - Reach) / size)); z <= static_cast<int>(std::floor((p.z + Reach) / size)); ++z)
auto const low{heightmap::chunk_at(p.x - Reach, p.z - Reach)};
auto const high{heightmap::chunk_at(p.x + Reach, p.z + Reach)};
for (auto x = low.first; x <= high.first; ++x)
for (auto z = low.second; z <= high.second; ++z)
keys.insert({x, z});
}
auto const terrains{active_terrains()};
std::string const texture{m_terrain_texture};
auto const cells{heightmap::cells_for(m_terrain_spacing)};
auto const cellsize{size / cells};
auto const side{static_cast<std::size_t>(cells) + 1};
int made{0}, skipped{0};
std::size_t const side{static_cast<std::size_t>(cells) + 1};
for (auto const &key : keys)
{
glm::dvec2 const low{key.first * size, key.second * size};
glm::dvec2 const middle{low + glm::dvec2{size * 0.5}};
if (streaming ? m_streamer.terrain_at(middle.x, middle.y) != nullptr : m_grid_chunks.count(key) > 0)
if (m_streamer.exists(key))
continue;
auto const low{heightmap::chunk_corner(key)};
glm::dvec2 const middle{low + glm::dvec2{size * 0.5}};
std::vector<std::size_t> around;
for (std::size_t i = 0; i < samples.size(); ++i)
if (std::abs(samples[i].position.x - middle.x) <= size * 0.5 + Reach && std::abs(samples[i].position.z - middle.y) <= size * 0.5 + Reach)
around.push_back(i);
if (around.empty())
continue;
std::vector<glm::dvec3> points;
std::vector<std::size_t> nearest;
points.reserve(side * side);
nearest.reserve(side * side);
// where the scenery has its own ground, the chunk isn't made: the two would cover each other
std::vector<glm::dvec3> probes;
for (int iz = 1; iz < 10; ++iz)
for (int ix = 1; ix < 10; ++ix)
probes.emplace_back(low.x + ix * size / 10.0, profile::elevation(state.line, samples[around.front()].chainage), low.y + iz * size / 10.0);
std::vector<char> found;
ground_heights(probes, true, &found);
if (std::count(found.begin(), found.end(), 1) * 20 > static_cast<long>(probes.size()))
{
++skipped;
continue;
}
// the ground of the chunk: the ground under the nearest point of the route, or the formation there
std::vector<float> heights(side * side);
for (std::size_t iz = 0; iz < side; ++iz)
for (std::size_t ix = 0; ix < side; ++ix)
{
glm::dvec3 point{low.x + ix * cellsize, 0.0, low.y + iz * cellsize};
glm::dvec3 const point{low.x + ix * cellsize, 0.0, low.y + iz * cellsize};
auto best{around.front()};
auto distance{std::numeric_limits<double>::max()};
for (auto const i : around)
@@ -537,56 +546,16 @@ std::pair<int, int> editor_mode::profile_generate_ground(std::vector<std::pair<d
best = i;
}
}
point.y = profile::elevation(state.line, samples[best].chainage);
points.push_back(point);
nearest.push_back(best);
heights[iz * side + ix] = static_cast<float>(best < state.terrain.size() && std::isfinite(state.terrain[best]) ? state.terrain[best] : profile::elevation(state.line, samples[best].chainage) - Beds[best].first);
}
std::vector<char> found;
auto heights{ground_heights(points, true, &found)};
std::size_t own{0};
for (std::size_t i = 0; i < points.size(); ++i)
if (found[i] != 0 && std::none_of(terrains.begin(), terrains.end(), [&](editor_terrain const *Terrain) { return Terrain->contains(points[i].x, points[i].z); }))
++own;
if (own * 20 > points.size())
{
++skipped;
continue;
}
for (std::size_t i = 0; i < points.size(); ++i)
{
if (found[i] != 0)
continue;
auto const sample{nearest[i]};
heights[i] = sample < state.terrain.size() && std::isfinite(state.terrain[sample]) ? state.terrain[sample] : profile::elevation(state.line, samples[sample].chainage) - Beds[sample].first;
}
auto const sampler = [&](double const X, double const Z, double &Height) {
auto const added{m_streamer.add_chunk(key.first, key.second, m_terrain_spacing, [&](double const X, double const Z) {
auto const ix{static_cast<std::size_t>(std::clamp<long>(std::lround((X - low.x) / cellsize), 0, cells))};
auto const iz{static_cast<std::size_t>(std::clamp<long>(std::lround((Z - low.y) / cellsize), 0, cells))};
Height = heights[iz * side + ix];
return true;
};
if (streaming)
{
m_streamer.add_chunk(key.first, key.second);
auto *terrain{m_streamer.terrain_at(middle.x, middle.y)};
if (terrain == nullptr)
continue;
terrain->reshape([&](double const X, double const Z, float &Height) {
double height;
sampler(X, Z, height);
Height = static_cast<float>(height);
return true;
});
}
else
{
auto chunk{std::make_unique<editor_terrain>()};
if (false == chunk->create(glm::dvec3{middle.x, m_terrain_baseheight, middle.y}, cells, static_cast<float>(cellsize), texture, sampler))
continue;
m_grid_chunks[key] = std::move(chunk);
}
return heights[iz * side + ix];
})};
if (false == added)
continue;
works.generated.push_back(key);
works.generated_streaming = streaming;
++made;
}
return {made, skipped};
@@ -599,6 +568,11 @@ void editor_mode::profile_shape_ground()
auto const &samples{state.samples};
if (samples.size() < 2 || state.line.points.size() < 2)
return;
if (false == ensure_terrain())
{
state.error = STR_C("The heightmap terrain can't be opened");
return;
}
auto const slope{std::max(0.1, works.slope)};
std::vector<std::pair<double, double>> beds(samples.size(), {works.depth, works.half_width});
if (works.from_ballast)
@@ -610,137 +584,91 @@ void editor_mode::profile_shape_ground()
for (auto const &bed : beds)
widest = std::max(widest, bed.second);
auto const reach{widest + works.reach};
works.undo.clear();
works.generated.clear();
auto const generated{works.generate ? profile_generate_ground(beds, reach) : std::pair<int, int>{0, 0}};
auto const terrains{active_terrains()};
if (terrains.empty())
{
state.error = generated.second > 0 ? STR_C("The scenery has its own ground under the route: convert it to editor terrain first") : STR_C("There's no editor terrain to shape: convert the scenery ground first");
return;
}
// the pieces of the axis of the route, found by the cell of a grid over the plan
double constexpr cell{16.0};
auto const key = [&](double const X, double const Z) { return std::make_pair(static_cast<int>(std::floor(X / cell)), static_cast<int>(std::floor(Z / cell))); };
std::map<std::pair<int, int>, std::vector<std::size_t>> grid;
glm::dvec2 low{std::numeric_limits<double>::max()}, high{-std::numeric_limits<double>::max()};
for (std::size_t i = 0; i + 1 < samples.size(); ++i)
// the shaping is a modifier of the terrain: the formation along the axis of the route, followed by the ground
// around it. shaped again, the route replaces its modifier, and the terrain follows the new grade line
heightmap::modifier modifier;
auto const name = [](TTrack const *Track) { return Track != nullptr && false == Track->name().empty() ? Track->name() : std::string{"?"}; };
modifier.name = "route " + name(state.from) + " - " + name(state.to);
modifier.slope = slope;
modifier.reach = works.reach;
modifier.rounding = std::max(0.0, works.rounding);
std::vector<heightmap::corridor_point> points;
points.reserve(samples.size());
for (std::size_t i = 0; i < samples.size(); ++i)
points.push_back({{samples[i].position.x, samples[i].position.z}, profile::elevation(state.line, samples[i].chainage) - beds[i].first, beds[i].second});
// the points the line between their neighbours passes within a few millimetres of are left out
double constexpr tolerance{0.005};
modifier.points.push_back(points.front());
std::size_t anchor{0};
for (std::size_t next = 2; next < points.size(); ++next)
{
auto const &a{samples[i].position};
auto const &b{samples[i + 1].position};
auto const from{key(std::min(a.x, b.x), std::min(a.z, b.z))};
auto const to{key(std::max(a.x, b.x), std::max(a.z, b.z))};
for (auto x = from.first; x <= to.first; ++x)
for (auto z = from.second; z <= to.second; ++z)
grid[{x, z}].push_back(i);
low = glm::min(low, glm::dvec2{std::min(a.x, b.x), std::min(a.z, b.z)});
high = glm::max(high, glm::dvec2{std::max(a.x, b.x), std::max(a.z, b.z)});
auto const &a{points[anchor]};
auto const &b{points[next]};
auto const run{b.position - a.position};
auto const length{glm::length(run)};
bool straight{true};
for (auto i = anchor + 1; i < next && straight; ++i)
{
auto const t{length > 1e-9 ? glm::dot(points[i].position - a.position, run) / (length * length) : 0.0};
auto const offset{glm::length(points[i].position - (a.position + run * t))};
auto const formation{a.formation + (b.formation - a.formation) * t};
auto const width{a.half_width + (b.half_width - a.half_width) * t};
straight = offset <= tolerance && std::abs(points[i].formation - formation) <= tolerance && std::abs(points[i].half_width - width) <= tolerance;
}
if (false == straight)
{
anchor = next - 1;
modifier.points.push_back(points[anchor]);
}
}
modifier.points.push_back(points.back());
std::size_t moved{0};
double fill{0.0}, cut{0.0};
for (auto *terrain : terrains)
for (std::size_t i = 0; i < samples.size(); ++i)
{
auto const before{terrain->heights()};
auto const shaped{terrain->reshape([&](double const X, double const Z, float &Height) {
if (X < low.x - reach || X > high.x + reach || Z < low.y - reach || Z > high.y + reach)
return false;
double distance{reach};
double chainage{-1.0};
std::size_t nearest{0};
auto const from{key(X - reach, Z - reach)};
auto const to{key(X + reach, Z + reach)};
for (auto x = from.first; x <= to.first; ++x)
for (auto z = from.second; z <= to.second; ++z)
{
auto const found{grid.find({x, z})};
if (found == grid.end())
continue;
for (auto const i : found->second)
{
glm::dvec2 const a{samples[i].position.x, samples[i].position.z};
glm::dvec2 const b{samples[i + 1].position.x, samples[i + 1].position.z};
auto const run{b - a};
auto const length{glm::dot(run, run)};
auto const t{length > 1e-12 ? std::clamp(glm::dot(glm::dvec2{X, Z} - a, run) / length, 0.0, 1.0) : 0.0};
auto const offset{glm::length(glm::dvec2{X, Z} - (a + run * t))};
if (offset < distance)
{
distance = offset;
chainage = samples[i].chainage + (samples[i + 1].chainage - samples[i].chainage) * t;
nearest = t < 0.5 ? i : i + 1;
}
}
}
if (chainage < 0.0)
return false;
auto const &bed{beds[nearest]};
auto const formation{profile::elevation(state.line, chainage) - bed.first};
auto const ground{static_cast<double>(Height)};
// the bends at the edge of the formation and at the foot of the slope are rounded off over the given length
auto const rounding{std::max(0.0, works.rounding)};
auto const soft_max = [](double const A, double const B, double const K) {
if (K <= 1e-6)
return std::max(A, B);
auto const h{std::max(K - std::abs(A - B), 0.0) / K};
return std::max(A, B) + h * h * K * 0.25;
};
auto const beyond{soft_max(0.0, distance - bed.second, rounding)};
auto const bend{rounding / slope};
auto const target{ground > formation ? -soft_max(-ground, -(formation + beyond / slope), bend) : soft_max(ground, formation - beyond / slope, bend)};
if (std::abs(target - ground) < 0.005)
return false;
fill = std::max(fill, target - ground);
cut = std::max(cut, ground - target);
Height = static_cast<float>(target);
++moved;
return true;
})};
if (shaped)
works.undo.emplace_back(terrain, before);
}
auto const chunks = [&](std::string Text) {
if (generated.first > 0)
Text += format(STR_C("; %d chunk(s) of terrain made under the route"), generated.first);
if (generated.second > 0)
Text += format(STR_C("; %d chunk(s) left out: the scenery has its own ground there, convert it to shape it"), generated.second);
return Text;
};
if (moved == 0)
{
state.status = chunks(STR_C("The editor terrain doesn't reach the route, or it's shaped already"));
return;
if (i >= state.terrain.size() || false == std::isfinite(state.terrain[i]))
continue;
fill = std::max(fill, points[i].formation - state.terrain[i]);
cut = std::max(cut, state.terrain[i] - points[i].formation);
}
if (false == works.modifier.empty() && works.modifier != modifier.name)
m_streamer.remove_modifier(works.modifier);
works.modifier = modifier.name;
auto const count{modifier.points.size()};
m_streamer.modifier(std::move(modifier));
forget_ground();
auto status{format(STR_C("Terrain shaped by the modifier \"%s\" (%zu points): embankment up to %.2f m, cutting up to %.2f m"), works.modifier.c_str(), count, fill, cut)};
if (generated.first > 0)
status += format(STR_C("; %d chunk(s) of terrain made under the route"), generated.first);
if (generated.second > 0)
status += format(STR_C("; %d chunk(s) left out: the scenery has its own ground there, convert it to shape it"), generated.second);
state.error.clear();
profile_resample();
state.status = chunks(format(STR_C("Terrain shaped at %zu points: embankment up to %.2f m, cutting up to %.2f m"), moved, fill, cut));
state.status = status;
WriteLog("Editor: vertical profile - " + state.status, logtype::generic);
}
void editor_mode::profile_restore_ground()
{
auto &state{m_profile};
auto const terrains{active_terrains()};
auto &works{state.earthworks};
std::size_t restored{0};
for (auto const &entry : state.earthworks.undo)
if (std::find(terrains.begin(), terrains.end(), entry.first) != terrains.end())
{
entry.first->restore(entry.second);
++restored;
}
state.earthworks.undo.clear();
for (auto const &key : state.earthworks.generated)
if (false == works.modifier.empty() && m_streamer.remove_modifier(works.modifier))
++restored;
works.modifier.clear();
for (auto const &key : works.generated)
{
if (state.earthworks.generated_streaming)
m_streamer.remove_chunk(key.first, key.second);
else
remove_grid_chunk(key.first, key.second);
m_streamer.remove_chunk(key.first, key.second);
++restored;
}
state.earthworks.generated.clear();
works.generated.clear();
forget_ground();
profile_resample();
state.status = restored > 0 ? STR_C("The terrain is back as it was before the shaping") : STR_C("The shaped terrain isn't loaded any more");
state.status = restored > 0 ? STR_C("The terrain is back as it was before the shaping") : STR_C("The shaping of the terrain is gone already");
}
void editor_mode::render_ground_conversion()
@@ -748,7 +676,7 @@ void editor_mode::render_ground_conversion()
auto &conversion{m_ground_conversion};
if (false == conversion.open)
{
if (ImGui::Button(STR_C("Convert the scenery ground to editor terrain...")))
if (ImGui::Button(STR_C("Convert the scenery ground to heightmap terrain...")))
{
conversion.open = true;
conversion.materials = ground_materials();
@@ -759,7 +687,7 @@ void editor_mode::render_ground_conversion()
conversion.chosen.insert(most->first);
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Replaces the triangles of the chosen materials with editor terrain of the same shape,\nwhich can be shaped and sculpted; the scenery files lose these triangles on save"));
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"));
if (false == conversion.status.empty())
ImGui::TextWrapped("%s", conversion.status.c_str());
return;
@@ -782,7 +710,7 @@ void editor_mode::render_ground_conversion()
}
ImGui::PopID();
}
ImGui::TextWrapped("%s", STR_C("The terrain gets a single texture: that of the material which covers the most. Until saved, loading the scenery again takes it all back."));
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."));
if (conversion.chosen.empty())
ImGui::TextDisabled("%s", STR_C("Tick at least one material"));
else if (ImGui::Button(STR_C("Convert")))
@@ -802,13 +730,13 @@ void editor_mode::render_profile_earthworks()
auto &works{state.earthworks};
if (false == ImGui::TreeNode(STR_C("Shape the terrain to the grade line")))
return;
ImGui::TextWrapped("%s", STR_C("Only the editor terrain changes: a formation under the track and slopes to the ground; the triangles and models of the scenery stay as they are."));
ImGui::TextWrapped("%s", STR_C("Only the heightmap terrain changes: a formation under the track and slopes to the ground, kept as a modifier of the terrain which follows the grade line when it's shaped again; the triangles and models of the scenery stay as they are."));
render_ground_conversion();
auto const drag = [](char const *Label, double &Value, float const Speed, char const *Format) { return ImGui::DragScalar(STR_C(Label), ImGuiDataType_Double, &Value, Speed, nullptr, nullptr, Format); };
ImGui::PushItemWidth(90.0f);
ImGui::Checkbox(STR_C("Make terrain where the route has none"), &works.generate);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Chunks of editor terrain are made along the route where there's none, from the ground around,\nthen shaped. Where the scenery has its own ground triangles the chunk is left out: convert them instead"));
ImGui::SetTooltip("%s", STR_C("Chunks of heightmap terrain are made along the route where there's none, from the ground around,\nthen shaped. Where the scenery has its own ground triangles the chunk is left out: convert them instead"));
ImGui::Checkbox(STR_C("Formation under the ballast of the track"), &works.from_ballast);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("The terrain goes as deep under the track as its ballast reaches and as wide as its foot,\nread from each path of the route; off: the values given below"));
@@ -842,15 +770,15 @@ void editor_mode::render_profile_earthworks()
ImGui::TextDisabled("%s", STR_C("Correct the errors of the grade line first"));
else if (ImGui::Button(STR_C("Shape the terrain")))
profile_shape_ground();
if (false == works.undo.empty() || false == works.generated.empty())
if (false == works.modifier.empty() || false == works.generated.empty())
{
ImGui::SameLine();
if (ImGui::Button(STR_C("Restore the terrain")))
profile_restore_ground();
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Puts back the terrain as it was before the last shaping, and takes away the terrain it made"));
ImGui::SetTooltip("%s", STR_C("Removes the modifier of the route, and takes away the terrain the shaping made"));
}
if (false == active_terrains().empty())
if (m_streamer.active())
{
// the terrain tools take the tool options window over, the profile stays open under them
if (ImGui::Button(STR_C("Sculpting...")))

View File

@@ -204,17 +204,15 @@ void editor_mode::work_area_tools(std::vector<tool_button> &Buttons)
case work_area::terrain:
{
// what the left button does on the terrain
auto const choose = [this](bool const Sculpt, bool const Smooth, bool const Chunks) {
return [this, Sculpt, Smooth, Chunks]() {
m_terrain_sculpt = Sculpt;
m_terrain_brush_smooth = Smooth;
m_chunk_edit = Chunks;
};
};
Buttons.push_back({icon::select, STR_C("Select"), nullptr, STR_C("LMB picks the models, as in the surroundings"), false == m_terrain_sculpt && false == m_chunk_edit, choose(false, m_terrain_brush_smooth, false)});
Buttons.push_back({icon::sculpt, STR_C("Sculpt"), nullptr, STR_C("LMB raises the terrain under the brush, Shift+LMB lowers it"), m_terrain_sculpt && false == m_terrain_brush_smooth, choose(true, false, false)});
Buttons.push_back({icon::smooth, STR_C("Smooth"), nullptr, STR_C("LMB evens the terrain out under the brush"), m_terrain_sculpt && m_terrain_brush_smooth, choose(true, true, false)});
Buttons.push_back({icon::chunks, STR_C("Chunks"), nullptr, STR_C("LMB adds a chunk next to the clicked one, Shift+LMB deletes it"), m_chunk_edit, choose(false, m_terrain_brush_smooth, true)});
auto const choose = [this](terrain_tool const Tool) { return [this, Tool]() { m_terrain_tool = Tool; }; };
auto const chosen = [this](terrain_tool const Tool) { return m_terrain_tool == Tool; };
Buttons.push_back({icon::select, STR_C("Select"), nullptr, STR_C("LMB picks the models, as in the surroundings"), chosen(terrain_tool::none), choose(terrain_tool::none)});
Buttons.push_back({icon::sculpt, STR_C("Sculpt"), nullptr, STR_C("LMB raises the terrain under the brush, Shift+LMB lowers it"), chosen(terrain_tool::sculpt), choose(terrain_tool::sculpt)});
Buttons.push_back({icon::smooth, STR_C("Smooth"), nullptr, STR_C("LMB evens the terrain out under the brush"), chosen(terrain_tool::smooth), choose(terrain_tool::smooth)});
Buttons.push_back({icon::brush, STR_C("Paint"), nullptr, STR_C("LMB paints the material chosen in the palette, Shift+LMB the first material of the palette"), chosen(terrain_tool::paint), choose(terrain_tool::paint)});
Buttons.push_back({icon::chunks, STR_C("Chunks"), nullptr, STR_C("LMB adds a chunk next to the clicked one, Shift+LMB removes it"), chosen(terrain_tool::chunks), choose(terrain_tool::chunks)});
Buttons.push_back({icon::count, STR_C("Point spacing"), nullptr, STR_C("LMB gives the clicked chunk the point spacing chosen in the tool options"), chosen(terrain_tool::spacing), choose(terrain_tool::spacing)});
Buttons.push_back({icon::area_fill, STR_C("Water"), nullptr, STR_C("LMB adds a point of the outline of a body of water, Shift+LMB takes the last one back"), chosen(terrain_tool::water), choose(terrain_tool::water)});
auto const orthophoto{m_orthophoto.enabled()};
Buttons.push_back({icon::orthophoto, STR_C("Orthophoto"), nullptr, STR_C("Aerial imagery of geoportal.gov.pl under the scenery, laid out by the origin of the scenery"), orthophoto,
[this, orthophoto]() { m_orthophoto.enabled(false == orthophoto); }, true});

View File

@@ -10,446 +10,33 @@ http://mozilla.org/MPL/2.0/.
#include "stdafx.h"
#include "editor/editorTerrain.hpp"
#include "scene/scene.h"
#include "scene/scenenode.h"
#include "simulation/simulation.h"
#include "rendering/renderer.h"
#include "model/vertex.h"
#include <glad/glad.h>
#include <algorithm>
#include <cmath>
namespace
heightmap::chunk_data &editor_terrain::edit(bool const Shape, bool const Paint)
{
constexpr double kPi = 3.14159265358979323846;
}
bool editor_terrain::create(glm::dvec3 const &Center, int Cells, float CellSize, std::string const &TextureName,
height_sampler const &Sampler)
{
if (Cells < 1 || CellSize <= 0.0f || simulation::Region == nullptr)
return false;
m_cells = Cells;
m_cellsize = CellSize;
double const half = 0.5 * static_cast<double>(Cells) * CellSize;
m_x0 = Center.x - half;
m_z0 = Center.z - half;
m_heights.assign(static_cast<std::size_t>(Cells + 1) * (Cells + 1), static_cast<float>(Center.y));
// optionally seed the grid by sampling whatever geometry is already there (terrain capture)
if (Sampler)
{
for (int iz = 0; iz <= Cells; ++iz)
for (int ix = 0; ix <= Cells; ++ix)
{
double const vx = m_x0 + static_cast<double>(ix) * CellSize;
double const vz = m_z0 + static_cast<double>(iz) * CellSize;
double y;
if (Sampler(vx, vz, y))
m_heights[index(ix, iz)] = static_cast<float>(y);
}
}
m_material = TextureName.empty() ? null_handle : GfxRenderer->Fetch_Material(TextureName);
// section-level shapes are rendered relative to the section centre, so that is our geometry origin
scene::basic_section &sec = simulation::Region->section(Center);
sec.create_geometry(); // ensure existing section geometry is already built (idempotent)
m_origin = sec.m_area.center;
m_section = &sec;
std::vector<world_vertex> verts;
build_vertices(verts, false);
m_vertexcount = verts.size();
scene::shape_node shape;
shape.make_terrain(m_material, std::move(verts), m_origin);
// upload to a dedicated bank; the renderer resolves draw calls by handle regardless of bank
m_bank = GfxRenderer->Create_Bank();
shape.create_geometry(m_bank); // sets the shape's geometry handle, clears its CPU vertices
m_geometry = shape.data().geometry;
glm::dvec3 const shapecenter = shape.data().area.center;
float const shaperadius = shape.radius(); // cached inside make_terrain, vertices already gone
sec.m_shapes.emplace_back(std::move(shape));
// extend the section bounds so the new terrain isn't frustum-culled at its edges
sec.m_area.radius = std::max(
sec.m_area.radius,
static_cast<float>(glm::length(sec.m_area.center - shapecenter) + shaperadius));
return true;
}
glm::dvec3 editor_terrain::vertex_position(int Ix, int Iz) const
{
return glm::dvec3(
m_x0 + static_cast<double>(Ix) * m_cellsize,
static_cast<double>(m_heights[index(Ix, Iz)]),
m_z0 + static_cast<double>(Iz) * m_cellsize);
}
glm::vec3 editor_terrain::vertex_normal(int Ix, int Iz) const
{
// central differences on the heightfield; clamp to edges
int const xl = std::max(0, Ix - 1), xr = std::min(m_cells, Ix + 1);
int const zl = std::max(0, Iz - 1), zr = std::min(m_cells, Iz + 1);
float const hl = m_heights[index(xl, Iz)], hr = m_heights[index(xr, Iz)];
float const hd = m_heights[index(Ix, zl)], hu = m_heights[index(Ix, zr)];
float const dx = static_cast<float>((xr - xl)) * m_cellsize;
float const dz = static_cast<float>((zr - zl)) * m_cellsize;
glm::vec3 n(-(hr - hl) / (dx > 0.f ? dx : 1.f), 1.0f, -(hu - hd) / (dz > 0.f ? dz : 1.f));
return glm::normalize(n);
}
world_vertex editor_terrain::make_vertex(int Ix, int Iz) const
{
world_vertex v;
v.position = vertex_position(Ix, Iz);
v.normal = vertex_normal(Ix, Iz);
v.texture = glm::vec2(static_cast<float>(Ix), static_cast<float>(Iz));
return v;
}
// emits one quad (two upward-facing triangles) spanning grid corners (X0,Z0)..(X1,Z1)
void editor_terrain::emit_quad(int X0, int Z0, int X1, int Z1, std::vector<world_vertex> &Out) const
{
world_vertex const v00 = make_vertex(X0, Z0);
world_vertex const v10 = make_vertex(X1, Z0);
world_vertex const v01 = make_vertex(X0, Z1);
world_vertex const v11 = make_vertex(X1, Z1);
Out.push_back(v00);
Out.push_back(v01);
Out.push_back(v10);
Out.push_back(v11);
Out.push_back(v10);
Out.push_back(v01);
}
// true if every grid vertex inside the block stays within Error of the bilinear plane of its corners
bool editor_terrain::block_flat(int X0, int Z0, int X1, int Z1, float Error) const
{
float const h00 = m_heights[index(X0, Z0)];
float const h10 = m_heights[index(X1, Z0)];
float const h01 = m_heights[index(X0, Z1)];
float const h11 = m_heights[index(X1, Z1)];
double const wx = X1 - X0, wz = Z1 - Z0;
for (int iz = Z0; iz <= Z1; ++iz)
for (int ix = X0; ix <= X1; ++ix)
{
double const tx = (wx > 0.0) ? (ix - X0) / wx : 0.0;
double const tz = (wz > 0.0) ? (iz - Z0) / wz : 0.0;
double const top = h00 + tx * (h10 - h00);
double const bot = h01 + tx * (h11 - h01);
double const interp = top + tz * (bot - top);
if (std::abs(static_cast<double>(m_heights[index(ix, iz)]) - interp) > Error)
return false;
}
return true;
}
// adaptive quadtree: flat blocks stay whole, the others are split into four
void editor_terrain::collect_blocks(int X0, int Z0, int X1, int Z1, float Error, std::vector<std::array<int, 4>> &Out) const
{
bool const splitx = (X1 - X0) > 1;
bool const splitz = (Z1 - Z0) > 1;
if ((!splitx && !splitz) || block_flat(X0, Z0, X1, Z1, Error))
{
Out.push_back({X0, Z0, X1, Z1});
return;
}
int const xm = splitx ? (X0 + X1) / 2 : X1;
int const zm = splitz ? (Z0 + Z1) / 2 : Z1;
collect_blocks(X0, Z0, xm, zm, Error, Out);
if (splitx)
collect_blocks(xm, Z0, X1, zm, Error, Out);
if (splitz)
collect_blocks(X0, zm, xm, Z1, Error, Out);
if (splitx && splitz)
collect_blocks(xm, zm, X1, Z1, Error, Out);
}
void editor_terrain::emit_block(std::array<int, 4> const &Block, std::vector<char> const &Marked, std::vector<world_vertex> &Out) const
{
int const x0 = Block[0], z0 = Block[1], x1 = Block[2], z1 = Block[3];
// the edges walked the way the quads wind
std::vector<std::pair<int, int>> ring;
for (int z = z0; z < z1; ++z)
if (z == z0 || Marked[index(x0, z)] != 0)
ring.push_back({x0, z});
for (int x = x0; x < x1; ++x)
if (x == x0 || Marked[index(x, z1)] != 0)
ring.push_back({x, z1});
for (int z = z1; z > z0; --z)
if (z == z1 || Marked[index(x1, z)] != 0)
ring.push_back({x1, z});
for (int x = x1; x > x0; --x)
if (x == x1 || Marked[index(x, z0)] != 0)
ring.push_back({x, z0});
if (ring.size() <= 4)
{
emit_quad(x0, z0, x1, z1, Out);
return;
}
world_vertex centre;
if ((x1 - x0) % 2 == 0 && (z1 - z0) % 2 == 0)
centre = make_vertex((x0 + x1) / 2, (z0 + z1) / 2);
else
{
auto const a = make_vertex(x0, z0), b = make_vertex(x1, z0), c = make_vertex(x0, z1), d = make_vertex(x1, z1);
centre = a;
centre.position = (a.position + b.position + c.position + d.position) * 0.25;
centre.normal = glm::normalize(a.normal + b.normal + c.normal + d.normal);
centre.texture = (a.texture + b.texture + c.texture + d.texture) * 0.25f;
}
for (std::size_t i = 0; i < ring.size(); ++i)
{
auto const &from = ring[i];
auto const &to = ring[(i + 1) % ring.size()];
Out.push_back(centre);
Out.push_back(make_vertex(from.first, from.second));
Out.push_back(make_vertex(to.first, to.second));
}
}
void editor_terrain::build_vertices(std::vector<world_vertex> &Out, bool Simplify) const
{
Out.clear();
Out.reserve(static_cast<std::size_t>(m_cells) * m_cells * 6);
if (Simplify)
{
std::vector<std::array<int, 4>> blocks;
collect_blocks(0, 0, m_cells, m_cells, m_simplify_error, blocks);
// the corners of the blocks, and the whole edge of the patch which the neighbouring patch meets in its own way
std::vector<char> marked(static_cast<std::size_t>(m_cells + 1) * (m_cells + 1), 0);
for (int i = 0; i <= m_cells; ++i)
marked[index(i, 0)] = marked[index(i, m_cells)] = marked[index(0, i)] = marked[index(m_cells, i)] = 1;
for (auto const &block : blocks)
marked[index(block[0], block[1])] = marked[index(block[2], block[1])] = marked[index(block[0], block[3])] = marked[index(block[2], block[3])] = 1;
for (auto const &block : blocks)
emit_block(block, marked, Out);
return;
}
for (int iz = 0; iz < m_cells; ++iz)
for (int ix = 0; ix < m_cells; ++ix)
emit_quad(ix, iz, ix + 1, iz + 1, Out);
}
void editor_terrain::regenerate(bool Simplify)
{
if (!valid())
return;
std::vector<world_vertex> verts;
build_vertices(verts, Simplify);
gfx::vertex_array gpuverts;
gpuverts.reserve(verts.size());
for (auto const &v : verts)
gpuverts.emplace_back(gfx::basic_vertex::convert(v, m_origin));
gfx::userdata_array nouserdata;
// fast path: same vertex count -> in-place swap into the existing chunk
if (gpuverts.size() == m_vertexcount && (m_geometry.bank != 0 || m_geometry.chunk != 0))
{
GfxRenderer->Replace(gpuverts, nouserdata, m_geometry, GL_TRIANGLES);
return;
}
// count changed (optimize / un-optimize): upload a fresh chunk and point the shape at it
gfx::geometry_handle const newhandle = GfxRenderer->Insert(gpuverts, nouserdata, m_bank, GL_TRIANGLES);
if (m_section != nullptr)
{
for (auto &shape : m_section->m_shapes)
{
auto const h = shape.data().geometry;
if (h.bank == m_geometry.bank && h.chunk == m_geometry.chunk)
{
shape.geometry(newhandle);
break;
}
}
}
m_geometry = newhandle;
m_vertexcount = gpuverts.size();
}
void editor_terrain::optimize(float ErrorMetres)
{
m_simplify = true;
m_simplify_error = (ErrorMetres > 0.0f ? ErrorMetres : 0.01f);
m_dirty = false;
regenerate(true);
}
void editor_terrain::unoptimize()
{
m_simplify = false;
regenerate(false);
}
void editor_terrain::destroy()
{
if (m_section != nullptr)
{
// erase the shape whose geometry handle is ours; other shapes keep their handles (so they
// keep rendering), and the geometry GC reclaims our now-undrawn chunk's GPU memory
for (auto it = m_section->m_shapes.begin(); it != m_section->m_shapes.end(); ++it)
{
auto const h = it->data().geometry;
if (h.bank == m_geometry.bank && h.chunk == m_geometry.chunk)
{
m_section->m_shapes.erase(it);
break;
}
}
}
m_section = nullptr;
m_geometry = gfx::geometry_handle{0, 0};
m_cells = 0; // mark invalid
m_heights.clear();
}
bool editor_terrain::contains(double X, double Z) const
{
double const x1 = m_x0 + static_cast<double>(m_cells) * m_cellsize;
double const z1 = m_z0 + static_cast<double>(m_cells) * m_cellsize;
return (X >= m_x0 && X <= x1 && Z >= m_z0 && Z <= z1);
}
double editor_terrain::height_at(double X, double Z) const
{
return height_in(m_heights, X, Z);
}
double editor_terrain::height_in(std::vector<float> const &Heights, double X, double Z) const
{
auto const &m_heights = Heights;
double const fx = (X - m_x0) / m_cellsize;
double const fz = (Z - m_z0) / m_cellsize;
int ix = static_cast<int>(std::floor(fx));
int iz = static_cast<int>(std::floor(fz));
ix = std::clamp(ix, 0, m_cells - 1);
iz = std::clamp(iz, 0, m_cells - 1);
double const tx = std::clamp(fx - ix, 0.0, 1.0);
double const tz = std::clamp(fz - iz, 0.0, 1.0);
double const h00 = m_heights[index(ix, iz)];
double const h10 = m_heights[index(ix + 1, iz)];
double const h01 = m_heights[index(ix, iz + 1)];
double const h11 = m_heights[index(ix + 1, iz + 1)];
// matches the triangulation in build_vertices
if (tx + tz <= 1.0)
return h00 + tx * (h10 - h00) + tz * (h01 - h00);
return h11 + (1.0 - tx) * (h01 - h11) + (1.0 - tz) * (h10 - h11);
}
bool editor_terrain::sculpt(double X, double Z, double Radius, double Strength)
{
if (!valid() || Radius <= 0.0)
return false;
bool changed = false;
for (int iz = 0; iz <= m_cells; ++iz)
for (int ix = 0; ix <= m_cells; ++ix)
{
double const vx = m_x0 + static_cast<double>(ix) * m_cellsize;
double const vz = m_z0 + static_cast<double>(iz) * m_cellsize;
double const d = std::sqrt((vx - X) * (vx - X) + (vz - Z) * (vz - Z));
if (d > Radius)
continue;
// smooth cosine falloff: full strength at the centre, zero at the rim
double const falloff = 0.5 * (std::cos(kPi * d / Radius) + 1.0);
m_heights[index(ix, iz)] += static_cast<float>(Strength * falloff);
changed = true;
}
if (changed)
{
// sculpting edits the full-resolution mesh (fixed vertex count => fast in-place update);
// mark dirty so it can be auto-simplified once the stroke finishes, and modified for saving
m_simplify = false;
m_dirty = true;
m_modified = true;
regenerate(false);
}
return changed;
}
bool editor_terrain::smooth(double X, double Z, double Radius, double Amount, std::function<double(double X, double Z, double Fallback)> const &Ground)
{
if (!valid() || Radius <= 0.0 || Amount <= 0.0)
return false;
double const reach = Radius + m_cellsize;
if (X + reach < m_x0 || X - reach > m_x0 + m_cells * m_cellsize || Z + reach < m_z0 || Z - reach > m_z0 + m_cells * m_cellsize)
return false;
double const step = m_cellsize;
return reshape([&](double const Vx, double const Vz, float &Height) {
double const d = std::sqrt((Vx - X) * (Vx - X) + (Vz - Z) * (Vz - Z));
if (d > Radius)
return false;
double const own = Height;
// the neighbours a cell away, read from the ground as it was before the stroke, whichever patch holds them
double const average = (Ground(Vx - step, Vz, own) + Ground(Vx + step, Vz, own) + Ground(Vx, Vz - step, own) + Ground(Vx, Vz + step, own) + own * 4.0) / 8.0;
double const falloff = 0.5 * (std::cos(kPi * d / Radius) + 1.0);
double const smoothed = own + (average - own) * std::min(1.0, Amount * falloff);
if (std::abs(smoothed - own) < 1e-5)
return false;
Height = static_cast<float>(smoothed);
return true;
});
}
bool editor_terrain::reshape(std::function<bool(double X, double Z, float &Height)> const &Shaper)
{
if (!valid())
return false;
bool changed = false;
for (int iz = 0; iz <= m_cells; ++iz)
for (int ix = 0; ix <= m_cells; ++ix)
changed |= Shaper(m_x0 + static_cast<double>(ix) * m_cellsize, m_z0 + static_cast<double>(iz) * m_cellsize, m_heights[index(ix, iz)]);
if (changed)
{
m_simplify = false;
m_dirty = true;
m_modified = true;
regenerate(false);
}
return changed;
}
void editor_terrain::restore(std::vector<float> const &Heights)
{
if (!valid() || Heights.size() != m_heights.size())
return;
m_heights = Heights;
m_simplify = false;
m_dirty = true;
// a worker may still be making a mesh of the current data, so a copy is changed instead
if (m_data.use_count() > 1)
m_data = std::make_shared<heightmap::chunk_data>(*m_data);
m_modified = true;
regenerate(false);
if (Shape)
++m_shapeversion;
if (Paint)
++m_paintversion;
// the pointer is to const for everyone else; this chunk is its only holder now
return const_cast<heightmap::chunk_data &>(*m_data);
}
void editor_terrain::replace(heightmap::chunk_ptr Data, bool const Shape, bool const Paint)
{
m_data = std::move(Data);
m_modified = true;
if (Shape)
++m_shapeversion;
if (Paint)
++m_paintversion;
}
glm::dvec3 editor_terrain::centre() const
{
double const c = 0.5 * static_cast<double>(m_cells) * m_cellsize;
double y = 0.0;
if (!m_heights.empty())
y = m_heights[index(m_cells / 2, m_cells / 2)];
return glm::dvec3(m_x0 + c, y, m_z0 + c);
auto const low{m_data->corner()};
auto const half{heightmap::chunk_size * 0.5};
return {low.x + half, m_data->height_at(low.x + half, low.y + half), low.y + half};
}

View File

@@ -9,119 +9,72 @@ http://mozilla.org/MPL/2.0/.
#pragma once
#include <array>
#include <cstdint>
#include <memory>
#include <vector>
#include <string>
#include <functional>
#include <glm/glm.hpp>
#include "scene/heightmapterrain.h"
#include "utilities/Classes.h" // material_handle
#include "interfaces/ITexture.h" // null_handle
#include "rendering/geometrybank.h" // gfx::geometry_handle / geometrybank_handle
#include "rendering/geometrybank.h" // gfx::geometry_handle
namespace scene { class basic_section; }
// Editor-owned, editable terrain patch.
// a chunk of the heightmap terrain held in memory: its data, and what's drawn of it.
//
// The engine's scene::shape_node drops its CPU-side vertices the moment it uploads them to the GPU,
// so it can't be edited or raycast after load. This class keeps the authoritative, editable data
// (a regular grid heightmap) on the CPU, generates a shape_node purely for rendering, and answers
// height/raycast queries directly from the heightmap (fast and exact). Sculpting updates the
// heightmap and pushes the new vertex positions into the shape's existing geometry chunk.
// the data is shared with the worker threads making its meshes, so it's never changed in place: an edit gets a copy
// of its own when the current data is in use elsewhere. what's drawn is managed by the terrain streamer
class editor_terrain
{
public:
editor_terrain() = default;
explicit editor_terrain(heightmap::chunk_ptr Data) : m_data(std::move(Data)) {}
// builds an NxN-cell grid centred on Center, each quad CellSize metres across, using the
// material fetched from TextureName (empty => untextured). when Sampler is supplied it provides
// the starting height at each grid vertex (returns false to fall back to Center.y) - used to
// capture existing terrain. returns false on failure.
using height_sampler = std::function<bool(double X, double Z, double &OutY)>;
bool create(glm::dvec3 const &Center, int Cells, float CellSize, std::string const &TextureName,
height_sampler const &Sampler = {});
heightmap::chunk_data const &data() const { return *m_data; }
heightmap::chunk_ptr const &shared() const { return m_data; }
// the data, to be changed. marks the chunk as edited; Shape: the heights changed, Paint: the materials changed
heightmap::chunk_data &edit(bool const Shape, bool const Paint);
// puts provided data in place of the current one
void replace(heightmap::chunk_ptr Data, bool const Shape, bool const Paint);
// true if (X,Z) lies within the terrain's horizontal footprint
bool contains(double X, double Z) const;
// surface height at (X,Z) (bilinear over the covering quad); only valid when contains() is true
double height_at(double X, double Z) const;
heightmap::chunk_key key() const { return m_data->key; }
// true if (X,Z) lies within the chunk
bool contains(double X, double Z) const { return m_data->contains(X, Z); }
// surface height at (X,Z), the way the full grid is triangulated
double height_at(double X, double Z) const { return m_data->height_at(X, Z); }
// the same, from heights taken from heights() earlier
double height_in(std::vector<float> const &Heights, double X, double Z) const;
// raises/lowers vertices within Radius of (X,Z) by Strength (metres, signed), with a smooth
// falloff; regenerates the rendered geometry (full resolution). returns true if anything changed.
bool sculpt(double X, double Z, double Radius, double Strength);
// gives every grid vertex to Shaper, with its world position and height; Shaper returns true if it changed the height.
// regenerates the rendered geometry (full resolution). returns true if anything changed.
bool reshape(std::function<bool(double X, double Z, float &Height)> const &Shaper);
// pulls the vertices within Radius of (X,Z) towards the average of their neighbours, Amount (0..1) of the way at the centre,
// with a smooth falloff. Ground gives the height before the stroke at a world point, Fallback where there's no terrain
bool smooth(double X, double Z, double Radius, double Amount, std::function<double(double X, double Z, double Fallback)> const &Ground);
// puts back heights taken from heights() earlier
void restore(std::vector<float> const &Heights);
// rebuilds the rendered mesh, collapsing regions flatter than ErrorMetres into larger quads
// (adaptive quadtree). the editable heightmap is untouched, so sculpting/raycast stay exact.
void optimize(float ErrorMetres);
// rebuilds the rendered mesh at full resolution (undoes optimize)
void unoptimize();
// removes the rendered shape from its section (stops drawing it); the renderer's geometry
// garbage collector reclaims the GPU memory shortly after. used by terrain streaming to unload.
void destroy();
// horizontal centre and extent, handy for the UI / camera framing
double height_in(std::vector<float> const &Heights, double X, double Z) const { return m_data->height_in(Heights, X, Z); }
std::vector<float> const &heights() const { return m_data->heights; }
int cells() const { return m_data->cells; }
glm::dvec3 centre() const;
float extent() const { return m_cells * m_cellsize; }
bool valid() const { return m_cells > 0; }
float extent() const { return static_cast<float>(heightmap::chunk_size); }
bool valid() const { return m_data != nullptr && m_data->valid(); }
gfx::geometry_handle geometry() const { return m_geometry; }
bool optimized() const { return m_simplify; }
// set by sculpt() when the mesh changed and is currently full-resolution; cleared by optimize().
// used to auto-simplify only the chunks that were actually edited, once a stroke finishes.
bool dirty() const { return m_dirty; }
// set by sculpt() and never auto-cleared; used by streaming to know a chunk needs saving to disk
// rendered triangle count, and the count of the full grid
std::size_t triangles() const { return m_triangles; }
std::size_t full_triangles() const { return static_cast<std::size_t>(cells()) * cells() * 2; }
// changed since it was loaded or saved
bool modified() const { return m_modified; }
void clear_modified() { m_modified = false; }
// direct access to the editable heightmap (row-major world Y, (cells+1)^2), for save/load
std::vector<float> const &heights() const { return m_heights; }
int cells() const { return m_cells; }
// rendered triangle count (drops after optimize)
std::size_t triangles() const { return m_vertexcount / 3; }
// full-resolution triangle count, for reference
std::size_t full_triangles() const { return static_cast<std::size_t>(m_cells) * m_cells * 2; }
private:
int index(int Ix, int Iz) const { return Iz * (m_cells + 1) + Ix; }
glm::dvec3 vertex_position(int Ix, int Iz) const;
glm::vec3 vertex_normal(int Ix, int Iz) const;
world_vertex make_vertex(int Ix, int Iz) const;
// fills Out with the GL_TRIANGLES world-space vertex list; Simplify enables adaptive merging
void build_vertices(std::vector<world_vertex> &Out, bool Simplify) const;
// adaptive quadtree helpers (used when Simplify is on)
bool block_flat(int X0, int Z0, int X1, int Z1, float Error) const;
void collect_blocks(int X0, int Z0, int X1, int Z1, float Error, std::vector<std::array<int, 4>> &Out) const;
// a block as a fan through the marked grid vertices of its edges, so it meets its smaller neighbours without cracks
void emit_block(std::array<int, 4> const &Block, std::vector<char> const &Marked, std::vector<world_vertex> &Out) const;
void emit_quad(int X0, int Z0, int X1, int Z1, std::vector<world_vertex> &Out) const;
// rebuilds and re-uploads the rendered geometry (Replace when the count is unchanged, otherwise
// a fresh chunk whose handle is swapped into the shape)
void regenerate(bool Simplify);
friend class terrain_streamer;
int m_cells{0}; // quads per side; (m_cells+1)^2 grid vertices
float m_cellsize{1.0f}; // metres per quad
double m_x0{0.0}, m_z0{0.0}; // world position of grid corner (ix=0, iz=0)
std::vector<float> m_heights; // per-vertex world Y, row-major (m_cells+1)^2
heightmap::chunk_ptr m_data;
bool m_modified{false};
std::uint64_t m_shapeversion{1}; // grows with every change of the heights
std::uint64_t m_paintversion{1}; // grows with every change of the paint
// drawn state, managed by the streamer
int m_slot{-1}; // geometry slot holding the mesh
gfx::geometry_handle m_geometry{0, 0};
std::size_t m_triangles{0};
int m_stride{0}; // of the mesh in the slot, 0: none yet
std::uint64_t m_builtversion{0}; // shape version the mesh was made from
std::uint64_t m_paintedversion{0}; // paint version the material was made from
bool m_neighbours{false}; // a neighbour came or changed, the edges of the mesh are to be made again
bool m_building{false}; // a mesh is being made by a worker
material_handle m_material{null_handle};
gfx::geometrybank_handle m_bank{0, 0}; // geometry bank owning the rendered chunk
gfx::geometry_handle m_geometry{0, 0}; // rendered chunk
std::size_t m_vertexcount{0}; // current chunk's vertex count (for Replace vs recreate)
glm::dvec3 m_origin{0.0}; // origin the GPU vertices are stored relative to
scene::basic_section *m_section{nullptr}; // section holding the shape, for handle swaps
bool m_simplify{false}; // whether the rendered mesh is currently simplified
float m_simplify_error{0.5f}; // flatness tolerance used by optimize()
bool m_dirty{false}; // edited since the last optimize (full-res, awaiting simplification)
bool m_modified{false}; // edited since load/save (for streaming persistence)
scene::basic_section *m_section{nullptr}; // holding the shape, null: not drawn
glm::dvec3 m_low{0.0}, m_high{0.0}; // bounds of the drawn mesh
};

File diff suppressed because it is too large Load Diff

View File

@@ -9,112 +9,237 @@ http://mozilla.org/MPL/2.0/.
#pragma once
#include <condition_variable>
#include <deque>
#include <map>
#include <memory>
#include <vector>
#include <mutex>
#include <set>
#include <string>
#include <thread>
#include <vector>
#include <glm/glm.hpp>
#include "editor/editorTerrain.hpp"
// Streams editable terrain chunks around the camera for an effectively unbounded world.
struct world_vertex;
namespace scene { class basic_section; class basic_cell; }
// the heightmap terrain of the scenery (scene/heightmapterrain.h), loaded around the camera.
//
// Phase 1: keeps a ring of resident chunks (each an editor_terrain mesh) within a radius of the
// camera; chunks entering the radius are built, chunks leaving it are destroyed (the renderer's
// geometry GC reclaims their GPU memory). Heights are flat + a gentle procedural roll so streaming
// is observable. Phase 2 will add 16-bit on-disk chunk paging, per-chunk flag bits and persistence.
// the pack files are read and the meshes made by worker threads; the main thread only puts finished meshes on the gpu,
// within a few milliseconds of each frame. chunks are loaded around the camera and around the place the camera is heading
// to, so a moving train finds its terrain loaded. each chunk is drawn with a grid as coarse as its distance allows.
//
// the editor changes the chunks in memory; they're written to the pack files when the scenery is saved, until then the
// edited chunks stay in memory, also when they're far from the camera
class terrain_streamer
{
public:
using chunk_key = std::pair<int, int>; // (cx, cz) chunk coordinate
using chunk_key = heightmap::chunk_key;
terrain_streamer() = default;
~terrain_streamer();
void active(bool State) { m_active = State; }
// opens the terrain kept in terrain/<Name>/ (the folder may not exist yet). returns: true if there's terrain to show
bool open(std::string const &Name);
// stops showing the terrain
void close();
bool active() const { return m_active; }
std::string const &name() const { return m_name; }
// the folder of the terrain, with a trailing slash
std::string const &directory() const { return m_directory; }
// per-chunk configuration (applied to chunks built from now on)
void configure(int Cells, float CellSize, int Radius, float BaseHeight, std::string const &Texture);
void simplify(bool Auto, float Error) { m_auto_optimize = Auto; m_simplify_error = Error; }
// safe to change while resident (only affects how far chunks load/unload)
void radius(int Radius) { m_radius = Radius < 0 ? 0 : Radius; }
// persist edited chunks to disk on unload, and load them back instead of regenerating
void persist(bool Enable) { m_persist = Enable; }
void directory(std::string const &Dir)
{
if (!Dir.empty() && Dir != m_dir)
{
m_dir = Dir;
m_known.clear(); // existence cache is per-folder; drop it when the folder changes
}
}
std::string const &directory() const { return m_dir; }
// loads/unloads chunks so the resident set matches the radius around CameraPos
void update(glm::dvec3 const &CameraPos);
// drops all resident chunks (e.g. when streaming is switched off), saving edits first
void clear();
// loads and unloads the chunks around the camera, and puts finished meshes on the gpu
void update(glm::dvec3 const &Camera);
// hard reset used when a new scenery loads: drops everything WITHOUT touching scene sections
// (the old region is being torn down, so its section pointers are already dangling)
void reset();
// appends raw pointers to every resident chunk (for sculpt / snap / optimize routing)
void collect(std::vector<editor_terrain *> &Out) const;
// resident chunk whose footprint covers (X,Z), or nullptr
editor_terrain *terrain_at(double X, double Z) const;
// authoring: create a flat chunk at (Cx,Cz), make it resident and persist it to disk
void add_chunk(int Cx, int Cz);
// authoring: delete the chunk at (Cx,Cz) from memory and disk
void remove_chunk(int Cx, int Cz);
// persist an externally-authored chunk (e.g. a manual grid chunk being handed over to streaming)
void save_chunk(int Cx, int Cz, editor_terrain const &Terrain);
// chunk coordinate covering (X,Z)
chunk_key key_for(double X, double Z) const { return key_at(X, Z); }
std::size_t resident() const { return m_chunks.size(); }
float chunk_world_size() const { return m_cells * m_cellsize; }
int cells() const { return m_cells; }
float cellsize() const { return m_cellsize; }
// drawn detail: metres of distance per metre of grid spacing (larger: finer grid further away)
void detail(float const Detail) { m_detail = std::max(10.f, Detail); }
float detail() const { return m_detail; }
// smallest distance (chunks) the terrain is loaded to, whatever the draw range
void radius(int const Radius) { m_radius = std::max(0, Radius); }
int radius() const { return m_radius; }
std::string const &texture() const { return m_texture; }
// writes every resident, edited chunk to disk (without unloading) - used on save
void flush();
// chunks held in memory
void collect(std::vector<editor_terrain *> &Out) const;
// chunk covering (X,Z), or nullptr
editor_terrain *terrain_at(double X, double Z) const;
// height of the terrain at (X,Z); false where there's no chunk in memory
bool height_at(double X, double Z, double &Height) const;
// triangles of the full grid of the chunks in memory overlapping the rectangle (x, z)
void gather_triangles(glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector<std::array<glm::dvec3, 3>> &Out) const;
// true if the geometry belongs to the terrain (or its water)
bool owns(gfx::geometry_handle const &Geometry) const;
std::size_t resident() const { return m_chunks.size(); }
// chunks in the pack files and in memory, less the removed ones; the files known so far
bool exists(chunk_key const &Key) const;
// editing; the chunks changed are written to the files by save()
heightmap::manifest const &manifest() const { return m_manifest; }
// a flat chunk at (Cx, Cz) of specified spacing and height, covered with the material of palette entry Layer;
// returns false if there's a chunk already
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)
bool add_chunk(int Cx, int Cz, float Spacing, std::function<float(double X, double Z)> const &Height, std::uint16_t Layer = 0);
void remove_chunk(int Cx, int Cz);
// changes the point spacing of the chunk at (Cx, Cz), heights resampled
void spacing(int Cx, int Cz, float Spacing);
// raises (Strength > 0) or lowers the ground within Radius of (X,Z), with a smooth falloff
bool sculpt(double X, double Z, double Radius, double Strength);
// evens out the ground within Radius of (X,Z), Amount (0..1) of the way at the centre
bool smooth(double X, double Z, double Radius, double Amount);
// gives every grid point within the rectangle to Shaper, with its world position and height (before the modifiers);
// Shaper returns true if it changed the height
bool reshape(glm::dvec2 const &Min, glm::dvec2 const &Max, std::function<bool(double X, double Z, float &Height)> const &Shaper);
// paints the material of the palette entry Layer within Radius of (X,Z), Strength (0..1) of the way at the centre
bool paint(double X, double Z, double Radius, double Strength, std::uint16_t Layer);
// palette entry of the material, added to the palette if needed
std::uint16_t layer(std::string const &Material, float Size = 0.f);
void layer_size(std::uint16_t Layer, float Size);
void compress(bool State);
void default_spacing(float Spacing);
// bodies of water
std::vector<heightmap::water_body> const &water() const { return m_manifest.water; }
void water(std::vector<heightmap::water_body> Water);
// modifiers: added or replaced by name
void modifier(heightmap::modifier Modifier);
bool remove_modifier(std::string const &Name);
std::vector<heightmap::modifier> const &modifiers() const { return m_manifest.modifiers; }
// changed since the last save
bool modified() const;
// writes the description and the changed pack files. returns: true on success, Error describing the failure
bool save(std::string *Error = nullptr);
private:
// per-chunk state bits cached so we don't stat the filesystem repeatedly
enum chunk_flag : uint8_t
struct pack_state
{
cf_exists_on_disk = 1u << 0, // a saved 16-bit chunk file exists
enum class state
{
unknown,
loading,
ready,
missing
} status{state::unknown};
std::shared_ptr<heightmap::pack_file const> file;
std::uint32_t generation{0};
};
struct job
{
enum class kind
{
pack,
chunk,
mesh
} type{kind::pack};
heightmap::pack_key pack{0, 0};
chunk_key chunk{0, 0};
std::shared_ptr<heightmap::pack_file const> file;
std::uint32_t generation{0};
heightmap::chunk_ptr data;
std::array<heightmap::chunk_ptr, 4> neighbours; // -x, +x, -z, +z
int stride{1};
glm::dvec3 origin{0.0};
std::uint64_t version{0};
double priority{0.0};
};
struct result
{
job::kind type{job::kind::pack};
heightmap::pack_key pack{0, 0};
chunk_key chunk{0, 0};
std::uint32_t generation{0};
std::shared_ptr<heightmap::pack_file const> file;
std::shared_ptr<heightmap::chunk_data> data;
heightmap::mesh mesh;
int stride{1};
std::uint64_t version{0};
std::string error;
};
struct geometry_slot
{
gfx::geometrybank_handle bank{0, 0};
gfx::geometry_handle geometry{0, 0};
int cells{0};
int stride{0};
bool used{false};
};
struct water_shape
{
gfx::geometry_handle geometry{0, 0};
scene::basic_section *section{nullptr};
scene::basic_cell *cell{nullptr};
};
chunk_key key_at(double X, double Z) const;
glm::dvec3 chunk_centre(int Cx, int Cz) const;
std::string chunk_path(int Cx, int Cz) const;
bool chunk_on_disk(chunk_key const &Key); // cached existence test
bool load_heights(int Cx, int Cz, std::vector<float> &Out) const; // reads a 16-bit chunk file
void save_heights(int Cx, int Cz, editor_terrain const &Terrain); // writes a 16-bit chunk file
void start_workers();
void stop_workers();
void worker();
void submit(job Job);
void process(result &Result);
// the chunk is drawn: its shape put in the scene, or put back with a new mesh
void upload(editor_terrain &Chunk, heightmap::mesh &Mesh, int const Stride);
void update_material(editor_terrain &Chunk);
// puts the current material of the chunk in its shape
void upload_shape_material(editor_terrain &Chunk);
// takes the chunk out of the scene, its geometry slot and material given back
void undraw(editor_terrain &Chunk);
void request_mesh(editor_terrain &Chunk, int const Stride, double const Priority);
int wanted_stride(editor_terrain const &Chunk, glm::dvec3 const &Camera) const;
heightmap::chunk_ptr neighbour(chunk_key const &Key) const;
void touch_neighbours(chunk_key const &Key);
material_handle palette_material(std::uint16_t const Layer);
// chunk loaded right away from its pack file, for editing far from the camera
editor_terrain *load_now(chunk_key const &Key);
// chunks in memory or in the files overlapping the rectangle, loaded if needed
std::vector<editor_terrain *> chunks_in(glm::dvec2 const &Min, glm::dvec2 const &Max, bool const Load);
// leads the heights of the chunk from its base heights through the modifiers, within the rectangle
void apply_modifiers(editor_terrain &Chunk, glm::dvec2 const &Min, glm::dvec2 const &Max);
void apply_modifiers(glm::dvec2 const &Min, glm::dvec2 const &Max);
void build_water();
void clear_water(bool const Touch);
std::string pack_path(heightmap::pack_key const &Key) const;
bool m_active{false};
int m_cells{32};
float m_cellsize{2.0f};
int m_radius{2}; // chunks loaded around the camera (Chebyshev radius)
float m_baseheight{0.0f};
std::string m_texture;
bool m_auto_optimize{true};
float m_simplify_error{0.5f};
bool m_persist{true};
std::string m_dir{"editor_terrain"}; // folder for 16-bit chunk files
std::string m_name;
std::string m_directory;
heightmap::manifest m_manifest;
bool m_manifestchanged{false};
float m_detail{120.f};
int m_radius{2};
std::map<heightmap::pack_key, pack_state> m_packs;
std::map<chunk_key, std::unique_ptr<editor_terrain>> m_chunks;
std::map<chunk_key, std::uint8_t> m_known; // cached per-chunk flag bits
// offsets of the chunks around the camera, the nearest first, for the radius in effect
std::vector<chunk_key> m_ring;
int m_ring_radius{-1};
std::set<chunk_key> m_loading; // asked from a worker
std::set<chunk_key> m_removed; // removed in the editor, still in the files until saved
std::vector<geometry_slot> m_slots;
std::map<std::uint64_t, int> m_slotofgeometry; // geometry key -> slot
std::map<std::uint16_t, material_handle> m_palette;
std::vector<water_shape> m_watershapes;
std::vector<gfx::geometry_handle> m_waterfree; // geometry of the water shown before, to be filled anew
gfx::geometrybank_handle m_waterbank{0, 0};
bool m_waterchanged{false};
glm::dvec3 m_camera{0.0};
glm::dvec3 m_velocity{0.0};
double m_cameratime{0.0};
bool m_hascamera{false};
std::vector<chunk_key> m_ring; // offsets of the chunks around a point, the nearest first
int m_ringradius{-1};
// shared with the workers, guarded by m_mutex
std::mutex m_mutex;
std::condition_variable m_wakeup;
std::vector<job> m_jobs;
std::deque<result> m_results;
std::size_t m_busy{0}; // jobs taken by the workers and not finished yet
bool m_stop{false};
std::vector<std::thread> m_workers;
// the pack files are read by the workers and written by save()
std::mutex m_filemutex;
};
// single, simulation-level streamer instance shared by the editor (authoring) and the scenery
// loader (so streamed terrain renders in every mode, including the driver). loaded from the
// `editorterrain` scenery directive and updated each frame with the active camera.
// single, simulation-level instance shared by the editor (authoring) and the scenery loader, so the terrain
// renders in every mode. opened by the `heightmap_terrain` scenery directive and updated each frame with the active camera.
extern terrain_streamer EditorTerrain;

View File

@@ -640,6 +640,28 @@ material_manager::create( std::string const &Filename, bool const Loadnow ) {
return materialhandle;
};
material_handle
material_manager::create_from_text( std::string const &Name, std::string const &Definition ) {
opengl_material material;
cParser materialparser( Definition, cParser::buffer_TEXT );
if( false == material.deserialize( materialparser, true ) ) {
return null_handle;
}
material.name = Name;
try {
material.finalize( true );
}
catch( gl::shader_exception const &e ) {
ErrorLog( "invalid shader: " + std::string( e.what() ) );
return null_handle;
}
auto const materialhandle { static_cast<material_handle>( m_materials.size() ) };
m_materials.emplace_back( std::move( material ) );
// not listed under its name: the name isn't a file which could be asked for
return materialhandle;
}
void
material_manager::on_weather_change() {

View File

@@ -139,6 +139,9 @@ public:
material_handle
create( std::string const &Filename, bool const Loadnow );
// material described by provided text (the content of a .mat file), not kept in a file. returns: handle to the material or null_handle
material_handle
create_from_text( std::string const &Name, std::string const &Definition );
opengl_material const &
material( material_handle const Material ) const { return m_materials[ Material ]; }
opengl_material &

30
ref/zstd/LICENSE Normal file
View File

@@ -0,0 +1,30 @@
BSD License
For Zstandard software
Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name Facebook, nor Meta, nor the names of its contributors may
be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

9
ref/zstd/README.txt Normal file
View File

@@ -0,0 +1,9 @@
Zstandard 1.5.6 (https://github.com/facebook/zstd), BSD licence (see LICENSE).
zstd.c is the single file amalgamation made with build/single_file_libs/combine.py from
zstd-in.c, without the dictionary builder and without multithreading:
sed -e "s/^#ifndef __EMSCRIPTEN__$/#if 0/" -e "/dictBuilder/d" zstd-in.c > zstd-maszyna-in.c
python3 combine.py -r ../../lib -x legacy/zstd_legacy.h -o zstd.c zstd-maszyna-in.c
Used by the heightmap terrain files (.tch).

45229
ref/zstd/zstd.c Normal file

File diff suppressed because it is too large Load Diff

3089
ref/zstd/zstd.h Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -616,6 +616,8 @@ bool opengl33_renderer::Render()
}
// generate new frame
opengl_texture::reset_unit_cache();
// textures of new heightmap terrain layers go to the arrays the terrain shader reads them from
m_terrainmaterials.update();
if( EditorModeFlag ) {
// the editor moves and resizes model instances in place, which leaves the copies of their bounds out of date
++m_instanceboundsversion;
@@ -2417,6 +2419,20 @@ IMaterial const *opengl33_renderer::Material(material_handle const Material) con
return &m_materials.material(Material);
}
material_handle opengl33_renderer::Terrain_Material(material_handle const Reuse, std::vector<gfx::terrain_layer> const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement)
{
auto const material{m_terrainmaterials.make(Reuse, Layers, Samples, Weights, Placement)};
if (material != null_handle)
return material;
// the terrain shader can't be used, the chunk makes do with a plain material
return gfx_renderer::Terrain_Material(Reuse, Layers, Samples, Weights, Placement);
}
void opengl33_renderer::Terrain_Release(material_handle const Material)
{
m_terrainmaterials.release(Material);
}
opengl_material &opengl33_renderer::Material(material_handle const Material)
{
return m_materials.material(Material);

View File

@@ -29,6 +29,7 @@ http://mozilla.org/MPL/2.0/.
#include "gl/glsl_common.h"
#include "gl/pbo.h"
#include "gl/query.h"
#include "rendering/opengl33terrain.h"
// bare-bones render controller, in lack of anything better yet
class opengl33_renderer : public gfx_renderer {
@@ -81,6 +82,10 @@ class opengl33_renderer : public gfx_renderer {
Bind_Material( material_handle const Material, TSubModel const *sm = nullptr, lighting_data const *lighting = nullptr ) override;
IMaterial const *
Material( material_handle const Material ) const override;
material_handle
Terrain_Material( material_handle const Reuse, std::vector<gfx::terrain_layer> const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement ) override;
void
Terrain_Release( material_handle const Material ) override;
// shader methods
auto Fetch_Shader( std::string const &name ) -> std::shared_ptr<gl::program> override;
// texture methods
@@ -350,6 +355,7 @@ class opengl33_renderer : public gfx_renderer {
gfx::geometrybank_manager m_geometry;
material_manager m_materials;
texture_manager m_textures;
opengl33_terrain_materials m_terrainmaterials{m_materials, m_textures};
opengl33_light m_sunlight;
opengllight_array m_lights;
/*

View File

@@ -0,0 +1,423 @@
/*
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 "rendering/opengl33terrain.h"
#include "rendering/opengl33geometrybank.h"
#include "utilities/Globals.h"
#include "utilities/Logs.h"
namespace
{
std::size_t constexpr max_layers{8};
int constexpr initial_capacity{8};
int constexpr copy_attempts{600}; // frames to wait for the textures of a layer to load before it's left with what it has
float constexpr default_repeat{8.f};
// array textures: diffuse, normal maps, specular/gloss maps
std::array<int, 3> constexpr array_sizes{1024, 512, 256};
void bind_for_upload(opengl_texture const &Texture)
{
::glActiveTexture(GL_TEXTURE0);
::glBindTexture(Texture.target, Texture.id);
}
void set_anisotropy(GLenum const Target)
{
if (Global.AnisotropicFiltering >= 0 && (GLAD_GL_EXT_texture_filter_anisotropic || GLAD_GL_ARB_texture_filter_anisotropic))
::glTexParameterf(Target, GL_TEXTURE_MAX_ANISOTROPY_EXT, Global.AnisotropicFiltering);
}
} // namespace
bool opengl33_terrain_materials::init()
{
if (m_state != 0)
return m_state > 0;
m_state = -1;
try
{
m_copy = std::make_unique<gl::postfx>(gl::shader("copy.frag"));
}
catch (gl::shader_exception const &e)
{
ErrorLog("heightmap terrain: can't use the copy shader: " + std::string(e.what()));
return false;
}
m_framebuffer = std::make_unique<gl::framebuffer>();
m_white = make_texture("internal_src:terrain/white", GL_TEXTURE_2D);
{
auto const &white{m_textures.texture(m_white)};
std::uint8_t const texel[4]{255, 255, 255, 255};
bind_for_upload(white);
::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, texel);
}
char const *names[]{"internal_src:terrain/layers_diffuse", "internal_src:terrain/layers_normal", "internal_src:terrain/layers_specgloss"};
for (std::size_t i = 0; i < m_arrays.size(); ++i)
{
auto &array{m_arrays[i]};
array.handle = make_texture(names[i], GL_TEXTURE_2D_ARRAY);
array.size = array_sizes[i];
array.srgb = (i == 0);
array.format = (array.srgb ? GL_SRGB8_ALPHA8 : GL_RGBA8);
make_storage(array, initial_capacity);
}
m_capacity = initial_capacity;
opengl_texture::reset_unit_cache();
// the shader is checked now, so a broken one makes the terrain fall back on plain materials instead of the failure indicator
try
{
GfxRenderer->Fetch_Shader("terrain");
}
catch (gl::shader_exception const &e)
{
ErrorLog("heightmap terrain: can't use the terrain shader: " + std::string(e.what()));
return false;
}
m_state = 1;
return true;
}
texture_handle opengl33_terrain_materials::make_texture(std::string const &Name, GLenum const Target)
{
auto const handle{m_textures.create(Name, false, GL_RGBA)};
auto &texture{m_textures.texture(handle)};
if (texture.id == opengl_texture::invalid_id)
::glGenTextures(1, &texture.id);
texture.target = Target;
// the content is supplied here, the texture manager isn't to load, release or replace it
texture.is_ready = true;
texture.is_static = true;
texture.is_rendertarget = true;
texture.is_texstub = false;
texture.data_state = resource_state::good;
return handle;
}
void opengl33_terrain_materials::make_storage(texture_array &Array, int const Layers)
{
auto &texture{m_textures.texture(Array.handle)};
bind_for_upload(texture);
::glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
::glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
::glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_REPEAT);
::glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_REPEAT);
set_anisotropy(GL_TEXTURE_2D_ARRAY);
::glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, Array.format, Array.size, Array.size, Layers, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
texture.data_width = Array.size;
texture.data_height = Array.size;
texture.layers = Layers;
texture.data_format = Array.format;
texture.data_components = GL_RGBA;
}
int opengl33_terrain_materials::layer_index(gfx::terrain_layer const &Layer)
{
for (auto const &existing : m_layers)
if (existing.material == Layer.material && existing.size == Layer.size)
return existing.index;
layer entry;
entry.material = Layer.material;
entry.size = Layer.size;
entry.index = static_cast<int>(m_layers.size());
auto const &material{m_materials.material(Layer.material)};
entry.repeat = (Layer.size > 0.f ? Layer.size : (material.size.x > 0.f ? material.size.x : default_repeat));
if (material.shader)
{
entry.normalmap = material.shader->texture_conf.count("normalmap") > 0;
entry.specgloss = material.shader->texture_conf.count("specgloss") > 0;
}
entry.reflection = (std::isfinite(material.params[1].z) ? material.params[1].z : 0.f);
m_layers.push_back(entry);
m_pending = true;
if (static_cast<int>(m_layers.size()) > m_capacity)
{
// more room: the arrays are made anew, and every layer copied again
auto const capacity{m_capacity * 2};
for (auto &array : m_arrays)
{
auto &texture{m_textures.texture(array.handle)};
::glDeleteTextures(1, &texture.id);
::glGenTextures(1, &texture.id);
make_storage(array, capacity);
}
m_capacity = capacity;
for (auto &existing : m_layers)
{
existing.copied = false;
existing.attempts = 0;
}
opengl_texture::reset_unit_cache();
}
return entry.index;
}
std::size_t opengl33_terrain_materials::acquire_slot(material_handle const Reuse)
{
if (Reuse != null_handle)
{
auto const lookup{m_slotof.find(Reuse)};
if (lookup != m_slotof.end())
{
m_slots[lookup->second].used = true;
return lookup->second;
}
}
if (false == m_free.empty())
{
auto const index{m_free.back()};
m_free.pop_back();
m_slots[index].used = true;
return index;
}
auto const index{m_slots.size()};
auto const suffix{std::to_string(index)};
slot entry;
entry.weights[0] = make_texture("internal_src:terrain/weights0_" + suffix, GL_TEXTURE_2D);
entry.weights[1] = make_texture("internal_src:terrain/weights1_" + suffix, GL_TEXTURE_2D);
entry.table = make_texture("internal_src:terrain/table_" + suffix, GL_TEXTURE_2D);
// the textures get their content before the material is made of them, which checks they're there
std::uint8_t const blank[4]{0, 0, 0, 0};
for (auto const handle : entry.weights)
{
bind_for_upload(m_textures.texture(handle));
::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, blank);
::glGenerateMipmap(GL_TEXTURE_2D);
}
{
bind_for_upload(m_textures.texture(entry.table));
::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
std::array<float, max_layers * 2 * 4> const zeros{};
::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, static_cast<GLsizei>(max_layers), 2, 0, GL_RGBA, GL_FLOAT, zeros.data());
}
opengl_texture::reset_unit_cache();
auto const name = [&](texture_handle const Handle) { return "internal_src:" + m_textures.texture(Handle).name; };
std::string const definition{
"shader: terrain\n"
"texture1: " + name(m_white) + "\n"
"texture2: " + name(entry.weights[0]) + "\n"
"texture3: " + name(entry.weights[1]) + "\n"
"texture4: " + name(m_arrays[0].handle) + "\n"
"texture5: " + name(m_arrays[1].handle) + "\n"
"texture6: " + name(m_arrays[2].handle) + "\n"
"texture7: " + name(entry.table) + "\n"};
entry.material = m_materials.create_from_text("internal_src:terrain/chunk_" + suffix, definition);
if (entry.material == null_handle || false == m_materials.material(entry.material).is_good)
{
ErrorLog("heightmap terrain: can't make the material of a chunk");
m_state = -1;
return static_cast<std::size_t>(-1);
}
entry.used = true;
m_slots.push_back(entry);
m_slotof.emplace(entry.material, index);
return index;
}
material_handle opengl33_terrain_materials::make(material_handle const Reuse, std::vector<gfx::terrain_layer> const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement)
{
if (Layers.empty() || false == init())
return null_handle;
auto const count{std::min(Layers.size(), max_layers)};
std::array<int, max_layers> indices{};
for (std::size_t i = 0; i < count; ++i)
{
if (Layers[i].material == null_handle)
return null_handle;
indices[i] = layer_index(Layers[i]);
}
auto const slotindex{acquire_slot(Reuse)};
if (slotindex == static_cast<std::size_t>(-1))
return null_handle;
auto &entry{m_slots[slotindex]};
// weights, four layers to a texture
auto const painted{Weights != nullptr && Samples > 0 && count > 1};
auto const side{painted ? Samples + 1 : 1};
auto const samples{static_cast<std::size_t>(side) * side};
std::vector<std::uint8_t> texels(samples * 4);
for (std::size_t texture = 0; texture < 2; ++texture)
{
std::fill(texels.begin(), texels.end(), 0);
for (std::size_t channel = 0; channel < 4; ++channel)
{
auto const layer{texture * 4 + channel};
if (layer >= count)
break;
if (false == painted)
{
if (layer == 0)
texels[channel] = 255;
continue;
}
auto const *source{Weights + layer * samples};
for (std::size_t i = 0; i < samples; ++i)
texels[i * 4 + channel] = source[i];
}
bind_for_upload(m_textures.texture(entry.weights[texture]));
::glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, side, side, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
::glGenerateMipmap(GL_TEXTURE_2D);
}
// what the layers are: index in the arrays, repeat, which maps they have, reflection
std::array<float, max_layers * 2 * 4> table{};
for (std::size_t i = 0; i < count; ++i)
{
auto const &layer{m_layers[indices[i]]};
table[i * 4 + 0] = static_cast<float>(layer.index);
table[i * 4 + 1] = 1.f / std::max(0.01f, layer.repeat);
table[i * 4 + 2] = layer.normalmap ? 1.f : 0.f;
table[i * 4 + 3] = layer.specgloss ? 1.f : 0.f;
table[(max_layers + i) * 4 + 0] = layer.reflection;
}
bind_for_upload(m_textures.texture(entry.table));
::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, static_cast<GLsizei>(max_layers), 2, 0, GL_RGBA, GL_FLOAT, table.data());
opengl_texture::reset_unit_cache();
auto &material{m_materials.material(entry.material)};
material.params[2] = glm::vec4(Placement.x, Placement.y, 1.f / std::max(1.f, Placement.z), static_cast<float>(side - 1));
return entry.material;
}
void opengl33_terrain_materials::release(material_handle const Material)
{
auto const lookup{m_slotof.find(Material)};
if (lookup == m_slotof.end() || false == m_slots[lookup->second].used)
return;
m_slots[lookup->second].used = false;
m_free.push_back(lookup->second);
}
bool opengl33_terrain_materials::copy_layer(layer &Layer)
{
auto const &material{m_materials.material(Layer.material)};
auto const slot = [&](char const *Name, int const Fallback) {
if (material.shader)
{
auto const lookup{material.shader->texture_conf.find(Name)};
if (lookup != material.shader->texture_conf.end())
return static_cast<int>(lookup->second.id);
return -1;
}
return Fallback;
};
std::array<int, 3> const slots{slot("diffuse", 0), Layer.normalmap ? slot("normalmap", 1) : -1, Layer.specgloss ? slot("specgloss", 2) : -1};
// all maps of the layer have to be ready, so they go together
std::array<opengl_texture *, 3> sources{};
for (std::size_t i = 0; i < slots.size(); ++i)
{
if (slots[i] < 0 || material.textures[slots[i]] == null_handle)
continue;
auto &texture{m_textures.mark_as_used(material.textures[slots[i]])};
if (false == texture.create())
{
if (++Layer.attempts < copy_attempts)
return false;
continue; // waited long enough, the layer goes without it
}
sources[i] = &texture;
}
// defaults for maps the layer doesn't have: grey, flat, without specular reflections
std::array<std::array<float, 4>, 3> const defaults{{{0.5f, 0.5f, 0.5f, 1.f}, {0.5f, 0.5f, 1.f, 1.f}, {0.f, 1.f, 0.f, 1.f}}};
for (std::size_t i = 0; i < m_arrays.size(); ++i)
{
auto const &array{m_arrays[i]};
auto &target{m_textures.texture(array.handle)};
m_framebuffer->attach(target, GL_COLOR_ATTACHMENT0, Layer.index);
m_framebuffer->setup_drawing(1);
::glViewport(0, 0, array.size, array.size);
if (sources[i] == nullptr)
{
::glClearBufferfv(GL_COLOR, 0, defaults[i].data());
continue;
}
if (false == Global.gfx_usegles)
{
if (array.srgb)
::glEnable(GL_FRAMEBUFFER_SRGB);
else
::glDisable(GL_FRAMEBUFFER_SRGB);
}
m_copy->apply(*sources[i], m_framebuffer.get());
}
Layer.copied = true;
return true;
}
void opengl33_terrain_materials::update()
{
if (false == m_pending || m_state <= 0)
return;
auto const blend{::glIsEnabled(GL_BLEND)};
auto const scissor{::glIsEnabled(GL_SCISSOR_TEST)};
auto const cull{::glIsEnabled(GL_CULL_FACE)};
auto const depth{::glIsEnabled(GL_DEPTH_TEST)};
auto const srgb{(false == Global.gfx_usegles) && ::glIsEnabled(GL_FRAMEBUFFER_SRGB)};
::glDisable(GL_BLEND);
::glDisable(GL_SCISSOR_TEST);
::glDisable(GL_CULL_FACE);
bool copied{false};
m_pending = false;
for (auto &layer : m_layers)
{
if (layer.copied)
continue;
if (copy_layer(layer))
copied = true;
else
m_pending = true;
}
if (copied)
{
for (auto const &array : m_arrays)
{
bind_for_upload(m_textures.texture(array.handle));
::glGenerateMipmap(GL_TEXTURE_2D_ARRAY);
}
}
// leave the state as the rest of the renderer knows it
gl::framebuffer::unbind();
if (blend)
::glEnable(GL_BLEND);
if (scissor)
::glEnable(GL_SCISSOR_TEST);
if (cull)
::glEnable(GL_CULL_FACE);
if (depth)
::glEnable(GL_DEPTH_TEST);
else
::glDisable(GL_DEPTH_TEST);
if (false == Global.gfx_usegles)
{
if (srgb)
::glEnable(GL_FRAMEBUFFER_SRGB);
else
::glDisable(GL_FRAMEBUFFER_SRGB);
}
::glDepthMask(GL_TRUE);
opengl_texture::reset_unit_cache();
gfx::opengl33_vaogeometrybank::reset();
}

View File

@@ -0,0 +1,85 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#pragma once
#include <array>
#include <memory>
#include <unordered_map>
#include <vector>
#include "rendering/renderer.h"
#include "model/material.h"
#include "model/Texture.h"
#include "gl/framebuffer.h"
#include "gl/postfx.h"
// materials of the heightmap terrain for the opengl 3.3 renderer (shader mat_terrain).
// the textures of the layer materials are copied into texture arrays shared by all chunks; each chunk gets a material
// of its own, with textures holding the weights of its layers and a table of what its layers are
class opengl33_terrain_materials
{
public:
opengl33_terrain_materials(material_manager &Materials, texture_manager &Textures) : m_materials(Materials), m_textures(Textures) {}
// see gfx_renderer::Terrain_Material. returns null_handle if the terrain shader can't be used
material_handle make(material_handle const Reuse, std::vector<gfx::terrain_layer> const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement);
void release(material_handle const Material);
// copies the textures of new layers into the arrays; to be called before the frame is drawn
void update();
private:
struct layer
{
material_handle material{null_handle};
float size{0.f}; // as given by the chunk
float repeat{8.f}; // metres the textures are repeated at
int index{0}; // in the arrays
bool normalmap{false};
bool specgloss{false};
float reflection{0.f};
bool copied{false};
int attempts{0};
};
struct texture_array
{
texture_handle handle{null_handle};
int size{0};
GLint format{0};
bool srgb{false};
};
struct slot
{
material_handle material{null_handle};
std::array<texture_handle, 2> weights{null_handle, null_handle};
texture_handle table{null_handle};
bool used{false};
};
bool init();
texture_handle make_texture(std::string const &Name, GLenum const Target);
void make_storage(texture_array &Array, int const Layers);
int layer_index(gfx::terrain_layer const &Layer);
bool copy_layer(layer &Layer);
std::size_t acquire_slot(material_handle const Reuse);
material_manager &m_materials;
texture_manager &m_textures;
int m_state{0}; // 0: not set up yet, 1: ready, -1: can't be used
texture_handle m_white{null_handle};
std::array<texture_array, 3> m_arrays; // diffuse, normal maps, specular/gloss
int m_capacity{0}; // layers the arrays have room for
std::vector<layer> m_layers;
bool m_pending{false};
std::vector<slot> m_slots;
std::vector<std::size_t> m_free;
std::unordered_map<material_handle, std::size_t> m_slotof;
std::unique_ptr<gl::postfx> m_copy;
std::unique_ptr<gl::framebuffer> m_framebuffer;
};

View File

@@ -39,5 +39,29 @@ gfx_renderer_factory *gfx_renderer_factory::get_instance()
gfx_renderer_factory *gfx_renderer_factory::instance;
material_handle gfx_renderer::Terrain_Material( material_handle const Reuse, std::vector<gfx::terrain_layer> const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement )
{
// without blending the chunk gets the material which covers the most of it
if( Layers.empty() ) {
return null_handle;
}
std::size_t best { 0 };
if( ( Weights != nullptr ) && ( Samples > 0 ) ) {
auto const samples { static_cast<std::size_t>( Samples + 1 ) * ( Samples + 1 ) };
std::uint64_t bestsum { 0 };
for( std::size_t layer = 0; layer < Layers.size(); ++layer ) {
std::uint64_t sum { 0 };
for( std::size_t i = 0; i < samples; ++i ) {
sum += Weights[ layer * samples + i ];
}
if( sum > bestsum ) {
bestsum = sum;
best = layer;
}
}
}
return Layers[ best ].material;
}
//---------------------------------------------------------------------------

View File

@@ -21,6 +21,16 @@ namespace gl
class program;
}
namespace gfx
{
// material painted over the heightmap terrain, and the size (metres) its textures are repeated at; 0: the size given by the material, or 8 m
struct terrain_layer
{
material_handle material{null_handle};
float size{0.f};
};
} // namespace gfx
class gfx_renderer {
public:
@@ -58,6 +68,14 @@ public:
virtual auto Fetch_Material( std::string const &Filename, bool const Loadnow = true ) -> material_handle = 0;
virtual void Bind_Material( material_handle const Material, TSubModel const *sm = nullptr, lighting_data const *lighting = nullptr ) = 0;
virtual auto Material( material_handle const Material ) const -> IMaterial const * = 0;
// heightmap terrain: material of a chunk blending the materials of up to 8 layers by their weights.
// Weights: (Samples + 1)^2 values per layer, planar, rows of growing z, the weights of a sample adding up to 255; null with a single layer.
// Placement: corner of the chunk (x, z) in the texture coordinates of its mesh, and the size of the chunk.
// Reuse: a material made by this call before, to be filled anew, or null_handle.
// returns: the material. backends which can't blend give back the material of the layer covering the most
virtual auto Terrain_Material( material_handle const Reuse, std::vector<gfx::terrain_layer> const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement ) -> material_handle;
// gives back a material made by Terrain_Material, once its chunk is gone
virtual void Terrain_Release( material_handle const Material ) {}
// shader methods
virtual auto Fetch_Shader( std::string const &name ) -> std::shared_ptr<gl::program> = 0;
// texture methods

1141
scene/heightmapterrain.cpp Normal file

File diff suppressed because it is too large Load Diff

241
scene/heightmapterrain.h Normal file
View File

@@ -0,0 +1,241 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#pragma once
#include <array>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include <glm/glm.hpp>
#include "rendering/geometrybank.h"
// heightmap terrain: the ground of a scenery as a grid of square chunks of heights, kept in the folder
// terrain/<name>/ of the simulator: a description of the terrain (terrain.yaml) and pack files (.tch, terrain chunks)
// holding 16 x 16 chunks each. the chunks are loaded around the camera as they're needed.
//
// a chunk has its own point spacing (0.5, 1, 2 or 5 m), heights of its grid points including the points of its edges
// (shared with the neighbouring chunk), and weights of the materials painted over it, up to 8 of them per chunk.
namespace heightmap
{
double constexpr chunk_size{250.0}; // metres, side of a chunk; whole cells of every allowed spacing, and of the scene
int constexpr pack_side{16}; // chunks along the side of a pack file
double constexpr height_step{0.005}; // metres, quantum of the stored heights
std::size_t constexpr max_chunk_layers{8}; // materials painted over a single chunk
std::uint16_t constexpr no_layer{0xffff};
int constexpr default_paint_samples{250}; // paint samples along the side of a chunk, less one: a sample per metre
// point spacings a chunk can have, finest first
std::array<float, 4> constexpr spacings{0.5f, 1.f, 2.f, 5.f};
float constexpr default_spacing{2.f};
// cells along the side of a chunk with specified point spacing
int cells_for(float const Spacing);
// the allowed spacing closest to specified one
float valid_spacing(float const Spacing);
using chunk_key = std::pair<int, int>;
using pack_key = std::pair<int, int>;
// chunk covering specified point
chunk_key chunk_at(double const X, double const Z);
// corner of the chunk with the lowest coordinates
glm::dvec2 chunk_corner(chunk_key const &Key);
// pack holding specified chunk
pack_key pack_of(chunk_key const &Key);
// corner of the pack with the lowest coordinates; texture coordinates of the terrain start over at it
glm::dvec2 pack_corner(pack_key const &Key);
// name of the file of specified pack
std::string pack_file_name(pack_key const &Key);
struct chunk_data
{
chunk_key key{0, 0};
int cells{0}; // quads along a side; (cells + 1)^2 grid points
std::vector<float> heights; // world y of the grid points, rows of growing z, from the lowest x
std::vector<float> base; // the heights before the modifiers were applied; empty where no modifier reaches the chunk
int paint{0}; // paint samples along a side, less one. 0: the chunk is covered with its first layer alone
std::array<std::uint16_t, max_chunk_layers> layers; // indices of the materials in the terrain palette, no_layer for unused
std::vector<std::uint8_t> weights; // planar, (paint + 1)^2 per used layer, rows like the heights; weights of a sample add up to 255
chunk_data() { layers.fill(no_layer); }
int side() const { return cells + 1; }
double spacing() const { return chunk_size / cells; }
glm::dvec2 corner() const { return chunk_corner(key); }
bool valid() const { return cells > 0 && heights.size() == static_cast<std::size_t>(side()) * side(); }
// number of layers in use, the used ones come first
std::size_t layer_count() const;
float height(int const Ix, int const Iz) const { return heights[static_cast<std::size_t>(Iz) * side() + Ix]; }
// height of the surface at specified world point, the way the full grid is triangulated; points outside are clamped to the chunk
double height_at(double const X, double const Z) const;
// as above, from provided heights of the grid points of the chunk
double height_in(std::vector<float> const &Heights, double const X, double const Z) const;
bool contains(double const X, double const Z) const;
// a flat chunk of specified spacing at specified height, covered with the first material of the palette
static std::shared_ptr<chunk_data> make_flat(chunk_key const &Key, float const Spacing, float const Height);
// the same chunk with the grid of another spacing, heights and paint taken over
std::shared_ptr<chunk_data> resampled(float const Spacing) const;
// weight of specified layer slot at paint sample (Ix, Iz)
std::uint8_t weight(std::size_t const Slot, int const Ix, int const Iz) const;
// layer slot of specified palette index, or -1
int slot_of(std::uint16_t const Layer) const;
// layer slot of the material covering the most of the chunk
std::size_t dominant_slot() const;
// gives the chunk paint samples (with its first layer as the only one painted) if it has none yet
void ensure_paint(int const Samples = default_paint_samples);
// drops the layers left without weight anywhere, and the paint samples altogether when a single layer is left
void compact_layers();
};
using chunk_ptr = std::shared_ptr<chunk_data const>;
// body of water: a level surface within an outline
struct water_body
{
std::string name;
double level{0.0};
std::string material; // empty: the default water material
std::vector<glm::dvec2> outline; // x, z
};
// modifier of the terrain: a corridor along a line (axis of a track or a road) with a formation of specified
// height and width, and slopes from its edges to the ground
struct corridor_point
{
glm::dvec2 position{0.0}; // x, z
double formation{0.0}; // height of the formation
double half_width{3.5}; // from the axis to the edge of the formation
};
struct modifier
{
std::string name;
double slope{1.5}; // horizontal run of the slopes per metre of height
double reach{60.0}; // from the edge of the formation, where the slopes end even if they don't get to the ground
double rounding{1.5}; // over which the slopes bend into the formation and into the ground
std::vector<corridor_point> points;
// rectangle (x, z) the modifier can change the ground in
std::pair<glm::dvec2, glm::dvec2> bounds() const;
};
// modifier prepared for many questions
class modifier_shape
{
public:
explicit modifier_shape(modifier const &Modifier);
// leads Height at specified point the way the modifier does; returns: true if the height changed
bool apply(double const X, double const Z, float &Height) const;
bool reaches(glm::dvec2 const &Min, glm::dvec2 const &Max) const;
private:
modifier const &m_modifier;
double m_widest{0.0};
glm::dvec2 m_min{0.0}, m_max{0.0};
double m_cell{16.0};
std::vector<std::pair<std::uint64_t, std::uint32_t>> m_grid; // (cell key, segment), sorted
};
// material of the palette, and the size (metres) its textures repeat at; 0: the size given by the material, or 8 m
struct layer_def
{
std::string material;
float size{0.f};
};
// description of the terrain, terrain.yaml
struct manifest
{
int version{1};
std::vector<layer_def> layers{{"grass", 0.f}}; // the palette of materials, the first covers new chunks
std::string water_material{"water"}; // of the bodies of water which don't name their own
bool compress{true}; // pack files store their data compressed with zstd
float spacing{default_spacing}; // point spacing of new chunks
std::vector<water_body> water;
std::vector<modifier> modifiers;
};
bool load_manifest(std::string const &Path, manifest &Manifest, std::string *Error = nullptr);
bool save_manifest(std::string const &Path, manifest const &Manifest, std::string *Error = nullptr);
// pack file (.tch), little endian:
//
// header, 64 bytes: 'TCH1', uint32 version, int32 pack x, int32 pack z, uint32 chunks along a side, float chunk size,
// uint64 offset of the chunk index, reserved
// chunk index: per chunk (rows of growing z): uint64 offset of its slice table (0: no chunk), uint32 slice count, uint32 reserved
// slice tables: per slice: uint32 tag, uint32 codec (0: stored, 1: zstd), uint64 offset, uint32 stored size, uint32 size,
// uint32 crc-32 of the data, uint32 reserved
// slice data
//
// slices of a chunk:
// 'CHNK' uint16 cells, uint16 paint samples less one, uint16 layers[8]
// 'HGHT' heights: uint8 encoding (0: int32 base and uint16 steps of height_step above it, delta coded in row order;
// 1: float32), then the values
// 'HBAS' heights before the modifiers, as 'HGHT'
// 'SPLT' paint weights: uint8 per sample per used layer, planar
class pack_file
{
public:
// reads the header and the index of specified file. returns: true on success
bool open(std::string const &Path, std::string *Error = nullptr);
bool has(chunk_key const &Key) const;
// chunks the file holds
std::vector<chunk_key> chunks() const;
// reads specified chunk. returns: the chunk, or null on failure
std::shared_ptr<chunk_data> read(chunk_key const &Key, std::string *Error = nullptr) const;
pack_key key() const { return m_key; }
private:
struct slice
{
std::uint32_t tag{0};
std::uint32_t codec{0};
std::uint64_t offset{0};
std::uint32_t stored{0};
std::uint32_t size{0};
std::uint32_t crc{0};
};
std::string m_path;
pack_key m_key{0, 0};
std::array<std::vector<slice>, pack_side * pack_side> m_slices;
};
// writes specified chunks (all of the same pack) to the pack file. returns: true on success
bool write_pack(std::string const &Path, pack_key const &Key, std::vector<chunk_ptr> const &Chunks, bool const Compress, std::string *Error = nullptr);
// triangles (indices of the points, three to a triangle) covering a simple polygon in the XZ plane
std::vector<std::uint32_t> triangulate(std::vector<glm::dvec2> const &Outline);
// samples the height of the ground outside of a chunk; returns false where it doesn't know
using height_source = std::function<bool(double const X, double const Z, double &Height)>;
// geometry of a chunk at specified level of detail: grid points taken every Stride points of the grid, with a skirt
// along the edges hiding the gaps to the neighbours drawn with another grid. positions relative to Origin
struct mesh
{
gfx::vertex_array vertices;
gfx::index_array indices;
float radius{0.f}; // from the centre of the vertices
glm::dvec3 centre{0.0};
glm::dvec3 low{0.0}, high{0.0}; // world bounds of the grid, the skirts included
};
// strides the grid of a chunk with specified cells can be drawn with
int max_stride(int const Cells);
void build_mesh(chunk_data const &Chunk, int const Stride, glm::dvec3 const &Origin, height_source const &Outside, mesh &Out);
// indices of the mesh of a chunk with specified cells and stride; the same for every chunk of these
void build_indices(int const Cells, int const Stride, gfx::index_array &Out);
// vertices the mesh of a chunk with specified cells and stride has
std::size_t mesh_vertex_count(int const Cells, int const Stride);
} // namespace heightmap

134
shaders/mat_terrain.frag Normal file
View File

@@ -0,0 +1,134 @@
in vec3 f_normal;
in vec2 f_coord;
in vec4 f_pos;
in mat3 f_tbn;
in vec4 f_clip_pos;
in vec4 f_clip_future_pos;
#include <common>
layout(location = 0) out vec4 out_color;
#if MOTIONBLUR_ENABLED
layout(location = 1) out vec4 out_motion;
#endif
// heightmap terrain: up to 8 materials (layers) painted over a chunk, blended by their weights.
// the textures of all layers of the terrain are kept in texture arrays; the table of the chunk tells which layers it has:
// row 0, per layer: index in the arrays, 1 / metres the textures repeat at, has normal map, has specular/gloss map
// row 1, per layer: reflection
// f_coord holds metres from the corner of the pack of chunks, along x and -z
#param (color, 0, 0, 4, diffuse)
#param (diffuse, 1, 0, 1, diffuse)
#param (specular, 1, 1, 1, specular)
#param (reflection, 1, 2, 1, one)
#param (glossiness, 1, 3, 1, glossiness)
// corner of the chunk (x, z) in texture coordinates, 1 / size of the chunk, paint samples along a side less one
#param (placement, 2, 0, 4, zero)
#texture (opaque, 0, RGBA)
uniform sampler2D opaque;
#texture (weights0, 1, RGBA)
uniform sampler2D weights0;
#texture (weights1, 2, RGBA)
uniform sampler2D weights1;
#texture (layerdiffuse, 3, sRGB_A)
uniform highp sampler2DArray layerdiffuse;
#texture (layernormal, 4, RGBA)
uniform highp sampler2DArray layernormal;
#texture (layerspecgloss, 5, RGBA)
uniform highp sampler2DArray layerspecgloss;
#texture (layertable, 6, RGBA)
uniform highp sampler2D layertable;
#define NORMALMAP
#include <light_common.glsl>
#include <apply_fog.glsl>
#include <tonemapping.glsl>
void main()
{
// place within the chunk, 0..1 along x and z, and the paint samples around it
vec2 local = clamp(vec2(f_coord.x - param[2].x, -f_coord.y - param[2].y) * param[2].z, 0.0, 1.0);
float samples = param[2].w;
vec2 paintcoord = (local * samples + 0.5) / (samples + 1.0);
vec4 w0 = texture(weights0, paintcoord);
vec4 w1 = texture(weights1, paintcoord);
float weights[8] = float[8](w0.r, w0.g, w0.b, w0.a, w1.r, w1.g, w1.b, w1.a);
float painted = dot(w0, vec4(1.0)) + dot(w1, vec4(1.0));
// the layers are sampled only where they're painted, so the gradients are taken here, where every pixel gets them
vec2 coorddx = dFdx(f_coord);
vec2 coorddy = dFdy(f_coord);
vec3 albedo = vec3(0.0);
vec3 normal = vec3(0.0);
float reflectivity = 0.0;
float specularity = 0.0;
float gloss = 0.0;
float metal = 0.0;
float total = 0.0;
for (int i = 0; i < 8; ++i)
{
// a chunk without weights shows its first layer
float weight = (painted < 0.004 ? (i == 0 ? 1.0 : 0.0) : weights[i]);
if (weight < 0.004)
continue;
vec4 info = texelFetch(layertable, ivec2(i, 0), 0);
vec4 extra = texelFetch(layertable, ivec2(i, 1), 0);
vec3 coord = vec3(f_coord * info.y, info.x);
vec2 dx = coorddx * info.y;
vec2 dy = coorddy * info.y;
albedo += textureGrad(layerdiffuse, coord, dx, dy).rgb * weight;
vec3 layernormal_ts = vec3(0.0, 0.0, 1.0);
float layerreflection = extra.x;
if (info.z > 0.5)
{
vec4 normalmap = textureGrad(layernormal, coord, dx, dy);
layernormal_ts.xy = normalmap.rg * 2.0 - 1.0;
layernormal_ts.z = sqrt(1.0 - clamp(dot(layernormal_ts.xy, layernormal_ts.xy), 0.0, 1.0));
layerreflection *= normalmap.a;
}
normal += layernormal_ts * weight;
if (info.w > 0.5)
{
vec4 specgloss = textureGrad(layerspecgloss, coord, dx, dy);
specularity += specgloss.r * weight;
gloss += specgloss.g * weight;
metal += specgloss.b * weight;
}
else
{
gloss += weight;
}
reflectivity += layerreflection * weight;
total += weight;
}
total = max(total, 0.004);
albedo /= total;
specularity /= total;
metal /= total;
reflectivity = param[1].z * reflectivity / total;
glossiness = (gloss / total) * abs(param[1].w);
metalic = metal;
vec3 fragnormal = normalize(f_tbn * normalize(normal));
vec3 fragcolor = ambient;
fragcolor = apply_lights(fragcolor, fragnormal, albedo, reflectivity, specularity, shadow_tone);
vec4 color = vec4(apply_fog(fragcolor), alpha_mult);
#if POSTFX_ENABLED
out_color = color;
#else
out_color = tonemap(color);
#endif
#if MOTIONBLUR_ENABLED
{
vec2 a = (f_clip_future_pos.xy / f_clip_future_pos.w) * 0.5 + 0.5;
vec2 b = (f_clip_pos.xy / f_clip_pos.w) * 0.5 + 0.5;
out_motion = vec4(a - b, 0.0f, 0.0f);
}
#endif
}

View File

@@ -134,6 +134,7 @@ state_serializer::deserialize_begin( std::string const &Scenariofile ) {
{ "time", &state_serializer::deserialize_time },
{ "trainset", &state_serializer::deserialize_trainset },
{ "terrain", &state_serializer::deserialize_terrain },
{ "heightmap_terrain", &state_serializer::deserialize_heightmapterrain },
{ "editorterrain", &state_serializer::deserialize_editorterrain },
{ "endtrainset", &state_serializer::deserialize_endtrainset },
{ "reversed", &state_serializer::deserialize_reversed } };
@@ -1296,37 +1297,43 @@ state_serializer::deserialize_terrain(cParser &Input, scene::scratch_data &Scrat
}
void
state_serializer::deserialize_editorterrain(cParser &Input, scene::scratch_data &Scratchpad)
state_serializer::deserialize_heightmapterrain(cParser &Input, scene::scratch_data &Scratchpad)
{
// editor-authored streaming terrain. format:
// editorterrain <folder> <cells> <cellsize> <radius> [texture] endeditorterrain
// the global streamer loads its 16-bit chunk files around the camera in every mode
std::string folder;
int cells = 32;
float cellsize = 2.0f;
int radius = 4;
Input.getTokens(4);
Input >> folder >> cells >> cellsize >> radius;
std::string texture { Input.getToken<std::string>() };
if( texture == "endeditorterrain" ) {
texture.clear();
// heightmap terrain, kept in terrain/<name>/ of the simulator. format:
// heightmap_terrain [name] endheightmap_terrain
// without the name the terrain is named after the scenery file. the chunks are loaded around the camera in every mode
std::string name;
while( true ) {
auto const token { Input.getToken<std::string>( false ) };
if( token.empty() || token == "endheightmap_terrain" ) {
break;
}
if( name.empty() ) {
name = token;
}
}
else {
skip_until(Input, "endeditorterrain");
if( name.empty() ) {
name = Global.SceneryFile;
auto const slash { name.find_last_of( "/\\" ) };
if( slash != std::string::npos ) {
name.erase( 0, slash + 1 );
}
erase_extension( name );
}
scene::Layers.terrain_directive(true);
if (!folder.empty() && cells > 0 && cellsize > 0.0f)
{
EditorTerrain.directory(folder);
EditorTerrain.configure(cells, cellsize, radius < 0 ? 0 : radius, 0.0f, texture);
EditorTerrain.active(true);
WriteLog("Editor terrain stream enabled: " + folder + " (cells " + std::to_string(cells)
+ ", cellsize " + std::to_string(cellsize) + ", radius " + std::to_string(radius) + ")",
logtype::generic);
if( false == name.empty() ) {
EditorTerrain.open( name );
}
}
void
state_serializer::deserialize_editorterrain(cParser &Input, scene::scratch_data &Scratchpad)
{
// the chunk files of the first editor terrain were replaced with the heightmap terrain before they were used in sceneries
skip_until(Input, "endeditorterrain");
WriteLog("Bad scenario: obsolete \"editorterrain\" directive ignored, the editor terrain is \"heightmap_terrain\" now", logtype::generic);
}
void
state_serializer::deserialize_endtrainset( cParser &Input, scene::scratch_data &Scratchpad ) {
@@ -1711,14 +1718,9 @@ state_serializer::export_as_text(std::string const &Scenariofile) const {
scmfile << "// sounds\n";
Region->export_as_text( scmfile );
// editor-authored streaming terrain: emit a directive pointing at its 16-bit chunk folder so the
// scenery streams it on load (in every mode)
// heightmap terrain: the directive which opens its folder, so the scenery streams it on load (in every mode)
if( EditorTerrain.active() ) {
scmfile << "// editor terrain\neditorterrain "
<< EditorTerrain.directory() << ' '
<< EditorTerrain.cells() << ' '
<< EditorTerrain.cellsize() << ' '
<< EditorTerrain.radius() << " endeditorterrain\n";
scmfile << "// heightmap terrain\nheightmap_terrain " << EditorTerrain.name() << " endheightmap_terrain\n";
}
scmfile << "// modified objects\ninclude " << filename << "_export_dirty.scm\n";
@@ -1768,16 +1770,9 @@ state_serializer::export_as_text(std::string const &Scenariofile) const {
scmfile << "// sounds\n";
Region->export_as_text( scmfile );
// editor-authored streaming terrain: emit a directive pointing at its 16-bit chunk folder so the
// scenery streams it on load (in every mode)
// heightmap terrain: the directive which opens its folder, so the scenery streams it on load (in every mode)
if( EditorTerrain.active() ) {
scmfile << "// editor terrain\neditorterrain "
<< EditorTerrain.directory() << ' '
<< EditorTerrain.cells() << ' '
<< EditorTerrain.cellsize() << ' '
<< EditorTerrain.radius()
<< ( EditorTerrain.texture().empty() ? "" : " " + EditorTerrain.texture() )
<< " endeditorterrain\n";
scmfile << "// heightmap terrain\nheightmap_terrain " << EditorTerrain.name() << " endheightmap_terrain\n";
}
scmfile << "// modified objects\ninclude " << filename << "_export_dirty.scm\n";
@@ -1921,7 +1916,7 @@ std::pair<int, int> state_serializer::preview_include(std::string const &Directi
static std::unordered_map<std::string, std::string> const statementends {
{ "event", "endevent" }, { "trainset", "endtrainset" }, { "isolated", "endisolated" }, { "area", "endarea" }, { "assignment", "endassignment" },
{ "atmo", "endatmo" }, { "camera", "endcamera" }, { "config", "endconfig" }, { "description", "enddescription" }, { "light", "endlight" },
{ "sky", "endsky" }, { "test", "endtest" }, { "time", "endtime" }, { "terrain", "endterrain" }, { "editorterrain", "endeditorterrain" } };
{ "sky", "endsky" }, { "test", "endtest" }, { "time", "endtime" }, { "terrain", "endterrain" }, { "heightmap_terrain", "endheightmap_terrain" }, { "editorterrain", "endeditorterrain" } };
cParser parser(Directive, cParser::buffer_TEXT, Global.asCurrentSceneryPath, Global.bLoadTraction);
// the template is processed with the placement its directive is going to be loaded with

View File

@@ -80,6 +80,7 @@ private:
void deserialize_time( cParser &Input, scene::scratch_data &Scratchpad );
void deserialize_trainset( cParser &Input, scene::scratch_data &Scratchpad );
void deserialize_terrain( cParser &Input, scene::scratch_data &Scratchpad );
void deserialize_heightmapterrain( cParser &Input, scene::scratch_data &Scratchpad );
void deserialize_editorterrain( cParser &Input, scene::scratch_data &Scratchpad );
void deserialize_endtrainset( cParser &Input, scene::scratch_data &Scratchpad );
void deserialize_reversed( cParser &Input, scene::scratch_data &Scratchpad );