feat(render): integrate Control effects and animated world scene
Docs Deploy / Build and Deploy MkDocs (push) Successful in 37s

This commit is contained in:
Valentin Popov committed 2026-10-31 17:18:47 +04:00
1 parent 4a589f42fc
commit 09fcd35023
7 files changed
+8364 -2601

No files matched your search

+101 -46
View File
@@ -236,11 +236,19 @@ packed: [key.colors[10], key.colors[14]],
`AtmosphereFrame` пока не несёт этот бит обновления, поэтому sampler
выдаёт интерполированный цвет при каждом запросе.
## Экранная геометрия солнца и луны
## Отрисовка солнца, луны и бликов
`CSun::Render` сначала переводит направление небесного тела в пиксели текущего
viewport, затем собирает quad с native XYZRHW-позицией. При положительной высоте
viewport Vulkan пиксели переводятся в clip-space так:
`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:
```rust
position = [
@@ -250,13 +258,17 @@ position = [
];
```
Vertex shader пропускает `clip_from_world` для строк солнца и луны. CPU каждый
кадр пересчитывает центр и размер quad по текущему viewport; старые пиксельные
координаты не переносятся на новый размер. World-space billboards бликов
остаются отдельным путём. Материал экранного quad не освещается, vertex fog
отключён, а sky far-depth bit к нему не применяется.
Положительная высота 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);
```
Материал остаётся unlit, fog factor равен нулю. Для мировой геометрии
оригинал задаёт viewport `MinZ = 0.1`, `MaxZ = 0.99`: после отсечения
проекционная глубина отображается в этот диапазон:
@@ -280,35 +292,39 @@ RVA `0x7170`, участок `0x71E7–0x723C`, копирует MinZ/MaxZ в re
[таблице бинарников](original-binaries.md). Нативные значения этих двух полей
имеют биты `0x3DCCCCCD` и `0x3F7D70A4`.
Sky-background bit 4 продолжает помещать слои на дальнюю плоскость. Отдельные
flare rows 5 и 6 пока используют world-space billboard builder: для них не
подтверждён этот же native TL путь.
## Геометрия визуальных FX-команд
## Эффекты Control
`fparkan-fx::fx_visual` формирует кадры для двух подтверждённых вариантов
`f_gener_ball`. Op4 принимает mode 0 с положительным знаменателем в `body+0xC4`
и выдаёт camera-facing quad; расстояние камеры считается через native
component-wise projection родительской матрицы. Op9 принимает mode 2:
селектор `body+0xC4` со значением 0, 1 или 2 отображается в native flag base
3, 5 или 6 и выбирает одну из трёх сеток; категория render flags выбирает её
детализацию. `body+0xC8` выбирает UV path:
нулевое значение проецирует позицию вершины на XY, ненулевое использует
фиксированные координаты quad и cap. Кадр сохраняет MAT0 resource, фазу,
opacity, вершины и triangle indices для renderer.
Погодное расписание `sky.ske` — только один источник эффектов. У объектов
миссии таблица Control (`.ctl`) связывает строки с FXID-командами. Текущий
preview выбирает начальную группу Control и исполняет только строки без
predicate, chain и state gate. Условия игровых событий и переходы между
группами принадлежат полному игровому Control runtime и здесь не моделируются.
Evaluator получает готовые command body, phase, camera point и внешнюю
матрицу. Выбор Control row, manager gates, owner attachment, header jitter и
global RNG остаются задачей владельца. `fparkan-game` пока использует для Op9
категорию 0 с наибольшей детализацией; выбор категории по projected size ещё
не реализован.
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).
Для Op3 поддерживаются body-типы 0, 1 и 2. Поля отсчитываются от
`FxCommand.raw_body`, то есть после 4-байтового command word. Типы 0 и 1
используют общие position/scale/phase/opacity tracks и camera-facing основу;
тип 1 и тип 2 также интерполируют ось, а тип 2 строит basis через native
D110-порядок. Все три evaluator-а принимают уже собранную матрицу владельца и
не выбирают Control row или attachment. Текущий путь для типов 0 и 1
предполагает native render context 0: `Effect.dll` проверяет общий gate
`context + 0x60 < 8`, а API этих evaluator-ов не выбирает другой context.
Evaluator типа 2 принимает индекс context явно и учитывает этот gate в `active`.
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`.
У Op3 mode 1 ось задаёт длину прямоугольника, а не его нормаль. Для
локального вектора к камере `v` и анимируемой оси `a` столбцы базиса имеют
@@ -359,27 +375,66 @@ RVA `0x93D0`, ветка mode 1 с `0x9739`, сборка столбцов `0x97
семена отдельных экземпляров лишь детерминированно заменяют process-global
состояние и не воспроизводят общий порядок вызовов.
`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 и полное визуальное совпадение размера
и формы пока не реализованы.
## Частицы Op8
`fparkan-fx::smoke` разбирает body Op8, хранит кольцевой пул команды и выдаёт
quad-ы с ориентацией к камере для активных слотов. Нативная нормализованная
фаза окна ограничивается диапазоном 0–1 и задаёт интервал проверки следующего события;
сравнение строгое (`schedule_base + interval < update_clock`). Все события
catch-up в одном обновлении проверяются по тому же нормализованному интервалу;
после события нативный код продвигает базу расписания интервалом, вычисленным
по сырой фазе эффекта, а возраст слота считает по нормализованному интервалу.
Когда фаза равна единственной границе окна, деление `0/0` даёт NaN, который
нативный unordered-путь сравнения ограничивает нулём.
фаза окна ограничивается диапазоном 0–1 и задаёт интервал проверки следующего
события; сравнение строгое (`schedule_base + interval < update_clock`). Все
события catch-up в одном обновлении проверяются по тому же нормализованному
интервалу; после события нативный код продвигает базу расписания интервалом,
вычисленным по сырой фазе эффекта, а возраст слота считает по нормализованному
интервалу. Когда фаза равна единственной границе окна, деление `0/0` даёт NaN,
который нативный unordered-путь сравнения ограничивает нулём.
Нулевая база расписания служит признаком начального состояния: обновление
сохраняет текущую опору эмиттера и базу времени, не создавая частицу. Если
первое время равно нулю, следующее обновление повторно захватывает опору. При
событии скорость опоры считается от сохранённой позиции до текущей за время
от базы расписания; слот смещает сохранённую опору на `скорость * due_interval`.
Обновление без событий не переносит сохранённую опору к текущему владельцу.
Откат часов корректирует только базу расписания до текущего времени: слоты,
опора и три потока случайных
чисел не сбрасываются.
При нулевой скорости оригинал подставляет ось X владельца и для направления,
и для этого смещения. Обновление без событий не переносит сохранённую опору к
текущему владельцу. Откат часов корректирует только базу расписания до текущего
времени: слоты, опора и три потока случайных чисел не сбрасываются.
Evaluator принимает отдельные начальные состояния для эмиттера, знаковых
отклонений и материала. Переход каждого потока совпадает с общим native LFSR,
+45
View File
@@ -95,6 +95,51 @@ translation суммируется, а rotations умножаются; посл
запускает игровой Control/AI и не обещает полного совпадения с runtime или
всеми x87 rounding modes.
## Control clock и pose компонентов
Статический Vulkan preview начинает с позы, вычисленной из сохранённых `.ctl`
bindings. Пока не задан `--static-animation-frame`, при каждом новом сэмпле
сцены приложение сэмплирует поддержанные tag 21 записи для каждого экземпляра
компонента и заново строит его MSH node hierarchy; time и transition используют
общий runtime clock. Динамический sampler принимает tag 21 mode `0`,
controller flags `5` (raw value `0xFFFFFFFF` трактуется как `5`),
разрешённый gate provider и static row mapping: оба
variable index равны `0xFFFFFFFF`, previous-row flag не установлен, transition
rate конечен и положителен.
Fallback к статической CTLD-позе всего экземпляра компонента срабатывает только
при следующих ошибках sampler: `UnsupportedTag21Mode`,
`UnsupportedVariableMapping`, `MissingGateProvider`,
`GateProviderKeyOutOfRange`, `MissingGateProviderValue` и
`GateProviderCountMismatch`. Он заменяет весь динамический результат экземпляра,
а не одну запись. Остальные sampling errors завершают обновление сцены; например,
`UnsupportedPreviousRowMapping` и `InvalidTransitionRate` не включают fallback.
`--control-clock-ms <u32>` воспроизводит общий MSH/FX clock линейно: первый из
`--frames` успешно представленных кадров получает `0`, последний — заданное
время. Replay clock привязан к числу представленных кадров: повторная подготовка
того же кадра, включая swapchain recreation, использует прежний sample.
`--static-animation-frame <u16>` отключает динамическую MSH выборку и задаёт один
frame всем компонентам. Этот override не включает AI и не превращает preview
в полный игровой Control runtime.
## Control FX и owner transform
Для выбранной Control effect group runtime берёт строки без predicate, chain и
state gate. Type 4 row offsets выбирают три точки из CTL point list; позиции
точек задают локальное начало, а их направления — локальную basis. К basis
применяются FX header scale и локальные random shifts. Затем матрица проходит
через mission rotation/translation и object scale, optional mounted-component
pose, owner-node pose и локальную матрицу эффекта. Поэтому FX mounted-компонента
следует её текущему MSH owner pose, а не остаётся привязанным к mission root.
Когда point record задаёт допустимый owner node, его pose разрешается из MSH
hierarchy. Для mounted external components node pose вычисляется относительно
component root и применяется вместе с уже рассчитанным mount pose. Internal
`INTO` child controller/equipment links пока не интерпретируются как MSH socket;
если owner pose нельзя разрешить, его FX пропускается. Нативное сочетание child
component scale, equipment attachment и CPT basis остаётся неустановленным.
## Vertex streams
Основные vertex streams имеют фиксированный source index. У type 3 `attr1`
+43 -28
View File
@@ -24,22 +24,22 @@
затенения, вызывается завершение рендерера, снимается `in_render`, затем
рассылается `end-of-render`.
При retry кадра после пересоздания swapchain сохраняется прежний pending
sample, включая уже вычисленные позы и состояния FX и материалов. Для нового
drawable обновляются aspect, проекция и геометрия, зависящая от viewport; pose
берётся из pending camera. Новые события управления камерой остаются в
input controller и применяются следующим sample. Экранные billboards погоды
повторно проецируются из закэшированных мировых головы и хвоста, без второго
шага симуляции окружения и повторной отправки звуковых событий. Пауза нулевого
drawable не добавляет время.
В интерактивном preview часы материалов и окружения прибавляют полное время
между обновлениями. Ограничение шага в 100 мс остаётся у движения свободной
камеры и не обрезает эти часы. В режиме replay окружение получает разницу
последних применённых значений общего control clock.
Pending frame хранит пакет MAT0 для мира и, при наличии GPU-окружения, отдельный
пакет неба. Пакеты содержат выбранные селектор и категорию конвейера, фазовые
коэффициенты материала, четыре UV-преобразования и кортеж фаз дополнительных
текстур. Повторное применение пакета при retry или нескольких resize не выбирает
фазу заново и не продвигает RNG. Перед отрисовкой сначала применяются материалы
мира и их порядок прозрачности, затем MAT0 неба, после чего viewport-зависимый
проход неба записывает свои переопределения.
Если swapchain просит повторить ещё не представленный кадр, preview сохраняет
его общий clock, уже вычисленные позы, FX/RNG и фазы материалов. При изменении
drawable обновляются камера, aspect и viewport, после чего sky, environment
geometry и projected shadows пересобираются для нового размера.
Положение и ориентация камеры берутся из сохранённого состояния
ожидающего кадра; новый ввод мыши применяется при следующем сэмпле. Сэмпл
погоды остаётся прежним: экранные billboards дождя и снега повторно
проецируются из закэшированных мировых головы и хвоста, без второго шага
симуляции окружения или повторной отправки звуковых событий. Следующий такт
выбирается после успешного представления кадра; пауза нулевого drawable не
добавляет время.
## Элемент отрисовки
@@ -117,6 +117,10 @@ RVA `0x13D90`. Эти данные подтверждают только пор
путь превью принимает входные данные с матрицей `selector0_words`, полученные через
[процедуру захвата нативного кадра](../../tools/native-frame-capture/README.md).
Формат захвата сохраняет локальную матрицу `selector0_words` и, когда она
доступна, записывает `effective_world_matrix_words` с маркером пространства
`world`.
Процедура захвата записывает наблюдавшийся путь миссии и значение
`atmosphere_seconds`, если проверка потока, поколения и часов прошла успешно.
В превью с фиксированной камерой эта абсолютная фаза расписания используется,
@@ -163,14 +167,26 @@ RVA `0x4EBE0` использует соглашение о вызовах `__std
события, слушателю задаются захваченные позиция, направление вперёд и верхняя
ось; начальная сортировка диапазонов мира использует ту же позицию.
Если в JSON присутствует пара `effective_world_matrix_space: "world"` и
`effective_world_matrix_words`, превью использует мировую матрицу. Матрица
`selector0_words` остаётся локальной и используется как запасной вариант только
при отсутствии мировых полей, если `parented_camera` не равно `true`. Для
родительской камеры без эффективной мировой матрицы вход отклоняется.
Захват может содержать наблюдавшийся путь миссии и проверенное значение
`atmosphere_seconds`; последнее используется только при отсутствии явного
параметра командной строки. Это значение фиксирует фазу расписания атмосферы
при сравнении с неподвижной камерой, тогда как FX продолжает получать реальное
при сравнении с неподвижной камерой, тогда как FX по умолчанию получает реальное
время кадра. `simulation_time_seconds`, погода, RNG и полное состояние миссии
остаются неизвестными, поэтому совпадение камеры и фазы не подтверждает
визуального совпадения с нативным кадром.
Параметр `--control-clock-ms` воспроизводит единые часы MSH/FX от нуля до
выбранного значения в миллисекундах за указанное число кадров. Без этого
параметра часы идут в реальном времени. Для `--readback-sequence-out` этот
управляемый режим обязателен. Он не восстанавливает
`simulation_time_seconds`, погоду, RNG или полное состояние миссии.
Для неба превью вычисляет направление как `atan2(forward.x, forward.y)`. Оно
совпадает со вторым выходным значением, вычисляемым в `Terrain.dll` по RVA
`0x4F9C0`; при выборе сектора экранного градиента `CSky` добавляет π по RVA
@@ -183,15 +199,9 @@ RVA `0x4EBE0` использует соглашение о вызовах `__std
`1.3` рад — параметры, наблюдавшиеся в проверенном контексте GOG Ngi32
([формула native projection](../tomes/05-render.md#камера-и-размещение-в-оригинале)).
Для отношения ширины к высоте `a` превью вычисляет вертикальный угол
```rust
fn vertical_fov(self, aspect: f32) -> f32 {
2.0 * ((self.horizontal_fov * 0.5).tan() / aspect).atan()
}
```
Код заменяет невалидный `aspect` на отношение 16:9; горизонтальный угол
остаётся `1.3` рад после изменения размера окна, а новый viewport и матрица
камеры применяются при следующей отрисовке.
`2 * atan(tan(horizontal_fov / 2) / a)`. Поэтому горизонтальный угол остаётся
`1.3` рад после изменения размера окна; новый viewport и матрица камеры
применяются при следующей отрисовке.
Проекция направления не сравнивает косинус угла с ближней дистанцией: она
отбрасывает только лучи за камерой и почти параллельные плоскости изображения
@@ -254,6 +264,11 @@ D3D7 в `fparkan-render`, а затем один раз меняет знак в
- переход на запасной набор возможностей устройства;
- квантование времени симуляции.
Для отладки требуется детерминированный захват кадра: состояние камеры,
идентификаторы видимых объектов, список элементов отрисовки, ключи конвейера,
матрицы и хэши промежуточных буферов.
Параметр `--readback-sequence-out` сохраняет необработанные байты каждого
отправленного кадра; в имя файла записываются индекс кадра, значение общих
управляемых часов и размер изображения. Байты соответствуют выбранному формату
изображения swapchain Vulkan и не гарантируют порядок каналов R, G, B, A. Числовое
формат выводится как `readback_format`; расширение `.rgba` само по себе порядок
каналов не определяет. Последовательность не содержит структурированных ID
видимых объектов, списка элементов отрисовки, ключей конвейера, матриц или
хэшей промежуточных буферов.