# 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 строк [пустая строка] [LIGHTMAPS ... 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).