Разработчик построил DOOM на чистом CSS — можно поиграть прямо в браузере
Чистый CSS, без JavaScript — вся логика на :checked, ~, counter() и CSS Grid. Работает в любом современном браузере, можно пострелять прямо сейчас.
Новости Tproger, отредактировано
Веб-разработчик Нильс Ленхер (Niels Leenheer) построил полноценный DOOM на чистом CSS. Каждая стена, пол, бочка и имп — это <div>, позиционированный в 3D-пространстве через CSS transforms. Игровая логика работает на JavaScript, но весь рендеринг — исключительно CSS.
Поиграть прямо в браузере: cssdoom.wtf — полноценный первый уровень DOOM, целиком на CSS. Chrome или Safari, WASD + мышь.
Ленхер — автор нескольких экспериментальных проектов, включая DOOM на осциллографе 1980-х годов. Новый проект использует координаты из оригинального WAD-файла (формат хранения данных карт DOOM) 1993 года и современные CSS-функции: @property, shape() и clip-path (свойство CSS для обрезки элемента по произвольному контуру) с правилом evenodd (правило заливки SVG-путей: закрашиваются области, ограниченные нечётным числом линий). Исходная статья набрала 306 очков и 67 комментариев на Hacker News, а код опубликован на GitHub.
Ключевые выводы
— CSS умеет считать тригонометрию: ширина стены через hypot(), угол через atan2()
— Камеры в CSS нет — двигается весь мир вокруг игрока через translate3d с инвертированными координатами
— Двери, лифты и снаряды анимируются через CSS transitions и CSS animations без JS animation loop
— Освещение секторов работает через filter: brightness(), наследуемый по каскаду
— @property регистрирует custom properties как числа, что позволяет анимировать их плавно
Как это работает — CSS как 3D-движок
Сцена строится из нескольких тысяч <div>-элементов. Каждый получает сырые координаты из оригинального WAD-файла DOOM в виде CSS custom properties: две пары x/y-координат, высоту пола и потолка. CSS сам вычисляет всё остальное.
Тригонометрия в CSS
Ширина стены — это расстояние между двумя точками, которое вычисляется по теореме Пифагора через функцию hypot(). Угол поворота — это арктангенс, вычисляемый через atan2(). Обе функции добавлены в CSS специально для подобных расчётов.
JavaScript передаёт сырые данные DOOM. CSS считает тригонометрию. Это разделение — ключевой архитектурный принцип проекта.
Движение мира вместо камеры
В CSS нет понятия камеры. Вместо этого используется классический трюк: перемещается весь мир в направлении, противоположном движению игрока. JavaScript задаёт всего четыре custom property — --player-x, --player-y, --player-z и --player-angle. CSS делает остальное:
Обратите внимание на инвертированные знаки: если игрок шагает вперёд — мир сдвигается назад. Если игрок поднимается по лестнице — лестница опускается вниз.
Полы, текстуры и клипы
DOM-элементы по умолчанию вертикальны — существуют в плоскости x/y. Чтобы превратить <div> в пол, достаточно rotateX(90deg) — элемент «ложится» горизонтально.
Сложные формы через clip-path
Секторы DOOM — это произвольные многоугольники: L-образные комнаты, неправильные формы, закруглённые коридоры. Для них используется clip-path с polygon(). Для секторов с отверстиями (колонны, платформы, окна) — clip-path с path() и правилом заливки evenodd.
Однако polygon() работает с процентами, а path() требует координат в пространстве CSS — что нарушает чистоту разделения. Решение нашлось в новой функции shape(), которая поддерживает проценты и evenodd одновременно.
Выравнивание текстур
Два соседних сектора с одинаковой текстурой пола должны бесшовно стыковаться. Поскольку background-image повторяется бесконечно, достаточно выровнять начало паттерна по мировым координатам:
Каждый сектор ссылается на одну и ту же текстурную сетку — независимо от того, где расположен его <div>. В результате переход между секторами выглядит бесшовным.
Анимации — двери, снаряды, спрайты
Прежде чем перейти к деталям — важный термин: billboarding. Это техника, при которой 2D-спрайт всегда повёрнут лицом к камере, независимо от положения игрока. В DOOM все враги, бочки и снаряды — billboarded-элементы.
Двери и лифты на CSS transitions
Открытие двери в DOOM — это поднятие потолка сектора. В CSS все элементы двери группируются в контейнер, а анимация запускается переключением атрибута data-state:
Никакого JS animation loop — достаточно установить атрибут состояния на элементе. CSS transitions берут анимацию на себя.
С лифтами всё сложнее: игрок едет вместе с платформой, поэтому --player-z должен обновляться синхронно с CSS transition. Но --player-z управляется из JavaScript. Поэтому для лифтов JS вынужден использовать функцию cubic ease-in-out (t² * (3 - 2t)), чтобы оставаться в синхронизации с CSS-анимацией — это признанное ограничение текущей архитектуры.
Снаряды на CSS animations
Ракеты и файерболы импов — это billboarded <div>. При создании снаряда JavaScript вычисляет конечную точку и длительность полёта, а CSS анимирует перемещение от точки A к точке B:
Благодаря разделению translate и rotate как отдельных CSS-свойств анимация управляет только позицией, а rotate реагирует на --player-angle — файербол продолжает смотреть на камеру при движении игрока.
Параллельно с CSS-анимацией игровой цикл на JavaScript рассчитывает позицию снаряда тем же линейным методом — для обнаружения столкновений (collision detection). Когда снаряд попадает в стену, пол, игрока или врага, JS удаляет элемент посреди полёта и порождает взрыв.
Спрайты с billboarding и mirroring
Враги в DOOM — 2D-спрайты, которые всегда повёрнуты к камере (billboarding). Оригинальная игра хранит 5 уникальных наборов кадров из 8 ракурсов — ракурсы 6-8 это зеркальные отражения 2-4. CSS воспроизводит это через scaleX(-1):
Анимация ходьбы — это spritesheet (набор кадров анимации в одном изображении) со сдвигом background-position-x через steps(). При атаке или смерти JavaScript меняет data-state, и CSS переключается на другой фрагмент spritesheet.
Одна из проблем: изначально все враги маршировали идеально в ногу — левая нога каждого зомби касалась земли в один и тот же момент. Решение — случайный animation-delay, задаваемый из JavaScript. Когда в браузерах появится CSS-функция random(), этот параметр можно будет перенести целиком в CSS.
Освещение и @property
DOOM хранит уровень освещённости для каждого сектора. В CSS это реализовано через custom property --light на контейнере сектора:
Каскад CSS идеально подходит для этого: все стены, полы и спрайты в тёмном секторе автоматически затемняются — без необходимости устанавливать яркость на каждом элементе отдельно. Мерцающие лампы — это @keyframes-анимации переменной --light.
Но анимировать custom properties можно только если они зарегистрированы через @property. Без этого браузер воспринимает их как строки:
Эта регистрация позволяет плавно анимировать --player-z — например, при падении игрока с уступа CSS сам создаёт плавный переход.
Что делает JavaScript, а что CSS
Автор чётко разделил ответственности:
- JavaScript — игровой цикл, состояние игры, коллизии, порождение и удаление DOM-элементов, установка custom properties и data-атрибутов
- CSS — все 3D-трансформации, вычисление геометрии (тригонометрия), анимации дверей и снарядов, освещение, CSS рендеринг спрайтов
По словам Ленхера, game loop на JavaScript — наименее интересная часть проекта. Исходный код DOOM на C доступен публично уже много лет, поэтому для портирования он использовал Claude, чтобы сосредоточиться на CSS-рендеринге.
Частые вопросы
Можно ли в это играть?
Да, демо доступно онлайн. Это полноценный первый уровень DOOM с врагами, дверями, лифтами и стрельбой. Управление — стандартные WASD + мышь.
Почему не Canvas или WebGL?
Цель проекта — показать возможности современного CSS, а не создать производительный порт. Canvas и WebGL изначально созданы для рендеринга, а CSS — нет, что делает результат особенно впечатляющим.
Какие браузеры поддерживаются?
Проект использует новейшие CSS-функции: hypot(), atan2(), @property, shape(). Для запуска потребуется Chrome или Safari последних версий. Firefox пока не поддерживает все необходимые функции.
Насколько это производительно?
Не очень. Сцена состоит из тысяч DOM-элементов с 3D-трансформациями, и браузерный движок CSS не оптимизирован для игрового рендеринга. Но проект и не ставил целью производительность — это эксперимент, доказывающий мощь современного CSS.
Выводы
Я хотел найти границы того, на что способен браузер. Увидеть, насколько мощным стал современный CSS. И потому что это DOOM. На CSS. Вам правда нужна ещё какая-то причина?
Проект демонстрирует, как далеко продвинулся CSS за 30 лет. Функции вроде hypot(), atan2() и @property превращают каскадные таблицы стилей в полноценный инструмент для 3D-вычислений. Разделение на JS game loop и CSS-рендеринг оказалось не только возможным, но и элегантным.
Исходный код: GitHub. Подробный разбор: CSS is DOOMed.