Complete the interactive mission viewer with environment rendering, audio events, dynamic shadows, free-flight camera controls, and per-component CTL pose sampling.
12 KiB
Эффекты окружения
Погода в 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 оставляет этот множитель явным для
владельца, который хранит собственную плотность.
Порядок обновления частицы важен:
- при создании три 15-битных значения детерминированного генератора заполняют локальный объём;
- локальная точка переводится через основу камеры в мировую позицию;
- мировая позиция интегрируется с мировым вектором скорости;
- для проверки границ позиция временно переводится обратно в локальные координаты, и к каждой оси применяется floor-based wrap;
- после 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.