mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-22 20:11:11 +02:00
feat(terrain): holes cut in the heightmap terrain and shader grass painted over it
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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<glm::dvec2> 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::uint16_t>(std::clamp(m_grass_kind, 0, std::max(0, static_cast<int>(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<glm::dvec3> 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<int>(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<std::uint16_t>(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<int>(kinds.size()) - 1));
|
||||
for (std::size_t i = 0; i < kinds.size(); ++i)
|
||||
{
|
||||
ImGui::PushID(static_cast<int>(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<int>(i) == m_grass_kind))
|
||||
m_grass_kind = static_cast<int>(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<std::uint16_t>(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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -115,6 +115,11 @@ editor_mode::selftest_result editor_mode::selftest_dispatch(std::string const &c
|
||||
// terrainfill <palette entry> <blur> <rotation> <x> <z> <x> <z> <x> <z>... the area outlined filled, as the fill tool does it
|
||||
// terrainturn <palette entry> <degrees> the textures of the palette entry turned
|
||||
// terrainwater <index> <material> [size] material and repeat of a body of water; terrainwaterdefault <material>
|
||||
// terraincut <x> <z> <radius> <1 cut|0 close> a stroke of the holes brush; terraincutarea <1|0> <x> <z>... the area outlined
|
||||
// terraingrasskind <preset name> a preset added to the grass palette; terraingrass <x> <z> <radius> <kind> [erase 0|1] a stroke
|
||||
// of the grass brush; terraingrassarea <kind, -1 takes the grass away> <x> <z>... the area outlined
|
||||
// terraincursor logs the point of the ground under the cursor, found along the ray (over holes too)
|
||||
// terraintool <holes|grass|paint|sculpt|none> the tool the left button works on the terrain, with a brush of 3 m
|
||||
// orthophoto <north> <east> <radius> <fit 0|1> [in scene 0|1] the imagery layer on, placed and fitted as given
|
||||
// join <x> <z> <x> <z> [radius] [variant] [transitions 0|1] the free ends at the points joined, as a drag from one to the other
|
||||
// layto <x> <z> <x> <z> <layout 0..3> <radius> from the free end at the first point towards the second, as a drag in the layout
|
||||
|
||||
@@ -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<glm::dvec2> 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::uint16_t>(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<std::uint16_t>(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<int>(std::lround((x - corner.x) / heightmap::chunk_size * data.grass)), 0, data.grass)};
|
||||
auto const iz{std::clamp(static_cast<int>(std::lround((z - corner.y) / heightmap::chunk_size * data.grass)), 0, data.grass)};
|
||||
auto const plane{static_cast<std::size_t>(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<std::size_t>(iz) * (data.grass + 1) + ix]);
|
||||
}
|
||||
if (data.paint > 0)
|
||||
{
|
||||
// weights at the paint sample nearest the point
|
||||
|
||||
@@ -229,10 +229,14 @@ void editor_mode::work_area_tools(std::vector<tool_button> &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});
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,15 @@ http://mozilla.org/MPL/2.0/.
|
||||
#include "stdafx.h"
|
||||
#include "editor/editorTerrain.hpp"
|
||||
|
||||
#include <atomic>
|
||||
|
||||
namespace
|
||||
{
|
||||
std::atomic<std::uint64_t> 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<heightmap::chunk_data &>(*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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<float>(corner.x - packcorner.x), static_cast<float>(corner.y - packcorner.y), static_cast<float>(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<gfx::grass_kind> kinds;
|
||||
std::vector<gfx::grass_chunk> 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<double>(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<std::size_t>(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<float>(heightmap::chunk_size);
|
||||
grass.low = static_cast<float>(chunk.m_low.y);
|
||||
grass.high = static_cast<float>(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<glm::dvec2> 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<glm::dvec2, glm::dvec2> outline_bounds(std::vector<glm::dvec2> const &Outline)
|
||||
{
|
||||
glm::dvec2 low{std::numeric_limits<double>::max()}, high{-std::numeric_limits<double>::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<glm::dvec2> 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<bool(double X, double Z)> 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<heightmap::chunk_data>(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<std::size_t>(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<glm::dvec2> 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<double(double X, double Z)> 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<heightmap::chunk_data>(current)};
|
||||
data->ensure_grass();
|
||||
auto const samples{data->grass};
|
||||
auto const plane{static_cast<std::size_t>(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<int>(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<std::size_t>(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::uint8_t>(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<int>(std::lround(own + (255 - own) * amount))};
|
||||
if (target == own)
|
||||
continue;
|
||||
own = static_cast<std::uint8_t>(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<int>(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<int>(other) != slot)
|
||||
{
|
||||
auto &density{data->grass_density[other * plane + sample]};
|
||||
density = static_cast<std::uint8_t>(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<std::uint16_t>(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;
|
||||
|
||||
@@ -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<std::array<glm::dvec3, 3>> &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<glm::dvec2> 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<glm::dvec2> 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<glm::dvec2> const &Outline, std::uint16_t Kind, bool Erase);
|
||||
// the grass palette
|
||||
std::vector<heightmap::grass_def> 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<heightmap::water_body> water;
|
||||
std::string water_material;
|
||||
std::vector<heightmap::modifier> modifiers;
|
||||
std::vector<heightmap::grass_def> 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<double(double X, double Z)> 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<bool(double X, double Z)> 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<double(double X, double Z)> 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<chunk_key> m_loading; // asked from a worker
|
||||
std::set<chunk_key> m_removed; // removed in the editor, still in the files until saved
|
||||
std::vector<geometry_slot> m_slots;
|
||||
bool m_nocut{false}; // the renderer can't change the indices of the chunks: the holes are cut by the materials alone
|
||||
std::map<std::uint64_t, int> m_slotofgeometry; // geometry key -> slot
|
||||
std::map<std::uint16_t, material_handle> m_palette;
|
||||
std::vector<water_shape> m_watershapes;
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 55 KiB |
@@ -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)])
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 ) {
|
||||
|
||||
@@ -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
|
||||
|
||||
303
rendering/opengl46/opengl46grass.cpp
Normal file
303
rendering/opengl46/opengl46grass.cpp
Normal file
@@ -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<gl::program>(std::vector<std::reference_wrapper<const gl::shader>>{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<gl::vao>();
|
||||
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<std::size_t>(samples) * samples};
|
||||
std::size_t kinds{0};
|
||||
while (kinds < Source.kinds.size() && Source.kinds[kinds] >= 0)
|
||||
++kinds;
|
||||
std::vector<std::uint8_t> 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<double>(Source.size) / Source.cells};
|
||||
auto const step{static_cast<double>(Source.size) / Source.samples};
|
||||
auto const tilesize{static_cast<double>(Source.size) / tiles};
|
||||
for (int tz = 0; tz < tiles; ++tz)
|
||||
for (int tx = 0; tx < tiles; ++tx)
|
||||
{
|
||||
auto &part{Chunk.parts[static_cast<std::size_t>(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<int>(std::floor(From / Step)), 0, Last), std::clamp(static_cast<int>(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<float>::max();
|
||||
part.high = -std::numeric_limits<float>::max();
|
||||
for (int z = gz0; z <= gz1; ++z)
|
||||
for (int x = gx0; x <= gx1; ++x)
|
||||
{
|
||||
auto const height{Source.heights[static_cast<std::size_t>(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<std::size_t>(z) * samples + x]);
|
||||
part.most[kind] = most / 255.f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void opengl46_grass::update(std::vector<gfx::grass_kind> const &Kinds, std::vector<gfx::grass_chunk> 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<void(glm::mat4 const &)> 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<float>(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<glm::vec4, 4> 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<int>(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<int>(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<std::size_t>(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<float>(glm::length(nearest - camera))};
|
||||
if (distance > reach)
|
||||
continue;
|
||||
if (false == Camera.visible(scene::bounding_area{(low + high) * 0.5, static_cast<float>(glm::length(high - low) * 0.5)}))
|
||||
continue;
|
||||
auto const full{tilesize * tilesize * densest * multiplier * fill};
|
||||
auto const count{std::min(max_blades, static_cast<int>(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<float>(chunk.cells), static_cast<float>(chunk.samples), 0.f);
|
||||
::glUniform4f(m_uniforms.camera, static_cast<float>(camera.x - chunk.corner.x), static_cast<float>(camera.y), static_cast<float>(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<float>(std::floor(chunk.corner.x / tilesize + 0.5) + tx)};
|
||||
auto const seedz{static_cast<float>(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<float>(segments), seedx, seedz);
|
||||
::glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, segments * 2 + 1, count);
|
||||
triangles += static_cast<std::size_t>(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;
|
||||
}
|
||||
85
rendering/opengl46/opengl46grass.h
Normal file
85
rendering/opengl46/opengl46grass.h
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#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<gfx::grass_kind> const &Kinds, std::vector<gfx::grass_chunk> 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<void(glm::mat4 const &)> 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<float, 4> 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<int, 4> 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<tile, tiles * tiles> 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<gl::program> m_program;
|
||||
std::unique_ptr<gl::vao> m_vao; // without attributes, the blades are made of the numbers of their vertices
|
||||
std::vector<gfx::grass_kind> m_kinds;
|
||||
std::unordered_map<std::uint64_t, chunk> 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;
|
||||
};
|
||||
@@ -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<std::uint64_t>(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<gfx::terrain_layer> 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<gfx::terrain_layer> 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<gfx::grass_kind> const &Kinds, std::vector<gfx::grass_chunk> 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<int>(triangles);
|
||||
}
|
||||
|
||||
void opengl46_renderer::Render_particles()
|
||||
{
|
||||
model_ubs.set_modelview(OpenGLMatrices.data(GL_MODELVIEW));
|
||||
|
||||
@@ -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<gfx::terrain_layer> const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement ) override;
|
||||
Terrain_Material( material_handle const Reuse, std::vector<gfx::terrain_layer> const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement,
|
||||
int const HoleSamples = 0, std::uint8_t const *Holes = nullptr ) override;
|
||||
void
|
||||
Terrain_Release( material_handle const Material ) override;
|
||||
void
|
||||
Terrain_Grass( std::vector<gfx::grass_kind> const &Kinds, std::vector<gfx::grass_chunk> const &Chunks ) override;
|
||||
// shader methods
|
||||
auto Fetch_Shader( std::string const &name ) -> std::shared_ptr<gl::program> 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};
|
||||
|
||||
@@ -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<float, max_layers * 2 * 4> const zeros{};
|
||||
::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, static_cast<GLsizei>(max_layers), 2, 0, GL_RGBA, GL_FLOAT, zeros.data());
|
||||
}
|
||||
{
|
||||
// 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<gfx::terrain_layer> 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<gfx::terrain_layer> 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<float>(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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<gfx::terrain_layer> const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement);
|
||||
material_handle make(material_handle const Reuse, std::vector<gfx::terrain_layer> const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement,
|
||||
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<texture_handle, 2> 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<slot> m_slots;
|
||||
std::vector<std::size_t> m_free;
|
||||
std::unordered_map<material_handle, std::size_t> m_slotof;
|
||||
std::shared_ptr<gl::program> m_program; // of the chunks, mat_terrain
|
||||
std::shared_ptr<gl::program> m_holesprogram; // of the chunks with holes, mat_terrain_holes
|
||||
std::unique_ptr<gl::postfx> m_copy;
|
||||
std::unique_ptr<gl::framebuffer> m_framebuffer;
|
||||
};
|
||||
|
||||
@@ -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<gfx::terrain_layer> 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<gfx::terrain_layer> 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() ) {
|
||||
|
||||
@@ -9,6 +9,9 @@ http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
#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<int, 4> 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<gfx::terrain_layer> 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<gfx::terrain_layer> 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<gfx::grass_kind> const &Kinds, std::vector<gfx::grass_chunk> const &Chunks ) {}
|
||||
// shader methods
|
||||
virtual auto Fetch_Shader( std::string const &name ) -> std::shared_ptr<gl::program> = 0;
|
||||
// texture methods
|
||||
|
||||
@@ -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<int>(std::floor(X / chunk_size)), static_cast<int>(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<double>(holes))};
|
||||
auto const fz{std::clamp((Z - low.y) / step, 0.0, static_cast<double>(holes))};
|
||||
auto const ix{std::min(static_cast<int>(fx), holes - 1)};
|
||||
auto const iz{std::min(static_cast<int>(fz), holes - 1)};
|
||||
auto const tx{fx - ix}, tz{fz - iz};
|
||||
auto const side{static_cast<std::size_t>(holes + 1)};
|
||||
auto const at = [&](int const X_, int const Z_) { return hole_mask[static_cast<std::size_t>(Z_) * side + X_] / 255.0; };
|
||||
return static_cast<float>((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<double>(holes) / cells};
|
||||
auto const range = [&](int const From, int const To) {
|
||||
return std::make_pair(std::clamp(static_cast<int>(std::floor(From * ratio + 1e-9)), 0, holes), std::clamp(static_cast<int>(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<std::size_t>(holes + 1)};
|
||||
for (int z = z0; z <= z1; ++z)
|
||||
for (int x = x0; x <= x1; ++x)
|
||||
if (hole_mask[static_cast<std::size_t>(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<std::size_t>(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<int>(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<std::size_t>(grass + 1) * (grass + 1)};
|
||||
auto const count{grass_count()};
|
||||
std::vector<std::uint8_t> kept;
|
||||
std::array<std::uint16_t, max_chunk_grass> 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<glm::dvec2, glm::dvec2> modifier::bounds() const
|
||||
@@ -541,6 +653,19 @@ std::optional<modifier_shape::effect> modifier_shape::evaluate(double const X, d
|
||||
|
||||
// manifest
|
||||
|
||||
std::vector<grass_def> const &grass_presets()
|
||||
{
|
||||
static std::vector<grass_def> 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<float>(), Node[1].as<float>(), Node[2].as<float>()}, glm::vec3{0.f}, glm::vec3{1.f});
|
||||
};
|
||||
if (auto const name{entry["name"]})
|
||||
kind.name = name.as<std::string>();
|
||||
colour(entry["base"], kind.base);
|
||||
colour(entry["tip"], kind.tip);
|
||||
if (auto const height{entry["height"]})
|
||||
kind.height = std::max(0.01f, height.as<float>());
|
||||
if (auto const variation{entry["variation"]})
|
||||
kind.variation = std::clamp(variation.as<float>(), 0.f, 1.f);
|
||||
if (auto const width{entry["width"]})
|
||||
kind.width = std::max(0.001f, width.as<float>());
|
||||
if (auto const density{entry["density"]})
|
||||
kind.density = std::max(0.f, density.as<float>());
|
||||
if (auto const bend{entry["bend"]})
|
||||
kind.bend = std::clamp(bend.as<float>(), 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<chunk_data> pack_file::read(chunk_key const &Key, std::string *E
|
||||
|
||||
auto chunk{std::make_shared<chunk_data>()};
|
||||
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<chunk_data> 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<chunk_data> 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<std::size_t>(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<std::size_t>(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::vector<chunk_
|
||||
}
|
||||
if (chunk->paint > 0 && chunk->layer_count() > 1)
|
||||
add(table, tag_paint, chunk->weights);
|
||||
if (chunk->has_holes())
|
||||
{
|
||||
data.clear();
|
||||
put<std::uint16_t>(data, static_cast<std::uint16_t>(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<std::size_t>(chunk->grass + 1) * (chunk->grass + 1) * kinds)
|
||||
{
|
||||
data.clear();
|
||||
put<std::uint16_t>(data, static_cast<std::uint16_t>(chunk->grass));
|
||||
for (auto const kind : chunk->grass_kinds)
|
||||
put<std::uint16_t>(data, kind);
|
||||
data.insert(data.end(), chunk->grass_density.begin(), chunk->grass_density.end());
|
||||
add(table, tag_grass, data);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> 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<std::size_t>(n) * n * 6};
|
||||
if (Indices.size() != grid + static_cast<std::size_t>(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<bool> cut(static_cast<std::size_t>(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<std::size_t>(j) * n + i] = true;
|
||||
std::fill_n(Indices.begin() + (static_cast<std::size_t>(j) * n + i) * 6, 6, Indices[(static_cast<std::size_t>(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<std::size_t>(n - 1) * n + K]; });
|
||||
skirt(2, [&](int const K) { return cut[static_cast<std::size_t>(K) * n]; });
|
||||
skirt(3, [&](int const K) { return cut[static_cast<std::size_t>(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<float>(glm::length(highest - lowest) * 0.5 + skirtdepth);
|
||||
Out.low = lowest - glm::dvec3{0.0, skirtdepth, 0.0};
|
||||
|
||||
@@ -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<float, 4> 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<int, int>;
|
||||
using pack_key = std::pair<int, int>;
|
||||
@@ -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<std::uint16_t, max_chunk_layers> layers; // indices of the materials in the terrain palette, no_layer for unused
|
||||
std::vector<std::uint8_t> weights; // planar, (paint + 1)^2 per used layer, rows like the heights; weights of a sample add up to 255
|
||||
// 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<std::uint8_t> 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<std::uint16_t, max_chunk_grass> grass_kinds; // indices of the kinds in the grass palette, no_layer for unused
|
||||
std::vector<std::uint8_t> 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<std::size_t>(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<chunk_data const>;
|
||||
@@ -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<grass_def> 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_body> water;
|
||||
std::vector<modifier> modifiers;
|
||||
std::vector<grass_def> 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);
|
||||
|
||||
|
||||
41
shaders/grass.frag
Normal file
41
shaders/grass.frag
Normal file
@@ -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 <common>
|
||||
|
||||
layout(location = 0) out vec4 out_color;
|
||||
#if MOTIONBLUR_ENABLED
|
||||
layout(location = 1) out vec4 out_motion;
|
||||
#endif
|
||||
|
||||
#include <light_common.glsl>
|
||||
#include <apply_fog.glsl>
|
||||
#include <tonemapping.glsl>
|
||||
|
||||
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
|
||||
}
|
||||
206
shaders/grass.vert
Normal file
206
shaders/grass.vert
Normal file
@@ -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 <common>
|
||||
|
||||
// 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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
9
shaders/mat_terrain_holes.frag
Normal file
9
shaders/mat_terrain_holes.frag
Normal file
@@ -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 <mat_terrain.frag>
|
||||
@@ -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 );
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user