Атмосфера

Модуль 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.gdAtmosphere (Node3D)тонкая нода: время, сборка поля скоростей, визуал
wind_model.gdWindModelстепенной профиль ветра, детерминированные пульсации (FastNoiseLite, нормированы на СКО = 1)
wind_profile.gdWindProfileпрофиль ветра по высоте: α по классу устойчивости Паскуилла–Тёрнера (ветер, высота солнца, облачность), насыщение на z_sat — одна функция для WindModel и решателя поля (C2 v4)
ground_field.gdGroundFieldкеш рельефа кусками 16×16 узлов по 30 м: высота, градиент, «линия тени» против ветра; билинейно
thermal_field.gdThermalFieldгенерация по клеткам (детерминированно), жизненный цикл, сетка поиска, профиль
atmo_thermal.gdAtmoThermalодин термик: источник, наклон, времена жизни, облако
cloud_model.gdCloudModelстадия/размер/положение облака, выбор видимых, слияние наложившихся
cloud_layer.gdCloudLayer (Node3D)пул боксов с raymarch-шейдером
cloud_shadow_map.gd, cloud_shadow_map.gdshaderCloudShadowMapкарта теней облаков (пропускание прямого солнца) вокруг камеры
cloud_common.gdshaderinc—форма, плотность, свет, проход луча (общий код)
cloud_volume.gdshader—бокс на облако (Compatibility)
cloud_compositor_effect.gd, cloud_raymarch_cs.gdshaderinc, cloud_composite_cs.gdshaderincCloudCompositorEffectcompute: буфер пониженного разрешения + апскейл
bird_flock.gd, bird.gdshaderBirdFlock (Node3D)птицы в MultiMesh
cloud_physics.gdCloudPhysicsподсос, бурление в облаке, cloud_density_at
storm_field.gdStormFieldCb: нисходящий поток, растекание, фронт порывов
wave_field.gdWaveFieldподветренные волны, роторы, гребни для лентикуляров
cirrus_layer.gd, cirrus.gdshaderCirrusLayerперистая пелена
dust_model.gd, dust_devils.gd, dust_devil.gdshaderDustModel, 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_wind 0,25, turbulence_per_wind 0,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) -> float 0..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_strongstrong: 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 км≈ 02–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,760,43— (сносит из ядра)
strong, 0+1,15 (с набором +1,59)2,51−1,2
strong, 15 км/ч+1,392,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.

ПараметрweakmediumstrongПочему
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_m1500800 (было 1250)700 (было 1000)Allen: N ≈ 0,6·XY/(zi·r) → шаг ≈ 650 м при zi 1800; на маршруте — термик каждые 1–3 км
cloud_size_factor1,0 (было 1,6)0,8 (было 2,1)0,9 (было 2,1)термиков больше и они сильнее — облака мельче при прежней облачности; «+8» (7–9 м/с) по-прежнему под облаком ≥ 1,6 км (подсос)

Модель (configs/atmosphere.json → thermal):

  • drift_factor 0,85, drift_delay_s 60: термик сносится ветром целиком (раньше основание стояло, и пилот, набирающий медленнее воздуха, выносился из ядра со скоростью ≈ u·(1 − c/w)); теперь расхождение ≈ 0,15·u. Источник даёт следующий пузырь в следующем цикле клетки.
  • source_candidates 8 (было 6): в клетке 800 м источник надёжнее садится на триггер (гребень, скалы, опушка — thermal_source_strength_at); такие клетки дают термики чаще (source_frequency_exponent) — у стартового гребня термик в 1–3 км.
  • «+8» не менялись: thermal_extreme_chance 0,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_west134
altai/sinyukha_east242
altai/tugaya_south−85 (слабый/узкий склон, см. ниже)
askarovo/biyagoda_west195
askarovo/biyagoda_east65
askarovo/biyagoda_south_west261
aushkul/aushtau_east126
aushkul/aushtau_south56
aushkul/ridge_west−25 (слабый/узкий склон, см. ниже)
ongudai/kayancha_south225

При ветре 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 с.