aboutsummaryrefslogtreecommitdiff
path: root/docs/reference
diff options
context:
space:
mode:
Diffstat (limited to 'docs/reference')
-rw-r--r--docs/reference/atmosphere.md40
-rw-r--r--docs/reference/environment-effects.md155
-rw-r--r--docs/reference/msh.md45
-rw-r--r--docs/reference/render-frame.md162
4 files changed, 303 insertions, 99 deletions
diff --git a/docs/reference/atmosphere.md b/docs/reference/atmosphere.md
index 65ed176..8d44e4e 100644
--- a/docs/reference/atmosphere.md
+++ b/docs/reference/atmosphere.md
@@ -50,7 +50,25 @@ key_time = floor(duration * (key_hour * 60 + key_minute) * 60 / 86400)
theta = (clamp((now - birth_start) / lifetime, 0, 1) * 1.2 - 0.1) * pi
```
-Затем к нему применяется матрица рождения. У солнца и луны независимые
+Затем к нему применяется матрица рождения как к вектору-столбцу. Если `A` и
+`B` — углы рождения, а `x = cos(theta)` и `z = -sin(theta)`, результат равен
+
+```text
+[x*cos(A) + z*sin(A)*sin(B),
+ x*sin(A) - z*cos(A)*sin(B),
+ z*cos(B)]
+```
+
+Порядок важен для всего освещения: транспонированное умножение переносит
+солнце на другую сторону мира и меняет освещённые склоны, блики и тени.
+В GOG `Terrain.dll` (SHA-256
+`af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead`)
+матрицу строит RVA `0x4A550`, а вектор преобразует `CSun::Update`, RVA
+`0x4B570`. Живой замер после 282184 мс при длительности 525000 мс и углах
+`[90, 30]` дал направление `[-0.4950135, -0.1408745, -0.8573891]`;
+Rust-тест проверяет этот результат с допуском `3e-6`.
+
+У солнца и луны независимые
интервалы и независимые направления: при отсутствии активной луны нельзя
подменять её направление противоположностью солнца. `SunFrame::color` — это
единственный primary directional RGB. Второй источник получает направление
@@ -167,21 +185,27 @@ RGB palette 12 с минимумом `80/255` на канал и alpha `150/255`
## Туман и свет
-Нативное обновление `CSky` записывает две дальности и один packed RGB.
+Нативное обновление `CSky` записывает две дальности и нижнюю границу освещения.
`FogFrame` повторяет этот контракт:
```text
fog.start = sky.values[0] * 700
fog.end = sky.values[1] * 700
-fog.color = RGB(sky.packed[1]) / 255
+lighting_floor = RGB(sky.packed[1]) / 255
```
Поля не скрывают порядок исходных параметров: значения сохраняются в
-`SkySample`, а преобразование выполняется только в `fog_frame`. Vulkan path
-передаёт цвет и дальности в frame uniforms; vertex shader вычисляет линейный
-коэффициент по расстоянию до камеры, а fragment shader смешивает RGB материала
-с fog color в ветвях combiner, которые используют туман. Конкретный draw state
-может обходить это смешивание.
+`SkySample`, а преобразование выполняется только в `fog_frame`. Нижняя граница
+освещения ограничивает результат расчёта света снизу; она не задаёт цвет тумана.
+Обычный цвет тумана выбирается из `sky.colors[0..4]` по направлению камеры,
+как цвет горизонта. Это подтверждено в `CSky::Render` и живым совпадением
+его значения с `D3DRENDERSTATE_FOGCOLOR` в том же игровом тике.
+
+Vulkan path передаёт эти параметры раздельно. Vertex shader вычисляет
+коэффициент по квадратам расстояния и границ, а fragment shader смешивает
+RGB материала с цветом тумана. Конкретный native draw state может обходить
+смешивание или временно менять его цвет, например на чёрный для отдельного
+прохода. Полный выбор таких проходов ещё не воспроизведён.
Цвет primary света солнца — RGB palette 12, нормированный делением на 255 и
умноженный на trailing parameter 2. `SunFrame::direction` — нормированный
diff --git a/docs/reference/environment-effects.md b/docs/reference/environment-effects.md
index 799a969..45d7725 100644
--- a/docs/reference/environment-effects.md
+++ b/docs/reference/environment-effects.md
@@ -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`.
-
-## Геометрия визуальных FX-команд
-
-`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.
-
-Evaluator получает готовые command body, phase, camera point и внешнюю
-матрицу. Выбор Control row, manager gates, owner attachment, header jitter и
-global RNG остаются задачей владельца. `fparkan-game` пока использует для Op9
-категорию 0 с наибольшей детализацией; выбор категории по projected size ещё
-не реализован.
-
-Для 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`.
+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`.
У 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,
diff --git a/docs/reference/msh.md b/docs/reference/msh.md
index 0a8a2cb..130a203 100644
--- a/docs/reference/msh.md
+++ b/docs/reference/msh.md
@@ -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`
diff --git a/docs/reference/render-frame.md b/docs/reference/render-frame.md
index 10de209..19ccf3e 100644
--- a/docs/reference/render-frame.md
+++ b/docs/reference/render-frame.md
@@ -24,22 +24,91 @@
затенения, вызывается завершение рендерера, снимается `in_render`, затем
рассылается `end-of-render`.
-При retry кадра после пересоздания swapchain сохраняется прежний pending
-sample, включая уже вычисленные позы и состояния FX и материалов. Для нового
-drawable обновляются aspect, проекция и геометрия, зависящая от viewport; pose
-берётся из pending camera. Новые события управления камерой остаются в
-input controller и применяются следующим sample. Экранные billboards погоды
-повторно проецируются из закэшированных мировых головы и хвоста, без второго
-шага симуляции окружения и повторной отправки звуковых событий. Пауза нулевого
-drawable не добавляет время.
-
-Pending frame хранит пакет MAT0 для мира и, при наличии GPU-окружения, отдельный
-пакет неба. Пакеты содержат выбранные селектор и категорию конвейера, фазовые
-коэффициенты материала, четыре UV-преобразования и кортеж фаз дополнительных
-текстур. Повторное применение пакета при retry или нескольких resize не выбирает
-фазу заново и не продвигает RNG. Перед отрисовкой сначала применяются материалы
-мира и их порядок прозрачности, затем MAT0 неба, после чего viewport-зависимый
-проход неба записывает свои переопределения.
+В интерактивном preview часы материалов и окружения прибавляют полное время
+между обновлениями. Ограничение шага в 100 мс остаётся у движения свободной
+камеры и не обрезает эти часы. В режиме replay окружение получает разницу
+последних применённых значений общего control clock.
+
+Если swapchain просит повторить ещё не представленный кадр, preview сохраняет
+его общий clock, уже вычисленные позы, FX/RNG и фазы материалов. При изменении
+drawable обновляются камера, aspect и viewport, после чего sky, environment
+geometry и projected shadows пересобираются для нового размера.
+Положение и ориентация камеры берутся из сохранённого состояния
+ожидающего кадра; новый ввод мыши применяется при следующем сэмпле. Сэмпл
+погоды остаётся прежним: экранные billboards дождя и снега повторно
+проецируются из закэшированных мировых головы и хвоста, без второго шага
+симуляции окружения или повторной отправки звуковых событий. Следующий такт
+выбирается после успешного представления кадра; пауза нулевого drawable не
+добавляет время.
+
+## Прозрачность порталов MSH
+
+GOG `Terrain.dll` (SHA-256
+`af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead`),
+RVA `0x1B0F0`, заменяет diffuse alpha пакета без type 16 с batch bit `0x0008`.
+При `0x0010` аргумент кривой — `sqrt(distance)`, границы — `2.6 / fov` и
+`7.7999997 / fov`, наклон — `fov / (7.7999997 - 2.6)`. Без этого бита
+`0x0040` задаёт ноль; иначе границы — `75 * 1.3 / fov`, `95 * 1.3 / fov`,
+наклон — `fov / (95 * 1.3 - 75 * 1.3)`. До ближней границы alpha равна нулю,
+после дальней — единице. Внутри перехода alpha равна произведению смещения от
+ближней границы на наклон. Это замена alpha MAT0, а не дополнительный множитель.
+
+Метод расстояния `0x246A0` через `0x247B0` читает первую индексированную
+вершину пакета, преобразует её в мир и вычисляет квадрат расстояния до камеры.
+Текущий Ngi32 dispatch `FastProc+0x2C` указывает на RVA `0x1D6D0`:
+`RSQRTSS` возвращает приближение обратного корня, которое умножается на квадрат
+расстояния. Получается приближённое евклидово расстояние; дополнительный `FSQRT`
+ветви `0x0010` выполняется уже над ним. Хеш Ngi32 —
+`bab9840d94f4e4e74ffc26677724fa896cf4823845504d09a9e025f80016edf5`.
+Скаляр камеры в шестом float getter `0x4F5F0` и поле CShade `+0xBB4`
+проверены в работающем оригинале: оба содержат текущий горизонтальный FOV.
+
+`legacy_msh_portal_alpha` хранит кривую в `fparkan-render`. Приложение пока
+берёт первую индексированную вершину своего диапазона и обычный `sqrt`,
+поэтому выбор вершины после native CPU clipping и точность `RSQRTSS` не
+воспроизводятся. Счётчик `render_coverage portal_fade_packet_selection_approx`
+показывает число таких диапазонов. Поток type 16 сохраняет прежний отдельный
+путь и ещё не декодирован. Это ограничивает приёмку на частично отсечённых
+порталах и не позволяет считать интерьер принятой эталонной сценой.
+
+## Дистанционная кривая тумана
+
+Программный `CShade::ShadeIndexedStrided` в GOG Terrain.dll сравнивает квадрат
+дистанции с квадратами ближней и дальней границ. По RVA `0x2EEED–0x2EF45`
+видимость равна `1 - (distance² - near²) / (far² - near²)` между границами,
+единице перед ближней и нулю за дальней. Такой же закон использует отдельный
+путь цвета пакета `0x2F230`. Линейная интерполяция по самой дистанции давала
+слишком плотный туман в середине диапазона.
+
+В работающем оригинале границы 0 и `0x442389E6`, квадрат дистанции
+`0x47707B04` дали видимость `0x3F5B2B94`. Rust-тест проверяет передаваемые GPU
+квадраты границ по этому наблюдению. GLSL вычисляет квадрат расстояния вершины,
+после чего растеризатор интерполирует коэффициент по треугольнику.
+`native_fog.effective.sink_rgb` совпадает с `sky.packed[1]`, нижней границей
+освещения CShade. Обычный GPU fog color подтверждён отдельно: `CSky::Render`
+по RVA `0x48341` задаёт renderer default из цвета горизонта. В живом замере
+`0xFF404064` дошёл до `IDirect3DDevice7::SetRenderState(34)` в том же игровом
+тике. В другом ракурсе палитра `[0xFFA8A5CE, 0xFFABA9D5, 0xFFBAA9D5,
+0xFFABA9D5]` дала default `0xFFA9A7D1`. Это не покрывает временные изменения
+цвета отдельных проходов: например, native render path устанавливает чёрный
+цвет и затем восстанавливает сохранённый.
+Выбор проходов по native shade-state пока приближён общим туманом мира и
+считается как `render_coverage fog_pass_eligibility_approx`.
+
+## Радиус Control Op1
+
+GOG `Effect.dll` (SHA-256
+`386a3e8db1ce23f61957825d633c7dae1ed40c25cab0cf10a534385fd7cee789`),
+RVA `0xF6E0`, преобразует направление текущей матрицей, сохраняет его длину
+перед нормализацией и умножает на неё интерполированный локальный радиус.
+Умножение видно по RVA `0xFB13`; перед LightManager `+0x10` неположительный
+результат заменяется на `0.01`. В живом «Конструкторе» для синего источника
+завода локальный радиус 7 и длина `0x409B1F91` дали радиус `0x4207BB9F`.
+Это же значение проверяется Rust-тестом общего Op1 evaluator.
+
+Случайные добавки цвета и радиуса ещё не воспроизводятся; приложение сообщает
+число таких Control-команд как `control_op1_jitter_unsupported` и использует
+их авторские значения. У синего источника в проверенном захвате добавки нулевые.
## Элемент отрисовки
@@ -69,21 +138,17 @@ Pending frame хранит пакет MAT0 для мира и, при налич
глубину. За фиксированными диапазонами окружения сохраняются их явные состояния.
Для MSH без потока цветов type 16 диапазон, цвет которого вычисляет CPU, при
-raw-флаге `Batch20` `0x0008` получает переопределение alpha только в одном
-подтверждённом случае. В GOG `Terrain.dll` с SHA-256
-`af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead` проверка
-по RVA `0x1B0F0` задаёт alpha `0`, когда установлен bit `0x0040` и сброшен
-`0x0010`; поэтому для `0x0148` alpha равна нулю. Для `0x0108` и `0x0118`
-превью сохраняет alpha MAT0; для `0x0058` действует приоритет bit `0x0010`,
-также оставляя alpha MAT0. При наличии type 16 или отсутствии `0x0008`
-переопределения нет. Оно относится к одному draw range и не меняет общий
+raw-флаге `Batch20` `0x0008` получает переопределение alpha по
+[дистанционной кривой портала](#прозрачность-порталов-msh). Bit `0x0010`
+имеет приоритет; иначе bit `0x0040` задаёт нулевую alpha, а оставшаяся
+ветвь использует границы 75/95 с поправкой на FOV. При наличии type 16 или
+отсутствии `0x0008` переопределения нет. Оно относится к одному draw range и не меняет общий
материал; alpha `0` временно задаёт число индексов `0`, а при очистке
переопределения исходное число индексов восстанавливается.
-Общий fade по расстоянию пока не реализован. Открыты точная метрика расстояния,
-выбор первого CPU-принятого треугольника диапазона и эквивалентность frustum- и
-culling-отбору оригинала. Поэтому состояния `0x0108` и `0x0118` оставляют alpha
-MAT0, пока эти входные данные не установлены.
+Fade обновляется с камерой каждый кадр. Открыт выбор первой принятой вершины
+после native CPU clipping: приложение пока берёт первую индексированную
+вершину диапазона и считает это приближение в `render_coverage`.
В превью с фиксированной камерой диапазоны туманности и звёзд, купола и облаков
явно помечаются как прозрачный фон. Непрозрачные диапазоны мира остаются на
@@ -117,6 +182,10 @@ RVA `0x13D90`. Эти данные подтверждают только порÑ
путь превью принимает входные данные с матрицей `selector0_words`, полученные через
[процедуру захвата нативного кадра](../../tools/native-frame-capture/README.md).
+Формат захвата сохраняет локальную матрицу `selector0_words` и, когда она
+доступна, записывает `effective_world_matrix_words` с маркером пространства
+`world`.
+
Процедура захвата записывает наблюдавшийся путь миссии и значение
`atmosphere_seconds`, если проверка потока, поколения и часов прошла успешно.
В превью с фиксированной камерой эта абсолютная фаза расписания используется,
@@ -163,14 +232,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 +264,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 +329,11 @@ D3D7 в `fparkan-render`, а затем один раз меняет знак в
- переход на запасной набор возможностей устройства;
- квантование времени симуляции.
-Для отладки требуется детерминированный захват кадра: состояние камеры,
-идентификаторы видимых объектов, список элементов отрисовки, ключи конвейера,
-матрицы и хэши промежуточных буферов.
+Параметр `--readback-sequence-out` сохраняет необработанные байты каждого
+отправленного кадра; в имя файла записываются индекс кадра, значение общих
+управляемых часов и размер изображения. Байты соответствуют выбранному формату
+изображения swapchain Vulkan и не гарантируют порядок каналов R, G, B, A. Числовое
+формат выводится как `readback_format`; расширение `.rgba` само по себе порядок
+каналов не определяет. Последовательность не содержит структурированных ID
+видимых объектов, списка элементов отрисовки, ключей конвейера, матриц или
+хэшей промежуточных буферов.