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.
This commit is contained in:
@@ -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#погода-осадки-и-звуковые-события).
|
||||
Reference in New Issue
Block a user