Как создать OpenGL 3.3 рендерер с нормал-маппингом для Unreal Tournament 2004 без знания C++ и исходного кода игры

Прокомментировать Просмотры: 16

Тест карт нормалей и Blinn‑Phong specular

Unreal Tournament 2004 — бесспорная классика, визуально, впрочем, застрявшая в эру Direct3D 8 и фиксированного конвейера. Спустя три месяца реверс-инжиниринга, бесчисленных крашей с ошибками доступа к памяти и без единой строчки C++ в голове я расскажу, как при помощи ИИ-агента всего за 500 рублей создал нативный DLL-рендерер для Unreal Engine 2, поддерживающий карты нормалей, спекуляр Blinn-Phong и современный постпроцессинг.

Поводом для столь дерзкого эксперимента стал релиз патча от команды OldUnreal, переведшего игру на полноценные 64 бита и внедрившего OpenGL 3.3 с обратной совместимостью как для древнего «железа», так и для актуальных производительных систем.

Преград на старте хватало: абсолютное незнание C++ (ни синтаксиса, ни архитектурных паттернов, ни способности свободно читать сторонний код), полное отсутствие оригинальных исходников (утекшие со исходники Unreal Championship 2 и Unreal Warfare разительно отличались от UT2004), а также нулевой багаж знаний об устройстве самого движка. Ситуацию усугублял скромный бюджет, исключавший подписки на Claude Code или Codex. Пришлось задействовать модель Gemini Flash через консольный инструмент Antigravity CLI, что обошлось в символические 500 рублей за годовой доступ.


Этап 1. Вслепую по бинарнику: воссоздание VTable и первый запуск

Любой графический рендерер в экосистеме Unreal Engine 2 представляет собой внешнюю динамическую библиотеку, реализующую стандартизированный интерфейс URenderDevice. Эта DLL выступает связующим звеном между игровым ядром и графическим адаптером, а коммуникация между ними выстроена через таблицы виртуальных функций — VTable.

В языке C++ последовательность элементов в VTable должна соблюдаться с точностью до байта. Смещение хотя бы на одну функцию неизбежно приводит к тому, что вместо отрисовки кадра процессор уходит в случайную область памяти, вызывая моментальный аварийный сбой.

[ Движок UE2 ] ‑> ( VTable: смещение +0×18 ) ‑> [ ENDrv.dll ] │

Ошибка на 4 байта? ‑> CRASH (Access Violation)

Первые две недели превратились в изнурительную угадайку: препарирование бинарников UT2004 в дизассемблере, изучение утекших исходников смежных проектов, попытки вычислить точный набор функций и их смещения для нашей версии, регулярная пересборка библиотеки, запуск игры каждые несколько минут, изучение дампов памяти и корректировка адресов.

Долгожданный запуск без вылета увенчался черным экраном — это произошло ровно в тот момент, когда удалось безошибочно определить 6 базовых процедур из 32 необходимых (на тот момент достоверно были известны лишь 14). Изображения еще не было, но звуковое сопровождение подтвердило успех: реагируя на клики в меню вслепую, можно было запустить матч и услышать топот бегающих и стреляющих ботов. Система заработала!

Следующая фаза потребовала передачи вершинных данных и вывода примитивной геометрии в каркасном режиме (Wireframe). Требовалось не просто создать пустые функции-заглушки, возвращающие дефолтные значения, а написать их рабочую логику. Дополнительные трудности создавало то, что движок проектировался с расчетом на DirectX, тогда как OpenGL диктует собственные правила работы с системами координат и состояниями рендеринга. Пройдя через каскад визуальных артефактов и мешанину из полигональных линий, мне все же удалось добиться корректного отображения каркасной сцены.

Этап 2. От Fixed‑Function к GLSL: Материалы, Комбинеры и статический свет

Оригинальный 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 со своими собственными субкомпонентами, процедурными текстурами с анимированными UV (TexPanner, TexRotator, TexScaler) или кубическими картами окружения. Все это разнообразие требовалось транслировать в GLSL, который не поддерживает рекурсию и динамические ветвления образца 2004 года, оперируя исключительно линейными графами фиксированной глубины.

Пришлось спроектировать архитектуру «регистров комбинеров»: перед итоговым вычислением цвета шейдер последовательно обрабатывает до 32 нод, записывая промежуточные результаты в массив. Каждая нода хранит индексы входных потоков, выполняемую математическую операцию (умножение, сложение, альфа-блендинг, модуляцию и т. д.) и маски. Дополнительно были задействованы 16 текстурных слотов с автономными матрицами трансформации UV, поддержка отражений через кубические карты, проективные текстуры, анимированные панорамы и режим FinalBlend с альфа-тестированием. Обнаружение под конец работ исходников 32-битной версии игры существенной погоды не сделало, поскольку базовый код все равно был жестко привязан к Direct3D 8. Тем не менее они помогли устранить пару трудноуловимых багов при обработке многослойных поверхностей и буфера глубины.

Пример хитрого инженерного решения: точные смещения полей структуры UTexture в памяти были неизвестны, поэтому код автоматически сканирует память методом перебора по заданным логическим критериям. Обнаружив искомую область, программа навсегда кеширует адрес.

Интеграция постпроцессинга заняла всего неделю, поскольку создание экранных эффектов вроде Depth of Field, Bloom или Chromatic Aberration на GLSL происходит прямолинейно и почти не зависит от специфики игрового движка.

Эффект Bloom не применяется ко всем ярким пикселям на экране, а честно работает только на самосветящихся объектах.
Эффект Bloom не применяется ко всем ярким пикселям на экране, а честно работает только на самосветящихся объектах.

Этап 3. Самая сложная часть разработки: Карты нормалей, BSP и оптимизация.

Ключевым амбициозным замыслом проекта было внедрение PBR-подобного освещения и карт нормалей. Эта задача оказалась значительно сложнее калибровки VTable.

Для корректного просчета рельефа рендереру необходимы данные о пространственной ориентации полигонов, включая векторы тангенсов и битангенсов, однако игровой движок их просто не передает. Сам шейдер нормалей заработал оперативно благодаря вычислению тангенсного базиса на лету через экранные производные (dFdx / dFdy), что избавило от необходимости модифицировать вершинный формат игры. Тем не менее, производительность рухнула катастрофически: появление в кадре пары десятков моделей с нормалями приводило к сильнейшим лагам.

Корень проблемы крылся в архитектуре освещения: все статические источники света в игре (динамические практически отсутствуют) запекаются на этапе компиляции карт. Интенсивность и цвет текстур модулируются картами освещения (Lightmaps) и вершинными цветами. Упрощенно говоря, это эквивалентно наложению градиента в Photoshop в режиме смешивания «Умножение». Иными словами, созданный рендерер изначально «не видел» полноценных источников света, оперируя лишь текстурной раскраской полигонов. Но поскольку наша DLL внедрена в процесс игры, у нас есть полный доступ к внутренним структурам движка: все данные об источниках света были успешно обнаружены и считаны.

Полноценный пересчет всех источников света покадрово для всей геометрии на шейдерах оказался неподъемной ношей для однопоточного процессора, на который опирался движок. Пришлось реализовывать многоуровневое кеширование: базовая геометрия сохраняет оригинальное статическое освещение (Lightmaps и Vertex Colors), созданное дизайнерами уровней четверть века назад, в то время как динамический расчет силами GLSL отвечает исключительно за микрорельеф (Normal Map) и бликовые эффекты (Blinn-Phong Specular).

Итоговый цвет = (Lightmap Diffuse) + (NormalMap_Affect DirectLight) + BlinnPhong_Specular

Здесь компонент NormalMap_Affect представляет собой не полный объем освещения, а лишь векторную прибавку от рельефа — отклонение нормали от плоской поверхности. Базовая лайтмапа сохраняется в роли неизменной подложки, поверх которой накладывается рельефная детализация.

Однако даже этого не хватило для поддержания приемлемой кадровой частоты: при появлении сотен мешей с картами нормалей FPS проседал до 10–20 кадров в секунду. Потребовалось внедрить агрессивное кеширование данных рендеринга. Фактически, в первые секунды загрузки карты поверх запеченного света происходит дополнительный предрасчет нормалей.

Трудности с BSP-геометрией:

Скрытый текст

Если со статическими мешами (StaticMesh) удалось разобраться относительно быстро, то BSP-поверхности стен оказали ожесточенное сопротивление. Архитектура Unreal Engine 2 динамически нарезает BSP-стены на узлы прямо во время рендеринга кадра. Движок отчаянно сопротивлялся кешированию: текстурные координаты деформировались, кеш сбрасывался на каждом кадре, а эффекты нормалей и спекуляров то пропадали, то появлялись, порождая дикие артефакты и просаживая частоту кадров до единичных значений. Мне неоднократно приходилось откатываться к резервным копиям недельной давности и переписывать архитектуру с нуля. Проблему удалось разрешить путем создания специализированной хэш-таблицы состояний источников света и выделения массива из 128 статическо-динамических слотов освещения прямо в теле шейдера, увязав их со стримингом геометрии.

Порт карт из Unreal Tournament 3

Ограничиться техническими улучшениями без наглядной демонстрации возможностей было бы упущением, поэтому я портировал две локации из Unreal Tournament 3 в среду UT2004. По сравнению с разработкой рендерера это оказалось легкой прогулкой. Выбор пал на UT3 из-за качественной развертки UV и готовых карт нормалей нового поколения. Архитектура BSP, статические меши, их свойства и расстановка перенеслись всего за несколько минут с помощью запросов к CLI. Модели были конвертированы из формата pskx в ase посредством скрипта для 3ds Max, написанного ИИ за пять минут. Используя документацию инструмента UModel, нейросеть самостоятельно извлекла нужные ассеты, после чего их оставалось лишь импортировать в UnrealED.

Единственной затычкой стала компиляция шейдеров в редакторе уровней. Спустя два вечера обе карты обрели новую жизнь. Поскольку целью было не создание полноценных игровых арен, а демонстрация визуального потенциала, различные изъяны вроде отсутствующих лифтов или скриптовых триггеров были оставлены без внимания. При желании эти локации можно доработать напильником или подготовить облегченные версии под оригинальный пайплайн без поддержки нормалей.

Итоги

Результатом проделанной работы стал функциональный рендерер ENDrv (OpenGL 3.3) для Unreal Tournament 2004, обладающий следующими достоинствами:

  • корректный попиксельный расчет Blinn-Phong Specular и карт нормалей на основе экранных производных без модификации вершинного буфера игры;

  • поддержка двойных карт нормалей (Dual Normal Maps) для достоверной имитации ряби на водной глади;

  • постпроцессинг эффекты Depth of Field, Bloom и Chromatic Aberration, причем Bloom корректно функционирует на большинстве стандартных карт без правок исходных ресурсов;

  • комфортная кадровая частота на оригинальном контенте благодаря многоуровневому кешированию и гибридной модели освещения.

Шейдер с картой нормали и Blinn-Phong
Шейдер с картой нормали и Blinn‑Phong
Тест шейдера с двойной анимированной картой нормали для воды
Тест шейдера с двойной анимированной картой нормали для воды
Bloom на спецэффектах
Bloom на спецэффектах
Порт карты DM-RisinSun
Порт карты DM‑RisinSun
 

Источник

Поделиться:

Похожие статьи

Поиск по играм, новостям и статьям…

Введите не менее двух символов

Введите не менее двух символов