Files
fparkan/docs/reference/atmosphere.md
T
Valentin Popov a8faba8aad feat: complete map viewer scene and static CTL pose preview
Complete the interactive mission viewer with environment rendering, audio events, dynamic shadows, free-flight camera controls, and per-component CTL pose sampling.
2026-10-11 13:12:50 +04:00

237 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Атмосфера и небо
Атмосфера Parkan задаётся двумя связанными ресурсами: `sky.ske` хранит
расписание, цвета и параметры, а `sky.wea` сопоставляет позиционные строки с
материалами. Ресурсы сначала разбираются в `TypedAtmosphere` и `SkyMaterials`,
затем на каждом кадре из расписания получается `AtmosphereFrame`. Модуль
`fparkan-fx::sky` превращает этот кадр в данные для рендерера: постоянную
геометрию купола, динамические цвета вершин, небесные слои, положение
светил, туман и нижнюю границу освещения.
## Игровые сутки и смена дня и ночи
Вызов `TypedAtmosphere::sample(real_seconds)` принимает секунды от начала
повторяющегося расписания. Дорожки идут в порядке файла и занимают свои
реальные длительности; после последней дорожки время возвращается к началу.
Внутри выбранной дорожки игровой день всегда проходит от 00:00 до 24:00:
```text
track_seconds = real_seconds_in_track
day_seconds = track_seconds / track_duration * 86400
```
Время ключа вычисляется по часам и минутам даты. Нативный конструктор
игнорирует сохранённое поле секунд, поэтому для планировщика используется:
```text
duration = (end_hour * 60 + end_minute) * 60
key_time = floor(duration * (key_hour * 60 + key_minute) * 60 / 86400)
```
Сэмплер ищет предыдущий и следующий ключ во всём расписании. Переход через
конец дорожки или конец цикла интерполируется так же, как переход внутри
дорожки. `SkeDate::seconds_of_day()` сохраняет все три компонента даты для
инструментов; оно не меняет формулу реального времени расписания.
День и ночь в самом формате не являются отдельным флагом. Их вид получается
из интерполированных цветовых параметров купола и состояния объектов солнца и
луны. Ключи с kind `0` и `1` запускают и останавливают небесное светило. Для
активного интервала модуль восстанавливает рождение объекта: ищет ближайший
предыдущий ключ `SunStart`, следующий соответствующий `SunStop`, вычисляет
реальное время начала и длительность, включая переход через цикл, и сохраняет
эти значения в `SunBirth`.
Имя в первой фиксированной строке определяет объект. Для строки `sun`
нативный код использует углы `[90, 30]` градусов и строку материала 3; для
`moon` — `[0, 50]` и строку 4. Начальный вектор орбиты строится по формуле
`[cos(theta), 0, -sin(theta)]`, где
```text
theta = (clamp((now - birth_start) / lifetime, 0, 1) * 1.2 - 0.1) * pi
```
Затем к нему применяется матрица рождения. У солнца и луны независимые
интервалы и независимые направления: при отсутствии активной луны нельзя
подменять её направление противоположностью солнца. `SunFrame::color` — это
единственный primary directional RGB. Второй источник получает направление
`-SunFrame::direction` и RGB из `SunSample::packed[1]`; оба источника для
активного объекта находятся в `SkyFrame::directional_lights` (сначала солнце,
затем луна). Экранная геометрия использует `-SunFrame::direction`, а
`world_center` размещает её по нативной формуле относительно camera
translation и половины far plane. Материал, строка и состояние объекта
берутся из его `SunBirth` и таблицы `sky.wea`.
## Купол
Оригинальный `CSky` строит конечный купол, а не кубическую skybox. Значения
конструктора, подтверждённые в Terrain, такие: высота `10000`,
`theta_max = pi / 4`, вертикальный масштаб `1`, `16` азимутальных секторов и
`5` колец. Радиус вычисляется как
```text
radius = height * 0.5 / sin(theta_max * 0.5)^2
```
Вершина с индексом 0 находится в зените `[0, 0, height * z_scale]`.
Остальные вершины идут в порядке `azimuth`, затем `ring`; для них
```text
phi = azimuth / azimuth_count * 2*pi
theta = (ring + 1) / rings * theta_max
x = radius * sin(theta) * sin(phi)
y = radius * sin(theta) * cos(phi)
z = (radius * cos(theta) + height - radius) * z_scale
```
На сектор приходится один треугольник веера и по два треугольника на каждую
пару соседних колец. Текстурные координаты трёх исходных стадий используют
масштабы `[1, 15, 3]` для `position.x / radius` и `position.y / radius`.
Цвет зенита и первого пояса берётся из palette 12. Следующие пояса используют
группы palette 8..11, 4..7 и 0..3 с интерполяцией по азимуту.
Позиции, signed normal bytes, UV и индексы создаются один раз через
`SkyGeometryConfig::mesh`/`SkySystem::new`. Последующие кадры меняют только
цвета вершин методом `SkyMesh::update_colors`; индексы и topology не
пересоздаются. Ошибка конфигурации возвращается как `SkyGeometryError`, а
ошибка входного кадра — как `SkyUpdateError`; они не превращаются в пустой
буфер. Рендерер может загрузить `SkySystem::mesh()` в статический
vertex/index buffer и обновлять только небольшой цветовой диапазон.
Нормали купола повторяют native `Terrain47BFF` contract: каждый компонент
хранится как signed `i8`, округляется режимом nearest-even и умножается на
`127.0`. `SkyVertex::normal_vector()` декодирует эти bytes делением на `127`;
зенит имеет `[0, 0, 127]`. Вершинный backend должен передавать signed byte
representation без трактовки его как unsigned `0..255`.
## Облака, звёзды и спрайты
`sky.wea` — позиционная таблица. В стандартном AutoDemo строки имеют роли:
| Строка | Роль | Имя в AutoDemo |
|---:|---|---|
| 0 | фон/туманность | `ENV_NEBULA_0` |
| 1 | звёзды | `ENV_STARS` |
| 2 | облака | `ENV_CLOUDS` |
| 3 | солнце | `ENV_SUN_3` |
| 4 | луна | `ENV_MOON` |
| 5 | первый flare | `ENV_FLARE_00` |
| 6 | второй flare | `ENV_FLARE_01` |
| 7 | снежинка | `SNOWFLAKE` |
| 8 | капля дождя | `RAIN_DROP` |
Это наблюдаемые строки, а не глобальные имена, на которые можно полагаться в
миссии. `SkyMaterials::parse` сохраняет числовой id и имя каждой строки;
миссия может заменить любой материал. `SkyLayerKind` и `SkyLayerFrame` дают
роль, позиционную строку, активность, intensity, UV stage и фазу времени.
Нативный порядок купола находится в `SkyFrame::passes`: экранный gradient,
nebula row 0 со stars row 1 как второй texture (material mode 4), dome
gradient и clouds row 2. Для nebula/stars/clouds используются UV scales
`1/15/3`; clouds получают translation Z `-5000` относительно камеры и
directional RGB из `sky.packed[0]`. Это четыре фиксированных pass, а не три
независимых цветных слоя.
Цвет первого screen-gradient и последнего пояса вычисляет
`SkyFrame::screen_gradient(yaw_radians, glare_rgb_delta)`. Native `CSky` сначала
округляет `(yaw + pi) * 180 / pi` режимом nearest-even, выбирает сектор
`(floor(degrees / 90) - 2) mod 4`, а затем смешивает соседние цвета
`SkySample::colors[0..4]`. Остаток сектора умножается на точный коэффициент
из PE `65F64` (`0x3C360B61`, примерно `0.01113567`), alpha результата равна
`255`. `glare_rgb_delta` — входной RGB delta от light manager: для каждого
канала это `max(current_primary_light_rgb - base_sun_rgb, 0)`. Каждый компонент
затем преобразуется так: `d <= 1 -> d*0.8`, `1 < d <= 3 -> d*0.1 + 0.7`,
`d > 3 -> 1`.
`SkyGradientFrame::horizon_floor` задаёт RGB minimum для dome palette, а
`clamp_dome_color` применяет его, сохраняя alpha исходного цвета.
Солнце, луна и две строки flare являются направленными экранными слоями.
Нативный quad использует UV
`[[.005,.005],[.005,.995],[.995,.995],[.995,.005]]`. Полуразмеры sun/moon в
пикселях вычисляет `sprite_half_size_pixels`: `viewport_width / horizontal_fov
* .325 * .5 * values[0/1]`, где `values` — первые два tail float SKE.
Цвет sprite и его alpha берутся из `SunSample::packed[0]`, отдельно от
directional `SunFrame::color`.
`SkyFrame::sun_optics` принимает projected sun, viewport, camera forward и
результат world-ray visibility. Он применяет native 15-degree glare cone и
возвращает фиксированные 12 `FlareQuad`; flare slots активны только при
видимом солнце, `length(primary RGB) > 1.1` и cone amount `<= .1`. При cone
`> .1` остаётся только glare boost. Renderer отвечает за сам projection и
occlusion query.
Дождь, снег и молния используют те же состояния расписания, но их camera-local
геометрия и звуки обновляются `EnvironmentSystem`. Для активного интервала
ресурсы берутся только из ключа, который его запустил. Интенсивность дождя,
снега и молнии — четвёртый trailing float; цвет активной погоды использует
RGB palette 12 с минимумом `80/255` на канал и alpha `150/255`.
## Туман и свет
Нативное обновление `CSky` записывает две дальности и один packed RGB.
`FogFrame` повторяет этот контракт:
```text
fog.start = sky.values[0] * 700
fog.end = sky.values[1] * 700
fog.color = RGB(sky.packed[1]) / 255
```
Поля не скрывают порядок исходных параметров: значения сохраняются в
`SkySample`, а преобразование выполняется только в `fog_frame`. Vulkan path
передаёт цвет и дальности в frame uniforms; vertex shader вычисляет линейный
коэффициент по расстоянию до камеры, а fragment shader смешивает RGB материала
с fog color в ветвях combiner, которые используют туман. Конкретный draw state
может обходить это смешивание.
Цвет primary света солнца — RGB palette 12, нормированный делением на 255 и
умноженный на trailing parameter 2. `SunFrame::direction` — нормированный
вектор после матрицы рождения и направление primary light; sprite/world
geometry использует его противоположность. Второй directional RGB берётся из
packed-параметра 1 без дополнительного умножения intensity. Когда объект
неактивен, sampled поля сохраняются, но оба light slot и sprite inactive.
В native `CShade` sampled RGB из состояния атмосферы также устанавливает
глобальную нижнюю границу освещения. Для каждого RGB-канала она объединяется
операцией `max` с накопленным цветом материала. Поэтому `SkyFrame` отдаёт
`lighting_floor` из того же sampled packed RGB, а владелец renderer передаёт
его в глобальный light state. Фиксированный ambient-цвет в этом месте меняет
ночной уровень и не соответствует цепочке Terrain → CShade → Ngi32.
## Бинарный формат `sky.ske`
Файл little-endian, версия 5. В начале находятся `i32 marker = -1`,
`u32 version = 5` и число дорожек. Дорожка содержит `u32 version = 1`, число
ключей, две даты по 32 байта и ключи. Дата состоит из восьми `u32`; для
планировщика значимы слова 3, 4 и 5 (часы, минуты, секунды), а остальные слова
сохраняются без интерпретации.
Ключ имеет `u32 version = 3`, дату, raw kind, opaque word, четыре packed ARGB
цвета, два `f32`, пятнадцать packed ARGB цветов, шесть length-prefixed строк,
четыре trailing `f32` и список length-prefixed ссылок. Строка хранится как
`u32 byte_length` и ровно столько байт без завершающего NUL. Неизвестный kind
сохраняется как `AtmosphereKind::Unknown`, как и opaque слова/строки.
В конце находятся дата и два opaque `u32`. Декодер ограничивает количества и
размеры строк, отклоняет NaN/Infinity во float-полях и требует точного конца
файла. Трейлер выбирает исходную дорожку и позицию час/минута; метод
`initial_offset_seconds()` складывает длительности предыдущих дорожек и
применяет ту же формулу floor.
Из интерполированного ключа `SkySample` формируется в исходном порядке:
```text
colors = [header[1], header[2], header[0], header[3],
palette[0], palette[3], palette[1], palette[2],
palette[4], palette[7], palette[5], palette[6], palette[8]]
values = header_values
packed = [palette[11], palette[13]]
```
Солнце и луна используют palette 12 и trailing parameter 2 для RGB,
parameters 0 и 1 сохраняются как raw values, а packed-параметры светила —
palette 10 и 14. Kind `0/1` управляет солнцем и луной по имени, `3/4` —
дождём, `5/6` — снегом, `8/9` — молнией; kind `2` и `7` меняют
интерполируемое состояние. Поля, для которых в native renderer ещё нет
проверенного назначения, остаются raw в Rust API и не получают придуманных
значений по умолчанию.