Данная статья - это продолжение повествования о том, как я делаю AAA-движок для запуска старых игр, а именно GTA San Andreas и её модов, в браузере. В ней речь пойдет о том, как я уперся в производительность Three.js и перевел движок на свой фреймверк под WebGPU. Читать далее
Если лень читать - сразу дам ссылки на важные ресурсы:
Demo: https://opensa.cc
New Trailer: https://www.youtube.com/watch?v=eA1gVWzANRU
Прошлые статьи серии:
В прошлых эпизодах я описывал создание первого прототипа, а затем исправление проблем с картой.
Настала пора самого интересного - сделать современную графику.
Изначально движок RenderWare не предоставляет возможностей динамических теней и освещения: все эти данные зашиты внутри объектов мира как статический prelight и как нарисованные тени (тоже статика).
Моим планом было накрутить эффектов, чтобы понять, на что способен браузер и как далеко можно зайти.
Перед началом, я написал пару тестовых сценариев, где камера пролетает над самыми тяжёлыми местами карты, чтобы измерять FPS. Замерять FPS я решил на каждом шаге своей работы.
Первый прогон, ещё без всех модификаций, показал не слишком оптимистичные результаты: мои улучшения карты, особенно высокополигональная растительность, дали серьёзную просадку по FPS. Причём просело неравномерно: за городом было 43 FPS, в Сан-Фиерро и Лас-Вентурасе - по 30, а в Лос-Сантосе, самом плотном городе, всего 18–19. То есть тяжёлый город лежал ещё до того, как я взялся за графику. Что ж, посмотрим, как будет дальше.
Вначале я подобрал систему управления цветом, выбирал из трёх вариантов: AgX, ACES, Neutral.

AGX

ACES

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

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

Cars

City

Night

Night 2

Shadows

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

Car lights

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

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

Shadows

Shadows 2
Подходы рендера 3D-графики в браузереЕсли объяснять по-простому, то существует два способа рендера.
WebGL - довольно старая технология. У нас много Draw Call, а так как карта состоит из множества объектов, их подготовка осуществляется на уровне CPU и забивает main thread, поэтому появляются фризы и теряются FPS.
WebGPU - новая технология, позволяющая использовать все возможности видеокарты устройства: в ней мы можем убрать нагрузку с CPU, освободив main thread.
Моя текущая версия была полностью построена на WebGL, значит, надо переезжать на WebGPU. Но есть проблема - ThreeJS не в полной мере поддерживает WebGPU. Правда, тогда я этого ещё не знал, поэтому об этом позже.
WebGPUПрежде чем брать и переписывать, я решил сделать отдельную сцену с эмуляцией стриминга: разместил 15к объектов с заменой 2к на лету в течение нескольких секунд.
Результат на ThreeJS оказался неутешительным.

Test
На FPS в такой сцене смотреть бесполезно: 15к коробок стоят друг за другом, видеокарта много раз перерисовывает одно и то же место, и цифра ничего не значит. Смотреть надо на другое - сколько времени процессор тратит, чтобы собрать один кадр и отдать его видеокарте. Чем меньше, тем лучше.
И вот тут WebGPU в ThreeJS дал 27 миллисекунд против 10 на старом WebGL. То есть новая технология оказалась в два с половиной раза МЕДЛЕННЕЕ старой. Такой переезд ничего не даёт!
Попробовал Babylon. На обычном WebGL он оказался ещё хуже - втрое медленнее ThreeJS. Зато в режиме WebGPU он умеет один хитрый трюк: записать всю сцену один раз и дальше просто проигрывать эту запись. С ним сборка кадра ускорилась почти в сто раз. Такого я не ожидал.
Но стриминг всё портил. Запись у Babylon одна на весь мир: стоит добавить или убрать хоть один объект - и её надо перезаписывать целиком, а это 50 миллисекунд. У меня же карта грузит и выгружает объекты постоянно, пока едешь. То есть я получал бы заметный рывок картинки на каждой такой подгрузке. Babylon - отличное решение для статики, но в динамике он проседает.
Мы с Fable решили написать патч для ThreeJS. И патч сработал: сборка кадра ускорилась в пять раз, карта стала загружаться быстро, камера летала, машина ехала.

Block

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

Stuck
Весь проект был под угрозой. Что делать?
Создание своего ThreeJSОставалось только одно - писать свой ThreeJS, который будет WebGPU-first и сразу адаптирован под нагруженный стриминг.
Я не фанат таких поворотов и стараюсь избегать переписывания ядра, так как очень редко такие вещи дают реальный профит. Поэтому я решил в начале провести проверку.
Было создано отдельное приложение, я назвал его "Лаба", где мы с Fable шаг за шагом воспроизводили то, что умеет прод-версия.

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

Map
При этом на каждом шаге я делал замеры benchmark, о которых ниже.
Карта с базовым освещением. 120 fps.

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

Map Night
Глобальная переделкаВ прошлой статье по улучшению карты я описал подход, в котором заимплементил ряд инструментов для обработки моделей и текстур, исправляя различные баги.
Её архитектура была сделана в unix-стиле, где каждый инструмент получает на вход игру и отдаёт пропатченную игру.
Packer
И я подумал: а что если - раз уж мы полностью меняем движок - сделать свой формат как моделей, так и текстур?
Что это может дать:
Мы можем зашить тени в LOD-объекты, а считать тени только для HD-сектора (равно как и свет).
Мы можем сложить текстуры в массивы, чтобы видеокарта брала их пачкой, и резать этим количество вызовов отрисовки. Классический вариант - свалить всё в один большой атлас - я тоже попробовал, и он оказался на 7 % хуже: в GTA текстуры замощают поверхность повторением, а в атласе повторять нечего.
Мы можем сделать абсолютно новые игровые механики, которые не предусматривает оригинальная игра.
Короче, начали делать дополнительный инструмент.
Так выглядят запечённые тени.

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

Bug 1

Bug 2

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

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

Results
После таких оптимистических цифр было принято решение - ПЕРЕПИСЫВАТЬ THREEJS!!!
В игреПервый запуск в игре вышел комом, но - мир на движке, исторический скрин!

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

Bug 1

Bug 2

Bug 3

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

Clouds
Воду.

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

Lights Bug

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

Fix
Процесс работыТакой большой пласт работы невозможен без постоянного контроля как качества, так и бенчмарков. Я делал замеры постоянно, реагировал на просадки FPS. В большинстве случаев они были связаны с багами.
Графика пока по уровню не полностью соответствует моим ожиданиям, но я буду над этим работать.
Также из наблюдений: Fable ошибается! И сильно! Была ситуация - две проблемы подряд: движок рассчитан только на одно авто, а авто рассчитано на один набор краски. То есть в мире бы ездили только авто одной марки с одним цветом. Это был не баг, а архитектурное решение.
Вообще работа с Fable мне напоминает работу с реально крутым разработчиком - Fable может спорить, отстаивать свои достижения, но все равно финальное слово за мной.
Подход с "Лабой" это настоящий опыт крупных компаний которые проводят масштабные рефакторинги - на малых фрагментах мы осуществляем гипотезу, потом мелкими шагами проводим замену, замеряя каждый шаг. На каждом этапе мы можем вернутсья к старой версии, так как удаление происходит только после полной миграции, мы можем сравнить и посмотреть, чего не хватает и как было сделано ранее. Последовательно пряча за флаги старые фичи и заменяя их новыми. И в финале мы просто деперкейтим устаревший код.
ИтогиБуквально за 2 недели я улучшил перфоманс с 16–18 до 100-120 FPS. Кадр стал собираться в 3–7 раз быстрее, а команд видеокарте движок отдаёт в 5–12 раз меньше. Всё это большими трудами и усилиями, в пока ещё не оконченном и не до конца подогнанном состоянии, но это огромный шаг вперёд, дающий мне хороший буфер производительности для имплементации самого важного - системы жизни в городе.
Мой следующий шаг - создать полноценный живой город, с персонажами и авто, дальностью прорисовки и прочим. Чтобы это смотрелось как мегаполис.
После этого я снова вернусь к графике, так как пока многие вещи находятся в процессе и мне нужно делать замеры на реально загруженном городе, чтобы понимать лимиты.
Продублирую полезные ссылки на важные ресурсы:
Demo: https://opensa.cc
New Trailer: https://www.youtube.com/watch?v=eA1gVWzANRU
Прошлые статьи серии:
Всем спасибо за внимание. Продолжение следует.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Сложные сайты на вайбкоде: четыре лендинга, два пути к «дорогим» эффектам и 3D-логотип из спрайтов | 0 | 9.77 | 05-08-2026 |
| 2 | Реальная и мнимая многопоточность в JS | 0 | 5.83 | 30-07-2026 |
| 3 | Объекты в JS не бесплатные | 0 | 8.74 | 05-08-2026 |
| 4 | Next.js, localStorage в App Router без hydration-ошибок | 0 | 7.17 | 30-07-2026 |
| 5 | Картинка грузится, а fetch к тому же хосту — нет: как CSP тихо сломала три интеграции | 0 | 7.2 | 04-08-2026 |
| 6 | Как мы несколько дней искали проблему с перемоткой WebM | 0 | 8.86 | 31-07-2026 |
| 7 | React Update Lifecycle: что происходит при обновлении компонента | 0 | 8.86 | 05-08-2026 |
| 8 | Offline‑first PWA на примере фитнес‑трекера | 0 | 8.55 | 05-08-2026 |
| 9 | [Перевод] <usermedia> — новый HTML элемент | 0 | 8.96 | 31-07-2026 |
| 10 | Продакшн‑разработка в одиночку с AI‑агентами: принуждай к правилам, а не объясняй их | 0 | 8.25 | 24-07-2026 |