diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/reference/environment-effects.md | 47 | ||||
| -rw-r--r-- | docs/reference/msh.md | 47 | ||||
| -rw-r--r-- | docs/reference/original-binaries.md | 15 | ||||
| -rw-r--r-- | docs/reference/render-frame.md | 350 | ||||
| -rw-r--r-- | docs/tomes/05-render.md | 70 |
5 files changed, 338 insertions, 191 deletions
diff --git a/docs/reference/environment-effects.md b/docs/reference/environment-effects.md index 31024f7..fdc29f0 100644 --- a/docs/reference/environment-effects.md +++ b/docs/reference/environment-effects.md @@ -189,6 +189,53 @@ OneShot -> воспроизвести разовый звук молни отдельный spatial source, применяет DirectSound range/pan и текущий listener. Отсутствие устройства вывода не должно останавливать симуляцию. +## Цвет солнца + +Солнце берёт цвет спрайта и прозрачность бликов из `palette[10]` расписания +`sky.ske`; `palette[9]` — чёрный specular-компонент. Вторичный направленный +свет использует `palette[14]`. При выборке расписания это выражено так: + +```rust +packed: [key.colors[10], key.colors[14]], +``` + +Этот порядок соответствует живому GOG-замеру: `CSun+0x95c` содержал +`0x41FF3C00` (интерполяция `palette[10]`), `+0x960` — `0xFF000000` +(`palette[9]`), а `+0x964` — вторичный свет из `palette[14]`. В `Terrain.dll` +по VA `0x1004AA92..0x1004AAAB` цвет в `+0x95c` обновляется, только если +установлен бит 0 результата sky-query; иначе остаётся прежнее значение. +`AtmosphereFrame` пока не несёт этот бит обновления, поэтому sampler +выдаёт интерполированный цвет при каждом запросе. + +## Экранная геометрия солнца и луны + +`CSun::Render` сначала переводит направление небесного тела в пиксели текущего +viewport, затем собирает quad с native XYZRHW-позицией. При положительной высоте +viewport Vulkan пиксели переводятся в clip-space так: + +```rust +position = [ + pixel[0] * (2.0 / width) - 1.0, + pixel[1] * (2.0 / height) - 1.0, + 0.999, +]; +``` + +Vertex shader пропускает `clip_from_world` для строк солнца и луны. CPU каждый +кадр пересчитывает центр и размер quad по текущему viewport; старые пиксельные +координаты не переносятся на новый размер. World-space billboards бликов +остаются отдельным путём. Материал экранного quad не освещается, vertex fog +отключён, а sky far-depth bit к нему не применяется. + +Native `z = 0.999` не означает, что спрайт гарантированно лежит за любой +поверхностью. Vulkan использует `LESS_OR_EQUAL` и не записывает depth для +прозрачного ряда. При near `0.5`, far `700` мировая поверхность на расстоянии +`320` получает depth +`700 / (700 - 0.5) * (1 - 0.5 / 320) ≈ 0.999151`; поэтому экранный `0.999` +чуть ближе этой поверхности и проходит depth test перед ней. Это фиксированное +native значение сохраняется для совместимости, но само по себе не обещает +окклюзию солнца всей геометрией местности. + ## Что остаётся за владельцем `EnvironmentSystem` не извлекает камеру из renderer, не разрешает материалы diff --git a/docs/reference/msh.md b/docs/reference/msh.md index 165b750..a28514d 100644 --- a/docs/reference/msh.md +++ b/docs/reference/msh.md @@ -153,3 +153,50 @@ Byte `material_index` выбирает строку WEAR; high word at `+0x02` Ñ `index_start + index_count` остаётся внутри type 6, а каждый использованный индекс через `base_vertex` попадает в position stream. `vertex_count` и `local_batch_index` доступны renderer для исходного draw contract. + +## Сторона грани в цветовом проходе + +В цветном проходе `Terrain.dll`, функция `0x100296F0`, сравнивает камеру с +каждой гранью по знаку тройного произведения: + +```c +d = dot(eye - v0, cross(v1 - v0, v2 - v0)); +``` + +Проход оставляет грань только при `d > 0`. Raw `Batch20.flags` задаёт два +исключения: `0x0002` пропускает программную проверку стороны, поэтому обратная +грань не отбрасывается этим условием. С `0x4000` проверяется только первая +грань записи: если `d <= 0`, цикл её граней прекращается; если `d > 0`, +оставшиеся грани этой записи уже не проходят повторную программную проверку. +`Terrain.dll!0x100296F0` также настраивает пакет этого цветного пути: flags по +адресу `packet+0x128` равны `0x404` или `0x414`. Путь отправки в +`Terrain.dll!0x1001D660` перед вызовом renderer vtable `+0x48` очищает только +маску `0x1`; `Ngi32.dll!0x10007470` проверяет маску `0x4` этого аргумента. При +установленной маске render state 22 получает `D3DCULL_NONE`, иначе — +`D3DCULL_CW`. Оба значения CShade сохраняют маску `0x4`, поэтому в этом +цветном пути аппаратное отсечение выключено, а набор обычных граней определяет +программный тест `d > 0`. Raw-флаги `0x0002` и `0x4000` меняют этот тест, как +описано выше. + +Статический Vulkan preview сохраняет каждую исходную запись `Batch20` одним +диапазоном рисования: +`append_static_preview_component_with_lightmaps` добавляет исходные индексы без +перестановки, а в диапазон переносит `index_count` и исходный `first_index` с +общим смещением компонента. Поэтому `0x4000` выбирает ту же первую грань, даже +если диапазон получил другой материал для lightmap. + +Поскольку вершины preview переведены в мировые координаты, знак `d` для MSH +диапазона корректируется знаком детерминанта полной матрицы размещения. Повороты +узлов и mounts — единичные кватернионы с determinant `+1`; отрицательный object +scale меняет знак. Renderer Vulkan задаёт `front_face=CLOCKWISE` и viewport с +положительной высотой. Для используемых матриц legacy и free-flight preview +при положительном determinant native-visible `d > 0` относится к Vulkan +back-face; поэтому preview отсекает `FrontFace`. При отрицательном determinant +режим меняется на `BackFace`. +Для `0x0002` и `0x4000` аппаратное отсечение выключено, чтобы сохранить их +программную политику. Отклонённый `0x4000` диапазон получает число индексов 0; +каждый кадр это число заново вычисляется из исходного значения, поэтому после +изменения положения камеры или opacity материала диапазон может вернуться в +draw queue. Проверка первой грани +применяется только к MSH-диапазонам цветовой мировой очереди; terrain и +shadow/sun запросы её не используют. diff --git a/docs/reference/original-binaries.md b/docs/reference/original-binaries.md index 05c0141..a988638 100644 --- a/docs/reference/original-binaries.md +++ b/docs/reference/original-binaries.md @@ -15,11 +15,16 @@ RVA в книге относится к определённой сборке DL ## Сборка GOG The canonical disassembly source is the windowed GOG installation, not the -Part 1/Part 2 test installations. Its `Terrain.dll` is a distinct revision: - -| Binary | SHA-256 | Size | Notes | -| --- | --- | ---: | --- | -| `Terrain.dll` | `af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead` | 499712 | GOG camera receiver evidence; do not reuse Part 1/2 RVAs without a matching hash | +Part 1/Part 2 test installations. The rows below identify GOG-specific DLL +builds. Match the module size and hash before applying one of their RVAs; the +Part 1/Part 2 builds remain listed separately above. + +| Binary | Size (bytes) | SHA-256 | +| --- | ---: | --- | +| `AniMesh.dll` | 172032 | `65659bf67f20e5be77d67a5b2f59968f61b94293143807fa59ca451c560c4f7e` | +| `Effect.dll` | 163840 | `386a3e8db1ce23f61957825d633c7dae1ed40c25cab0cf10a534385fd7cee789` | +| `iron3d.dll` | 1175552 | `d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa` | +| `Terrain.dll` | 499712 | `af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead` | ## Различающиеся модули diff --git a/docs/reference/render-frame.md b/docs/reference/render-frame.md index 015f9f3..de71603 100644 --- a/docs/reference/render-frame.md +++ b/docs/reference/render-frame.md @@ -1,174 +1,190 @@ -# Render frame +# Кадр отрисовки -Кадр является последней стадией цикла, а не самостоятельной функцией renderer-а. -До draw calls уже накоплен input, рассчитан tick, применены отложенные операции, -выбрана камера и обновлён 3D sound listener. +Кадр завершает цикл и не является отдельной операцией рендерера. До вызовов +отрисовки собирается ввод, вычисляется такт, применяются отложенные операции, +выбирается камера и обновляется слушатель 3D-звука. -## Frame skeleton +## Порядок кадра ```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 +системные сообщения и ввод + -> расчёт симуляции + -> отложенные операции над объектами + -> анимация и преобразования + -> камера и звуковой слушатель + -> видимость и очереди отрисовки + -> материалы и проходы отрисовки + -> завершение рендерера + -> обратные вызовы после отрисовки и UI ``` -В `World3D::stdRenderGame` доказан крупный порядок: camera передаётся Terrain, -настраиваются viewport/matrices, вызываются renderer boundary slots, -устанавливается `in_render`, выполняется traversal мира, закрывается world/shade -pass, вызывается renderer completion, снимается `in_render`, рассылается -end-of-render. - -## Draw item - -Подготовленный draw item содержит: - -- 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. - -Подготовленный item должен ссылаться на immutable данные кадра. Изменение phase -или texture cache посреди прохода не должно менять уже собранную очередь. - -## MAT0 state and transparent backgrounds - -For mission world ranges, the selected MAT0 material supplies its native -pipeline category. The category sets blend and alpha-test state while preserving -the range's depth and cull state; an animated material selector updates the -category with the selector. Queue membership is independent: sampled opacity -below one or native batch flags `0x108` mark a world range transparent, and a -queued range with depth enabled tests depth without writing it. Fixed -environment ranges keep their explicit states. - -The fixed-camera preview explicitly marks nebula/stars, dome and cloud ranges as -transparent backgrounds. Opaque world ranges stay in their world slots; the -transparent world queue is sorted back-to-front and inserted immediately after -the last marked background. Other fixed ranges retain their relative order. -Thus cloud layers can be drawn before transparent foliage, while later sun, -weather and other foreground ranges keep their existing relative order. This -is a narrow queue rule, not a reordering of every sky or effects pass. - -## Regression evidence - -`cargo test -p fparkan-render-vulkan --lib material_category_and_transparency_preserve_world_state_in_either_order --offline` -checks that category 1 remains source-alpha with alpha testing, retains depth -and cull settings in either update order, and rejects unsupported categories. -`cargo test -p fparkan-render-vulkan --lib tree_canopy_queue_follows_sky_and_precedes_foreground_effects --offline` -checks the explicit queue order: screen gradient, opaque terrain/building, -shadow, nebula/dome/clouds, far-to-near transparent canopy, then sun/weather. - -Viewer-only paired readbacks used `--frames 3 --validation ---legacy-camera-capture` with the pre-fix and patched builds. On AutoDemo TMA -object 4 (`wor_l_n1.dat`, `fr_l_plant`), the 1280x720 camera at eye -`[464.319, 904.719, 38.0]`, aimed at `[374.319, 814.719, 12.0]` with FOV -`0.62`, changed the two black facade panels to tinted, textured portal -surfaces. On Mission.01 object 12 (`s_tree_04`), the 1280x1024 camera at eye -`[492.63855, 312.90244, 46.5]` aimed at -`[492.63855, 442.90244, 55.0]` crosses the canopy against open sky; paired -three-frame captures at the same synthetic atmosphere phase (600 seconds) show -the patched build restoring the missing upper/right fronds along that sky -boundary. Both runs reported clean Vulkan validation. These captures verify our -preview behavior and do not prove the native renderer's pass order, portal -transmittance or exact color. - -## GOG camera dispatch evidence - -For the GOG `World3D.dll` baseline with SHA-256 +По анализу вызовов установлен общий порядок в `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`, -the exported `stdRenderGame` is RVA `0x13BD0`. It first calls -`Terrain::stdSetCurrentCamera2(camera)`, stores the camera pointer only for -the frame, and clears it before return. `sendEndOfRender` is a separate export -at RVA `0x13D90`. This is frame-order evidence only: the camera ABI, matrices -and viewport values are not recovered by this call-order analysis. The -implemented preview path below accepts selector-0 input produced through the -[native frame capture workflow](../../tools/native-frame-capture/README.md). -The capture workflow also records an observed mission path and, when -its thread/generation and clock checks succeed, an `atmosphere_seconds` sample. -For a fixed-camera preview, the renderer uses that optional absolute schedule -phase when no `--atmosphere-seconds` override is supplied and keeps schedule -sampling at that exact phase across readback frames. An explicit CLI value takes -precedence. The phase sample is not full simulation time: weather, RNG, and -other mission state remain unknown, so this evidence does not establish -full-frame parity. - -Receiver-side GOG Terrain evidence refines this contract. `stdSetCurrentCamera2` -(RVA `0x4FD40` in Terrain SHA-256 -`af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead`) queries -selector `18` on the supplied object and invokes slot `+12` on that result. It -does not store the supplied pointer in `stdGetCurrentCamera2`; that getter -returns a Terrain global populated by selector `8` during Terrain initialization. - -`Terrain.dll!LoadCamera` adds a separate factory-side contract. Its GOG entry -at RVA `0x4EBE0` is `__stdcall` with four machine-word arguments, allocates -`0x1A4` bytes, calls the constructor at RVA `0x4EC60`, and returns the interface -at object offset `+0x134`. The constructor passes arguments 3 and 4 into base -initialization at `this + 4`, writes argument 3 to `this + 0x138`, and calls a -helper at RVA `0x4CEF0` with arguments 1, 2 and 4 plus `this`. Argument 1 is a -mode string: only `REFLECTION` and `REFLECTION_SHIFTED` produce a non-zero mode -value at `this + 0x1A0`. This is factory ABI evidence, not a recovered view or -projection matrix contract. - -## Legacy camera preview path - -`fparkan-game --legacy-camera-capture <json>` consumes the selector-0 transform, -the native D3D7 viewport RECT `[left, top, right, bottom]`, near/far planes and -field of view. It derives the D3D7 view and clip transform from those inputs, -carries the resulting `clip_from_world` matrix into the preview camera, and -keeps the view-matrix camera axes directly, so the captured basis, including -roll, is not reduced to yaw/pitch. The RECT becomes a physical drawable extent -of `right - left` by `bottom - top`. The preview window is fixed to that extent; -startup and non-zero resize events reject a physical-size mismatch instead of -stretching the captured view. Compare the result with the corresponding -`[left, top, right, bottom]` crop of the original PNG. `FreeFlightCamera` is not -created in this mode, but redraws still update environment and projected -shadows from the captured camera. Before environment updates can emit sound -events, the audio listener receives the captured position, forward and up axes; -initial world draw sorting uses the same captured position. A phase capture may -include an observed mission path and a verified `atmosphere_seconds` value; the -latter is used only when no explicit CLI override is supplied. This value pins -the atmosphere schedule sample during a fixed-camera comparison while FX still -receives real frame time. `simulation_time_seconds`, weather, RNG and complete -mission state remain unknown, and a camera/phase match alone does not establish -visual parity with a native frame. - -The preview's sky heading is `atan2(forward.x, forward.y)`, matching the second -output calculated at `Terrain.dll` RVA `0x4F9C0`; `CSky` adds π at RVA `0x481C5` -when selecting screen-gradient sectors. This mapping is applied only at the sky -boundary and does not change `FreeFlightCamera` yaw or movement controls. -Matched same-camera, same-phase three-frame captures at phases 56.227 and -998.893 showed zero changed pixels before and after this correction; the -remaining sky and color mismatch is unresolved, so this source-level match does -not establish visual parity. - -## Parity risks - -`VulkanStaticCamera::from_legacy_d3d7` keeps the D3D7 view and projection -reconstruction in `fparkan-render`, then negates the vertical projection term -once at the Vulkan adapter boundary. D3D7's top-down viewport places positive -NDC Y at the top; the Vulkan swapchain uses a positive viewport height, so the -adapter flips NDC Y while the shader preserves `gl_Position`. The generic -`from_row_major_view_projection` path remains unchanged; its callers supply -Vulkan-ready matrices, as the free-flight camera already does. - -- x87 precision and rounding; -- scalar/SIMD `g_FastProc` differences; -- object, batch and transparent primitive order; -- depth, cull, alpha test and blend transitions; -- mip-skip, palette and Page coordinates; -- material fallback and phase selection; -- RNG sequence for FX and atmosphere; -- device capability fallback; -- simulation time quantization. - -Для отладки нужен deterministic frame capture: camera state, visible object IDs, -draw-item list, pipeline keys, matrices и hashes промежуточных buffers. +экспорт `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 и атмосферы; +- переход на запасной набор возможностей устройства; +- квантование времени симуляции. + +Для отладки требуется детерминированный захват кадра: состояние камеры, +идентификаторы видимых объектов, список элементов отрисовки, ключи конвейера, +матрицы и хэши промежуточных буферов. diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md index 01038b7..b04af31 100644 --- a/docs/tomes/05-render.md +++ b/docs/tomes/05-render.md @@ -87,7 +87,9 @@ drivers и video modes, проверяет поддержку 3D, перевод современная реализация заменяет наблюдаемую fixed-function семантику над этой границей, а не должна эмулировать COM-объекты DirectX. Равный SHA-256 Ngi32 в Частях 1 и 2 зафиксирован в [таблице оригинальных модулей](../reference/original-binaries.md); -это поддерживает единый контракт Vulkan backend. +это поддерживает единый контракт Vulkan backend. RVA относятся к конкретной +сборке DLL: перед переносом адреса на другую установку сверяйте размер и +SHA-256 соответствующего модуля в этой таблице. ```text enumerate adapters and video modes @@ -173,10 +175,11 @@ struct Node38 { }; ``` -`slot_index` образует матрицу `3 LOD x 5 groups`. Выбор выполняется как -`slot_index[lod * 5 + group]`; `0xFFFF` означает отсутствие geometry для этой -комбинации. Поле `parent_or_link` участвует в иерархии или связи узлов, но -название остаётся описательным. +`slot_index` хранит 3 состояния по 5 LOD: `slot_index[state * 5 + lod]`; +`0xFFFF` означает отсутствие геометрии для комбинации. Публичные Rust-обёртки +сохраняют исторические имена `Lod` и `Group`: аргумент `Lod` задаёт нативное +состояние (0..2), а `Group` — нативный уровень LOD (0..4). Поле `parent_or_link` +участвует в иерархии или связи узлов, но название остаётся описательным. Type 2 начинается с header `0x8C`, затем содержит slots по 68 байт: @@ -269,7 +272,7 @@ static_assert(sizeof(Batch20) == 20); ```c for (Node& node : model.nodes) { Matrix node_world = parent_world * local_transform(node); - uint16_t sid = node.slot_index[lod * 5 + group]; + uint16_t sid = node.slot_index[native_state * 5 + native_lod]; if (sid == 0xFFFF) continue; Slot& slot = model.slots[sid]; @@ -306,17 +309,17 @@ render queues и сортировка, но связи данных остают struct AnimKey24 { float position[3]; float time; + int16_t qw; 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 -проверки: она может изменить крайние кадры. +На диске quaternion записан в порядке WXYZ; каждый signed component +декодируется делением на `32767.0`. Декодер переставляет поля в portable +`Pose.rotation = [x, y, z, w]` и оставляет их ненормализованными. Нормализация +выполняется при подготовке pose к sampling. Type 19 является массивом `uint16_t`; его `attr2` задаёт общее число кадров timeline. Для конкретного узла `anim_map_start` указывает на блок длиной @@ -622,14 +625,18 @@ Lightmap не является обычной diffuse texture. WEAR содерж отдельным slot или texture stage. Замена lightmap предварительным умножением в diffuse texture ломает LOD, atlas coordinates и динамическую модуляцию. -Тени проходят отдельным render pass. Terrain содержит пути для теней зданий и -роботов, ограничения максимального числа, detail level и smoothing. Реализованы -native visibility по шести Vulkan clip-плоскостям с радиусом sphere +100, -порядок кандидатов и лимит 20 до построения страниц, projected-size LOD, -directional/point light contribution, native page layout, raster, smoothing и -receiver projection с edge clipping. Unknown settings из `shade.cfg` читаются и -сохраняются по именам, а не заменяются произвольными modern defaults. Для native ray query -surface descriptor в `+0x10` требует наличия всех переданных bits, а descriptor +Vulkan preview строит projected shadows для выбранных зданий и роботов, +отбрасывает spheres вне шести clip-плоскостей с радиусом `sphere + 100` и +сохраняет порядок видимых кандидатов; frame builder обрабатывает не более 20 +casters. Текущая реализация выделяет страницы в atlas `256 x 256` с 28 page +slots и использует одну smoothing pass. Вызов preview передаёт `fade = 1.0` и +LOD defaults: minimum LOD `1` и detail multiplier `1.0` отдельно для зданий и +роботов. Rust-путь не загружает `shade.cfg`: эти значения задаются кодом, а +параметры файла пока не влияют на preview. Проекция учитывает вклад +directional и point lights. + +Receiver projection обрезает projected quad по рёбрам треугольника. Native ray +query descriptor в `+0x10` требует наличия всех переданных bits, а descriptor в `+0x14` исключает поверхности с любым переданным bit. Общий shadow query использует full exclusion mask `0x2000`; actor four-ray path получает compact mask `0x8008`, который landscape boundary разворачивает в full mask @@ -923,6 +930,31 @@ sun-ray queries поэтому используют исходные коордÐ Эти два указателя нельзя подменять друг другом. Формулы камеры и размещения реализованы и проверяются в `fparkan-render`. +## LOD ландшафта + +У `Land.msh` список slot-ссылок terrain хранится в 38-байтовых записях +Node38, а диапазоны граней — в 68-байтовых Slot68. В текущем runtime первые +две ссылки Node38 выбирают соответственно LOD0 и LOD1; каждая запись слота +задаёт начало и число граней в исходном массиве `TerrainFace28`. Все грани +проверяются на единственное назначение, а выходящий за массив, повторный или +неподдерживаемый диапазон считается ошибкой загрузки. + +Первый рендер карты, terrain-запросы высоты и пересечения, receiver-ы теней и +обрезка контуром зданий используют только LOD0. Отбор сохраняет порядок +исходных граней, а контурный проход отдельно переводит индекс спроецированной +грани обратно в исходный. LOD1 пока не подставляется по расстоянию: такой +переход не реализован. + +```rust +let assignments = mesh.face_lod_assignments()?; +for (face, assignment) in mesh.faces.iter().zip(assignments.iter()) { + if assignment.lod != TerrainLod::Lod0 { + continue; + } + // Build terrain query geometry from this source face. +} +``` + ## Материалы ландшафта Рядом с `Land.msh` лежат `Land1.wea` и `Land2.wea`. Старший байт |
