diff options
| author | Valentin Popov <valentin@popov.link> | 2026-10-11 12:12:50 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-10-11 12:12:50 +0300 |
| commit | a8faba8aad441e87f8a8b94c148c01f9c075d621 (patch) | |
| tree | 97aa4972990edba9991ff2e125edac3a7883f50f /docs/reference/environment-effects.md | |
| parent | aa51f3574d3d4d4e0fa59653eedd718fceb5d1bd (diff) | |
| download | fparkan-a8faba8aad441e87f8a8b94c148c01f9c075d621.tar.xz fparkan-a8faba8aad441e87f8a8b94c148c01f9c075d621.zip | |
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.
Diffstat (limited to 'docs/reference/environment-effects.md')
| -rw-r--r-- | docs/reference/environment-effects.md | 203 |
1 files changed, 203 insertions, 0 deletions
diff --git a/docs/reference/environment-effects.md b/docs/reference/environment-effects.md new file mode 100644 index 0000000..31024f7 --- /dev/null +++ b/docs/reference/environment-effects.md @@ -0,0 +1,203 @@ +# Эффекты окружения + +Погода в Parkan состоит из двух связанных частей. Файл `sky.ske` задаёт +расписание: когда начинается дождь или снег, как меняются интенсивность и цвет, +какие имена ресурсов принадлежат активному интервалу. Таблица `sky.wea` задаёт +материалы, которыми эти осадки рисуются. `fparkan-fx::environment` соединяет +оба входа с текущей камерой и возвращает один `EnvironmentFrame`. + +```text +sky.ske + -> AtmosphereFrame: время, цвет, интенсивность, ссылки на ресурсы +sky.wea + -> SkyMaterials: имена материалов строк 7 (снег) и 8 (дождь) +камера + EnvironmentSystem + -> EnvironmentFrame: мировые частицы, экранные квадраты, гром и дождевой loop +``` + +Модуль не владеет графическим устройством или звуковой картой. Renderer +разрешает имя материала и отправляет геометрию в прозрачный проход, а audio +backend разрешает архив и имя звука. Это позволяет одной и той же модели +погоды работать в preview и в игровом цикле. + +## Входы и границы + +Для расписания используется такой вызов: + +```rust +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 от владельца кадра. + +## Объём и движение осадков + +Исходный эмиттер хранит прямоугольный объём в координатах камеры. Его +эталонные параметры таковы: + +```text +near = 2 +far = 50 +half_angle_x = 0.65 +half_angle_y = 0.4875 +``` + +Для эталонной камеры: + +```text +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 пересчитывают две боковые грани по той же глубине: + +```text +half_y = tan(vertical_fov / 2) * far +half_x = half_y * aspect_ratio +``` + +Количество частиц зависит от объёма и интенсивности. Вспомогательная +формула использует округление FISTP к ближайшему чётному целому: + +```text +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 точка снова переводится в мир, а её хвост сбрасывается на голову. + +Скорости не принадлежат системе координат камеры: + +```text +rain = [ 0.5, 0, -60 ] +snow = [ 0.5, 0, -4 ] +``` + +Это мировые векторы. Поворот камеры меняет область появления, wrap и +проекцию, но не вращает уже движущуюся каплю вокруг наблюдателя. В +`EnvironmentPrimitive::Particle` `position`, `velocity`, `world_head` и +`world_tail` имеют мировые координаты. + +Размер частиц состоит из коэффициента класса и масштаба погодного объекта: + +```text +rain_size = scalar * 0.0065 +snow_size = scalar * 0.0195 +``` + +`scalar` вычисляется из той же проекции, которую передаёт владелец кадра: + +```text +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-битного диапазона +вычисляется задержка: + +```text +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`: + +```text +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](../tomes/06-behavior.md#погода-осадки-и-звуковые-события). |
