Files
fparkan/docs/reference/environment-effects.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

12 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. Отсутствие устройства вывода не должно останавливать симуляцию.

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

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.