208 lines
18 KiB
Markdown
208 lines
18 KiB
Markdown
# Кадр отрисовки
|
||
|
||
Кадр завершает цикл и не является отдельной операцией рендерера. До вызовов
|
||
отрисовки собирается ввод, вычисляется такт, применяются отложенные операции,
|
||
выбирается камера и обновляется слушатель 3D-звука.
|
||
|
||
## Порядок кадра
|
||
|
||
```text
|
||
системные сообщения и ввод
|
||
-> расчёт симуляции
|
||
-> отложенные операции над объектами
|
||
-> анимация и преобразования
|
||
-> камера и звуковой слушатель
|
||
-> видимость и очереди отрисовки
|
||
-> материалы и проходы отрисовки
|
||
-> завершение рендерера
|
||
-> обратные вызовы после отрисовки и UI
|
||
```
|
||
|
||
По анализу вызовов установлен общий порядок в `World3D::stdRenderGame`: камера
|
||
передаётся в Terrain, задаются `viewport` и матрицы, вызываются граничные слоты
|
||
рендерера, выставляется `in_render`, обходится мир, завершаются проходы мира и
|
||
затенения, вызывается завершение рендерера, снимается `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-зависимый
|
||
проход неба записывает свои переопределения.
|
||
|
||
## Элемент отрисовки
|
||
|
||
Подготовленный элемент отрисовки содержит:
|
||
|
||
- мировую матрицу узла;
|
||
- флаги пакета `batch` и диапазон индексов;
|
||
- дескриптор материала WEAR;
|
||
- активную фазу и коэффициенты MAT0;
|
||
- дескриптор текстуры;
|
||
- необязательный дескриптор карты освещения;
|
||
- фазу отрисовки и ключ сортировки;
|
||
- состояние конвейера устаревшей отрисовки.
|
||
|
||
Подготовленный элемент должен ссылаться на неизменяемые данные кадра. Изменение
|
||
фазы или кэша текстур посреди прохода не должно менять уже собранную очередь.
|
||
|
||
## Состояние MAT0 и прозрачные фоны
|
||
|
||
Для диапазонов мира миссии выбранный материал MAT0 задаёт нативную категорию
|
||
конвейера. Категория определяет смешивание и альфа-тест, сохраняя настройки
|
||
проверки глубины и отсечения граней. Если селектор материала анимируется,
|
||
вместе с ним обновляется и категория.
|
||
Принадлежность к очереди определяется отдельно: прозрачным считается диапазон
|
||
мира с выбранной прозрачностью меньше единицы или с нативными флагами пакета
|
||
`batch` `0x108`. Такой диапазон при включённой проверке глубины не записывает
|
||
глубину. За фиксированными диапазонами окружения сохраняются их явные состояния.
|
||
|
||
В превью с фиксированной камерой диапазоны туманности и звёзд, купола и облаков
|
||
явно помечаются как прозрачный фон. Непрозрачные диапазоны мира остаются на
|
||
своих местах; прозрачная очередь мира сортируется от дальних элементов к
|
||
ближним и вставляется сразу после последнего помеченного фона. Порядок остальных
|
||
фиксированных диапазонов сохраняется. Поэтому облака могут отрисовываться перед
|
||
прозрачной листвой, а расположенные позднее солнце, погода и другие передние
|
||
эффекты сохраняют свой относительный порядок. Это локальное правило очереди, а
|
||
не перестановка всех проходов неба или эффектов.
|
||
|
||
## Проверки
|
||
|
||
`cargo test --locked --offline -p fparkan-render-vulkan --lib material_category_and_transparency_preserve_world_state_in_either_order`
|
||
Проверяет, что категория 1 сохраняет смешивание `source-alpha` и альфа-тест,
|
||
сохраняет настройки проверки глубины и отсечения граней при любом порядке
|
||
обновления и отклоняет неподдерживаемые категории.
|
||
`cargo test --locked --offline -p fparkan-render-vulkan --lib tree_canopy_queue_follows_sky_and_precedes_foreground_effects`
|
||
Проверяет заданный порядок очереди: градиент экрана, непрозрачная местность и
|
||
здание, тень, туманность, купол и облака, прозрачная крона от дальних элементов
|
||
к ближним, затем солнце и погода.
|
||
|
||
## Свидетельства диспетчеризации камеры GOG
|
||
|
||
Для версии GOG `World3D.dll` с SHA-256
|
||
`17e4a3089b2583a8cf2356c9db0390b1aba138356a09130d79b4e7e4791da61e`,
|
||
экспорт `stdRenderGame` находится по RVA `0x13BD0`. Вначале он вызывает
|
||
`Terrain::stdSetCurrentCamera2(camera)`, сохраняет указатель камеры на время
|
||
кадра и очищает его перед возвратом. `sendEndOfRender` — отдельный экспорт по
|
||
RVA `0x13D90`. Эти данные подтверждают только порядок вызовов в кадре: анализ
|
||
не восстанавливает ABI камеры, матрицы или значения `viewport`. Реализованный
|
||
путь превью принимает входные данные с матрицей `selector0_words`, полученные через
|
||
[процедуру захвата нативного кадра](../../tools/native-frame-capture/README.md).
|
||
|
||
Процедура захвата записывает наблюдавшийся путь миссии и значение
|
||
`atmosphere_seconds`, если проверка потока, поколения и часов прошла успешно.
|
||
В превью с фиксированной камерой эта абсолютная фаза расписания используется,
|
||
если не задан параметр `--atmosphere-seconds`; на всех кадрах чтения сохраняется
|
||
та же фаза. Явный параметр командной строки имеет приоритет. Эта фаза не равна
|
||
времени полной симуляции: погода, RNG и другие состояния миссии остаются
|
||
неизвестны, поэтому она не подтверждает полного совпадения кадра.
|
||
|
||
Анализ принимающей стороны уточняет этот контракт. `stdSetCurrentCamera2`
|
||
(RVA `0x4FD40` в Terrain с SHA-256
|
||
`af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead`)
|
||
запрашивает селектор `18` у переданного объекта и вызывает слот `+12` полученного
|
||
интерфейса. Указатель не сохраняется в `stdGetCurrentCamera2`: этот геттер
|
||
возвращает глобальное значение Terrain, установленное селектором `8` при
|
||
инициализации Terrain.
|
||
|
||
`Terrain.dll!LoadCamera` задаёт отдельный контракт фабрики. Точка входа GOG по
|
||
RVA `0x4EBE0` использует соглашение о вызовах `__stdcall` и принимает четыре
|
||
машинных слова, выделяет `0x1A4` байта, вызывает конструктор по RVA `0x4EC60` и
|
||
возвращает интерфейс по смещению объекта `+0x134`. Конструктор передаёт
|
||
аргументы 3 и 4 в инициализацию базового объекта по `this + 4`, записывает
|
||
аргумент 3 в `this + 0x138` и вызывает вспомогательную функцию по RVA `0x4CEF0`,
|
||
передавая ей аргументы 1, 2 и 4 вместе с `this`. Аргумент 1 — строка режима:
|
||
ненулевое значение по `this + 0x1A0` дают только `REFLECTION` и
|
||
`REFLECTION_SHIFTED`. Это свидетельство ABI фабрики, а не восстановленный
|
||
контракт матрицы вида или проекции.
|
||
|
||
## Предпросмотр по захвату камеры D3D7
|
||
|
||
`fparkan-game --legacy-camera-capture <json>` принимает матрицу камеры
|
||
`selector0_words`, прямоугольник области отображения `viewport` D3D7
|
||
`[left, top, right, bottom]`, ближнюю и дальнюю плоскости и угол поля зрения.
|
||
По этим данным превью строит матрицы вида и проекции D3D7, передаёт полученную
|
||
матрицу `clip_from_world` камере превью и напрямую использует оси камеры из
|
||
матрицы вида. Поэтому базис захвата, включая крен, не сводится к рысканию и
|
||
тангажу. Прямоугольник задаёт физический размер области отрисовки:
|
||
`right - left` на `bottom - top`. Размер окна превью закреплён: если при
|
||
запуске или после ненулевого изменения окна физический размер не совпадает с
|
||
заданным, приложение завершает работу, а не растягивает изображение.
|
||
Сравнивайте результат с фрагментом исходного PNG по прямоугольнику
|
||
`[left, top, right, bottom]`. В этом режиме `FreeFlightCamera` не создаётся,
|
||
но при повторной отрисовке обновляются окружение и проецируемые тени для
|
||
захваченной камеры. До обновления окружения, которое может создавать звуковые
|
||
события, слушателю задаются захваченные позиция, направление вперёд и верхняя
|
||
ось; начальная сортировка диапазонов мира использует ту же позицию.
|
||
|
||
Захват может содержать наблюдавшийся путь миссии и проверенное значение
|
||
`atmosphere_seconds`; последнее используется только при отсутствии явного
|
||
параметра командной строки. Это значение фиксирует фазу расписания атмосферы
|
||
при сравнении с неподвижной камерой, тогда как FX продолжает получать реальное
|
||
время кадра. `simulation_time_seconds`, погода, RNG и полное состояние миссии
|
||
остаются неизвестными, поэтому совпадение камеры и фазы не подтверждает
|
||
визуального совпадения с нативным кадром.
|
||
|
||
Для неба превью вычисляет направление как `atan2(forward.x, forward.y)`. Оно
|
||
совпадает со вторым выходным значением, вычисляемым в `Terrain.dll` по RVA
|
||
`0x4F9C0`; при выборе сектора экранного градиента `CSky` добавляет π по RVA
|
||
`0x481C5`. Это преобразование применяется только на границе неба и не меняет
|
||
рыскание или управление движением `FreeFlightCamera`.
|
||
|
||
## Камера свободного полёта
|
||
|
||
Начальный профиль использует `near = 0.5`, `far = 700` и горизонтальный FOV
|
||
`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 и матрица
|
||
камеры применяются при следующей отрисовке.
|
||
|
||
Проекция направления не сравнивает косинус угла с ближней дистанцией: она
|
||
отбрасывает только лучи за камерой и почти параллельные плоскости изображения
|
||
(`forward · direction <= 1e-4`). Лучи в передней полусфере могут проецироваться
|
||
за край viewport; владелец спрайта отдельно решает, нужно ли его рисовать.
|
||
|
||
## Ограничения точного совпадения
|
||
|
||
`VulkanStaticCamera::from_legacy_d3d7` выполняет восстановление вида и проекции
|
||
D3D7 в `fparkan-render`, а затем один раз меняет знак вертикального члена
|
||
проекции на границе адаптера Vulkan. В области отображения D3D7 положительная
|
||
координата NDC Y направлена вверх экрана; Vulkan использует положительную высоту
|
||
области отображения, поэтому адаптер меняет знак NDC Y, а шейдер сохраняет
|
||
`gl_Position`. Общий путь
|
||
`from_row_major_view_projection` не меняется: вызывающие его функции передают
|
||
матрицы, уже подготовленные для Vulkan, как и камера свободного полёта.
|
||
|
||
- точность и округление x87;
|
||
- различия scalar/SIMD в `g_FastProc`;
|
||
- порядок объектов, пакетов и прозрачных примитивов;
|
||
- переходы состояний проверки глубины, отсечения граней, альфа-теста и смешивания;
|
||
- пропуск mip-уровней, палитра и координаты Page;
|
||
- запасной материал и выбор фазы;
|
||
- последовательность RNG для FX и атмосферы;
|
||
- переход на запасной набор возможностей устройства;
|
||
- квантование времени симуляции.
|
||
|
||
Для отладки требуется детерминированный захват кадра: состояние камеры,
|
||
идентификаторы видимых объектов, список элементов отрисовки, ключи конвейера,
|
||
матрицы и хэши промежуточных буферов.
|