Обтекание склона ветром: что у нас сейчас, как в жизни, что можно сделать

Исследование, без правок кода. Повод — отзыв пилота: «обтекание склона ветром ощущается не физично». Дополнительно от пилота:

  • «Когда ветер дует в понижение между двумя вершинами (седловина, перевал) — там труба, ветер сильнее». Разобрано отдельно (§3.6, §4, варианты в §5).
  • «А провалы чёткие, бросает конкретно ногами к тросам, похоже» — провалы и удары вниз ощущаются правильно. Любая переделка склонового потока обязана их сохранить (§1.6).

Сентябрь 2026. Ссылки на источники — в тексте и в конце.


1. Как сейчас считается ветер у рельефа

Всё проходит через одну функцию Atmosphere.air_velocity_at(pos) (scripts/atmosphere/atmosphere.gd). Её вызывает модель полёта в трёх точках — центр и две консоли (FlightModel._step_air, air_sampling.tip_fraction), поэтому разница потоков по размаху кренит крыло. Рельеф атмосфера видит через кеш GroundField (ground_field.gd): сетка 30 м (ground.cell_m), в каждом узле — высота, dh/dx, dh/dz (центральные разности, база 60 м), «линия тени» и «превышение гребня» против ветра; между узлами — билинейно.

1.1 Горизонтальный ветер

u = speed_ref · profile(agl) · altitude_factor(msl)
profile(agl)        = min((max(agl, 1 м) / 10 м)^0,14 ; 1,8)
altitude_factor(msl)= clamp(1 + 0,6 · (msl − msl_старта)/1000 ; 0,5 ; 2,0)
направление         = всегда направление прогноза (ŵ), одно на весь мир
  • Профиль по высоте — степенной закон от высоты над землёй под пилотом (agl). Над гребнем на 50 м AGL ветер тот же, что над долиной на 50 м AGL.
  • altitude_factor — ветер растёт с высотой над морем (над стартом +60 %/км). Это единственное, что делает ветер над вершиной сильнее, чем в долине, — но оно зависит только от абсолютной высоты, а не от формы рельефа.
  • Направление никогда не меняется рельефом: поток не огибает холм сбоку, не затекает в лощину, не поворачивает вдоль долины. Ослабление только одно — в подветренной зоне (lee.wind_reduction 0,6) и обратный поток ротора.

1.2 Склоновый подъём (вертикаль)

shift   = min(0,8 · agl ; 400 м)                  # точка берётся ВПЕРЕДИ по ветру
g       = ground.sample(pos + ŵ·shift)            # высота и градиент там
slope   = ŵ · ∇h(g)                               # уклон вдоль ветра, безразмерный
agl_s   = max(y − h(g), 0)                        # высота над той точкой
w_ridge = clamp(0,85 · u · slope · exp(−agl_s / 250 м) ; ±6 м/с) · (1 − lee)

(configs/atmosphere.json → ridge: efficiency 0,85, decay_height_m 250, forward_shift_factor 0,8, forward_shift_max_m 400, max_lift_ms 6.)

Смысл: у самой земли воздух идёт вдоль склона, w = U·tg(уклона) (граничное условие), и 85 % этого приписывается пилоту. С высотой зона подъёма выдвигается против ветра (сдвиг 0,8·agl) и затухает экспонентой с постоянным масштабом 250 м, одинаковым для любого рельефа.

1.3 Подветренная зона и ротор

Линия тени s = max_d (h(p − ŵd) − d·tg 12°) по отсчётам d = 60…1600 м против ветра. Пилот ниже неё (и в слое сдвига 60 м над ней) — в зоне силы lee 0..1, умноженной на clamp(r / 60 м), где r — превышение гребня над землёй под пилотом. В зоне: опускание 0,25…0,7 · u · lee, болтанка σ = 0,5…0,9 · u · lee, редкие рывки вниз 1,0 · u · lee · g (пятна шума 120 м), обратный поток у земли −0,9 · u · lee · g · exp(−agl / 0,4r); жёсткость g растёт от ветра 2 до 5 м/с. Подробно — docs/guide/atmosphere.md, проверка — tests/atmosphere/test_lee_rotor.gd.

Отдельно — подветренные волны (wave_field.gd, линейная теория вдоль ветра со Скорером), по умолчанию выключены (wave_strength 0 во всех пресетах).

1.4 Турбулентность у склона

σ = √(механика² + конвекция² + край термика² + ротор² + …), механика 0,14 · u · (1 + e^(−agl/150)). От формы склона механическая болтанка не зависит — только от ветра и высоты над землёй. Поле шума — «замороженное», переносится ветром одного направления.

1.5 Где это не физично (по пунктам)

Сравнил нашу формулу с линейным потенциальным обтеканием двумерного хребта (профиль Аньези, H = 300 м, ветер 5 м/с; скрипт — в сессии, воспроизводится за минуту). Максимум w на наветренной стороне на заданной высоте над землёй:

agl, мкрутой хребет (L = 300 м, макс. уклон 33°) — у нас / потенц.пологий (L = 800 м, 14°) — у нас / потенц.прирост ветра над гребнем (потенц.) крутой / пологий
202,7 / 2,91,0 / 1,2+88 % / +36 %
502,5 / 2,40,9 / 1,1+73 % / +33 %
1002,3 / 1,80,75 / 0,96+56 % / +30 %
2001,8 / 1,20,54 / 0,78+36 % / +24 %
3001,4 / 0,80,39 / 0,64+25 % / +20 %
5000,7 / 0,50,20 / 0,46+14 % / +14 %

(L — полуширина хребта на половине высоты. Потенциальный поток — верхняя оценка без трения и отрыва; порядок величин подтверждается измерениями, §3.1.)

Выводы:

  1. Сила подъёма у склона в целом правдоподобна — у земли совпадает с потенциальным потоком в пределах 10–20 %, место максимума тоже (x ≈ −0,6…−0,7 L).
  2. Высота полосы подъёма не зависит от размера склона. В жизни (и в теории) поле возмущения затухает на масштабе ширины склона L: узкий крутой гребень — подъём низкий, «прижатый»; широкий склон — полоса высокая. У нас всегда 250 м: над узким гребнем подъём тянется слишком высоко (на 300 м AGL — 1,4 вместо 0,8 м/с), над широким склоном гаснет слишком рано (0,39 вместо 0,64).
  3. Нет ускорения ветра над гребнем. Реально на бровке ветер на 30–90 % сильнее, чем перед склоном (§3.1) — пилот чувствует это как «на бровке встал, не проходишь вперёд», «на верхушке дует». У нас горизонтальный ветер меняется только от высоты над морем.
  4. Мелкий рельеф бьёт на любой высоте. Уклон берётся в одной точке с базой 60 м. Лощинка шириной 100 м даёт такой же провал на 100 м AGL, как у земли (только умноженный на общую экспоненту 250 м). Реально возмущение от детали размером λ затухает с высотой как exp(−2π z / λ): на высоте ~λ/2 от неё почти ничего не остаётся. Это прямо видно в docs/guide/atmosphere.md: на tugaya_south и ridge_west «рядом, в 50–100 м вдоль гребня, провал такой же силы», что и пик. Возможное следствие — склон «пятнистый», полоса подъёма рваная. (Гипотеза, не проверял: height_at — DSM Copernicus с кронами; ступенька у опушки 10–12 м даёт у нас «склон» ~0,2 на 60 м, т. е. заметный подъём/провал на опушке и на высоте.)
  5. Поток не трёхмерный. Уединённая сопка, отрог («нос») и цирк («чаша») с одинаковым уклоном вдоль ветра дают у нас одинаковый подъём. Реально: сопку воздух частично обтекает с боков (подъём и ускорение слабее, чем у длинного хребта: коэффициент 1,6 против 2,0 в §3.1), в чаше поток сходится — подъём сильнее; на отроге — расходится.
  6. Угол ветра к хребту учтён только как cos (скалярное произведение с градиентом). Поток вдоль склона, «скатывание» ветра вдоль хребта и усиление болтанки при косом ветре — нет.
  7. Седловина / перевал («труба») — нет, и даже наоборот. В седловине уклон вдоль ветра обычно положе, а высота над морем ниже → у нас в седловине и подъём, и ветер слабее, чем на соседних вершинах. Сжатия потока в понижении нет вообще (§3.6).
  8. Нет устойчивости атмосферы. Склоновый поток одинаков в устойчивый и неустойчивый день. В жизни при устойчивой стратификации (инверсия, утро) низкий воздух не переваливает через высокие вершины, а обтекает их и идёт в понижения — «труба» в перевалах сильнее, подъём ниже.
  9. Масса не сохраняется. w и горизонталь считаются независимо: над гребнем воздух поднимается, но не ускоряется; при shift над самой бровкой пилот уже получает опускание подветренного склона.
  10. Отрыв — только через «линию тени» с фиксированным углом 12°, не зависящим от ветра, шероховатости и формы гребня; наветренного отрыва у подножия крутого обрыва («пузырь») нет.

1.6 Что даёт «чёткие провалы» — не ломать

По отзыву пилота провалы и удары вниз — правильные. Они складываются из:

ИсточникГдеПараметры
Кольцо опускания вокруг термикаthermal_field.gd, профиль Gedeonthermal.ring_sink_factor 1,6
Фоновое опускание между термикамиair_velocity_at, _bg_sinkпресет background_sink_ms
Опускание и рывки вниз за гребнем_lee_flowlee.danger_sink_per_wind, burst_*
Вертикальные пульсации (мелкие вихри 45 м)WindModel.gust_unit, vertical_ratio 0,7turbulence.*, mech_per_wind
Роторы волн, грозыwave_field.gd, storm_field.gd—
Отклик крыла и «тела»FlightModel (три точки забора воздуха → угол атаки → перегрузка), LoadMeter.load_raw, GliderVisual._update_buzz (buzz.kick_m_per_g — толчок трапеции от мгновенной перегрузки)flight.json → visual.buzz, load_factor.filter_s

Правила для любых переделок склона:

  • заменять только слагаемое w_ridge и множитель горизонтального ветра; lee, термики, фон, турбулентность и отклик крыла — не трогать;
  • механическую болтанку и подветренное опускание считать от ветра без ускорения над гребнем (иначе σ на бровке вырастет на те же 30–90 % — это уже другая болтанка);
  • после правки гонять test_ridge_starts, test_lee_rotor, tools/flight/roll_sway.gd и дать пилоту слетать в тот же день.

2. Реальная физика: итог в числах

ЯвлениеТипичные значенияИсточник
Ускорение над вершиной (у земли, ~10 м)ΔS_max ≈ B·H/L: B ≈ 2,0 (длинный хребет), 1,6 (сопка), 0,8 (уступ/обрыв на плато)Jackson & Hunt 1975; Taylor & Lee 1984
Спад ускорения с высотойΔS(z) ≈ ΔS_max · exp(−A·z/L), A ≈ 3 (хребет), 4 (сопка), 2,5 (уступ)Taylor & Lee 1984, так же в ASCE 7 (K3)
Насыщениепри H/L > 0,5 (средний уклон ≳ 14–27°) ускорение больше не растёт — поток отрываетсяASCE 7 (правило «H/Lh ≤ 0,5»)
Askervein (H = 116 м, уклоны до 20–30 %)ускорение на вершине на 10 м ≈ +80 % (по сводкам)Taylor & Teunissen 1987
Седловина (натурные замеры)на 10 м AGL в седловине +40–60 % к подходузамеры над седловиной, Boundary-Layer Meteorol.
Хребты в горах в синоптический ветерна гребнях ветер до ×3 к долинеmountainmeteorology.org
Где лучший подъёмчуть ниже бровки на наветренной стороне; с набором высоты — дальше вперёд от склонаCondor, York Soaring, Chess in the Air
Угол ветрахорошо ±20° от перпендикуляра; при ≥45° подъём сильно слабее, болтанка большеChess in the Air, York Soaring
Ветер для паренияработает от ~5 м/с (10 уз), хорошо 7–10 м/с; болтанка ∝ ветру²Chess in the Air
Отрыв за гребнемначинается при подветренных уклонах порядка 15–20° и круче (зависит от шероховатости)Wood 1995 и др.

3. Реальная физика подробнее

3.1 Ускорение над гребнем (линейная теория)

Jackson & Hunt (1975) разделили поток над пологим холмом на тонкий внутренний слой (где трение, толщина l из l·ln²(l/z0) ≈ 2κ²L — для наших склонов единицы–десятки метров) и внешний почти невязкий. Почти всё ускорение — во внутреннем слое, максимум — на ~1/10 его толщины. Практические «правила» Taylor & Lee (1984) (они же в строительной норме ASCE 7, коэффициент Kzt = (1 + K1·K2·K3)²):

  • ΔS_max = B · H / L, где H — высота холма, L — расстояние от вершины до половины высоты против ветра;
  • ΔS(z) = ΔS_max · exp(−A · z / L);
  • вдоль ветра ускорение падает от вершины линейно до нуля на ~1,5 L перед ней (ASCE 7, K2);
  • при H/L > 0,5 брать H/L = 0,5 (обтекание с отрывом).

Проверка на Askervein (H = 116 м, L ≈ 250 м): 1,6 · 0,46 ≈ 0,74 против измеренных ≈ 0,8.

Вывод для нас: ускорение над бровкой порядка десятков процентов и затухает на масштабе L — у узкого гребня прижато к земле, у широкого — толще.

3.2 Полоса подъёма

  • У земли воздух идёт вдоль склона, w ≈ U·tg(уклона).
  • С высотой вертикальная скорость гаснет на масштабе ~L, зона подъёма смещается вперёд (против ветра): «чем выше — тем дальше от склона» (York Soaring, Condor). Наше смещение 0,8·agl качественно так же (в потенциальном потоке максимум уходит примерно на 0,3·agl, при круче склоне — дальше).
  • «Лучший подъём чуть ниже бровки на наветренной стороне; если провалился ниже бровки — обратно не вылезешь» (York Soaring). «Сжатие» линий тока над гребнем — ускорение §3.1.
  • Высота полосы — «примерно высота склона», на хорошем хребте — до 1–2 высот над гребнем (для планеров в FSX-моделях — «на 300–600 м выше гребня с убыванием»).
  • Чаша/цирк собирает поток и даёт подъём лучше, выпуклый отрог — хуже (поток расходится) (Condor: «bowls and gullies channel the air and give improved lift»; Chess in the Air).
  • Устойчивый воздух — ровный предсказуемый подъём, но сильнее опускание и болтанка за гребнем; неустойчивый — подъём рвут термики.

3.3 Отрыв и роторы

  • На пологих холмах (Askervein, уклоны < 20 %) поток прилегает почти всегда.
  • Отрыв за гребнем начинается при подветренных уклонах порядка 15–20° и круче, раньше — при шероховатой поверхности (лес) и острой бровке (Wood 1995). Зона возвратного течения тянется на несколько высот хребта за гребень.
  • У подножия крутого обрыва на наветренной стороне тоже бывает «пузырь» с возвратным течением (наблюдался лидаром на натуре, модели часто его не видят).
  • RANS/CFD плохо предсказывают турбулентность в следе даже на Askervein; LES лучше. То есть подветренная сторона — самое трудное место для любых моделей, и эмпирика (наша lee) там вполне оправдана.

3.4 Профиль ветра у земли

Логарифмический/степенной профиль (показатель 0,14 — открытое место, лес — больше). Над гребнем профиль «выпрямляется»: максимум ускорения у земли, поэтому на бровке ветер у земли почти такой же, как на 50–100 м — на старте «дует сильнее, чем в долине» и ощущается «ровнее».

3.5 Устойчивость, блокировка

Число Фруда Fr = U / (N·H) (N — частота Брента–Вяйсяля, в конфиге wave.default_stability_n_per_s 0,01 1/с). По Шеппарду воздух ниже высоты Hs = H·(1 − Fr) не переваливает через препятствие, а обтекает его. Пример: U = 5 м/с, N = 0,01, H = 500 м → Fr = 1, почти всё переваливает; при инверсии (N = 0,02) Fr = 0,5 — нижняя половина склона «блокирована», воздух уходит в обход и в понижения.

3.6 Седловины, перевалы, ущелья («труба»)

  • Малый масштаб (сотни метров – км): сохранение массы — где сечение потока сужается, скорость растёт (эффект Вентури в бытовом смысле). Натурные замеры над седловиной: +40–60 % на 10 м AGL к подходу. Поток в седловине может быть сильнее, чем на соседних (более высоких!) вершинах (mountainmeteorology.org).
  • Зависит от устойчивости: при устойчивом воздухе (Fr < 1) низкий воздух не лезет через вершины, а идёт через понижение — «труба» резко усиливается. В неустойчивый день (перемешивание) эффект слабее.
  • Зависит от геометрии: чем глубже седловина относительно соседних вершин и чем уже, тем сильнее; долина, выходящая к перевалу по ветру, работает как воронка.
  • Мезомасштаб (км – десятки км, gap winds): ускорение уже не «Вентури», а гидравлическое — разность давлений холодного воздуха по обе стороны хребта; максимум скорости на выходе из прохода, а не в самом узком месте; сильный ветер на ~100 м над землёй и болтанка/роторы под ним; спокойный ветер на старте не гарантирует спокойную посадку у выхода ущелья (XC Mag).
  • Для пилота это значит: в седловине сильнее встречный (медленнее летишь вперёд), выше подъём на наветренном склоне самой седловины, сильнее болтанка и опускание за ней (ветер быстрее — и подветренная зона злее).

4. Как это делают другие симуляторы и инженеры

ПодходЧто даётЦенаИсточник
Condor 3 (планерный сим, закрытый)подъём зависит от расстояния до склона, ветра, высоты, высоты над долиной, угла ветра к хребту; зона лучшего подъёма уходит вперёд к бровке; ускорение над вершиной; чаши и лощины сводят поток; за гребнем турбулентное опускание; волныформулы не раскрытыcondorsoaring.com
FSX sim_probe (Forster-Lewis; по памяти, та же схема перенесена во FlightGear ridge_lift)5 отсчётов высоты по линии ветра (0, −250, −750, −2000, +100 м), уклоны между ними, нормировка `sin(atan(5·slope^1,7))`, взвешенная сумма × ветер; множитель по высоте: 0–40 м AGL 0,5→1, 40–130 м — 1, выше — экспонента; получается до +6,9 м/с на бровке и −5,4 м/с за гребнем
Орнитологи (подъём для хищных птиц, Brandes & Ombalski 2004)w = v · sin(уклон) · cos(аспект − ветер) — по сути то же, что у нас, на сетке 10 мтривиальноPMC7434798
Строительные нормы, Taylor & Leeускорение над хребтом/сопкой/уступом по формулам §3.1тривиальноASCE 7 §26.8
Линейная теория (Jackson–Hunt, WAsP, MS3DJH)3D-поле возмущения через БПФ рельефа; ускорение над вершинами, обтекание сопок, сход в чашах; без отрываБПФ сетки один раз на направление ветраJackson & Hunt 1975; Hunt 1988
Массосогласованная диагностическая модель (WindNinja, MATHEW)берём начальный ветер, минимально правим его так, чтобы ∇·V = 0 над рельефом; параметр устойчивости α (сколько потоку «разрешено» подниматься вместо обтекания) → при устойчивом воздухе поток идёт в перевалы и долины; сетка ~100 м, 50×50 км, ~10 с на ПК; отрыва и роторов нет3D-сетка, итерационный решательWindNinja docs; Wagenbrenner 2016
CFD (RANS/LES, OpenFOAM, WindNinja momentum solver)отрыв, рециркуляция, всё; LES даже следчасы на направление; объёмные данныеAskervein benchmarks

Наша модель — это «FSX/орнитологи» (V·∇h с затуханием по высоте) плюс более развитая подветренная часть. Condor делает больше: ускорение над вершиной, чаши, зависимость от высоты над долиной.


5. Варианты для нас — от дешёвого к дорогому

Во всех вариантах результат подключается к air_velocity_at одинаково:

u_h   = u · (1 + ΔS(pos))            # ускорение над гребнем / в «трубе»
dir   = ŵ повернутое на δ(pos)        # (для 3D-вариантов) обтекание сбоку, затекание в понижения
w     = w_terrain(pos) · (1 − lee)   # вместо w_ridge
mech, lee_sink — от u БЕЗ ускорения   # §1.6: провалы не трогаем

Новые величины кладутся дополнительными каналами в куски GroundField (_CH 5 → 7–10), чтобы в air_velocity_at оставалось одно билинейное чтение — стоимость вызова почти прежняя. Кеш перестраивается при смене ветра (уже так: set_wind_dir).

A. Улучшенная аналитика (дёшево: 2–4 дня с тестами; рантайм ≈ прежний)

  1. Сглаживание уклона с высотой. Пирамида градиентов (сглаженный рельеф 60 / 120 / 240 / 480 м) в кеше; на высоте agl брать уровень с радиусом ~agl (интерполяция между уровнями). Это дешёвая замена exp(−|k| z) линейной теории: мелкие лощины и опушки перестают давать провалы на 100 м AGL, полоса подъёма становится ровнее. Лечит пункт 4.
  2. Масштаб затухания = ширина склона. По профилю рельефа вдоль ветра (у нас уже есть отсчёты против ветра для линии тени; добавить по ветру) оценивать H и L локального гребня; decay = c · L вместо 250 м. Лечит пункт 2.
  3. Ускорение над гребнем по Taylor–Lee: ΔS = B · min(H/L, 0,5) · (1 − |x|/(1,5L)) · exp(−A·agl/L), B/A для хребта/сопки по поперечной протяжённости. Лечит пункт 3.
  4. «Труба». Признак седловины: по линии поперёк ветра (±0,5…2 км) рельеф с обеих сторон выше точки, а вдоль ветра точка — локальный гребень. Глубина понижения D и ширина W → множитель ветра 1 + k · D/(D + W·c) (калибровать по +40–60 %), с усилением при устойчивом воздухе (Fr из wind и stability_n_per_s пресета). Грубо, но даёт именно то, что описал пилот.
  5. Чаша / отрог: поправка к подъёму по кривизне рельефа поперёк ветра (вогнуто → ×(1+…), выпукло → ×(1−…)).

Плюсы: быстро, ничего нового в архитектуре, всё в GDScript, работает на любых локациях (в том числе загружаемых по координатам). Минусы: набор эвристик, могут конфликтовать; горизонталь и вертикаль не согласованы; в сложном рельефе (несколько гребней подряд) — оценка H/L неоднозначна.

B. Линейная 3D-теория через БПФ (средне: 1–2 недели; расчёт при смене ветра)

Потенциальное (или Jackson–Hunt) возмущение: η̂(k, z) = ĥ(k) · e^(−|k| z), u' = −U ∂η/∂z, v' и w = U ∂η/∂x — из одного БПФ рельефа вокруг пилота (например, 20×20 км на сетке 40–80 м → 256²–512²) на 4–6 высот над землёй. Устойчивость можно добавить так же, как уже сделано в wave_field.gd (там 1D-свёртка вдоль ветра той же природы).

  • Даёт согласованно: подъём (с правильной высотой полосы для узких и широких склонов), ускорение над гребнем, обтекание сопок сбоку, сход в чашах, поворот ветра у рельефа; мелочь рельефа сама гаснет с высотой.
  • «Труба» в седловине — частично (нейтральная теория даёт сход потока в понижение гребня, но не блокировку; блокировку можно добавить через «срезание» рельефа выше Hs = H(1 − Fr) при расчёте — грубый приём).
  • Отрыва нет — подветренная часть остаётся нашей lee, как сейчас.
  • Где считать: БПФ 512² в GDScript — секунды–десятки секунд (в потоке при загрузке/смене ветра — терпимо); лучше — compute shader Godot (RenderingDevice) или маленький GDExtension (C++, FFTW/ pocketfft), тогда доли секунды. Память: 512² × 3 компоненты × 5 высот × float16 ≈ 8 МБ.
  • Проверка: Askervein-подобный холм в тесте (+74–80 % на 10 м), наши 10 стартов.

C. Массосогласованное диагностическое поле (WindNinja-подобное) (дорого: 2–4 недели)

3D-сетка по рельефу (например 10×10 км × 1,5 км, 50 м по горизонтали, 20 уровней ≈ 0,8 млн ячеек), начальное поле = наш профиль ветра, решение уравнения Пуассона на поправку (сопряжённые градиенты/многосеточный) с параметром устойчивости α.

  • Даёт то же, что B, плюс блокировку и «трубу» в перевалах при устойчивом воздухе естественным образом, и корректное сохранение массы в сложном рельефе (несколько гребней, ущелья).
  • Отрыва и роторов нет (WindNinja mass-conserving этого не умеет) — lee остаётся.
  • В GDScript — слишком медленно (минуты); нужен C++ (GDExtension) или compute shader, либо офлайн-инструмент для встроенных локаций (можно гонять сам WindNinja — он открытый, CLI; выход — сетки ветра на заданной высоте над землёй, 3D-поле — через VTK; вертикаль можно восстановить из неразрывности). Для локаций «по координатам» — только если решатель есть в рантайме.
  • Хранение: поле на направление ветра (8–16 направлений) × несколько высот; для 20×20 км вокруг стартов — десятки МБ на локацию.

D. Офлайн-CFD (RANS/LES) для встроенных локаций (очень дорого: 1–2 месяца на инструментарий)

OpenFOAM или момент-решатель WindNinja, 8–16 направлений на локацию, часы счёта на каждое, нейтральный поток масштабируется по скорости (одно решение на направление).

  • Даёт отрыв, реальные роторы, пузыри у обрывов, трубы.
  • Минусы: только для 4 встроенных локаций (для «по координатам» не работает), сотни МБ данных, сложная цепочка, и даже CFD плохо даёт турбулентность в следе (Askervein) — то есть самое «ощущаемое» (провалы) всё равно останется эмпирикой. Сломать текущие хорошие провалы тут проще всего.

Сводка

Высота полосыУскорение над бровкойМелочь рельефаСопка/чаша«Труба»ОтрывТрудоёмкостьРантайм
Сейчасфикс. 250 мнетбьёт на высотенетнаоборот (слабее)линия тени 12°——
A≈ по Lпо формулесглаженаэвристикаэвристикакак сейчасдни≈ 0
Bдадададачастичнокак сейчас1–2 нед.секунды при смене ветра
Cдадададада (с α)как сейчас2–4 нед. + C++секунды (C++)
Dдадададададамесяцыданные с диска

6. Рекомендация

  1. Сначала — вопросы пилоту (§7): «не физично» может означать очень разное (полоса слишком высокая/низкая, рваная, нет ветра на бровке, нет трубы). Ответы решают, какие пункты A нужны.
  2. Затем вариант A, пункты 1–4 (сглаживание по высоте, масштаб = ширина склона, ускорение над гребнем, «труба» в седловине) — это закрывает почти все найденные расхождения, работает на любых локациях и не трогает провалы. Горизонтальное ускорение подключить к полёту, но не к болтанке и не к подветренному опусканию. Прогнать test_ridge_starts (набор на слабых стартах может измениться — в обе стороны), test_lee_rotor, новый тест «Askervein» и тест седловины. Не шлифовать — дать пилоту слетать.
  3. Вариант B — только если после A пилот всё ещё говорит «воздух течёт не так» про обтекание сопок/чаш и повороты ветра у рельефа. Это самое «физичное за свои деньги» и ложится рядом с уже существующей линейной волновой моделью.
  4. C и D — не рекомендую сейчас: дорого, D не работает на локациях по координатам, а подветренная сторона (где «провалы» и главное ощущение) у них не лучше нашей эмпирики.

7. Вопросы пилоту — простыми словами

  1. Когда летаешь вдоль склона — подъём слишком сильный, слишком слабый или нормальный по силе?
  2. Подъём слишком близко к склону (приходится жаться) или слишком далеко (держит и в стороне от горы)?
  3. Насколько высоко над бровкой держит? В жизни на таком склоне выше какой высоты подъём заканчивается — примерно столько, сколько высота самого склона, меньше, больше?
  4. Когда подходишь к бровке и выше — чувствуется ли, что наверху дует сильнее (стоишь на месте, трудно пройти вперёд)? В игре этого не хватает?
  5. Подъём вдоль склона слишком ровный (одинаковый везде) или наоборот рваный — через каждые сто метров провал? Как в жизни?
  6. Над выступом горы («носом») и над впадиной («цирком», «чашей») разница есть? В жизни где лучше?
  7. Если ветер дует наискось к склону — в игре подъём пропадает слишком рано, слишком поздно или как в жизни?
  8. Седловина / перевал («труба»): насколько там в жизни сильнее ветер — чуть-чуть, в полтора раза, в два? Всегда или в какие дни (утро, вечер, прохладный спокойный день)? Где сильнее всего — в самой низкой точке, перед ней или на выходе за ней? Там болтает сильнее?
  9. Когда перелетаешь за гребень — провал и болтанка сейчас правильные? (Провалы и «ногами к тросам» — хорошо, это не трогаем.) Не слишком ли рано/поздно начинается опускание, если лететь над самой бровкой?
  10. Ветер у земли на старте и на посадке — похож на жизнь? На бровке у старта в жизни ветер сильнее, чем внизу у подножия?
  11. Есть ли конкретное место в игре (старт, склон), где «не физично» заметнее всего? Если можно — при каком ветре.

Источники

8. Ответ пилота (29.09.2026) и что из него следует для модели

Слова пилота:

При обтекании склона чем выше по склону и ближе к нему, тем ветер больше усиливается. Это усиление распространяется на 2 высоты горы от подножия. На 2,5 высоты горы уже ламинарное обтекание. Зависит также от крутизны склона — чем круче, тем больше прирастание силы ветра. В седловине ветер сильнее примерно в 1,5 раза, всегда вдоль седла, даже при косом до 15 градусов. Даже может сдуть за гору. При косом ветре другое обтекание, эффект меньше.

Что это значит для варианта A (формулы):

  • Разгон ветра над склоном растёт вверх по склону и ближе к поверхности — максимум у бровки, у самой земли (сходится с Jackson–Hunt / Taylor–Lee). Сейчас разгона нет вовсе (§ «главное»: п. 3).
  • Толщина слоя возмущения ≈ 2 H от подножия (H — высота горы над подножием), на 2,5 H — невозмущённый поток. Значит, затухание подъёма и разгона должно масштабироваться высотой горы, а не фиксированными 250 м: множитель вида f(z_над_подножием / H), ≈ 1 у земли → 0 к 2–2,5 H. Нужна оценка H и «подножия» по рельефу против ветра (минимум высоты на 1–3 км против ветра).
  • Круче — сильнее разгон: ΔS ∝ H/L (крутизна) — совпадает с Taylor–Lee; ограничить сверху (срыв потока на очень крутых склонах, > ~17–20°, уходит в ротор).
  • Седловина: ветер ×1,5 относительно окружающего, направлен вдоль оси седловины (поток «затягивается» в неё) при отклонении ветра до ~15° от оси; течёт через седловину дальше и за гору («может сдуть за гору») — в подветренной стороне за седловиной сильный поток, а не тень. Нужна эвристика: найти седловину (понижение гребня между вершинами поперёк ветра), разгон ×1,5 и поворот вектора к оси седла, затухание с высотой по тому же 2 H.
  • Косой ветер: эффект меньше — множитель от угла между ветром и нормалью к склону (порядка cos угла, для седловины — «ловит» только до ~15°).
  • Провалы и болтанку не трогать (§ ранее: пилот доволен провалами).

Для нейросетевого варианта (обсуждение 29.09): эти слова пилота — проверочные условия для обученной модели (толщина слоя ≈ 2 H, седловина ×1,5, ослабление при косом ветре).

9. Поле ветра, согласованное по массе, на Rust (обсуждение 29.09.2026)

Альтернатива варианту A: при загрузке места один раз посчитать трёхмерное поле ветра (u, v, w) на сетке так, чтобы воздух не проходил сквозь землю и нигде не накапливался (массово-согласованная диагностическая модель, как WindNinja в простом режиме). В полёте — только трилинейная интерполяция в air_velocity_at.

Что даёт и чего не даёт

  • Само, без отдельных правил: разгон над бровкой, «труба» в седловине (поток идёт в понижение и разгоняется, при косом ветре затягивается вдоль седла), трёхмерное обтекание (сопка ≠ отрог ≠ чаша), ослабление при косом ветре. Толщина возмущённого слоя (~2 высоты горы, слова пилота) — через верхнее граничное условие и сетку по высоте.
  • Устойчивость воздуха — одно число: отношение «цены» вертикального и горизонтального смещения. Устойчивый воздух обтекает гору сбоку и уходит в седловины, неустойчивый переваливает сверху.
  • Не даёт: срыва потока и роторов за крутыми гребнями (модель без инерции) — нынешняя эвристика подветренной стороны остаётся поверх; термики, кольца опускания и болтанка — как сейчас (пилот доволен провалами, §8).

Математика

  1. Начальное поле u₀ — ветер с профилем по высоте (как сейчас).
  2. Поправка u = u₀ + W·∇λ, λ — скалярное поле (по числу на ячейку), W — веса (горизонталь «дешёвая», вертикаль «дорогая» — устойчивость).
  3. Условие ∇·u = 0 → уравнение Пуассона ∇·(W∇λ) = −∇·u₀; у земли — непротекание, на открытых границах λ = 0.
  4. Дискретно — система A·λ = b: ~10⁶ уравнений (сетка ~100 м × 20–30 уровней на 20×20 км), в каждом 7 неизвестных (ячейка и 6 соседей). A разреженная, симметричная, положительно определённая.
  5. Решение — итерационное «без матрицы» (A не хранится, A·x — проход по сетке): сопряжённые градиенты с предобуславливателем (~100–300 итераций) или многосеточный метод (5–10 V-циклов). Прямые методы не годятся: плотная матрица 10⁶×10⁶ не помещается, разреженный Холецкий на 3D-сетке даёт гигабайты заполнения.
  6. Рельеф — сетка по рельефу (σ-координаты: нижний слой на земле, верхние растянуты до потолка) — склон гладкий; маска «под землёй» даёт лесенку.

Главный приём: линейность по ветру (ранг 2)

При фиксированной устойчивости λ линейно зависит от u₀, u₀ — от ветра, поэтому поле для любого направления θ — F(θ) = cos θ · Fₓ + sin θ · F_y (базовые решения для ветра на восток и на север), скорость — множитель. Итог:

  • на место — 2 решения на уровень устойчивости; смена ветра в полёте мгновенная, без пересчёта;
  • устойчивость нелинейна (веса внутри уравнения) — 3–4 уровня и интерполяция; набор полей по устойчивости сжимается SVD до 2–3 главных компонент;
  • для встроенных мест базовые поля можно посчитать заранее и положить в данные игры — одинаковы у всех игроков (сетевая игра) и не стоят ничего при загрузке. Low-rank разложение самой матрицы A не помогает (лапласиан полного ранга; H-матрицы для обратной сложны и не быстрее многосеточного метода).

Реализация: Rust + GDExtension

  • Крейт godot (godot-rust / gdext): cdylib с классом (#[derive(GodotClass)], #[godot_api]), в проекте — файл .gdextension со списком библиотек (linux.x86_64 = res://native/bin/libwindfield.so, windows.x86_64 = …/windfield.dll); из GDScript класс виден как встроенный.
  • Данные — PackedFloat32Array на вход (высоты, параметры) и на выход (поле); внутри — rayon по слоям/кускам. API Godot из рабочих потоков не трогать. Долгий расчёт — из WorkerThreadPool (или фоном внутри Rust), окно не замирает.
  • Сборка: Linux — cargo build --release; Windows с Linux — x86_64-pc-windows-gnu (mingw) или cargo-xwin; шаг в tools/build.sh перед экспортом, экспорт кладёт .so/.dll рядом с игрой. Версию крейта godot подобрать под API 4.7.
  • Детерминизм (сетевая игра): скалярные произведения суммировать с фиксированным разбиением на куски и фиксированным порядком сложения — результат одинаков при любом числе потоков и на любой машине. Иначе — поле считает создатель зоны и рассылает (несколько МБ).
  • Объём счёта: ~10⁶ ячеек × 7 × сотни итераций ≈ 10⁹ простых операций — доли секунды на всех ядрах; 6–8 решений на место — секунды.
  • Своё проще готовых крейтов (sprs, faer): проход по сетке + сопряжённые градиенты — пара сотен строк.

GPU

  • CUDA — нет: только NVIDIA, лишние библиотеки и сборка под Windows, хуже детерминизм.
  • Если понадобится GPU — вычислительный шейдер Godot (Vulkan; так уже считаются облака) или wgpu в Rust — работают на любой видеокарте.
  • Для расчёта раз на место (и ранг-2 приёма) CPU хватает с запасом.

Нейросеть (обсуждение)

Локальная модель «точка → ветер»: вход — высоты вокруг точки в осях ветра (сетка 7×7 в трёх масштабах 50/200/800 м относительно земли под точкой, ~147 чисел), высота над землёй, число Фруда U/(N·H) (устойчивость); выход — безразмерные u/U, v/U, w/U (+ σ/U болтанки). MLP 150→64→64→4 (~14 тыс. весов) правдоподобно достаточно (линейная теория обтекания почти линейна по рельефу). Данные — решатель (WindNinja или решатель из этого раздела) на тысячах кусков DEM. В игре — прогон по сетке при загрузке (шейдер), дальше интерполяция. Нужна только для произвольных точек с карты; для встроенных мест проще готовые поля.

Сравнение и порядок

ОбъёмЧто даёт
A. Формулы (§8, TODO)3–5 днейразгон, толщина слоя 2H, крутизна, косой ветер; седловина — отдельной эвристикой
B’. Поле по массе на Rust1–2 неделивсё из A «само» + трёхмерное обтекание, устойчивость; готовые поля для встроенных мест
Нейросеть2–4 неделипроизвольные точки с карты без решателя в игре
Если браться за B’, вариант A можно пропустить.

10. Согласование с термиками/облаками и сетевой игрой (обсуждение 29.09.2026)

Проверено чтением кода (без правок), два вопроса: (1) как вариант A/B’ согласуется с моделью термиков и облаков; (2) как расчёт поля на сетке (§9) согласуется с моделью погоды и сетевой игрой.

10.1 Термики и облака ветер air_velocity_at не видят

Термики (thermal_field.gd) и облака (та же система, «шапки» термиков) не используют Atmosphere.air_velocity_at(pos) — они напрямую читают WindModel:

  • update_wind_frame() (thermal_field.gd:159-179) строит сетку «улиц» термиков из глобального wind.dir — один азимут на весь мир, без позиции;
  • снос/наклон термика — _apply_wind() через wind.vec2_at() (thermal_field.gd:208-227);
  • облачные «улицы» — cloud_layer.gd:440, тоже из atmo.wind.dir напрямую.

Следствие: локальный разгон и поворот ветра у гребня/в седловине, которые варианты A/B’ добавляют внутри air_velocity_at, никак не подействуют на рождение/снос термиков и ориентацию облаков — они продолжат жить по старому, невозмущённому направлению. Термик визуально может «висеть» не там, куда на самом деле дует воздух в его точке (особенно в седловине, где ось «трубы» способна сильно отличаться от общего wind.dir). Пилот об этом не жаловался (термики/провалы её устраивают) — можно сознательно не трогать термики/облака в первом заходе, это отдельная работа уровня варианта B (нужно тащить позицию в update_wind_frame, сейчас глобальную по конструкции).

10.2 mean_wind_at vs air_velocity_at — ложная турбулентность

Порывистость/болтанка во многих местах считается как разница air_velocity_at(pos) − mean_wind_at(pos) (terrain_wind.gd; тесты test_lee_rotor.gd:98, test_atmosphere.gd:252-253, test_cloud_suck.gd:91-92), где mean_wind_at — глобальный ветер без поправок рельефа. Если добавить систематический разгон/поворот только в air_velocity_at, эта разница перестанет быть чистой турбулентностью на склонах: появится ложная составляющая (трава задёргается сильнее обычного именно у гребней), и test_lee_rotor.gd:98 («над тенью ветер ≈ mean_wind») может начать падать за пределами тени.

Вывод: mean_wind_at нужно обновлять той же формулой ΔS(pos)/поворота, что и air_velocity_at (без шума/lee) — тогда разница снова станет чистой турбулентностью. Дёшево (одна и та же функция переиспользуется), но требует пересчитать допуски в перечисленных тестах и в test_ridge_starts.gd (там же меняется decay 250→f(H)).

10.3 Ветер и устойчивость на самом деле константы на полёт — часть §9 упрощается

  • Направление/скорость ветра берутся из прогноза один раз при Atmosphere.set_weather() и не меняются в течение полёта — все мягкие дневные обновления (_apply_day, atmosphere.gd:209-261) явно сохраняют wind_speed_kmh/wind_from_deg. Значит ранг-2 разложение по θ из §9 не нужно ради динамики одного полёта (направление известно и фиксировано на старте) — оно полезно только чтобы не пересчитывать поле заново при заходе на разные старты/локации.
  • Устойчивость stability_n_per_s формально пересчитывается по часу внутри WeatherModel.derive (weather_model.gd:167), но фактически замораживается при set_weather() — WaveField.setup() вызывается только оттуда, не из мягких обновлений. На один полёт нужно одно N, без интерполяции между уровнями устойчивости.
  • Итог: решатель на сетку/полёт можно считать один раз при старте зоны для одного известного направления и одного N — заметно проще, чем «несколько направлений × несколько уровней N», предложенные в §9 с запасом на будущее.
  • Прецедент архитектуры уже есть: GroundField и WaveField считаются лениво, кусками, в памяти, без bake на диск при билде (ground_field.gd, wave_field.gd) — новое поле стоит делать тем же паттерном, а не монолитным расчётом по всей округе при входе в локацию.

10.4 Сетевой детерминизм — GPU-шейдер на самом деле не проблема

Первая гипотеза (детерминизм между клиентами обязателен, значит нужен CPU с фиксированным порядком суммирования, GPU рискован) — не подтвердилась при проверке кода. Собственная физика полёта в сетевой модели клиент-авторитетна: NetPilots.set_local_state()/set_local_provider() (net_pilots.gd:83-89) — каждый клиент считает свой полёт локально и рассылает готовое состояние (позиция/скорость/ротация), никто его не пересчитывает. Значит, если разные видеокарты дадут чуть разное поле ветра — это повлияет только на ощущение полёта у самого пилота, других игроков это не касается вообще (они видят только реплицированную позицию, сравнивать не с чем).

Боты — тоже не проблема: считаются только у ведущего зоны и рассылаются всем на ~10 Гц (send_bot_state, net_pilots.gd:98-121, комментарий NET-44), а не пересчитываются независимо каждым клиентом. Единственный тонкий момент — смена ведущего (лидер вышел, подхватывает следующий): новый ведущий посчитает поле на своей видеокарте, возможен небольшой разовый блип в поведении бота в момент хендовера, но не постоянное расхождение.

Термики/облака, которые обязаны совпадать у всех (это и есть «тот же мир», ключ — WorldKey, world_key.gd), как показано в §10.1, вообще не читают это поле — идут из WindModel напрямую.

Вывод: требование «детерминизм ради сети» из §9 (раздел «Детерминизм (сетевая игра)») для конкретно поля ветра излишне строгое — жёсткий фиксированный порядок суммирования на CPU нужен не для сетевой синхронизации, а самое большее — для одинакового ощущения полёта на разных машинах одного и того же человека (не требование продукта) или для воспроизводимости тестов. Вычислительный шейдер Godot (Vulkan, как уже считаются облака) можно использовать без опасений по сети — упрощает выбор в §9/GPU в пользу шейдера, а не обязательно Rust+CPU.

11. Связанная система: ветер, термики, погода (обсуждение 29.09.2026)

Вопрос пользователя: насколько поле ветра на сетке (план docs/archive/plan/wind-field.md) связывает модели погоды, ветра и термиков — хочется одной связанной системы, а не отдельных моделей. Разбор по коду, без правок.

11.1 Кто сейчас читает ветер и что даст поле

ПотребительСейчас (код)С полем
Полёт, боты, птицы (подъём), дым костра, флюгерair_velocity_at (glider.gd, bot_pilots.gd:149, bird_flock.gd:255, campfire.gd через update_wind)сами получают поле — без правок
Трава, деревьяmean_wind_at под камерой (terrain_wind.gd)сразу; по всей земле — WF-10
Снос и наклон термика (и его облака)wind.vec2_at(span·0,5) / vec2_at(span) — глобальный (thermal_field.gd:208-227, :635)средний по столбцу ветер поля в точке источника на момент рождения → константа на термик: снос остаётся чистой функцией t, start_at работает
Сетка клеток источников, «улицы»ось _ax = wind.dir на весь мир (update_wind_frame, thermal_field.gd:164; cloud_layer.gd:440)не трогать: это дискретная сетка (номер клетки → сид термика); локальная ось = другие термики у разных клиентов
Выбор источника (лучшая из точек клетки)_source_at = солнце × покров (thermal_field.gd:~490)можно добавить множитель «схождения/обтекания» поля (гребни, наветренные склоны) — но выбор дискретный (argmax), разница полей между GPU может перебросить термик на сотни метров
Тень облаков на источникахокно по _ax и wind.vec2_at (:594, :635)как у сноса (константа на термик)
Облачный шум, «пыльные вихри»cloud_layer.gd:264, dust_devils.gd:119 — vec2_atполе в точке — только визуал, безопасно
Подветренная зоналиния тени по глобальному ветру (ground_field.gd)остаётся; поле добавляет свой нисходящий поток на склоне (× (1 − lee), как было с w_ridge)
Волнылинейная теория вдоль wind.dir (wave_field.gd)остаётся; позже — направление поля над гребнем
Кольца опускания, фон, подсос облакаGedeon-профиль термика, _bg_sink, cloud_physics.gdне от ветра — поле их не касается

11.2 Чего поле не даст

Поле согласовано по массе, без энергии и инерции: нет плавучести — значит, нет самих термиков, их силы, отрыва пузырей, облаков; нет срыва потока и роторов; нет суточного хода. Термики остаются отдельной моделью (источники по солнцу и покрову). Резкие «провалы» — тоже не отсюда: отклик несжимаемого поля на источник массы — широкое слабое опускание ~1/r², а не узкое кольцо Gedeon; кольца, фон и рывки за гребнем остаются эмпирикой.

11.3 Связанная система по этапам

  1. Поле — ветер для всего (≈ 2–3 дня, это WF-13): снос/наклон термиков, тени, облачный шум, пыльные вихри — из грубого поля 200 м (средний по столбцу 0…кромка ветер у источника в момент рождения). Сетка клеток и «улицы» — на глобальном ветре. Итог: термик в седловине уходит вдоль седла, облако — туда же, куда дует у пилота.
  2. Термики — источники в том же уравнении (≈ 1–2 недели): вертикальный поток массы термиков как правая часть Пуассона → приток у земли к термику (трава/пыль «тянутся» к нему, схождение) и компенсирующее опускание вокруг. Цена: термики меняются каждую секунду → решение в окне вокруг пилота раз в 1–2 с (МС-PCG ~десятки мс на 4070 SUPER, на слабой AMD — сомнительно) и постоянное чтение с GPU. Кольца Gedeon оставить (иначе пропадут «провалы»); добавка поля — только горизонтальный приток. Отдача мала относительно цены.
  3. Устойчивость из погодной модели (≈ 2–3 дня): N по часу (WeatherModel.derive, stability_n_per_s) → вес T, высота инверсии/кромки → «крышка» сетки (устойчивый слой запирает поток — «труба» в перевалах сильнее утром/вечером). Выбор уровня T бесплатный (ранг-2 × уровни); крышка на другой высоте = пересчёт (этап загрузки или фон). Нужно, чтобы мягкие обновления дня перестали «замораживать» N (§10.3).

11.4 Сеть и детерминизм

  • Поле у клиентов расходится ≲ 0,01 м/с. Непрерывное использование (снос термика) → за 20 мин жизни термика расхождение ~10 м при радиусе ~100 м — пилоты не заметят, но test_determinism (допуск 1e-3) не пройдёт → отпечаток мира считать на аналитике (поле off), а в сети проверять отдельный допуск (позиции термиков ≤ 20 м). Можно огрубить снос (направление 2°, скорость 0,1 м/с) — уменьшает, но не убирает расхождение у границы округления.
  • Дискретные решения (клетки, «улицы», выбор источника, рождение) от поля не брать — там малая разница даёт другой термик. Ключ мира не меняется.
  • Этап 2 в сети: окно и термики у каждого своё — горизонтальный приток у всех чуть разный, допустимо (клиент-авторитетная физика, §10.4).

11.5 Риски для «провалов»

Этап 1 провалы не трогает (кольца, фон, lee — прежние). Этап 2 опасен: второе опускание вокруг термика поверх Gedeon — провалы станут «вязкими»/сильнее; только горизонтальный приток. Этап 3 меняет силу «трубы» и подъёма по часу — провалы не трогает, но меняет набор на стартах утром.

11.6 Рекомендация

В план поля включить этап 1 (WF-13) и лёгкую часть этапа 3 — T по N из погоды на момент старта (уже почти так в плане, бесплатно). Крышку по инверсии и этап 2 — «на потом», после полёта пилота на поле.