2026-10-11 13:12:50 +04:00
|
|
|
|
# Эффекты окружения
|
|
|
|
|
|
|
|
|
|
|
|
Погода в 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 и в игровом цикле.
|
|
|
|
|
|
|
2026-10-19 22:43:16 +04:00
|
|
|
|
Цвета неба, ambient lighting и fog renderer-а используют разные поля того же
|
|
|
|
|
|
`sky.ske` sample. `SkySample::packed[1]` задаёт глобальный lighting floor;
|
|
|
|
|
|
`CShade` объединяет его с material lighting по поканальному максимуму.
|
|
|
|
|
|
`D3DRS_FOGCOLOR` (`0x22`) зависит от направления камеры и интерполируется из
|
|
|
|
|
|
`SkySample::colors[0..4]` по native sky heading. Значения `sky.values[0..1]`
|
|
|
|
|
|
продолжают задавать fog start/end, масштабируемые на 700.
|
|
|
|
|
|
|
2026-10-11 13:12:50 +04:00
|
|
|
|
## Входы и границы
|
|
|
|
|
|
|
|
|
|
|
|
Для расписания используется такой вызов:
|
|
|
|
|
|
|
|
|
|
|
|
```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 точка снова переводится в мир, а её хвост сбрасывается на голову.
|
|
|
|
|
|
|
2026-10-18 10:21:25 +04:00
|
|
|
|
Повторное обновление на том же времени имеет нулевой `dt`: позиция капли и
|
|
|
|
|
|
сохранённое начало хвоста остаются прежними. Хвост продвигается только при
|
|
|
|
|
|
положительном шаге; wrap по-прежнему сбрасывает его на голову.
|
|
|
|
|
|
|
2026-10-11 13:12:50 +04:00
|
|
|
|
Скорости не принадлежат системе координат камеры:
|
|
|
|
|
|
|
|
|
|
|
|
```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 сам решает,
|
|
|
|
|
|
как загрузить этот квадрат и как смешать его материал.
|
|
|
|
|
|
|
2026-10-18 09:54:32 +04:00
|
|
|
|
## Точечный свет из opcode 1
|
|
|
|
|
|
|
|
|
|
|
|
Preview вычисляет generic Op1 отдельно от визуальных команд. Переданный
|
|
|
|
|
|
владельцем `command_time` сравнивается с началом и концом интервала из body
|
|
|
|
|
|
opcode 1; обе границы включены, а коэффициент выборки равен
|
|
|
|
|
|
`(command_time - start) / (end - start)`. Заголовочная длительность FXID не
|
|
|
|
|
|
заменяет этот интервал. Для нулевой ширины адаптер не создаёт кадр света,
|
|
|
|
|
|
поскольку у команды нет конечного коэффициента интерполяции.
|
|
|
|
|
|
|
|
|
|
|
|
Позиция, направление, цвет и range интерполируются по концам команды.
|
|
|
|
|
|
Позиция и направление переводятся через основу родителя; направление после
|
|
|
|
|
|
этого нормализуется, а нулевой вектор остаётся нулевым. Range сохраняет
|
|
|
|
|
|
авторское значение без ограничения снизу. Типы Op1 `1`, `2`, `5`, `6` и `7`
|
|
|
|
|
|
выбирают point-light selector `1`; их native flags сохраняются. Типы `3` и
|
|
|
|
|
|
`4` выбирают другие классы записей LightManager и не попадают в generic список
|
|
|
|
|
|
точечного света. У `LightningEffect` выборка тех же полей использует отдельные
|
|
|
|
|
|
ограничение по длительности FXID и правила безопасной обработки.
|
|
|
|
|
|
|
2026-10-11 13:12:50 +04:00
|
|
|
|
## Молния и звуковые события
|
|
|
|
|
|
|
|
|
|
|
|
`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.
|
|
|
|
|
|
Отсутствие устройства вывода не должно останавливать симуляцию.
|
|
|
|
|
|
|
2026-10-31 22:06:49 +04:00
|
|
|
|
### Звуки Control
|
|
|
|
|
|
|
|
|
|
|
|
Команда Op2 может принадлежать не погоде, а экземпляру эффекта на узле модели.
|
|
|
|
|
|
`fparkan-fx::ambient_sound` хранит её временное окно и предыдущее значение
|
|
|
|
|
|
фазы. В режиме 0 разовый звук начинается при строгом пересечении начала окна:
|
|
|
|
|
|
`previous <= start && start < phase`, если фаза движется вперёд. Значение
|
|
|
|
|
|
`phase == 1` для этой проверки заменяется нулём и подготавливает следующий
|
|
|
|
|
|
цикл. Режим 2 включает loop на обеих границах окна; внутри окна уже работающий
|
|
|
|
|
|
источник обновляется, а не запускается заново. Выход за окно останавливает его.
|
|
|
|
|
|
|
|
|
|
|
|
Позиция интерполируется между двумя локальными точками и проходит через ту же
|
|
|
|
|
|
матрицу владельца, что и графические команды. Минимальная и максимальная
|
|
|
|
|
|
дистанции остаются постоянными; частота интерполируется по фазе окна.
|
|
|
|
|
|
Каждая команда получает отдельный источник звука, даже если ресурс совпадает
|
|
|
|
|
|
с ресурсом соседней команды. Дождь сохраняет свой loop. Перемещение слушателя,
|
|
|
|
|
|
потеря фокуса и завершение приложения затрагивают все эти источники.
|
|
|
|
|
|
|
|
|
|
|
|
Эти ветви восстановлены в GOG `Effect.dll` (сборка из
|
|
|
|
|
|
[таблицы бинарников](original-binaries.md)): init RVA `0x12D10`, update
|
|
|
|
|
|
`0x12EB0`. Живой loop `sounds.lib:f_defl.wav` передаёт в звуковой backend
|
|
|
|
|
|
диапазон `12/80`, частоту `1` и мировую позицию из матрицы владельца.
|
|
|
|
|
|
В режиме 2 признак loop берётся из первого DWORD body, а не из command flag.
|
|
|
|
|
|
|
|
|
|
|
|
Обычное создание эффекта Control сохраняет time mode заголовка и задаёт
|
|
|
|
|
|
постоянную фазу нулём. Raw selector строки служит ID экземпляра, а не номером
|
|
|
|
|
|
режима времени. Поэтому `f_comp_sfx` с time mode 0 и началом окна `0.001`
|
|
|
|
|
|
остаётся тихим до управляющей команды Control. Preview такие команды ещё не
|
|
|
|
|
|
исполняет. Периодические звуки с time mode 2 уже следуют общим часам и текущей
|
|
|
|
|
|
позе узла. `render_coverage control_op2_supported` считает подготовленные
|
|
|
|
|
|
команды, а `control_sound_start/stop` показывает фактические переходы.
|
|
|
|
|
|
|
|
|
|
|
|
Неизвестные backend modes 1/3, ненулевое поле body+76, неподдержанное время
|
|
|
|
|
|
или привязка владельца пропускаются с `render_coverage control_op2_unsupported`.
|
|
|
|
|
|
Они не заменяются разовым звуком или произвольным loop.
|
|
|
|
|
|
|
2026-10-18 05:48:38 +04:00
|
|
|
|
## Цвет солнца
|
|
|
|
|
|
|
|
|
|
|
|
Солнце берёт цвет спрайта и прозрачность бликов из `palette[10]` расписания
|
|
|
|
|
|
`sky.ske`; `palette[9]` — чёрный specular-компонент. Вторичный направленный
|
|
|
|
|
|
свет использует `palette[14]`. При выборке расписания это выражено так:
|
|
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
|
packed: [key.colors[10], key.colors[14]],
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Этот порядок соответствует живому GOG-замеру: `CSun+0x95c` содержал
|
|
|
|
|
|
`0x41FF3C00` (интерполяция `palette[10]`), `+0x960` — `0xFF000000`
|
|
|
|
|
|
(`palette[9]`), а `+0x964` — вторичный свет из `palette[14]`. В `Terrain.dll`
|
|
|
|
|
|
по VA `0x1004AA92..0x1004AAAB` цвет в `+0x95c` обновляется, только если
|
|
|
|
|
|
установлен бит 0 результата sky-query; иначе остаётся прежнее значение.
|
|
|
|
|
|
`AtmosphereFrame` пока не несёт этот бит обновления, поэтому sampler
|
|
|
|
|
|
выдаёт интерполированный цвет при каждом запросе.
|
|
|
|
|
|
|
2026-10-31 17:18:47 +04:00
|
|
|
|
## Отрисовка солнца, луны и бликов
|
2026-10-18 07:28:16 +04:00
|
|
|
|
|
2026-10-31 17:18:47 +04:00
|
|
|
|
`CSun::Render` получает текущий viewport, проецирует направление солнца или
|
|
|
|
|
|
луны и собирает quad уже в его пикселях. `Terrain.dll` VA `0x1004AA40` передаёт
|
|
|
|
|
|
эту геометрию через shade callback; native FVF `0x1c4` задаёт
|
|
|
|
|
|
`XYZRHW | DIFFUSE | SPECULAR | TEX1`. Конвертер `0x10020700` сохраняет входные
|
|
|
|
|
|
`x`, `y` и `z`, а в `RHW` записывает `1.0`; исходный `z` равен `0.999`. Callback
|
|
|
|
|
|
включает depth test и выключает запись depth, поэтому более близкая поверхность
|
|
|
|
|
|
может закрыть спрайт, а сам спрайт не меняет глубину для последующих слоёв.
|
|
|
|
|
|
|
|
|
|
|
|
В Vulkan ряды 3 и 4 сохраняют этот contract отдельным bit 8 material mode.
|
|
|
|
|
|
CPU каждый кадр преобразует viewport-пиксели к clip space текущего размера
|
|
|
|
|
|
swapchain:
|
2026-10-18 07:28:16 +04:00
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
|
position = [
|
|
|
|
|
|
pixel[0] * (2.0 / width) - 1.0,
|
|
|
|
|
|
pixel[1] * (2.0 / height) - 1.0,
|
|
|
|
|
|
0.999,
|
|
|
|
|
|
];
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-10-31 17:18:47 +04:00
|
|
|
|
Положительная высота Vulkan viewport отображает эти координаты в тот же
|
|
|
|
|
|
top-down экран. Vertex shader пропускает мировую матрицу только для этого
|
|
|
|
|
|
режима:
|
|
|
|
|
|
|
|
|
|
|
|
```glsl
|
|
|
|
|
|
vec4 clip_position = screen_tl
|
|
|
|
|
|
? vec4(in_position, 1.0)
|
|
|
|
|
|
: frame.clip_from_world * vec4(in_position, 1.0);
|
|
|
|
|
|
```
|
2026-10-18 07:28:16 +04:00
|
|
|
|
|
2026-10-31 17:18:47 +04:00
|
|
|
|
Материал остаётся unlit, fog factor равен нулю. Для мировой геометрии
|
2026-10-30 18:28:45 +04:00
|
|
|
|
оригинал задаёт viewport `MinZ = 0.1`, `MaxZ = 0.99`: после отсечения
|
|
|
|
|
|
проекционная глубина отображается в этот диапазон:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
world_depth = 0.1 + (0.99 - 0.1) * (clip_z / clip_w)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Даже дальняя гора получает глубину не больше `0.99` и закрывает экранный
|
|
|
|
|
|
диск с `z = 0.999`. Если оставить мировой диапазон `0–1`, при near `0.5`
|
|
|
|
|
|
и far `700` поверхность дальше примерно `292` единиц окажется позади диска:
|
|
|
|
|
|
солнце начнёт просвечивать сквозь неё при движении камеры. Одного переноса
|
|
|
|
|
|
нативного `z` в экранные вершины недостаточно.
|
|
|
|
|
|
|
|
|
|
|
|
В Vulkan диапазон задаётся динамическим viewport отдельно для мировых
|
|
|
|
|
|
пакетов; экранные TL и фоновые sky-пакеты сохраняют диапазон `0–1`.
|
|
|
|
|
|
Матрица проекции и плоскости отсечения при этом не меняются. Константы
|
|
|
|
|
|
мирового профиля записывает GOG `Terrain.dll` RVA `0x27422–0x27452` из таблицы
|
|
|
|
|
|
`0x65088/0x6508C/0x650A0/0x650A4` (near, far, MinZ, MaxZ). GOG `Ngi32.dll`
|
|
|
|
|
|
RVA `0x7170`, участок `0x71E7–0x723C`, копирует MinZ/MaxZ в renderer
|
|
|
|
|
|
`+0x30/+0x34` и передаёт их `IDirect3DDevice7::SetViewport`; сборки указаны в
|
|
|
|
|
|
[таблице бинарников](original-binaries.md). Нативные значения этих двух полей
|
|
|
|
|
|
имеют биты `0x3DCCCCCD` и `0x3F7D70A4`.
|
|
|
|
|
|
|
2026-10-31 17:18:47 +04:00
|
|
|
|
Sky-background bit 4 продолжает помещать слои на дальнюю плоскость. Отдельные
|
|
|
|
|
|
flare rows 5 и 6 пока используют world-space billboard builder: для них не
|
|
|
|
|
|
подтверждён этот же native TL путь.
|
|
|
|
|
|
|
|
|
|
|
|
## Эффекты Control
|
|
|
|
|
|
|
|
|
|
|
|
Погодное расписание `sky.ske` — только один источник эффектов. У объектов
|
|
|
|
|
|
миссии таблица Control (`.ctl`) связывает строки с FXID-командами. Текущий
|
|
|
|
|
|
preview выбирает начальную группу Control и исполняет только строки без
|
|
|
|
|
|
predicate, chain и state gate. Условия игровых событий и переходы между
|
|
|
|
|
|
группами принадлежат полному игровому Control runtime и здесь не моделируются.
|
|
|
|
|
|
|
|
|
|
|
|
Type 4 row задаёт локальную basis эффекта через три точки CTL point list. Для
|
|
|
|
|
|
валидной привязки первая точка также задаёт owner node. Путь MSH выбирает позу
|
|
|
|
|
|
этого узла из текущей pose hierarchy; для внешнего mounted-компонента к ней
|
|
|
|
|
|
добавляется поза socket. После этого применяются object transform, header
|
|
|
|
|
|
scale и локальная матрица FX. Подробнее о порядке матриц см.
|
|
|
|
|
|
[описание MSH](msh.md#control-fx-и-owner-transform).
|
|
|
|
|
|
|
|
|
|
|
|
Renderer preview поддерживает FXID-команды Op3 modes 0–2, Op4, Op7, Op8 и Op9.
|
|
|
|
|
|
Op3 mode0 строит camera-facing поверхность из анимируемых локальных позиции и
|
|
|
|
|
|
масштаба; Op7/Op8 обновляют отдельные элементы particle pool; Op9 создаёт
|
|
|
|
|
|
индексированную полусферическую сетку. Op1 вычисляется отдельно как список
|
|
|
|
|
|
point lights: selector 1 и записи без `flags & 0x80000000` входят в общий
|
|
|
|
|
|
diffuse light set. Записи с этим битом сохраняют native flags для специальных
|
|
|
|
|
|
путей и projected shadows, но не добавляются в общий diffuse список.
|
|
|
|
|
|
|
|
|
|
|
|
Body discriminator Op3 читается из первого DWORD `FxCommand.raw_body`; offsets
|
|
|
|
|
|
в evaluator-е относительны к данным после четырёхбайтового command word. Mode 1
|
|
|
|
|
|
добавляет анимируемую ось к camera-facing basis, mode 2 использует native
|
|
|
|
|
|
D110 basis. Для modes 0 и 1 preview предполагает render context 0, который
|
|
|
|
|
|
проходит общий gate `Effect.dll` `0x106C0`; их API не выбирает другой context.
|
|
|
|
|
|
Evaluator mode 2 принимает индекс context и учитывает gate при расчёте `active`.
|
2026-10-18 08:39:37 +04:00
|
|
|
|
|
2026-10-30 18:15:19 +04:00
|
|
|
|
У Op3 mode 1 ось задаёт длину прямоугольника, а не его нормаль. Для
|
|
|
|
|
|
локального вектора к камере `v` и анимируемой оси `a` столбцы базиса имеют
|
|
|
|
|
|
порядок `[a, u, w]`, где `u = normalize(v × a)`, `w = a × u`. Плоскость
|
|
|
|
|
|
прямоугольника — XY, поэтому первая сторона идёт вдоль авторской оси, а
|
|
|
|
|
|
вторая поворачивается к камере. Это сохраняет соединение между точками
|
|
|
|
|
|
локатора при движении камеры. Если поставить ось в третий столбец,
|
|
|
|
|
|
прямоугольник займёт поперечную плоскость и связь превратится в полоску или
|
|
|
|
|
|
небольшой диск.
|
|
|
|
|
|
|
|
|
|
|
|
Порядок подтверждён аргументами конструктора матрицы в GOG `Effect.dll`
|
|
|
|
|
|
(сборка из [таблицы бинарников](original-binaries.md), SHA-256
|
|
|
|
|
|
`386a3e8db1ce23f61957825d633c7dae1ed40c25cab0cf10a534385fd7cee789`):
|
|
|
|
|
|
RVA `0x93D0`, ветка mode 1 с `0x9739`, сборка столбцов `0x97F6–0x983C`.
|
|
|
|
|
|
Например, `a = (1, 0, 0)` и `v = (2, 1, 0)` дают столбцы
|
|
|
|
|
|
`(1, 0, 0)`, `(0, 0, -1)`, `(0, 1, 0)`. При нативном вызове длины последних
|
|
|
|
|
|
двух столбцов могут немного отличаться от единицы из-за приближённой
|
|
|
|
|
|
нормализации выбранного `g_FastProc`; порядок осей от этого не меняется.
|
|
|
|
|
|
|
2026-10-30 20:16:19 +04:00
|
|
|
|
У сооружения `s_tree_58` в миссии «Конструктор» FXID `tree_ray_58` задаёт два
|
|
|
|
|
|
слоя жёлтого луча: команды Op3 mode 1 с ресурсами `defl_y_add` и `defl_y1_add`.
|
|
|
|
|
|
Их геометрию сверили с CPU-вызовом GOG `Effect.dll` `0x93D0`. На семи временных
|
|
|
|
|
|
фазах и трёх направлениях камеры для каждой команды (42 выборки) вершины
|
|
|
|
|
|
совпали с Rust evaluator с максимальной покомпонентной разницей `0.0001373291`.
|
|
|
|
|
|
Оракул получает уже выбранные Rust позицию, ось и масштаб, а world-camera
|
|
|
|
|
|
переводится в `cameraLocal` тем же component-wise projection, что и native
|
|
|
|
|
|
`FastProc + 0x68`. Поэтому это подтверждает только геометрию Type 1 helper-а на
|
|
|
|
|
|
этих входах: обновление и расписание эффекта, UV/material и пиксельное
|
|
|
|
|
|
совпадение здесь не проверялись.
|
|
|
|
|
|
|
2026-10-18 08:55:47 +04:00
|
|
|
|
## Частицы Op7
|
|
|
|
|
|
|
|
|
|
|
|
`fparkan-fx::op7` вычисляет фиксированный набор частиц Op7 по body команды,
|
|
|
|
|
|
фазе эффекта, матрице владельца, позиции камеры и render context. После
|
|
|
|
|
|
инициализации состояние команды сохраняет случайно изменённые треки позиции
|
|
|
|
|
|
и масштаба. Body задаёт время групп, opacity, фазу материала и вертикальное
|
|
|
|
|
|
ускорение; последнее масштабируется переданным world scalar и применяется к
|
|
|
|
|
|
копии матрицы владельца каждой активной группы. Текущий preview
|
|
|
|
|
|
`fparkan-game` передаёт `world_scalar = 10.0`; чтение действующей настройки
|
|
|
|
|
|
мира пока не подключено.
|
|
|
|
|
|
|
|
|
|
|
|
Часы активной группы обновляются до применения detail cutoff отдельной
|
|
|
|
|
|
частицы. Cutoff сравнивает ordinal render context с постоянным порогом слота.
|
|
|
|
|
|
`Effect.dll` `0x1300` использует тот же порядок, поэтому скрытие слота не
|
|
|
|
|
|
останавливает часы его активной группы. Поток частиц последовательно задаёт
|
|
|
|
|
|
случайные изменения позиции и масштаба; для порогов видимости используется
|
|
|
|
|
|
отдельный поток. Шаг LFSR совпадает с нативным переходом двух половин, однако
|
|
|
|
|
|
семена отдельных экземпляров лишь детерминированно заменяют process-global
|
|
|
|
|
|
состояние и не воспроизводят общий порядок вызовов.
|
|
|
|
|
|
|
2026-10-31 17:18:47 +04:00
|
|
|
|
`f_gener_ball` evaluator покрывает Op4 mode 0 с положительным знаменателем в
|
|
|
|
|
|
`body+0xC4` и Op9 mode 2. Для Op9 selectors `body+0xC4` со значениями 0, 1 и 2
|
|
|
|
|
|
отображаются в native flag bases 3, 5 и 6. Нулевой `body+0xC8` проецирует UV на
|
|
|
|
|
|
XY, ненулевой выбирает статические quad/cap координаты. Op4 вычисляет локальное
|
|
|
|
|
|
расстояние камеры тем же component-wise projection, что использует native path.
|
|
|
|
|
|
|
|
|
|
|
|
FX-команды используют общий preview clock и ограниченный набор time modes:
|
|
|
|
|
|
mode 0 оставляет фазу равной нулю, mode 1 вычисляет `elapsed / duration` без
|
|
|
|
|
|
зажатия, mode 2 повторяет цикл. Header flags `0x200` и `0x20` преобразуют эту
|
|
|
|
|
|
фазу; command path `0x8120` зажимает результат только после преобразований.
|
|
|
|
|
|
Effect renderer проверяет header bit `0x800` только при
|
|
|
|
|
|
render selector 0; native AniMesh component path был захвачен с selector 1,
|
|
|
|
|
|
поэтому preview использует selector 1 и не подавляет такие visual commands.
|
|
|
|
|
|
Этот capture также показал `0xD00` FX entries в 53-entry manager. Динамическое
|
|
|
|
|
|
состояние manager `A8`, связанное с header gate и фактической FX phase, пока не
|
|
|
|
|
|
моделируется. Header bit `0x10` относится к activation lifecycle и сам по себе
|
|
|
|
|
|
не отключает draw.
|
|
|
|
|
|
Header jitter и random shift хранятся один раз на Control row и совместно
|
|
|
|
|
|
применяются к его sibling-командам; отдельный детерминированный поток каждой
|
|
|
|
|
|
строки сохраняет эту связь, но не повторяет порядок глобального native RNG.
|
|
|
|
|
|
Неразобранные группы, event predicates и неразрешённые owner attachments
|
|
|
|
|
|
пропускаются.
|
|
|
|
|
|
|
|
|
|
|
|
Для visual FX material берётся из resource reference команды. MAT0 выбирает
|
|
|
|
|
|
фазу по normalized FX phase, независимо от wall-clock анимации world materials.
|
|
|
|
|
|
Command opacity передаётся vertex alpha, а FX MAT0 alpha не умножает её второй
|
|
|
|
|
|
раз; texture alpha продолжает участвовать в shader. Текущая preview-ветка
|
|
|
|
|
|
использует SourceAlpha и read-only depth state и допускает только MAT0-фазы,
|
|
|
|
|
|
проходящие локальный predicate additive-RGB approximation. Семантика native
|
|
|
|
|
|
`flag40` и directional-light ветки остаётся открытой.
|
|
|
|
|
|
|
|
|
|
|
|
Generated Op9 mesh разделяется на отдельный draw range для каждого native
|
|
|
|
|
|
quad и cap. Каждый range получает ключ по камере до первой индексированной
|
|
|
|
|
|
вершины, в порядке убывания расстояния. Равные ключи используют midpoint
|
|
|
|
|
|
insertion из native queue, поэтому их порядок не является стабильным по
|
|
|
|
|
|
первоначальному времени поступления. Preview использует category 0 как
|
|
|
|
|
|
highest-detail mesh; projected-size LOD и полное визуальное совпадение размера
|
|
|
|
|
|
и формы пока не реализованы.
|
|
|
|
|
|
|
2026-10-18 09:27:18 +04:00
|
|
|
|
## Частицы Op8
|
|
|
|
|
|
|
|
|
|
|
|
`fparkan-fx::smoke` разбирает body Op8, хранит кольцевой пул команды и выдаёт
|
|
|
|
|
|
quad-ы с ориентацией к камере для активных слотов. Нативная нормализованная
|
2026-10-31 17:18:47 +04:00
|
|
|
|
фаза окна ограничивается диапазоном 0–1 и задаёт интервал проверки следующего
|
|
|
|
|
|
события; сравнение строгое (`schedule_base + interval < update_clock`). Все
|
|
|
|
|
|
события catch-up в одном обновлении проверяются по тому же нормализованному
|
|
|
|
|
|
интервалу; после события нативный код продвигает базу расписания интервалом,
|
|
|
|
|
|
вычисленным по сырой фазе эффекта, а возраст слота считает по нормализованному
|
|
|
|
|
|
интервалу. Когда фаза равна единственной границе окна, деление `0/0` даёт NaN,
|
|
|
|
|
|
который нативный unordered-путь сравнения ограничивает нулём.
|
2026-10-18 09:27:18 +04:00
|
|
|
|
|
|
|
|
|
|
Нулевая база расписания служит признаком начального состояния: обновление
|
|
|
|
|
|
сохраняет текущую опору эмиттера и базу времени, не создавая частицу. Если
|
|
|
|
|
|
первое время равно нулю, следующее обновление повторно захватывает опору. При
|
|
|
|
|
|
событии скорость опоры считается от сохранённой позиции до текущей за время
|
|
|
|
|
|
от базы расписания; слот смещает сохранённую опору на `скорость * due_interval`.
|
2026-10-31 17:18:47 +04:00
|
|
|
|
При нулевой скорости оригинал подставляет ось X владельца и для направления,
|
|
|
|
|
|
и для этого смещения. Обновление без событий не переносит сохранённую опору к
|
|
|
|
|
|
текущему владельцу. Откат часов корректирует только базу расписания до текущего
|
|
|
|
|
|
времени: слоты, опора и три потока случайных чисел не сбрасываются.
|
2026-10-18 09:27:18 +04:00
|
|
|
|
|
|
|
|
|
|
Evaluator принимает отдельные начальные состояния для эмиттера, знаковых
|
|
|
|
|
|
отклонений и материала. Переход каждого потока совпадает с общим native LFSR,
|
|
|
|
|
|
но эти локальные seed не воспроизводят порядок process-global вызовов игры.
|
|
|
|
|
|
Предел 100000 событий catch-up — защитная граница evaluator-а; соответствующего
|
|
|
|
|
|
лимита в исследованном native цикле не обнаружено.
|
|
|
|
|
|
|
2026-10-11 13:12:50 +04:00
|
|
|
|
## Что остаётся за владельцем
|
|
|
|
|
|
|
|
|
|
|
|
`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#погода-осадки-и-звуковые-события).
|