- 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の4つのカスタムプロパティだけを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**で位置を自動調整
- タッチ操作ボタンも同じアンカー方式で配置
観戦モード
- マップ全体の俯瞰と三人称追跡視点をサポート
- 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.
1件のコメント
Hacker Newsのコメント
「これでDOOMを動かしてみた」系の人たちは、政府の宇宙推進システム部門に雇われるべきだと思う
指先を動かすだけでは物足りない、もっと風変わりな課題が必要な人たちだ
これは「できるからやってみる」タイプのプロジェクトっぽい
CSSはもともと宣言的なスタイリング言語だったのに、今では条件分岐や数学関数、レンダリングのトリックまで入り込み、どんどんプログラム可能なシステムへと変わってきている
重要なのは「CSSでDOOMを動かせるか」ではなく、本来その用途ではなかったレイヤーにどれだけ多くのロジックを押し込んでいるかだ
CSSはプログラミング言語になりたがっているのを隠しているが、結局は完全に間違った抽象化になってしまった
以前はドロップダウンやツールチップ、レイアウトのためにJSが必要だったが、今ではCSSプロパティでアンカー位置や条件(
if())まで指定できるアニメーション、detailsのトグル、アクセシビリティ関連の効果までCSSで処理できるようになっている
CSSで3Dシーンを作ること自体は昔から可能だったが、相互作用にはJSが必要だった
今では x86CSSプロジェクト のように、JSなしでCPUをCSSだけでエミュレートすることすらできる
だからDOOMも純粋なCSSだけでリアルタイム実装できるのか気になる
この事例は、人々がなぜTypeScriptベースのCSSを欲しがるのかをよく示している
Chromeでしか動かない
if()のような機能のせいで、開発者はこうした裏技を使うたとえば
animation-delayと@keyframesを使って可視性トグルを擬似的に実装するようなトリックだCSSの
if()が標準化されれば、こうしたハックなしにきれいに条件処理できるようになるはずだDOOMのチートコード IDDQD と IDKFA は、残念ながら動かなかった
昔、divに角丸を付けるのに4枚のGIFが必要だった時代を思い出す
本当に印象的だ! divを1つ消すだけで**壁抜け(wall hack)**ができる
.wallにopacity: 0.7を与えるだけで、昔ながらの透明壁ハック感までそのまま再現できる「これどこで実際に試せるんだ?」と思ったが、cssdoom.wtfで試せる
Chromiumではむしろさらにカクつき、ストレーフキーは見つけられなかった
それでも全体として驚くべき実装だ
CSSは委員会設計の限界を示す代表的な仕様だ
SVGと並んで「最も醜い仕様」の座を争っている
この素晴らしい実装について1つ付け加えると、
実際にはプレイヤーが動いているのではなく、世界のほうが動いている
カメラは単に視野角(frustrum)を計算するための概念的な道具にすぎない