1993年的DOOM需要多少行代码才能跑在浏览器里?答案是:零行画布(Canvas),零行WebGL,全靠CSS的transform和几千个div。
开发者Håkon Wium Lie的继任者、现任CSS工作组成员Bramus Van Damme最近干了件疯狂的事——他把完整的第一人称射击游戏DOOM,用纯CSS渲染了出来。不是demo,不是切片,是真正能玩的关卡。游戏逻辑用JavaScript,但每一面墙、每一个油桶、每一只恶魔,都是CSS 3D变换算出来的。
这相当于用Excel画出了蒙娜丽莎,而且笔触还对得上原作。
从示波器到浏览器:一个执念的两次实现
这个项目真正的起点不是浏览器,是一台1980年代的示波器。Bramus先在那块绿莹莹的屏幕上跑通了DOOM,解决了地图提取和数学运算。所以当转向浏览器时,他手里已经握着WAD文件解析器和基础几何算法。
最初的野心更大:能不能连游戏状态都用CSS存?变量(CSS Variables)理论上可以保存数值,计算(calc())可以做数学运算。Bramus试了一段时间,发现渲染完全可行,但游戏逻辑——碰撞检测、敌人AI、弹药计算——用CSS写等于自虐。
「游戏状态……可以,如果你真想的话。逻辑?不行,太复杂了。」他在博客里的原话。
于是他做了产品经理都会做的决策:拆分MVP。用Claude把原版DOOM的C代码转成JavaScript跑游戏循环,自己专注CSS渲染。这个选择让项目从"不可能"变成了"只是很难"。
每一面墙都是一道几何题
打开开发者工具,你会看到几千个div,每个带着这样的内联样式:
--start-x: 2560; --start-y: -2112; --end-x: 2560; --end-y: -2496; --floor-z: 32; --ceiling-z: 88;
这些数字直接来自1993年的WAD文件:顶点(vertices)、线段(linedefs)、面(sidedefs)、扇区(sectors)。Bramus没有硬编码3D变换,而是把原始坐标丢给CSS,让浏览器自己算宽度、角度和深度。
墙的宽度?勾股定理算delta X和delta Y。角度?arctangent2求朝向。透视?CSS的perspective和transform-style: preserve-3d包办。这相当于把高中数学作业交给了浏览器引擎,而且它还真的全对了。
整个场景是静态的,摄像机移动时只更新一个父容器的变换矩阵。这个 trick 让性能从"幻灯片"变成了"能玩"。
CSS的暗礁:那些文档不会告诉你的事
项目能跑,但Bramus踩遍了现代CSS的坑。最大的发现?z-index在3D变换里 behaves like a drunk person。
理论上,CSS 3D变换应该按Z轴深度自动排序,近的挡住远的。实际测试里,这个机制随机失效——同一面墙,摄像机转个角度,渲染顺序就乱了。Bramus的解决方案是暴力枚举:给每个元素手动计算深度,用z-index强行覆盖。
另一个陷阱是纹理过滤。DOOM的原始纹理是8位色索引图,浏览器放大时默认的双线性过滤会让画面糊成水彩画。Bramus用image-rendering: pixelated强制近邻插值,才还原了那种粗糙的1993年质感。
性能方面,Chrome和Firefox差异巨大。某段走廊在Chrome里稳60帧,Firefox直接掉到20帧——不是代码问题,是合成器(Compositor)的实现差异。Bramus最后做了动态细节降级:帧率掉的时候自动减少视野距离。
这到底证明了什么
Bramus自己说得很清楚:「我想找到浏览器的极限,看看现代CSS有多强。」
30年前,CSS只是给HTML加点颜色。现在它能实时渲染3D场景,而且不需要任何图形API。这不是"CSS比WebGL好"——DOOM的CSS版本在复杂场景下必然更慢——而是"CSS的能力边界远比大多数人想象的远"。
对前端开发者来说,这个项目是个残酷的提醒:你每天都在用的属性,可能藏着从未探索过的用法。对浏览器厂商来说,这是份免费的压力测试报告——当有人真的把CSS推到极限,哪里慢、哪里错,一目了然。
游戏已经开源在GitHub,可以直接玩。Bramus在文末留了句话:「DOOM不仅把我带回高中时代,也让我重新做了一遍高中数学题。」
下一个用CSS跑起来的经典游戏,会是什么?Quake的BSP树(二叉空间分割树)用CSS算,是更优雅还是更崩溃?
热门跟贴