18 KiB
Атмосфера и небо
Атмосфера 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], отдельно от directionalSunFrame::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 и не получают придуманных
значений по умолчанию.