diff --git a/CMakeLists.txt b/CMakeLists.txt index 25104d32..31cb327c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -394,6 +394,7 @@ if (WITH_OPENGL_MODERN) "rendering/opengl46/opengl46precipitation.cpp" "rendering/opengl46/opengl46particles.cpp" "rendering/opengl46/opengl46terrain.cpp" + "rendering/opengl46/opengl46grass.cpp" "rendering/opengl46/opengl46renderer.cpp") set(DEFINITIONS ${DEFINITIONS} "WITH_OPENGL_MODERN") endif() diff --git a/application/editormode.cpp b/application/editormode.cpp index 96759802..d7bd72bd 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1763,6 +1763,17 @@ void editor_mode::render_terrain_ui() ImGui::TextDisabled("%s", STR_C("A chunk holds up to 8 materials")); } + if (ImGui::CollapsingHeader(STR_C("Grass"), m_terrain_tool == terrain_tool::grass ? ImGuiTreeNodeFlags_DefaultOpen : 0)) + render_grass_ui(); + + if (ImGui::CollapsingHeader(STR_C("Holes"), m_terrain_tool == terrain_tool::holes ? ImGuiTreeNodeFlags_DefaultOpen : 0)) + { + ImGui::TextWrapped("%s", STR_C("Openings in the ground for the portals of tunnels, cuttings under bridges and the like. " + "With the Holes tool LMB cuts the ground under the brush and Shift+LMB closes it; " + "the brush stays on the ground also over an opening. An area outlined with the Fill tool can be cut or closed in the Fill section.")); + ImGui::TextDisabled("%s", STR_C("The holes follow the ground every metre (every grid point of finer chunks), whatever the drawn grid")); + } + if (ImGui::CollapsingHeader(STR_C("Fill"), m_terrain_tool == terrain_tool::fill ? ImGuiTreeNodeFlags_DefaultOpen : 0)) { auto const &layers{m_streamer.manifest().layers}; @@ -1790,6 +1801,49 @@ void editor_mode::render_terrain_ui() ImGui::SameLine(); if (ImGui::Button(STR_C("Clear the outline##fill"))) m_paint_outline.clear(); + // the same outline for the holes and the grass + std::vector outline; + for (auto const &point : m_paint_outline) + outline.emplace_back(point.x, point.z); + ImGui::BeginDisabled(outline.size() < 3); + if (ImGui::Button(STR_C("Cut a hole"))) + { + auto const cut = m_streamer.cut(outline, true); + m_terrain_status = cut ? std::string(STR_C("Hole cut in the outlined area")) : std::string(STR_C("Nothing cut: no chunk under the outline")); + if (cut) + { + ground_tiles.clear(); + forget_ground_shapes(); + } + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Cuts the ground within the outline away, e.g. for the portal of a tunnel")); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Close the hole"))) + { + auto const closed = m_streamer.cut(outline, false); + m_terrain_status = closed ? std::string(STR_C("Hole closed in the outlined area")) : std::string(STR_C("No hole under the outline")); + if (closed) + { + ground_tiles.clear(); + forget_ground_shapes(); + } + } + auto const &grasskinds = m_streamer.grass_kinds(); + ImGui::BeginDisabled(grasskinds.empty()); + if (ImGui::Button(STR_C("Grow grass"))) + { + auto const kind = static_cast(std::clamp(m_grass_kind, 0, std::max(0, static_cast(grasskinds.size()) - 1))); + m_terrain_status = m_streamer.grass(outline, kind, false) ? format(STR_C("Grass grown: %s"), grasskinds[kind].name.c_str()) + : std::string(STR_C("Nothing grown: no chunk under the outline, or no room for another kind of grass")); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Grows the kind of grass chosen in the Grass section within the outline")); + ImGui::EndDisabled(); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Clear the grass"))) + m_terrain_status = m_streamer.grass(outline, 0, true) ? std::string(STR_C("Grass taken away from the outlined area")) : std::string(STR_C("No grass under the outline")); + ImGui::EndDisabled(); } if (ImGui::CollapsingHeader(STR_C("Water"), m_terrain_tool == terrain_tool::water ? ImGuiTreeNodeFlags_DefaultOpen : 0)) @@ -2000,8 +2054,12 @@ void editor_mode::draw_terrain_overlay() } }; - if (terrain_brush() && hovering) + // the holes and the grass are aimed at the ground itself, also over an opening and behind the blades + glm::dvec3 brushcentre = cursor; + bool const onground = (m_terrain_tool == terrain_tool::holes || m_terrain_tool == terrain_tool::grass) && false == ImGui::GetIO().WantCaptureMouse && terrain_cursor(brushcentre); + if (terrain_brush() && (hovering || onground)) { + auto const cursor = onground ? brushcentre : Camera.Pos + offset; std::vector circle; for (int i = 0; i < 64; ++i) { @@ -2011,6 +2069,8 @@ void editor_mode::draw_terrain_overlay() circle.emplace_back(x, ground(x, z, cursor.y) + 0.1, z); } auto const color = m_terrain_tool == terrain_tool::paint ? IM_COL32(120, 230, 120, 220) + : m_terrain_tool == terrain_tool::grass ? IM_COL32(170, 255, 90, 220) + : m_terrain_tool == terrain_tool::holes ? IM_COL32(255, 90, 90, 230) : m_terrain_tool == terrain_tool::smooth ? IM_COL32(120, 200, 255, 220) : m_terrain_tool == terrain_tool::restore ? IM_COL32(230, 120, 230, 220) : IM_COL32(255, 180, 80, 220); @@ -3192,9 +3252,15 @@ void editor_mode::handle_terrain_brush(double Deltatime) { // world point under the cursor (Mouse_Position is camera-relative, like the brush placement uses) glm::dvec3 const offset = GfxRenderer->Mouse_Position(); - if (glm::length(offset) < 1e-3) + glm::dvec3 world = Camera.Pos + offset; + if (m_terrain_tool == terrain_tool::holes || m_terrain_tool == terrain_tool::grass) + { + // the ground itself: the cursor may be over a hole (and what's seen through it), or over the blades of the grass + if (false == terrain_cursor(world)) + return; + } + else if (glm::length(offset) < 1e-3) return; - glm::dvec3 const world = Camera.Pos + offset; // only when the cursor is over the terrain (avoids editing on a stale depth read) if (terrain_at(world.x, world.z) == nullptr) return; @@ -3227,6 +3293,22 @@ void editor_mode::handle_terrain_brush(double Deltatime) case terrain_tool::restore: shaped = m_streamer.restore_stitching(world.x, world.z, m_terrain_brush_radius, std::min(1.0, rate * 0.5)); break; + case terrain_tool::holes: + // the ground there is no longer ground for the tools which ask about it + shaped = m_streamer.cut(world.x, world.z, m_terrain_brush_radius, false == Global.shiftState); + break; + case terrain_tool::grass: + { + if (m_streamer.grass_kinds().empty() && false == Global.shiftState) + { + // the first kind of the presets, so the brush grows something + m_streamer.add_grass_kind(heightmap::grass_presets().front()); + m_grass_kind = 0; + } + m_grass_kind = std::clamp(m_grass_kind, 0, std::max(0, static_cast(m_streamer.grass_kinds().size()) - 1)); + m_streamer.grass(world.x, world.z, m_terrain_brush_radius, std::min(1.0, rate * 0.25), static_cast(m_grass_kind), Global.shiftState); + break; + } default: break; } @@ -3238,6 +3320,115 @@ void editor_mode::handle_terrain_brush(double Deltatime) } } +bool editor_mode::terrain_cursor(glm::dvec3 &World) const +{ + // along the ray to what the depth under the cursor shows, or, with nothing there, the ray through the cursor + glm::dvec3 const offset = GfxRenderer->Mouse_Position(); + if (glm::length(offset) > 1e-3) + return m_streamer.raycast(Camera.Pos, glm::normalize(offset), 5000.0, World); + ImGuiIO const &io = ImGui::GetIO(); + if (io.DisplaySize.x <= 0.0f || io.DisplaySize.y <= 0.0f) + return false; + auto const inverse = glm::inverse(editor_mode::projection_matrix(io.DisplaySize.x / io.DisplaySize.y) * GfxRenderer->Camera_View_Matrix()); + glm::vec2 const ndc{io.MousePos.x / io.DisplaySize.x * 2.0f - 1.0f, 1.0f - io.MousePos.y / io.DisplaySize.y * 2.0f}; + // two points of the ray, whatever the depth range of the projection + auto const point = [&](float const Depth) { + auto const clip = inverse * glm::vec4(ndc, Depth, 1.0f); + return glm::dvec3{glm::vec3{clip} / clip.w}; + }; + auto const direction = point(0.5f) - point(0.0f); + if (glm::length(direction) < 1e-9) + return false; + return m_streamer.raycast(GfxRenderer->Camera_Position() + point(0.0f), glm::normalize(direction), 5000.0, World); +} + +void editor_mode::render_grass_ui() +{ + auto const &kinds = m_streamer.grass_kinds(); + if (kinds.empty()) + ImGui::TextWrapped("%s", STR_C("No kinds of grass yet. Add one of the presets below; the first stroke of the brush adds a meadow.")); + m_grass_kind = std::clamp(m_grass_kind, 0, std::max(0, static_cast(kinds.size()) - 1)); + for (std::size_t i = 0; i < kinds.size(); ++i) + { + ImGui::PushID(static_cast(i)); + // a swatch of the colours of the kind, from the root to the tip + auto *drawlist = ImGui::GetWindowDrawList(); + auto const corner = ImGui::GetCursorScreenPos(); + auto const height = ImGui::GetTextLineHeight(); + auto const colour = [](glm::vec3 const &Colour) { return ImGui::ColorConvertFloat4ToU32(ImVec4(Colour.r, Colour.g, Colour.b, 1.0f)); }; + drawlist->AddRectFilledMultiColor(corner, ImVec2(corner.x + height, corner.y + height), colour(kinds[i].tip), colour(kinds[i].tip), colour(kinds[i].base), colour(kinds[i].base)); + ImGui::Dummy(ImVec2(height, height)); + ImGui::SameLine(); + if (ImGui::Selectable(format(STR_C("%zu: %s, %.2f m, %.0f blades/m2"), i, kinds[i].name.c_str(), kinds[i].height, kinds[i].density).c_str(), static_cast(i) == m_grass_kind)) + m_grass_kind = static_cast(i); + ImGui::PopID(); + } + if (false == kinds.empty()) + { + // the chosen kind, given to the palette once a field is left + if (m_grass_edit_of != m_grass_kind || false == m_grass_edit_active) + { + m_grass_edit = kinds[m_grass_kind]; + m_grass_edit_of = m_grass_kind; + } + auto &kind = m_grass_edit; + bool changed = false, active = false; + auto const field = [&](bool const Edited) { + changed |= ImGui::IsItemDeactivatedAfterEdit() || (Edited && false == ImGui::IsItemActive()); + active |= ImGui::IsItemActive(); + }; + char name[64]; + std::snprintf(name, sizeof(name), "%s", kind.name.c_str()); + ImGui::SetNextItemWidth(160.0f); + if (ImGui::InputText(STR_C("Name##grass"), name, sizeof(name))) + kind.name = name; + field(false); + ImGui::SetNextItemWidth(200.0f); + field(ImGui::ColorEdit3(STR_C("Root colour"), &kind.base.r, ImGuiColorEditFlags_NoInputs)); + ImGui::SameLine(); + field(ImGui::ColorEdit3(STR_C("Tip colour"), &kind.tip.r, ImGuiColorEditFlags_NoInputs)); + ImGui::SetNextItemWidth(120.0f); + field(ImGui::DragFloat(STR_C("Height (m)##grass"), &kind.height, 0.005f, 0.02f, 2.0f, "%.2f")); + ImGui::SetNextItemWidth(120.0f); + field(ImGui::SliderFloat(STR_C("Height varies by"), &kind.variation, 0.0f, 1.0f, "%.2f")); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Share of the height the blades differ by, either way")); + ImGui::SetNextItemWidth(120.0f); + field(ImGui::DragFloat(STR_C("Blade width (m)"), &kind.width, 0.0002f, 0.002f, 0.05f, "%.3f m")); + ImGui::SetNextItemWidth(120.0f); + field(ImGui::DragFloat(STR_C("Density (blades/m2)"), &kind.density, 0.5f, 1.0f, 300.0f, "%.0f")); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Blades on a square metre painted fully. The renderer draws fewer further away, and wider;\n" + "gfx.grass.distance and gfx.grass.density in eu07.ini set how far and how many")); + ImGui::SetNextItemWidth(120.0f); + field(ImGui::SliderFloat(STR_C("Lean"), &kind.bend, 0.0f, 1.0f, "%.2f")); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("How far the tips of the blades lean over, share of their height; the wind adds to it")); + m_grass_edit_active = active; + if (changed) + { + kind.height = std::max(0.02f, kind.height); + kind.width = std::max(0.002f, kind.width); + kind.density = std::max(1.0f, kind.density); + m_streamer.grass_kind(static_cast(m_grass_kind), kind); + } + } + // the presets, added to the palette + ImGui::TextUnformatted(STR_C("Add:")); + for (auto const &preset : heightmap::grass_presets()) + { + ImGui::SameLine(); + if (ImGui::SmallButton(STR(preset.name.c_str()).c_str())) + { + m_grass_kind = m_streamer.add_grass_kind(preset); + m_grass_edit_of = -1; + } + } + ImGui::TextDisabled("%s", STR_C("A chunk holds up to 4 kinds of grass. LMB grows the chosen kind, Shift+LMB takes all grass away")); + if (false == Global.gfx_grass) + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", STR_C("The grass isn't drawn: gfx.grass is off")); +} + void editor_mode::fill_terrain_area() { auto const &layers = m_streamer.manifest().layers; diff --git a/application/editormode.h b/application/editormode.h index 4ef8dac9..653d70ae 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -436,14 +436,21 @@ class editor_mode : public application_mode, private editor_track::observer spacing, // selects chunks (Ctrl: adds or takes away one), whose point spacing is then converted water, // outlines a body of water restore, // takes the touch-ups over the modifiers back - fill // outlines an area filled with a material + fill, // outlines an area filled with a material + holes, // cuts holes in the ground (portals of tunnels), Shift: closes them + grass // grows the chosen kind of grass, Shift: takes the grass away }; 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::level || m_terrain_tool == terrain_tool::paint || - m_terrain_tool == terrain_tool::restore; + m_terrain_tool == terrain_tool::restore || m_terrain_tool == terrain_tool::holes || m_terrain_tool == terrain_tool::grass; } + // the point of the ground of the terrain under the cursor, found along the ray through the cursor, so it's found also over + // a hole and behind the grass. returns: false if the ray meets no terrain + bool terrain_cursor(glm::dvec3 &World) const; + // the grass of the terrain: the palette, the kind grown, presets + void render_grass_ui(); // converts the point spacing of the selected chunks to m_terrain_convert_spacing void convert_selected_chunks(); // takes the height of whatever is under the cursor (terrain, track, model) for the target of the level brush. returns: false if there's nothing @@ -552,6 +559,10 @@ class editor_mode : public application_mode, private editor_track::observer bool m_terrain_stroke{false}; // a stroke of the brush is under way bool m_terrain_picking{false}; // the next click picks the target height int m_terrain_layer{0}; // entry of the palette painted + int m_grass_kind{0}; // entry of the grass palette grown + heightmap::grass_def m_grass_edit; // the chosen kind of grass while it's edited + int m_grass_edit_of{-1}; // the kind in m_grass_edit, -1: none + bool m_grass_edit_active{false}; // a field of the chosen kind is being dragged 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 float m_terrain_layer_size{0.0f}; // repeat of the chosen palette entry, while it's dragged diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 2a7909bd..e163b632 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -115,6 +115,11 @@ editor_mode::selftest_result editor_mode::selftest_dispatch(std::string const &c // terrainfill ... the area outlined filled, as the fill tool does it // terrainturn the textures of the palette entry turned // terrainwater [size] material and repeat of a body of water; terrainwaterdefault +// terraincut <1 cut|0 close> a stroke of the holes brush; terraincutarea <1|0> ... the area outlined +// terraingrasskind a preset added to the grass palette; terraingrass [erase 0|1] a stroke +// of the grass brush; terraingrassarea ... the area outlined +// terraincursor logs the point of the ground under the cursor, found along the ray (over holes too) +// terraintool the tool the left button works on the terrain, with a brush of 3 m // orthophoto [in scene 0|1] the imagery layer on, placed and fitted as given // join [radius] [variant] [transitions 0|1] the free ends at the points joined, as a drag from one to the other // layto from the free end at the first point towards the second, as a drag in the layout diff --git a/application/editormodeselftestterrain.cpp b/application/editormodeselftestterrain.cpp index 8f2c66ca..3380a8a1 100644 --- a/application/editormodeselftestterrain.cpp +++ b/application/editormodeselftestterrain.cpp @@ -408,6 +408,69 @@ editor_mode::selftest_result editor_mode::selftest_terrain(std::string const &co WriteLog("SELFTEST terrainadd: " + m_terrain_status); return selftest_result::handled; } + if (command == "terraincut") + { + double x{0.0}; + double z{0.0}; + double radius{2.0}; + int cut{1}; + words >> x >> z >> radius >> cut; + auto const changed{m_streamer.cut(x, z, radius, cut != 0)}; + forget_ground(); + WriteLog(format("SELFTEST terraincut %.1f %.1f: %s", x, z, changed ? "changed" : "nothing changed")); + return selftest_result::handled; + } + if (command == "terraincutarea" || command == "terraingrassarea") + { + int value{1}; + words >> value; + std::vector outline; + double x, z; + while (words >> x >> z) + outline.emplace_back(x, z); + auto const changed{command == "terraincutarea" ? m_streamer.cut(outline, value != 0) : m_streamer.grass(outline, static_cast(std::max(0, value)), value < 0)}; + forget_ground(); + WriteLog(format("SELFTEST %s: %s", command.c_str(), changed ? "changed" : "nothing changed")); + return selftest_result::handled; + } + if (command == "terraingrasskind") + { + std::string name; + std::getline(words >> std::ws, name); + for (auto const &preset : heightmap::grass_presets()) + if (preset.name == name) + m_grass_kind = m_streamer.add_grass_kind(preset); + WriteLog(format("SELFTEST terraingrasskind %s: %zu kinds", name.c_str(), m_streamer.grass_kinds().size())); + return selftest_result::handled; + } + if (command == "terraingrass") + { + double x{0.0}; + double z{0.0}; + double radius{5.0}; + int kind{0}; + int erase{0}; + words >> x >> z >> radius >> kind >> erase; + auto const changed{m_streamer.grass(x, z, radius, 1.0, static_cast(kind), erase != 0)}; + WriteLog(format("SELFTEST terraingrass %.1f %.1f: %s", x, z, changed ? "changed" : "nothing changed")); + return selftest_result::handled; + } + if (command == "terraintool") + { + std::string name; + words >> name; + m_terrain_tool = name == "holes" ? terrain_tool::holes : name == "grass" ? terrain_tool::grass : name == "paint" ? terrain_tool::paint : name == "sculpt" ? terrain_tool::sculpt : terrain_tool::none; + m_terrain_brush_radius = 3.0f; + WriteLog("SELFTEST terraintool " + name); + return selftest_result::handled; + } + if (command == "terraincursor") + { + glm::dvec3 world; + auto const found{terrain_cursor(world)}; + WriteLog(format("SELFTEST terraincursor: %s %.2f %.2f %.2f", found ? "ground" : "none", world.x, world.y, world.z)); + return selftest_result::handled; + } if (command == "terrainprobe") { double x{0.0}; @@ -435,6 +498,18 @@ editor_mode::selftest_result editor_mode::selftest_terrain(std::string const &co if (false == data.adjust.empty()) adjust = data.height_in(data.adjust, x, z); grid = format(", spacing %.1f, paint %d, base %s %.3f, touch-up %.3f", data.spacing(), data.paint, data.base.empty() ? "none" : "", base, adjust); + // the holes and the grass at the point + grid += format(", holes %d%s, grass kinds %zu", data.holes, data.hole_at(x, z) ? " (cut here)" : "", data.grass_count()); + if (data.grass > 0) + { + auto const corner{data.corner()}; + auto const ix{std::clamp(static_cast(std::lround((x - corner.x) / heightmap::chunk_size * data.grass)), 0, data.grass)}; + auto const iz{std::clamp(static_cast(std::lround((z - corner.y) / heightmap::chunk_size * data.grass)), 0, data.grass)}; + auto const plane{static_cast(data.grass + 1) * (data.grass + 1)}; + grid += ", grass"; + for (std::size_t slot = 0; slot < data.grass_count(); ++slot) + grid += format(" %u:%d", data.grass_kinds[slot], data.grass_density[slot * plane + static_cast(iz) * (data.grass + 1) + ix]); + } if (data.paint > 0) { // weights at the paint sample nearest the point diff --git a/application/editormodeviewport.cpp b/application/editormodeviewport.cpp index efa81240..2e3dff19 100644 --- a/application/editormodeviewport.cpp +++ b/application/editormodeviewport.cpp @@ -229,10 +229,14 @@ void editor_mode::work_area_tools(std::vector &Buttons) Buttons.push_back({icon::profile, STR_C("Level"), nullptr, STR_C("LMB leads the terrain under the brush to the target height, Ctrl+LMB takes the target height from what's under the cursor"), chosen(terrain_tool::level), choose(terrain_tool::level)}); 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::grass, STR_C("Grass"), nullptr, STR_C("LMB grows the kind of grass chosen in the grass palette, Shift+LMB takes the grass away"), chosen(terrain_tool::grass), + choose(terrain_tool::grass)}); 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::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)}); Buttons.push_back({icon::count, STR_C("Fill"), nullptr, STR_C("LMB adds a point of the outline of an area to fill with a material, Shift+LMB takes the last one back"), chosen(terrain_tool::fill), choose(terrain_tool::fill)}); + Buttons.push_back({icon::hole, STR_C("Holes"), nullptr, STR_C("LMB cuts a hole in the terrain under the brush (portals of tunnels), Shift+LMB closes it"), chosen(terrain_tool::holes), + choose(terrain_tool::holes)}); 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}); diff --git a/editor/editorIcons.hpp b/editor/editorIcons.hpp index eec9f4a8..3a5f1ba0 100644 --- a/editor/editorIcons.hpp +++ b/editor/editorIcons.hpp @@ -57,6 +57,9 @@ enum class icon work_tracks, work_roads, work_terrain, + // the tools of the terrain added later, at the end so the other cells stay where they were + hole, + grass, count }; diff --git a/editor/editorTerrain.cpp b/editor/editorTerrain.cpp index 16daaf1e..8b39ba9e 100644 --- a/editor/editorTerrain.cpp +++ b/editor/editorTerrain.cpp @@ -10,6 +10,15 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "editor/editorTerrain.hpp" +#include + +namespace +{ +std::atomic serials{0}; +} + +editor_terrain::editor_terrain(heightmap::chunk_ptr Data) : m_data(std::move(Data)), m_serial(++serials) {} + heightmap::chunk_data &editor_terrain::edit(bool const Shape, bool const Paint) { // a worker may still be making a mesh of the current data, so a copy is changed instead @@ -24,7 +33,7 @@ heightmap::chunk_data &editor_terrain::edit(bool const Shape, bool const Paint) return const_cast(*m_data); } -void editor_terrain::replace(heightmap::chunk_ptr Data, bool const Shape, bool const Paint) +void editor_terrain::replace(heightmap::chunk_ptr Data, bool const Shape, bool const Paint, bool const Grass) { m_data = std::move(Data); m_modified = true; @@ -32,6 +41,8 @@ void editor_terrain::replace(heightmap::chunk_ptr Data, bool const Shape, bool c ++m_shapeversion; if (Paint) ++m_paintversion; + if (Grass) + ++m_grassversion; } glm::dvec3 editor_terrain::centre() const diff --git a/editor/editorTerrain.hpp b/editor/editorTerrain.hpp index d26c4c52..310e0fc5 100644 --- a/editor/editorTerrain.hpp +++ b/editor/editorTerrain.hpp @@ -28,14 +28,18 @@ namespace scene { class basic_section; } class editor_terrain { public: - explicit editor_terrain(heightmap::chunk_ptr Data) : m_data(std::move(Data)) {} + explicit editor_terrain(heightmap::chunk_ptr Data); 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); + // puts provided data in place of the current one; Grass: the grass changed + void replace(heightmap::chunk_ptr Data, bool const Shape, bool const Paint, bool const Grass = false); + // tells this chunk from any other, also from the same chunk loaded again + std::uint64_t serial() const { return m_serial; } + // changes with every change of what the grass of the chunk is made of: the ground, the holes and the grass itself + std::uint64_t grass_version() const { return m_shapeversion + m_paintversion + m_grassversion; } heightmap::chunk_key key() const { return m_data->key; } // true if (X,Z) lies within the chunk @@ -64,6 +68,8 @@ class editor_terrain 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 + std::uint64_t m_grassversion{1}; // grows with every change of the grass + std::uint64_t m_serial{0}; // drawn state, managed by the streamer int m_slot{-1}; // geometry slot holding the mesh diff --git a/editor/editorTerrainStreamer.cpp b/editor/editorTerrainStreamer.cpp index 8f799c46..c2254e89 100644 --- a/editor/editorTerrainStreamer.cpp +++ b/editor/editorTerrainStreamer.cpp @@ -145,6 +145,8 @@ void terrain_streamer::close() m_active = false; m_name.clear(); m_directory.clear(); + if (GfxRenderer) + GfxRenderer->Terrain_Grass({}, {}); } void terrain_streamer::reset() @@ -172,6 +174,8 @@ void terrain_streamer::reset() m_name.clear(); m_directory.clear(); m_hascamera = false; + if (GfxRenderer) + GfxRenderer->Terrain_Grass({}, {}); } // workers @@ -370,7 +374,9 @@ void terrain_streamer::update_material(editor_terrain &Chunk) auto const packcorner{heightmap::pack_corner(heightmap::pack_of(data.key))}; glm::vec3 const placement{static_cast(corner.x - packcorner.x), static_cast(corner.y - packcorner.y), static_cast(heightmap::chunk_size)}; auto const painted{data.paint > 0 && count > 1}; - auto const material{GfxRenderer->Terrain_Material(Chunk.m_material, layers, painted ? data.paint : 0, painted ? data.weights.data() : nullptr, placement)}; + auto const holes{data.has_holes()}; + auto const material{GfxRenderer->Terrain_Material(Chunk.m_material, layers, painted ? data.paint : 0, painted ? data.weights.data() : nullptr, placement, holes ? data.holes : 0, + holes ? data.hole_mask.data() : nullptr)}; if (Chunk.m_material != null_handle && Chunk.m_material != material) GfxRenderer->Terrain_Release(Chunk.m_material); Chunk.m_material = material; @@ -385,8 +391,29 @@ void terrain_streamer::upload(editor_terrain &Chunk, heightmap::mesh &Mesh, int auto const triangles{Mesh.indices.size() / 3}; gfx::userdata_array nouserdata; - // the mesh goes to a geometry slot made for its grid; a slot of the same kind is refilled in place - auto fits = [&](geometry_slot const &Slot) { return Slot.cells == cells && Slot.stride == Stride; }; + // the mesh goes to a geometry slot made for its grid; a slot of the same kind is refilled in place. the indices of a chunk + // with holes are its own, its slot gets them anew with the vertices + auto fits = [&](geometry_slot const &Slot) { return Slot.cells == cells && Slot.stride == Stride && Slot.cut == Mesh.cut; }; + if (m_nocut && Mesh.cut) + { + // the backend can't change the indices of a chunk: the mesh goes whole, the material cuts the holes if it can + heightmap::build_indices(cells, Stride, Mesh.indices); + Mesh.cut = false; + } + auto const refill = [&](geometry_slot const &Slot) { + if (Slot.cut) + return GfxRenderer->Replace_Indexed(Mesh.indices, Mesh.vertices, nouserdata, Slot.geometry); + return GfxRenderer->Replace(Mesh.vertices, nouserdata, Slot.geometry, GL_TRIANGLES); + }; + auto const refilled = [&](int const Slot) { + if (refill(m_slots[Slot]) || false == m_slots[Slot].cut) + return true; + // the slot keeps the indices of the chunk it was made for, it's never used again + m_nocut = true; + m_slots[Slot].cells = -1; + Chunk.m_slot = -1; + return false; + }; if (Chunk.m_slot < 0 || false == fits(m_slots[Chunk.m_slot])) { if (Chunk.m_slot >= 0) @@ -404,6 +431,7 @@ void terrain_streamer::upload(editor_terrain &Chunk, heightmap::mesh &Mesh, int slot.bank = GfxRenderer->Create_Bank(); slot.cells = cells; slot.stride = Stride; + slot.cut = Mesh.cut; slot.geometry = GfxRenderer->Insert(Mesh.indices, Mesh.vertices, nouserdata, slot.bank, GL_TRIANGLES); if (slot.geometry.chunk == 0) { @@ -419,12 +447,17 @@ void terrain_streamer::upload(editor_terrain &Chunk, heightmap::mesh &Mesh, int else { m_slots[Chunk.m_slot].used = true; - GfxRenderer->Replace(Mesh.vertices, nouserdata, m_slots[Chunk.m_slot].geometry, GL_TRIANGLES); + if (false == refilled(Chunk.m_slot)) + { + upload(Chunk, Mesh, Stride); + return; + } } } - else + else if (false == refilled(Chunk.m_slot)) { - GfxRenderer->Replace(Mesh.vertices, nouserdata, m_slots[Chunk.m_slot].geometry, GL_TRIANGLES); + upload(Chunk, Mesh, Stride); + return; } auto const &slot{m_slots[Chunk.m_slot]}; @@ -744,6 +777,66 @@ void terrain_streamer::update(glm::dvec3 const &Camera) request_mesh(*entry.second, wanted_stride(*entry.second, Camera), entry.first / heightmap::chunk_size); ++meshing; } + + publish_grass(Camera); +} + +void terrain_streamer::publish_grass(glm::dvec3 const &Camera) +{ + if (GfxRenderer == nullptr) + return; + std::vector kinds; + std::vector chunks; + if (Global.gfx_grass && false == m_manifest.grass.empty()) + { + for (auto const &kind : m_manifest.grass) + { + gfx::grass_kind entry; + // the palette holds the colours as they're seen, the renderer lights them in linear rgb + entry.base = glm::pow(glm::clamp(kind.base, glm::vec3{0.f}, glm::vec3{1.f}), glm::vec3{2.2f}); + entry.tip = glm::pow(glm::clamp(kind.tip, glm::vec3{0.f}, glm::vec3{1.f}), glm::vec3{2.2f}); + entry.height = kind.height; + entry.variation = kind.variation; + entry.width = kind.width; + entry.density = kind.density; + entry.bend = kind.bend; + kinds.push_back(entry); + } + auto const reach{static_cast(Global.gfx_grass_distance) + 2.0}; + for (auto const &entry : m_chunks) + { + auto const &chunk{*entry.second}; + auto const &data{chunk.data()}; + auto const count{data.grass_count()}; + if (count == 0 || data.grass_density.size() != static_cast(data.grass + 1) * (data.grass + 1) * count || false == data.valid()) + continue; + // distance from the camera to the area of the chunk + auto const low{data.corner()}; + glm::dvec2 const nearest{std::clamp(Camera.x, low.x, low.x + heightmap::chunk_size), std::clamp(Camera.z, low.y, low.y + heightmap::chunk_size)}; + if (glm::length(nearest - glm::dvec2{Camera.x, Camera.z}) > reach) + continue; + gfx::grass_chunk grass; + grass.id = chunk.serial(); + grass.version = chunk.grass_version(); + grass.corner = low; + grass.size = static_cast(heightmap::chunk_size); + grass.low = static_cast(chunk.m_low.y); + grass.high = static_cast(chunk.m_high.y); + grass.cells = data.cells; + grass.heights = data.heights.data(); + grass.samples = data.grass; + grass.density = data.grass_density.data(); + for (std::size_t slot = 0; slot < count; ++slot) + grass.kinds[slot] = data.grass_kinds[slot]; + if (data.has_holes()) + { + grass.holes = data.holes; + grass.hole_mask = data.hole_mask.data(); + } + chunks.push_back(grass); + } + } + GfxRenderer->Terrain_Grass(kinds, chunks); } void terrain_streamer::upload_shape_material(editor_terrain &Chunk) @@ -813,6 +906,9 @@ void terrain_streamer::gather_triangles(glm::dvec2 const &Min, glm::dvec2 const for (int iz = zs.first; iz <= zs.second && iz < data.cells; ++iz) for (int ix = xs.first; ix <= xs.second && ix < data.cells; ++ix) { + // the cells cut out entirely aren't ground + if (data.cut_cells(ix, iz, ix + 1, iz + 1)) + continue; auto const v00{point(ix, iz)}, v10{point(ix + 1, iz)}, v01{point(ix, iz + 1)}, v11{point(ix + 1, iz + 1)}; Out.push_back({v00, v01, v10}); Out.push_back({v11, v10, v01}); @@ -820,6 +916,45 @@ void terrain_streamer::gather_triangles(glm::dvec2 const &Min, glm::dvec2 const } } +bool terrain_streamer::raycast(glm::dvec3 const &Origin, glm::dvec3 const &Direction, double const Reach, glm::dvec3 &Hit) const +{ + if (glm::length(Direction) < 1e-9) + return false; + auto const direction{glm::normalize(Direction)}; + // above the ground (or where there's none) at the start of a step, below it at its end: the crossing is in between + auto const above = [&](double const T) { + auto const point{Origin + direction * T}; + double height; + return false == height_at(point.x, point.z, height) || point.y > height; + }; + if (false == above(0.0)) + return false; // starts under the ground + double previous{0.0}; + double t{0.0}; + while (t < Reach) + { + // finer near the camera, where the brush is aimed most + t = std::min(Reach, t + std::max(0.25, t * 0.004)); + if (above(t)) + { + previous = t; + continue; + } + auto low{previous}, high{t}; + for (int i = 0; i < 24; ++i) + { + auto const middle{(low + high) * 0.5}; + if (above(middle)) + low = middle; + else + high = middle; + } + Hit = Origin + direction * high; + return true; + } + return false; +} + bool terrain_streamer::owns(gfx::geometry_handle const &Geometry) const { if (Geometry.bank == 0 && Geometry.chunk == 0) @@ -1320,6 +1455,210 @@ bool terrain_streamer::paint_area(glm::dvec2 const &Min, glm::dvec2 const &Max, return changed; } +namespace +{ +// true if the point lies within the outline (x, z) +bool inside_outline(std::vector const &Outline, double const X, double const Z) +{ + bool inside{false}; + for (std::size_t i = 0, j = Outline.size() - 1; i < Outline.size(); j = i++) + { + auto const &a{Outline[i]}; + auto const &b{Outline[j]}; + if ((a.y > Z) != (b.y > Z) && X < (b.x - a.x) * (Z - a.y) / (b.y - a.y) + a.x) + inside = !inside; + } + return inside; +} + +std::pair outline_bounds(std::vector const &Outline) +{ + glm::dvec2 low{std::numeric_limits::max()}, high{-std::numeric_limits::max()}; + for (auto const &point : Outline) + { + low = glm::min(low, point); + high = glm::max(high, point); + } + return {low, high}; +} +} // namespace + +bool terrain_streamer::cut(double X, double Z, double Radius, bool Cut) +{ + if (Radius <= 0.0) + return false; + return cut_area(glm::dvec2{X - Radius, Z - Radius}, glm::dvec2{X + Radius, Z + Radius}, Cut, false, + [&](double const Px, double const Pz) { return (Px - X) * (Px - X) + (Pz - Z) * (Pz - Z) <= Radius * Radius; }); +} + +bool terrain_streamer::cut(std::vector const &Outline, bool Cut) +{ + if (Outline.size() < 3) + return false; + auto const [low, high]{outline_bounds(Outline)}; + return cut_area(low, high, Cut, true, [&](double const Px, double const Pz) { return inside_outline(Outline, Px, Pz); }); +} + +bool terrain_streamer::cut_area(glm::dvec2 const &Min, glm::dvec2 const &Max, bool const Cut, bool const Load, std::function const &Inside) +{ + bool changed{false}; + for (auto *chunk : chunks_in(Min, Max, Load)) + { + auto const ¤t{chunk->data()}; + if (false == Cut && false == current.has_holes()) + continue; // nothing to close + auto data{std::make_shared(current)}; + data->ensure_holes(); + auto const step{heightmap::chunk_size / data->holes}; + auto const low{data->corner()}; + auto const xs{index_range(low.x, step, data->holes + 1, Min.x, Max.x)}; + auto const zs{index_range(low.y, step, data->holes + 1, Min.y, Max.y)}; + std::uint8_t const value{Cut ? std::uint8_t{255} : std::uint8_t{0}}; + bool touched{false}; + for (int iz = zs.first; iz <= zs.second; ++iz) + for (int ix = xs.first; ix <= xs.second; ++ix) + { + auto &sample{data->hole_mask[static_cast(iz) * (data->holes + 1) + ix]}; + if (sample == value || false == Inside(low.x + ix * step, low.y + iz * step)) + continue; + sample = value; + touched = true; + } + if (false == touched) + continue; + data->compact_holes(); + remember(*chunk); + // the mesh leaves the cells cut out entirely, the material cuts the rest + chunk->replace(std::move(data), true, true); + changed = true; + } + return changed; +} + +bool terrain_streamer::grass(double X, double Z, double Radius, double Strength, std::uint16_t Kind, bool Erase) +{ + if (Radius <= 0.0 || Strength <= 0.0) + return false; + return grass_area(glm::dvec2{X - Radius, Z - Radius}, glm::dvec2{X + Radius, Z + Radius}, Kind, Erase, false, [&](double const Px, double const Pz) { + auto const distance{std::sqrt((Px - X) * (Px - X) + (Pz - Z) * (Pz - Z))}; + return distance > Radius ? 0.0 : Strength * falloff(distance, Radius); + }); +} + +bool terrain_streamer::grass(std::vector const &Outline, std::uint16_t Kind, bool Erase) +{ + if (Outline.size() < 3) + return false; + auto const [low, high]{outline_bounds(Outline)}; + return grass_area(low, high, Kind, Erase, true, [&](double const Px, double const Pz) { return inside_outline(Outline, Px, Pz) ? 1.0 : 0.0; }); +} + +bool terrain_streamer::grass_area(glm::dvec2 const &Min, glm::dvec2 const &Max, std::uint16_t const Kind, bool const Erase, bool const Load, std::function const &Amount) +{ + if (false == Erase && Kind >= m_manifest.grass.size()) + return false; + bool changed{false}; + for (auto *chunk : chunks_in(Min, Max, Load)) + { + auto const ¤t{chunk->data()}; + if (Erase && current.grass_count() == 0) + continue; // nothing to take away + auto data{std::make_shared(current)}; + data->ensure_grass(); + auto const samples{data->grass}; + auto const plane{static_cast(samples + 1) * (samples + 1)}; + int slot{-1}; + if (false == Erase) + { + slot = data->grass_slot_of(Kind); + if (slot < 0) + { + data->compact_grass(); + data->ensure_grass(); + auto const count{data->grass_count()}; + if (count >= heightmap::max_chunk_grass) + { + WriteLog("Editor: chunk " + std::to_string(current.key.first) + "," + std::to_string(current.key.second) + " has " + std::to_string(count) + + " kinds of grass already, no room for another", + logtype::generic); + continue; + } + slot = static_cast(count); + data->grass_kinds[slot] = Kind; + data->grass_density.resize(data->grass_density.size() + plane, 0); + } + } + auto const count{data->grass_count()}; + auto const step{heightmap::chunk_size / samples}; + auto const low{data->corner()}; + auto const xs{index_range(low.x, step, samples + 1, Min.x, Max.x)}; + auto const zs{index_range(low.y, step, samples + 1, Min.y, Max.y)}; + bool grown{false}; + for (int iz = zs.first; iz <= zs.second; ++iz) + for (int ix = xs.first; ix <= xs.second; ++ix) + { + auto const amount{std::clamp(Amount(low.x + ix * step, low.y + iz * step), 0.0, 1.0)}; + if (amount <= 0.0) + continue; + auto const sample{static_cast(iz) * (samples + 1) + ix}; + if (Erase) + { + for (std::size_t other = 0; other < count; ++other) + { + auto &density{data->grass_density[other * plane + sample]}; + auto const target{static_cast(std::lround(density * (1.0 - amount)))}; + grown |= (target != density); + density = target; + } + continue; + } + auto &own{data->grass_density[slot * plane + sample]}; + auto const target{static_cast(std::lround(own + (255 - own) * amount))}; + if (target == own) + continue; + own = static_cast(target); + // the other kinds give way, so the densities add up to the full at most + int others{0}; + for (std::size_t other = 0; other < count; ++other) + if (static_cast(other) != slot) + others += data->grass_density[other * plane + sample]; + auto const room{255 - target}; + if (others > room) + for (std::size_t other = 0; other < count; ++other) + if (static_cast(other) != slot) + { + auto &density{data->grass_density[other * plane + sample]}; + density = static_cast(density * room / others); + } + grown = true; + } + if (false == grown) + continue; + data->compact_grass(); + remember(*chunk); + chunk->replace(std::move(data), false, false, true); + changed = true; + } + return changed; +} + +std::uint16_t terrain_streamer::add_grass_kind(heightmap::grass_def Kind) +{ + remember_manifest(); + m_manifest.grass.push_back(std::move(Kind)); + m_manifestchanged = true; + return static_cast(m_manifest.grass.size() - 1); +} + +void terrain_streamer::grass_kind(std::uint16_t const Index, heightmap::grass_def Kind) +{ + if (Index >= m_manifest.grass.size()) + return; + remember_manifest(); + m_manifest.grass[Index] = std::move(Kind); + m_manifestchanged = true; +} + std::uint16_t terrain_streamer::layer(std::string const &Material, float const Size, float const Rotation) { for (std::size_t i = 0; i < m_manifest.layers.size(); ++i) @@ -1671,6 +2010,7 @@ void terrain_streamer::remember_manifest() m_change.water = m_manifest.water; m_change.water_material = m_manifest.water_material; m_change.modifiers = m_manifest.modifiers; + m_change.grass = m_manifest.grass; } terrain_streamer::change terrain_streamer::restore(change const &Change) @@ -1695,7 +2035,7 @@ terrain_streamer::change terrain_streamer::restore(change const &Change) } if (current != nullptr) { - current->replace(data, true, true); + current->replace(data, true, true, true); } else { @@ -1718,11 +2058,13 @@ terrain_streamer::change terrain_streamer::restore(change const &Change) opposite.water = m_manifest.water; opposite.water_material = m_manifest.water_material; opposite.modifiers = m_manifest.modifiers; + opposite.grass = m_manifest.grass; auto const samelayers{std::equal(Change.layers.begin(), Change.layers.end(), m_manifest.layers.begin(), m_manifest.layers.end(), [](heightmap::layer_def const &A, heightmap::layer_def const &B) { return A.material == B.material && A.size == B.size && A.rotation == B.rotation; })}; m_manifest.layers = Change.layers; m_manifest.modifiers = Change.modifiers; m_manifest.water = Change.water; + m_manifest.grass = Change.grass; if (false == Change.water_material.empty()) m_manifest.water_material = Change.water_material; m_manifestchanged = true; diff --git a/editor/editorTerrainStreamer.hpp b/editor/editorTerrainStreamer.hpp index beff9746..a93632dc 100644 --- a/editor/editorTerrainStreamer.hpp +++ b/editor/editorTerrainStreamer.hpp @@ -71,6 +71,9 @@ class terrain_streamer 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> &Out) const; + // the first point where a ray from Origin along Direction (normalized) meets the ground of the chunks in memory, within + // Reach metres; the holes are taken for ground. returns: false if it meets none + bool raycast(glm::dvec3 const &Origin, glm::dvec3 const &Direction, double Reach, glm::dvec3 &Hit) 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(); } @@ -116,6 +119,21 @@ class terrain_streamer // Feather: metres across the outline the material blends in over, half of them inside; 0: a sharp edge, as sharp as // the paint samples of the chunks go. the chunks are loaded where needed bool fill(std::vector const &Outline, double Feather, std::uint16_t Layer, double Strength = 1.0); + // holes, for the portals of tunnels and the like: cut in the ground (Cut true) within Radius of (X,Z), or closed again + bool cut(double X, double Z, double Radius, bool Cut); + // the same within Outline (x, z); the chunks are loaded where needed + bool cut(std::vector const &Outline, bool Cut); + // grass: grows the kind of the grass palette Kind within Radius of (X,Z), Strength (0..1) of the way at the centre; + // Erase: takes every kind of grass away there instead + bool grass(double X, double Z, double Radius, double Strength, std::uint16_t Kind, bool Erase); + // the same within Outline (x, z), fully; the chunks are loaded where needed + bool grass(std::vector const &Outline, std::uint16_t Kind, bool Erase); + // the grass palette + std::vector const &grass_kinds() const { return m_manifest.grass; } + // adds a kind to the grass palette; returns: its index + std::uint16_t add_grass_kind(heightmap::grass_def Kind); + // changes a kind of the grass palette; the grass of that kind changes with it + void grass_kind(std::uint16_t Index, heightmap::grass_def Kind); // palette entry of the material (repeated at Size, 0: any, turned by Rotation degrees), added to the palette if needed std::uint16_t layer(std::string const &Material, float Size = 0.f, float Rotation = 0.f); void layer_size(std::uint16_t Layer, float Size); @@ -153,6 +171,7 @@ class terrain_streamer std::vector water; std::string water_material; std::vector modifiers; + std::vector grass; bool empty() const { return chunks.empty() && false == manifest; } }; @@ -218,6 +237,7 @@ class terrain_streamer gfx::geometry_handle geometry{0, 0}; int cells{0}; int stride{0}; + bool cut{false}; // the indices are of a chunk with holes, filled anew with every mesh bool used{false}; }; struct water_shape @@ -247,6 +267,14 @@ class terrain_streamer // paints the material of the palette entry Layer over the paint samples within the rectangle, Amount (0..1) of the way // at each, as Amount gives it for the place of the sample. Load: chunks not in memory are loaded bool paint_area(glm::dvec2 const &Min, glm::dvec2 const &Max, std::uint16_t Layer, bool Load, std::function const &Amount); + // cuts (Cut true) or closes the holes at the hole samples within the rectangle where Inside says so. Load: chunks not in + // memory are loaded + bool cut_area(glm::dvec2 const &Min, glm::dvec2 const &Max, bool Cut, bool Load, std::function const &Inside); + // grows the kind Kind of the grass palette (or takes all grass away, Erase) over the grass samples within the rectangle, + // Amount (0..1) of the way at each + bool grass_area(glm::dvec2 const &Min, glm::dvec2 const &Max, std::uint16_t Kind, bool Erase, bool Load, std::function const &Amount); + // hands the renderer the grass of the chunks near the camera + void publish_grass(glm::dvec3 const &Camera); // 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 @@ -279,6 +307,7 @@ class terrain_streamer std::set m_loading; // asked from a worker std::set m_removed; // removed in the editor, still in the files until saved std::vector m_slots; + bool m_nocut{false}; // the renderer can't change the indices of the chunks: the holes are cut by the materials alone std::map m_slotofgeometry; // geometry key -> slot std::map m_palette; std::vector m_watershapes; diff --git a/editor/icons/editor_icons.png b/editor/icons/editor_icons.png index affe75b5..f54cb31d 100644 Binary files a/editor/icons/editor_icons.png and b/editor/icons/editor_icons.png differ diff --git a/editor/icons/editor_icons.py b/editor/icons/editor_icons.py index a1756b34..2c99e4b0 100644 --- a/editor/icons/editor_icons.py +++ b/editor/icons/editor_icons.py @@ -378,11 +378,24 @@ def work_terrain(d): line(d, [at(u, v) for v in steps], W - 6) +def hole(d): + # a hill with the portal of a tunnel cut in it (the opening is in cutouts()) + poly(d, [(2, 58), (2, 40), (14, 22), (28, 12), (42, 16), (54, 30), (62, 42), (62, 58)]) + + +def grass(d): + # blades growing from the ground, leaning either way + line(d, [(4, 58), (60, 58)], W - 8) + for x, lean, height in ((10, -6, 30), (20, 4, 44), (31, -3, 52), (42, 7, 40), (53, -5, 30)): + pts = [(x + lean * t * t, 56 - height * t) for t in (i / 10 for i in range(11))] + line(d, pts, W - 8) + + ICONS = [select, insert, brush, area_fill, copy_to_bank, translate, rotate, scale, local_space, ortho, perspective, sculpt, smooth, chunks, orthophoto, road, place, lanes, lay_track, switch, straight, curve, signal, objects, vehicle, profile, speed, joints, infra, gauge, turntable, parallel, more, work_surroundings, - work_tracks, work_roads, work_terrain] + work_tracks, work_roads, work_terrain, hole, grass] def cutouts(name, d): @@ -403,6 +416,9 @@ def cutouts(name, d): # the door and the gap between the crowns of the tree rect(d, 46, 46, 52, 60) line(d, [(9, 31), (27, 31)], 8) + elif name == "hole": + # the opening of the portal, an arch on the ground + poly(d, [(20, 60), (20, 42)] + [(32 + 12 * math.cos(math.radians(180 - a)), 42 - 12 * math.sin(math.radians(180 - a))) for a in range(0, 181, 10)] + [(44, 60)]) elif name == "work_roads": # the windows, and the gaps around the wheels poly(d, [(25, 22), (31, 22), (31, 30), (19, 30)]) diff --git a/lang/editor_pl.po b/lang/editor_pl.po index ec07525b..89f3ec3b 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -9949,3 +9949,128 @@ msgstr "Dobudowuj teren przy układaniu" msgid "Where a track is laid past the edge of the terrain, the terrain model is made around the cursor as it goes" msgstr "Gdy tor wychodzi poza teren, NMT dobudowuje się wokół kursora na bieżąco" + +# scenery editor: holes and grass of the heightmap terrain + +msgid "%zu: %s, %.2f m, %.0f blades/m2" +msgstr "%zu: %s, %.2f m, %.0f źdźbeł/m2" + +msgid "A chunk holds up to 4 kinds of grass. LMB grows the chosen kind, Shift+LMB takes all grass away" +msgstr "Kawałek mieści do 4 rodzajów trawy. LPM sieje wybrany rodzaj, Shift+LPM usuwa całą trawę" + +msgid "Add:" +msgstr "Dodaj:" + +msgid "Blade width (m)" +msgstr "Szerokość źdźbła (m)" + +msgid "Blades on a square metre painted fully. The renderer draws fewer further away, and wider;\ngfx.grass.distance and gfx.grass.density in eu07.ini set how far and how many" +msgstr "Źdźbła na metrze kwadratowym w pełni pomalowanym. Dalej renderer rysuje ich mniej, ale szerszych;\ngfx.grass.distance i gfx.grass.density w eu07.ini ustalają, jak daleko i ile" + +msgid "Clear the grass" +msgstr "Usuń trawę" + +msgid "Close the hole" +msgstr "Zamknij dziurę" + +msgid "Cut a hole" +msgstr "Wytnij dziurę" + +msgid "Cuts the ground within the outline away, e.g. for the portal of a tunnel" +msgstr "Wycina teren wewnątrz obrysu, np. pod portal tunelu" + +msgid "Density (blades/m2)" +msgstr "Gęstość (źdźbła/m2)" + +msgid "Grass grown: %s" +msgstr "Wysiano trawę: %s" + +msgid "Grass taken away from the outlined area" +msgstr "Usunięto trawę z obrysowanego obszaru" + +msgid "Grass" +msgstr "Trawa" + +msgid "Grow grass" +msgstr "Posiej trawę" + +msgid "Grows the kind of grass chosen in the Grass section within the outline" +msgstr "Sieje wewnątrz obrysu rodzaj trawy wybrany w sekcji Trawa" + +msgid "Height (m)##grass" +msgstr "Wysokość (m)##grass" + +msgid "Height varies by" +msgstr "Rozrzut wysokości" + +msgid "Hole closed in the outlined area" +msgstr "Zamknięto dziurę w obrysowanym obszarze" + +msgid "Hole cut in the outlined area" +msgstr "Wycięto dziurę w obrysowanym obszarze" + +msgid "Holes" +msgstr "Dziury" + +msgid "How far the tips of the blades lean over, share of their height; the wind adds to it" +msgstr "Jak bardzo czubki źdźbeł się pochylają, jako część ich wysokości; wiatr dokłada swoje" + +msgid "LMB cuts a hole in the terrain under the brush (portals of tunnels), Shift+LMB closes it" +msgstr "LPM wycina dziurę w terenie pod pędzlem (portale tuneli), Shift+LPM ją zamyka" + +msgid "LMB grows the kind of grass chosen in the grass palette, Shift+LMB takes the grass away" +msgstr "LPM sieje rodzaj trawy wybrany w palecie traw, Shift+LPM usuwa trawę" + +msgid "Lean" +msgstr "Pochylenie" + +msgid "Name##grass" +msgstr "Nazwa##grass" + +msgid "No grass under the outline" +msgstr "Pod obrysem nie ma trawy" + +msgid "No hole under the outline" +msgstr "Pod obrysem nie ma dziury" + +msgid "No kinds of grass yet. Add one of the presets below; the first stroke of the brush adds a meadow." +msgstr "Nie ma jeszcze rodzajów trawy. Dodaj któryś z gotowych poniżej; pierwsze pociągnięcie pędzla doda łąkę." + +msgid "Nothing cut: no chunk under the outline" +msgstr "Nic nie wycięto: pod obrysem nie ma kawałka terenu" + +msgid "Nothing grown: no chunk under the outline, or no room for another kind of grass" +msgstr "Nic nie wysiano: pod obrysem nie ma kawałka terenu albo nie ma miejsca na kolejny rodzaj trawy" + +msgid "Openings in the ground for the portals of tunnels, cuttings under bridges and the like. With the Holes tool LMB cuts the ground under the brush and Shift+LMB closes it; the brush stays on the ground also over an opening. An area outlined with the Fill tool can be cut or closed in the Fill section." +msgstr "Otwory w terenie pod portale tuneli, przejścia pod mostami i podobne. Narzędziem Dziury LPM wycina teren pod pędzlem, a Shift+LPM go zamyka; pędzel trzyma się terenu także nad otworem. Obszar obrysowany narzędziem Wypełnienie można wyciąć albo zamknąć w sekcji Wypełnienie." + +msgid "Root colour" +msgstr "Kolor u nasady" + +msgid "Share of the height the blades differ by, either way" +msgstr "O jaką część wysokości źdźbła różnią się w górę i w dół" + +msgid "The grass isn't drawn: gfx.grass is off" +msgstr "Trawa nie jest rysowana: gfx.grass jest wyłączone" + +msgid "The holes follow the ground every metre (every grid point of finer chunks), whatever the drawn grid" +msgstr "Dziury idą za terenem co metr (co punkt siatki w gęstszych kawałkach), niezależnie od rysowanej siatki" + +msgid "Tip colour" +msgstr "Kolor czubka" + +msgid "meadow" +msgstr "łąka" + +msgid "lawn" +msgstr "trawnik" + +msgid "dry grass" +msgstr "sucha trawa" + +msgid "tall grass" +msgstr "wysoka trawa" + +msgid "weeds" +msgstr "chwasty" diff --git a/rendering/geometrybank.cpp b/rendering/geometrybank.cpp index 551c2b5a..5b4abf12 100644 --- a/rendering/geometrybank.cpp +++ b/rendering/geometrybank.cpp @@ -309,6 +309,27 @@ geometry_bank::replace( gfx::vertex_array &Vertices, userdata_array &Userdata, g return true; } +// replaces the index and the vertex data of specified indexed chunk with the supplied data of the same counts +bool +geometry_bank::replace( gfx::index_array &Indices, gfx::vertex_array &Vertices, userdata_array &Userdata, gfx::geometry_handle const &Geometry ) { + + if( ( Geometry.chunk == 0 ) || ( Geometry.chunk > m_chunks.size() ) ) { return false; } + + auto &chunk = gfx::geometry_bank::chunk( Geometry ); + // the place of the chunk in the buffers of the bank is kept, so the sizes have to stay + if( ( Indices.size() != chunk.indices.size() ) + || ( Vertices.size() != chunk.vertices.size() ) + || ( Userdata.size() != chunk.userdata.size() ) ) { + return false; + } + chunk.indices.swap( Indices ); + chunk.vertices.swap( Vertices ); + chunk.userdata.swap( Userdata ); + // template method implementation; the chunk is uploaded whole the next time it's drawn + replace_( Geometry ); + return true; +} + // adds supplied vertex data at the end of specified chunk bool geometry_bank::append( gfx::vertex_array &Vertices, userdata_array &Userdata, gfx::geometry_handle const &Geometry ) { @@ -441,6 +462,13 @@ geometrybank_manager::replace( gfx::vertex_array &Vertices, userdata_array &User return bank( Geometry ).first->replace( Vertices, Userdata, Geometry, Offset ); } +// replaces the index and the vertex data of specified indexed chunk with the supplied data of the same counts +bool +geometrybank_manager::replace( gfx::index_array &Indices, gfx::vertex_array &Vertices, userdata_array &Userdata, gfx::geometry_handle const &Geometry ) { + + return bank( Geometry ).first->replace( Indices, Vertices, Userdata, Geometry ); +} + // adds supplied vertex data at the end of specified chunk bool geometrybank_manager::append( gfx::vertex_array &Vertices, userdata_array &Userdata, gfx::geometry_handle const &Geometry ) { diff --git a/rendering/geometrybank.h b/rendering/geometrybank.h index e245486f..eae11241 100644 --- a/rendering/geometrybank.h +++ b/rendering/geometrybank.h @@ -125,6 +125,8 @@ public: auto create( gfx::index_array &Indices, gfx::vertex_array &Vertices, gfx::userdata_array& Userdata, unsigned int const Type ) -> gfx::geometry_handle; // replaces vertex data of specified chunk with the supplied data, starting from specified offset auto replace( gfx::vertex_array &Vertices, gfx::userdata_array& Userdata, gfx::geometry_handle const &Geometry, std::size_t const Offset = 0 ) -> bool; + // replaces the index and the vertex data of specified indexed chunk with the supplied data of the same counts. returns: false if the counts differ + auto replace( gfx::index_array &Indices, gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry ) -> bool; // adds supplied vertex data at the end of specified chunk auto append( gfx::vertex_array &Vertices, gfx::userdata_array& Userdata, gfx::geometry_handle const &Geometry ) -> bool; // draws geometry stored in specified chunk @@ -243,6 +245,8 @@ public: auto create_chunk( gfx::index_array &Indices, gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometrybank_handle const &Geometry, unsigned int const Type ) -> gfx::geometry_handle; // replaces data of specified chunk with the supplied vertex data, starting from specified offset auto replace( gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, std::size_t const Offset = 0 ) -> bool; + // replaces the index and the vertex data of specified indexed chunk with the supplied data of the same counts. returns: false if the counts differ + auto replace( gfx::index_array &Indices, gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry ) -> bool; // adds supplied vertex data at the end of specified chunk auto append( gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry ) -> bool; // draws geometry stored in specified chunk diff --git a/rendering/opengl46/opengl46grass.cpp b/rendering/opengl46/opengl46grass.cpp new file mode 100644 index 00000000..d9c58965 --- /dev/null +++ b/rendering/opengl46/opengl46grass.cpp @@ -0,0 +1,303 @@ +/* +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/opengl46/opengl46grass.h" + +#include "rendering/openglcamera.h" +#include "model/Texture.h" +#include "simulation/simulationenvironment.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/Timer.h" + +namespace +{ + +int constexpr max_blades{65536}; // drawn in a tile at most +float constexpr blade_reach{1.5f}; // metres the bounds of a tile reach above its ground, for the blades + +// the share of the blades drawn at specified distance; the same as lod_share() of grass.vert +float lod_share(float const Distance, float const Near) +{ + return Distance <= Near ? 1.f : std::pow(Near / Distance, 1.5f); +} + +GLuint make_texture(GLenum const Format, int const Width, int const Height, GLenum const Filter) +{ + GLuint texture{0}; + ::glCreateTextures(GL_TEXTURE_2D, 1, &texture); + ::glTextureStorage2D(texture, 1, Format, Width, Height); + ::glTextureParameteri(texture, GL_TEXTURE_MIN_FILTER, Filter); + ::glTextureParameteri(texture, GL_TEXTURE_MAG_FILTER, Filter); + ::glTextureParameteri(texture, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + ::glTextureParameteri(texture, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + return texture; +} + +} // namespace + +opengl46_grass::~opengl46_grass() +{ + // the textures go with the context; deleted here only if it's still there + if (::glfwGetCurrentContext() != nullptr) + clear(); +} + +bool opengl46_grass::init() +{ + if (m_state != 0) + return m_state > 0; + m_state = -1; + try + { + gl::shader const vertex("grass.vert"); + gl::shader const fragment("grass.frag"); + m_program = std::make_unique(std::vector>{vertex, fragment}); + } + catch (gl::shader_exception const &e) + { + ErrorLog("heightmap terrain: can't use the grass shader: " + std::string(e.what())); + return false; + } + m_vao = std::make_unique(); + auto const location = [&](char const *Name) { return ::glGetUniformLocation(*m_program, Name); }; + m_uniforms.tile = location("grass_tile"); + m_uniforms.part = location("grass_part"); + m_uniforms.chunk = location("grass_chunk"); + m_uniforms.camera = location("grass_camera"); + m_uniforms.lod = location("grass_lod"); + m_uniforms.wind = location("grass_wind"); + m_uniforms.base = location("grass_base"); + m_uniforms.tip = location("grass_tip"); + m_uniforms.shape = location("grass_shape"); + m_uniforms.share = location("grass_share"); + m_state = 1; + return true; +} + +void opengl46_grass::release(chunk &Chunk) +{ + for (auto *texture : {&Chunk.heights, &Chunk.density, &Chunk.holes}) + if (*texture != 0) + { + ::glDeleteTextures(1, texture); + *texture = 0; + } +} + +void opengl46_grass::clear() +{ + for (auto &entry : m_chunks) + release(entry.second); + m_chunks.clear(); +} + +void opengl46_grass::upload(chunk &Chunk, gfx::grass_chunk const &Source) +{ + release(Chunk); + Chunk.version = Source.version; + Chunk.corner = Source.corner; + Chunk.size = Source.size; + Chunk.cells = Source.cells; + Chunk.samples = Source.samples; + Chunk.kinds = Source.kinds; + + auto const side{Source.cells + 1}; + Chunk.heights = make_texture(GL_R32F, side, side, GL_NEAREST); + ::glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + ::glTextureSubImage2D(Chunk.heights, 0, 0, 0, side, side, GL_RED, GL_FLOAT, Source.heights); + + // the densities of the kinds side by side, as the channels of a texel + auto const samples{Source.samples + 1}; + auto const plane{static_cast(samples) * samples}; + std::size_t kinds{0}; + while (kinds < Source.kinds.size() && Source.kinds[kinds] >= 0) + ++kinds; + std::vector texels(plane * 4, 0); + for (std::size_t kind = 0; kind < kinds; ++kind) + for (std::size_t i = 0; i < plane; ++i) + texels[i * 4 + kind] = Source.density[kind * plane + i]; + Chunk.density = make_texture(GL_RGBA8, samples, samples, GL_LINEAR); + ::glTextureSubImage2D(Chunk.density, 0, 0, 0, samples, samples, GL_RGBA, GL_UNSIGNED_BYTE, texels.data()); + + std::uint8_t const none{0}; + auto const holeside{(Source.holes > 0 && Source.hole_mask != nullptr) ? Source.holes + 1 : 1}; + Chunk.holes = make_texture(GL_R8, holeside, holeside, GL_LINEAR); + ::glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + ::glTextureSubImage2D(Chunk.holes, 0, 0, 0, holeside, holeside, GL_RED, GL_UNSIGNED_BYTE, holeside > 1 ? Source.hole_mask : &none); + ::glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + + // the ground and the densest place of every kind of each tile, to skip the empty tiles and to draw as many blades as needed + auto const spacing{static_cast(Source.size) / Source.cells}; + auto const step{static_cast(Source.size) / Source.samples}; + auto const tilesize{static_cast(Source.size) / tiles}; + for (int tz = 0; tz < tiles; ++tz) + for (int tx = 0; tx < tiles; ++tx) + { + auto &part{Chunk.parts[static_cast(tz) * tiles + tx]}; + auto const range = [](double const From, double const To, double const Step, int const Last) { + return std::make_pair(std::clamp(static_cast(std::floor(From / Step)), 0, Last), std::clamp(static_cast(std::ceil(To / Step)), 0, Last)); + }; + auto const [gx0, gx1]{range(tx * tilesize, (tx + 1) * tilesize, spacing, Source.cells)}; + auto const [gz0, gz1]{range(tz * tilesize, (tz + 1) * tilesize, spacing, Source.cells)}; + part.low = std::numeric_limits::max(); + part.high = -std::numeric_limits::max(); + for (int z = gz0; z <= gz1; ++z) + for (int x = gx0; x <= gx1; ++x) + { + auto const height{Source.heights[static_cast(z) * side + x]}; + part.low = std::min(part.low, height); + part.high = std::max(part.high, height); + } + part.most.fill(0.f); + auto const [sx0, sx1]{range(tx * tilesize, (tx + 1) * tilesize, step, Source.samples)}; + auto const [sz0, sz1]{range(tz * tilesize, (tz + 1) * tilesize, step, Source.samples)}; + for (std::size_t kind = 0; kind < kinds; ++kind) + { + std::uint8_t most{0}; + for (int z = sz0; z <= sz1; ++z) + for (int x = sx0; x <= sx1; ++x) + most = std::max(most, Source.density[kind * plane + static_cast(z) * samples + x]); + part.most[kind] = most / 255.f; + } + } +} + +void opengl46_grass::update(std::vector const &Kinds, std::vector const &Chunks) +{ + m_kinds = Kinds; + for (auto &entry : m_chunks) + entry.second.listed = false; + if (false == Global.gfx_grass) + { + clear(); + return; + } + for (auto const &source : Chunks) + { + if (source.heights == nullptr || source.density == nullptr || source.cells <= 0 || source.samples <= 0) + continue; + auto &entry{m_chunks[source.id]}; + entry.listed = true; + if (entry.heights == 0 || entry.version != source.version) + upload(entry, source); + } + // what's no longer near goes + for (auto it = m_chunks.begin(); it != m_chunks.end();) + { + if (it->second.listed) + { + ++it; + continue; + } + release(it->second); + it = m_chunks.erase(it); + } +} + +std::size_t opengl46_grass::render(opengl_camera const &Camera, glm::mat4 const &View, std::function const &Modelview) +{ + if (false == Global.gfx_grass || m_chunks.empty() || m_kinds.empty() || false == init()) + return 0; + + auto const reach{std::max(5.f, Global.gfx_grass_distance)}; + auto const near{std::max(3.f, reach * 0.2f)}; + auto const multiplier{std::max(0.f, Global.gfx_grass_density)}; + auto const camera{Camera.position()}; + + m_program->bind(); + m_vao->bind(); + ::glDisable(GL_CULL_FACE); + ::glUniform4f(m_uniforms.lod, near, reach, 0.f, 0.f); + // the wind sways the blades along it, harder in the gusts + auto const wind{glm::vec3{simulation::Environment.wind()}}; + auto const windspeed{glm::length(glm::vec2{wind.x, wind.z})}; + auto const winddirection{windspeed > 0.01f ? glm::vec2{wind.x, wind.z} / windspeed : glm::vec2{1.f, 0.f}}; + ::glUniform4f(m_uniforms.wind, winddirection.x, winddirection.y, std::clamp(windspeed / 15.f, 0.f, 1.f), static_cast(std::fmod(Timer::GetTime(), 3600.0))); + + std::size_t triangles{0}; + for (auto &entry : m_chunks) + { + auto &chunk{entry.second}; + // the kinds of the chunk, and the share of the blades of each against its densest + std::array base{}, tip{}, shape{}; + glm::vec4 share{0.f}; + float densest{0.f}; + for (std::size_t i = 0; i < chunk.kinds.size(); ++i) + if (chunk.kinds[i] >= 0 && chunk.kinds[i] < static_cast(m_kinds.size())) + densest = std::max(densest, m_kinds[chunk.kinds[i]].density); + if (densest <= 0.f) + continue; + for (std::size_t i = 0; i < chunk.kinds.size(); ++i) + { + if (chunk.kinds[i] < 0 || chunk.kinds[i] >= static_cast(m_kinds.size())) + continue; + auto const &kind{m_kinds[chunk.kinds[i]]}; + base[i] = glm::vec4{kind.base, 0.f}; + tip[i] = glm::vec4{kind.tip, 0.f}; + shape[i] = glm::vec4{kind.height, kind.variation, kind.width, kind.bend}; + share[i] = kind.density / densest; + } + auto const tallest{std::max({shape[0].x, shape[1].x, shape[2].x, shape[3].x}) * 1.5f + blade_reach}; + + auto const tilesize{chunk.size / tiles}; + bool set{false}; + for (int tz = 0; tz < tiles; ++tz) + for (int tx = 0; tx < tiles; ++tx) + { + auto const &part{chunk.parts[static_cast(tz) * tiles + tx]}; + auto const fill{std::min(1.f, glm::dot(glm::vec4{part.most[0], part.most[1], part.most[2], part.most[3]}, share))}; + if (fill <= 0.f) + continue; + // distance to the box of the tile + glm::dvec3 const low{chunk.corner.x + tx * tilesize, part.low, chunk.corner.y + tz * tilesize}; + glm::dvec3 const high{low.x + tilesize, part.high + tallest, low.z + tilesize}; + auto const nearest{glm::clamp(camera, low, high)}; + auto const distance{static_cast(glm::length(nearest - camera))}; + if (distance > reach) + continue; + if (false == Camera.visible(scene::bounding_area{(low + high) * 0.5, static_cast(glm::length(high - low) * 0.5)})) + continue; + auto const full{tilesize * tilesize * densest * multiplier * fill}; + auto const count{std::min(max_blades, static_cast(std::ceil(full * lod_share(distance, near))))}; + if (count <= 0) + continue; + if (false == set) + { + // the chunk is drawn relative to its corner, the heights as they are + set = true; + Modelview(glm::translate(View, glm::vec3{glm::dvec3{chunk.corner.x - camera.x, -camera.y, chunk.corner.y - camera.z}})); + ::glUniform4f(m_uniforms.chunk, chunk.size, static_cast(chunk.cells), static_cast(chunk.samples), 0.f); + ::glUniform4f(m_uniforms.camera, static_cast(camera.x - chunk.corner.x), static_cast(camera.y), static_cast(camera.z - chunk.corner.y), 0.f); + ::glUniform4fv(m_uniforms.base, 4, glm::value_ptr(base[0])); + ::glUniform4fv(m_uniforms.tip, 4, glm::value_ptr(tip[0])); + ::glUniform4fv(m_uniforms.shape, 4, glm::value_ptr(shape[0])); + ::glUniform4fv(m_uniforms.share, 1, glm::value_ptr(share)); + GLuint const textures[]{chunk.heights, chunk.density, chunk.holes}; + ::glBindTextures(0, 3, textures); + } + // fewer segments further away + auto const segments{distance < near * 1.5f ? 3 : distance < reach * 0.5f ? 2 : 1}; + // the number of the tile in the world, the seed of its blades + auto const seedx{static_cast(std::floor(chunk.corner.x / tilesize + 0.5) + tx)}; + auto const seedz{static_cast(std::floor(chunk.corner.y / tilesize + 0.5) + tz)}; + ::glUniform4f(m_uniforms.tile, tx * tilesize, tz * tilesize, tilesize, std::max(1.f, full)); + ::glUniform4f(m_uniforms.part, fill, static_cast(segments), seedx, seedz); + ::glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, segments * 2 + 1, count); + triangles += static_cast(count) * (segments * 2 - 1); + } + } + + ::glEnable(GL_CULL_FACE); + gl::vao::unbind(); + // the units of the textures were taken behind the back of the texture cache + opengl_texture::reset_unit_cache(); + return triangles; +} diff --git a/rendering/opengl46/opengl46grass.h b/rendering/opengl46/opengl46grass.h new file mode 100644 index 00000000..34c4c056 --- /dev/null +++ b/rendering/opengl46/opengl46grass.h @@ -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 +#include +#include +#include +#include +#include + +#include "rendering/renderer.h" +#include "gl/shader.h" +#include "gl/vao.h" + +class opengl_camera; + +// grass of the heightmap terrain for the opengl 4.6 renderer: blades made up by the vertex shader (grass.vert) where the +// grass is painted, in the chunks near the camera. +// +// a chunk is cut into tiles; each tile visible within the reach of the grass is drawn as instances of a blade, as many as +// its densest kind and its distance ask for. a blade finds its place, its kind and its shape from its number, so the same +// blades stand at the same places in every frame. further away the blades thin out and grow wider, and fade out at the end +// of the reach. the heights, the densities and the holes of a chunk are kept in textures, made when the chunk comes near +class opengl46_grass +{ + public: + opengl46_grass() = default; + ~opengl46_grass(); + + // see gfx_renderer::Terrain_Grass() + void update(std::vector const &Kinds, std::vector const &Chunks); + // draws the grass seen by the camera. View: the modelview of the pass with the camera at its origin; Modelview: hands a + // modelview to the model block of the draws which follow. returns: triangles drawn + std::size_t render(opengl_camera const &Camera, glm::mat4 const &View, std::function const &Modelview); + // drops everything made on the gpu, e.g. when the context goes + void clear(); + bool empty() const { return m_chunks.empty(); } + + static int constexpr tiles{16}; // along a side of a chunk + + private: + struct tile + { + float low{0.f}, high{0.f}; // of the ground + std::array most{}; // the highest density of each kind of the chunk within the tile, 0..1 + }; + struct chunk + { + std::uint64_t version{0}; + glm::dvec2 corner{0.0}; + float size{0.f}; + int cells{0}; + int samples{0}; + std::array kinds{-1, -1, -1, -1}; + GLuint heights{0}; // R32F, the grid points + GLuint density{0}; // RGBA8, the densities of the kinds + GLuint holes{0}; // R8, a single texel of 0 without holes + std::array parts; + bool listed{false}; // in the last list given + }; + + bool init(); + void upload(chunk &Chunk, gfx::grass_chunk const &Source); + void release(chunk &Chunk); + + int m_state{0}; // 0: not set up yet, 1: ready, -1: can't be used + std::unique_ptr m_program; + std::unique_ptr m_vao; // without attributes, the blades are made of the numbers of their vertices + std::vector m_kinds; + std::unordered_map m_chunks; + // locations of the uniforms of the program + struct locations + { + GLint tile{-1}, part{-1}, chunk{-1}, camera{-1}, lod{-1}, wind{-1}; + GLint base{-1}, tip{-1}, shape{-1}, share{-1}; + } m_uniforms; +}; diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index 6f0f53ef..a04d3be7 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -1108,6 +1108,12 @@ void opengl46_renderer::draw_debug_ui() if (Global.gfx_contactshadows > 0) ImGui::TextUnformatted("(drawn anyway for the contact shadows, gfx.contactshadows)"); ImGui::Checkbox("walk the scene once for its depth and colour, drawing both from a record (gfx.depthprepass.replay)", &Global.gfx_depthprepass_replay); + ImGui::Checkbox("grass of the heightmap terrain (gfx.grass)", &Global.gfx_grass); + if (Global.gfx_grass) + { + ImGui::SliderFloat("reach of the grass, m (gfx.grass.distance)", &Global.gfx_grass_distance, 10.f, 250.f, "%.0f"); + ImGui::SliderFloat("density of the grass (gfx.grass.density)", &Global.gfx_grass_density, 0.1f, 2.f, "%.2f"); + } auto info{m_instancecull.info()}; if (Global.gfx_gpucull_shadows) { @@ -1428,6 +1434,13 @@ void opengl46_renderer::Render_pass(viewport_config &vp, rendermode const Mode) m_scenedepthdrawn = false; m_indirectcolourpending = false; + // the grass of the terrain, opaque, over the ground drawn + if (vp.main && (false == Global.bWireFrame)) + { + gpupart("grass"); + Render_grass(future); + } + Render_vr_models(); // ...translucent parts @@ -2742,6 +2755,14 @@ bool opengl46_renderer::Replace(gfx::vertex_array &Vertices, gfx::userdata_array return m_geometry.replace(Vertices, Userdata, Geometry, Offset); } +// replaces the index and the vertex data of specified indexed chunk with the supplied data of the same counts +bool opengl46_renderer::Replace_Indexed(gfx::index_array &Indices, gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry) +{ + // NOTE: we expect indexed geometry to come with calculated tangents + m_chunkspheres.erase(static_cast(Geometry)); + return m_geometry.replace(Indices, Vertices, Userdata, Geometry); +} + // adds supplied vertex data at the end of specified chunk bool opengl46_renderer::Append(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type) { @@ -3041,13 +3062,14 @@ IMaterial const *opengl46_renderer::Material(material_handle const Material) con return &m_materials.material(Material); } -material_handle opengl46_renderer::Terrain_Material(material_handle const Reuse, std::vector const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement) +material_handle opengl46_renderer::Terrain_Material(material_handle const Reuse, std::vector const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement, + int const HoleSamples, std::uint8_t const *Holes) { - auto const material{m_terrainmaterials.make(Reuse, Layers, Samples, Weights, Placement)}; + auto const material{m_terrainmaterials.make(Reuse, Layers, Samples, Weights, Placement, HoleSamples, Holes)}; 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); + return gfx_renderer::Terrain_Material(Reuse, Layers, Samples, Weights, Placement, HoleSamples, Holes); } void opengl46_renderer::Terrain_Release(material_handle const Material) @@ -3055,6 +3077,11 @@ void opengl46_renderer::Terrain_Release(material_handle const Material) m_terrainmaterials.release(Material); } +void opengl46_renderer::Terrain_Grass(std::vector const &Kinds, std::vector const &Chunks) +{ + m_grass.update(Kinds, Chunks); +} + opengl_material &opengl46_renderer::Material(material_handle const Material) { return m_materials.material(Material); @@ -6801,6 +6828,27 @@ void opengl46_renderer::Render(TMemCell *Memcell) ::glPopMatrix(); } +void opengl46_renderer::Render_grass(glm::mat4 const &Future) +{ + if (false == Global.gfx_grass || m_grass.empty()) + return; + glDebug("render grass"); + // the blades bring their colour, the lights do the rest + model_ubs.future = Future; + model_ubs.param[0] = glm::vec4(1.f); + model_ubs.param[1] = glm::vec4(1.f, 1.f, 0.f, 1.f); // diffuse, specular, reflection, glossiness + model_ubs.param[2] = glm::vec4(0.f); + model_ubs.emission = 0.f; + model_ubs.alpha_mult = 1.f; + model_ubs.opacity = 0.5f; + glm::mat4 const view{OpenGLMatrices.data(GL_MODELVIEW)}; + auto const triangles{m_grass.render(m_renderpass.pass_camera, view, [this](glm::mat4 const &Modelview) { + model_ubs.set_modelview(Modelview); + model_ubo->update(model_ubs); + })}; + m_renderpass.draw_stats.triangles += static_cast(triangles); +} + void opengl46_renderer::Render_particles() { model_ubs.set_modelview(OpenGLMatrices.data(GL_MODELVIEW)); diff --git a/rendering/opengl46/opengl46renderer.h b/rendering/opengl46/opengl46renderer.h index 71213811..188eead0 100644 --- a/rendering/opengl46/opengl46renderer.h +++ b/rendering/opengl46/opengl46renderer.h @@ -34,6 +34,7 @@ http://mozilla.org/MPL/2.0/. #include "gl/pbo.h" #include "gl/query.h" #include "rendering/opengl46/opengl46terrain.h" +#include "rendering/opengl46/opengl46grass.h" #include "rendering/opengl46/opengl46sky.h" #include "rendering/opengl46/opengl46weather.h" #include "rendering/opengl46/opengl46exposure.h" @@ -77,6 +78,8 @@ class opengl46_renderer : public gfx_renderer { gfx::geometry_handle Insert(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometrybank_handle const &Geometry, int const Type) override; // replaces data of specified chunk with the supplied vertex data, starting from specified offset bool Replace(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type, const std::size_t Offset = 0) override; + // replaces the index and the vertex data of specified indexed chunk with the supplied data of the same counts + bool Replace_Indexed(gfx::index_array &Indices, gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry) override; // adds supplied vertex data at the end of specified chunk bool Append(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type) override; // provides direct access to index data of specfied chunk @@ -102,9 +105,12 @@ class opengl46_renderer : public gfx_renderer { IMaterial const * Material( material_handle const Material ) const override; material_handle - Terrain_Material( material_handle const Reuse, std::vector const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement ) override; + Terrain_Material( material_handle const Reuse, std::vector const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement, + int const HoleSamples = 0, std::uint8_t const *Holes = nullptr ) override; void Terrain_Release( material_handle const Material ) override; + void + Terrain_Grass( std::vector const &Kinds, std::vector const &Chunks ) override; // shader methods auto Fetch_Shader( std::string const &name ) -> std::shared_ptr override; // texture methods @@ -489,6 +495,8 @@ class opengl46_renderer : public gfx_renderer { void Render(TMemCell *Memcell); void Render_particles(); void Render_precipitation(); + // the grass of the heightmap terrain around the camera of the main view; Future: the matrix of the motion vectors + void Render_grass(glm::mat4 const &Future); void Render_vr_models(); void Render_Alpha(scene::basic_region *Region); void Render_Alpha(cell_sequence::reverse_iterator First, cell_sequence::reverse_iterator Last); @@ -544,6 +552,7 @@ class opengl46_renderer : public gfx_renderer { material_manager m_materials; texture_manager m_textures; opengl46_terrain_materials m_terrainmaterials{m_materials, m_textures}; + opengl46_grass m_grass; // of the heightmap terrain opengl46_light m_sunlight; // fog colour of the frame: the one of the simulation, or of the atmosphere (see opengl46_sky::update()) glm::vec3 m_fogcolor{0.f}; diff --git a/rendering/opengl46/opengl46terrain.cpp b/rendering/opengl46/opengl46terrain.cpp index 3e73952c..294a7338 100644 --- a/rendering/opengl46/opengl46terrain.cpp +++ b/rendering/opengl46/opengl46terrain.cpp @@ -79,7 +79,7 @@ bool opengl46_terrain_materials::init() // 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"); + m_program = GfxRenderer->Fetch_Shader("terrain"); } catch (gl::shader_exception const &e) { @@ -192,6 +192,7 @@ std::size_t opengl46_terrain_materials::acquire_slot(material_handle const Reuse 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); + entry.holes = make_texture("internal_src:terrain/holes_" + 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) @@ -211,6 +212,18 @@ std::size_t opengl46_terrain_materials::acquire_slot(material_handle const Reuse std::array const zeros{}; ::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, static_cast(max_layers), 2, 0, GL_RGBA, GL_FLOAT, zeros.data()); } + { + // led between the samples, so the edges of the holes run smoothly across the cells + bind_for_upload(m_textures.texture(entry.holes)); + ::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_MIN_FILTER, GL_LINEAR); + ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + std::uint8_t const none{0}; + ::glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + ::glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, 1, 1, 0, GL_RED, GL_UNSIGNED_BYTE, &none); + ::glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + } opengl_texture::reset_unit_cache(); auto const name = [&](texture_handle const Handle) { return "internal_src:" + m_textures.texture(Handle).name; }; std::string const definition{ @@ -221,7 +234,8 @@ std::size_t opengl46_terrain_materials::acquire_slot(material_handle const Reuse "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"}; + "texture7: " + name(entry.table) + "\n" + "texture8: " + name(entry.holes) + "\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) { @@ -235,7 +249,8 @@ std::size_t opengl46_terrain_materials::acquire_slot(material_handle const Reuse return index; } -material_handle opengl46_terrain_materials::make(material_handle const Reuse, std::vector const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement) +material_handle opengl46_terrain_materials::make(material_handle const Reuse, std::vector const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement, + int const HoleSamples, std::uint8_t const *Holes) { if (Layers.empty() || false == init()) return null_handle; @@ -286,8 +301,32 @@ material_handle opengl46_terrain_materials::make(material_handle const Reuse, st entry.rotations[i] = Layers[i].rotation; write_table(entry); + // the holes: the cells cut out entirely are left out of the mesh, the program of the holes cuts the rest of the openings + auto const cut{Holes != nullptr && HoleSamples > 0}; + { + auto const holeside{cut ? HoleSamples + 1 : 1}; + std::uint8_t const none{0}; + bind_for_upload(m_textures.texture(entry.holes)); + ::glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + ::glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, holeside, holeside, 0, GL_RED, GL_UNSIGNED_BYTE, cut ? Holes : &none); + ::glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + 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(side - 1)); + if (m_holesprogram == nullptr && cut) + { + try + { + m_holesprogram = GfxRenderer->Fetch_Shader("terrain_holes"); + } + catch (gl::shader_exception const &e) + { + ErrorLog("heightmap terrain: can't use the shader of the holes: " + std::string(e.what())); + } + } + // the program of the holes discards fragments, which the depth prepass of the plain one never does + material.shader = (cut && m_holesprogram != nullptr) ? m_holesprogram : m_program; return entry.material; } diff --git a/rendering/opengl46/opengl46terrain.h b/rendering/opengl46/opengl46terrain.h index 76997f42..abcf74ab 100644 --- a/rendering/opengl46/opengl46terrain.h +++ b/rendering/opengl46/opengl46terrain.h @@ -20,7 +20,7 @@ http://mozilla.org/MPL/2.0/. #include "gl/framebuffer.h" #include "gl/postfx.h" -// materials of the heightmap terrain for the opengl 3.3 renderer (shader mat_terrain). +// materials of the heightmap terrain for the opengl 4.6 renderer (shader mat_terrain, mat_terrain_holes for chunks with holes). // 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 opengl46_terrain_materials @@ -29,7 +29,8 @@ class opengl46_terrain_materials opengl46_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 const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement); + material_handle make(material_handle const Reuse, std::vector const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement, + int const HoleSamples = 0, std::uint8_t const *Holes = nullptr); 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(); @@ -63,6 +64,7 @@ class opengl46_terrain_materials material_handle material{null_handle}; std::array weights{null_handle, null_handle}; texture_handle table{null_handle}; + texture_handle holes{null_handle}; // where the ground is cut, R8; a single texel of 0 without holes bool used{false}; // what the table was made of, kept to make it again when a layer changes std::size_t count{0}; @@ -90,6 +92,8 @@ class opengl46_terrain_materials std::vector m_slots; std::vector m_free; std::unordered_map m_slotof; + std::shared_ptr m_program; // of the chunks, mat_terrain + std::shared_ptr m_holesprogram; // of the chunks with holes, mat_terrain_holes std::unique_ptr m_copy; std::unique_ptr m_framebuffer; }; diff --git a/rendering/renderer.cpp b/rendering/renderer.cpp index d9d4b5df..d3116c80 100644 --- a/rendering/renderer.cpp +++ b/rendering/renderer.cpp @@ -39,7 +39,7 @@ 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 const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement ) +material_handle gfx_renderer::Terrain_Material( material_handle const Reuse, std::vector const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement, int const HoleSamples, std::uint8_t const *Holes ) { // without blending the chunk gets the material which covers the most of it if( Layers.empty() ) { diff --git a/rendering/renderer.h b/rendering/renderer.h index 649dd795..be4bc9f8 100644 --- a/rendering/renderer.h +++ b/rendering/renderer.h @@ -9,6 +9,9 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include +#include + #include "rendering/geometrybank.h" #include "interfaces/IMaterial.h" #include "interfaces/ITexture.h" @@ -30,6 +33,35 @@ struct terrain_layer float size{0.f}; float rotation{0.f}; // degrees the textures are turned by }; + +// kind of grass grown over the heightmap terrain (heightmap::grass_def), colours in linear rgb +struct grass_kind +{ + glm::vec3 base{0.f}; // at the root of the blades + glm::vec3 tip{0.f}; + float height{0.3f}; // metres + float variation{0.4f}; // share of the height the blades differ by, either way + float width{0.012f}; // metres, at the root + float density{60.f}; // blades per square metre where painted fully + float bend{0.3f}; // share of the height the tips lean over by +}; + +// a chunk of the heightmap terrain the grass grows on, see gfx_renderer::Terrain_Grass() +struct grass_chunk +{ + std::uint64_t id{0}; // the same for the same chunk, as long as it's in memory + std::uint64_t version{0}; // changes with every change of what the grass is made of + glm::dvec2 corner{0.0}; // x, z + float size{0.f}; // metres along a side + float low{0.f}, high{0.f}; // the lowest and the highest point + int cells{0}; // quads of the grid along a side + float const *heights{nullptr}; // (cells + 1)^2, rows of growing z + int samples{0}; // grass samples along a side less one + std::uint8_t const *density{nullptr}; // (samples + 1)^2 per kind, planar + std::array kinds{-1, -1, -1, -1}; // indices of the kinds of the samples, -1 unused + int holes{0}; // hole samples along a side less one, 0: none + std::uint8_t const *hole_mask{nullptr}; // (holes + 1)^2, 255: cut +}; } // namespace gfx class gfx_renderer { @@ -57,6 +89,9 @@ public: virtual auto Insert(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometrybank_handle const &Geometry, int const Type) -> gfx::geometry_handle = 0; // replaces data of specified chunk with the supplied vertex data, starting from specified offset virtual auto Replace(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type, const std::size_t Offset = 0) -> bool = 0; + // replaces the index and the vertex data of specified indexed chunk with the supplied data of the same counts. + // returns: false if it wasn't replaced (the counts differ, or the backend can't) + virtual auto Replace_Indexed( gfx::index_array &Indices, gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry ) -> bool { return false; } // adds supplied vertex data at the end of specified chunk virtual auto Append(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type) -> bool = 0; // provides direct access to index data of specfied chunk @@ -78,10 +113,17 @@ public: // 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. + // Holes: (HoleSamples + 1)^2 values (255: the ground is cut there), rows like the weights; null without holes. the cells + // cut out entirely are left out of the geometry, the material cuts the rest of the openings. // 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 const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement ) -> material_handle; + virtual auto Terrain_Material( material_handle const Reuse, std::vector const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement, + int const HoleSamples = 0, std::uint8_t const *Holes = nullptr ) -> material_handle; // gives back a material made by Terrain_Material, once its chunk is gone virtual void Terrain_Release( material_handle const Material ) {} + // heightmap terrain: the grass to draw, the kinds of the grass palette and the chunks near the camera which have grass. + // the data of the chunks is read during the call only; a chunk the renderer has with the same id and version isn't read + // again. chunks left out are dropped. backends which can't draw grass ignore it + virtual void Terrain_Grass( std::vector const &Kinds, std::vector const &Chunks ) {} // shader methods virtual auto Fetch_Shader( std::string const &name ) -> std::shared_ptr = 0; // texture methods diff --git a/scene/heightmapterrain.cpp b/scene/heightmapterrain.cpp index e7b8f491..b346f764 100644 --- a/scene/heightmapterrain.cpp +++ b/scene/heightmapterrain.cpp @@ -39,6 +39,8 @@ std::uint32_t constexpr tag_heights{fourcc('H', 'G', 'H', 'T')}; std::uint32_t constexpr tag_base{fourcc('H', 'B', 'A', 'S')}; std::uint32_t constexpr tag_paint{fourcc('S', 'P', 'L', 'T')}; std::uint32_t constexpr tag_adjust{fourcc('H', 'A', 'D', 'J')}; +std::uint32_t constexpr tag_holes{fourcc('H', 'O', 'L', 'E')}; +std::uint32_t constexpr tag_grass{fourcc('G', 'R', 'A', 'S')}; std::uint32_t constexpr pack_magic{fourcc('T', 'C', 'H', '1')}; std::uint32_t constexpr pack_version{1}; @@ -188,6 +190,11 @@ float valid_spacing(float const Spacing) return best; } +int detail_samples(int const Cells) +{ + return std::max(default_paint_samples, Cells); +} + chunk_key chunk_at(double const X, double const Z) { return {static_cast(std::floor(X / chunk_size)), static_cast(std::floor(Z / chunk_size))}; @@ -429,6 +436,111 @@ void chunk_data::compact_layers() weights = std::move(kept); } +float chunk_data::hole_value(double const X, double const Z) const +{ + if (false == has_holes()) + return 0.f; + auto const low{corner()}; + auto const step{chunk_size / holes}; + auto const fx{std::clamp((X - low.x) / step, 0.0, static_cast(holes))}; + auto const fz{std::clamp((Z - low.y) / step, 0.0, static_cast(holes))}; + auto const ix{std::min(static_cast(fx), holes - 1)}; + auto const iz{std::min(static_cast(fz), holes - 1)}; + auto const tx{fx - ix}, tz{fz - iz}; + auto const side{static_cast(holes + 1)}; + auto const at = [&](int const X_, int const Z_) { return hole_mask[static_cast(Z_) * side + X_] / 255.0; }; + return static_cast((at(ix, iz) * (1.0 - tx) + at(ix + 1, iz) * tx) * (1.0 - tz) + (at(ix, iz + 1) * (1.0 - tx) + at(ix + 1, iz + 1) * tx) * tz); +} + +bool chunk_data::cut_cells(int const Ix0, int const Iz0, int const Ix1, int const Iz1) const +{ + if (false == has_holes() || cells <= 0) + return false; + // every sample the surface of the cells is led from has to be cut, then all of it is + auto const ratio{static_cast(holes) / cells}; + auto const range = [&](int const From, int const To) { + return std::make_pair(std::clamp(static_cast(std::floor(From * ratio + 1e-9)), 0, holes), std::clamp(static_cast(std::ceil(To * ratio - 1e-9)), 0, holes)); + }; + auto const [x0, x1]{range(Ix0, Ix1)}; + auto const [z0, z1]{range(Iz0, Iz1)}; + auto const side{static_cast(holes + 1)}; + for (int z = z0; z <= z1; ++z) + for (int x = x0; x <= x1; ++x) + if (hole_mask[static_cast(z) * side + x] < 128) + return false; + return true; +} + +void chunk_data::ensure_holes() +{ + if (has_holes()) + return; + holes = detail_samples(cells); + hole_mask.assign(static_cast(holes + 1) * (holes + 1), 0); +} + +void chunk_data::compact_holes() +{ + if (has_holes() && std::any_of(hole_mask.begin(), hole_mask.end(), [](std::uint8_t const Value) { return Value != 0; })) + return; + holes = 0; + hole_mask.clear(); +} + +std::size_t chunk_data::grass_count() const +{ + if (grass <= 0) + return 0; + std::size_t count{0}; + while (count < grass_kinds.size() && grass_kinds[count] != no_layer) + ++count; + return count; +} + +int chunk_data::grass_slot_of(std::uint16_t const Kind) const +{ + for (std::size_t slot = 0; slot < grass_count(); ++slot) + if (grass_kinds[slot] == Kind) + return static_cast(slot); + return -1; +} + +void chunk_data::ensure_grass() +{ + if (grass > 0) + return; + grass = default_paint_samples; + grass_kinds.fill(no_layer); + grass_density.clear(); +} + +void chunk_data::compact_grass() +{ + if (grass <= 0) + return; + auto const samples{static_cast(grass + 1) * (grass + 1)}; + auto const count{grass_count()}; + std::vector kept; + std::array keptkinds; + keptkinds.fill(no_layer); + std::size_t used{0}; + for (std::size_t slot = 0; slot < count; ++slot) + { + auto const begin{grass_density.begin() + slot * samples}; + if (slot * samples + samples > grass_density.size() || std::all_of(begin, begin + samples, [](std::uint8_t const Value) { return Value == 0; })) + continue; + kept.insert(kept.end(), begin, begin + samples); + keptkinds[used++] = grass_kinds[slot]; + } + grass_kinds = keptkinds; + grass_density = std::move(kept); + if (used == 0) + { + grass = 0; + grass_density.clear(); + } +} + // modifiers std::pair modifier::bounds() const @@ -541,6 +653,19 @@ std::optional modifier_shape::evaluate(double const X, d // manifest +std::vector const &grass_presets() +{ + static std::vector const presets{ + // name, root, tip, height, variation, width, density, bend + {"meadow", {0.22f, 0.33f, 0.09f}, {0.58f, 0.68f, 0.27f}, 0.25f, 0.45f, 0.020f, 90.f, 0.30f}, + {"lawn", {0.18f, 0.30f, 0.08f}, {0.42f, 0.58f, 0.20f}, 0.07f, 0.30f, 0.012f, 200.f, 0.15f}, + {"dry grass", {0.36f, 0.32f, 0.16f}, {0.80f, 0.72f, 0.45f}, 0.30f, 0.45f, 0.016f, 70.f, 0.40f}, + {"tall grass", {0.18f, 0.27f, 0.07f}, {0.60f, 0.66f, 0.30f}, 0.70f, 0.35f, 0.022f, 40.f, 0.45f}, + {"weeds", {0.16f, 0.25f, 0.08f}, {0.45f, 0.55f, 0.22f}, 0.16f, 0.60f, 0.030f, 30.f, 0.25f}, + }; + return presets; +} + bool load_manifest(std::string const &Path, manifest &Manifest, std::string *Error) { try @@ -615,6 +740,30 @@ bool load_manifest(std::string const &Path, manifest &Manifest, std::string *Err if (item.points.size() >= 2) result.modifiers.push_back(std::move(item)); } + if (auto const node{document["grass"]}; node && node.IsSequence()) + for (auto const &entry : node) + { + grass_def kind; + auto const colour = [](YAML::Node const &Node, glm::vec3 &Colour) { + if (Node && Node.IsSequence() && Node.size() >= 3) + Colour = glm::clamp(glm::vec3{Node[0].as(), Node[1].as(), Node[2].as()}, glm::vec3{0.f}, glm::vec3{1.f}); + }; + if (auto const name{entry["name"]}) + kind.name = name.as(); + colour(entry["base"], kind.base); + colour(entry["tip"], kind.tip); + if (auto const height{entry["height"]}) + kind.height = std::max(0.01f, height.as()); + if (auto const variation{entry["variation"]}) + kind.variation = std::clamp(variation.as(), 0.f, 1.f); + if (auto const width{entry["width"]}) + kind.width = std::max(0.001f, width.as()); + if (auto const density{entry["density"]}) + kind.density = std::max(0.f, density.as()); + if (auto const bend{entry["bend"]}) + kind.bend = std::clamp(bend.as(), 0.f, 1.f); + result.grass.push_back(std::move(kind)); + } Manifest = std::move(result); return true; } @@ -689,6 +838,24 @@ bool save_manifest(std::string const &Path, manifest const &Manifest, std::strin } out << YAML::EndSeq; } + if (false == Manifest.grass.empty()) + { + out << YAML::Key << "grass" << YAML::Value << YAML::BeginSeq; + for (auto const &kind : Manifest.grass) + { + out << YAML::BeginMap; + out << YAML::Key << "name" << YAML::Value << kind.name; + out << YAML::Key << "base" << YAML::Value << YAML::Flow << YAML::BeginSeq << kind.base.r << kind.base.g << kind.base.b << YAML::EndSeq; + out << YAML::Key << "tip" << YAML::Value << YAML::Flow << YAML::BeginSeq << kind.tip.r << kind.tip.g << kind.tip.b << YAML::EndSeq; + out << YAML::Key << "height" << YAML::Value << kind.height; + out << YAML::Key << "variation" << YAML::Value << kind.variation; + out << YAML::Key << "width" << YAML::Value << kind.width; + out << YAML::Key << "density" << YAML::Value << kind.density; + out << YAML::Key << "bend" << YAML::Value << kind.bend; + out << YAML::EndMap; + } + out << YAML::EndSeq; + } out << YAML::EndMap; if (false == out.good()) { @@ -844,7 +1011,7 @@ std::shared_ptr pack_file::read(chunk_key const &Key, std::string *E auto chunk{std::make_shared()}; chunk->key = Key; - slice const *heights{nullptr}, *base{nullptr}, *adjust{nullptr}, *paint{nullptr}; + slice const *heights{nullptr}, *base{nullptr}, *adjust{nullptr}, *paint{nullptr}, *holes{nullptr}, *grass{nullptr}; bool header{false}; for (auto const &slice : slices) { @@ -871,6 +1038,10 @@ std::shared_ptr pack_file::read(chunk_key const &Key, std::string *E adjust = &slice; else if (slice.tag == tag_paint) paint = &slice; + else if (slice.tag == tag_holes) + holes = &slice; + else if (slice.tag == tag_grass) + grass = &slice; // slices of unknown kinds are left for newer versions of the simulator } if (false == header || heights == nullptr || chunk->cells <= 0 || chunk->cells > 4096) @@ -892,6 +1063,34 @@ std::shared_ptr pack_file::read(chunk_key const &Key, std::string *E if (paint == nullptr || false == load(*paint, chunk->weights) || chunk->weights.size() != expected) return fail("damaged paint"); } + if (holes != nullptr) + { + if (false == load(*holes, data)) + return fail("damaged holes"); + reader input{data.data(), data.size()}; + std::uint16_t samples{0}; + input.get(samples); + auto const count{static_cast(samples + 1) * (samples + 1)}; + chunk->holes = samples; + chunk->hole_mask.resize(count); + if (samples == 0 || input.left() != count || false == input.get(chunk->hole_mask.data(), count)) + return fail("damaged holes"); + } + if (grass != nullptr) + { + if (false == load(*grass, data)) + return fail("damaged grass"); + reader input{data.data(), data.size()}; + std::uint16_t samples{0}; + input.get(samples); + for (auto &kind : chunk->grass_kinds) + input.get(kind); + chunk->grass = samples; + auto const count{static_cast(samples + 1) * (samples + 1) * chunk->grass_count()}; + chunk->grass_density.resize(count); + if (samples == 0 || input.left() != count || false == input.get(chunk->grass_density.data(), count)) + return fail("damaged grass"); + } return chunk; } @@ -958,6 +1157,22 @@ bool write_pack(std::string const &Path, pack_key const &Key, std::vectorpaint > 0 && chunk->layer_count() > 1) add(table, tag_paint, chunk->weights); + if (chunk->has_holes()) + { + data.clear(); + put(data, static_cast(chunk->holes)); + data.insert(data.end(), chunk->hole_mask.begin(), chunk->hole_mask.end()); + add(table, tag_holes, data); + } + if (auto const kinds{chunk->grass_count()}; kinds > 0 && chunk->grass_density.size() == static_cast(chunk->grass + 1) * (chunk->grass + 1) * kinds) + { + data.clear(); + put(data, static_cast(chunk->grass)); + for (auto const kind : chunk->grass_kinds) + put(data, kind); + data.insert(data.end(), chunk->grass_density.begin(), chunk->grass_density.end()); + add(table, tag_grass, data); + } } std::vector file; @@ -1146,6 +1361,42 @@ void build_indices(int const Cells, int const Stride, gfx::index_array &Out) skirt(grid + 3 * row, [&](gfx::basic_index const K) { return at(n, K); }, false); } +void cut_indices(chunk_data const &Chunk, int const Stride, gfx::index_array &Indices) +{ + if (false == Chunk.has_holes()) + return; + auto const cells{Chunk.cells}; + auto const stride{std::clamp(Stride, 1, max_stride(cells))}; + auto const n{segments(cells, stride)}; + auto const grid{static_cast(n) * n * 6}; + if (Indices.size() != grid + static_cast(n) * 24) + return; + auto const coordinate = [&](int const I) { return std::min(I * stride, cells); }; + // the triangles of a cell cut out entirely become empty ones, so the count of the indices stays + std::vector cut(static_cast(n) * n, false); + for (int j = 0; j < n; ++j) + for (int i = 0; i < n; ++i) + { + if (false == Chunk.cut_cells(coordinate(i), coordinate(j), coordinate(i + 1), coordinate(j + 1))) + continue; + cut[static_cast(j) * n + i] = true; + std::fill_n(Indices.begin() + (static_cast(j) * n + i) * 6, 6, Indices[(static_cast(j) * n + i) * 6]); + } + // the skirts along the cells cut out would hang in the opening + auto const skirt = [&](std::size_t const Skirt, auto const &Cell) { + for (int k = 0; k < n; ++k) + if (Cell(k)) + { + auto const first{grid + (Skirt * n + k) * 6}; + std::fill_n(Indices.begin() + first, 6, Indices[first]); + } + }; + skirt(0, [&](int const K) { return cut[K]; }); + skirt(1, [&](int const K) { return cut[static_cast(n - 1) * n + K]; }); + skirt(2, [&](int const K) { return cut[static_cast(K) * n]; }); + skirt(3, [&](int const K) { return cut[static_cast(K) * n + n - 1]; }); +} + void build_mesh(chunk_data const &Chunk, int const Stride, glm::dvec3 const &Origin, height_source const &Outside, mesh &Out) { auto const cells{Chunk.cells}; @@ -1223,6 +1474,9 @@ void build_mesh(chunk_data const &Chunk, int const Stride, glm::dvec3 const &Ori vertex(cells, coordinate(k), skirtdepth); build_indices(cells, stride, Out.indices); + Out.cut = Chunk.has_holes(); + if (Out.cut) + cut_indices(Chunk, stride, Out.indices); Out.centre = (lowest + highest) * 0.5; Out.radius = static_cast(glm::length(highest - lowest) * 0.5 + skirtdepth); Out.low = lowest - glm::dvec3{0.0, skirtdepth, 0.0}; diff --git a/scene/heightmapterrain.h b/scene/heightmapterrain.h index 707af808..0a0a077e 100644 --- a/scene/heightmapterrain.h +++ b/scene/heightmapterrain.h @@ -35,6 +35,7 @@ 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 +std::size_t constexpr max_chunk_grass{4}; // kinds of grass growing on a single chunk // point spacings a chunk can have, finest first std::array constexpr spacings{0.5f, 1.f, 2.f, 5.f}; @@ -43,6 +44,9 @@ float constexpr default_spacing{2.f}; int cells_for(float const Spacing); // the allowed spacing closest to specified one float valid_spacing(float const Spacing); +// samples along the side of a chunk less one, of the holes and the grass of a chunk with specified cells: a sample a metre, +// or one at every grid point of a finer grid +int detail_samples(int const Cells); using chunk_key = std::pair; using pack_key = std::pair; @@ -70,8 +74,22 @@ struct chunk_data int paint{0}; // paint samples along a side, less one. 0: the chunk is covered with its first layer alone std::array layers; // indices of the materials in the terrain palette, no_layer for unused std::vector weights; // planar, (paint + 1)^2 per used layer, rows like the heights; weights of a sample add up to 255 + // holes cut in the ground, for the portals of tunnels and the like: (holes + 1)^2 samples, rows like the heights, 255 where + // the ground is cut and 0 where it stays. the surface is cut where the value led between the samples is over the half. + // 0: no holes + int holes{0}; + std::vector hole_mask; + // grass grown on the chunk by the renderer: the density (0..255) of up to 4 kinds of the grass palette at (grass + 1)^2 + // samples, planar per used kind, rows like the heights. the densities of a sample add up to 255 at most. 0: no grass + int grass{0}; + std::array grass_kinds; // indices of the kinds in the grass palette, no_layer for unused + std::vector grass_density; - chunk_data() { layers.fill(no_layer); } + chunk_data() + { + layers.fill(no_layer); + grass_kinds.fill(no_layer); + } int side() const { return cells + 1; } double spacing() const { return chunk_size / cells; } @@ -101,6 +119,28 @@ struct chunk_data 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(); + + // holes + bool has_holes() const { return holes > 0 && hole_mask.size() == static_cast(holes + 1) * (holes + 1); } + // 0..1, how much the point is cut, led between the samples; the surface is cut where it's over the half + float hole_value(double const X, double const Z) const; + bool hole_at(double const X, double const Z) const { return has_holes() && hole_value(X, Z) > 0.5f; } + // true if the whole of the grid cells from (Ix0, Iz0) to (Ix1, Iz1) (exclusive) is cut out + bool cut_cells(int const Ix0, int const Iz0, int const Ix1, int const Iz1) const; + // gives the chunk a hole mask with nothing cut, if it has none + void ensure_holes(); + // drops the hole mask if nothing is cut + void compact_holes(); + + // grass + // number of kinds of grass in use, the used ones come first + std::size_t grass_count() const; + // grass slot of specified kind of the palette, or -1 + int grass_slot_of(std::uint16_t const Kind) const; + // gives the chunk grass samples (with no kind) if it has none yet + void ensure_grass(); + // drops the kinds left without density anywhere, and the samples altogether when no grass is left + void compact_grass(); }; using chunk_ptr = std::shared_ptr; @@ -171,6 +211,22 @@ struct layer_def float rotation{0.f}; // degrees the textures are turned by, counterclockwise seen from above }; +// kind of grass of the grass palette: blades the renderer makes up where the grass is painted +struct grass_def +{ + std::string name; + glm::vec3 base{0.22f, 0.33f, 0.09f}; // colour at the root of the blades, sRGB 0..1 + glm::vec3 tip{0.58f, 0.68f, 0.27f}; // colour at the tips + float height{0.25f}; // metres, of an average blade + float variation{0.45f}; // share of the height the blades differ by, either way + float width{0.02f}; // metres, of a blade at its root + float density{90.f}; // blades per square metre where the kind is painted fully + float bend{0.3f}; // how far the tips lean over, share of the height +}; + +// a few kinds to start the palette with +std::vector const &grass_presets(); + // description of the terrain, terrain.yaml struct manifest { @@ -181,6 +237,7 @@ struct manifest float spacing{default_spacing}; // point spacing of new chunks std::vector water; std::vector modifiers; + std::vector grass; // the grass palette }; bool load_manifest(std::string const &Path, manifest &Manifest, std::string *Error = nullptr); @@ -202,6 +259,8 @@ bool save_manifest(std::string const &Path, manifest const &Manifest, std::strin // 'HBAS' heights before the modifiers, as 'HGHT' // 'HADJ' touch-ups over the modifiers, as 'HGHT' (only with 'HBAS'; older versions skip it and keep the heights) // 'SPLT' paint weights: uint8 per sample per used layer, planar +// 'HOLE' holes: uint16 samples less one, then uint8 per sample (255: cut) +// 'GRAS' grass: uint16 samples less one, uint16 kinds[4], then uint8 density per sample per used kind, planar class pack_file { public: @@ -247,12 +306,17 @@ struct mesh 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 + // the chunk has holes: the cells cut out are left out of the indices (their triangles made empty, so the count of the + // indices is the same as without the holes), the indices are its own + bool cut{false}; }; // 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); +// leaves the cells of the chunk cut out entirely, and the skirts along them, out of the indices made by build_indices() +void cut_indices(chunk_data const &Chunk, int const Stride, gfx::index_array &Indices); // vertices the mesh of a chunk with specified cells and stride has std::size_t mesh_vertex_count(int const Cells, int const Stride); diff --git a/shaders/grass.frag b/shaders/grass.frag new file mode 100644 index 00000000..49128796 --- /dev/null +++ b/shaders/grass.frag @@ -0,0 +1,41 @@ +// grass of the heightmap terrain (opengl46_grass, grass.vert): the blades lit like the rest of the scene + +in vec3 f_normal; +in vec4 f_pos; +in vec3 f_albedo; +in vec4 f_clip_pos; +in vec4 f_clip_future_pos; + +#include + +layout(location = 0) out vec4 out_color; +#if MOTIONBLUR_ENABLED +layout(location = 1) out vec4 out_motion; +#endif + +#include +#include +#include + +void main() +{ + vec3 fragnormal = normalize(f_normal); + glossiness = 6.0; + metalic = 0.0; + vec3 fragcolor = apply_lights(ambient, fragnormal, f_albedo, 0.0, 0.2, shadow_tone); + + vec4 color = vec4(apply_fog(fragcolor), 1.0); +#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 +} diff --git a/shaders/grass.vert b/shaders/grass.vert new file mode 100644 index 00000000..1d62b607 --- /dev/null +++ b/shaders/grass.vert @@ -0,0 +1,206 @@ +// grass of the heightmap terrain (opengl46_grass): a blade made of the numbers of its vertex and its instance. +// +// the instances of a draw stand in a tile of a chunk. a blade takes its place in the tile from a sequence which spreads +// any number of its first members evenly (R2, shifted for each tile), so drawing fewer of them thins the grass out +// evenly; a blade picks its kind by the densities painted there, and gets its height, its turn and its lean from its own +// numbers, so it's the same in every frame. blades of no kind, in the holes, or past the share drawn at their distance are +// collapsed into a point outside the view. the vertices go up the blade in pairs, the last one is its tip. +// the chunk is drawn relative to its corner: x, z in metres from the corner, y the height as it is + +#include + +// x, z: corner of the tile in the chunk, metres; y: side of the tile; w: blades of the tile where its densest kind is painted fully +uniform vec4 grass_tile; +// x: the share of the densest kind at the densest place of the tile (the blades of the tile are counted for it); y: segments +// of a blade; z, w: number of the tile in the world +uniform vec4 grass_part; +// x: side of the chunk, metres; y: cells of its grid along a side; z: grass samples along a side less one +uniform vec4 grass_chunk; +// the camera relative to the corner of the chunk (y as it is) +uniform vec4 grass_camera; +// x: distance the grass starts to thin out at; y: reach of the grass +uniform vec4 grass_lod; +// x, y: direction of the wind (x, z of the world); z: its strength, 0..1; w: time, seconds +uniform vec4 grass_wind; +// the kinds of the chunk: colour at the root and at the tip (linear rgb), and height, its variation, width, lean +uniform vec4 grass_base[4]; +uniform vec4 grass_tip[4]; +uniform vec4 grass_shape[4]; +// share of the blades of each kind against the densest one of the chunk +uniform vec4 grass_share; + +layout(binding = 0) uniform sampler2D grass_heights; // heights of the grid points, R32F +layout(binding = 1) uniform sampler2D grass_density; // densities of the kinds, RGBA8 +layout(binding = 2) uniform sampler2D grass_holes; // holes cut in the ground, R8 + +out vec3 f_normal; +out vec4 f_pos; +out vec3 f_albedo; +out vec4 f_clip_pos; +out vec4 f_clip_future_pos; +out vec4 UserData; + +uint hash(uint x) +{ + x ^= x >> 16; + x *= 0x7feb352dU; + x ^= x >> 15; + x *= 0x846ca68bU; + x ^= x >> 16; + return x; +} + +float random(inout uint state) +{ + state = hash(state); + return float(state >> 8) * (1.0 / 16777216.0); +} + +// the share of the blades drawn at specified distance; the same as lod_share() of opengl46grass.cpp +float lod_share(float distance) +{ + return distance <= grass_lod.x ? 1.0 : pow(grass_lod.x / distance, 1.5); +} + +// height of the ground at a point of the chunk and its normal, the way the full grid is triangulated +float ground(vec2 point, out vec3 normal) +{ + float cells = grass_chunk.y; + float spacing = grass_chunk.x / cells; + vec2 place = clamp(point / spacing, vec2(0.0), vec2(cells)); + ivec2 cell = min(ivec2(place), ivec2(int(cells) - 1)); + vec2 t = place - vec2(cell); + float h00 = texelFetch(grass_heights, cell, 0).r; + float h10 = texelFetch(grass_heights, cell + ivec2(1, 0), 0).r; + float h01 = texelFetch(grass_heights, cell + ivec2(0, 1), 0).r; + float h11 = texelFetch(grass_heights, cell + ivec2(1, 1), 0).r; + // the quads are cut along the diagonal from (x0, z1) to (x1, z0), as the mesh is made + if (t.x + t.y <= 1.0) + { + normal = normalize(vec3(-(h10 - h00) / spacing, 1.0, -(h01 - h00) / spacing)); + return h00 + t.x * (h10 - h00) + t.y * (h01 - h00); + } + normal = normalize(vec3(-(h11 - h01) / spacing, 1.0, -(h11 - h10) / spacing)); + return h11 + (1.0 - t.x) * (h01 - h11) + (1.0 - t.y) * (h10 - h11); +} + +void collapse() +{ + gl_Position = vec4(2.0, 2.0, 2.0, 1.0); + f_normal = vec3(0.0, 1.0, 0.0); + f_pos = vec4(0.0); + f_albedo = vec3(0.0); + f_clip_pos = gl_Position; + f_clip_future_pos = gl_Position; + UserData = vec4(0.0); +} + +void main() +{ + int blade = gl_InstanceID; + // the numbers of the blade, drawn in the same order whatever becomes of them + uint state = hash(uint(blade) * 0x9E3779B9U ^ hash(uint(int(grass_part.z)) * 0x85EBCA6BU ^ hash(uint(int(grass_part.w)) + 0x27D4EB2FU))); + uint tilestate = hash(uint(int(grass_part.z)) * 0x85EBCA6BU ^ hash(uint(int(grass_part.w)) + 0x165667B1U)); + vec2 shift = vec2(random(tilestate), random(tilestate)); + vec2 jitter = vec2(random(state), random(state)) - 0.5; + float pick = random(state); + float heightrandom = random(state) * 2.0 - 1.0; + float widthrandom = random(state); + float turnrandom = random(state) * 6.2831853; + float leanrandom = random(state) * 6.2831853; + float leanamount = random(state); + float shade = random(state); + float phase = random(state); + + // place: R2 spreads the first blades evenly at any count, a little jitter breaks its lattice + vec2 spot = fract(shift + float(blade) * vec2(0.7548776662466927, 0.5698402909980532)); + vec2 point = grass_tile.xy + fract(spot + jitter * (0.7 / sqrt(grass_tile.w))) * grass_tile.z; + if (any(lessThan(point, vec2(0.0))) || any(greaterThanEqual(point, vec2(grass_chunk.x)))) + { + collapse(); + return; + } + vec2 local = point / grass_chunk.x; + float samples = grass_chunk.z; + vec4 density = texture(grass_density, (local * samples + 0.5) / (samples + 1.0)); + float holeside = float(textureSize(grass_holes, 0).x); + float hole = texture(grass_holes, (local * (holeside - 1.0) + 0.5) / holeside).r; + + // the kind: the blades of the tile are shared among the kinds by their densities here + vec4 cover = density * grass_share / max(grass_part.x, 1e-3); + float covered = cover.x + cover.y + cover.z + cover.w; + if (covered > 1.0) + cover /= covered; + int kind = -1; + float sum = 0.0; + for (int i = 0; i < 4; ++i) + { + sum += cover[i]; + if (pick < sum) + { + kind = i; + break; + } + } + + vec3 groundnormal; + vec3 root = vec3(point.x, ground(point, groundnormal), point.y); + float distance = length(root - grass_camera.xyz); + float share = lod_share(distance); + float rank = (float(blade) + 0.5) / grass_tile.w; + if (kind < 0 || hole > 0.5 || rank >= share || distance > grass_lod.y) + { + collapse(); + return; + } + + vec4 shape = grass_shape[kind]; + float height = shape.x * (1.0 + shape.y * heightrandom); + // the last of the blades drawn at the distance grow in, and the grass fades out at the end of its reach + height *= 1.0 - smoothstep(share * 0.8, share, rank); + height *= 1.0 - smoothstep(grass_lod.y * 0.75, grass_lod.y, distance); + // where the paint thins out the blades get shorter + height *= mix(0.5, 1.0, clamp(density[kind] * 1.5, 0.0, 1.0)); + // the blades drawn further away stand for more of them, so they're wider + float width = shape.z * (0.8 + 0.4 * widthrandom) / sqrt(share); + + // the face of the blade turns towards the camera further away, so the blades don't vanish edge on + vec2 facing = vec2(cos(turnrandom), sin(turnrandom)); + vec2 toview = grass_camera.xz - root.xz; + toview = toview / max(length(toview), 1e-3); + facing = mix(facing, toview, smoothstep(grass_lod.x * 0.5, grass_lod.x * 2.0, distance)); + facing = facing / max(length(facing), 1e-3); + vec2 across = vec2(-facing.y, facing.x); + + // the lean of the blade and the wind: a steady push along it, with gusts running across the field, and a flutter + vec2 lean = vec2(cos(leanrandom), sin(leanrandom)) * (shape.w * (0.4 + 0.6 * leanamount)); + float gust = 0.5 + 0.5 * sin(grass_wind.w * 0.9 - dot(root.xz, grass_wind.xy) * 0.12); + float flutter = sin(grass_wind.w * (2.0 + phase) + phase * 6.2831853); + vec2 sway = grass_wind.xy * grass_wind.z * (0.15 + 0.45 * gust + 0.08 * flutter); + vec2 bend = lean + sway; + // a long lean takes some of the height + float bent = min(length(bend), 0.9); + + int segments = int(grass_part.y); + int level = min(gl_VertexID / 2, segments); + float side = (gl_VertexID >= segments * 2) ? 0.0 : ((gl_VertexID & 1) == 0 ? -0.5 : 0.5); + float t = float(level) / float(segments); + float halfwidth = width * (1.0 - pow(t, 1.5)); + vec2 xz = root.xz + across * (side * halfwidth) + bend * (height * t * t); + float y = root.y - 0.02 + height * t * (1.0 - 0.35 * bent * t); + vec3 position = vec3(xz.x, y, xz.y); + + // lit mostly as the ground under it, which keeps a field of blades from shimmering + vec3 normal = normalize(groundnormal * 0.8 + vec3(facing.x, 0.0, facing.y) * (side == 0.0 ? 0.0 : 0.25) + vec3(0.0, 0.3, 0.0)); + vec3 albedo = mix(grass_base[kind].rgb, grass_tip[kind].rgb, t) * (0.85 + 0.3 * shade); + // the lower part of the blades is shaded by the others + albedo *= mix(0.7, 1.0, t); + + f_pos = modelview * vec4(position, 1.0); + f_normal = normalize(modelviewnormal * normal); + f_albedo = albedo; + f_clip_pos = projection * f_pos; + f_clip_future_pos = (projection * future) * f_pos; + gl_Position = f_clip_pos; + UserData = vec4(0.0); +} diff --git a/shaders/mat_terrain.frag b/shaders/mat_terrain.frag index 77519e1c..cd219690 100644 --- a/shaders/mat_terrain.frag +++ b/shaders/mat_terrain.frag @@ -51,6 +51,12 @@ 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); +#if TERRAIN_HOLES + // the openings cut in the ground (mat_terrain_holes.frag): the mask led between its samples, cut over the half. + // discarded at the end, the neighbours of the fragment still need it for their derivatives + float holeside = float(textureSize(holes, 0).x); + bool cut = (texture(holes, (local * (holeside - 1.0) + 0.5) / holeside).r > 0.5); +#endif float samples = param[2].w; vec2 paintcoord = (local * samples + 0.5) / (samples + 1.0); vec4 w0 = texture(weights0, paintcoord); @@ -122,6 +128,10 @@ void main() vec3 fragcolor = ambient; fragcolor = apply_lights(fragcolor, fragnormal, albedo, reflectivity, specularity, shadow_tone); +#if TERRAIN_HOLES + if (cut) + discard; +#endif vec4 color = vec4(apply_fog(fragcolor), alpha_mult); #if POSTFX_ENABLED out_color = color; diff --git a/shaders/mat_terrain_holes.frag b/shaders/mat_terrain_holes.frag new file mode 100644 index 00000000..4848b1df --- /dev/null +++ b/shaders/mat_terrain_holes.frag @@ -0,0 +1,9 @@ +// heightmap terrain with holes cut in it, for the portals of tunnels and the like (opengl46_terrain_materials): the cells cut +// out entirely are left out of the mesh, the rest of the openings is cut here. the mask of the holes covers the chunk, 255 +// where the ground is cut; the fragments where the value led between its samples is over the half are discarded +#define TERRAIN_HOLES 1 + +#texture (holes, 7, R) +uniform sampler2D holes; + +#include diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index 49fee1ae..55f87c29 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -1528,6 +1528,23 @@ global_settings::ConfigParse_gfx( cParser &Parser, std::string_view const Token Parser.getTokens(1); Parser >> gfx_depthprepass_replay; } + else if (Token == "gfx.grass") + { + Parser.getTokens(1); + Parser >> gfx_grass; + } + else if (Token == "gfx.grass.distance") + { + Parser.getTokens(1); + Parser >> gfx_grass_distance; + gfx_grass_distance = std::clamp(gfx_grass_distance, 5.f, 500.f); + } + else if (Token == "gfx.grass.density") + { + Parser.getTokens(1); + Parser >> gfx_grass_density; + gfx_grass_density = std::clamp(gfx_grass_density, 0.f, 4.f); + } else if (Token == "gfx.bufferstreaming") { Parser.getTokens(1); @@ -2041,6 +2058,9 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "gfx.gpucull.models", gfx_gpucull_models ); export_as_text( Output, "gfx.depthprepass.scene", gfx_depthprepass_scene ); export_as_text( Output, "gfx.depthprepass.replay", gfx_depthprepass_replay ); + export_as_text( Output, "gfx.grass", gfx_grass ); + export_as_text( Output, "gfx.grass.distance", gfx_grass_distance ); + export_as_text( Output, "gfx.grass.density", gfx_grass_density ); export_as_text( Output, "gfx.bufferstreaming", gfx_bufferstreaming ); export_as_text( Output, "gfx.postfx.motionblur.enabled", gfx_postfx_motionblur_enabled ); export_as_text( Output, "gfx.postfx.motionblur.shutter", gfx_postfx_motionblur_shutter ); diff --git a/utilities/Globals.h b/utilities/Globals.h index c90cdf9f..cf08a8c0 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -321,6 +321,11 @@ struct global_settings { // the colour pass as well: its opaque draws are recorded once, their depth drawn from the record (with the contact shadows // alone only within their reach) and their colour too. false: the scene is walked twice, as before bool gfx_depthprepass_replay = true; + // (opengl 4.6) grass of the heightmap terrain, grown where it's painted: drawn, how far from the camera (metres), and how + // many blades of the density painted (1: all) + bool gfx_grass = true; + float gfx_grass_distance = 60.f; + float gfx_grass_density = 1.f; // (opengl 4.6) the model block of every draw and the matrices of instances are written to persistently mapped buffers, // one for each of the last frames, instead of with glBufferSubData. read at the start bool gfx_bufferstreaming = true;