1 pontos por GN⁺ 2026-03-29 | 1 comentários | Compartilhar no WhatsApp
  • Projeto experimental que renderiza DOOM em 3D usando apenas CSS, com todas as paredes e objetos construídos com <div> e transformações 3D (transform)
  • A lógica do jogo fica por conta do JavaScript, mas a renderização é totalmente feita em CSS, explorando os limites do navegador e do CSS moderno
  • Usa recursos modernos de CSS como trigonometria, clip-path, @property, filtros SVG e anchor positioning para implementar paredes, piso, iluminação, sprites e até efeitos de explosão
  • Como o CSS não tem conceito de câmera, o ponto de vista é tratado movendo o mundo em vez do jogador, e todo o movimento é controlado por atualizações de propriedades personalizadas
  • Embora não tenha desempenho de nível WebGL, é um caso que comprova a expressividade do CSS e o potencial de expansão de sua capacidade de cálculo

Renderização 3D de DOOM feita com CSS

  • Projeto experimental que renderiza DOOM usando apenas CSS, com todas as paredes, pisos e objetos compostos por <div> e posicionados com transformações 3D (transform)
    • A lógica do jogo roda em JavaScript, mas a renderização fica inteiramente a cargo do CSS
    • O objetivo do projeto é explorar os limites do navegador e do CSS moderno

Voltando à matemática do ensino médio

  • Os dados do arquivo WAD do DOOM original (vertices, linedefs, sidedefs, sectors) são extraídos para montar uma cena estática com milhares de <div>
  • Cada parede recebe as coordenadas inicial e final e as alturas do piso e do teto por meio de propriedades personalizadas de CSS
  • As funções CSS hypot() e atan2() calculam o comprimento e o ângulo de rotação das paredes
  • O JavaScript fornece os dados brutos, e o CSS faz os cálculos trigonométricos para renderizar
  • O loop do jogo e o renderizador ficam separados: o JS cuida apenas do estado e da atualização das coordenadas

O problema da conversão de coordenadas

  • O DOOM usa um sistema de coordenadas 2D em que Y aumenta para o norte, enquanto o CSS 3D usa Y para cima e Z na direção do observador
  • Na conversão, usa-se translate3d(x,-z,-y) para alinhar os sistemas de coordenadas
  • Um ponto característico é que o cálculo rotateY(atan2(var(--delta-y), var(--delta-x))) funciona sem transformações adicionais

Movendo o mundo em vez da câmera

  • Como o CSS não tem conceito de câmera, a solução usada é mover o mundo na direção oposta em vez do jogador
  • Apenas quatro propriedades personalizadas são atualizadas pelo JS: --player-x/y/z/angle
  • translate: 0 0 var(--perspective) faz o ajuste do ponto de vista, enquanto rotateY e translate3d aplicam rotação da visão e deslocamento de posição
  • Todo o movimento é tratado apenas com atualização de propriedades

O piso é uma div deitada

  • Como elementos DOM são planos verticais por padrão, o piso é colocado na horizontal com rotateX(90deg)
  • clip-path, polygon() e path() são usados para representar áreas poligonais complexas e buracos
  • A função moderna shape() do CSS também permite usar caminhos baseados em porcentagem com a regra evenodd

Alinhamento de texturas

  • Para evitar cortes entre texturas de setores adjacentes, usa-se background-position baseado em coordenadas do mundo
  • Todos os setores compartilham a mesma grade de textura, permitindo conexões suaves nas bordas

Portas, elevadores e animação com @property

  • A abertura de portas funciona elevando o teto do setor, tratada com transições CSS (transition) no transform do contêiner <div>
  • Como o elevador move o jogador junto, o JS sincroniza --player-z
  • Com @property, propriedades personalizadas são registradas como numéricas, permitindo efeitos suaves de queda e movimento

Sprites e espelhamento

  • Os sprites dos inimigos usam o sistema de billboard, sempre voltados para a câmera
  • Das 8 direções, apenas 5 conjuntos têm imagens reais; as demais são tratadas com espelhamento horizontal (scaleX)
  • As trocas de quadros de caminhada, ataque e morte são feitas com animações steps()
  • O problema de todos os inimigos andarem ao mesmo tempo foi resolvido com animation-delay aleatório em JS

Projéteis, explosões e efeito de tiro

  • Foguetes, bolas de fogo e outros elementos usam animações CSS para fazer automaticamente o movimento de A até B
  • O JS define apenas as coordenadas inicial e final e a duração; em caso de colisão, o elemento é removido e um sprite de explosão é criado
  • Explosões e fumaça de tiro usam animações de 3 quadros com steps() e depois são removidas automaticamente

Iluminação e filtros

  • O valor de brilho de cada setor é definido na propriedade --light, e os elementos internos o herdam com filter: brightness()
  • Luzes piscantes alteram periodicamente o valor de --light com @keyframes
  • O inimigo transparente (Spectre) é representado com uma silhueta distorcida usando filtros SVG (feColorMatrix, feTurbulence, feDisplacementMap)

UI responsiva e anchor positioning

  • O jogo é adaptado para mobile, e o HUD quebra linha com flex-wrap
  • O sprite da arma se ajusta automaticamente à altura do HUD com anchor-name / position-anchor
  • Os botões de toque também são posicionados com a mesma técnica de âncora

Modo espectador

  • Há suporte para visão completa do mapa e câmera de perseguição em terceira pessoa
  • As funções CSS sin() e cos() são usadas para calcular a posição da câmera atrás do jogador
  • As propriedades rotate e translate são separadas para permitir transições de câmera mais suaves
  • O JS atualiza apenas posição e ângulo, enquanto a matemática da câmera fica a cargo do CSS

Culling e desempenho

  • Milhares de elementos 3D geram carga no compositor do navegador
  • Culling baseado em JS: elementos fora do campo de visão recebem hidden
  • Experimento de culling em CSS: controle de visibility por meio de valores calculados, usando o truque de type grinding
  • Se a função if() for padronizada, isso poderá ser substituído por expressões condicionais mais simples

Ordenação por profundidade

  • O navegador faz automaticamente a ordenação por profundidade (z-order)
  • Objetos no mesmo plano recebem um pequeno deslocamento para evitar cintilação

Os “truques” do DOOM e o tratamento do céu

  • No DOOM original, o céu é desenhado como uma parede usando um truque de projeção com textura 2D
  • Como o renderizador em CSS precisa posicionar o céu em um espaço 3D real, em algumas cenas surge o problema de o fundo do mapa ficar visível
  • A solução é excluir da renderização os elementos atrás da parede do céu na etapa de culling

Conclusão — limites e possibilidades do CSS

  • O loop completo do jogo fica em JS, enquanto a renderização é separada em uma abordagem puramente baseada em CSS
  • Recursos modernos como trigonometria, @property, clip-path, filtros SVG e anchor positioning são levados ao extremo
  • Embora não alcance desempenho de nível WebGL, o projeto comprova a possibilidade de expandir a expressividade do CSS
  • Também revelou vários bugs e problemas de desempenho ligados a 3D no Safari e no Chrome
  • Conclusão final: “É possível rodar DOOM com CSS?” → Sim. Yes, it can.

1 comentários

 
GN⁺ 2026-03-29
Comentários do Hacker News
  • Acho que o pessoal do tipo “rodei isso em DOOM” deveria ser contratado pelo departamento de sistemas de propulsão espacial do governo
    São pessoas que precisam de desafios excêntricos demais para ficarem só mexendo os dedos

    • Mas no fim das contas, provavelmente até os sistemas de propulsão que eles fizerem vão conseguir rodar DOOM
  • Isso parece um projeto no estilo “fiz porque dava para fazer”
    CSS originalmente era uma linguagem declarativa de estilização, mas agora, com condicionais, funções matemáticas e truques de renderização, está gradualmente se transformando em um sistema programável
    O ponto importante não é “dá para rodar DOOM em CSS?”, mas sim quanta lógica estamos enfiando em uma camada que originalmente não era para isso

    • Este é um caso típico de inversão de abstração (abstraction inversion)
      O CSS tenta esconder seu desejo de virar uma linguagem de programação, mas no fim acabou se tornando uma abstração completamente errada
    • A questão central é até onde vai a fronteira entre apresentação (CSS) e interação (JavaScript)
      Antes, era preciso JS para dropdowns, tooltips e layout, mas agora já dá para definir até posicionamento por âncora e condicionais (if()) com propriedades CSS
      Animações, alternância de detalhes e até efeitos relacionados à acessibilidade agora podem ser tratados em CSS
  • Criar cenas 3D com CSS já era possível há muito tempo, mas para interação era preciso JS
    Agora, com projetos como o x86CSS, dá até para emular uma CPU só com CSS, sem JS
    Então fica a curiosidade se DOOM também poderia ser implementado em tempo real com CSS puro

    • Mas dizem que a CPU x86 em CSS é lenta demais para lidar com um game loop. No fim, JS ainda é necessário
    • Essa evolução do CSS foi um resultado previsível, e há quem ache que o lado HTML deveria ter adotado DSSSL desde o início
  • Este caso mostra bem por que as pessoas acabam querendo CSS baseado em TypeScript
    Por causa de recursos como if() que só funcionam no Chrome, desenvolvedores recorrem a esse tipo de gambiarra
    Por exemplo, usam truques com animation-delay e @keyframes para imitar alternância de visibilidade
    Quando o if() do CSS for padronizado, vai dar para tratar condicionais de forma limpa sem esses hacks

  • Os códigos de trapaça do DOOM, IDDQD e IDKFA, infelizmente não funcionaram

  • Isso me fez lembrar da época em que era preciso quatro GIFs para fazer cantos arredondados em uma div

    • Div? Antes disso houve a época em que tudo era feito com layout em tabela
  • Muito impressionante! Dá até para atravessar parede com um wall hack só apagando uma div

    • Indo além, se você aplicar apenas opacity: 0.7 em .wall, já recria perfeitamente aquela sensação dos antigos transparent wall hacks
  • Fiquei pensando “onde dá para testar isso na prática?”, e dá em cssdoom.wtf

    • Assim que rodei no celular, o aparelho esquentou
    • Foi a primeira vez que vi DOOM rodar assim tão liso no mobile
    • Funciona perfeitamente até no Safari — o que quase nunca acontece
    • No Firefox roda bem, mas o mapeamento da tecla Alt abria e fechava o menu, o que foi incômodo
      No Chromium, pelo contrário, ficou ainda mais travado, e não consegui encontrar a tecla de strafing
      Ainda assim, no geral, é uma implementação surpreendente
  • CSS é uma especificação representativa dos limites do design por comitê
    Junto com SVG, disputa o posto de “spec mais feia de se ver”

    • Também houve quem respondesse perguntando se a pessoa não comentou só de ler o título
  • Só para acrescentar uma observação sobre essa implementação incrível:
    na prática, não é o jogador que se move, e sim o mundo
    A câmera é apenas uma ferramenta conceitual para calcular o campo de visão (frustum)