Как я Quake в браузере делал

:


2 месяца назад я выложил на GitHub первую бета-сборку WebQuake — порта первого Quake, работающего в браузере через WebGL.

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


Разработку WebQuake в текущем его виде я начал в сентябре 2012 года. Но идея у меня зародилась задолго до этого.

Первый раз я решил сделать что-то подобное летом 2011 года, когда я ничего не соображал в JavaScript. Тогда я делал порт «на глаз», не глядя на код Quake, и сделал только небольшой кусок меню игры. В той версии я работал с двоичными данными через строки (а в парсере чисел с плавающей запятой вообще использовал Math.pow и биты хранил в строке из символов 0 и 1). Очень хорошо, что непонимание сути работы с буферами и шейдерами в WebGL уберегло мир от такой струи блевотины.

Затем ради прямого доступа к файлам я хотел сделать WebQuake десктопным приложением. Стал выбирать между HTA и XUL. Но ни один из них не поддерживает WebGL. Поэтому от этой идеи я тоже отказался.

В итоге я перешел на чистый HTML5.


От начала до первой беты прошло 6 месяцев. Если мне не изменяет память, на создание GWT Quake 2 у Google ушло 2 месяца, но Google делали свой порт втроём, и у них была база в виде Jake2, а я переписывал весь код вручную.

Переписывание вручную было выбрано из-за того, что так мне проще подгонять код под общий принцип работы всего движка, делать движок независимым от размера окна браузера, а некоторые области (как графика) в браузере работают совсем не так, как в оригинальном Quake.

Но у такого подхода есть и недостатки. Иногда получались опечатки, а из-за неправильного оператора мне дважды (в первый раз скользил по стенам с бешеной скоростью из-за && вместо ||, а во второй были ужасные дергания в сетевой игре из-за !== вместо ===), пришлось потратить 3 недели на перекапывание всей системы.

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


А теперь перейдем к деталям самого движка.

Графика


Отрисовка графики, естественно, реализована через WebGL.

Но WebQuake портом GLQuake назвать нельзя. Практически вся графическая подсистема была переписана с нуля.

Главным отличием WebQuake от GLQuake является использование шейдеров и буферов вместо фиксированного набора функций OpenGL. В WebQuake шейдеры используются везде, для каждого типа объектов: BSP-модель, полигональная модель, игрок, спрайт, частица, небо — написан свой шейдер.

Через шейдеры были возвращены эффекты, присутствующие в DOS Quake/WinQuake, но убранные из GLQuake из-за ограничений старых версий OpenGL, например, текстуры с освещенными участками и яркий свет.


Начало E1M1 в GLQuake. Лампочки не горят.


То же место в WebQuake.

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

precision mediump float;
uniform float uGamma;
uniform sampler2D tTexture;
uniform sampler2D tLightmap;
uniform sampler2D tDlight;
uniform sampler2D tLightStyle;
varying vec4 vTexCoord;
varying vec4 vLightStyle;
void main(void)
{
	vec4 texture = texture2D(tTexture, vTexCoord.xy);
	gl_FragColor = vec4(texture.rgb *
		mix(1.0, dot(texture2D(tLightmap, vTexCoord.zw), vec4(
			texture2D(tLightStyle, vec2(vLightStyle.x, 0.0)).a,
			texture2D(tLightStyle, vec2(vLightStyle.y, 0.0)).a,
			texture2D(tLightStyle, vec2(vLightStyle.z, 0.0)).a,
			texture2D(tLightStyle, vec2(vLightStyle.w, 0.0)).a)
		* 43.828125) + texture2D(tDlight, vTexCoord.zw).a, texture.a), 1.0);
	gl_FragColor.r = pow(gl_FragColor.r, uGamma);
	gl_FragColor.g = pow(gl_FragColor.g, uGamma);
	gl_FragColor.b = pow(gl_FragColor.b, uGamma);
}

Как вы видите, dot здесь используется для слегка необычной для него задачи — перемножение 4 карт освещения на их текущую яркость для данного источника освещения, которая находится в текстуре 64x1 как значения от 0 до 25 или от 0.0 до 0.0980392.

Небо, ужасно заломанное в GLQuake, здесь сделано в виде приплюснутой сферы, рисующейся вокруг всего уровня через хаки с depth testing'ом, в отличие от GLQuake, который разбивает полигоны с текстурой неба на множество маленьких и искажает их странными способами, приводя к нехорошим эффектам и волнам при перемещении.


Небо в GLQuake.

Двухмерные изображения тоже рисуются через WebGL (через quad с длиной 1, умножающийся в вершинном шейдере). Изначально планировалось использовать для этого 2D Canvas, но при высоком разрешении FPS падало до 15.

Также, в отличие от оригинального Quake (и GWT Quake 2), WebQuake никак не зависит от размера окна браузера. Для этого также используется и так называемый Hor+vert+ FOV, о котором я писал ранее на Хабрахабре.

Звук


Звук реализован сразу двумя способами.

По умолчанию используется Web Audio API, поддерживающий стереозвук и плавный повтор звука.

Если браузер не поддерживает Web Audio API, включается HTML5 Audio, но звук в таком случае одноканальный и повторяется с некоторой задержкой.

В ранних бета-релизах использовался только HTML5 Audio, но из-за этого вылетал Chrome сначала на Android, Linux и Mac, а затем и на Windows, поэтому была добавлена поддержка Web Audio.

Музыка тоже присутствует, но проигрывается не с диска, а из OGG-файлов на сервере через HTML5.

Сетевая игра


Так как браузер не может быть сервером WebSocket, сделать listen-сервер было невозможно.

Выделенный сервер работает через Node.js и использует крупную часть кода WebQuake.

В выделенном сервере поддерживаются одновременно и WebSockets, и UDP, поэтому на серверах WebQuake можно играть через обычный клиент Quake (не QuakeWorld). Возможно, в будущем я напишу прокси для подключения к уже существующим серверам обычного Quake.

Информацию о сервере можно запросить как HTTP-запросами на тот же адрес и порт, на котором запущен сервер (данные возвращаются в формате JSON), так и уже существующими способами через UDP.

Управление


Поддержка мыши на данный момент работает только в Chrome. Несмотря на то, что в Firefox pointer lock тоже есть, там он требует полноэкранного режима для самого canvas, что создает некоторые неудобства для игрока и для разработчика.

Файловая система


Доступ к файлам сделан через синхронный XMLHttpRequest.

Да, синхронный XHR — это, может быть, «не модно», но это реализуется гораздо проще, не приводя к callback hell'у, и возможно, даже приятнее для пользователя, чем видеть повсюду временные текстуры наподобие тех, что используются в GWT Quake 2.

Во время загрузки появляется (по крайней мере в Firefox) картинка «loading» посередине экрана, поэтому игрок понимает, что идет загрузка.

Записываются сохранения, настройки и демки в Local Storage. Сохранения, находящиеся в Local Storage, можно удалить кнопкой Delete в меню загрузки/сохранения.

В отличие от GWT Quake 2, WebQuake не требует конвертирования файлов и может загружать файлы прямо из .pak'ов (через HTTP 1.1 Range), а значит, присутствует полная поддержка модов.


Тестировал я WebQuake на разных устройствах и браузерах.

Что было несколько удивительно, так это то, что приемлемой производительности (не знаю, сколько FPS, но не меньше 30) можно было добиться даже на телефоне (LG Optimus L9) через бета-версию Chrome, хоть стены и черные (не знаю точную причину этого, к тому же работает динамическое освещение).

На моем предыдущем компьютере WebQuake работал на максимальных 60 FPS, в отличие от 5-10 FPS в GWT Quake 2. При разработке я неявно учел ошибки GWT Quake 2, например, использовал ArrayBuffer/Typed Arrays/DataView где мог, и возможно это помогло добиться высокой скорости.

На чём были огромные тормоза, так это на старом компьютере с NVIDIA GeForce 5200 и на нетбуке Samsung N130. На ASUS Transformer Pad TF300T работает довольно гладко.