- 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.
Aucun commentaire pour le moment.