javascript

AAA Движок для GTA San Andreas в браузере и переписывание ThreeJS

  • среда, 5 августа 2026 г. в 00:00:09
https://habr.com/ru/articles/1066360/

Если лень читать - сразу дам ссылки на важные ресурсы:

Прошлые статьи серии:

В прошлых эпизодах я описывал создание первого прототипа, а затем исправление проблем с картой.

Настала пора самого интересного - сделать современную графику.

Изначально движок RenderWare не предоставляет возможностей динамических теней и освещения: все эти данные зашиты внутри объектов мира как статический prelight и как нарисованные тени (тоже статика).

Моим планом было накрутить эффектов, чтобы понять, на что способен браузер и как далеко можно зайти.

Перед началом, я написал пару тестовых сценариев, где камера пролетает над самыми тяжёлыми местами карты, чтобы измерять FPS. Замерять FPS я решил на каждом шаге своей работы.

Первый прогон, ещё без всех модификаций, показал не слишком оптимистичные результаты: мои улучшения карты, особенно высокополигональная растительность, дали серьёзную просадку по FPS. Причём просело неравномерно: за городом было 43 FPS, в Сан-Фиерро и Лас-Вентурасе - по 30, а в Лос-Сантосе, самом плотном городе, всего 18–19. То есть тяжёлый город лежал ещё до того, как я взялся за графику. Что ж, посмотрим, как будет дальше.

Вначале я подобрал систему управления цветом, выбирал из трёх вариантов: AgX, ACES, Neutral.

AGX
AGX
ACES
ACES
Neutral
Neutral

Остановился на ACES как на золотой середине между HDR-подходом Neutral и совсем блёклой картинкой AgX.

Сделал PBR-небо и правильный туман.

City
City

Картинка становилась всё лучше и лучше…

Cars
Cars
City
City
Night
Night
Night 2
Night 2
Shadows
Shadows
Sun
Sun

И наконец-то я сделал настоящий свет фар.

Car lights
Car lights
Car lights 2
Car lights 2

Но FPS всё падал и падал. В очередной проход, после докрутки каскадных теней и света, я сделал замер.

Low FPS
Low FPS

FPS скатился до 12–18. Это никуда не годилось, так как по сути мы имеем только пустой город, а его ещё надо наполнять машинами - и на этот счёт у меня большие идеи сделать эмуляцию жизни, а на неё нужны большие ресурсы.

Также имелся ряд проблем, которые надо будет решать. Например, тени вблизи выглядят ну очень хреново.

Shadows
Shadows
Shadows 2
Shadows 2

Подходы рендера 3D-графики в браузере

Если объяснять по-простому, то существует два способа рендера.

WebGL - довольно старая технология. У нас много Draw Call, а так как карта состоит из множества объектов, их подготовка осуществляется на уровне CPU и забивает main thread, поэтому появляются фризы и теряются FPS.

WebGPU - новая технология, позволяющая использовать все возможности видеокарты устройства: в ней мы можем убрать нагрузку с CPU, освободив main thread.

Моя текущая версия была полностью построена на WebGL, значит, надо переезжать на WebGPU. Но есть проблема - ThreeJS не в полной мере поддерживает WebGPU. Правда, тогда я этого ещё не знал, поэтому об этом позже.

WebGPU

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

Результат на ThreeJS оказался неутешительным.

Test
Test

На FPS в такой сцене смотреть бесполезно: 15к коробок стоят друг за другом, видеокарта много раз перерисовывает одно и то же место, и цифра ничего не значит. Смотреть надо на другое - сколько времени процессор тратит, чтобы собрать один кадр и отдать его видеокарте. Чем меньше, тем лучше.

И вот тут WebGPU в ThreeJS дал 27 миллисекунд против 10 на старом WebGL. То есть новая технология оказалась в два с половиной раза МЕДЛЕННЕЕ старой. Такой переезд ничего не даёт!

Попробовал Babylon. На обычном WebGL он оказался ещё хуже - втрое медленнее ThreeJS. Зато в режиме WebGPU он умеет один хитрый трюк: записать всю сцену один раз и дальше просто проигрывать эту запись. С ним сборка кадра ускорилась почти в сто раз. Такого я не ожидал.

Но стриминг всё портил. Запись у Babylon одна на весь мир: стоит добавить или убрать хоть один объект - и её надо перезаписывать целиком, а это 50 миллисекунд. У меня же карта грузит и выгружает объекты постоянно, пока едешь. То есть я получал бы заметный рывок картинки на каждой такой подгрузке. Babylon - отличное решение для статики, но в динамике он проседает.

Мы с Fable решили написать патч для ThreeJS. И патч сработал: сборка кадра ускорилась в пять раз, карта стала загружаться быстро, камера летала, машина ехала.

Block
Block
Low FPS
Low FPS

А FPS не вырос. Вообще. Кадр всё так же занимал треть секунды, игра шла на 3–4 FPS.

Дальше пошли раунды раскопок, и вот что выяснилось: время сидело не в процессоре. Всё, до чего можно дотянуться снаружи фреймворка, мы вычистили - сборка кадра в итоге стала быстрее раз в пятнадцать, память прижали. А кадр всё равно уходил в видеокарту и пропадал там на сотни миллисекунд. Причём если уменьшить разрешение в четыре раза, это время не менялось вообще - значит, дело не в том, что видеокарте много работы. Оно сидело внутри самого ThreeJS, в его прослойке к видеодрайверу. А туда снаружи не залезть - надо владеть этой прослойкой.

После очередных раундов кодинга Fable выдал мне это.

Stuck
Stuck

Весь проект был под угрозой. Что делать?

Создание своего ThreeJS

Оставалось только одно - писать свой ThreeJS, который будет WebGPU-first и сразу адаптирован под нагруженный стриминг.

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

Было создано отдельное приложение, я назвал его "Лаба", где мы с Fable шаг за шагом воспроизводили то, что умеет прод-версия.

First Render
First Render

Вначале отрендерили множество простых кубиков и кубиков с альфа-каналом. Результат - 120 fps. Неплохо, идём дальше.

Рендер одного из городов. 120 fps:

Map
Map

При этом на каждом шаге я делал замеры benchmark, о которых ниже.

Карта с базовым освещением. 120 fps.

Map Lights
Map Lights

Карта с ночным освещением - всё те же 120:

Map Night
Map Night

Глобальная переделка

В прошлой статье по улучшению карты я описал подход, в котором заимплементил ряд инструментов для обработки моделей и текстур, исправляя различные баги.

Её архитектура была сделана в unix-стиле, где каждый инструмент получает на вход игру и отдаёт пропатченную игру.

Packer
Packer

И я подумал: а что если - раз уж мы полностью меняем движок - сделать свой формат как моделей, так и текстур?

Что это может дать:

  • Мы можем зашить тени в LOD-объекты, а считать тени только для HD-сектора (равно как и свет).

  • Мы можем сложить текстуры в массивы, чтобы видеокарта брала их пачкой, и резать этим количество вызовов отрисовки. Классический вариант - свалить всё в один большой атлас - я тоже попробовал, и он оказался на 7 % хуже: в GTA текстуры замощают поверхность повторением, а в атласе повторять нечего.

  • Мы можем сделать абсолютно новые игровые механики, которые не предусматривает оригинальная игра.

Короче, начали делать дополнительный инструмент.

Так выглядят запечённые тени.

Shadows
Shadows

Конечно, без багов никуда.

Bug 1
Bug 1
Bug 2
Bug 2
Bug 3
Bug 3

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

Возникла новая проблема - так как это новый формат, мы должны познакомить с ним и анимацию, и автомобили, и прочие вещи… Спустя время мы имели:

Car
Car

Промежуточные итоги:

Results
Results

После таких оптимистических цифр было принято решение - ПЕРЕПИСЫВАТЬ THREEJS!!!

В игре

Первый запуск в игре вышел комом, но - мир на движке, исторический скрин!

In Game
In Game

Куча багов, куда же без них.

Bug 1
Bug 1
Bug 2
Bug 2
Bug 3
Bug 3
Bug 4
Bug 4

Новый движок позволил сделать красивые облака.

Clouds
Clouds

Воду.

Water
Water

Реалистичное освещение было бы невозможно без преобразования карты. Вот как оно выглядит на карте без обработки, о которой я рассказывал в прошлой статье:

Lights Bug
Lights Bug
Normals Bug
Normals Bug

А вот как оно выглядит с обработкой:

Fix
Fix

Процесс работы

Такой большой пласт работы невозможен без постоянного контроля как качества, так и бенчмарков. Я делал замеры постоянно, реагировал на просадки FPS. В большинстве случаев они были связаны с багами.

Графика пока по уровню не полностью соответствует моим ожиданиям, но я буду над этим работать.

Также из наблюдений: Fable ошибается! И сильно! Была ситуация - две проблемы подряд: движок рассчитан только на одно авто, а авто рассчитано на один набор краски. То есть в мире бы ездили только авто одной марки с одним цветом. Это был не баг, а архитектурное решение.

Вообще работа с Fable мне напоминает работу с реально крутым разработчиком - Fable может спорить, отстаивать свои достижения, но все равно финальное слово за мной.

Подход с "Лабой" это настоящий опыт крупных компаний которые проводят масштабные рефакторинги - на малых фрагментах мы осуществляем гипотезу, потом мелкими шагами проводим замену, замеряя каждый шаг. На каждом этапе мы можем вернутсья к старой версии, так как удаление происходит только после полной миграции, мы можем сравнить и посмотреть, чего не хватает и как было сделано ранее. Последовательно пряча за флаги старые фичи и заменяя их новыми. И в финале мы просто деперкейтим устаревший код.

Итоги

Буквально за 2 недели я улучшил перфоманс с 16–18 до 100-120 FPS. Кадр стал собираться в 3–7 раз быстрее, а команд видеокарте движок отдаёт в 5–12 раз меньше. Всё это большими трудами и усилиями, в пока ещё не оконченном и не до конца подогнанном состоянии, но это огромный шаг вперёд, дающий мне хороший буфер производительности для имплементации самого важного - системы жизни в городе.

Мой следующий шаг - создать полноценный живой город, с персонажами и авто, дальностью прорисовки и прочим. Чтобы это смотрелось как мегаполис.

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

Продублирую полезные ссылки на важные ресурсы:

Прошлые статьи серии:

Всем спасибо за внимание. Продолжение следует.