237 lines
17 KiB
Markdown
237 lines
17 KiB
Markdown
# Атмосфера и небо
|
||||
|
|
|
|||
|
|
Атмосфера 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 и не получают придуманных
|
|||
|
|
значений по умолчанию.
|