aboutsummaryrefslogtreecommitdiff
path: root/docs/reference/environment-effects.md
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-10-11 12:12:50 +0300
committerValentin Popov <valentin@popov.link>2026-10-11 12:12:50 +0300
commita8faba8aad441e87f8a8b94c148c01f9c075d621 (patch)
tree97aa4972990edba9991ff2e125edac3a7883f50f /docs/reference/environment-effects.md
parentaa51f3574d3d4d4e0fa59653eedd718fceb5d1bd (diff)
downloadfparkan-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.md203
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#погода-осадки-и-звуковые-события).