Растительность вблизи камеры

Группа «Растительность» (V01: рекомендации 1, 3, 4 из docs/archive/plan/groups-top-level.md, группа 5; V02 — деревья по маске леса 10 м). Травинки вокруг камеры (GrassField) и расстановка деревьев/импостеров по кромке леса. Файлы остаются в scripts/terrain/; основной конфиг деревьев — configs/world.json → trees (группа «Рельеф»), добавка V02 — configs/vegetation.json → trees (её ключи поверх, TreePlacer.with_vegetation).

Файлы

ФайлЧто делает
scripts/terrain/grass_field.gd (GrassField, MultiMeshInstance3D)сетка пучков травы вокруг камеры, приминание у ног пилота, посадки-прямоугольники, тестируемые статические формулы (far_density, press_factor, in_landing_rect, …)
scripts/terrain/grass.gdshaderгде трава (по карте поверхности), высота, прореживание/расширение вдали, ветер (terrain_wind.gdshaderinc), приминание, цвет — цвет земли луга в этой точке (meadow_look.gdshaderinc), скошенные площадки
scripts/terrain/meadow_look.gdshaderincцвет луга, общий для рельефа и травинок: палитра/сухость локации, влажность и экспозиция из полей рельефа, пятна с высоты (meadow_macro: оттенок, выгоревшие пятна, покосы) — трава вблизи того же тона, что земля под ней и луг с высоты
configs/vegetation.jsonпараметры травы (grass): свой файл, не делится с configs/world.json (владение группы «Рельеф»)
tests/vegetation/test_grass_field.gdпрореживание вдали, приминание по высоте ног, прямоугольник посадки
scripts/terrain/tree_placer.gd (TreePlacer)расстановка деревьев-моделей: маска 10 м, опушка, породы, утопление
scripts/terrain/terrain_tree_models.gd (TerrainTreeModels)MultiMesh моделей пород × LOD, пересчёт в рабочем потоке
scripts/terrain/forest_impostors.gd + .gdshader (ForestImpostors)билборды среднего плана (0,35–3,5 км) по той же маске
tests/vegetation/test_trees_mask.gdдеревья на лесе маски, не в лугу, просеки, опушка гуще и шире
tests/vegetation/test_trees_water.gdна всех локациях ни модели, ни импостеры не стоят в воде (G маски), лес у берега остаётся; водоёмы OSM — замкнутые кольца с островами

API

  • Terrain.get_grass_palette() — общая с рельефом палитра (цвет, сухость), читает GrassField.
  • Terrain.set_pilot(node) — трава приминается у ног ноды node.
  • GrassField.set_landing_sites(sites: Array[Dictionary]) — посадки прямоугольником, не кругом: sites — как WorldObjects.get_landing_sites(): [{position: Vector3, axis_deg, length_m, width_m, mowed_height_k?}]. Подключает группа «Сцена игры» (WorldObjects.get_landing_sites() → terrain.grass.set_landing_sites(...)). До вызова действует круговое скашивание вокруг Terrain.get_landing_sites() радиусом grass.landing_mow_radius_m.

Деревья по кромке маски 10 м (V02, VR-21, VR-6)

  • Terrain после создания деревьев передаёт маску: TerrainTreeModels.set_forest_mask(image, origin, cell_m) и ForestImpostors.set_forest_mask(...) (Terrain.get_forest_mask(); origin — угол пикселя (0, 0)). Дерево стоит, где доля леса (R маски, билинейно между центрами пикселей) ≥ 0,5 — та же кромка, что у полога рельефа и Terrain.forest_at. Без маски (рантайм-локации) — по классу «лес» карты 25 м, как раньше. И не на воде: доля воды (канал G маски — реки/озёра OSM, та же вода, что рисует рельеф) меньше vegetation.json → trees.water_max (0,35; кромка воды в шейдере — 0,5). WorldCover часто зовёт лесом кроны над руслом и острова, поэтому без этого деревья стояли «в воде». Вода — тоже кромка: у берега деревья гуще и утоплены меньше, как на опушке. Острова (role=inner) мультиполигонов OSM вычитаются из воды, внешние контуры собираются из участков (tools/osm/fetch_osm.py → join_rings; раньше каждый участок замыкался хордой и заливал пойму водой прямо по лесу). Импостеры берут ту же текстуру маски, что рельеф (без второй копии в видеопамяти).
  • Опушка: расстояние до кромки — кольца по 8 направлений (0,25…1,4 × edge_band_m, ближайший пиксель). В полосе edge_band_m доля клеток с деревом растёт от trees.density до edge_density, в клетке появляется второе дерево-модель (edge_extra), крона шире на edge_scale_k (высота прежняя — чтобы кроны меньше нависали над лугом при взгляде сверху). Утопление — от edge_sink_fraction у кромки до sink_fraction на edge_probe_m вглубь (DSM на опушке уже приподнят кронами).
  • Импостеры: те же правила в шейдере (4 направления на edge_band_m/2 и edge_band_m); у outer_m растворяются по одному (порог — хеш клетки) в полосе impostors.dissolve_m, без кольца. Тени не отбрасывают (тени — только у моделей LOD0–1, до 180 м). CPU-повтор правил — present_positions() (для тестов).
  • Пересчёт расстановки моделей (радиус 500 м, рабочий поток): ≈ 130 мс против 95 мс по классу 25 м.

Замеры V02 (стенд T01, Онгудай, 1920×1080, до = T02 / после):

  • Тест test_trees_mask: 38 707 деревьев (5 916 моделей + импостеры до 1,5 км) — 100 % на доле леса ≥ 0,5, ни одного центра в лугу, на просеках — 0; у кромки (0–8 м) деревьев на площадь ×1,6 против глубины.
  • S3 (300 м): луг в 0–30 м от кромки темнее луга в 100 м — 7,2 → 6,7 % (без деревьев 2,1 %); 50 % перехода яркости лес→луг — в 1–2 м от кромки маски (и до, и после), переход 10→90 % 15 → 12,5 м.
  • S5 (2 км): средняя яркость леса с импостерами / без деревьев на 3,1–3,7 км — 0,990–0,993 → 0,997–1,000 (скачок на границе импостеры/полог ≤ 0,3 %); лес/луг 1,073 при фото (a) 1,159 (−7 %).
  • 120 м AGL, pitch −50°: тени моделей на лугу у опушки не добавляют темноты (с тенями и без — ±0,4 из 255); тёмная полоса 20–30 м — нависание крон при взгляде сверху, не тень.
  • GPU деревьев (полный − --no-trees, медиана 4 прогонов): S1 0,51 → 0,46 мс, S3 0,52 → 0,48 мс.

Параметры (configs/vegetation.json → trees, поверх world.json → trees)

  • edge_band_m (16) — полоса опушки; edge_density (1,0), edge_extra (0,5), edge_scale_k (0,15) — см. выше.
  • edge_sink_fraction (0,15, в world.json 0,05) — утопление у самой кромки.
  • impostors.dissolve_m (800) — полоса растворения билбордов у outer_m.

Параметры (configs/vegetation.json → grass)

  • radius_m, clump_spacing_m, blades_per_clump, segments, blade_width_m — форма и число пучков.
  • blade_height_m, crop_height_m, shrub_density — высота по классу поверхности (луг/поле/кустарник).
  • forest_density (0,4), forest_height_m (0,12–0,35 м), forest_shade (0,8) — трава под пологом леса (класс FOREST, SF-2): реже, ниже и темнее луга — тень полога (под кронами светлого леса ~10–30 % света открытого места, травяной ярус покрывает ~30–50 % земли; оценка, не замер). Цвет — палитра луга в этой точке × forest_shade; у стволов не выкашивается. forest_density 0 — травы в лесу нет.
  • far_density_min, thin_start_k, thin_end_k — прореживание вдали (рекомендация 1): доля видимых пучков падает от 1.0 у камеры до far_density_min на доле thin_end_k от radius_m (начиная с thin_start_k); пучок виден, если hash(cell) < density. По умолчанию даёт в 0–10 м от камеры вершин в ≈ 6–7 раз больше на единицу площади, чем в кольце 20–30 м.
  • far_width_k — во столько раз шире дальние пучки (компенсирует прореживание визуально).
  • press_radius_m, press_agl_full_m, press_agl_zero_m — приминание у ног пилота (рекомендация 3): полное при высоте ног над землёй ≤ press_agl_full_m (0,4 м), ноль при ≥ press_agl_zero_m (1,2 м); нормаль травинки не переворачивается на изнанке (cull_disabled + явный NORMAL, изнанка светится BACKLIGHT, а не темнеет).
  • mowed_height_k, landing_mow_radius_m — круговое скашивание по умолчанию (см. API выше).
  • dryness_add_by_location — сухость травинок сверх палитры локации, только для травы; сейчас пусто — травинки того же цвета, что земля.
  • sway_hz, sway_amp, bend_amp, color_variation, max_agl_m — качание, разброс цвета, высота отключения; sway_hz_var — у каждого пучка своя частота (±25 %) и фаза, без синхронного «полотна».
  • density_pct — густота травы, %: пучков на площадь ∝ густоте (шаг clump_spacing_m / √(density_pct/100)), 0 — трава выключена. Задают пресеты графики (configs/game.json → graphics_presets.*.configs.vegetation: Низкое 0, Среднее 100, Высокое 150) и слайдер «Густота травы» в настройках (0–200 %, шаг 10, density_range_pct/density_step_pct; выбор пресета ставит его густоту, слайдер её переопределяет; применяется со следующей загрузки местности). Дальний слой (far.density_scaled = false) не меняется.
  • Травинки: blade_width_m 0,025 м (вдвое тоньше прежних 0,05), clump_spacing_m 0,21 м (вдвое чаще прежних 0,3 при 100 %), far_width_k 1,35 (прирост вдали вдвое меньше прежнего 1,7).
  • Цвет: травинка = цвет земли луга в своей точке (meadow_ground_color + meadow_macro, те же поля рельефа — GrassField.set_relief) × blade_shade (у корня / у кончика) × blade_tint; редкие сухие пучки — dry_clump_share. Зеленее/суше — только через terrain_look (вместе с землёй).

Замеры (V01, критерий приёмки)

  • Перенос grass из configs/world.json в configs/vegetation.json не меняет кадр (PSNR ≥ 45 дБ, тот же --autopilot --screenshot --time --camera, пока стенд T01 не готов).
  • Прореживание: вершин в кольце 20–30 м от камеры в 5–8 раз меньше на единицу площади, чем в 0–10 м (test_far_thinning_ratio, ratio ≈ 6,5).
  • Приминание: пилот на земле — полное (press_factor ≈ 1); в 1,5 м над землёй — 0 (test_press_full_at_low_agl, test_press_zero_above_1_2m).
  • Посадка прямоугольником: точка в 1 м от края внутри площадки — скошено, в 1 м за краем — нет (test_landing_rect_inside_and_outside).
  • Против солнца (--look/камера сзади, yaw +180°): изнанка травинок не темнее лица > 15 % — BACKLIGHT в grass.gdshader.
  • GPU травы (кадр с травой минус --no-grass в terrain_preview.gd) ≤ 0,3 мс на radius_m 30 м, clump_spacing_m 0,3 м.
  • Тонкая частая трава (0,025 м, шаг 0,21 м): GPU травы (тот же способ, --bench-static, Онгудай, 1918×1036, RTX 4070 SUPER, видеокарта делилась с другими прогонами — разброс большой, по p10): «Высокое» (150 %) ≈ 2,3–2,6 мс, «Среднее» (100 %) ≈ 2,0 мс, прежняя трава (HEAD, шаг 0,3) ≈ 1,7 мс — бюджет ~1 мс превышен, прежняя оценка 0,3 мс для нынешнего шейдера уже не верна.