- PVSM.RU - https://www.pvsm.ru -
Unreal Tournament 2004 — безусловная классика, но графически она застряла в эпохе Direct3D 8 и Fixed‑Function Pipeline. Три месяца реверс‑инжиниринга, сотни падений с Access Violation и ни строчки C++ в голове — история о том, как с помощью ИИ‑агента за 500 рублей я написал нативный DLL‑рендерер с картами нормалей, Blinn‑Phong Specular и постпроцессингом для движка Unreal Engine 2.
После выхода патча от OldUnreal, где игру перевели на полноценные 64 бита и внедрили OpenGL 3.3 с широкой совместимостью как со старым железом эпохи игры, так и с современными ПК, у меня появилась идея для эксперимента.
Было на пути и несколько проблем: Я не знаю C++. Не знаю его архитектуры, синтаксиса, и даже читать чужой код на C++ могу с большим трудом. У меня не было исходников игры. (да, есть слитые от Unreal Championship 2 и Unreal Warfare, но в них код сильно отличается от UT2004) Полное отсутствие каких‑либо знаний об устройстве движка, его архитектуре и особенностях. Всё это осложнялось ещё и невозможностью купить подписку на Claude Code или Codex. Пришлось работать на Gemini Flash через консольный Antigravity CLI. Зато цена входа — 500р на год.
Любой рендерер для Unreal Engine 2 — это внешняя динамическая библиотека, реализующая внутренний интерфейс URenderDevice. DLL рендерера — это мост между движком и железом. Движок общается с библиотекой через таблицы виртуальных функций — VTable.
В C++ порядок функций в VTable критичен до байта. Ошибка в смещении одной функции — вместо отрисовки кадра игра прыгает в случайный участок памяти и мгновенно падает.
[ Движок UE2 ] ‑> ( VTable: смещение +0×18 ) ‑> [ ENDrv.dll ] │
Ошибка на 4 байта? ‑> CRASH (Access Violation)
Первые две недели ушли на настоящую «игру в угадайку»: прогонка бинарников UT2004 через дизассемблер, изучение исходников UC2 и Unreal Warfare, попытки угадать набор функций в UT2004 и их точные смещения, пересборка библиотеки и запуск игры каждые 5–10 минут, логирование крашей и подгонка смещений на основе адресов падения.
Игра впервые запустилась со сплошным чёрным экраном и не упала только тогда, когда удалось с высокой точностью угадать 6 ключевых базовых функций из 32-х (на тот момент достоверно известно было лишь о 14 функциях). Картинки ещё не было, но DLL принята движком — это был первый прорыв! Игра не просто не вылетала, но и по звуку было слышно, что она работает: можно кликать по меню и «вслепую» запустить матч — было слышно, как бегают и стреляются боты. Работает!
Следующие несколько дней ушли на налаживание передачи вершин и вывод на экран хоть какой‑то геометрии в режиме каркаса (Wireframe). Необходимо было не просто объявить функции в виде заглушек (вместо реальной работы функции возвращали True или Null), но уже полностью реализовать их функционал. Осложняло работу ещё и то, что движок изначально разработан под DirectX, а OpenGL имеет свои особенности координатных систем и управления состоянием. Сначала удавалось вывести на экран только кашу из линий, но через десятки проб и ошибок мой dll наконец‑то выдал корректную картинку в режиме Wireframe.
В оригинале UT2004 работала с материалами через стейт‑машину старых видеокарт — так называемый Fixed‑Function Pipeline с комбинаторами (Combiner, Shader, FinalBlend). Современный OpenGL 3.3 Core Profile вообще не имеет фиксированного конвейера: всё нужно писать на шейдерах GLSL.
Около 4–6 недель ушло на воссоздание логики материалов «вслепую». Польза от исходников Unreal Warfare и UC2 практически нулевая, да ещё и ИИ начинал из‑за них сильно путаться — доходило до того, что иногда приходилось каждый ответ агента закидывать в другой ИИ для проверки логики и отлова галлюцинаций. В итоге было принято решение вообще не опираться на чужой исходный код и делать всё самостоятельно.
Главная трудность заключалась не в том, чтобы «просто нарисовать текстуру». Система материалов UE2 — это полноценный нодовый граф. Один материал типа UShader может содержать отдельные слоты под Diffuse, Opacity, Specular, SpecMask, SelfIllumination и SelfIllumMask, каждый из которых — потенциально ещё один материал. А в слоте Diffuse вполне может сидеть UCombiner — нода смешения, у которой есть Material1, Material2 и Mask, каждый из которых, в свою очередь, тоже может быть комбинером, текстурой с анимированными UV (TexPanner, TexRotator, TexScaler) или кубической картой окружения. Всё это нужно развернуть в GLSL, который не умеет в рекурсию и динамические циклы образца 2004 года — только в линейный граф с фиксированным числом шагов.
Пришлось реализовать систему «регистров комбинеров»: перед основным расчётом цвета шейдер последовательно вычисляет до 32 нод и складывает результат в массив. Каждая нода знает индексы своих входов (текстурный слот или другой регистр), операцию (Multiply, Add, AlphaBlend, Modulate и так далее) и маску. Дополнительно — по 16 текстурных слотов с независимыми матрицами UV‑трансформаций, поддержка кубических карт с отражением, проективные текстуры (Projector), анимированные панорамы и режим FinalBlend с альфа‑тестом. Под конец работы с текстурами удалось найти исходники 32-битной версии игры, но толку от них было немного: оригинальный код был целиком завязан на Direct3D 8 / Fixed‑Function. Впрочем, исходники всё же помогли поправить пару неприятных логических багов при обработке сложных многослойных текстур, материалов с альфа‑каналами и проблем с буфером глубины.
Пример одного из решений проблем: Смещения полей UTexture неизвестны (нет исходников) — поэтому код сам ищет их в памяти путём перебора и проверки логических условий. Нашёл — закэшировал навсегда.
Затем за одну неделю был интегрирован постпроцессинг — с ним сложностей почти не возникло, так как логика экранных эффектов (Depth of Field, Bloom, Chromatic Aberration) пишется на GLSL довольно прямолинейно и мало зависит от особенностей движка.
Главная цель проекта — внедрение PBR‑подобного освещения и карт нормалей. И эта задача оказалась даже сложнее, чем первоначальная подгонка VTable.
Для работы нормалей рендерер должен получать от движка информацию о пространственном положении полигонов, их тангенс/битангенс — но ничего подобного игра нам не предоставляет. Сам эффект нормалей подключить в шейдере удалось быстро: с помощью экранных производных (dFdx / dFdy) тангентный базис рассчитывается прямо в шейдере, что избавило от необходимости менять формат вертексов игры. Но возникла катастрофическая проблема с производительностью: игра безбожно тормозила, если в кадр попадало пара десятков моделей с нормалями.
В чём основная проблема: всё статическое освещение в игре (а динамического в ней практически ничего нет) запекается заранее при создании карты — цвет и яркость текстур модулируется лайтмап‑текстурой и векторными цветами вершин. Если упрощённо — это примерно то же самое, что в Photoshop наложить градиент на текстуру в режиме «умножение». Получается, наш рендерер вообще не видит источников освещения — мы просто рисуем текстуры на полигонах. Но так как наш dll является модулем игры, у нас есть прямой доступ ко всему, что происходит в движке «под капотом»: источники света найдены, параметры прочитаны.
Пересчитывать все источники света на честном попиксельном конвейере каждый кадр для всей геометрии оказалось слишком дорого — тем более что движок архитектурно не умеет работать более чем с одним ядром процессора. Пришлось внедрять глубокую оптимизацию с кэшированием: на всей геометрии сохраняется родной статический свет (Lightmaps и Vertex Colors), запечённый авторами карт 25 лет назад. Динамический попиксельный расчёт в GLSL отвечает только за дельту рельефа (Normal Map) и бликовое пятно (Blinn‑Phong Specular).
Итоговый цвет = (Lightmap Diffuse) + (NormalMap_Affect DirectLight) + BlinnPhong_Specular
Где NormalMap_Affect — это не полный свет, а только его «приросток» от рельефа: насколько перпендикуляр нормали отличается от плоской геометрии. Lightmap при этом остаётся как основа — авторский свет 25-летней давности никуда не девается, он просто получает рельефную «шапку» сверху.
Но и этого оказалось недостаточно для нормального FPS: при наличии нескольких сотен мешей с картами нормалей на уровне количество кадров падало до 10–20. Пришлось изобретать кэширование для всего пайплайна. Фактически к уже заранее запечённому освещению мы дополнительно запекаем данные для нормалей при запуске карты в первые несколько кадров игры.
Проблема BSP‑геометрии:
Если для StaticMesh кэширование нормалей удалось настроить относительно быстро, то BSP‑стеновые поверхности оказали жесточайшее сопротивление. Архитектура Unreal Engine 2 нарезает BSP‑поверхности на динамические узлы прямо во время кадра. Движок сопротивлялся кэшированию всеми силами: текстурные координаты «плыли», кэши инвалидировались каждый кадр, а эффект от нормалей и Blinn‑Phong то исчезал, то появлялся — вместе с целым зоопарком визуальных артефактов или обвалом FPS до однозначных значений. Мне несколько раз приходилось откатываться на бэкапы недельной давности и разрабатывать архитектуру сначала. В итоге проблему удалось решить созданием хэш‑таблицы состояний источников света и массива из 128 статическо‑динамических слотов освещения прямо в шейдере, связав их со стримингом геометрии.
Просто завезти апгрейд в игру без практической демонстрации новых возможностей было недостаточно, поэтому я конвертировал две карты из UT3 в UT2004. По сравнению с разработкой самого рендерера это оказалось лёгкой прогулкой. Карты из UT3 были выбраны намеренно: там качественные UV‑развёртки и готовые нормал‑карты нового поколения — именно то, что нужно для демонстрации. Геометрия BSP, модели (статические меши), их параметры и расположение портировались за 5–10 минут парой запросов к CLI. Сами модели конвертированы из pskx в формат ase скриптом для 3ds max (также собранным с помощью ИИ за пять минут). ИИ сам прочитал мануал UModel и вытащил всё необходимое в отдельную папку. Так же и с импортом моделей и текстур в UnrealED.
Единственное, с чем пришлось реально повозиться — сборка шейдеров в редакторе. В итоге за 2 вечера получился практически полный порт обеих карт. Задача была не сделать их полностью играбельными, а показать красивую картинку — поэтому различные недоработки (отсутствие лифтов и скрипт‑событий) так и останутся недоработками. Если у тебя есть интерес к игре и этим картам — их можно довести до ума, а также сделать упрощённые версии для оригинального рендерера без карт нормалей.
Результат — рабочий рендерер ENDrv (OpenGL 3.3) для Unreal Tournament 2004:
честный Per‑Pixel Blinn‑Phong Specular и карты нормалей на базе экранных производных — без изменения формата вершин движка;
Dual Normal Maps для убедительной анимированной поверхности воды;
постпроцессинг: Depth of Field, Bloom, Chromatic Aberration — Bloom работает на многих оригинальных картах без модификации ресурсов;
приемлемый FPS на оригинальном контенте за счёт многоуровневого кэширования и гибридного освещения.
Автор: SoMuchCo
Источник [1]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/pbr/456595
Ссылки в тексте:
[1] Источник: https://habr.com/ru/articles/1070592/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1070592
Нажмите здесь для печати.