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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
|
# Кадр отрисовки
Кадр завершает цикл и не является отдельной операцией рендерера. До вызовов
отрисовки собирается ввод, вычисляется такт, применяются отложенные операции,
выбирается камера и обновляется слушатель 3D-звука.
## Порядок кадра
```text
системные сообщения и ввод
-> расчёт симуляции
-> отложенные операции над объектами
-> анимация и преобразования
-> камера и звуковой слушатель
-> видимость и очереди отрисовки
-> материалы и проходы отрисовки
-> завершение рендерера
-> обратные вызовы после отрисовки и UI
```
По анализу вызовов установлен общий порядок в `World3D::stdRenderGame`: камера
передаётся в Terrain, задаются `viewport` и матрицы, вызываются граничные слоты
рендерера, выставляется `in_render`, обходится мир, завершаются проходы мира и
затенения, вызывается завершение рендерера, снимается `in_render`, затем
рассылается `end-of-render`.
В интерактивном 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. Приложение пока
сохраняет цвет экранного горизонта; выбор цвета и проходов требует отдельной
проверки GPU-состояния.
Выбор проходов по 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` и использует
их авторские значения. У синего источника в проверенном захвате добавки нулевые.
## Элемент отрисовки
Подготовленный элемент отрисовки содержит:
- мировую матрицу узла;
- флаги пакета `batch` и диапазон индексов;
- дескриптор материала WEAR;
- активную фазу и коэффициенты MAT0;
- дескриптор текстуры;
- необязательный дескриптор карты освещения;
- фазу отрисовки и ключ сортировки;
- состояние конвейера устаревшей отрисовки.
Подготовленный элемент должен ссылаться на неизменяемые данные кадра. Изменение
фазы или кэша текстур посреди прохода не должно менять уже собранную очередь.
## Состояние MAT0 и прозрачные фоны
Для диапазонов мира миссии выбранный материал MAT0 задаёт нативную категорию
конвейера. Категория определяет смешивание и альфа-тест, сохраняя настройки
проверки глубины и отсечения граней. Если селектор материала анимируется,
вместе с ним обновляется и категория.
Принадлежность к очереди определяется отдельно: прозрачным считается диапазон
мира с выбранной прозрачностью меньше единицы или с нативными флагами пакета
`batch` `0x108`. Такой диапазон при включённой проверке глубины не записывает
глубину. За фиксированными диапазонами окружения сохраняются их явные состояния.
Для MSH без потока цветов type 16 диапазон, цвет которого вычисляет CPU, при
raw-флаге `Batch20` `0x0008` получает переопределение alpha по
[дистанционной кривой портала](#прозрачность-порталов-msh). Bit `0x0010`
имеет приоритет; иначе bit `0x0040` задаёт нулевую alpha, а оставшаяся
ветвь использует границы 75/95 с поправкой на FOV. При наличии type 16 или
отсутствии `0x0008` переопределения нет. Оно относится к одному draw range и не меняет общий
материал; alpha `0` временно задаёт число индексов `0`, а при очистке
переопределения исходное число индексов восстанавливается.
Fade обновляется с камерой каждый кадр. Открыт выбор первой принятой вершины
после native CPU clipping: приложение пока берёт первую индексированную
вершину диапазона и считает это приближение в `render_coverage`.
В превью с фиксированной камерой диапазоны туманности и звёзд, купола и облаков
явно помечаются как прозрачный фон. Непрозрачные диапазоны мира остаются на
своих местах; прозрачная очередь мира сортируется от дальних элементов к
ближним и вставляется сразу после последнего помеченного фона. Порядок остальных
фиксированных диапазонов сохраняется. Поэтому облака могут отрисовываться перед
прозрачной листвой, а расположенные позднее солнце, погода и другие передние
эффекты сохраняют свой относительный порядок. Это локальное правило очереди, а
не перестановка всех проходов неба или эффектов.
## Проверки
`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).
Формат захвата сохраняет локальную матрицу `selector0_words` и, когда она
доступна, записывает `effective_world_matrix_words` с маркером пространства
`world`.
Процедура захвата записывает наблюдавшийся путь миссии и значение
`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` не создаётся,
но при повторной отрисовке обновляются окружение и проецируемые тени для
захваченной камеры. До обновления окружения, которое может создавать звуковые
события, слушателю задаются захваченные позиция, направление вперёд и верхняя
ось; начальная сортировка диапазонов мира использует ту же позицию.
Если в JSON присутствует пара `effective_world_matrix_space: "world"` и
`effective_world_matrix_words`, превью использует мировую матрицу. Матрица
`selector0_words` остаётся локальной и используется как запасной вариант только
при отсутствии мировых полей, если `parented_camera` не равно `true`. Для
родительской камеры без эффективной мировой матрицы вход отклоняется.
Захват может содержать наблюдавшийся путь миссии и проверенное значение
`atmosphere_seconds`; последнее используется только при отсутствии явного
параметра командной строки. Это значение фиксирует фазу расписания атмосферы
при сравнении с неподвижной камерой, тогда как 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
`0x481C5`. Это преобразование применяется только на границе неба и не меняет
рыскание или управление движением `FreeFlightCamera`.
## Камера свободного полёта
Начальный профиль использует `near = 0.5`, `far = 700` и горизонтальный FOV
`1.3` рад — параметры, наблюдавшиеся в проверенном контексте GOG Ngi32
([формула native projection](../tomes/05-render.md#камера-и-размещение-в-оригинале)).
Для отношения ширины к высоте `a` превью вычисляет вертикальный угол
`2 * atan(tan(horizontal_fov / 2) / a)`. Поэтому горизонтальный угол остаётся
`1.3` рад после изменения размера окна; новый viewport и матрица камеры
применяются при следующей отрисовке.
Проекция направления не сравнивает косинус угла с ближней дистанцией: она
отбрасывает только лучи за камерой и почти параллельные плоскости изображения
(`forward · direction <= 1e-4`). Лучи в передней полусфере могут проецироваться
за край viewport; владелец спрайта отдельно решает, нужно ли его рисовать.
После движения свободная камера ограничивает X и Y прямоугольником
поверхностей карты, затем поднимается над найденным рельефом на заданный
зазор. Границы берутся из корня пространственного индекса `TerrainWorld`,
поэтому неиспользуемые вершины файла их не расширяют и повторного обхода
геометрии в каждом кадре нет:
```rust
position[axis] = position[axis].clamp(min[axis], max[axis]); // axis = 0, 1
position[2] = position[2].max(ground_height + ground_clearance);
```
Для каждого отрисовываемого корневого объекта класса `FORT` и каждого его
отрисовываемого компонента интерактивный путь создаёт отдельную запись проверки.
Она использует те же текущие позы узлов и преобразования присоединённых
компонентов, что и геометрия кадра. После ограничения целевой позиции по
TerrainWorld камера проверяет непрерывный отрезок от прежней позиции до цели.
Если после нативных MSH-фильтров остаётся пересечение, позиция возвращается к
началу шага, а рассчитанный из ввода новый поворот сохраняется. Маски batch и
type-7, а также продолжение отрезка по helper-link описаны в
[разделе MSH о сегментном запросе](msh.md#type-7-и-сегментный-запрос).
Повторная отправка того же ожидающего кадра, например после изменения размера
области отображения, использует уже подготовленное состояние и не применяет
движение второй раз. `--legacy-camera-capture` сохраняет положение захваченной
камеры без изменений и не запускает проверку зданий. Это диагностическое
ограничение, а не восстановленное правило игровой или следящей камеры
оригинала. В проверке начальная поза берётся из декодированных связей CTLD;
проход двери отдельно проверяется после явной выборки записанного в MSH кадра 1
только для Node3. Так проверяется вход запроса столкновения и путь камеры;
видимая дверь не анимируется, а миссионные команды не воспроизводятся.
Высота полёта сверху не ограничена. Это ограничение диагностической камеры
превью; точные правила игровых и следящих камер оригинала остаются
неподтверждёнными.
## Ограничения точного совпадения
`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 и атмосферы;
- переход на запасной набор возможностей устройства;
- квантование времени симуляции.
Параметр `--readback-sequence-out` сохраняет необработанные байты каждого
отправленного кадра; в имя файла записываются индекс кадра, значение общих
управляемых часов и размер изображения. Байты соответствуют выбранному формату
изображения swapchain Vulkan и не гарантируют порядок каналов R, G, B, A. Числовое
формат выводится как `readback_format`; расширение `.rgba` само по себе порядок
каналов не определяет. Последовательность не содержит структурированных ID
видимых объектов, списка элементов отрисовки, ключей конвейера, матриц или
хэшей промежуточных буферов.
|