diff options
Diffstat (limited to 'docs/reference')
| -rw-r--r-- | docs/reference/atmosphere.md | 236 | ||||
| -rw-r--r-- | docs/reference/environment-effects.md | 203 | ||||
| -rw-r--r-- | docs/reference/materials.md | 80 | ||||
| -rw-r--r-- | docs/reference/msh.md | 106 | ||||
| -rw-r--r-- | docs/reference/original-binaries.md | 33 | ||||
| -rw-r--r-- | docs/reference/render-frame.md | 8 |
6 files changed, 633 insertions, 33 deletions
diff --git a/docs/reference/atmosphere.md b/docs/reference/atmosphere.md new file mode 100644 index 0000000..65ed176 --- /dev/null +++ b/docs/reference/atmosphere.md @@ -0,0 +1,236 @@ +# Атмосфера и небо + +Атмосфера Parkan задаётся двумя связанными ресурсами: `sky.ske` хранит +расписание, цвета и параметры, а `sky.wea` сопоставляет позиционные строки с +материалами. Ресурсы сначала разбираются в `TypedAtmosphere` и `SkyMaterials`, +затем на каждом кадре из расписания получается `AtmosphereFrame`. Модуль +`fparkan-fx::sky` превращает этот кадр в данные для рендерера: постоянную +геометрию купола, динамические цвета вершин, небесные слои, положение +светил, туман и нижнюю границу освещения. + +## Игровые сутки и смена дня и ночи + +Вызов `TypedAtmosphere::sample(real_seconds)` принимает секунды от начала +повторяющегося расписания. Дорожки идут в порядке файла и занимают свои +реальные длительности; после последней дорожки время возвращается к началу. +Внутри выбранной дорожки игровой день всегда проходит от 00:00 до 24:00: + +```text +track_seconds = real_seconds_in_track + day_seconds = track_seconds / track_duration * 86400 +``` + +Время ключа вычисляется по часам и минутам даты. Нативный конструктор +игнорирует сохранённое поле секунд, поэтому для планировщика используется: + +```text +duration = (end_hour * 60 + end_minute) * 60 +key_time = floor(duration * (key_hour * 60 + key_minute) * 60 / 86400) +``` + +Сэмплер ищет предыдущий и следующий ключ во всём расписании. Переход через +конец дорожки или конец цикла интерполируется так же, как переход внутри +дорожки. `SkeDate::seconds_of_day()` сохраняет все три компонента даты для +инструментов; оно не меняет формулу реального времени расписания. + +День и ночь в самом формате не являются отдельным флагом. Их вид получается +из интерполированных цветовых параметров купола и состояния объектов солнца и +луны. Ключи с kind `0` и `1` запускают и останавливают небесное светило. Для +активного интервала модуль восстанавливает рождение объекта: ищет ближайший +предыдущий ключ `SunStart`, следующий соответствующий `SunStop`, вычисляет +реальное время начала и длительность, включая переход через цикл, и сохраняет +эти значения в `SunBirth`. + +Имя в первой фиксированной строке определяет объект. Для строки `sun` +нативный код использует углы `[90, 30]` градусов и строку материала 3; для +`moon` — `[0, 50]` и строку 4. Начальный вектор орбиты строится по формуле +`[cos(theta), 0, -sin(theta)]`, где + +```text +theta = (clamp((now - birth_start) / lifetime, 0, 1) * 1.2 - 0.1) * pi +``` + +Затем к нему применяется матрица рождения. У солнца и луны независимые +интервалы и независимые направления: при отсутствии активной луны нельзя +подменять её направление противоположностью солнца. `SunFrame::color` — это +единственный primary directional RGB. Второй источник получает направление +`-SunFrame::direction` и RGB из `SunSample::packed[1]`; оба источника для +активного объекта находятся в `SkyFrame::directional_lights` (сначала солнце, +затем луна). Экранная геометрия использует `-SunFrame::direction`, а +`world_center` размещает её по нативной формуле относительно camera +translation и половины far plane. Материал, строка и состояние объекта +берутся из его `SunBirth` и таблицы `sky.wea`. + +## Купол + +Оригинальный `CSky` строит конечный купол, а не кубическую skybox. Значения +конструктора, подтверждённые в Terrain, такие: высота `10000`, +`theta_max = pi / 4`, вертикальный масштаб `1`, `16` азимутальных секторов и +`5` колец. Радиус вычисляется как + +```text +radius = height * 0.5 / sin(theta_max * 0.5)^2 +``` + +Вершина с индексом 0 находится в зените `[0, 0, height * z_scale]`. +Остальные вершины идут в порядке `azimuth`, затем `ring`; для них + +```text +phi = azimuth / azimuth_count * 2*pi +theta = (ring + 1) / rings * theta_max +x = radius * sin(theta) * sin(phi) +y = radius * sin(theta) * cos(phi) +z = (radius * cos(theta) + height - radius) * z_scale +``` + +На сектор приходится один треугольник веера и по два треугольника на каждую +пару соседних колец. Текстурные координаты трёх исходных стадий используют +масштабы `[1, 15, 3]` для `position.x / radius` и `position.y / radius`. +Цвет зенита и первого пояса берётся из palette 12. Следующие пояса используют +группы palette 8..11, 4..7 и 0..3 с интерполяцией по азимуту. + +Позиции, signed normal bytes, UV и индексы создаются один раз через +`SkyGeometryConfig::mesh`/`SkySystem::new`. Последующие кадры меняют только +цвета вершин методом `SkyMesh::update_colors`; индексы и topology не +пересоздаются. Ошибка конфигурации возвращается как `SkyGeometryError`, а +ошибка входного кадра — как `SkyUpdateError`; они не превращаются в пустой +буфер. Рендерер может загрузить `SkySystem::mesh()` в статический +vertex/index buffer и обновлять только небольшой цветовой диапазон. + +Нормали купола повторяют native `Terrain47BFF` contract: каждый компонент +хранится как signed `i8`, округляется режимом nearest-even и умножается на +`127.0`. `SkyVertex::normal_vector()` декодирует эти bytes делением на `127`; +зенит имеет `[0, 0, 127]`. Вершинный backend должен передавать signed byte +representation без трактовки его как unsigned `0..255`. + +## Облака, звёзды и спрайты + +`sky.wea` — позиционная таблица. В стандартном AutoDemo строки имеют роли: + +| Строка | Роль | Имя в AutoDemo | +|---:|---|---| +| 0 | фон/туманность | `ENV_NEBULA_0` | +| 1 | звёзды | `ENV_STARS` | +| 2 | облака | `ENV_CLOUDS` | +| 3 | солнце | `ENV_SUN_3` | +| 4 | луна | `ENV_MOON` | +| 5 | первый flare | `ENV_FLARE_00` | +| 6 | второй flare | `ENV_FLARE_01` | +| 7 | снежинка | `SNOWFLAKE` | +| 8 | капля дождя | `RAIN_DROP` | + +Это наблюдаемые строки, а не глобальные имена, на которые можно полагаться в +миссии. `SkyMaterials::parse` сохраняет числовой id и имя каждой строки; +миссия может заменить любой материал. `SkyLayerKind` и `SkyLayerFrame` дают +роль, позиционную строку, активность, intensity, UV stage и фазу времени. + +Нативный порядок купола находится в `SkyFrame::passes`: экранный gradient, +nebula row 0 со stars row 1 как второй texture (material mode 4), dome +gradient и clouds row 2. Для nebula/stars/clouds используются UV scales +`1/15/3`; clouds получают translation Z `-5000` относительно камеры и +directional RGB из `sky.packed[0]`. Это четыре фиксированных pass, а не три +независимых цветных слоя. + +Цвет первого screen-gradient и последнего пояса вычисляет +`SkyFrame::screen_gradient(yaw_radians, glare_rgb_delta)`. Native `CSky` сначала +округляет `(yaw + pi) * 180 / pi` режимом nearest-even, выбирает сектор +`(floor(degrees / 90) - 2) mod 4`, а затем смешивает соседние цвета +`SkySample::colors[0..4]`. Остаток сектора умножается на точный коэффициент +из PE `65F64` (`0x3C360B61`, примерно `0.01113567`), alpha результата равна +`255`. `glare_rgb_delta` — входной RGB delta от light manager: для каждого +канала это `max(current_primary_light_rgb - base_sun_rgb, 0)`. Каждый компонент +затем преобразуется так: `d <= 1 -> d*0.8`, `1 < d <= 3 -> d*0.1 + 0.7`, +`d > 3 -> 1`. +`SkyGradientFrame::horizon_floor` задаёт RGB minimum для dome palette, а +`clamp_dome_color` применяет его, сохраняя alpha исходного цвета. + +Солнце, луна и две строки flare являются направленными экранными слоями. +Нативный quad использует UV +`[[.005,.005],[.005,.995],[.995,.995],[.995,.005]]`. Полуразмеры sun/moon в +пикселях вычисляет `sprite_half_size_pixels`: `viewport_width / horizontal_fov +* .325 * .5 * values[0/1]`, где `values` — первые два tail float SKE. +Цвет sprite и его alpha берутся из `SunSample::packed[0]`, отдельно от +directional `SunFrame::color`. + +`SkyFrame::sun_optics` принимает projected sun, viewport, camera forward и +результат world-ray visibility. Он применяет native 15-degree glare cone и +возвращает фиксированные 12 `FlareQuad`; flare slots активны только при +видимом солнце, `length(primary RGB) > 1.1` и cone amount `<= .1`. При cone +`> .1` остаётся только glare boost. Renderer отвечает за сам projection и +occlusion query. + +Дождь, снег и молния используют те же состояния расписания, но их camera-local +геометрия и звуки обновляются `EnvironmentSystem`. Для активного интервала +ресурсы берутся только из ключа, который его запустил. Интенсивность дождя, +снега и молнии — четвёртый trailing float; цвет активной погоды использует +RGB palette 12 с минимумом `80/255` на канал и alpha `150/255`. + +## Туман и свет + +Нативное обновление `CSky` записывает две дальности и один packed RGB. +`FogFrame` повторяет этот контракт: + +```text +fog.start = sky.values[0] * 700 +fog.end = sky.values[1] * 700 +fog.color = RGB(sky.packed[1]) / 255 +``` + +Поля не скрывают порядок исходных параметров: значения сохраняются в +`SkySample`, а преобразование выполняется только в `fog_frame`. Vulkan path +передаёт цвет и дальности в frame uniforms; vertex shader вычисляет линейный +коэффициент по расстоянию до камеры, а fragment shader смешивает RGB материала +с fog color в ветвях combiner, которые используют туман. Конкретный draw state +может обходить это смешивание. + +Цвет primary света солнца — RGB palette 12, нормированный делением на 255 и +умноженный на trailing parameter 2. `SunFrame::direction` — нормированный +вектор после матрицы рождения и направление primary light; sprite/world +geometry использует его противоположность. Второй directional RGB берётся из +packed-параметра 1 без дополнительного умножения intensity. Когда объект +неактивен, sampled поля сохраняются, но оба light slot и sprite inactive. + +В native `CShade` sampled RGB из состояния атмосферы также устанавливает +глобальную нижнюю границу освещения. Для каждого RGB-канала она объединяется +операцией `max` с накопленным цветом материала. Поэтому `SkyFrame` отдаёт +`lighting_floor` из того же sampled packed RGB, а владелец renderer передаёт +его в глобальный light state. Фиксированный ambient-цвет в этом месте меняет +ночной уровень и не соответствует цепочке Terrain → CShade → Ngi32. + +## Бинарный формат `sky.ske` + +Файл little-endian, версия 5. В начале находятся `i32 marker = -1`, +`u32 version = 5` и число дорожек. Дорожка содержит `u32 version = 1`, число +ключей, две даты по 32 байта и ключи. Дата состоит из восьми `u32`; для +планировщика значимы слова 3, 4 и 5 (часы, минуты, секунды), а остальные слова +сохраняются без интерпретации. + +Ключ имеет `u32 version = 3`, дату, raw kind, opaque word, четыре packed ARGB +цвета, два `f32`, пятнадцать packed ARGB цветов, шесть length-prefixed строк, +четыре trailing `f32` и список length-prefixed ссылок. Строка хранится как +`u32 byte_length` и ровно столько байт без завершающего NUL. Неизвестный kind +сохраняется как `AtmosphereKind::Unknown`, как и opaque слова/строки. + +В конце находятся дата и два opaque `u32`. Декодер ограничивает количества и +размеры строк, отклоняет NaN/Infinity во float-полях и требует точного конца +файла. Трейлер выбирает исходную дорожку и позицию час/минута; метод +`initial_offset_seconds()` складывает длительности предыдущих дорожек и +применяет ту же формулу floor. + +Из интерполированного ключа `SkySample` формируется в исходном порядке: + +```text +colors = [header[1], header[2], header[0], header[3], + palette[0], palette[3], palette[1], palette[2], + palette[4], palette[7], palette[5], palette[6], palette[8]] +values = header_values +packed = [palette[11], palette[13]] +``` + +Солнце и луна используют palette 12 и trailing parameter 2 для RGB, +parameters 0 и 1 сохраняются как raw values, а packed-параметры светила — +palette 10 и 14. Kind `0/1` управляет солнцем и луной по имени, `3/4` — +дождём, `5/6` — снегом, `8/9` — молнией; kind `2` и `7` меняют +интерполируемое состояние. Поля, для которых в native renderer ещё нет +проверенного назначения, остаются raw в Rust API и не получают придуманных +значений по умолчанию. diff --git a/docs/reference/environment-effects.md b/docs/reference/environment-effects.md new file mode 100644 index 0000000..31024f7 --- /dev/null +++ b/docs/reference/environment-effects.md @@ -0,0 +1,203 @@ +# Эффекты окружения + +Погода в Parkan состоит из двух связанных частей. Файл `sky.ske` задаёт +расписание: когда начинается дождь или снег, как меняются интенсивность и цвет, +какие имена ресурсов принадлежат активному интервалу. Таблица `sky.wea` задаёт +материалы, которыми эти осадки рисуются. `fparkan-fx::environment` соединяет +оба входа с текущей камерой и возвращает один `EnvironmentFrame`. + +```text +sky.ske + -> AtmosphereFrame: время, цвет, интенсивность, ссылки на ресурсы +sky.wea + -> SkyMaterials: имена материалов строк 7 (снег) и 8 (дождь) +камера + EnvironmentSystem + -> EnvironmentFrame: мировые частицы, экранные квадраты, гром и дождевой loop +``` + +Модуль не владеет графическим устройством или звуковой картой. Renderer +разрешает имя материала и отправляет геометрию в прозрачный проход, а audio +backend разрешает архив и имя звука. Это позволяет одной и той же модели +погоды работать в preview и в игровом цикле. + +## Входы и границы + +Для расписания используется такой вызов: + +```rust +let frame = environment.update_atmosphere_with_materials( + dt_seconds, + &atmosphere_frame, + &sky_materials, + camera, +); +``` + +`AtmosphereFrame` передаёт ссылки из стартового ключа активного интервала. +Для дождя первая ссылка используется как звук фонового loop. Строка вида +`archive/name` разделяется на архив и имя. У простой строки `name` архив +остаётся пустым: звуковой владелец подставляет библиотеку, выбранную миссией. +Имя не заменяется глобальным именем из AutoDemo. + +`SkyMaterials` хранит разобранную таблицу, поэтому строки 7 и 8 являются +положением в формате исходной игры, а не зашитыми названиями. В AutoDemo там +находятся `SNOWFLAKE` и `RAIN_DROP`; миссия может передать другие имена. Если +активной погоде не назначен материал, частицы для неё не создаются. + +На входной границе конечные значения приводятся к безопасному диапазону: +`dt_seconds` и интенсивность неотрицательны, интенсивность и каждый компонент +цвета ограничены единицей, а нечисловые значения заменяются нулём. Камера +получает конечную ортонормированную основу, FOV, aspect ratio и размер +viewport от владельца кадра. + +## Объём и движение осадков + +Исходный эмиттер хранит прямоугольный объём в координатах камеры. Его +эталонные параметры таковы: + +```text +near = 2 +far = 50 +half_angle_x = 0.65 +half_angle_y = 0.4875 +``` + +Для эталонной камеры: + +```text +half_x = tan(half_angle_x) * far +half_y = tan(half_angle_y) * far +origin = [near, -half_x, -half_y] +extent = [far - near, 2 * half_x, 2 * half_y] +``` + +Текущий FOV и aspect ratio пересчитывают две боковые грани по той же глубине: + +```text +half_y = tan(vertical_fov / 2) * far +half_x = half_y * aspect_ratio +``` + +Количество частиц зависит от объёма и интенсивности. Вспомогательная +формула использует округление FISTP к ближайшему чётному целому: + +```text +N = round_even( + clamp(current_volume / reference_volume, 0, 1) + * density + * intensity + * 1000 + ) +``` + +Текущий эмиттер передаёт `density = 1`. Публичный +`PrecipitationVolume::particle_count_for` оставляет этот множитель явным для +владельца, который хранит собственную плотность. + +Порядок обновления частицы важен: + +1. при создании три 15-битных значения детерминированного генератора + заполняют локальный объём; +2. локальная точка переводится через основу камеры в мировую позицию; +3. мировая позиция интегрируется с мировым вектором скорости; +4. для проверки границ позиция временно переводится обратно в локальные + координаты, и к каждой оси применяется floor-based wrap; +5. после wrap точка снова переводится в мир, а её хвост сбрасывается на голову. + +Скорости не принадлежат системе координат камеры: + +```text +rain = [ 0.5, 0, -60 ] +snow = [ 0.5, 0, -4 ] +``` + +Это мировые векторы. Поворот камеры меняет область появления, wrap и +проекцию, но не вращает уже движущуюся каплю вокруг наблюдателя. В +`EnvironmentPrimitive::Particle` `position`, `velocity`, `world_head` и +`world_tail` имеют мировые координаты. + +Размер частиц состоит из коэффициента класса и масштаба погодного объекта: + +```text +rain_size = scalar * 0.0065 +snow_size = scalar * 0.0195 +``` + +`scalar` вычисляется из той же проекции, которую передаёт владелец кадра: + +```text +horizontal_fov = 2 * atan(tan(vertical_fov / 2) * aspect_ratio) +scalar = viewport_width / horizontal_fov +``` + +Это соответствует запросу размера native camera (`Terrain` primary vtable +slot `+6c`): ширина `RECT` делится на горизонтальный FOV в радианах. Поэтому +`Camera::with_projection(...).with_viewport([width, height])` должен получать +реальный viewport каждого кадра; отдельного setter-а масштаба нет. + +`ScreenBillboard` содержит мировые голову и хвост, их проекцию и четыре NDC +угла с UV. `half_size` остаётся native-величиной в пикселях, а обе глубины +хранятся отдельно; renderer не смешивает эти единицы. Дождь использует +предыдущую мировую голову как хвост и строит полосу перпендикулярно экранному +направлению движения. Снег остаётся квадратом в текущей голове, использует +исходную таблицу знаков и минимальный depth fade `0.1`. Renderer сам решает, +как загрузить этот квадрат и как смешать его материал. + +## Молния и звуковые события + +`decode_env_lightning_fxid` извлекает из FXID длительность, ссылки opcode 3 и +2, первые четыре числа opcode 1 и полный body opcode 1. Имена не подменяются +глобальным whitelist-ом: материал и звук принадлежат выбранному FXID. +`LightningEffect` устанавливается через `set_lightning_effect`; без него +погодный таймер не создаёт визуал или звук. + +Активный объект использует следующий таймер. При интенсивности `I` сначала +применяется `min(I, 0.95)`, затем для случайного `U` из 15-битного диапазона +вычисляется задержка: + +```text +delay_ms = round_even((1 - min(I, 0.95)) * 60000 * U) +``` + +После достижения срока выбираются X и Y из переданных `LightningBounds`, а Z +берётся без изменения. Если границы не заданы, используется позиция камеры. +Следующая попытка разрешена через 6000 миллисекунд. Длительность уже +созданного FX берётся из его заголовка. Renderer получает native descriptor +`[40, 40, 600]`, начало эффекта на `sampled_z + 300`; при нулевом локальном +смещении концы quad находятся на `sampled_z` и `sampled_z + 600`. Opcode 3 +задаёт материал, локальное смещение, scale и lifetime visual quad. Opcode 1 +вычисляется отдельно как динамический point light и не задаёт размеры или UV +quad. +CPU не подменяет numeric поля шириной или линейным lifetime fade. + +Звуки возвращаются рядом с графикой как `SoundEvent`: + +```text +StartLoop -> начать дождевой loop +SetLoopVolume -> обновить его громкость при интерполяции погоды +StopLoop -> остановить loop +OneShot -> воспроизвести разовый звук молнии +``` + +Каждое событие содержит архив, имя, мировую позицию и линейную громкость. +Для FX-звука также передаются native `min_distance`, `max_distance` и +`frequency_ratio`; у дождевого loop используются `0`, бесконечность и `1`. +Событие `OneShot` молнии использует ту же случайно выбранную позицию, что и +визуальный эффект, а его диапазон и частота читаются из opcode 2: в +поставленном `env_lightning` это `100`, `1500` и `1`. Audio backend загружает +объявленный архив и запись лениво, кэширует проверенный sample, создаёт +отдельный spatial source, применяет DirectSound range/pan и текущий listener. +Отсутствие устройства вывода не должно останавливать симуляцию. + +## Что остаётся за владельцем + +`EnvironmentSystem` не извлекает камеру из renderer, не разрешает материалы +или звуки и не выполняет финальный GPU pass. Владелец должен передать основу +камеры, projection, реальный viewport, `SkyMaterials` и декодированный FXID. +Из projection и viewport модуль сам получает native scalar осадков. + +CPU-контракт проверяет движение, wrap, количество, время молнии и порядок +звуковых переходов. Он не заявляет совместимость с нераскрытой семантикой +всех FX opcode, с точным native RNG sequence или с конкретным mixer/device +backend. Подробное описание связи с listener и mission audio находится в +[томе VI](../tomes/06-behavior.md#погода-осадки-и-звуковые-события). diff --git a/docs/reference/materials.md b/docs/reference/materials.md index 8146a2c..bfeccf4 100644 --- a/docs/reference/materials.md +++ b/docs/reference/materials.md @@ -38,13 +38,9 @@ MAT0 имеет type ID `0x3054414D`, обычно расположен в `Mate ```c #pragma pack(push, 1) -struct Mat0PrefixV4Plus { +struct Mat0Header { uint16_t phase_count; uint16_t animation_block_count; - uint8_t metadata_a; - uint8_t metadata_b; - uint32_t metadata_c_raw; - uint32_t metadata_d_raw; }; struct Phase34 { @@ -54,8 +50,78 @@ struct Phase34 { #pragma pack(pop) ``` -Versioned fields читаются только если версия их содержит. Для старых версий -используются runtime defaults, а raw values сохраняются. +`attr1` читается из NRes entry metadata и сохраняется вместе с разрешённым +материалом. Это runtime flags материала; его нельзя путать с `attr2`, который +задаёт версию payload. Versioned prefix bytes читаются только если версия их +содержит, а raw values сохраняются для последующих слоёв. + +### Pipeline category + +Call site извлекает native category как `(attr1 >> 2) & 0xF` и передаёт её в +`LegacyPipelineState::with_material_category`. Таблица содержит пять +подтверждённых категорий; helper возвращает `None` для остальных значений и не +меняет уже выбранные depth и cull state. + +| category | source/destination colour factors | alpha test | +| ---: | --- | --- | +| 0 | `ONE / ZERO`, blending disabled | disabled | +| 1 | `SRC_ALPHA / INV_SRC_ALPHA` | enabled, `GREATER_EQUAL` | +| 2 | `SRC_ALPHA / ONE` | enabled, `GREATER_EQUAL` | +| 3 | `ZERO / SRC_COLOR` | enabled, `GREATER_EQUAL` | +| 4 | `DEST_COLOR / SRC_COLOR` | enabled, `GREATER_EQUAL` | + +The mapping comes from the native five render-state pairs selected by the +material category. Alpha reference remains draw-range data, while the Vulkan +pipeline key includes the blend mode, depth mode, cull mode and alpha-test +variant so ranges with different native state do not share a pipeline. + +Каждая phase занимает 34 байта: 18 parameter bytes и 16-byte C string имени +текстуры. Loader переводит параметры в коэффициенты так: + +| bytes | runtime value | +| --- | --- | +| `p0..p2` | additive RGB, `p / 255` | +| `p3` | opacity, `p * 0.01` | +| `p4..p7` | directional values, `p / 255` | +| `p8..p11` | specular values, `p / 255` | +| `p12..p15` | extra values, `p / 255` | +| `p16` | integer power | +| `p17` | signed TEXM page index | + +Texture name и page index выбираются из текущей phase. Они не смешиваются с +соседней phase. + +## Animation + +После phase table идут `animation_block_count` плотных блоков. Каждый блок имеет +`u32 header_raw`, `u16 key_count`, затем `key_count` записей по три `u16`: + +```text +u32 header_raw +u16 key_count +repeat key_count: + u16 phase_index + u16 end_time_ms + u16 raw_k2 +``` + +`header_raw & 7` задаёт режим: `0` loop, `1` ping-pong, `2` clamp, `3` +random-per-query. Значения `4..7` сохраняются как unknown. `header_raw >> 3` +является interpolation mask: bits `1`, `2`, `4`, `8` смешивают группы +`p0..p2`, `p4..p6`, `p8..p10`, `p12..p14`, а bit `16` смешивает `p3`. +Компоненты `p7`, `p11`, `p15`, `p16` и `p17` копируются из текущей phase. + +Время для блока вычисляется как `clock_ms - wear_row_start_ms` с wrapping +`u32` subtraction. Для loop берётся остаток от последнего `end_time_ms`, для +ping-pong нечётный цикл идёт в обратном направлении, clamp после duration +выбирает последнюю phase, а random-per-query использует переданное случайное +значение по модулю duration. Ключ выбирается по интервалу +`previous_end <= local_time < end_time`; следующий ключ циклически замыкается на +первый. На ping-pong turnaround и clamp после duration native routine проходит +общий float interpolation path, включая unsigned subtraction endpoint arithmetic; +это может дать extrapolated coefficients при ненулевой mask. Переданный в +sampler `animation_block_index` уже выбран вызывающим runtime из block table; +packed WEAR handle и номер строки WEAR являются отдельными значениями. ## Fallback diff --git a/docs/reference/msh.md b/docs/reference/msh.md index 774e388..165b750 100644 --- a/docs/reference/msh.md +++ b/docs/reference/msh.md @@ -19,7 +19,7 @@ type 10 strings and node names type 13 Batch20 records type 15 auxiliary stream type 17 auxiliary data -type 18 rare stream +type 18 packed UV1 (optional secondary coordinates) type 19 animation frame map type 20 rare auxiliary table ``` @@ -40,27 +40,77 @@ struct Node38 { }; ``` -`slot_index[lod * 5 + group]` выбирает geometry slot. `0xFFFF` означает -отсутствие геометрии для комбинации LOD/group. +В исходном layout это 3 состояния по 5 LOD: `slot_index[state * 5 + lod]`. +Публичные Rust-обёртки пока сохраняют исторические имена `Lod` и `Group`, +поэтому в `selected_slot` аргумент `Lod` задаёт native state (0..2), а +`Group` задаёт native LOD (0..4). `0xFFFF` означает отсутствие геометрии для +комбинации state/LOD. -Validated `ModelAsset` также сохраняет decoded type 8 keys и type 19 map как +Validated `ModelAsset` сохраняет decoded type 8 keys и type 19 map как `ModelAnimation`. `node38_fallback_pose` возвращает pose по `fallback_key`, -то есть доказанный static input. `parent_or_link == 0xFFFF` означает root; -иначе это parent index, обязательно меньший индекса child. Этот контракт -подтверждён на licensed animation gates обеих частей и защищён fallback-ом: -модель с нарушенным порядком не получает придуманную hierarchy. - -В legacy-camera static preview стандартный узел уже получает свой fallback pose -до внешнего TMA/Iron3D transform. Parent pose поворачивает child translation, -затем translation суммируется, а rotations умножаются; после полученной global -pose применяется `Rz * Ry * Rx`, scale и mission translation. Геометрия -намеренно дублируется на draw-range узла, потому что один source vertex может -быть нарисован разными node poses. Это static fallback hierarchy, а не полная -animation parity: dynamic type-19 frame-map sampling остаётся отдельной задачей. - -Для воспроизводимого исследования `fparkan-cli model inspect --root <game> ---archive <archive> --resource <model.msh>` выводит Node38 metadata, включая -parent index, fallback key и наличие LOD0/group0 geometry. +а `node38_fallback_hierarchy` собирает parent-before-child hierarchy. +`node38_sampled_hierarchy` использует type 19 map в пределах объявленного +числа кадров и интерполирует соседние type 8 keys; при отсутствии usable map +или выходе за frame count используется fallback key. Для покомпонентного +sampling `node38_sampled_hierarchy_at_times` принимает отдельный optional float +time для каждого узла: `Some(time)` выбирает type 19 frame по округлению +`time - 0.5` к ближайшему целому с ties-to-even, затем интерполирует выбранный +type 8 key и его следующий key в исходный float time; `None` использует +fallback key. Значения должны быть конечными и неотрицательными. Это +portable-поведение при default-nearest rounding; полного совпадения со всеми +режимами округления x87 оно не заявляет. `parent_or_link == 0xFFFF` +означает root, иначе это parent index, который должен быть меньше индекса +child. Модель с нарушенным порядком не получает придуманную hierarchy. + +Type 8 записывает quaternion в порядке WXYZ, после decode `AnimKey24` хранит +его как XYZW. Native AniMesh `+0x12560` строит local matrix через NGI32 +`g_FastProc + 0x38`; scalar implementation выдаёт transpose обычной active +`Pose` basis (`M01=2(xy+zw)`, `M02=2(xz-yw)`, `M10=2(xy-zw)`). Поэтому путь +Node38 MSH сопрягает source-local quaternion ровно один раз после sampling и +до hierarchy composition (меняет знаки компонент `x`, `y`, `z`). Для fallback +poses применяется та же граница. Общий animation decoder и другие форматы +анимации не изменяются. + +В legacy-camera static preview без `--static-animation-frame <u16>` используются +сохранённые начальные значения из явной ссылки каждого prototype на `.ctl`. +Узлы без control binding получают time `0`. Для каждой component instance +обрабатываются её CTLD bindings, поэтому общий deduplicated MSH не смешивает +controls разных компонентов. Строки идут в source order, и последняя включённая +строка для одного узла заменяет предыдущую. Flag `0x01` выполняет один wrap +значения blend на единицу, иначе значение ограничивается `[0, 1]`; flag `0x02` +инвертирует результат (`1 - blend`), а строки с `0x04` пропускаются. Выбранный +float time равен `(1 - blend) * frame_a + blend * frame_b`. Если он выходит за +type 19 map, MSH sampler использует fallback key; если после одной wrap/clamp и +invert получается отрицательное или нечисловое время, preview завершится +понятной ошибкой. Явный `--static-animation-frame` остаётся global override +для всех компонентов. Parent pose поворачивает child translation, затем +translation суммируется, а rotations умножаются; после полученной global pose +применяется `Rz * Ry * Rx`, scale и mission translation. Геометрия намеренно +дублируется на draw-range узла, потому что один source vertex может быть +нарисован разными node poses. Это только начальная статическая pose: она не +запускает игровой Control/AI и не обещает полного совпадения с runtime или +всеми x87 rounding modes. + +## Vertex streams + +Основные vertex streams имеют фиксированный source index. У type 3 `attr1` +задаёт количество вершин, а `attr3` — размер записи в байтах. Type 4 +содержит четыре signed bytes нормали. Type 5 и optional type 18 содержат по +два little-endian `uint16` на вершину: + +```c +struct PackedUv16x2 { + uint16_t u; + uint16_t v; +}; +``` + +Type 5 — primary UV0, type 18 — secondary UV1 для lightmap/detail stage. +Для type 18 loader требует `attr3 == 4`, совпадение `attr1` и длины payload, +а также одинаковое число записей с type 3. Обе пары декодируются как +`packed / 1024.0`; renderer обращается к UV1 по тому же `source_index`, что и +к UV0. Отсутствующий type 18 означает, что secondary stage не имеет authored +координат. ## Slot and batch @@ -86,16 +136,20 @@ Type 13 задаёт draw ranges: #pragma pack(push, 1) struct Batch20 { uint16_t batch_flags; - uint16_t material_index; - uint16_t opaque4; - uint16_t opaque6; + uint16_t material_index_hi; // +0x02, native high selector/slot field + uint8_t material_index; // +0x04, WEAR material selector + uint8_t lightmap_index; // +0x05, 0xFF means no lightmap + uint16_t local_batch_index; // +0x06 uint16_t index_count; uint32_t index_start; - uint16_t opaque14; + uint16_t vertex_count; // +0x0E uint32_t base_vertex; }; #pragma pack(pop) ``` -Index check выполняется как `base_vertex + index < vertex_count` для всего -используемого slice. +Byte `material_index` выбирает строку WEAR; high word at `+0x02` сохраняется +отдельно для native overrides. Loader проверяет, что +`index_start + index_count` остаётся внутри type 6, а каждый использованный +индекс через `base_vertex` попадает в position stream. `vertex_count` и +`local_batch_index` доступны renderer для исходного draw contract. diff --git a/docs/reference/original-binaries.md b/docs/reference/original-binaries.md new file mode 100644 index 0000000..05c0141 --- /dev/null +++ b/docs/reference/original-binaries.md @@ -0,0 +1,33 @@ +# Оригинальные модули + +RVA в книге относится к определённой сборке DLL. SHA-256 позволяет +проверить, что сравнивается тот же код. + +## Совпадающие модули частей 1 и 2 + +| Binary | SHA-256 | Size | Notes | +| --- | --- | ---: | --- | +| `Ngi32.dll` | `bab9840d94f4e4e74ffc26677724fa896cf4823845504d09a9e025f80016edf5` | 253952 | Shared low-level render/resource/audio boundary | +| `World3D.dll` | `17e4a3089b2583a8cf2356c9db0390b1aba138356a09130d79b4e7e4791da61e` | 208896 | Shared gameplay/world/render lifecycle baseline | +| `Terrain.dll` | `6d3e68f0e15b297f6c184af3113baf1f31e19c3326c18f0150dec659242ed667` | 708608 | Shared terrain/shade/world baseline | +| `iron_3d.exe` / `iron_3d_p2.exe` | `f476af85c034a4b4f34f49d0806e4dff397b5da0ee26d382a7674231144979f7` | 36864 | Shared launcher binary | + +## Сборка 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 | + +## Различающиеся модули + +Эти модули нельзя автоматически считать behavior-compatible между частями: + +- `AniMesh.dll` +- `Effect.dll` +- `iron3d.dll` +- `services.dll` +- `Control.dll` +- `ArealMap.dll` diff --git a/docs/reference/render-frame.md b/docs/reference/render-frame.md index ec78020..5add02b 100644 --- a/docs/reference/render-frame.md +++ b/docs/reference/render-frame.md @@ -69,6 +69,14 @@ projection matrix contract. ## 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; |
