17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-23 01:51:10 +02:00

feat(terrain): grass follows the seasons and fades out over gfx.grass.fadedistance

This commit is contained in:
2026-10-20 02:20:03 +02:00
parent ceb439b218
commit 07ac7406a0
13 changed files with 129 additions and 28 deletions

View File

@@ -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)
{

View File

@@ -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<double>(Global.gfx_grass_distance) + 2.0};
auto const reach{static_cast<double>(std::max(Global.gfx_grass_distance, Global.gfx_grass_fadedistance)) + 2.0};
for (auto const &entry : m_chunks)
{
auto const &chunk{*entry.second};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View File

@@ -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"

View File

@@ -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<float>(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<glm::vec4, 4> base{}, tip{}, shape{};
std::array<glm::vec4, 4> 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<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, thinning))))};
auto const count{std::min(max_blades, static_cast<int>(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<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)};

View File

@@ -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;
};

View File

@@ -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()};

View File

@@ -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()

View File

@@ -656,12 +656,12 @@ std::optional<modifier_shape::effect> modifier_shape::evaluate(double const X, d
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},
// 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<float>());
if (auto const bend{entry["bend"]})
kind.bend = std::clamp(bend.as<float>(), 0.f, 1.f);
colour(entry["dry"], kind.dry);
if (auto const seasonal{entry["seasonal"]})
kind.seasonal = std::clamp(seasonal.as<float>(), 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;

View File

@@ -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

View File

@@ -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);

View File

@@ -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 );

View File

@@ -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