Files
fparkan/docs/reference/atmosphere.md
T
Valentin Popov 3f4059b726
Docs Deploy / Build and Deploy MkDocs (push) Successful in 40s
docs(render): distinguish fog color from the lighting floor
2026-10-31 20:46:33 +04:00

18 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

Затем к нему применяется матрица рождения как к вектору-столбцу. Если A и B — углы рождения, а x = cos(theta) и z = -sin(theta), результат равен

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

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 записывает две дальности и нижнюю границу освещения. FogFrame повторяет этот контракт:

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

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