mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-11-07 08:48:01 +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:
@@ -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()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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};
|
||||
|
||||
@@ -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...")))
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
|
||||
@@ -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
30
ref/zstd/LICENSE
Normal 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
9
ref/zstd/README.txt
Normal 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
45229
ref/zstd/zstd.c
Normal file
File diff suppressed because it is too large
Load Diff
3089
ref/zstd/zstd.h
Normal file
3089
ref/zstd/zstd.h
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
/*
|
||||
|
||||
423
rendering/opengl33terrain.cpp
Normal file
423
rendering/opengl33terrain.cpp
Normal 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();
|
||||
}
|
||||
85
rendering/opengl33terrain.h
Normal file
85
rendering/opengl33terrain.h
Normal 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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
@@ -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
1141
scene/heightmapterrain.cpp
Normal file
File diff suppressed because it is too large
Load Diff
241
scene/heightmapterrain.h
Normal file
241
scene/heightmapterrain.h
Normal 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
134
shaders/mat_terrain.frag
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 );
|
||||
|
||||
Reference in New Issue
Block a user