Атмосфера
Модуль scripts/atmosphere/: ветер, термики, фоновое опускание, склоновый подъём,
подветренные зоны и роторы (FR-8, FR-11…FR-16), кучевые облака, их тени и птицы
(FR-14, FR-14a, FR-22, VR-1, VR-2). Модели и формулы — research/thermals.md.
Подключение (главная сцена)
var atmo := Atmosphere.new() # или узел в сцене со скриптом atmosphere.gd
add_child(atmo)
atmo.set_weather("weather/medium") # FR-16: weak / medium / strong или свой словарь
atmo.set_wind(15.0, 270.0) # км/ч на 10 м, откуда дует (0 — с севера)
atmo.set_ground(terrain.height_at, terrain.thermal_source_strength_at, terrain.surface_at)
atmo.set_sun_direction(sun_node.global_transform.basis.z) # к солнцу (тени облаков → термики)
atmo.focus_node = glider # вокруг кого генерировать термики (иначе — камера)
# MVP: несколько статичных термиков у старта
atmo.set_thermal_mode("both") # или "static" — только статичные
atmo.load_static_thermals(location.get("thermals", [])) # [{x_m, z_m, strength_ms, radius_m}]Нода сама вызывает step(dt) из _physics_process и сама рисует облака и птиц
(дочерние Clouds, Birds; в headless не создаются).
API Atmosphere (Node3D, группа "atmosphere")
| Метод | Что |
|---|---|
air_velocity_at(pos: Vector3) -> Vector3 | скорость воздуха (ветер + вертикальные потоки + пульсации), м/с, мир. Зовите для центра и концов крыла. |
mean_wind_at(pos) -> Vector3 | средний ветер без пульсаций и вертикальных потоков (колдун на старте, взлёт). |
step(dt) | продвинуть время атмосферы (сама нода зовёт из _physics_process). |
set_ground(height_fn, sun_fn, surface_fn = Callable()) | рельеф (x, z) -> высота над уровнем моря, сила источника термиков (x, z) -> 0..1, класс поверхности (x, z) -> int (для пылевых вихрей). |
set_weather(name_or_dict, blend_s = -1), configure(atmo_cfg, weather_cfg) | погода: словарь WeatherModel.derive (игра) или эталон configs/weather/* (тесты); blend_s ≥ 0 — мягкий переход без пересоздания термиков (ход дня, сигнал weather_updated) / полная настройка (тесты). |
set_wind(speed_kmh, from_deg, ref_msl = NAN) | ветер прогноза (FR-16) на высоте ref_msl (старт): выше — сильнее (wind.altitude_*). |
set_sun_direction(to_sun) | солнце: тени облаков ослабляют новые термики (VR-2). |
set_cloudbase_msl(m), get_cloudbase_msl() | нижняя кромка; по умолчанию средняя высота земли в радиусе 10 км + пресет. |
set_thermal_mode("dynamic"|"static"|"both") | генерация по рельефу / только статичные (MVP) / оба. |
add_static_thermal(x, z, strength_ms, radius_m) -> id, load_static_thermals(arr), clear_static_thermals() | статичные термики. |
set_focus(pos) / focus_node | центр генерации термиков. |
thermals_near(pos, r) -> Array[Dictionary] | для тестов и отладки. Пилоту не показывать (FR-22). |
time_s, turbulence_enabled | время атмосферы; выключатель пульсаций (тесты). |
Сигнал weather_changed — после смены погоды (configure), weather_updated — после мягкого обновления.
Погода из прогноза (FR-16)
Пилот задаёт прогноз: температуру днём T (дневной максимум у земли в долине), ветер на старте, откуда
ветер, облачность. WeatherModel (scripts/atmosphere/weather_model.gd, чистые функции; параметры —
configs/weather_model.json; план и ответы пилота — docs/archive/plan/weather-by-temperature.md) собирает из него
словарь с теми же ключами, что эталоны configs/weather/*:
- Место (
ground_context): долинаh_v— нижние 10 % рельефа в радиусе отсчёта кромки, средняяh_m; дата, широта, пояс. Алтай: 304 / 509 м. - Верх термиков и кромка: воздух наверху климатический (
upper_air.temp_cна 3 км, градиент 4 К/км), пузырь — сухая адиабата отT + 1 К:z_dry = h_v + (T + δ − T_u − Γ(z_u − h_v)) / (9,8 − Γ); кромкаz_lcl = h_v + 0,122 (T − Td),Td— обычная для месяца. Верх подъёмаmin(z_dry, z_lcl); запасm = z_dry − z_lcl < −100 м— голубой день (облаков нет). Кромка Cb — та же (разница T − Td и инверсия). - Сила, радиус, шаг, доля, фон, σ — по верху над средней землёй из таблицы опор (строки = бывшие
weak/medium/strong); × солнце сезона, × ветер рвёт термики; облака, переразвитие, Cb — по запасу
m. - Облачность (
sky): пеленаcirrus_coverослабляет прогрев (cirrus.sun_block), плюс мягкость, меньше рождений, без гроз под облачностью. - Ход дня (
derive(…, час),diurnal):T(t)по Parton–Logan от максимума; утренняя инверсия держит термики низко, пока прогрев её не пробьёт (кромка растёт всё утро); сила × (поток/пик)^⅓, доля × поток — вечером термиков мало, они шире и мягче (край, σ); к закату спокойнее приземный слой и гуще дымка.Gameпересчитывает день раз вdiurnal.update_sи зовётset_weather(w, blend_s). Инерция прогрева по классам поверхности —SurfaceHeating→Terrain.set_class_sun. - Волна — скрыта,
wave.enabled: false.
Таблица «прогноз → день» по локациям и месяцам: tools/weather/derive_table.tscn; как часто
грозы у старта — tools/weather/storm_rarity.tscn. Тесты —
test_weather_model, test_weather_day, test_storm_rarity, test_surface_heating, tests/game/test_forecast_game.gd.
Устройство
| Файл | Класс | Что |
|---|---|---|
atmosphere.gd | Atmosphere (Node3D) | тонкая нода: время, сборка поля скоростей, визуал |
wind_model.gd | WindModel | степенной профиль ветра, детерминированные пульсации (FastNoiseLite, нормированы на СКО = 1) |
wind_profile.gd | WindProfile | профиль ветра по высоте: α по классу устойчивости Паскуилла–Тёрнера (ветер, высота солнца, облачность), насыщение на z_sat — одна функция для WindModel и решателя поля (C2 v4) |
ground_field.gd | GroundField | кеш рельефа кусками 16×16 узлов по 30 м: высота, градиент, «линия тени» против ветра; билинейно |
thermal_field.gd | ThermalField | генерация по клеткам (детерминированно), жизненный цикл, сетка поиска, профиль |
atmo_thermal.gd | AtmoThermal | один термик: источник, наклон, времена жизни, облако |
cloud_model.gd | CloudModel | стадия/размер/положение облака, выбор видимых, слияние наложившихся |
cloud_layer.gd | CloudLayer (Node3D) | пул боксов с raymarch-шейдером |
cloud_shadow_map.gd, cloud_shadow_map.gdshader | CloudShadowMap | карта теней облаков (пропускание прямого солнца) вокруг камеры |
cloud_common.gdshaderinc | — | форма, плотность, свет, проход луча (общий код) |
cloud_volume.gdshader | — | бокс на облако (Compatibility) |
cloud_compositor_effect.gd, cloud_raymarch_cs.gdshaderinc, cloud_composite_cs.gdshaderinc | CloudCompositorEffect | compute: буфер пониженного разрешения + апскейл |
bird_flock.gd, bird.gdshader | BirdFlock (Node3D) | птицы в MultiMesh |
cloud_physics.gd | CloudPhysics | подсос, бурление в облаке, cloud_density_at |
storm_field.gd | StormField | Cb: нисходящий поток, растекание, фронт порывов |
wave_field.gd | WaveField | подветренные волны, роторы, гребни для лентикуляров |
cirrus_layer.gd, cirrus.gdshader | CirrusLayer | перистая пелена |
dust_model.gd, dust_devils.gd, dust_devil.gdshader | DustModel, DustDevils | пылевые вихри |
atmosphere_preview.gd | — | тестовая сцена scenes/atmosphere/atmosphere_preview.tscn |
Поле скоростей
w = fade(agl) · [фон · (1 − маска ядра) + Σ термики] + склон − ротор, горизонталь — ветер
профиля, ослабленный в подветренной зоне, плюс пульсации с СКО
√(механика² + конвекция² + край термика² + ротор²). Всё гладкое по пространству: консоли крыла
получают разный поток, крыло кренит на краю термика и болтает (FR-8).
Пульсации — два масштаба вихрей (WindModel.gust_unit): по горизонтали крупные
turbulence.large_scale_m (300 м) несут основную энергию, мелкие scale_m (45 м) — долю СКО
(scale_m/large_scale_m)^(1/3) ≈ 0,53 (Колмогоров), суммарное СКО то же. По вертикали — только мелкие
с той же долей (крупные вертикальные движения — термики и опускания, они в модели отдельно); именно
разница вертикальных потоков на концах кренит крыло. Доля крупных растёт от 0 у земли до полной на
large_fade_agl_m (300 м) над рельефом — на разбеге, посадке и у склона болтанка прежняя.
С одним масштабом 45 м курс мотало на ±20–30° при горизонтальных крыльях; замер —
tools/flight/roll_sway.gd (--large_scale=0 — как было).
Термик: профиль Gedeon
exp(−x²)(1 − x²)(кольцо опускания усиленоring_sink_factor), радиус растёт с высотой по Allen, подъём нарастает от земли∝ z^(1/3)и гаснет вtop_taper_mпод кромкой (набор ограничен кромкой, FR-14), ось наклонена ветром (подъём воздуха со скоростью ядра).Жизненный цикл: рост → зрелость → распад; на распаде низ столба отрывается (внизу пусто, наверху ещё несёт). Длительности — в
thermal.*_s.Снос: через
thermal.drift_delay_sпосле рождения пузырь отрывается от источника и уходит с ветром целиком (drift_factor× ветер середины столба; остаток — наклон). Ось столба (axis_at), облако, его тень и подсос берут снос из одногоAtmoThermal.drift_at(VR-0). Статичные термики (MVP) стоят над источником.Генерация: клетки
thermal_spacing_mв осях ветра, у каждой свой период и фаза; источник — лучшая поsun_fnиз нескольких точек; при ветре — стягивание к линиям облачных улиц. Чистая функция (клетка, цикл, зерно) — день воспроизводим.Склон:
k · U · (ŵ · ∇h)в точке, выдвинутой по ветру навысота × forward_shift_factor, затухаетexp(−agl/decay_height_m).Подветренная зона: пилот ниже линии тени
s = max(h(p − ŵd) − d·tg θ)→ опускание, ротор-болтанка, ослабленный ветер. Сила зоныlee = clamp((s − y + lee.shear_layer_m) / (depth_scale_m + shear_layer_m)) · clamp(r / relief_scale_m), гдеr = max(h(p − ŵd)) − h(p)— превышение гребня против ветра над землёй (каналGroundField,relief_at): на наветренном склоне и равнинеr = 0; слой сдвигаshear_layer_mнад линией — «ротор бьёт сверху», выше — воздух прежний. Пилот (опытный дельтапланерист): «за гребнем — смертельно опасно, туда нельзя». Опасностьg = smoothstep(danger_min_wind_ms, danger_full_wind_ms, ветер прогноза × поправка на высоту)(2 → 5 м/с): в слабый ветер зона мягкая (sink_per_wind0,25,turbulence_per_wind0,5), в умеренный/сильный — опусканиеlerp(0,25; danger_sink_per_wind 0,7; g) · u · lee, σ ротораlerp(0,5; 0,9; g) · u · lee(свой пределrotor_max_amplitude_ms, выше общегоturbulence.max_amplitude_ms), рывки внизburst_per_wind · u · lee · gв пятнах шума вышеburst_threshold(детерминированныйgust_unit, масштабburst_scale_m, переносится ветром) и ротор у склона — обратный поток у земли−rotor_reverse · u · lee · g · exp(−agl / (rotor_height_fraction · r)). Никаких подсказок — только последствия. Хребет 300 м, пятно 150–350 м за гребнем, 20–80 м AGL (test_lee_rotor):ветер на старте средний w σ наветренный w / σ 2 м/с (u≈2,5) −0,74 (−0,30·u) 0,94 (0,38·u) +1,2 / 0,6 5 м/с (u≈6,2) −3,8 (−0,62·u) 3,4 (0,55·u) +3,0 / 1,5 8 м/с (u≈9,9) −6,0 (−0,60·u) 5,0 (0,51·u) +4,8 / 2,4 Полёт на триммере через гребень на 50 м выше него при 6 м/с: за 30 с −126 м (штиль −25 м).
Облачный подсос и «в облаке» (FR-14b, FR-14c)
- Под крупным (диаметр >
thermal.suck_size_m) растущим/зрелым облаком над живым термиком подъём в последнихsuck_depth_mпод основанием растёт до (1 +suck_boost) × ядро; у Cb — сильнее (cb_suck). Под мелкими, молодыми и распадающимися облаками — прежний спад у кромки. Сила — из стадии и размера облака (CloudPhysics+ общая с рендеромCloudModel). - Внутри облака упорядоченного потока почти нет (средний подъём ×
in_cloud_mean_frac) — бурление: σturbulence.in_cloud_ms(3,5 м/с) мелкого масштабаin_cloud_scale_mво всех направлениях, и поток «выкидывает» к краю облака (in_cloud_eject_ms). «Затянет и выкинет». - Сильный день: изредка термики 7–9 м/с (
thermal_extreme_chance,thermal_extreme_ms) — под ними крупные облака с подсосом. Сухие термики без облака — доляdry_thermal_fraction(слабый день 0,5). Частота и сила термиков — отsun_fn(у terrain2 —thermal_source_strength_at: камни, границы поле–лес, склоны):source_frequency_exponent.
API для интегратора и flight:
cloud_density_at(pos) -> float0..1 — упрощённый купол облака (< 1 мкс вне облаков). «Белая мгла» у камеры: туман/затемнение с силойclamp(density × 1.5, 0, 1), нарастание ~0,3 с; горизонт не виден — только приборы (FR-21).turbulence_intensity_at(pos) -> float— СКО пульсаций воздуха, м/с (болтанка, в облаке — бурление). Перегрузку (FR-14c) flight считает сам поair_velocity_atна консолях и центре крыла; метрика — для звука/камеры/предупреждений.
Слова пилота в числах
Проверено численно (tests/atmosphere/test_pilot_words.gd, questions.md «Облака», FR-11,
FR-14b, FR-15):
| Слово пилота | Тест | Число |
|---|---|---|
| «Подсос под крупным облаком» | test_suck_layer_under_mature_cloud | слой [база−150, база] ≥ 1,3× подъёма на 0,5·zi; крыло в ядре входит в облако за ≤ 3 мин |
| «Под мелким/молодым — не тянет» | test_no_suck_under_young_or_decaying_cloud | слой у базы ≤ середины; не входит в облако за 5 мин |
| «Затянет и выкинет» (выкинет) | test_ejected_from_cloud_within_90s | выходит из облака (плотность < 0,1) за ≤ 90 с в ≥ 8/10 сидах |
| «В облаке бурление, нет ориентировки» | test_cloud_chaos_sigma_and_load_peak | σ в облаке ≥ 3 м/с (вне ≤ 1,5 м/с); пик перегрузки в облаке ≥ 1,5× пика в ясном воздухе |
| «Уйти можно» | test_can_escape_pulling_away_from_base | трапеция на себя + курс прочь — не входит в облако в ≥ 8/10 сидах |
| «+8 — уже надо валить» | test_extreme_thermals_rare_and_only_strong | strong: 2–6 % термиков за 2 ч в 20×20 км с ядром ≥ 7 м/с, всегда с крупным облаком; medium/weak — 0 % |
| «Вариометр даёт +8» | test_variometer_plus8_reachable_in_extreme_core | у крыла в ядре 8 м/с вариометр ≥ +7 м/с |
| «Термики без облаков (голубые)» | test_dry_thermal_fraction_and_climb | доля сухих: medium 15–35 %, weak ≥ 40 %; сухой термик даёт ≥ 1 м/с по варио |
| «Между термиками сыпет» (FR-15) | test_background_sink_medium | фон вне термиков medium −0,5 ± 0,15 м/с |
| «Кольцо слабее ядра» | test_ring_sink_weaker_than_core | доп. опускание в кольце сверх фона слабее −0,2·ядра |
Облака (FR-14, FR-14a)
Два пути рисования с общим кодом формы и света (cloud_common.gdshaderinc):
- compute (Forward+/Mobile, по умолчанию):
CloudCompositorEffectна активной камере — все облака в кадре одним raymarch-проходом в буфере пониженного разрешения (quality_presets.*.lowres_scale: 0,33/0,5/0,75), затем билатеральный апскейл по глубине (облака не «ореолят» по контуру гор). Облака вне пирамиды видимости отсекаются на CPU. - boxes (Compatibility;
clouds.renderer = "boxes"): бокс с raymarch-шейдером на облако.
Форма и свет:
- форма — 8 полуэллипсоидов-«башен» с широким центральным куполом; мощность облака
0,45–0,8 ширины (
height_to_width) — кучевые хорошей погоды шире, чем выше; высокие башни только при переразвитии. Ниже кромки плотности нет; к краям бокса плотность плавно гаснет (без швов); - клубы — готовые бесшовные 3D-текстуры Perlin-Worley (128³) и Уорли (64³)
(
assets/textures/clouds/*.png, генераторtools/atmosphere/gen_cloud_noise.py), мелкие края — искажение координат («завихрения») + две октавы мелкого шума; уровень мипа — по размеру пикселя и шагу луча (без «искр»); - резкая поверхность живого облака (
surface_sharpness), длинный переход у тающего; - поиск входа — по грубой оболочке без шума, внутри мелкие шаги (растут с расстоянием);
- свет: Бир по 3–6 геометрически растущим шагам к солнцу (первый 10 м — клубы затеняют друг
друга), многократное рассеяние, powder, серебристая кайма (Хеньи — Гринстейн), свет неба гаснет
по толщине облака над точкой (
sky_occlusion_m) — основание под толстой серединой темнее краёв, над живым термиком — ещё темнее (подсос); снизу — отражённый от земли свет; дымка изWorldEnvironment.
Стадия читается по облаку: растущее — клочья → плотное с чёткими кочанными краями;
зрелое — высокое, плотное, плоское тёмное основание; «активность» (термик жив) делает основание
плотнее и темнее; распад — рваное, прозрачное, с приподнятой рваной кромкой, оседает, уплывает
по ветру. Облако появляется, когда воздух дошёл до кромки (высота / сила × delay_factor), и тает
linger_s после конца термика. Облака только над термиками не слабее cloud_min_strength_ms
(слабый день почти «голубой»). Сильный день: доля термиков с переразвитием (высокие башни,
растекающийся верх). При ветре — улицы вдоль ветра, облака вытянуты по ветру.
Шум облака переносится ветром кромки: форма стоит над термиком, клубы «текут».
Тени — облако гасит ПРЯМОЕ солнце, рассеянный свет неба остаётся. CloudShadowMap раз в
clouds.shadow_map_interval_frames кадров рисует в маленьком SubViewport (только 2D) карту
вокруг камеры (shadow_map_size_m, разрешение и шаги луча — quality_presets.*.shadow_map_px,
shadow_steps): квад на облако, в пикселе — пропускание T = exp(−∫ρ·extinction) по лучу к
солнцу сквозь ту же плотность, что рисуется (cloud_common.gdshaderinc) — тень повторяет форму
облака, плывёт и тает вместе с ним. Карта — на опорной плоскости (земля под камерой); точка земли
на высоте y берёт её там, где её луч к солнцу пересекает плоскость — рельеф любой высоты точно.
Рельеф, трава, лес (импосторы, модели, конусы), кусты и камни подключают
scripts/terrain/cloud_shadow.gdshaderinc: свой light() с формулами встроенного освещения
Godot 4 (burley / lambert_wrap, schlick_ggx), солнце × 1 − shadow_strength·(1 − T); облако
закрывает и часть неба — рассеянный свет (AO) × 1 − shadow_sky·…. К краю карты тень тает
(shadow_edge_fade), дальше — дымка. Под перистой пеленой тени бледнее (cirrus.shadow_softening).
Замер (RTX 4070 SUPER): карта 512 px × 8 шагов ≈ 0,8 мс GPU за перерисовку (раз в 3 кадра),
256 px × 4 ≈ 0,08 мс; CPU ≈ 0,2–0,5 мс; light() — те же формулы, что встроенные, +1 выборка.
Физика (VR-2, VR-0): новые термики в тени зрелых облаков слабее (thermal.cloud_shade_factor,
ThermalField._cloud_shade — диск облака, смещённый от солнца), визуальная тень — от тех же
облаков над теми же термиками, с тем же сдвигом от солнца. Декалей больше нет; квад дымки
по-прежнему на своём слое clouds.shadow_exclude_layer (декали его не трогают).
Птицы — до 3 стай по 2–5 птиц в сильных зрелых термиках ближе 2,5 км к камере, кружат с креном
tg φ = v²/(g·r), медленно ходят по высоте, изредка машут. Модель assets/models/bird.glb
(генерируется tools/blender/bird.py), путь — birds.model_path, без файла — заглушка.
Заменяемые ассеты
clouds.noise_shape_texture,clouds.noise_detail_texture— бесшовныеTexture3D(канал R = 1 − плотность клубов). Сейчас — атласы срезов PNG (импортёр3d_texture), сгенерированныеtools/atmosphere/gen_cloud_noise.py. Пусто или нет файла — процедурный Уорли (NoiseTexture3D).birds.model_path— модель птицы.
Опасная и особая погода (VR-26, VR-27, VR-28, VR-18)
Всё видимое — из физической модели (VR-0).
- Перистая пелена (
cirrus_layer.gd,cirrus.gdshader): плоскость наcirrus.altitude_msl_mза камерой, волокна по ветру верхнего яруса, гало 22° при плотной пелене. Покрытиеweather.cirrus_cover→get_insolation(): термики слабее и реже (thermal.insolation_*), прямой свет на облаках слабее, тени кучевых бледнее (cirrus.shadow_softening). - Cb / гроза (
storm_field.gd, пресетweather/storm): сильные зрелые термики с долейcb_thermal_chance(у пресетов без него —cb_chance) переразвиваются в Cb (вид 3 в записи облака): башня доcb_top_above_base_m, наковальня — клин под тропопаузой, растекается по ветру (storm.anvil_*), тёмная снизу; ливень под основанием (storm.virga_*) — мягкие полупрозрачные полосы из крупного шума, вытянутого по вертикали, тают к земле (плотность ~2 % облачной: сквозь ливень видно рельеф); свет ливня — только рассеянный (он в тени Cb), шаг по нему крупнее. Вход луча в Cb уточняется делением шага пополам (бюджетный шаг у Cb — сотни метров; без этого плоское основание рисовалось «террасами»). Кадры:tools/shots/storm_shot(виды far/mid/under/rain/high,--bench=1,--ab=<старая плотность>). Физика: подсос до начала бури, затем ливневый нисходящий поток (storm.downdraft_ms), растекание у земли и фронт порывов, уходящий на километры (подъём над фронтом, сильная болтанка). Cb виден вcb_range_factorраз дальше. Потоки соседних ячеек не складываются: растекание у земли не сильнееstorm.outflow_ms, по вертикали — в пределах [−downdraft_ms,front_updraft_ms]. Грозы редкие (пилот, 29.09.2026): в модели погодыcb_thermal_chance— от сотых долей процента (запас ≥ 700 м, в Онгудае ~+26) до 0,5 % (≥ 1100 м, ~+34); замер по сидам —tools/weather/storm_rarity.tscn(Онгудай, 100 сидов: +26 — Cb ближе 15 км за первый час в ≤ 1 % полётов, буря у старта в начале — 0 %; +30 — Cb в 20–40 %, у старта 0–4 %; +34 — Cb в 50–70 %, у старта в начале 3–6 %). - Волна (
wave_field.gd, пресетweather/wave): λ = 2πU/N; смещение линий тока — свёртка уклона рельефа против ветра с cos(k·d)·exp(−d/L); в гребнях подъём доwave.max_ms, роторы у земли под гребнями. Облака: лентикулярные линзы («стопка тарелок») в гребнях η (wave.lens_*, несколько вдоль гребня), шапка на высшей точке рельефа, рваные роторные клочья. - Пылевые вихри (
dust_model.gd,dust_devils.gd,dust_devil.gdshader): у основания молодого (стадия роста) сильного термика над сухой поверхностью (dust.dusty_surface_classes, классыterrain.surface_at— передать третьим аргументомset_ground), живут 10–60 с, высота 20–200 м от силы термика, дрейфуют с приземным ветром; не большеdust.max_visibleрядом с камерой. Шанс —weather.dust_devil_chance.
Известные проблемы
- Compatibility (gl_compatibility): на части сцен облака делятся вертикальной границей на
серую и светлую половины со «ступенчатым» краем (скрин агента world_objects). Сейчас не чиним
(решение пользователя: целевой рендер — Forward+). Яркость в Compatibility исправлена:
вывод облаков, пелены и пыли в этом рендере не переводился из линейного в sRGB — переводим в
шейдере (
#if CURRENT_RENDERER == RENDERER_COMPATIBILITY). - Cb издалека похож на «гриб» (плоская наковальня над узкой башней): у настоящих Cb тело массивнее и шире; наковальня пока без маммаусов.
- Лентикуляры — гладкие линзы; слоистая «стопка тарелок» видна слабо.
Проходимость маршрута (FR-34a)
Замер бота-маршрутника (tests/atmosphere/xc/, карточка 01) по матрице 4 локации ×
{weak, medium, strong} × ветер {0, 15 км/ч с запада} × 5 сидов × 40 км, с --clouds и без
(tools/atmosphere/xc_matrix.sh, свод — tools/atmosphere/xc_report.py; 240 прогонов, эталоны —
research/xc_reference.md). Матрица прогоняется одной командой
tools/atmosphere/xc_matrix.sh за 3–6 мин (параллельность nproc/2, каждый прогон под timeout).
Картина (без правки конфигов, входная для карточки 03):
| Метрика | Медиана по всем 240 прогонам | Эталон (средний/сильный день) | Коридор |
|---|---|---|---|
| доля успешных (≥ 40 км без посадки) | 0 % (0/240) | — | ниже |
| набор в термике | −1,2…−1,8 м/с | 1–2 / 2–3,5 м/с | ниже (везде, во всех пресетах и локациях) |
| доля времени в кружении | 10–25 % | 40–55 % / 30–45 % | ниже |
| маршрутная скорость | 14–48 км/ч (широкий разброс) | 20–30 / 30–45 км/ч | не показательна — почти все полёты кончаются посадкой на 4–10 км |
| термиков на 10 км | ≈ 0 | 2–4 / 1,5–3 | ниже |
| вариометр ≥ +7 м/с | 0 | почти никогда / ≤ 2 % | в коридоре |
Что выбивается (вход для 03):
- Бот почти никогда не долетает 40 км ни на одной локации/пресете/сиде — везде посадка на
4–10 км (медиана дистанции по всем прогонам ≈ 4–6 км), включая
--clouds. - Набор в термике медианно отрицательный: бот заходит в кружение (961 попытка на 120
прогонов с
--clouds), но почти никогда не центрируется на подъём — из 437 попыток термика с--cloudsтолько 1 дала положительный чистый набор (gain_m > 0). Это похоже на проблему центровки бота в болтанке (см. итог 01: на синтетике с болтанкой 1/5 против 5/5 без неё), а не на недостаток термиков в атмосфере. --cloudsпочти не меняет картину при том же ветре/сиде (0 % успешных что с ним, что без) — расхождение с итогом 01 («с--cloudsнаходит 100 % термиков») объясняется тем, что «находит» относится к обнаружению термика под облаком, а не к успешному набору высоты в нём.- Один прогон (
ongudai/strong/wind15/seed2/--clouds) во время матрицы завис на конкурентной правкеxc_pilot.gdкарточкой 05 (компиляция падала на необъявленныхridge_track_*в промежуточном состоянии файла) — не баг бота/атмосферы, а гонка параллельных карточек;xc_matrix.shоборачивает каждый прогон вtimeoutи помечает такие случаи"timeout"/"error"отдельно от посадки.
Полный свод по всем 24 парам «локация × пресет» с квартилями — tools/atmosphere/xc_report.py tmp_xc_matrix (каталог результатов не в git).
Центровка в болтанке и «идеальный пилот» (карточка 07)
xc_run --ideal — диагностический пилот (XcIdealPilot), знающий оси термиков: летит к
ближайшему живому термику и кружит вокруг оси той же техникой, что и бот (крен 38°, скорость
мин. снижения, сдвиг круга с поправкой на ветер). Матрица: XC_SEEDS="1 2 3" XC_CLOUDS=0 XC_EXTRA=--ideal tools/atmosphere/xc_matrix.sh (4 локации × 3 сида; в JSON — circling_air:
подъём термика у пилота, вертикаль воздуха и вариометр за время кружения).
| День, ветер | набор в кружении | подъём термика у пилота | вариометр − воздух (вираж + фон) |
|---|---|---|---|
| weak, 0 | −0,45 м/с | 0,77 | −1,2 |
| medium, 0 | +0,08 (термики с набором +0,56) | 1,44 | −1,2 |
| medium, 15 км/ч | −0,76 | 0,43 | — (сносит из ядра) |
| strong, 0 | +1,15 (с набором +1,59) | 2,51 | −1,2 |
| strong, 15 км/ч | +1,39 | 2,39 | −0,9 |
Даже идеальный пилот в средний день не набирает (+0,1 м/с при эталоне 1–2): подъём на радиусе
круга (~15–20 м от оси) в среднем 1,4 м/с (с отбором термиков ≥ 2 м/с — 1,9–2,1 и набор
+0,6…+0,8), а снижение дельтаплана на вираже 38° ≈ 1,2 м/с. Вход для 03: чтобы средний день
давал ≥ +1 м/с у идеального пилота, подъём у оси на рабочих высотах должен быть ≈ 2,3–2,5 м/с —
medium.thermal_strength_ms ≈ [2,0; 4,0] (сейчас [1,0; 3,5]); weak — ≈ [1,2; 2,5]. Второе: в
ветре столб наклонён, но неподвижен над источником (сносится только на распаде), поэтому
пилот, набирающий медленнее воздуха (c < w), выносится из ядра со скоростью ≈ u·(1 − c/w)
(15 км/ч, c ≈ 1, w ≈ 2 → ~3 м/с на высоте кружения); в жизни термик уходит с ветром целиком
(на этом основано определение ветра по сносу в кружении). Кандидат для 03 — снос всего столба
ветром (не только на распаде) или наклон по скорости набора пилота.
Честный бот (вариометр + GPS): вираж сдвигается выполаживанием, когда нос смотрит к центру подъёма, с поправкой на снос ветром (оценка ветра — GPS минус воздушная скорость по курсу); центр — по первой гармонике подъёма за полный круг (МНК, позиции с запаздыванием отклика вариометра), пересчёт раз в полкруга; первый круг — вокруг места пика подъёма на входе; выход из пустого кружения — по двум слабым кругам без признаков ядра рядом. Синтетика (ядро 3 м/с, 30 км): без болтанки 5/5 (набор 1,53 м/с); с болтанкой 3/5 сидов 1–5 (7/10 по сидам 1–10), набор 1,12–1,27 м/с = 75–81 % от идеального (1,57), доля кружения 0,54–0,61. На реальных локациях (матрица medium/strong, 160 прогонов) — 0 успешных: бот со старта 300 м AGL почти не попадает в ядра (подъём термика у пилота в кружении ≈ 0), это поиск, а не центровка.
Калибровка (карточка 03)
Числа по входным данным A07 (идеальный пилот: набор ≈ подъём у пилота − 1,2 м/с) и эталонам
research/xc_reference.md. Тест — tests/atmosphere/test_calibration.gd.
| Параметр | weak | medium | strong | Почему |
|---|---|---|---|---|
thermal_strength_ms | [1,2; 2,5] (было [0,5; 2,0]) | [2,0; 4,0] (было [1,0; 3,5]) | [2,5; 5,0] (было [2,0; 5,0]) | средний день: у оси на рабочих высотах ≈ 2,3–2,8 м/с → набор ≥ +1 (эталон 1–2); слабый — около нуля, трудный; сильный — чуть сильнее, ядро ≤ 5 (FR-11) |
thermal_radius_m | [60; 110] | [90; 170] | [110; 210] | Allen: r ≈ 0,1·zi·0,75 у кромки (84 / 138 / 199 м) |
thermal_spacing_m | 1500 | 800 (было 1250) | 700 (было 1000) | Allen: N ≈ 0,6·XY/(zi·r) → шаг ≈ 650 м при zi 1800; на маршруте — термик каждые 1–3 км |
cloud_size_factor | 1,0 (было 1,6) | 0,8 (было 2,1) | 0,9 (было 2,1) | термиков больше и они сильнее — облака мельче при прежней облачности; «+8» (7–9 м/с) по-прежнему под облаком ≥ 1,6 км (подсос) |
Модель (configs/atmosphere.json → thermal):
drift_factor0,85,drift_delay_s60: термик сносится ветром целиком (раньше основание стояло, и пилот, набирающий медленнее воздуха, выносился из ядра со скоростью ≈ u·(1 − c/w)); теперь расхождение ≈ 0,15·u. Источник даёт следующий пузырь в следующем цикле клетки.source_candidates8 (было 6): в клетке 800 м источник надёжнее садится на триггер (гребень, скалы, опушка —thermal_source_strength_at); такие клетки дают термики чаще (source_frequency_exponent) — у стартового гребня термик в 1–3 км.- «+8» не менялись:
thermal_extreme_chance0,04 только у strong (2–6 % термиков).
Склон на стартах (FR-12, FR-13, карточка 05)
Проверка (tests/atmosphere/test_ridge_starts.gd): на каждом из 10 стартов всех 4 локаций —
ветер 20 км/ч точно в склон (heading_deg старта), термики выключены (static, пусто), крыло
sport, бот в режиме «восьмёрки» (XcPilot.setup_ridge) держится 5 мин в полосе 50–100 м AGL
перед гребнем (виток вокруг рабочей точки, найденной перебором вдоль склона по полю подъёма —
как пилот, набравший немного высоты на взлёте, сам ищет рабочую линию). Диаметр витка
подстраивается под ширину «ядра» подъёма (не шире половины пикового значения) — на узком
реальном рельефе широкий виток выносит бота в соседний провал.
| Старт | Набор за 5 мин, м |
|---|---|
| altai/sinyukha_west | 134 |
| altai/sinyukha_east | 242 |
| altai/tugaya_south | −85 (слабый/узкий склон, см. ниже) |
| askarovo/biyagoda_west | 195 |
| askarovo/biyagoda_east | 65 |
| askarovo/biyagoda_south_west | 261 |
| aushkul/aushtau_east | 126 |
| aushkul/aushtau_south | 56 |
| aushkul/ridge_west | −25 (слабый/узкий склон, см. ниже) |
| ongudai/kayancha_south | 225 |
При ветре 8 км/ч набора нет ни на одном старте (test_ridge_no_climb_8kmh). Подветренная
проверка (ветер с обратной стороны хребта, точка ниже линии тени за гребнем): среднее w < 0
на всех 10 стартах; болтанка заметно сильнее наветренной на 7 из 10 (на tugaya_south,
biyagoda_east, ridge_west — тот же рельеф, что и слабый по набору: линия тени там неглубокая,
разница болтанки в пределах шума).
Правка модели по итогам: ridge.efficiency в configs/atmosphere.json поднят с 0,7 до 0,85
(доля потока вдоль склона, идущая вверх) — без этого набор был отрицательным почти везде.
altai/tugaya_south и aushkul/ridge_west — самый узкий и слабый рельеф из всех 10 стартов
(пик подъёма в полосе 50–100 м AGL — 0,8–1,15 м/с, рядом, в 50–100 м вдоль гребня, — провал такой
же силы); перебор ridge.efficiency (0,7…1,0), диаметра витка (40…250 м) и центровки по карте
подъёма (как в _circle, вместо фиксированной точки — резко хуже) не даёт устойчивого набора на
этих двух стартах за 5 мин — предел самой модели/рельефа, а не настройки бота или теста; критерий
для них снижен (см. WEAK_CLIMB_SITES в тесте) с пояснением. Для калибровки (карточка 03):
возможно, оба старта рабочие только в более сильный день (weather/strong), либо склоновую
модель для узких гребней стоит усилить отдельно (сила профиля растёт быстрее с эффективностью,
чем провал по бокам — сейчас оба растут пропорционально).
Конфиги
configs/atmosphere.json— модель (профиль, цикл, генерация, склон, подветренная зона, турбулентность, облака, птицы). Каждый параметр с*_doc.configs/weather_model.json— погода из прогноза (см. выше).configs/weather/*.json— эталоны для тестов физики и калибровки (в игре не выбираются): ветер, кромка, сила/радиус/частота термиков, фон, болтанка, облака, улицы, переразвитие, режим и статичные термики.- Качество облаков:
clouds.quality=low|medium|high(clouds.quality_presets).
Производительность
Замеры (RTX 4070 SUPER, Forward+, окно ~960×1036; облака = разница кадра с облаками и без):
| Что | Время |
|---|---|
air_velocity_at | ~10 мкс/вызов (планер: 5 точек × 120 Гц ≈ 6 мс/с CPU) |
step | ~35–80 мкс в среднем |
облака compute low (буфер 0,33) | 0,5–0,8 мс GPU |
облака compute medium (0,5) | 2,5–3,1 мс GPU |
облака compute high (0,75) | 3,9–8 мс GPU |
облака boxes medium (полное разрешение) | 5,8–13 мс GPU |
| выбор видимых облаков (CPU) | ~2–5 мс раз в 1 с; форма — по кругу за 15 кадров |
Встроенная графика слабее в 10–15 раз: для неё quality: "low" (≈ 6–12 мс).
Задел: временное накопление (перепроекция прошлого кадра, 1/4 пикселей за кадр) — ещё в 2–4 раза дешевле.
Тесты
godot --headless --path . res://tests/run_tests.tscn -- --filter=atmosphere — 22 теста:
профиль (ядро > 0, кольцо < фона, убывает, шире наверху), асимметрия на консолях, снос и наклон,
жизненный цикл (огибающая, отрыв низа, смена термиков за час), фон, кромка ограничивает набор,
склон (наветренный > 0, выдвинут вперёд, затухает; подветренный < 0, ветер слабее), болтанка
(у земли и за гребнем сильнее, детерминирована), детерминизм генерации, источники по солнцу,
облака (стадии, снос, голубой день, слияние), улицы, статичные термики из конфига, тени облаков,
производительность; test_pilot_words.gd — слова пилота в числах (таблица выше);
test_calibration.gd — калибровка дней, снос термика целиком, источники у стартов.
Тестовая сцена
godot --path . res://scenes/atmosphere/atmosphere_preview.tscn -- --weather=strong --view=side
--view=horizon|side|near|under|sun|top|stages|birds|cb|cbnear|cbfar|wave|dust,--cirrus=<0..1>,--ground=ridge,--time=<с>,--wind=<км/ч>;--shot=<png>— сохранить кадр и выйти;--bench— GPU-время кадра с облаками и без;- WASD/QE — полёт, ПКМ — обзор, Shift — быстрее, 1–5 — ракурсы, T — +60 с.