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

17 KiB
Raw Blame History

Атмосфера и небо

Атмосфера Parkan задаётся двумя связанными ресурсами: sky.ske хранит расписание, цвета и параметры, а sky.wea сопоставляет позиционные строки с материалами. Ресурсы сначала разбираются в TypedAtmosphere и SkyMaterials, затем на каждом кадре из расписания получается AtmosphereFrame. Модуль fparkan-fx::sky превращает этот кадр в данные для рендерера: постоянную геометрию купола, динамические цвета вершин, небесные слои, положение светил, туман и нижнюю границу освещения.

Игровые сутки и смена дня и ночи

Вызов TypedAtmosphere::sample(real_seconds) принимает секунды от начала повторяющегося расписания. Дорожки идут в порядке файла и занимают свои реальные длительности; после последней дорожки время возвращается к началу. Внутри выбранной дорожки игровой день всегда проходит от 00:00 до 24:00:

track_seconds = real_seconds_in_track
 day_seconds = track_seconds / track_duration * 86400

Время ключа вычисляется по часам и минутам даты. Нативный конструктор игнорирует сохранённое поле секунд, поэтому для планировщика используется:

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)], где

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 колец. Радиус вычисляется как

radius = height * 0.5 / sin(theta_max * 0.5)^2

Вершина с индексом 0 находится в зените [0, 0, height * z_scale]. Остальные вершины идут в порядке azimuth, затем ring; для них

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 повторяет этот контракт:

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 формируется в исходном порядке:

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 и не получают придуманных значений по умолчанию.