1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
|
# Кадр отрисовки
Кадр завершает цикл и не является отдельной операцией рендерера. До вызовов
отрисовки собирается ввод, вычисляется такт, применяются отложенные операции,
выбирается камера и обновляется слушатель 3D-звука.
## Порядок кадра
```text
системные сообщения и ввод
-> расчёт симуляции
-> отложенные операции над объектами
-> анимация и преобразования
-> камера и звуковой слушатель
-> видимость и очереди отрисовки
-> материалы и проходы отрисовки
-> завершение рендерера
-> обратные вызовы после отрисовки и UI
```
По анализу вызовов установлен общий порядок в `World3D::stdRenderGame`: камера
передаётся в Terrain, задаются `viewport` и матрицы, вызываются граничные слоты
рендерера, выставляется `in_render`, обходится мир, завершаются проходы мира и
затенения, вызывается завершение рендерера, снимается `in_render`, затем
рассылается `end-of-render`.
## Элемент отрисовки
Подготовленный элемент отрисовки содержит:
- мировую матрицу узла;
- флаги пакета `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 и атмосферы;
- переход на запасной набор возможностей устройства;
- квантование времени симуляции.
Для отладки требуется детерминированный захват кадра: состояние камеры,
идентификаторы видимых объектов, список элементов отрисовки, ключи конвейера,
матрицы и хэши промежуточных буферов.
|