Files
fparkan/docs/reference/environment-effects.md
T
Valentin Popov dda322d8be
Docs Deploy / Build and Deploy MkDocs (push) Successful in 42s
feat(fx): evaluate native generator mesh modes
2026-10-18 08:31:52 +04:00

17 KiB
Raw Blame History

Эффекты окружения

Погода в Parkan состоит из двух связанных частей. Файл sky.ske задаёт расписание: когда начинается дождь или снег, как меняются интенсивность и цвет, какие имена ресурсов принадлежат активному интервалу. Таблица sky.wea задаёт материалы, которыми эти осадки рисуются. fparkan-fx::environment соединяет оба входа с текущей камерой и возвращает один EnvironmentFrame.

sky.ske
  -> AtmosphereFrame: время, цвет, интенсивность, ссылки на ресурсы
sky.wea
  -> SkyMaterials: имена материалов строк 7 (снег) и 8 (дождь)
камера + EnvironmentSystem
  -> EnvironmentFrame: мировые частицы, экранные квадраты, гром и дождевой loop

Модуль не владеет графическим устройством или звуковой картой. Renderer разрешает имя материала и отправляет геометрию в прозрачный проход, а audio backend разрешает архив и имя звука. Это позволяет одной и той же модели погоды работать в preview и в игровом цикле.

Входы и границы

Для расписания используется такой вызов:

let frame = environment.update_atmosphere_with_materials(
    dt_seconds,
    &atmosphere_frame,
    &sky_materials,
    camera,
);

AtmosphereFrame передаёт ссылки из стартового ключа активного интервала. Для дождя первая ссылка используется как звук фонового loop. Строка вида archive/name разделяется на архив и имя. У простой строки name архив остаётся пустым: звуковой владелец подставляет библиотеку, выбранную миссией. Имя не заменяется глобальным именем из AutoDemo.

SkyMaterials хранит разобранную таблицу, поэтому строки 7 и 8 являются положением в формате исходной игры, а не зашитыми названиями. В AutoDemo там находятся SNOWFLAKE и RAIN_DROP; миссия может передать другие имена. Если активной погоде не назначен материал, частицы для неё не создаются.

На входной границе конечные значения приводятся к безопасному диапазону: dt_seconds и интенсивность неотрицательны, интенсивность и каждый компонент цвета ограничены единицей, а нечисловые значения заменяются нулём. Камера получает конечную ортонормированную основу, FOV, aspect ratio и размер viewport от владельца кадра.

Объём и движение осадков

Исходный эмиттер хранит прямоугольный объём в координатах камеры. Его эталонные параметры таковы:

near = 2
far  = 50
half_angle_x = 0.65
half_angle_y = 0.4875

Для эталонной камеры:

half_x = tan(half_angle_x) * far
half_y = tan(half_angle_y) * far
origin = [near, -half_x, -half_y]
extent = [far - near, 2 * half_x, 2 * half_y]

Текущий FOV и aspect ratio пересчитывают две боковые грани по той же глубине:

half_y = tan(vertical_fov / 2) * far
half_x = half_y * aspect_ratio

Количество частиц зависит от объёма и интенсивности. Вспомогательная формула использует округление FISTP к ближайшему чётному целому:

N = round_even(
        clamp(current_volume / reference_volume, 0, 1)
        * density
        * intensity
        * 1000
    )

Текущий эмиттер передаёт density = 1. Публичный PrecipitationVolume::particle_count_for оставляет этот множитель явным для владельца, который хранит собственную плотность.

Порядок обновления частицы важен:

  1. при создании три 15-битных значения детерминированного генератора заполняют локальный объём;
  2. локальная точка переводится через основу камеры в мировую позицию;
  3. мировая позиция интегрируется с мировым вектором скорости;
  4. для проверки границ позиция временно переводится обратно в локальные координаты, и к каждой оси применяется floor-based wrap;
  5. после wrap точка снова переводится в мир, а её хвост сбрасывается на голову.

Скорости не принадлежат системе координат камеры:

rain = [ 0.5, 0, -60 ]
snow = [ 0.5, 0,  -4 ]

Это мировые векторы. Поворот камеры меняет область появления, wrap и проекцию, но не вращает уже движущуюся каплю вокруг наблюдателя. В EnvironmentPrimitive::Particle position, velocity, world_head и world_tail имеют мировые координаты.

Размер частиц состоит из коэффициента класса и масштаба погодного объекта:

rain_size = scalar * 0.0065
snow_size = scalar * 0.0195

scalar вычисляется из той же проекции, которую передаёт владелец кадра:

horizontal_fov = 2 * atan(tan(vertical_fov / 2) * aspect_ratio)
scalar = viewport_width / horizontal_fov

Это соответствует запросу размера native camera (Terrain primary vtable slot +6c): ширина RECT делится на горизонтальный FOV в радианах. Поэтому Camera::with_projection(...).with_viewport([width, height]) должен получать реальный viewport каждого кадра; отдельного setter-а масштаба нет.

ScreenBillboard содержит мировые голову и хвост, их проекцию и четыре NDC угла с UV. half_size остаётся native-величиной в пикселях, а обе глубины хранятся отдельно; renderer не смешивает эти единицы. Дождь использует предыдущую мировую голову как хвост и строит полосу перпендикулярно экранному направлению движения. Снег остаётся квадратом в текущей голове, использует исходную таблицу знаков и минимальный depth fade 0.1. Renderer сам решает, как загрузить этот квадрат и как смешать его материал.

Молния и звуковые события

decode_env_lightning_fxid извлекает из FXID длительность, ссылки opcode 3 и 2, первые четыре числа opcode 1 и полный body opcode 1. Имена не подменяются глобальным whitelist-ом: материал и звук принадлежат выбранному FXID. LightningEffect устанавливается через set_lightning_effect; без него погодный таймер не создаёт визуал или звук.

Активный объект использует следующий таймер. При интенсивности I сначала применяется min(I, 0.95), затем для случайного U из 15-битного диапазона вычисляется задержка:

delay_ms = round_even((1 - min(I, 0.95)) * 60000 * U)

После достижения срока выбираются X и Y из переданных LightningBounds, а Z берётся без изменения. Если границы не заданы, используется позиция камеры. Следующая попытка разрешена через 6000 миллисекунд. Длительность уже созданного FX берётся из его заголовка. Renderer получает native descriptor [40, 40, 600], начало эффекта на sampled_z + 300; при нулевом локальном смещении концы quad находятся на sampled_z и sampled_z + 600. Opcode 3 задаёт материал, локальное смещение, scale и lifetime visual quad. Opcode 1 вычисляется отдельно как динамический point light и не задаёт размеры или UV quad. CPU не подменяет numeric поля шириной или линейным lifetime fade.

Звуки возвращаются рядом с графикой как SoundEvent:

StartLoop       -> начать дождевой loop
SetLoopVolume   -> обновить его громкость при интерполяции погоды
StopLoop        -> остановить loop
OneShot         -> воспроизвести разовый звук молнии

Каждое событие содержит архив, имя, мировую позицию и линейную громкость. Для FX-звука также передаются native min_distance, max_distance и frequency_ratio; у дождевого loop используются 0, бесконечность и 1. Событие OneShot молнии использует ту же случайно выбранную позицию, что и визуальный эффект, а его диапазон и частота читаются из opcode 2: в поставленном env_lightning это 100, 1500 и 1. Audio backend загружает объявленный архив и запись лениво, кэширует проверенный sample, создаёт отдельный spatial source, применяет DirectSound range/pan и текущий listener. Отсутствие устройства вывода не должно останавливать симуляцию.

Цвет солнца

Солнце берёт цвет спрайта и прозрачность бликов из palette[10] расписания sky.ske; palette[9] — чёрный specular-компонент. Вторичный направленный свет использует palette[14]. При выборке расписания это выражено так:

packed: [key.colors[10], key.colors[14]],

Этот порядок соответствует живому GOG-замеру: CSun+0x95c содержал 0x41FF3C00 (интерполяция palette[10]), +0x960 — 0xFF000000 (palette[9]), а +0x964 — вторичный свет из palette[14]. В Terrain.dll по VA 0x1004AA92..0x1004AAAB цвет в +0x95c обновляется, только если установлен бит 0 результата sky-query; иначе остаётся прежнее значение. AtmosphereFrame пока не несёт этот бит обновления, поэтому sampler выдаёт интерполированный цвет при каждом запросе.

Экранная геометрия солнца и луны

CSun::Render сначала переводит направление небесного тела в пиксели текущего viewport, затем собирает quad с native XYZRHW-позицией. При положительной высоте viewport Vulkan пиксели переводятся в clip-space так:

position = [
    pixel[0] * (2.0 / width) - 1.0,
    pixel[1] * (2.0 / height) - 1.0,
    0.999,
];

Vertex shader пропускает clip_from_world для строк солнца и луны. CPU каждый кадр пересчитывает центр и размер quad по текущему viewport; старые пиксельные координаты не переносятся на новый размер. World-space billboards бликов остаются отдельным путём. Материал экранного quad не освещается, vertex fog отключён, а sky far-depth bit к нему не применяется.

Native z = 0.999 не означает, что спрайт гарантированно лежит за любой поверхностью. Vulkan использует LESS_OR_EQUAL и не записывает depth для прозрачного ряда. При near 0.5, far 700 мировая поверхность на расстоянии 320 получает depth 700 / (700 - 0.5) * (1 - 0.5 / 320) ≈ 0.999151; поэтому экранный 0.999 чуть ближе этой поверхности и проходит depth test перед ней. Это фиксированное native значение сохраняется для совместимости, но само по себе не обещает окклюзию солнца всей геометрией местности.

Геометрия визуальных FX-команд

fparkan-fx::fx_visual формирует кадры для двух подтверждённых вариантов f_gener_ball. Op4 принимает mode 0 с положительным знаменателем в body+0xC4 и выдаёт camera-facing quad; расстояние камеры считается через native component-wise projection родительской матрицы. Op9 принимает mode 2: селектор body+0xC4 со значением 0, 1 или 2 отображается в native flag base 3, 5 или 6 и выбирает одну из трёх сеток; категория render flags выбирает её детализацию. body+0xC8 выбирает UV path: нулевое значение проецирует позицию вершины на XY, ненулевое использует фиксированные координаты quad и cap. Кадр сохраняет MAT0 resource, фазу, opacity, вершины и triangle indices для renderer.

Evaluator получает готовые command body, phase, camera point и внешнюю матрицу. Выбор Control row, manager gates, owner attachment, header jitter и global RNG остаются задачей владельца. fparkan-game пока использует для Op9 категорию 0 с наибольшей детализацией; выбор категории по projected size ещё не реализован.

Что остаётся за владельцем

EnvironmentSystem не извлекает камеру из renderer, не разрешает материалы или звуки и не выполняет финальный GPU pass. Владелец должен передать основу камеры, projection, реальный viewport, SkyMaterials и декодированный FXID. Из projection и viewport модуль сам получает native scalar осадков.

CPU-контракт проверяет движение, wrap, количество, время молнии и порядок звуковых переходов. Он не заявляет совместимость с нераскрытой семантикой всех FX opcode, с точным native RNG sequence или с конкретным mixer/device backend. Подробное описание связи с listener и mission audio находится в томе VI.