- Expérience de rendu 3D de DOOM uniquement en CSS, un projet où tous les murs et objets sont construits avec des
<div> et des transformations 3D (transform)
- La logique du jeu est gérée par JavaScript, mais le rendu est entièrement assuré par CSS, afin d’explorer les limites du navigateur et du CSS moderne
- Utilise des fonctionnalités CSS récentes comme la trigonométrie,
clip-path, @property, les filtres SVG et le positionnement par ancres pour réaliser murs, sols, éclairage, sprites et même les effets d’explosion
- Comme CSS ne dispose pas de concept de caméra, le point de vue est géré en déplaçant le monde au lieu du joueur, avec tous les mouvements contrôlés par mise à jour de propriétés personnalisées
- Les performances n’atteignent pas celles de WebGL, mais cela démontre la capacité d’expression de CSS et son potentiel d’extension côté calcul
Rendu 3D de DOOM réalisé en CSS
- Projet expérimental de rendu de DOOM uniquement en CSS, où tous les murs, sols et objets sont composés de
<div> et placés via des transformations 3D (transform)
- La logique du jeu s’exécute en JavaScript, mais le rendu est entièrement pris en charge par CSS
- L’objectif du projet est d’explorer les limites du navigateur et du CSS moderne
Retour aux maths du lycée
- Les données des fichiers WAD du DOOM original (vertices, linedefs, sidedefs, sectors) sont extraites pour construire une scène statique composée de milliers de
<div>
- Chaque mur reçoit ses coordonnées de début et de fin, ainsi que les hauteurs de sol et de plafond, via des propriétés personnalisées CSS
- Les fonctions CSS
hypot() et atan2() servent à calculer la longueur des murs et leur angle de rotation
- JavaScript transmet les données brutes, puis CSS effectue les calculs trigonométriques pour le rendu
- La boucle de jeu et le moteur de rendu sont séparés : JS ne s’occupe que de l’état et de la mise à jour des coordonnées
Le problème de transformation des coordonnées
- DOOM utilise un repère 2D où Y augmente vers le nord, alors que la 3D CSS a un axe Y vers le haut et un axe Z orienté vers l’observateur
- Lors de la conversion, la forme
translate3d(x,-z,-y) est utilisée pour faire correspondre les repères
- Particularité notable : le calcul
rotateY(atan2(var(--delta-y), var(--delta-x))) fonctionne sans transformation supplémentaire
Déplacer le monde plutôt qu’utiliser une caméra
- CSS n’ayant pas de notion de caméra, le choix a été fait de déplacer le monde en sens inverse plutôt que le joueur
- Seules quatre propriétés personnalisées sont mises à jour côté JS :
--player-x/y/z/angle
translate: 0 0 var(--perspective) corrige le point de vue, tandis que rotateY et translate3d gèrent la rotation de la vue et le déplacement dans l’espace
- Tous les déplacements sont gérés uniquement par mise à jour de propriétés
Le sol, c’est juste un div couché
- Les éléments DOM étant verticaux par défaut, le sol est couché à l’horizontale avec
rotateX(90deg)
clip-path, polygon() et path() permettent de représenter des zones polygonales complexes et des trous
- La fonction CSS récente
shape() permet aussi d’utiliser des chemins en pourcentage avec la règle evenodd
Alignement des textures
- Pour éviter les cassures entre textures de secteurs adjacents, le projet utilise un
background-position basé sur les coordonnées du monde
- Tous les secteurs partagent la même grille de texture, ce qui permet des jonctions de bord fluides
Portes, ascenseurs et animations avec @property
- L’ouverture des portes consiste à relever le plafond d’un secteur, géré via le
transform du conteneur <div> avec une transition CSS (transition)
- Les ascenseurs déplacent aussi le joueur, donc JS synchronise
--player-z
- Avec
@property, les propriétés personnalisées sont déclarées comme numériques afin d’obtenir des chutes et des déplacements fluides
Sprites et effet miroir
- Les sprites des ennemis utilisent une technique de billboard pour toujours faire face à la caméra
- Parmi les 8 directions, seules 5 séries d’images existent réellement, les autres étant obtenues par symétrie horizontale (
scaleX)
- Les changements d’images de marche, d’attaque et de mort sont gérés via des animations
steps()
- Le problème de tous les ennemis qui marchent en même temps est résolu avec un
animation-delay aléatoire côté JS
Projectiles, explosions et effets de balle
- Les roquettes, boules de feu, etc. se déplacent automatiquement de A vers B via des animations CSS
- JS ne définit que les coordonnées de départ, d’arrivée et la durée ; en cas de collision, l’élément est supprimé et un sprite d’explosion est créé
- Les explosions et la fumée des impacts sont supprimées automatiquement après une animation en 3 images basée sur
steps()
Éclairage et filtres
- La luminosité de chaque secteur est définie avec la propriété
--light, puis les éléments internes l’héritent via filter: brightness()
- Les lumières clignotantes modifient périodiquement
--light au moyen de @keyframes
- L’ennemi transparent (Spectre) est rendu sous forme de silhouette déformée grâce à des filtres SVG (
feColorMatrix, feTurbulence, feDisplacementMap)
Interface responsive et positionnement par ancres
- Le jeu est adapté au mobile, avec un HUD qui passe à la ligne via
flex-wrap
- Les sprites des armes s’ajustent automatiquement à la hauteur du HUD grâce à
anchor-name / position-anchor
- Les boutons de contrôle tactile utilisent le même système d’ancrage
Mode spectateur
- Prise en charge d’une vue d’ensemble de toute la carte et d’une vue de poursuite à la troisième personne
- Les fonctions CSS
sin() et cos() servent à calculer la position de la caméra derrière le joueur
- La séparation des propriétés
rotate et translate permet des transitions de point de vue fluides
- JS ne met à jour que la position et l’angle, et c’est CSS qui gère les calculs de caméra
Culling et performances
- Des milliers d’éléments 3D entraînent une charge importante sur le compositeur du navigateur
- Culling côté JS : les éléments hors champ sont passés en
hidden
- Expérimentation d’un culling côté CSS : contrôle de
visibility via des valeurs calculées, avec une astuce de type grinding
- Si la fonction
if() est standardisée, elle pourra remplacer cela par des conditions plus simples
Tri par profondeur
- Le navigateur gère automatiquement le tri de profondeur (
z-order)
- Les objets sur un même plan reçoivent un léger décalage pour éviter le scintillement
Les « astuces » de DOOM et le rendu du ciel
- Le DOOM original utilise une astuce de projection consistant à dessiner le ciel comme une texture 2D au-dessus d’un « mur »
- Le moteur CSS doit placer le ciel dans un véritable espace 3D, ce qui fait apparaître l’arrière de la carte dans certaines scènes
- La solution consiste à exclure du rendu les éléments situés derrière les murs du ciel lors de l’étape de culling
Conclusion — limites et potentiel de CSS
- La boucle de jeu complète est gérée en JS, tandis que le rendu est séparé en pur CSS
- Des fonctionnalités CSS modernes comme la trigonométrie,
@property, clip-path, les filtres SVG et le positionnement par ancres sont poussées à l’extrême
- Les performances ne rivalisent pas avec WebGL, mais le projet prouve le potentiel d’extension de l’expressivité de CSS
- De nombreux bugs 3D et problèmes de performances ont été observés dans Safari et Chrome
- Conclusion finale : « Peut-on faire tourner DOOM en CSS ? »
→ Oui, c’est possible. Yes, it can.
1 commentaires
Réactions sur Hacker News
Je pense que les gens du genre « j’ai réussi à faire tourner DOOM là-dessus » devraient être embauchés par le département des systèmes de propulsion spatiale
ce sont clairement des gens qui ont besoin de défis hors norme, pas de simples tâches à faire bouger du doigt
On dirait un projet du genre « on l’a fait parce qu’on le pouvait »
À l’origine, CSS était un langage de style déclaratif, mais avec l’arrivée des conditions, des fonctions mathématiques et des astuces de rendu, il devient de plus en plus un système programmable
La vraie question n’est pas « peut-on faire tourner DOOM en CSS ? », mais plutôt jusqu’à quel point on est en train d’empiler de la logique dans une couche qui n’était pas faite pour ça
CSS cache son envie de devenir un langage de programmation, mais finit par se transformer en une abstraction complètement inadaptée
Avant, il fallait du JS pour les menus déroulants, les infobulles ou la mise en page, mais aujourd’hui on peut même définir le positionnement par ancre ou des conditions
if()via des propriétés CSSLes animations, le basculement de détails et même certains effets liés à l’accessibilité peuvent désormais être gérés en CSS
Construire des scènes 3D en CSS est possible depuis longtemps, mais l’interactivité nécessitait du JS
Désormais, avec des projets comme x86CSS, on peut même émuler un CPU uniquement en CSS, sans JS
Du coup, on peut se demander s’il serait possible de faire tourner DOOM en temps réel en pur CSS
Cet exemple montre bien pourquoi certains en viennent à vouloir du CSS basé sur TypeScript
À cause de fonctionnalités comme
if()qui ne marchent que dans Chrome, les développeurs en sont réduits à ce genre de bidouillesPar exemple, utiliser
animation-delayet@keyframespour simuler un basculement de visibilitéSi
if()en CSS est standardisé, ce type de hack pourra laisser place à une logique conditionnelle propreLes codes de triche de DOOM, IDDQD et IDKFA, n’ont malheureusement pas fonctionné
Ça rappelle l’époque où il fallait quatre GIF pour faire des coins arrondis sur une div
Vraiment impressionnant ! Il suffit de supprimer une seule div pour activer un wall hack
opacity: 0.7à.wallpour recréer parfaitement le vieux style de mur transparent façon wallhackJe me demandais « où est-ce qu’on peut essayer ça soi-même ? », et c’est possible sur cssdoom.wtf
Dans Chromium, c’était encore plus saccadé, et je n’ai pas trouvé les touches de strafe
Malgré ça, dans l’ensemble, c’est une implémentation étonnante
CSS est une spécification emblématique des limites du design par comité
Avec SVG, il est en compétition pour le titre de « spécification la plus hideuse à regarder »
Une précision supplémentaire sur cette implémentation remarquable :
en réalité, ce n’est pas le joueur qui se déplace, c’est le monde qui bouge
La caméra n’est qu’un outil conceptuel servant à calculer le champ de vision (
frustum)