他真的把《毁灭战士》塞进了 CSS:当浏览器样式表开始渲染 3D 游戏

当《DOOM》遇上 CSS,这个玩笑居然成了
“用 CSS 写个按钮动画”早就不新鲜了,“用 CSS 做个 3D 卡片翻转”也已经是前端教程里的基础操作。但把 1993 年的经典射击游戏《DOOM》搬进 CSS 渲染管线,听起来还是像一句深夜黑客圈的醉话。偏偏,Niels Leenheer 真把这件事做成了。
这个项目最有意思的地方,不是“浏览器里跑 DOOM”——毕竟这些年,DOOM 已经被移植到打印机、验孕棒、ATM 甚至各种离谱设备上,几乎成了程序员圈子的“Hello, World”。真正让人眼前一亮的是它的技术选型:游戏逻辑交给 JavaScript,渲染则尽量交给 CSS。换句话说,屏幕里的墙、地板、木桶和怪物,本质上都是一个个 <div>,再通过 CSS 3D 变换摆到正确的位置。
这件事之所以迷人,在于它戳破了很多人对 CSS 的旧印象。过去我们总把 CSS 当作“给网页穿衣服”的工具:调颜色、设边距、修布局,稍微复杂点就是动画和响应式。可这几年,CSS 的能力增长速度远超大多数人的认知,从 hypot()、atan2() 这样的数学函数,到 clip-path、@property、3D transform,再到更现代的 shape 语法,它已经不只是样式语言,而是在往“声明式图形系统”迈进。Leenheer 这个项目,某种意义上像是在说:别再把 CSS 只当成 UI 化妆品了,它已经能干点更硬核的活。
浏览器替他算三角函数,CSS 开始接管几何学
Leenheer 的实现思路很“前端”,也很聪明。他没有让 JavaScript 把每一面墙的宽度、角度、位置都算好再塞进 DOM,而是只把原始的《DOOM》地图数据——起点、终点、地板高度、天花板高度——作为 CSS 自定义属性传进去。剩下的几何计算,交给浏览器的 CSS 引擎完成。
这意味着什么?意味着墙的长度,浏览器可以用 hypot() 直接算;墙体朝向,浏览器可以用 atan2() 求角度;再配合 translate3d() 和 rotateY(),一个普通 <div> 就能变成《DOOM》走廊里那堵熟悉的墙。看到这里,我脑子里浮现的不是高性能图形引擎,而是一个拿着尺子和量角器、埋头苦算的浏览器。很荒诞,但也很优雅。
更妙的是,这种分工很有现代 Web 工程的味道:JavaScript 负责状态与循环,CSS 负责表现与动画,边界非常清晰。Leenheer 自己也承认,他最初甚至想把游戏状态和逻辑也尽量塞进 CSS,最后发现渲染可以,逻辑不行。这个判断其实很重要。它说明 CSS 的边界确实在扩张,但它仍然不是一门通用编程语言。把它用在它擅长的地方——布局、变换、过渡、裁剪——反而能逼出惊人的效果。
如果把这件事放到 Web 技术演进的大背景里看,它特别像一个时代切面。十几年前,前端还在为浏览器兼容性和 IE 怒吼;再往后,大家开始讨论 Canvas、WebGL、WebAssembly 才是“真正的图形未来”。而现在,有人反过来证明:哪怕不用 WebGL,光凭现代 CSS,浏览器也已经足够强大到把一款经典 3D 游戏像模像样地摆出来。这不是说 CSS 会取代图形 API,而是它正在成为一层比我们想象中更有弹性的渲染能力。
没有摄像机?那就把整个世界反着挪
这个项目里最有画面感的细节,是 Leenheer 发现“CSS 里其实没有真正的摄像机”。传统 3D 引擎的思路,是给你一个 camera,然后你移动视角、调整朝向、让世界在镜头前展开。但 CSS 的 3D 世界更像一个舞台,它没有现成的游戏相机系统。
于是他用了一个经典又极客的办法:不动玩家,反过来移动整个世界。玩家往前走一步,就让场景整体向后退一点;玩家上楼梯,就让楼梯和世界一起往下挪。这个思路并不新鲜,很多图形系统都这么干,但放到 CSS 语境里,会显得特别“邪门又合理”。最终,JavaScript 只要更新四个变量:玩家的 x、y、z 坐标和朝向角度,剩下的一切由 CSS 自动接管。
我很喜欢这个设计,因为它恰好体现了 Web 技术的现实主义。前端工程师最常做的,不是用最完美的方法解决问题,而是在现有标准、浏览器实现和性能边界里,找到那个“够优雅也够能跑”的平衡点。Leenheer 在文章里甚至坦白,某些坐标系映射连他自己都觉得“我也不确定我完全理解,但它就是能工作”。这句话太像真实的软件开发了。很多技术突破,未必来自纯粹理论上的最优解,反而来自不断试错后那个“咔哒”一声对上的瞬间。
地板的处理也很能说明问题。DOM 元素默认是竖着的平面,要把它变成地板,就得 rotateX(90deg) 把它掀平。再往下走,《DOOM》的地面又不是规规矩矩的矩形,而是各种不规则多边形、镂空平台、带洞的房间。于是 clip-path: polygon() 和更复杂的 path()、evenodd 填充规则就派上了用场。你会突然意识到,很多网页设计师拿来做创意遮罩的功能,到了这里竟然成了“游戏地图切割器”。这就是 Web 技术最有趣的地方:功能本身中性,想象力才决定它最终长成什么样。
它不只是炫技,更像一场对现代 Web 的验收测试
很多人看到这种项目,第一反应是“酷,但没啥用”。这句话对,也不对。
对的地方在于,你当然不会真的期待未来 3A 游戏都靠 CSS 渲染。性能、调试难度、工程复杂度、浏览器差异,都会让这种路线难以成为主流方案。Leenheer 自己在实现过程中也踩了不少坑,比如纹理平铺要跨区域无缝对齐,门的开合可以交给 CSS 过渡,但升降平台如果玩家站在上面,就必须让 JavaScript 和 CSS 动画严格同步,这时纯声明式方案就开始露出短板。再比如敌人 sprite 面向玩家的 billboarding、镜像朝向、随机动画延迟,这些细节一旦多起来,系统复杂度会迅速上升。
但“不实用”不等于“不重要”。科技史上,很多看似无用的作品,真正价值在于它们提前暴露了工具链的能力上限。你可以把这个 CSS 版《DOOM》理解成一次浏览器生态的验收测试:现代 CSS 到底能承担多少原本属于图形层、引擎层、脚本层的工作?浏览器的样式引擎在数学计算、动画插值、几何裁剪和 3D 变换上的成熟度,到底到了什么地步?
这也是为什么我认为它比一般的“浏览器整活”更有新闻价值。它不是单纯把经典游戏换个平台运行,而是在重新审视 Web 的角色。过去几年,Web 平台一直在一个尴尬位置上摇摆:它足够通用,足够跨平台,但也常被嘲笑“什么都能做一点,什么都不是最强”。现在随着 CSS、Wasm、WebGPU 等能力不断增长,浏览器正在从“文档查看器”更彻底地变成“应用运行时”。Leenheer 的项目就像一根探针,扎进这个变化最有趣的部分。
真正值得想的问题:CSS 会不会越来越像“轻量引擎”
我看完这个项目,最大的感受不是“前端真卷”,而是一个更长线的问题:如果 CSS 继续这样进化,它会不会逐渐变成一种轻量级的图形与动画引擎描述语言?
这件事并非完全没有先例。早年 Keith Clark 的 CSS FPS 演示就已经证明,纯 CSS 做第一人称视角并不是天方夜谭;后来浏览器厂商不断补齐动画、变量、数学函数、裁剪和 Houdini 相关能力,实际上是在让“样式”越来越程序化、越来越可组合。今天你拿它做一个游戏实验,明天别人也许就会拿它做低门槛 3D 数据可视化、沉浸式产品展示、教育模拟器,甚至某些不需要高帧率的交互场景。
当然,争议也在这里。当前前端社区早就有一种疲惫感:技术栈过度复杂,概念不断膨胀,原本只管展示的 CSS 也越来越像编程语言。这到底是能力进步,还是认知负担加剧?当一个样式系统开始承担越来越多“原本不该归它管”的任务时,开发效率未必总是提升。炫酷 demo 背后,往往藏着难以维护的细枝末节。Leenheer 这个项目之所以好看,恰恰因为它是一件作者自己愿意为之较劲的实验品;一旦放进真实团队协作和商业项目里,故事就可能完全不同。
但即便如此,我还是愿意为这种项目鼓掌。科技行业最怕的不是有人做了“没什么商业价值”的尝试,最怕的是大家只剩下 KPI 和路线图,不再有人去试探工具的边界。CSS 渲染《DOOM》看起来像一个玩笑,可它背后有一种很珍贵的工程师气质:不是问“这个功能规范上允许吗”,而是先问“浏览器到底还能被逼成什么样”。
这份好奇心,往往才是技术真正向前走的起点。