CSS로 구현한 3D DOOM 렌더링
(nielsleenheer.com)-
CSS만으로 3D DOOM을 렌더링한 실험으로, 모든 벽과 오브젝트를
<div>와 3D 변환(transform) 으로 구성한 프로젝트 - 게임 로직은 JavaScript가 담당하지만, 렌더링은 전적으로 CSS가 수행되며 브라우저와 현대 CSS의 한계를 탐구
- 삼각함수, clip-path, @property, SVG 필터, 앵커 포지셔닝 등 최신 CSS 기능을 활용해 벽, 바닥, 조명, 스프라이트, 폭발 효과까지 구현
- 카메라 개념이 없는 CSS 특성상 플레이어 대신 세계를 이동시키는 방식으로 시점을 처리하고, 모든 이동은 커스텀 속성 업데이트로 제어
- WebGL 수준의 성능은 아니지만, CSS의 표현력과 계산 능력 확장 가능성을 입증한 사례
CSS로 구현한 3D DOOM 렌더링
-
CSS만으로 DOOM을 렌더링한 실험 프로젝트로, 모든 벽·바닥·오브젝트가
<div>로 구성되고 3D 변환(transform) 으로 배치됨- 게임 로직은 JavaScript에서 실행되지만, 렌더링은 전적으로 CSS가 담당
- 프로젝트의 목적은 브라우저와 현대 CSS의 한계 탐구
고등학교 수학으로 돌아가기
- 원본 DOOM의 WAD 파일 데이터(vertices, linedefs, sidedefs, sectors)를 추출해 수천 개의
<div>로 정적 장면 구성 - 각 벽은 시작·끝 좌표와 바닥·천장 높이를 CSS 커스텀 속성으로 전달받음
-
hypot()과atan2()CSS 함수로 벽의 길이와 회전 각도를 계산 - JavaScript는 원시 데이터를 전달하고, CSS가 삼각함수를 계산해 렌더링 수행
- 게임 루프와 렌더러는 분리되어 JS는 상태 관리와 좌표 갱신만 담당
좌표계 변환 문제
- DOOM은 Y가 북쪽으로 증가하는 2D 좌표계를 사용하지만, CSS 3D는 Y가 위쪽, Z가 시청자 방향으로 향함
- 변환 시
translate3d(x,-z,-y)형태를 사용해 좌표계를 맞춤 -
rotateY(atan2(var(--delta-y), var(--delta-x)))계산이 추가 변환 없이 작동하는 점이 특징
카메라 대신 세계를 움직이기
- CSS에는 카메라 개념이 없어, 플레이어 대신 세계를 반대로 이동시키는 방식 사용
-
--player-x/y/z/angle네 개의 커스텀 속성만 JS에서 갱신 -
translate: 0 0 var(--perspective)로 시점 보정,rotateY와translate3d로 시야 회전 및 위치 이동 - 모든 이동은 속성 업데이트만으로 처리
바닥은 눕힌 div
- 기본 DOM 요소는 수직 평면이므로, 바닥은
rotateX(90deg)로 눕혀 수평 배치 -
clip-path,polygon(),path()로 복잡한 다각형 영역과 구멍 표현 - 최신 CSS의
shape()함수로 퍼센트 기반 경로와evenodd규칙을 함께 사용 가능
텍스처 정렬
- 인접 섹터 간 텍스처가 끊기지 않도록 월드 좌표 기반의
background-position사용 - 모든 섹터가 동일한 텍스처 그리드를 공유해 매끄러운 경계 연결 구현
문, 리프트, @property 애니메이션
- 문 열림은 섹터 천장을 올리는 동작으로, 컨테이너
<div>의transform을 CSS 전환(transition) 으로 처리 - 리프트는 플레이어가 함께 움직이므로 JS에서
--player-z를 동기화 -
@property로 커스텀 속성을 숫자형으로 등록해 부드러운 낙하·이동 효과 구현
스프라이트와 미러링
- 적 스프라이트는 항상 카메라를 향하는 빌보드 방식
- 8방향 중 5세트만 실제 이미지, 나머지는 좌우 반전(scaleX) 으로 처리
-
steps()애니메이션으로 걷기·공격·사망 프레임 전환 - 모든 적이 동시에 걷는 문제는 JS의 무작위
animation-delay로 해결
발사체, 폭발, 총탄 효과
- 로켓·화염구 등은 CSS 애니메이션으로 A→B 이동 자동 처리
- JS는 시작·끝 좌표와 지속시간만 설정, 충돌 시 요소 제거 및 폭발 스프라이트 생성
- 폭발과 총탄 연기는
steps()기반 3프레임 애니메이션 후 자동 삭제
조명과 필터
- 섹터별 밝기값을
--light속성으로 지정하고, 내부 요소는filter: brightness()로 상속 - 깜박이는 조명은
@keyframes로--light값을 주기적으로 변경 - 투명한 적(Spectre)은 SVG 필터(
feColorMatrix,feTurbulence,feDisplacementMap)로 왜곡된 실루엣 표현
반응형 UI와 앵커 포지셔닝
- 게임은 모바일 대응형으로, HUD는
flex-wrap으로 줄바꿈 - 무기 스프라이트는 HUD 높이에 맞춰
anchor-name/position-anchor로 위치 자동 조정 - 터치 조작 버튼도 동일한 앵커 방식으로 배치
관전자 모드
- 맵 전체 조망과 3인칭 추적 시점 지원
- CSS의
sin()·cos()함수를 이용해 플레이어 뒤쪽 카메라 위치 계산 -
rotate·translate속성을 분리해 부드러운 시점 전환 구현 - JS는 위치·각도만 갱신하고, 카메라 수학은 CSS가 처리
컬링과 성능
- 수천 개의 3D 요소로 인해 브라우저 컴포지터 부하 발생
- JS 기반 컬링: 시야 밖 요소를
hidden처리 - CSS 기반 컬링 실험: 계산값으로
visibility제어, 타입 그라인딩(type grinding) 트릭 사용 -
if()함수가 표준화되면 더 간결한 조건식으로 대체 가능
깊이 정렬
- 브라우저가 깊이 정렬(z-order) 을 자동 처리
- 동일 평면의 오브젝트는 미세한 오프셋을 주어 깜빡임 방지
DOOM의 ‘속임수’와 하늘 처리
- 원본 DOOM은 하늘을 2D 텍스처로 “벽” 위에 그리는 투영 트릭 사용
- CSS 렌더러는 실제 3D 공간에 하늘을 배치해야 하므로, 일부 장면에서 맵 뒤쪽이 노출되는 문제 발생
- 해결책은 컬링 단계에서 하늘 벽 뒤의 요소를 렌더링 제외
결론 — CSS의 한계와 가능성
- 전체 게임 루프는 JS로, 렌더링은 순수 CSS 기반으로 분리
- 삼각함수, @property, clip-path, SVG 필터, 앵커 포지셔닝 등 최신 CSS 기능을 극한까지 활용
- WebGL 수준의 성능은 아니지만, CSS 표현력의 확장 가능성을 입증
- Safari·Chrome의 3D 관련 버그와 성능 이슈 다수 발견
- 최종 결론: “CSS로 DOOM을 실행할 수 있는가?” → 가능하다. Yes, it can.
Hacker News 의견들
-
“이걸 DOOM으로 돌려봤다”류의 사람들은 정부의 우주 추진 시스템 부서에 고용되어야 한다고 생각함
그들은 손가락만 굴리기엔 너무 이색적인 과제가 필요한 사람들임- 하지만 결국 그들이 만드는 추진 시스템조차 DOOM을 돌릴 수 있게 만들 것 같음
-
이건 “할 수 있으니까 해본다”식의 프로젝트 같음
CSS가 원래는 선언적 스타일링 언어였는데, 이제는 조건문, 수학 함수, 렌더링 트릭까지 생기면서 점점 프로그래밍 가능한 시스템으로 변하고 있음
중요한 건 “CSS로 DOOM을 돌릴 수 있나”가 아니라, 원래 그런 용도가 아니었던 레이어에 얼마나 많은 로직을 밀어넣고 있는가임- 이건 전형적인 추상화 역전(abstraction inversion) 의 사례임
CSS는 프로그래밍 언어가 되려는 욕망을 숨기고 있지만, 결국 완전히 잘못된 추상화로 변해버렸음 - 핵심은 표현(CSS) 과 상호작용(JavaScript) 의 경계가 어디까지인가임
예전엔 드롭다운, 툴팁, 레이아웃을 위해 JS가 필요했지만, 이제는 CSS 속성으로 앵커 위치나 조건(if())까지 지정할 수 있음
애니메이션, 디테일 토글, 접근성 관련 효과까지 CSS로 처리 가능해짐
- 이건 전형적인 추상화 역전(abstraction inversion) 의 사례임
-
CSS로 3D 장면을 만드는 건 예전부터 가능했지만, 상호작용엔 JS가 필요했음
이제는 x86CSS 프로젝트처럼 JS 없이도 CPU를 CSS만으로 에뮬레이션할 수 있음
그래서 DOOM도 순수 CSS로 실시간 구현이 가능할지 궁금함- 하지만 CSS x86 CPU는 너무 느려서 게임 루프를 처리할 수 없다고 함. 결국 JS가 필요함
- 이런 CSS의 진화는 예견된 결과였고, HTML 진영이 애초에 DSSSL을 채택했어야 했다고 생각함
-
이 사례는 사람들이 왜 TypeScript 기반 CSS를 원하게 되는지를 잘 보여줌
Chrome에서만 작동하는 if() 같은 기능 때문에, 개발자들은 이런 꼼수를 씀
예를 들어,animation-delay와@keyframes를 이용해 가시성 토글을 흉내내는 트릭을 쓰는 식임
CSS if()가 표준화되면 이런 해킹 없이 깔끔하게 조건 처리가 가능해질 것임 -
DOOM 치트 코드인 IDDQD와 IDKFA는 아쉽게도 작동하지 않았음
-
예전에 div에 둥근 모서리를 만들려면 GIF 네 장이 필요했던 시절이 떠오름
- div라니, 그 전엔 전부 table 레이아웃으로 하던 시절도 있었음
-
정말 인상적임! div 하나만 지워도 벽 통과(wall hack) 가 가능함
- 더 나아가
.wall에opacity: 0.7만 주면, 예전식 투명 벽핵 느낌을 그대로 재현할 수 있음
- 더 나아가
-
“이걸 어디서 직접 돌려볼 수 있나?” 싶었는데, cssdoom.wtf에서 가능함
- 휴대폰으로 실행하자마자 기기가 뜨거워짐
- 모바일에서 DOOM을 이렇게 부드럽게 돌려본 건 처음임
- Safari에서도 완벽히 작동함 — 이런 일은 거의 없음
- Firefox에서는 잘 돌아가지만, Alt 키 매핑이 메뉴를 열고 닫아서 불편했음
Chromium에서는 오히려 더 버벅였고, strafing 키는 찾지 못했음
그래도 전반적으로 놀라운 구현임
-
CSS는 위원회 설계의 한계를 보여주는 대표적인 명세임
SVG와 함께 “가장 보기 흉한 스펙” 경쟁을 벌이고 있음- 혹시 제목만 보고 댓글 단 거 아니냐는 반응도 있었음
-
이 멋진 구현에 대해 한 가지 덧붙이자면,
실제로는 플레이어가 움직이는 게 아니라 세상이 움직이는 것임
카메라는 단지 시야각(frustrum)을 계산하기 위한 개념적 도구일 뿐임