# Атмосфера и небо Атмосфера 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 ``` Затем к нему применяется матрица рождения как к вектору-столбцу. Если `A` и `B` — углы рождения, а `x = cos(theta)` и `z = -sin(theta)`, результат равен ```text [x*cos(A) + z*sin(A)*sin(B), x*sin(A) - z*cos(A)*sin(B), z*cos(B)] ``` Порядок важен для всего освещения: транспонированное умножение переносит солнце на другую сторону мира и меняет освещённые склоны, блики и тени. В GOG `Terrain.dll` (SHA-256 `af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead`) матрицу строит RVA `0x4A550`, а вектор преобразует `CSun::Update`, RVA `0x4B570`. Живой замер после 282184 мс при длительности 525000 мс и углах `[90, 30]` дал направление `[-0.4950135, -0.1408745, -0.8573891]`; Rust-тест проверяет этот результат с допуском `3e-6`. У солнца и луны независимые интервалы и независимые направления: при отсутствии активной луны нельзя подменять её направление противоположностью солнца. `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` записывает две дальности и нижнюю границу освещения. `FogFrame` повторяет этот контракт: ```text fog.start = sky.values[0] * 700 fog.end = sky.values[1] * 700 lighting_floor = RGB(sky.packed[1]) / 255 ``` Поля не скрывают порядок исходных параметров: значения сохраняются в `SkySample`, а преобразование выполняется только в `fog_frame`. Нижняя граница освещения ограничивает результат расчёта света снизу; она не задаёт цвет тумана. Обычный цвет тумана выбирается из `sky.colors[0..4]` по направлению камеры, как цвет горизонта. Это подтверждено в `CSky::Render` и живым совпадением его значения с `D3DRENDERSTATE_FOGCOLOR` в том же игровом тике. Vulkan path передаёт эти параметры раздельно. Vertex shader вычисляет коэффициент по квадратам расстояния и границ, а fragment shader смешивает RGB материала с цветом тумана. Конкретный native 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 и не получают придуманных значений по умолчанию.