aboutsummaryrefslogtreecommitdiff
path: root/docs/tomes/05-render.md
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-22 00:58:51 +0300
committerValentin Popov <valentin@popov.link>2026-06-22 00:58:51 +0300
commit78fc5f1debf1395d5df0bab7cc0dde54351205cb (patch)
treeef8f7c72a183723fcbea0b2d1fefd7c28ca7bc18 /docs/tomes/05-render.md
parent50c2cf4686b53ebd2b76318223096660e92305a4 (diff)
downloadfparkan-78fc5f1debf1395d5df0bab7cc0dde54351205cb.tar.xz
fparkan-78fc5f1debf1395d5df0bab7cc0dde54351205cb.zip
docs: rewrite MkDocs documentation
Diffstat (limited to 'docs/tomes/05-render.md')
-rw-r--r--docs/tomes/05-render.md863
1 files changed, 863 insertions, 0 deletions
diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md
new file mode 100644
index 0000000..6804fa6
--- /dev/null
+++ b/docs/tomes/05-render.md
@@ -0,0 +1,863 @@
+# V. Геометрия, материалы и рендер
+
+Этот том описывает путь от загруженного игрового состояния до pixels в back
+buffer. Renderer не решает игровые правила: он получает transforms, geometry,
+материалы, свет, эффекты, камеру и список видимых объектов, затем превращает
+их в упорядоченный набор draw calls и fixed-function states.
+
+Графический pipeline FParkan держится на нескольких слоях данных:
+
+```text
+MSH node/slot/batch
+ -> Batch20.material_index
+ -> строка WEAR
+ -> имя MAT0
+ -> активная phase
+ -> textureName и lightmap slot
+ -> Texm payload
+ -> LegacyRenderState
+ -> draw item кадра
+```
+
+Важное практическое правило: форматы ресурсов, runtime-состояние renderer-а и
+современный backend являются разными уровнями. Файл можно прочитать правильно и
+всё равно получить неверный кадр из-за другой сортировки, другого mip-skip,
+другой ветки material fallback или другого округления animation time.
+
+## Контур рендера
+
+Изображение является последней стадией длинного цикла. До renderer-а уже
+накоплен ввод, рассчитан simulation step, применены отложенные операции,
+обновлены animation states, выбрана camera и выставлен listener для 3D sound.
+
+```text
+system messages and input
+ -> simulation calculation
+ -> deferred object operations
+ -> animation and transforms
+ -> camera and sound listener
+ -> visibility and render queues
+ -> materials and draw passes
+ -> renderer completion
+ -> end-of-render callbacks and UI
+```
+
+CPU делает отбор объектов, сэмплирует animation, собирает matrices, выбирает
+LOD/slot, группирует batches и готовит состояния. Графический pipeline
+преобразует вершины из model space в screen space, rasterizes triangles,
+проверяет depth, применяет texture stages, lighting, alpha test/blend и пишет
+pixels.
+
+Координатный путь вершины:
+
+```text
+local/model space
+ -> world space
+ -> view/camera space
+ -> clip space
+ -> normalized device coordinates
+ -> viewport pixels
+```
+
+Порядок умножения матриц и соглашение о layout должны быть едины во всём
+движке. Ошибка транспонирования часто выглядит как сломанная анимация, хотя
+ключи модели прочитаны верно.
+
+## Граница Ngi32
+
+`Ngi32.dll` является платформенной границей Iron3D-era renderer-а. Она создаёт
+графический и звуковой interfaces, перечисляет устройства, хранит capability
+profile, предоставляет память, часы и быстрые математические процедуры.
+Высокоуровневые DLL должны обращаться к interface Ngi32, а не напрямую к
+конкретному DirectDraw/Direct3D device.
+
+`iron_3d.ini` задаёт выбранный `CURRENT_D3DCARD`. Display layer перечисляет
+drivers и video modes, проверяет поддержку 3D, переводит native capabilities во
+внутренний профиль и создаёт render object. `niCreate3DRender` принимает
+выбранный driver/mode, window handle и flags владения, динамически получает
+функции DirectDraw/Direct3D семейства 5-7 и публикует refcounted renderer.
+`niGet3DRender` возвращает уже созданный объект и увеличивает число владельцев.
+
+```text
+enumerate adapters and video modes
+ -> choose CURRENT_D3DCARD
+ -> translate native capabilities
+ -> create DirectDraw surfaces and 3D interface
+ -> construct engine renderer
+ -> publish global refcounted pointer
+```
+
+Старый API работает как state machine. Перед draw подсистема terrain/shade
+выбирает matrices, texture stages, filtering, depth test/write, culling, alpha
+test, blending и vertex format. Современный backend может собрать это в
+immutable pipeline key и реализовать через shaders, но compatibility layer
+должен видеть исходную fixed-function модель.
+
+```c
+struct LegacyRenderState {
+ Mat4 world, view, projection;
+ TextureStage stages[2];
+ BlendMode blend;
+ DepthMode depth;
+ CullMode cull;
+ bool alpha_test;
+ uint8_t alpha_ref;
+ VertexFormat vertex_format;
+};
+```
+
+Эта структура является переносимой моделью наблюдаемого контракта, а не
+утверждением о точном layout оригинального объекта renderer-а.
+
+Отдельная часть ABI -- таблица `g_FastProc`. При запуске выбираются scalar,
+MMX, Katmai/SSE, 3DNow или PPro-реализации процедур, а `niGetProcAddress(index)`
+возвращает pointer из изменяемой таблицы. Номер slot является частью ABI:
+signature менять нельзя. Различия scalar/SIMD округления способны менять
+animation sampling, culling, particles и даже gameplay-adjacent decisions.
+
+## MSH как граф модели
+
+`*.msh` является nested NRes, а не одной монолитной структурой. Geometry,
+nodes, slots, batches, animation и служебные streams лежат в отдельных entries
+и связываются по `type_id`. Физический порядок entries сохраняется для
+roundtrip, но reader не должен выводить из него смысловую связь.
+
+Карта основных entries:
+
+```text
+type 1 узлы и выбор slot, обычно stride 38
+type 2 header 0x8C + slots по 68 байт
+type 3 positions float3, stride 12
+type 4 packed normals, stride 4
+type 5 packed UV0, stride 4
+type 6 index buffer, u16
+type 7 triangle descriptors, stride 16
+type 8 animation keys, stride 24
+type 9 служебный поток модели
+type 10 строки и имена узлов
+type 13 draw batches, stride 20
+type 15 дополнительный поток, stride 8
+type 17 вспомогательные данные
+type 18 редкий поток, stride 4
+type 19 animation frame map, u16
+type 20 редкая вспомогательная таблица
+```
+
+Базовый набор types стабилен для проверенных моделей Частей 1 и 2. Расширенный
+вариант добавляет types 18 и 20. Редкий вариант `MTCHECK.MSH` имеет
+альтернативный атрибут type 1; его payload нужно поддерживать copy-through до
+закрытия layout.
+
+### Узлы и slots
+
+Type 1 обычно состоит из записей по 38 байт:
+
+```c
+struct Node38 {
+ uint16_t hdr0;
+ uint16_t parent_or_link;
+ uint16_t anim_map_start;
+ uint16_t fallback_key;
+ uint16_t slot_index[15];
+};
+```
+
+`slot_index` образует матрицу `3 LOD x 5 groups`. Выбор выполняется как
+`slot_index[lod * 5 + group]`; `0xFFFF` означает отсутствие geometry для этой
+комбинации. Поле `parent_or_link` участвует в иерархии или связи узлов, но
+название остаётся описательным.
+
+Type 2 начинается с header `0x8C`, затем содержит slots по 68 байт:
+
+```c
+struct Slot68 {
+ uint16_t tri_start;
+ uint16_t tri_count;
+ uint16_t batch_start;
+ uint16_t batch_count;
+ float aabb_min[3];
+ float aabb_max[3];
+ float sphere_center[3];
+ float sphere_radius;
+ uint32_t opaque[5];
+};
+```
+
+Slot связывает диапазон triangle descriptors, диапазон draw batches, AABB и
+sphere bounds. AABB удобен для более точных осевых тестов, sphere -- для
+быстрого отбрасывания. Последние пять слов сохраняются без интерпретации.
+
+Обязательные проверки:
+
+- `type 2` имеет размер не меньше `0x8C`;
+- остаток после header кратен 68;
+- каждый `slot_index` либо `0xFFFF`, либо меньше числа slots;
+- `tri_start + tri_count` не выходит за type 7;
+- `batch_start + batch_count` не выходит за type 13.
+
+### Vertex streams, triangles и batches
+
+Основные vertex streams:
+
+```text
+type 3: position = три float32
+type 4: normal = четыре int8
+type 5: UV0 = два int16
+type 6: index = uint16
+```
+
+Normal XYZ декодируется как signed component / `127.0` с clamp в `[-1, 1]`.
+Четвёртый byte normal stream не отбрасывается при roundtrip. UV декодируется
+как `packed / 1024.0`. Index buffer адресует вершины относительно `base_vertex`
+batch-а, поэтому проверка допустимости всегда использует
+`base_vertex + index < vertex_count`.
+
+Type 7 хранит descriptors triangles:
+
+```c
+struct TriDesc16 {
+ uint16_t tri_flags;
+ uint16_t link0;
+ uint16_t link1;
+ uint16_t link2;
+ int16_t nx;
+ int16_t ny;
+ int16_t nz;
+ uint16_t sel_packed;
+};
+```
+
+Descriptors используются коллизией, выбором и связями triangles. `sel_packed`
+содержит три двухбитовых selector-а; значение `3` преобразуется в отсутствие
+ссылки (`0xFFFF`). Полная семантика links и flags не закрывается одним layout.
+
+Type 13 задаёт draw ranges:
+
+```c
+#pragma pack(push, 1)
+struct Batch20 {
+ uint16_t batch_flags; // +0x00
+ uint16_t material_index; // +0x02
+ uint16_t opaque4; // +0x04
+ uint16_t opaque6; // +0x06
+ uint16_t index_count; // +0x08
+ uint32_t index_start; // +0x0A
+ uint16_t opaque14; // +0x0E
+ uint32_t base_vertex; // +0x10
+};
+#pragma pack(pop)
+static_assert(sizeof(Batch20) == 20);
+```
+
+`material_index` выбирает строку WEAR. `index_start`, `index_count` и
+`base_vertex` описывают один indexed draw. Неизвестные поля могут влиять на
+редкие проходы или state grouping, поэтому writer сохраняет их 1:1.
+
+Типовой обход модели:
+
+```c
+for (Node& node : model.nodes) {
+ Matrix node_world = parent_world * local_transform(node);
+ uint16_t sid = node.slot_index[lod * 5 + group];
+ if (sid == 0xFFFF) continue;
+
+ Slot& slot = model.slots[sid];
+ if (camera.culls(transform(slot.bounds, node_world))) continue;
+
+ for (uint32_t i = 0; i < slot.batch_count; ++i) {
+ Batch& b = model.batches[slot.batch_start + i];
+ bind_wear_material(b.material_index);
+ draw_indexed(b.base_vertex, b.index_start, b.index_count);
+ }
+}
+```
+
+В реальном кадре между culling и draw добавляются material resolve, lightmap,
+render queues и сортировка, но связи данных остаются такими.
+
+## Иерархия и анимация
+
+Анимация MSH меняет локальный transform узлов. Geometry streams не изменяются:
+для каждого узла на кадр строится matrix из position и quaternion. Дочерний
+узел наследует transform родителя, поэтому изменение корпуса переносит башню,
+точки крепления и все связанные slots.
+
+Связка состоит из:
+
+- type 8: пул animation keys;
+- type 19: карта кадров;
+- `anim_map_start` и `fallback_key` в `Node38`;
+- parent links, задающих порядок умножения matrices.
+
+Ключ type 8 занимает 24 байта:
+
+```c
+struct AnimKey24 {
+ float position[3];
+ float time;
+ int16_t qx;
+ int16_t qy;
+ int16_t qz;
+ int16_t qw;
+};
+```
+
+Quaternion components декодируются как signed value / `32767.0`. На диске
+порядок полей XYZ-W, но runtime math использует логическое `[w, x, y, z]`.
+Безусловная современная нормализация после чтения не добавляется без parity
+проверки: она может изменить крайние кадры.
+
+Type 19 является массивом `uint16_t`; его `attr2` задаёт общее число кадров
+timeline. Для конкретного узла `anim_map_start` указывает на блок длиной
+`frame_count` либо равен `0xFFFF`.
+
+Выбор ключа:
+
+1. вычислить frame index из времени;
+2. если frame вне диапазона, взять `fallback_key`;
+3. если `anim_map_start == 0xFFFF`, взять `fallback_key`;
+4. иначе прочитать `map_words[anim_map_start + frame]`;
+5. если значение не меньше `fallback_key`, снова использовать fallback;
+6. иначе использовать mapped key и следующий key для interpolation.
+
+Fallback возвращается без interpolation. Это защищает статические узлы и конец
+track-а.
+
+Для времени между двумя keys:
+
+```text
+alpha = (t - k0.time) / (k1.time - k0.time)
+position = lerp(k0.position, k1.position, alpha)
+rotation = shortest-path quaternion blend
+```
+
+Перед quaternion blend проверяется dot product. Если стороны находятся в
+противоположных полусферах, знак второй стороны меняется, чтобы пройти по
+короткому пути. При точном совпадении времени возвращается соответствующий key
+без вычисления alpha.
+
+Объект может переходить между двумя animation states. Тогда для каждого узла
+сэмплируются позы A и B, затем position смешивается линейно, а quaternion --
+через shortest-path blend. Если одна сторона невалидна, используется другая.
+
+```c
+Pose sample_node(Node n, float t);
+Pose blend_pose(Pose a, Pose b, float weight);
+Mat4 local = quaternion_matrix(pose.rotation);
+local.set_translation(pose.position);
+world[n] = world[parent(n)] * local;
+```
+
+Для parity особенно важны x87-compatible округление при выборе frame index и
+порядок операций. Одинаковая формула на SSE может выбрать соседний кадр возле
+границы.
+
+Проверки animation data:
+
+- размер type 8 кратен 24;
+- размер type 19 кратен 2;
+- каждый `fallback_key` меньше числа keys;
+- блок карты узла полностью помещается в type 19;
+- времена keys внутри track возрастают;
+- parent links не образуют cycle;
+- quaternion components читаются как signed 16-bit.
+
+## WEAR и MAT0
+
+MSH batch хранит только числовой `material_index`. WEAR переводит позиционный
+slot в имя материала. MAT0 по этому имени описывает phases, parameters,
+texture names и animation blocks. Такое разделение позволяет одной geometry
+использовать разные appearances.
+
+```text
+Batch20.material_index
+ -> строка WEAR
+ -> имя MAT0
+ -> активная phase
+ -> textureName и render parameters
+```
+
+### WEAR
+
+WEAR имеет type ID `0x52414557` и обычно хранится как `*.wea` рядом с моделью.
+Формат текстовый:
+
+```text
+<wearCount>
+<legacyId> <materialName>
+... wearCount строк
+
+[пустая строка]
+[LIGHTMAPS
+<lightmapCount>
+<legacyId> <lightmapName>
+... lightmapCount строк]
+```
+
+`legacyId` читается и сохраняется, но material выбирается по позиции строки и
+имени. Пустая строка перед `LIGHTMAPS` является частью совместимого framing:
+parser paths по-разному обрабатывают переход, и отсутствие разделителя ломает
+совместимость. Material handle кодируется как `(table_index << 16) |
+wear_index`; manager поддерживает ограниченное число wear tables.
+
+Fallback material resolve строго разделён:
+
+1. имя из WEAR;
+2. `DEFAULT`;
+3. entry 0;
+4. для lightmap отсутствие означает slot `-1`, а не замену обычной texture.
+
+Пустое имя texture внутри phase означает намеренно untextured surface.
+Lightmap ищется в отдельном cache и не подменяется diffuse texture.
+
+### MAT0
+
+MAT0 имеет type ID `0x3054414D` и обычно находится в `Material.lib`. `attr1`
+содержит runtime flags, `attr2` -- версию payload. Versioned metadata читается
+cursor-ом: старые версии получают runtime defaults, но reader не пытается
+насильно читать поля новой версии.
+
+```c
+#pragma pack(push, 1)
+struct Mat0PrefixV4Plus {
+ uint16_t phase_count; // +0x00
+ uint16_t animation_block_count; // +0x02, меньше 20
+ uint8_t metadata_a; // +0x04, attr2 >= 2
+ uint8_t metadata_b; // +0x05, attr2 >= 2
+ uint32_t metadata_c_raw; // +0x06, attr2 >= 3
+ uint32_t metadata_d_raw; // +0x0A, attr2 >= 4
+};
+
+struct Phase34 {
+ uint8_t parameters[18];
+ char texture_name[16];
+};
+#pragma pack(pop)
+static_assert(sizeof(Phase34) == 34);
+```
+
+Если `attr2 < 2`, metadata A/B получают default `255`; при `attr2 < 3`
+значение C соответствует `1.0f`; при `attr2 < 4` D равно 0. C/D сохраняются
+как raw 32-bit values до полного подтверждения интерпретации. Phase parameters
+сохраняются как 18 raw bytes даже там, где часть bytes уже имеет понятный
+смысл.
+
+Каждая phase разворачивается в runtime-запись примерно 76 байт: коэффициенты
+цвета, освещения и прозрачности, texture slot и служебные поля. Material time
+выбирает одну или две phases; только часть полей интерполируется, остальные
+копируются из активной записи.
+
+Animation block MAT0 имеет плотный framing без 4-byte tail alignment:
+
+```text
+u32 header_raw
+u16 key_count
+repeat key_count:
+ u16 k0
+ u16 k1
+ u16 k2
+```
+
+Младшие три бита `header_raw` задают числовой mode, остальные образуют mask
+interpolation. Наблюдаются modes 0, 1, 2 и 3, связанные с семействами loop,
+ping-pong, one-shot/clamp и random-offset, но точные boundary cases остаются
+предметом runtime parity. Поле `k2` сохраняется всегда.
+
+Проверки MAT0:
+
+- `animation_block_count < 20`;
+- все versioned metadata помещаются в payload;
+- секция phases имеет ровно `phase_count * 34` байта;
+- `texture_name` ограничено 16 байтами;
+- каждый animation block и его keys помещаются в payload;
+- parser заканчивает чтение на точном конце записи.
+
+Material manager кэширует разобранный MAT0 и texture handles. Current phase
+лучше вычислять на экземпляр материала, если random offset или локальное время
+различаются между объектами; immutable phase data остаются общими.
+
+## Texm: текстуры, mip-уровни и атласы
+
+`Texm` -- основной формат изображений. Он хранится в `Textures.lib`,
+`LightMap.lib` и других NRes-архивах. Payload содержит header, необязательную
+palette, mip chain и иногда `Page` chunk для atlas rectangles.
+
+```c
+struct TexmHeader32 {
+ uint32_t magic; // 'Texm'
+ uint32_t width;
+ uint32_t height;
+ uint32_t mip_count;
+ uint32_t flags4;
+ uint32_t flags5;
+ uint32_t unknown6;
+ uint32_t format;
+};
+```
+
+Подтверждённые formats:
+
+```text
+0 Indexed8 + palette 256 x 4 байта
+565 R5 G6 B5
+556 R5 G5 B6
+4444 A4 R4 G4 B4
+88 L8 A8
+888 RGB8 в четырёхбайтовом element
+8888 A8 R8 G8 B8
+```
+
+Formats 556 и 88 являются loader-confirmed, но не corpus-verified для
+доступных игровых payload. CPU decoder расширяет короткие каналы до 8 bit через
+повторение значимых bit, а не простым shift. Для 888 служебный четвёртый byte
+сохраняется при roundtrip.
+
+Layout:
+
+```text
+TexmHeader32
+[palette 1024 байта, только для format 0]
+level 0 pixels
+level 1 pixels
+...
+level mip_count-1 pixels
+[optional Page chunk]
+```
+
+Размер уровня `i` вычисляется из `max(1, width >> i)` и
+`max(1, height >> i)`. Bytes per pixel: 1 для indexed; 2 для 565, 556, 4444 и
+88; 4 для 888 и 8888. Parser суммирует размеры с проверкой overflow до чтения.
+
+`Page` chunk:
+
+```c
+struct PageHeader8 {
+ uint32_t magic; // 'Page'
+ uint32_t rect_count;
+};
+
+struct PageRect8 {
+ int16_t x;
+ int16_t width;
+ int16_t y;
+ int16_t height;
+};
+```
+
+Chunk обязан иметь размер `8 + rect_count * 8`; произвольный tail не
+допускается. Rectangles задаются в pixel space базового mip. Если loader
+пропускает верхние mip-уровни, rectangles масштабируются вместе с новым base
+level.
+
+Mip-skip является поведением loader-а, а не offline-изменением файла. После
+skip меняются runtime width, height, mip count и pointer на первый загружаемый
+уровень. Современный renderer должен повторить выбор base level или
+эквивалентно эмулировать его upload policy; использование полной texture при
+тех же UV меняет резкость и atlas coordinates.
+
+Indexed texture требует связанную palette. Часть palettes выбирается по suffix
+имени: буква `A..Z` и вариант пустой или `0..9`, всего 286 возможных slots.
+Невалидный suffix диагностируется явно.
+
+Обычные textures и lightmaps находятся в разных managers. Обычный cache
+отслеживает refcount и время неиспользования, а eviction выполняется
+отложенно. Lightmap lifetime связан с world/mission и не должен попадать под
+ту же политику удаления.
+
+Строгий Texm parser проверяет положительные dimensions, положительный
+`mip_count`, известный format, точный размер palette/mip chain, корректный
+`Page` и отсутствие лишних bytes. `flags4`, `flags5` и `unknown6` сохраняются
+1:1; участие `flags5` в mip-skip подтверждено, но полная семантика всех bits не
+закрыта.
+
+## Свет, тени, атмосфера и сортировка
+
+Свет является отдельной world-подсистемой. Terrain layer создаёт
+`LightManager`, `Shader` и primitive managers. Это не один глобальный
+коэффициент яркости: world управляет point lights, lightmaps, shadows,
+atmospheric objects и sort phases. Материал сообщает свойства поверхности, а
+CShade превращает их в states renderer-а.
+
+Подтверждённые точки: `CreateLightManager`, `CreateShader`,
+`CreateAtmosphere`, `CreatePrimitives`, `CreatePrimitives2`,
+`CShade::StartMeshRender`, `CShade::EndMeshRender` и
+`CShade::ConfigureTextureAndAlphaBlendModes`.
+
+CShade получает active MAT0 phase, capability profile устройства и pass
+context. Он выбирает texture mode, alpha blending, depth/cull behavior и способ
+освещения. Наличие fallback вроде `TEXTUREMODE_MODULATE not supported`
+означает, что material нельзя напрямую преобразовать в современный PBR.
+Сначала строится legacy state, затем он сопоставляется shader permutation.
+
+CLightManager выдаёт numeric IDs источникам и проверяет допустимое количество.
+Ветка `EmulatePointLights()` позволяет воспроизводить point lights даже при
+ограничениях hardware lighting. Неизвестный type light должен давать отдельную
+ошибку.
+
+Lightmap не является обычной diffuse texture. WEAR содержит отдельный блок
+`LIGHTMAPS`, manager открывает `LightMap.lib`, а shade path подаёт lightmap
+отдельным slot или texture stage. Замена lightmap предварительным умножением в
+diffuse texture ломает LOD, atlas coordinates и динамическую модуляцию.
+
+Тени проходят отдельным render pass. Terrain содержит пути для теней зданий и
+роботов, ограничения максимального числа, detail level и smoothing. Доказаны
+shadow manager/pass, настройки detail/smoothing/count и зависимость от
+Terrain/CShade; полная формула projection geometry для каждого caster требует
+dynamic trace. Unknown settings из `shade.cfg` читаются и сохраняются по
+именам, а не заменяются произвольными modern defaults.
+
+Atmosphere manager создаёт world objects для фоновых и погодных явлений.
+Отдельно подтверждены lightning, sun render, flare, `env_lightning`, rain
+background sound и обязательные ссылки на lightning effect. Эти объекты
+обновляются по игровому времени, но часть параметров зависит от camera: flare
+требует screen position и occlusion test, rain -- области рядом с observer,
+sound -- listener. Их нельзя один раз запечь в terrain.
+
+RNG для lightning, atmosphere phases и FX должен иметь стабильный порядок.
+Даже правильный средний интервал не даёт повторяемый кадр, если random values
+запрашиваются в другой последовательности.
+
+Согласованная модель sort phases:
+
+```text
+opaque terrain and models
+ -> lightmapped/state-grouped passes
+ -> shadows and projected primitives
+ -> alpha-tested surfaces
+ -> transparent objects/effects back-to-front
+ -> atmosphere, flares and overlays
+```
+
+Точный взаимный порядок отдельных FX, shadow и atmosphere subpasses требует
+capture. Новый renderer должен хранить явный `RenderPhase` и стабильный
+secondary sort key, а не сортировать всё только по material ID.
+
+## FXID: система эффектов
+
+FXID -- не готовая картинка, а описание небольшого runtime command stream.
+Header задаёт lifetime, time mode, random shifts и transform. Затем идут
+команды разных types. При создании manager превращает disk-команды в runtime
+objects; во время кадра они обновляются и выпускают sounds, particles,
+materials или projected primitives.
+
+Type ID равен `0x44495846`. Header занимает 60 байт:
+
+```c
+struct FxHeader60 {
+ uint32_t command_count;
+ uint32_t time_mode;
+ float duration_seconds;
+ float phase_jitter;
+ uint32_t flags;
+ uint32_t settings_id;
+ float random_shift[3];
+ float pivot[3];
+ float scale[3];
+};
+```
+
+Поток команд начинается строго с offset `0x3C`. `duration_seconds`
+преобразуется runtime-ом во внутреннюю шкалу времени. `phase_jitter` и
+`random_shift` используются только при соответствующих flags. Pivot задаёт
+локальную точку опоры, scale -- базовый масштаб экземпляра. Unknown flags и
+settings ID сохраняются.
+
+Каждая команда начинается с `uint32_t command_word`:
+
+```text
+opcode = command_word & 0xFF
+enabled = (command_word >> 8) & 1
+```
+
+Bits 9-31 являются частью данных и сохраняются. Между командами нет
+выравнивания. Размер команды, включая word:
+
+```text
+opcode 1 224 байта
+opcode 2 148 байт
+opcode 3 200 байт
+opcode 4 204 байта
+opcode 5 112 байт
+opcode 6 4 байта
+opcode 7 208 байт
+opcode 8 248 байт
+opcode 9 208 байт
+opcode 10 208 байт
+```
+
+Parser использует opcode только для выбора фиксированного размера. Неизвестный
+opcode отклоняется: попытка угадать длину потеряет синхронизацию всего stream.
+
+Opcodes 2, 3, 4, 5, 7, 8, 9 и 10 содержат pair fixed strings:
+
+```c
+struct FxResourceRef64 {
+ char archive[32];
+ char name[32];
+};
+```
+
+Имена сравниваются case-insensitive по ASCII, а tail после первого nul byte
+сохраняется. Resolve выполняется при создании command object или лениво при
+первом запуске, но ошибка должна включать имя эффекта, номер команды, archive
+и resource name.
+
+Базовый normalized age:
+
+```text
+tn = (now - start_time) / (end_time - start_time)
+```
+
+`time_mode` выбирает источник коэффициента: constant, forward/reverse age,
+cyclic phase, external world state и варианты с ограничением относительно
+предыдущего значения. Точные формулы редких modes являются parity-задачей.
+Flags могут умножать alpha на lifetime, применять triangular remap, случайно
+сдвигать phase/space, инвертировать active-state, фильтровать по времени суток
+или включать manager gates.
+
+Lifecycle:
+
+```text
+create instance
+ -> copy header and external transform
+ -> calculate end time and random offsets
+ -> create command objects in disk order
+ -> resolve required resources
+ -> Start
+
+on each calculation/render frame
+ -> evaluate time coefficient and gates
+ -> update commands in stable order
+ -> emit active primitives or sounds
+ -> collect render batches
+ -> handle Stop / Restart / end-of-life
+```
+
+Update и emit разделяются. Simulation может продолжаться в кадре без render, а
+emit не должен повторно менять игровое состояние. Для authoring безопасно
+типизировать header и resource references, а body редких commands сохранять raw
+до подтверждения field-level semantics.
+
+## Полный кадр
+
+Крупный вход в world render проходит через `World3D::stdRenderGame`. Доказан
+следующий порядок boundary операций:
+
+1. передать camera в Terrain через `stdSetCurrentCamera2` и сохранить её как
+ текущую;
+2. получить camera/view/viewport interfaces через virtual queries;
+3. обновить положение и ориентацию 3D sound listener;
+4. настроить renderer viewport и matrices;
+5. вызвать два renderer boundary slots перед traversal;
+6. установить глобальный флаг `in_render`;
+7. вызвать главный virtual метод camera/world traversal;
+8. выполнить дополнительную post queue при включённом режиме;
+9. завершить world/shade pass;
+10. вызвать renderer completion slot;
+11. снять `in_render`, восстановить viewport и разослать end-of-render.
+
+Семантические имена нескольких slots перед и после traversal не подтверждены,
+поэтому в compatibility code их лучше временно называть
+`frame_boundary_0`, `frame_boundary_1`, `frame_boundary_2`.
+
+Обход видимого мира:
+
+```text
+проверить active/visible state
+ -> выбрать LOD по расстоянию и настройкам
+ -> получить node matrices из animation state
+ -> выбрать slot для каждого node/group
+ -> преобразовать bounds в world space
+ -> выполнить culling
+ -> добавить batches в подходящую render queue
+```
+
+Material/texture resolve желательно выполнять после visibility и slot
+selection, чтобы невидимые объекты не меняли порядок обращений к caches и не
+создавали лишние side effects. Невидимость объекта и отсутствие slot являются
+разными причинами пропуска и диагностируются отдельно.
+
+Подготовленный draw item содержит:
+
+```text
+node world matrix
+batch flags and index range
+WEAR material handle
+MAT0 active phase and coefficients
+texture handle
+optional lightmap handle
+render phase and sorting key
+legacy pipeline state
+```
+
+Draw item должен ссылаться на immutable данные кадра. Изменение phase или
+texture cache посреди прохода не должно менять уже собранную очередь.
+
+Согласованная декомпозиция внутренних render phases:
+
+1. подготовка frame state, camera и viewport;
+2. непрозрачный terrain;
+3. непрозрачные object batches;
+4. lightmap и дополнительные material passes;
+5. projected primitives и тени;
+6. alpha-tested geometry;
+7. transparent objects и FX в сортировочных слоях;
+8. atmosphere, sun, flare и weather;
+9. renderer completion boundary;
+10. end-of-render callbacks;
+11. shell/UI и post-render state.
+
+Точный взаимный порядок пунктов 4-8 и связь completion slot с физическим
+DirectDraw flip/present требуют dynamic capture. Сортировка внутри каждой фазы
+должна быть стабильной: для opaque первичен pipeline/material key, для
+transparent -- distance layer и depth order, затем stable insertion ID.
+
+Геометрический draw использует streams type 3/4/5, optional streams, index
+buffer type 6, `base_vertex`, `index_start` и `index_count`. Матрица узла
+устанавливается как world transform, затем CShade привязывает texture stages и
+fixed-function state.
+
+```c
+set_world_matrix(item.node_world);
+bind_vertex_streams(model.streams);
+bind_index_buffer(model.indices);
+apply_legacy_state(item.pipeline);
+bind_texture(0, item.texture);
+bind_texture(1, item.lightmap);
+draw_indexed(item.batch.base_vertex,
+ item.batch.index_start,
+ item.batch.index_count);
+```
+
+После последнего world pass renderer закрывает сцену и выводит back buffer.
+World3D снимает `in_render`, восстанавливает временный viewport state и вызывает
+`on_end_render` у active objects. Только после этого допустимо освобождать
+temporary vertex buffers или заменять render representation. UI/shell
+обслуживается верхним уровнем после возврата из world-render path; для
+диагностики полезно уметь сохранять world-only command list и финальный
+framebuffer отдельно.
+
+## Проверки паритета
+
+Главные риски совпадения кадра:
+
+- x87 extended precision и правила округления;
+- различия scalar/SIMD slots `g_FastProc`;
+- порядок objects, batches и transparent primitives;
+- depth write/test, cull, alpha test и blend transitions;
+- mip-skip, palette и `Page` coordinates;
+- material fallback и выбор phase;
+- последовательность RNG для FX и atmosphere;
+- capability fallback конкретного устройства;
+- quantization времени и дополнительный simulation step;
+- eager/lazy resource resolve и cache side effects.
+
+Минимальный deterministic frame capture должен включать camera state, viewport,
+visible object IDs, выбранные LOD/group/slot, draw-item list, material и texture
+handles, pipeline keys, matrices, render phase, sort key, причины culling и
+hashes промежуточных buffers. Без такой трассировки нельзя уверенно отделить
+ошибку формата MSH от ошибки state machine renderer-а или сортировки.
+
+Связанные справочные страницы с таблицами форматов: [MSH](../reference/msh.md),
+[materials](../reference/materials.md), [Texm](../reference/texm.md) и
+[render frame](../reference/render-frame.md).