获奖站拆解
EN

Food for Fish

Food for Fish 是比利时图形程序员 Kris Temmerman 为了试验 WebGPU 而做的一款网页点击式冒险游戏,画面全部由作者自写的 WebGPU 引擎绘制。主角是一个整天做网站的光头小人,他为了找到爷爷找了一生的成吉思汗金内裤,在家和院子里一条条地收集内裤。屏幕中起始是端着咖啡的小人站在摄影棚里,游戏标题在他身旁一笔笔写出来。访客点地板,小人就走过去,访客点描出白边的物件,他就会查看或使用它。

整个游戏只署了 Kris Temmerman 一个人的名字,它拿到了 FWA 的当日最佳、2024 年 5 月的月度最佳和 2024 年的年度最佳。

灰色的摄影棚里站着一个端着咖啡杯、只穿一条红边白内裤的光头小人,他的胸口有一片红疹,右边是手写的黑色大字 FOOD FOR FISH
灰色的摄影棚里站着一个端着咖啡杯、只穿一条红边白内裤的光头小人,他的胸口有一片红疹,右边是手写的黑色大字 FOOD FOR FISH

01概念

Kris Temmerman 在游戏菜单的自述里写到,自己想看看 WebGPU 现在能做到什么,于是写了一套小小的 WebGPU 引擎,又用它做了这个“游戏”。FWA 上的说明也只有一句,称它是一次 WebGPU 实验。作者把实验成果做成了一个有剧情的点击式冒险游戏,没有做成一页技术演示,访客为了把故事玩下去,会走过每一个房间,也就把引擎能画出的每一种光线都看了一遍。游戏还拿这件事开了一个玩笑,小人接到的工作正是给一家广告公司做一个“内裤模拟器”,他在电脑前做的每一步,都是在演示三维画面是怎样一步步渲染出来的。

游戏的故事从一段自述开始,小人说爷爷两年前去世,爷爷一生都在找成吉思汗的金内裤,而他自己每天只是做网站,生活里几乎没有事情发生。小人随后读到一封邮件,广告公司请他做内裤模拟器,他决定先去喂鱼。此后的每一条线索都通向一条内裤,鱼吃饱后给小人留下了一份礼物,正是爷爷的旧内裤,树林里还藏着一条猎人丢下的内裤。模拟器交差后,广告公司寄来一个装着内裤的包裹,鸟屋被风吹歪后又掉出一条女孩的内裤,发光的花则让小人的发明做出一条会发光的内裤。集齐之后,小人用家里的机床磨出一把钥匙,打开了爷爷上锁的书柜,并在里面读到爷爷的信。他随后在夜里挖开爷爷的坟,找到了金内裤,游戏最后停在一句“THANKS FOR PLAYING!!”上。

游戏用一件接一件的小任务控制节奏,访客每次只需要想清楚下一件事。小人的台词会交代下一步,比如他想出门时会说要先喂鱼,想开书柜时会说应该先做一把钥匙。需要等待的事情不让访客干等,而是在访客做完别的事、回到实验室时才算完成,机床磨的钥匙和发明的机器都是这样。下一件该找的东西每隔三到六秒会闪一下亮光,访客卡住时,小人走着走着停下来,也会自言自语地提醒一句。

游戏画面上只有两个固定的入口,左上角是写着 INVENTORY 的硬纸标签,点开后会落下一个装着已拿到的内裤的托盘,右上角是菜单按钮。菜单里有音乐和音效两条音量滑杆、作者的自述、GitHub 源代码的链接和一个 Develop 勾选框,勾选框旁边写着 spoilers! crash warning!,提醒访客里面有剧透,还可能让游戏崩溃。打开 Develop 之后,画面左上角会出现引擎自己的开发面板,访客可以逐层查看画面是怎样合成的,可以调光照和画质,也可以直接改每一条任务线的进度。作者因此把引擎的调试工具也当成了网站内容的一部分,交给了想看个究竟的访客。

02设计

Food for Fish 的每一个场景都像一个放在桌上的微缩模型。客厅和实验室都少了朝向访客的那面墙,镜头始终从正面平视进去,像隔着玻璃看一个剖开的娃娃屋,而这两个房间其实是同一个模型里左右相邻的两半。院子是一条横向展开的长景,左边是树林和爷爷的坟,中间是池塘,右边是小人的房子,前沿是一道切开的泥土断面。开场和结尾放在一个灰色的摄影棚里,画面上只有小人和手写的字,和游戏里的房间完全分开。

几个场景各有一套光线,这是画面上变化最大的地方。客厅里亮着天花板灯、壁灯和台灯三盏暖橙色的灯,色值分别是 #FF955C、#FF914A 和 #FFB574,红色扶手椅、木书柜和几盆绿植都被照得很暖。实验室里是一盏蓝色灯泡(#103DF8)、一盏红灯和一盏青绿色的灯,墙面因此从玫红过渡到蓝紫再到青绿,桌上的烧瓶发出荧光绿。院子里是白天的天光,天空从上方的深蓝(#007ABD)渐变到地平线附近的浅蓝(#8CD6EB),远处的山和树虚化成一片柔和的颜色。到了夜里,院子变成一片深蓝,主要的光来自坟边的提灯和小人身上那条发光的内裤。

游戏的美术方向是手工模型的质感。小人像一个黏土捏成的角色,皮肤上有细小的凹凸,胸口还有一片红疹。小人的脸只用最简单的几笔画成,两只白色的圆眼睛、两个黑色的瞳孔和一条横线就是全部五官,他会每隔几秒眨一下眼,瞳孔也会时不时往旁边瞟一下。房间里的道具都在讲故事,实验室墙上贴着一张写着 The House off Gauss 的海报,客厅墙上挂着一张画着红色虚线路线的蒙古地图和一张裱起来的金内裤照片。

游戏的界面全部做成手画在硬纸板上的样子,和精致的三维场景形成了反差。INVENTORY 标签和菜单按钮是两块硬纸片,菜单则是一张边缘不齐的硬纸卡,上面画着一条笑着的鱼。菜单底部有作者的一行小字,说自己是开发者,请大家原谅这些开发者风格的文字和美术。光标停在一件能用的物件上时,物件四周会描出一圈米白色的边,旁边出现一个硬纸板做的小图标。图标的样子说明点下去会发生什么,眼睛表示查看,一只脚表示走过去,箭头表示去另一个房间,鱼、花、钥匙和铲子则表示小人会用上手里的东西。

实验室里亮着红、蓝、青绿三色的灯,桌上的笔记本电脑四周描出一圈米白色的边,电脑旁边出现一个硬纸板做的眼睛图标,屏幕上是一个带着红色数字 1 的邮件图标,光头小人站在左边
实验室里亮着红、蓝、青绿三色的灯,桌上的笔记本电脑四周描出一圈米白色的边,电脑旁边出现一个硬纸板做的眼睛图标,屏幕上是一个带着红色数字 1 的邮件图标,光头小人站在左边

每拿到一条内裤,屏幕中间都会落下一张硬纸板包装卡,这是界面里最有趣的一组设计。每张卡都戏仿了一种商品包装,爷爷的内裤叫“ORGANIC PANTS”,旁边画着一头写着 FREE RANGE 的奶牛,猎人的内裤是“AR17-TACTICAL”军规内裤。广告公司寄来的内裤配着一个手画的球星,写着“FANCY $$$ PANTSY”,女孩的内裤上写着“Live Laugh Buy now”和“dermatologically tested”。所有卡片都用同一套做法拼成,一张网点印刷的内裤照片剪下来贴在纸板上,四周是马克笔涂鸦、涂黑的字和“MADE IN CHINA”,右下角都是同一个“USE PANTS”按钮。

四张硬纸板包装卡拼在一起,左上是写着 ORGANIC PANTS 和 FREE RANGE 的卡片,右上是写着 REAL MEN PANTS 和 AR17-TACTICAL 的卡片,左下是写着 FANCY $$$ PANTSY 的卡片,右下是写着 Live Laugh Buy now 的卡片,每张右下角都有一个 USE PANTS 按钮,背后的场景都被虚化
四张硬纸板包装卡拼在一起,左上是写着 ORGANIC PANTS 和 FREE RANGE 的卡片,右上是写着 REAL MEN PANTS 和 AR17-TACTICAL 的卡片,左下是写着 FANCY $$$ PANTSY 的卡片,右下是写着 Live Laugh Buy now 的卡片,每张右下角都有一个 USE PANTS 按钮,背后的场景都被虚化

小人说的话用一款衬线字体(笔画末端带小装饰的字体)Georgia 写在小人身旁。字是白色的,关键的词换成金色(#E6CC80),比如 GRANDPA、GOLDEN UNDERPANTS 和 UNDERWEAR SIMULATOR。每个字还带一圈很深的棕色描边,所以不管背后是亮墙还是暗处都读得清。台词总是出现在小人靠画面中间的那一侧,不会被画面边缘截掉。游戏标题、结尾致谢和菜单上的大字都是手写的笔画,和硬纸板界面的笔触一致。

游戏只为装了 WebGPU 的桌面浏览器设计。在 1366 像素宽的笔记本屏幕上,场景会按比例缩小,版面和桌面一样。在手机和平板上,游戏不会开始,屏幕上只显示一张图。这张图里是端着咖啡的小人和一行道歉的话,“Sorry, No mobile for now :( Try it on a desktop with browser that supports WebGPU, like Chrome.”。浏览器窗口很窄的时候,游戏会把房间整体缩小,好让整个房间都放得进画面,并且收起客厅角落的一盆植物。

03动效

Food for Fish 一打开,屏幕先黑两秒多,然后小人在一秒内从黑暗里显出来,起初很慢,最后很快。标题 FOOD FOR FISH 在两秒之后开始一笔一笔地写出来,整个标题大约用三秒写完,起笔和收笔都比中间慢。标题下面接着画出一道横线,这道线其实是加载进度条,它跟着后台的下载进度慢慢画长,下载完成后再从头到尾收掉。在这段时间里,小人的咖啡一直冒着热气,热气随风轻轻摆动。

游戏的镜头一直跟着光标转动,光标往左移,整个场景就微微转向左边,往右移就转向右边,场景里前后的东西因此错开,像从一扇窗户的不同角度往里看。镜头追光标的速度先快后慢,光标停下后不到一秒,镜头就几乎追上了。在院子里,镜头还会带着一点延迟跟着小人横向移动,小人走到院子两头时,镜头就停在院子的边缘。

访客点地板或物件之后,小人先用半秒转向目标,转到一半就开始以固定的速度沿直线走过去,走路和站立的动作之间用半秒交叉过渡,到了之后再转身面对目标。小人说的话从他身旁稍微滑出来,并在半秒内淡入。台词开头的十个字一次出现,后面的字以每秒约九十个字的速度打出来,访客每点一下,台词就往下翻一句,同时响起一声轻轻的点击声。小人要去另一个房间时,先走到中间的门口,然后镜头用约两秒横移到隔壁,移动先快后慢。画面从开场进入实验室时则先用一秒半暗下去,停一秒,再用三秒亮起来。

游戏在做内裤模拟器的那一段,把三维渲染的流程直接演给访客看。小人站到电脑前开始打字,访客每点一下,实验室半空中就多出一步。半空中先出现一个发光的三角形,再变成一团发光的三角形,接着三角形拼成一条内裤的线框,然后贴上花纹、加上明暗,最后成品出现在笔记本电脑的屏幕上。每一步都配着一句台词,从“先渲染一个三角形”一直讲到“完成了”,访客因此亲手点完了一遍三维画面的渲染流程。

实验室半空中浮着一条用发光三角形拼成的内裤线框,小人背对镜头站在电脑前打字,旁边写着台词 and UNDERWEAR SIMULATORS.,其中 UNDERWEAR SIMULATORS 是金色的
实验室半空中浮着一条用发光三角形拼成的内裤线框,小人背对镜头站在电脑前打字,旁边写着台词 and UNDERWEAR SIMULATORS.,其中 UNDERWEAR SIMULATORS 是金色的

游戏的场景里有很多不需要访客操作的小动作。池塘里的两条鱼沿着随机的路线游动,尾巴来回摆,院子里的落叶不停地飘,草和树叶随风轻轻摇动。实验室里的示波器屏幕上跑着波形,架子上一台仪器的数码屏显示着游戏当下的帧率。刀头和工作台在机床磨钥匙时一来一回地移动,切削时溅出的火花还会一闪一闪地照亮机床周围。夜里,提灯和发光内裤照亮的地方会随着小人移动。

游戏界面上的卡片和按钮动起来时都带一点回弹。菜单卡和包装卡从稍高一点的地方落进画面,在三分之一秒里略微冲过头再停住,同时响起一声“嗖”,收起时则在十分之一秒内向上消失。卡片落下的同时,背后的场景在半秒内虚化并暗下一半,所以访客的视线自然落在卡片上。访客点 USE PANTS 之后,小人会把内裤往上一提,身上的内裤立刻换成新的那一条,再说一句“Looks great on me!”这样的台词。游戏的开场、实验室、客厅、白天和夜里的院子以及打开菜单的时候,画面都保持在每秒 60 帧。

04工程

Food for Fish 没有用 three.js 这类现成的引擎,整个画面都由 Kris Temmerman 用 TypeScript 写的 WebGPU 引擎绘制,源代码公开在 GitHub 上。网页本身由 Next.js 导出成静态文件,部署在 GitHub Pages 上,页面里除了一块 WebGPU 画布没有别的内容。所有动画的时间线都交给 GSAP,声音交给 Howler,矩阵和向量运算用 gl-matrix 和 math.gl。引擎在启动时向浏览器申请高性能显卡,并要求两项可选的纹理功能。浏览器的用户代理是手机或平板,或者申请 WebGPU 失败时,网页就改用一块 2D 画布画出那张不支持的提示图,提示图按横屏和竖屏各准备了一张。

这套引擎走的是延迟渲染,每一帧先把场景的颜色、法线、金属度和粗糙度、自发光和深度写进一组 G-buffer。引擎接着用计算着色器做环境光遮蔽,做法参考了 Intel 公开的 XeGTAO,再做一次降噪。光照按场景分成开场、室内和室外三套,都按 GGX 的 PBR 公式计算。每一盏点光源都有自己的立方体阴影贴图,室内六盏灯中只有当前那半边房间的灯在更新阴影。室外的太阳用两层阴影贴图,近处一层每帧更新,远处一层隔一帧更新,夜里再给坟边的提灯加一张立方体阴影。

一帧实验室画面拆成四层拼在一起,左上是只有颜色、没有光照的一层,墙是白的,右上是用颜色表示表面朝向的法线层,左下是灰白的环境光遮蔽层,物件接触的地方带着阴影,右下是合成后带着红、蓝、绿灯光的最终画面
一帧实验室画面拆成四层拼在一起,左上是只有颜色、没有光照的一层,墙是白的,右上是用颜色表示表面朝向的法线层,左下是灰白的环境光遮蔽层,物件接触的地方带着阴影,右下是合成后带着红、蓝、绿灯光的最终画面

光照之后,引擎还要依次做几趟后期处理。屏幕空间反射负责地板和水面的倒影,透明物件单独画一趟,合成之后再做景深。景深的模糊程度按深度算好后存在画面的透明通道里,打开菜单时背景的虚化也只是把这组景深参数调大。泛光只处理亮度超过阈值的部分,接着是曝光、对比度、饱和度和暗角的调色,最后用 FXAA 把边缘磨平,再把台词和硬纸板界面画在同一块画布上。开场画面每帧要过约 66 趟渲染,其中大部分是阴影,实验室和客厅约 50 到 56 趟,院子约 35 到 40 趟。

物件四周那圈米白色的边也是一趟独立的处理。引擎把光标下的物件单独画成一张黑白的遮罩,横竖各模糊一次,再减去原来的遮罩,剩下的一圈就是描边。画遮罩时,引擎会拿物件的深度和 G-buffer 里的深度比较,所以被别的东西挡住的部分描边会画得淡一些。一件物件由几块模型组成时,比如邮箱的箱体、小门和小旗,几块会一起描边。

游戏里的手写字、脸和小动作都用很省的办法做出来。标题、致谢和加载进度条的笔画是作者在引擎里用鼠标画下来的,每一笔存成一串圆点,绘制时每个点是一个实例化的小方块,着色器只保留其中的圆形,所以“写字”的动画只是逐渐增加要画出的点数。这些笔画和场景一起写进 G-buffer,所以它们也会被灯光照亮。台词用 Georgia 的多通道有符号距离场(MSDF)字体图集绘制,放大缩小都很清楚。小人的眼睛和嘴是脸部着色器里按几个圆和一条线段算出来的,眨眼只要把眼睛的半径改成零。鱼的游动路线来自噪声函数,草木的摆动则在世界空间里查一张滚动的噪声贴图,越高的地方摆得越多,阴影也会跟着一起摆。

游戏的玩法写成一组简单的状态机。每一条任务线都有自己的一串状态,喂鱼这条线依次是开始、拿到鱼食、喂过鱼、内裤出现和拿到内裤。场景里的每个可点的物件都有一个触发器,它根据当前的状态决定悬停时显示哪个图标、点下去时播放哪一段过场。鼠标点中哪个物件,靠的是从光标射出一条射线去碰物件的简化碰撞模型。过场都用 GSAP 的时间线编排,所以开发面板里的“Go fast”开关只要把全局时间放快五倍,就能让整个游戏加速。

游戏的资源由 Blender 导出成 glTF,再由引擎转成自己的二进制数据。打开网站时一共下载约 31.8 MB,最大的是室外场景的数据,约 11 MB。566 张 WebP 贴图合计约 12.1 MB,每件物件都有颜色、法线和一张把金属度、粗糙度和环境光遮蔽打包在一起的贴图,大多是 1024 像素见方。14 段声音约 2.7 MB,其中约 2 MB 是巴赫《哥德堡变奏曲》第 25 变奏的录音。加载分两批进行,第一批只有小人、开场场景、字体和手写笔画,够画出标题画面。房间、院子、贴图和声音在第二批里下载,正好藏在标题一笔一笔写出来的那几秒后面。

Food for Fish 的服务器在 0.1 到 0.35 秒内就开始返回页面,但屏幕要到 2.1 到 2.7 秒才画出第一帧,在那之前一直是黑的。小人大约在第三秒出现,标题大约在第九秒写完,剩下的贴图在十秒左右下载完。加载期间页面最长只卡住约 0.2 秒,画面没有跳动。引擎还会自己降低画质,如果连续 100 帧都低于每秒 30 帧,它就把内部画面的分辨率降到按 CSS 像素计算,而屏幕上的画布本身不变,所以在像素比为 1 的屏幕上画质不会变。

这个游戏只能用鼠标来玩,页面上没有一个能获得焦点的元素。Tab、空格、回车、方向键和 Esc 都会传到页面上,但游戏不响应任何按键。读屏软件只读得到网页标题,页面也没有声明语言。游戏的代码里没有读取系统的减少动态效果设置,所以打开这个设置之后,所有动画照常播放。

05评分与团队

奖项 出处 日期 分数
FWA of the Day The FWA 2024-05-07 87 POINTS,DAYS 4
FWA of the Month The FWA 2024 年 5 月 月度最佳,不另打分
FWA of the Year The FWA 2024 年 年度最佳,不另打分

Food for Fish 是 FWA 2024 年的年度最佳,由 500 多位评委从十二个月度最佳里选出。它的当日最佳得了 87 分,被归入实验和游戏两个类别。

署名 身份 战绩
Kris Temmerman 比利时的自由职业图形程序员,自述专长是 WebGL 和 WebGPU FWA 4

这个游戏从引擎、游戏、美术到文案都出自 Kris Temmerman 一个人,结尾的致谢里只多了一位演奏者,配乐是 Shelley Katz 演奏的巴赫录音。到写报告时,Kris Temmerman 在 FWA 上一共拿过四个奖,其中三个来自这个游戏,另一个是 2026 年 2 月的当日最佳,获奖的是一个做成海盗游戏的个人作品集 Stuff by Kris Temmerman。

06档案

获奖日期
2024-05-07
奖项
The FWA FOTD 2024-05-07,The FWA FOTM 2024-05,The FWA FOTY 2024
站点
https://foodforfish.org/
署名
Kris Temmerman
设计取向
spatial
技术路线
realtime
The FWA
87 POINTS,DAYS 4。https://thefwa.com/cases/food-for-fish
技术栈
自写的 WebGPU 引擎(TypeScript),延迟渲染,XeGTAO 环境光遮蔽,MSDF 文字,Next.js,GSAP,Howler,gl-matrix
资产总计
约 31.8 MB,其中场景和动画数据约 15.2 MB,WebP 贴图约 12.1 MB,声音约 2.7 MB
首屏与帧率
FCP 2,112 到 2,728 ms(之前一直是黑屏),标题约 9 秒写完,CLS 0。各场景 60 fps