From 07ac7406a0462bb9eae67282aafdc4b4b94190aa Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Tue, 20 Oct 2026 02:20:03 +0200 Subject: [PATCH] feat(terrain): grass follows the seasons and fades out over gfx.grass.fadedistance --- application/editormode.cpp | 13 +++++- editor/editorTerrainStreamer.cpp | 4 +- editor/icons/editor_icons.png | Bin 55924 -> 55924 bytes lang/editor_pl.po | 16 ++++++- rendering/opengl46/opengl46grass.cpp | 54 ++++++++++++++++++++---- rendering/opengl46/opengl46grass.h | 2 +- rendering/opengl46/opengl46renderer.cpp | 3 +- rendering/renderer.h | 2 + scene/heightmapterrain.cpp | 17 +++++--- scene/heightmapterrain.h | 2 + shaders/grass.vert | 35 ++++++++++++--- utilities/Globals.cpp | 7 +++ utilities/Globals.h | 2 + 13 files changed, 129 insertions(+), 28 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index d7bd72bd..59bbdc57 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -3399,11 +3399,22 @@ void editor_mode::render_grass_ui() 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")); + "gfx.grass.distance, gfx.grass.fadedistance 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")); + ImGui::SetNextItemWidth(200.0f); + field(ImGui::ColorEdit3(STR_C("Dried out"), &kind.dry.r, ImGuiColorEditFlags_NoInputs)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Colour the tips take as the grass dries out, in late summer, autumn and winter")); + ImGui::SameLine(); + ImGui::SetNextItemWidth(120.0f); + field(ImGui::SliderFloat(STR_C("Follows the seasons"), &kind.seasonal, 0.0f, 1.0f, "%.2f")); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("How much the colour and the height change through the year with the date of the scenario:\n" + "dry and low in winter, greening up in spring, fresh in May, drying out from August.\n" + "0: the same all year (e.g. a watered lawn), 1: fully")); m_grass_edit_active = active; if (changed) { diff --git a/editor/editorTerrainStreamer.cpp b/editor/editorTerrainStreamer.cpp index c2254e89..1344ed1d 100644 --- a/editor/editorTerrainStreamer.cpp +++ b/editor/editorTerrainStreamer.cpp @@ -800,9 +800,11 @@ void terrain_streamer::publish_grass(glm::dvec3 const &Camera) entry.width = kind.width; entry.density = kind.density; entry.bend = kind.bend; + entry.dry = glm::pow(glm::clamp(kind.dry, glm::vec3{0.f}, glm::vec3{1.f}), glm::vec3{2.2f}); + entry.seasonal = kind.seasonal; kinds.push_back(entry); } - auto const reach{static_cast(Global.gfx_grass_distance) + 2.0}; + auto const reach{static_cast(std::max(Global.gfx_grass_distance, Global.gfx_grass_fadedistance)) + 2.0}; for (auto const &entry : m_chunks) { auto const &chunk{*entry.second}; diff --git a/editor/icons/editor_icons.png b/editor/icons/editor_icons.png index f54cb31dfbadb34eb3d7342ffd786195ffe0300f..3de6e087e596af4f32b6feeb4cb486220a12bf6e 100644 GIT binary patch delta 469 zcmeyeh55@C<_VQ521%BN#wiBox<;0!Cb}kO$p*R>NhXH6CMJo=$wn5b1_mjH6X)g$ zurhA2?AkN$K+6Z;y1%_ruO>?~8mT0vCMTwtC!6S|SSB0mnxrP00ySG0>!zipnj~5p zTco9!7)>r@^cPru_uZG5c2#HiH*Q`3$>mbd8~6fSxOaYKK>ouw_@VvEGBbCf#+v=?xuMp*JOrVIKrR)NqO={=9fgddh$LN6%})! z3xHls)-^XUveY#(GD+06Gyr*(k|4#nL=+G8?PEIw8j_N-izRsnD9N$f_`T z4y&L-e)>P>wfrZyXtLY>_)^QXef!s!cNQMlwC2a}l}}EIY_XqwmepLr@_z)!xdxZh z|Kxnc&aX~k6PeNDJxyiNn`5D~Hd*tiKi#at<}BdSBKI`!-xSr4oyM!`KkH6C)LOgq zoW9r5!#9%U9`^))lvq?`mU(2#S^I0NtdyB1Tez+|m2&#x-k|SYer4azUHsr3m9A~L Jd9~Qd!vJ``#3BFy delta 469 zcmeyeh55@C<_VQ5CW*#LM&`+>y2i#PCb}j@hDN%Hrpacy25BZHsfp&MNtWiR6X)g$ zgw+R@Pkm~(>EC+~CK1`|6_ceIjZ}<*nvyIMEp^RJ4J>s{QcVnXEzL{}bQ6=zl8jT$ zlatJhQzsWP`U~Wm^?rSI)O6wT6(tM-9ag_4Utp|KSSA+LX5+~GwAF9lu7(5deF?r1 zZzOgz+dmKUk4si+U$i-k$(&JunU&o?M#E9mxn+ifO}6gb$s3tp66Na2`&d*|l9El$ z3=PaIbkmXy6Ln3}41q4RG`7$+OEs`eu{1YJPBu@T%*N`kPRKEfl1qznDzqjmvMNlT z!z!p?Z>7`G@y{?fQ*VRDRl~j|3f~?1>&~q`!fSSAqzQfnWiSJy^+rHJL&!F z=El_LLc2GKKRZ#~bV#m8K$LqPliICHn;Yff5r3Ac)!gwmGZ%XQigl{kj;8SQPdBd? HJ9!uYXqB(9 diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 89f3ec3b..48a1cd6d 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -9964,8 +9964,8 @@ 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 "Blades on a square metre painted fully. The renderer draws fewer further away, and wider;\ngfx.grass.distance, gfx.grass.fadedistance 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, gfx.grass.fadedistance i gfx.grass.density w eu07.ini ustalają, jak daleko i ile" msgid "Clear the grass" msgstr "Usuń trawę" @@ -10074,3 +10074,15 @@ msgstr "wysoka trawa" msgid "weeds" msgstr "chwasty" + +msgid "Dried out" +msgstr "Wyschnięta" + +msgid "Colour the tips take as the grass dries out, in late summer, autumn and winter" +msgstr "Kolor, jaki przybierają czubki, gdy trawa wysycha: późnym latem, jesienią i zimą" + +msgid "Follows the seasons" +msgstr "Zmienia się z porami roku" + +msgid "How much the colour and the height change through the year with the date of the scenario:\ndry and low in winter, greening up in spring, fresh in May, drying out from August.\n0: the same all year (e.g. a watered lawn), 1: fully" +msgstr "Jak bardzo kolor i wysokość zmieniają się w ciągu roku z datą scenariusza:\nzimą sucha i niska, wiosną zielenieje, w maju świeża, od sierpnia wysycha.\n0: taka sama przez cały rok (np. podlewany trawnik), 1: w pełni" diff --git a/rendering/opengl46/opengl46grass.cpp b/rendering/opengl46/opengl46grass.cpp index 79034db8..f208cd07 100644 --- a/rendering/opengl46/opengl46grass.cpp +++ b/rendering/opengl46/opengl46grass.cpp @@ -13,6 +13,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/openglcamera.h" #include "model/Texture.h" #include "simulation/simulationenvironment.h" +#include "simulation/simulationtime.h" #include "utilities/Globals.h" #include "utilities/Logs.h" #include "utilities/Timer.h" @@ -23,10 +24,37 @@ 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) +// the share of the blades drawn at specified distance: fewer further away, for speed (from Thinning), and fewer and fewer from +// Fade to Reach, where the grass ends; the same as lod_share() of grass.vert +float lod_share(float const Distance, float const Thinning, float const Fade, float const Reach) { - return Distance <= Near ? 1.f : std::pow(Near / Distance, 1.5f); + auto const share{Distance <= Thinning ? 1.f : std::pow(Thinning / Distance, 1.5f)}; + auto const t{std::clamp((Distance - Fade) / std::max(Reach - Fade, 1e-3f), 0.f, 1.f)}; + return share * (1.f - t * t * (3.f - 2.f * t)); +} + +// the grass through the year, by the day of the year: x how dry it is, y how fresh (the green of spring), z how grown (share +// of the height). the turns follow the seasons of the textures (world_environment::compute_season(): winter till day 65, +// spring till 158, summer till 252, autumn till 341): dormant and dry in winter, greening up and growing through spring, +// fresh in May, drying out from August, the dry grass lying lower towards winter +glm::vec3 grass_season(int const Yearday) +{ + struct point + { + float day; + glm::vec3 value; + }; + static point const year[]{{1.f, {0.80f, 0.f, 0.55f}}, {65.f, {0.80f, 0.f, 0.55f}}, {100.f, {0.45f, 0.3f, 0.70f}}, {135.f, {0.05f, 1.f, 0.90f}}, + {170.f, {0.f, 0.4f, 1.f}}, {200.f, {0.10f, 0.f, 1.f}}, {240.f, {0.35f, 0.f, 1.f}}, {280.f, {0.50f, 0.f, 0.95f}}, + {320.f, {0.65f, 0.f, 0.80f}}, {341.f, {0.80f, 0.f, 0.60f}}, {367.f, {0.80f, 0.f, 0.55f}}}; + auto const day{std::clamp(static_cast(Yearday), 1.f, 366.f)}; + for (std::size_t i = 1; i < std::size(year); ++i) + if (day <= year[i].day) + { + auto const t{(day - year[i - 1].day) / (year[i].day - year[i - 1].day)}; + return glm::mix(year[i - 1].value, year[i].value, t * t * (3.f - 2.f * t)); + } + return year[std::size(year) - 1].value; } GLuint make_texture(GLenum const Format, int const Width, int const Height, GLenum const Filter) @@ -78,6 +106,8 @@ bool opengl46_grass::init() m_uniforms.tip = location("grass_tip"); m_uniforms.shape = location("grass_shape"); m_uniforms.share = location("grass_share"); + m_uniforms.dry = location("grass_dry"); + m_uniforms.season = location("grass_season"); m_state = 1; return true; } @@ -207,15 +237,19 @@ std::size_t opengl46_grass::render(opengl_camera const &Camera, glm::mat4 const 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 thinning{std::max(3.f, reach * 0.2f)}; + // whole up to the distance, thinning out to the fading distance + auto const fade{std::max(5.f, Global.gfx_grass_distance)}; + auto const reach{std::max(fade + 1.f, Global.gfx_grass_fadedistance)}; + auto const thinning{std::max(3.f, fade * 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, thinning, reach, 0.f, 0.f); + ::glUniform4f(m_uniforms.lod, thinning, fade, reach, 0.f); + auto const season{grass_season(simulation::Time.year_day())}; + ::glUniform4f(m_uniforms.season, season.x, season.y, season.z, 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})}; @@ -227,7 +261,7 @@ std::size_t opengl46_grass::render(opengl_camera const &Camera, glm::mat4 const { auto &chunk{entry.second}; // the kinds of the chunk, and the share of the blades of each against its densest - std::array base{}, tip{}, shape{}; + std::array base{}, tip{}, shape{}, dry{}; glm::vec4 share{0.f}; float densest{0.f}; for (std::size_t i = 0; i < chunk.kinds.size(); ++i) @@ -243,6 +277,7 @@ std::size_t opengl46_grass::render(opengl_camera const &Camera, glm::mat4 const 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}; + dry[i] = glm::vec4{kind.dry, kind.seasonal}; 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}; @@ -266,7 +301,7 @@ std::size_t opengl46_grass::render(opengl_camera const &Camera, glm::mat4 const if (false == Camera.visible(scene::bounding_area{(low + high) * 0.5, static_cast(glm::length(high - low) * 0.5)})) continue; auto const full{tilesize * tilesize * densest * multiplier * fill}; - auto const count{std::min(max_blades, static_cast(std::ceil(full * lod_share(distance, thinning))))}; + auto const count{std::min(max_blades, static_cast(std::ceil(full * lod_share(distance, thinning, fade, reach))))}; if (count <= 0) continue; if (false == set) @@ -280,11 +315,12 @@ std::size_t opengl46_grass::render(opengl_camera const &Camera, glm::mat4 const ::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)); + ::glUniform4fv(m_uniforms.dry, 4, glm::value_ptr(dry[0])); GLuint const textures[]{chunk.heights, chunk.density, chunk.holes}; ::glBindTextures(0, 3, textures); } // fewer segments further away - auto const segments{distance < thinning * 1.5f ? 3 : distance < reach * 0.5f ? 2 : 1}; + auto const segments{distance < thinning * 1.5f ? 3 : distance < fade * 0.5f ? 2 : 1}; // the number of the tile in the world, the seed of its blades auto const seedx{static_cast(std::floor(chunk.corner.x / tilesize + 0.5) + tx)}; auto const seedz{static_cast(std::floor(chunk.corner.y / tilesize + 0.5) + tz)}; diff --git a/rendering/opengl46/opengl46grass.h b/rendering/opengl46/opengl46grass.h index 34c4c056..52370fe9 100644 --- a/rendering/opengl46/opengl46grass.h +++ b/rendering/opengl46/opengl46grass.h @@ -80,6 +80,6 @@ class opengl46_grass struct locations { GLint tile{-1}, part{-1}, chunk{-1}, camera{-1}, lod{-1}, wind{-1}; - GLint base{-1}, tip{-1}, shape{-1}, share{-1}; + GLint base{-1}, tip{-1}, shape{-1}, share{-1}, dry{-1}, season{-1}; } m_uniforms; }; diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index a04d3be7..e0166c01 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -1111,7 +1111,8 @@ void opengl46_renderer::draw_debug_ui() 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("the grass drawn whole up to, m (gfx.grass.distance)", &Global.gfx_grass_distance, 10.f, 300.f, "%.0f"); + ImGui::SliderFloat("thinning out up to, m (gfx.grass.fadedistance)", &Global.gfx_grass_fadedistance, 10.f, 350.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()}; diff --git a/rendering/renderer.h b/rendering/renderer.h index be4bc9f8..6946b911 100644 --- a/rendering/renderer.h +++ b/rendering/renderer.h @@ -44,6 +44,8 @@ struct grass_kind 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 + glm::vec3 dry{0.f}; // the tips dried out, in autumn and winter + float seasonal{1.f}; // how much the kind follows the seasons, 0..1 }; // a chunk of the heightmap terrain the grass grows on, see gfx_renderer::Terrain_Grass() diff --git a/scene/heightmapterrain.cpp b/scene/heightmapterrain.cpp index b346f764..434aa1e6 100644 --- a/scene/heightmapterrain.cpp +++ b/scene/heightmapterrain.cpp @@ -656,12 +656,12 @@ std::optional modifier_shape::evaluate(double const X, d std::vector const &grass_presets() { static std::vector const presets{ - // name, root, tip, height, variation, width, density, bend - {"meadow", {0.22f, 0.33f, 0.09f}, {0.58f, 0.68f, 0.27f}, 0.25f, 0.45f, 0.020f, 90.f, 0.30f}, - {"lawn", {0.18f, 0.30f, 0.08f}, {0.42f, 0.58f, 0.20f}, 0.07f, 0.30f, 0.012f, 200.f, 0.15f}, - {"dry grass", {0.36f, 0.32f, 0.16f}, {0.80f, 0.72f, 0.45f}, 0.30f, 0.45f, 0.016f, 70.f, 0.40f}, - {"tall grass", {0.18f, 0.27f, 0.07f}, {0.60f, 0.66f, 0.30f}, 0.70f, 0.35f, 0.022f, 40.f, 0.45f}, - {"weeds", {0.16f, 0.25f, 0.08f}, {0.45f, 0.55f, 0.22f}, 0.16f, 0.60f, 0.030f, 30.f, 0.25f}, + // name, root, tip, height, variation, width, density, bend, dried out, follows the seasons + {"meadow", {0.22f, 0.33f, 0.09f}, {0.58f, 0.68f, 0.27f}, 0.25f, 0.45f, 0.020f, 90.f, 0.30f, {0.74f, 0.66f, 0.40f}, 1.f}, + {"lawn", {0.18f, 0.30f, 0.08f}, {0.42f, 0.58f, 0.20f}, 0.07f, 0.30f, 0.012f, 200.f, 0.15f, {0.62f, 0.60f, 0.36f}, 0.6f}, + {"dry grass", {0.36f, 0.32f, 0.16f}, {0.80f, 0.72f, 0.45f}, 0.30f, 0.45f, 0.016f, 70.f, 0.40f, {0.82f, 0.74f, 0.50f}, 0.5f}, + {"tall grass", {0.18f, 0.27f, 0.07f}, {0.60f, 0.66f, 0.30f}, 0.70f, 0.35f, 0.022f, 40.f, 0.45f, {0.76f, 0.66f, 0.40f}, 1.f}, + {"weeds", {0.16f, 0.25f, 0.08f}, {0.45f, 0.55f, 0.22f}, 0.16f, 0.60f, 0.030f, 30.f, 0.25f, {0.52f, 0.44f, 0.28f}, 1.f}, }; return presets; } @@ -762,6 +762,9 @@ bool load_manifest(std::string const &Path, manifest &Manifest, std::string *Err kind.density = std::max(0.f, density.as()); if (auto const bend{entry["bend"]}) kind.bend = std::clamp(bend.as(), 0.f, 1.f); + colour(entry["dry"], kind.dry); + if (auto const seasonal{entry["seasonal"]}) + kind.seasonal = std::clamp(seasonal.as(), 0.f, 1.f); result.grass.push_back(std::move(kind)); } Manifest = std::move(result); @@ -852,6 +855,8 @@ bool save_manifest(std::string const &Path, manifest const &Manifest, std::strin 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::Key << "dry" << YAML::Value << YAML::Flow << YAML::BeginSeq << kind.dry.r << kind.dry.g << kind.dry.b << YAML::EndSeq; + out << YAML::Key << "seasonal" << YAML::Value << kind.seasonal; out << YAML::EndMap; } out << YAML::EndSeq; diff --git a/scene/heightmapterrain.h b/scene/heightmapterrain.h index 0a0a077e..0dacc292 100644 --- a/scene/heightmapterrain.h +++ b/scene/heightmapterrain.h @@ -222,6 +222,8 @@ struct grass_def 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 + glm::vec3 dry{0.74f, 0.66f, 0.40f}; // colour of the tips dried out, in autumn and winter, sRGB 0..1 + float seasonal{1.f}; // how much the kind follows the seasons: 0 the same all year, 1 fully }; // a few kinds to start the palette with diff --git a/shaders/grass.vert b/shaders/grass.vert index 1d62b607..444e5f7e 100644 --- a/shaders/grass.vert +++ b/shaders/grass.vert @@ -18,7 +18,8 @@ uniform vec4 grass_part; 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 +// x: distance the grass starts to thin out at, for speed; y: distance the fading starts at (gfx.grass.distance); z: where the +// grass ends (gfx.grass.fadedistance) 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; @@ -28,6 +29,10 @@ 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; +// the colour of each kind dried out (linear rgb), and how much it follows the seasons +uniform vec4 grass_dry[4]; +// the time of the year: x: how dry the grass is, y: how fresh (spring), z: how grown (share of the height) +uniform vec4 grass_season; 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 @@ -56,12 +61,19 @@ float random(inout uint 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) +// the share of the blades drawn at specified distance for speed: fewer further away, each standing for more of them +float lod_thinning(float distance) { return distance <= grass_lod.x ? 1.0 : pow(grass_lod.x / distance, 1.5); } +// the share of the blades drawn: the above, and fewer and fewer over the fading distance, where the grass really gets +// sparser; the same as lod_share() of opengl46grass.cpp +float lod_share(float distance) +{ + return lod_thinning(distance) * (1.0 - smoothstep(grass_lod.y, grass_lod.z, distance)); +} + // 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) { @@ -111,6 +123,7 @@ void main() float leanamount = random(state); float shade = random(state); float phase = random(state); + float early = 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)); @@ -148,7 +161,7 @@ void main() 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) + if (kind < 0 || hole > 0.5 || rank >= share || distance > grass_lod.z) { collapse(); return; @@ -158,11 +171,13 @@ void main() 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); + // lower in winter, growing in spring + float seasonal = grass_dry[kind].a; + height *= mix(1.0, grass_season.z, seasonal); // 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); + float width = shape.z * (0.8 + 0.4 * widthrandom) / sqrt(lod_thinning(distance)); // 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)); @@ -192,7 +207,13 @@ void main() // 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 seasons: some blades dry out (and green up) earlier than others, so a field turns gradually; a dry blade takes the + // dry colour, darker at its root. in spring the green is fresher + float dry = clamp(grass_season.x * seasonal * (0.6 + 0.8 * early), 0.0, 1.0); + vec3 bottom = mix(grass_base[kind].rgb, grass_dry[kind].rgb * 0.45, dry); + vec3 top = mix(grass_tip[kind].rgb, grass_dry[kind].rgb, dry); + top *= mix(vec3(1.0), vec3(1.08, 1.22, 0.85), grass_season.y * seasonal * (1.0 - dry)); + vec3 albedo = mix(bottom, top, t) * (0.85 + 0.3 * shade); // the lower part of the blades is shaded by the others albedo *= mix(0.7, 1.0, t); diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index 55f87c29..c1b85348 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -1539,6 +1539,12 @@ global_settings::ConfigParse_gfx( cParser &Parser, std::string_view const Token Parser >> gfx_grass_distance; gfx_grass_distance = std::clamp(gfx_grass_distance, 5.f, 500.f); } + else if (Token == "gfx.grass.fadedistance") + { + Parser.getTokens(1); + Parser >> gfx_grass_fadedistance; + gfx_grass_fadedistance = std::clamp(gfx_grass_fadedistance, 5.f, 600.f); + } else if (Token == "gfx.grass.density") { Parser.getTokens(1); @@ -2060,6 +2066,7 @@ global_settings::export_as_text( std::ostream &Output ) const { 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.fadedistance", gfx_grass_fadedistance ); 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 ); diff --git a/utilities/Globals.h b/utilities/Globals.h index cf08a8c0..ef8df458 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -325,6 +325,8 @@ struct global_settings { // many blades of the density painted (1: all) bool gfx_grass = true; float gfx_grass_distance = 60.f; + // (opengl 4.6) the grass thins out from gfx_grass_distance to here, where it ends + float gfx_grass_fadedistance = 70.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