- Un experimento que renderiza DOOM en 3D solo con CSS, con todas las paredes y objetos construidos con
<div> y transformaciones 3D (transform)
- La lógica del juego corre en JavaScript, pero el renderizado lo hace completamente CSS, explorando los límites del navegador y del CSS moderno
- Aprovecha funciones modernas de CSS como trigonometría,
clip-path, @property, filtros SVG y posicionamiento por anclas para implementar paredes, pisos, iluminación, sprites e incluso efectos de explosión
- Debido a que CSS no tiene un concepto de cámara, la perspectiva se maneja moviendo el mundo en lugar del jugador, y todo el movimiento se controla actualizando propiedades personalizadas
- No alcanza el rendimiento de WebGL, pero demuestra el potencial de expansión en expresividad y capacidad de cálculo de CSS
Renderizado 3D de DOOM hecho con CSS
- Un proyecto experimental que renderiza DOOM solo con CSS, donde todas las paredes, pisos y objetos están compuestos por
<div> y colocados mediante transformaciones 3D (transform)
- La lógica del juego se ejecuta en JavaScript, pero el renderizado queda totalmente a cargo de CSS
- El objetivo del proyecto es explorar los límites del navegador y del CSS moderno
Volver a las matemáticas de secundaria
- Se extrajeron los datos del archivo WAD del DOOM original (vertices, linedefs, sidedefs, sectors) para construir una escena estática con miles de
<div>
- Cada pared recibe sus coordenadas de inicio y fin, además de la altura del piso y del techo, mediante propiedades personalizadas de CSS
- Las funciones de CSS
hypot() y atan2() calculan la longitud y el ángulo de rotación de cada pared
- JavaScript entrega los datos crudos y CSS realiza los cálculos trigonométricos para el renderizado
- El bucle del juego y el renderizador están separados, así que JS solo se encarga de manejar el estado y actualizar coordenadas
El problema de la conversión de coordenadas
- DOOM usa un sistema de coordenadas 2D donde Y crece hacia el norte, mientras que CSS 3D usa Y hacia arriba y Z apuntando hacia el observador
- En la conversión se usa
translate3d(x,-z,-y) para alinear ambos sistemas de coordenadas
- Un detalle interesante es que el cálculo
rotateY(atan2(var(--delta-y), var(--delta-x))) funciona sin conversiones adicionales
Mover el mundo en lugar de usar una cámara
- Como CSS no tiene un concepto de cámara, se usa un enfoque de mover el mundo en dirección contraria al jugador
- JS solo actualiza cuatro propiedades personalizadas:
--player-x/y/z/angle
translate: 0 0 var(--perspective) ajusta la perspectiva, y rotateY junto con translate3d manejan la rotación de la vista y el movimiento de posición
- Todo el movimiento se procesa solo actualizando propiedades
El piso es un div acostado
- Como los elementos base del DOM son planos verticales, el piso se coloca horizontalmente usando
rotateX(90deg)
- Se usan
clip-path, polygon() y path() para representar áreas poligonales complejas y agujeros
- La función moderna
shape() de CSS permite usar rutas basadas en porcentajes junto con la regla evenodd
Alineación de texturas
- Para evitar cortes entre texturas de sectores adyacentes, se usa
background-position basado en coordenadas del mundo
- Todos los sectores comparten la misma cuadrícula de textura para lograr transiciones suaves entre bordes
Puertas, elevadores y animaciones con @property
- La apertura de puertas se logra elevando el techo de un sector, aplicando transiciones CSS (
transition) al transform del <div> contenedor
- En los elevadores, como el jugador se mueve junto con ellos, JS sincroniza
--player-z
- Con
@property, las propiedades personalizadas se registran como numéricas para lograr efectos suaves de caída y movimiento
Sprites y espejado
- Los sprites de enemigos usan el sistema de billboard, mirando siempre hacia la cámara
- De las 8 direcciones, solo existen imágenes reales para 5 conjuntos; las demás se resuelven con espejado horizontal (
scaleX)
- Las animaciones
steps() cambian los cuadros de caminar, atacar y morir
- El problema de que todos los enemigos caminaran al mismo tiempo se resolvió con
animation-delay aleatorio desde JS
Proyectiles, explosiones y efectos de disparo
- Cohetes y bolas de fuego se mueven automáticamente de A a B mediante animaciones CSS
- JS solo define coordenadas iniciales, finales y duración; al colisionar, elimina el elemento y crea el sprite de explosión
- Las explosiones y el humo de los disparos se eliminan automáticamente tras una animación de 3 cuadros basada en
steps()
Iluminación y filtros
- El brillo de cada sector se define con la propiedad
--light, y los elementos internos lo heredan mediante filter: brightness()
- Las luces intermitentes cambian periódicamente
--light usando @keyframes
- El enemigo transparente (Spectre) se representa con una silueta distorsionada mediante filtros SVG (
feColorMatrix, feTurbulence, feDisplacementMap)
UI responsiva y posicionamiento por anclas
- El juego se adapta a móviles, y el HUD hace saltos de línea con
flex-wrap
- El sprite del arma ajusta automáticamente su posición según la altura del HUD usando
anchor-name / position-anchor
- Los botones táctiles se colocan con el mismo sistema de anclas
Modo espectador
- Soporta vista completa del mapa y seguimiento en tercera persona
- Las funciones
sin() y cos() de CSS calculan la posición de la cámara detrás del jugador
- Separando las propiedades
rotate y translate se logra una transición de cámara suave
- JS solo actualiza posición y ángulo, mientras que CSS resuelve las matemáticas de la cámara
Culling y rendimiento
- Miles de elementos 3D generan carga en el compositor del navegador
- Culling en JS: los elementos fuera del campo visual se marcan como
hidden
- Experimento de culling en CSS: control de
visibility mediante valores calculados, usando el truco de type grinding
- Si la función
if() se estandariza, podría reemplazar esto con condiciones mucho más simples
Ordenamiento por profundidad
- El navegador maneja automáticamente el orden de profundidad (
z-order)
- Los objetos en el mismo plano reciben un pequeño desplazamiento para evitar parpadeos
Los “trucos” de DOOM y el manejo del cielo
- El DOOM original usaba un truco de proyección para dibujar el cielo como una “pared” con textura 2D
- Como el renderizador en CSS debe colocar el cielo en un espacio 3D real, en algunas escenas aparece el problema de que se ve la parte trasera del mapa
- La solución es excluir del renderizado los elementos que quedan detrás de las paredes del cielo durante la etapa de culling
Conclusión — límites y posibilidades de CSS
- Todo el bucle del juego corre en JS, mientras que el renderizado se separa como una base de CSS puro
- Se llevan al extremo funciones modernas de CSS como trigonometría,
@property, clip-path, filtros SVG y posicionamiento por anclas
- No ofrece rendimiento al nivel de WebGL, pero demuestra la posibilidad de expandir la expresividad de CSS
- También permitió descubrir numerosos bugs 3D y problemas de rendimiento en Safari y Chrome
- Conclusión final: “¿Se puede ejecutar DOOM con CSS?”
→ Sí, se puede. Yes, it can.
1 comentarios
Comentarios de Hacker News
Creo que la gente del tipo “hice que esto corriera DOOM” debería ser contratada por el departamento de sistemas de propulsión espacial del gobierno
Son personas que necesitan desafíos poco convencionales; hacer girar los dedos no les basta
Esto parece un proyecto de esos de “lo hice porque podía”
CSS originalmente era un lenguaje declarativo de estilos, pero ahora, con condicionales, funciones matemáticas y trucos de renderizado, cada vez se está convirtiendo más en un sistema programable
Lo importante no es “si se puede correr DOOM con CSS”, sino cuánta lógica estamos metiendo en una capa que originalmente no era para eso
CSS oculta su deseo de convertirse en un lenguaje de programación, pero al final se ha transformado en una abstracción completamente equivocada
Antes se necesitaba JS para dropdowns, tooltips y layouts, pero ahora ya se pueden definir con propiedades de CSS cosas como posicionamiento por anclas o condiciones con
if()Incluso animaciones, toggles de detalles y efectos relacionados con accesibilidad ya se pueden manejar con CSS
Crear escenas 3D con CSS ya era posible desde hace tiempo, pero para la interacción hacía falta JS
Ahora, con proyectos como x86CSS, incluso se puede emular una CPU usando solo CSS y sin JS
Por eso da curiosidad si DOOM podría implementarse en tiempo real con CSS puro
Este caso muestra muy bien por qué la gente termina queriendo CSS basado en TypeScript
Por funciones como
if()que solo funcionan en Chrome, los desarrolladores recurren a este tipo de trucosPor ejemplo, usar
animation-delayy@keyframespara imitar un toggle de visibilidadSi
if()de CSS se estandariza, se podrá manejar lógica condicional de forma limpia sin estos hacksLos códigos de truco de DOOM, IDDQD e IDKFA, lamentablemente no funcionaron
Esto me recordó a la época en la que para hacer esquinas redondeadas en un div hacían falta cuatro GIFs
¡De verdad es impresionante! Si borras un solo div, ya puedes hacer wall hack
.wallsolo le ponesopacity: 0.7, se recrea perfectamente esa sensación clásica de hack de paredes transparentesMe preguntaba “¿dónde se puede probar esto directamente?”, y sí se puede en cssdoom.wtf
En Chromium iba incluso más entrecortado, y no encontré las teclas de strafing
Aun así, en general es una implementación sorprendente
CSS es una especificación que representa muy bien los límites del diseño por comité
Junto con SVG, compite por el título de “la especificación más fea de ver”
Para agregar una cosa sobre esta implementación tan genial:
en realidad no se mueve el jugador; se mueve el mundo
La cámara es solo una herramienta conceptual para calcular el campo de visión (
frustum)