打开之后点屏幕任意一个地方,音乐就开始了,城市会跟着节拍亮起来。手机竖着开也可以。
Once it opens, tap anywhere on the screen — the music starts and the city lights up with the beat. A phone held upright works fine too.
一座跟着音乐呼吸的雨夜城市 —— 它是怎么工作的,我们又是怎么把它造出来的。
(写给十岁的你,也写给所有好奇的人。)
A rainy night city that breathes with your music — how it works, and how we actually built it.
(Written for the ten-year-old you. Curious grown-ups welcome too.)
打开之后点屏幕任意一个地方,音乐就开始了,城市会跟着节拍亮起来。手机竖着开也可以。
Once it opens, tap anywhere on the screen — the music starts and the city lights up with the beat. A phone held upright works fine too.
你在屏幕上看到的每一帧画面,都是在你看它的那一刻现场算出来的。没有一秒钟是提前录好的。你关掉再打开,雨落的位置、车开的地方,都是重新算的。
Every frame you see is computed live, at the very moment you look at it. Not one second is pre-recorded. Close it and open it again — where the rain falls and where the cars drive is all calculated fresh.
普通的 3D 游戏像搭乐高:美术师先做好每一栋楼的模型、贴好每一张贴图,游戏再把它们摆出来。这个项目连一块"乐高"都没有 —— 模型文件的数量是 0。
An ordinary 3D game is like building with LEGO: artists make a model for every building, paint every texture, and the game arranges the pieces. This project owns not a single brick — the number of model files is 0.
那楼是从哪来的?整座城市只是一道数学题。你问它:"从这里往那个方向看,最近会撞到什么?" 它就能算出答案:一面墙、一块招牌、或者湿漉漉的马路。把这道题问上几千万遍,答案拼在一起,就是你看到的画面。
So where do the buildings come from? The whole city is one math problem. You ask it: "looking from here in that direction, what's the nearest thing I'd hit?" — and it computes the answer: a wall, a neon sign, or the wet road. Ask that question tens of millions of times, stitch the answers together, and that's the picture you see.
你的屏幕由很多小点组成,每个点叫一个像素。
Your screen is made of tiny dots. Each one is a pixel.
先说一件反过来的事,这是最容易卡住的地方:真实世界里,光是从灯出发,照到墙上,再弹进你的眼睛。计算机偏偏倒着走 —— 它从"眼睛"的位置出发,朝外面射出一束看不见的探路光,去问:"我会撞到什么?"
First, something backwards — this is where most people get stuck. In the real world, light leaves a lamp, hits a wall, and bounces into your eye. The computer walks it in reverse: it starts at the "eye" and shoots an invisible scouting ray out into the world, asking, "what will I hit?"
为什么要倒着走?因为灯发出的光有千千万万束,其中绝大多数永远不会进你的眼睛(射到天上去了、射到你背后去了),算它们全是白费力气。从眼睛往外走,走出去的每一束都保证是你看得见的,一束都不浪费。
Why backwards? A lamp gives off millions of rays, and almost none of them ever reach your eye (they fly into the sky, or off behind you) — computing those would be wasted work. Walk outward from the eye instead, and every single ray you send is guaranteed to be one you can see. Not one is wasted.
那这些光是谁摆的、要不要一根一根设置?不用,一根都不用。一束探路光的方向,完全由"你是第几个像素"决定 —— 左上角那个像素就朝左上方射,正中间那个就笔直往前。我们只写了一遍"该朝哪射、射出去怎么走、撞上了怎么上色",剩下的交给显卡。
So who places all these rays — do we set them up one by one? No. Not a single one. A ray's direction is decided entirely by which pixel it belongs to — the top-left pixel aims up and to the left, the centre one aims dead ahead. We wrote the instructions once ("where to aim, how to travel, what colour to paint on a hit") and handed the rest to the graphics card.
显卡会把这一份程序同时复制几十万份,一个像素一份,每一份只知道一件事 ——「我是哪个点」。它们同时出发、同时撞墙、同时算出自己该是什么颜色,最后拼成一整张画。
The graphics card copies that one program a few hundred thousand times at once — one copy per pixel — and each copy knows exactly one thing: "which dot am I?" They all set off together, hit walls together, work out their own colours together, and the copies assemble into one picture.
这就是显卡的超能力:它不擅长做一道很难的题,它擅长同时做几十万道一模一样的小题。
That's the graphics card's superpower: it's not good at one very hard problem — it's good at hundreds of thousands of identical little problems at the same time.
这个办法叫 raymarching(光线行进)。像蝙蝠的回声定位:喊一声,听回声,就知道前面有什么、离多远。区别是蝙蝠一次喊一声,我们每一帧喊几十万声。
This technique is called raymarching. It works like a bat's echolocation: shout, listen for the echo, and you know what's ahead and how far. The difference: a bat shouts once — we shout a few hundred thousand times per frame.
算一算:一帧几十万束光 × 每秒几十帧 ≈ 每秒几千万次"我撞到了什么"。全都要在你眨眼之前算完。
Do the maths: a few hundred thousand rays per frame × dozens of frames per second ≈ tens of millions of "what did I hit?" every second. All finished before you can blink.
光线不是一毫米一毫米地挪 —— 那太慢了。整座城被切成 8 米 × 8 米的方格,像一张大棋盘,每格里最多放一栋楼。光线一格一格跳,每到一格只问一句:"这格里的楼挡住我了吗?"没挡住,跳下一格。
A ray doesn't inch forward millimetre by millimetre — far too slow. The city is cut into 8 m × 8 m squares, like a giant chessboard with at most one building per square. The ray hops square to square, asking one question per square: "does the building here block me?" If not — hop to the next.
探路光撞到了一面墙。那么 —— 窗户是谁放上去的?
The scouting ray has hit a wall. So — who put the windows on it?
答案是:没有人放。窗户根本不是一个"东西"。那面墙上没有任何地方存着"这里有一扇窗"。
Answer: nobody did. A window isn't a "thing" at all. Nowhere on that wall is it written that "a window lives here".
撞上的那一刻,我们手里只有一样东西:撞点在这面墙上的精确坐标(离墙角多远、离地面多高)。然后做三步:
At the moment of the hit, we hold exactly one piece of information: the hit point's precise coordinates on that wall (how far from the corner, how high off the ground). Then three steps:
① 这是第几扇窗?把坐标除以一扇窗的大小,取整数 —— 就像把墙按方格纸叠一遍再数格子。得到"第 3 排第 7 扇",顺便还知道"我落在这扇窗里偏左一点点"(这个零头决定你是踩在玻璃上还是踩在窗框上)。
① Which window is this? Divide the coordinates by the size of one window and keep the whole number — like laying graph paper over the wall and counting squares. You get "row 3, window 7", plus the leftover fraction ("I landed a little left of centre"), which decides whether you're standing on glass or on the frame.
② 这扇窗亮不亮?把"第 3 排第 7 扇"这个编号丢给有记性的骰子,它吐出一个 0 到 1 之间的数。低于 0.30 → 这扇是黑的;0.30~0.70 → 微微亮;0.70~0.86 → 亮;再往上 → 亮到发白。
② Is it lit? Feed "row 3, window 7" to the dice with a memory, and it returns a number between 0 and 1. Below 0.30 → this one is dark; 0.30–0.70 → a faint glow; 0.70–0.86 → lit; higher still → blazing near-white.
③ 涂什么颜色?亮的话,涂上老式钨丝灯泡那种暖黄。
③ What colour? If it's lit — the warm yellow of an old tungsten bulb.
注意:只有第 ③ 步是人定的。那是整个项目里我们真正"设置"的东西 —— 一套颜色基调:阴影偏青蓝、灯光偏暖黄、中间大片是黑的。至于哪几扇亮、亮到什么程度,一个都没人管,全是骰子说了算。
Notice: only step ③ was decided by a human. That's the one thing in the whole project we genuinely "set" — a colour direction: shadows lean teal-blue, lamps lean warm yellow, and in between lies plenty of black. As for which windows are lit and how brightly — nobody chose a single one. The dice did.
还有一件事:它其实并不"认识"窗户。探路光撞上东西的时候,只往小本子上记一个编号 —— 1 号是楼的外墙、2 号是马路、10 号是车、16 号是轮胎。上色的时候读这个编号,才决定"对这块地方该跑窗户那套算法,还是马路那套算法"。所以它不是认出了窗户,而是先知道自己撞的是一面楼墙,然后在那面墙上把窗户算了出来。
One more thing: it doesn't actually "recognise" windows. When the scouting ray hits something, all it jots down is a number — 1 means building wall, 2 means road, 10 means car, 16 means tyre. The colouring step reads that number and picks which maths to run: the window maths or the road maths. So it never recognised a window — it knew it had hit a building wall, and then computed the windows onto it.
骰子管的细节比你想的多:有的窗户拉了百叶窗,老城区的灯偏橘(像老式钠灯),整层办公室常常一起亮着。
The dice runs more details than you'd guess: some windows have their blinds drawn, the old quarter's lamps lean orange (like old sodium streetlights), and whole office floors tend to light up together.
最好玩的是 —— 每扇亮着的窗户后面,还现算了一个假房间:一面后墙、一盏天花板上的灯、一块暗地板。你走过去的时候这个小房间会跟着你的角度错动,看起来真的有深度。它不存在,它每一帧被重新算一次。
Best of all — behind every lit window there's a fake little room, computed live: a back wall, a ceiling lamp, a dark floor. As you move past, the room shifts with your angle, so it looks genuinely deep. It doesn't exist. It's recalculated every single frame.
刚才那个骰子不只管窗户。没有人一栋一栋摆过这些楼 —— 每个格子的楼有多高、开多少窗、挂不挂招牌,全是它掷出来的。
That dice does far more than windows. Nobody placed these buildings one by one — how tall each block's tower is, how many windows it gets, whether a sign hangs off it: all rolled.
它的特别之处是有记性(工程师叫它哈希):同一个路口,永远掷出同一个数。所以这座城既是随机长出来的,又每次打开都长得一模一样。你熟悉的那个粉色招牌,明天还在那个街角。
Its special trick is memory (engineers call it a hash): the same street corner always rolls the same number. So the city is random-grown, yet looks identical every time you open it. That pink sign you remember will still be on its corner tomorrow.
骰子还决定街区的性格:这一片掷出"商务区"就全是玻璃高楼,那一片掷出"霓虹街"就挤满招牌,再往外是灯很少的工业带 —— 就像真的城市。
The dice also deals out neighbourhood personalities: one district rolls "business" and fills with glass towers, another rolls "neon" and crowds up with signs, and further out lies the dim industrial belt — just like a real city.
这是个很合理的猜测 —— 但答案是不是。而且"为什么不是",比"是"有意思。
A very reasonable guess — but the answer is no. And why it's no is more interesting than a yes.
骰子确实一刻不停地在掷。但关键是:它每一帧掷的一样多。我把第 60 帧附近和第 460 帧附近各量了 60 帧:
The dice never stops rolling, true. But here's the key: it rolls exactly as much on every frame. We measured 60 frames near frame 60, and 60 more near frame 460:
| 第 60 帧附近near frame 60 | 33.27 毫秒 / 帧33.27 ms / frame |
| 第 460 帧附近near frame 460 | 33.25 毫秒 / 帧33.25 ms / frame |
差 0.02 毫秒 —— 也就是没有差别。A difference of 0.02 ms — in other words, no difference.
如果骰子是"第一次特别费劲、后面就轻松了"的那种,开头几帧会明显更慢。它没有。因为根本没有"先把城建好"这一步 —— 这座城从来没有被"建"出来过,它只是每一帧被重新算一遍,第一帧和第一千帧一样辛苦。
If the dice were the "works hard once, coasts afterwards" kind, the opening frames would be clearly slower. They aren't — because there is no "build the city first" step at all. The city was never built. It's simply recomputed every frame, and frame one works exactly as hard as frame one thousand.
真正只发生一次的是另外几件事:
The things that genuinely happen only once are these:
| 下载页面download the page | 74 KB | 整座城的说明书the whole city's recipe |
| 下载 three.jsdownload three.js | 265 KB | 一个帮忙搭桥的小工具a small helper library |
| 翻译着色器translate the shader | 78 毫秒78 ms | ← 真正的元凶← the real culprit |
| 画招牌图案paint the sign atlas | 24 毫秒24 ms | 唯一一次性的掷骰子,忽略不计the one one-time dice roll — negligible |
| 下载歌download the song | 7.8 MB | 在后台慢慢下,不挡画面streams in the background, never blocks the picture |
"翻译着色器"就是那个要等的东西。那 2360 行"数学题"是用一种通用语言写的,你的显卡听不懂 —— 得先翻译成它自己的机器语言,翻译完才能开工。这件事每台机器只做一次(之后浏览器会把翻译结果存起来)。
"Translating the shader" is the thing you wait for. Those 2,360 lines of maths are written in a universal language your graphics card doesn't speak — they must first be translated into its own machine code before any work can start. Each machine does this once (the browser keeps the translation afterwards).
麻烦在于:翻译的快慢,各家显卡能差上千倍。我这台 78 毫秒,你根本感觉不到;换一台老显卡可能要几十秒甚至更久 —— 屏幕全黑、风扇狂转,看起来就跟死机一样。(真的有人用另一台笔记本打开时遇到过。现在页面会告诉你它正卡在哪一步:"编译着色器…第一次打开最慢",而不是干黑着让你猜。)
The trouble: translation speed varies a thousandfold between graphics cards. On this machine it's 78 ms — you'd never notice. On an old card it can take tens of seconds or worse: black screen, fans screaming, looks exactly like a crash. (It genuinely happened on another laptop. The page now tells you which step it's on — "compiling the shader… slowest on the first visit" — instead of leaving you a black box to guess at.)
还有一件反过来的事:骰子不是让它变慢的原因,恰恰是让它变小的原因。
And one more thing that's backwards: the dice isn't what makes it slow — it's exactly what makes it small.
如果这座城是"存"起来的 —— 每栋楼一个模型、每面墙一张贴图 —— 那得下好几个 GB。改成"用算的",存储就换成了算术:整座城塞进一个 74 KB 的文件里,比你手机拍的一张照片还小。
If this city were stored — a model per building, a texture per wall — you'd be downloading gigabytes. Computing it instead trades storage for arithmetic: the whole city fits in one 74 KB file, smaller than a single photo from your phone.
所以第二次打开会快很多(实测 169 毫秒):页面把自己存在你浏览器里了,显卡的翻译结果也存着。只有骰子还在老老实实地掷 —— 因为它从来就不慢。
That's why the second visit is far faster (measured: 169 ms): the page has saved itself in your browser, and the graphics card kept its translation. Only the dice is still dutifully rolling — because it was never slow to begin with.
页面一边放歌,一边把声音拆开看:低音(咚咚)、中音(人声)、高音(镲片),有点像把白光分成彩虹。低音推着镜头走,高音让雨丝更亮。
While the song plays, the page takes the sound apart: bass (the thump), mids (the voice), highs (the cymbals) — a bit like splitting white light into a rainbow. The bass pushes the camera forward; the highs make the rain glint brighter.
它还会自己找歌的心跳(节拍)。每跳一下,就有一圈看不见的光从你脚下往外跑,速度是每秒 40 米 —— 光圈跑到哪,哪栋楼的窗户就"啪"地亮一下,再慢慢暗回去。
It also finds the song's heartbeat (the beat) on its own. On every beat, an invisible ring of light sets off from beneath your feet at 40 metres per second — and wherever the ring reaches, that building's windows flash awake, then slowly dim again.
还有一个小魔法:路边每一根霓虹灯管都被当成一小段太阳来算,所以粉色的灯是真的把对面的墙照成了粉色 —— 不是我们拿粉色笔涂上去的。
One more small magic: every neon tube along the street is computed as a tiny stretch of sun. So a pink lamp really does paint the opposite wall pink — nobody coloured it in with a pink pen.
不是一口气写完的。是改一点 → 检查 → 记错 → 再改一点,转了 33 圈(每一圈叫一个"里程碑"),一共提交了 139 次。两件"武器"最重要:
Not written in one sitting. The loop was change a little → check → write down the mistake → change a little more, thirty-three times around (each lap is a "milestone"), 139 commits in all. Two weapons mattered most:
① 机器人质检员。每次改完,一个自动程序会打开页面,站在城里 13 个固定的位置拍照:老城、车站、闹市、工业带、高空……然后拿尺子量每一张:最暗的地方够不够黑?最亮的地方够不够亮?有没有出现不该有的颜色?只要有一项不合格,这次修改就不算完成,不能说"应该可以了"。它还会假装成手机、假装成访客,点一下看音乐响不响。
① The robot inspector. After every change, an automatic program opens the page and takes photos from 13 fixed spots in the city — the old quarter, the station, downtown, the industrial belt, the sky… then measures each one with a ruler: are the darkest parts dark enough? The brightest bright enough? Any colour that shouldn't exist? If a single measurement fails, the change doesn't count as done — no "it should probably work". It even disguises itself as a phone, and as a first-time visitor, tapping once to hear whether the music plays.
② 错题本。每次出错 —— 哪怕是很蠢的错 —— 都写下来:出了什么事、真正的原因、怎么修的、下次怎么提前躲开。写到今天是 53 条。错题慢慢变成了规则,规则让后面的错越来越少。
② The book of mistakes. Every error — even the embarrassing ones — gets written down: what happened, the real cause, the fix, and how to dodge it earlier next time. It stands at 53 entries. Mistakes slowly turned into rules, and the rules made the later mistakes rarer.
挑三个错题本里最好玩的故事(点开看):
Three of the best stories from that book (tap to open):
我们画了个月亮,结果满屏找不到。后来发现真月亮挂在了画面外面,而我们一直盯着的那个"月亮",其实是一架飞机的灯被镜头拉出的光斑。教训:想让什么出现在画面里,先用数学算它会落在屏幕哪儿,别用眼睛猜。
We painted a moon and then couldn't find it anywhere on screen. It turned out the real moon hung just outside the frame — and the "moon" we'd been staring at was an aircraft's lamp, smeared into a glowing streak by the lens. Lesson: if you want something to appear in the picture, compute where it lands on screen first. Don't guess with your eyes.
招牌上的日文字太小太糊,我们盯着看了四遍,得出四个不同的结论。最后往字的位置画了"左红右蓝"两根色条 —— 一眼看穿:81 块竖招牌全部左右镜像了。教训:看不清的东西别硬看,造一个一眼能分辨的记号。
The Japanese lettering on the signs was too small and too blurred — we squinted at it four times and reached four different conclusions. Finally we painted two bars where the letters go, red-on-the-left, blue-on-the-right — and saw it instantly: all 81 vertical signs were mirror-flipped. Lesson: don't strain at what you can't read. Build a marker you can tell apart at a glance.
做到最后我们才发现:负责"让灯发光晕"的那一步,从项目第一天起就没执行过 —— 因为犯了一条规矩(一边往画上画、一边又在看这张画,计算机就悄悄罢工,只留了一行小字警告)。我们一直以为那行警告"没关系"。教训:警告分两种,"提醒你一下"和"这件事我根本没做"—— 要分清。
Near the very end we discovered that the step responsible for the lamps' soft glow had never run — not once since day one. We had broken a rule (painting onto a picture while also looking at that same picture), and the computer quietly went on strike, leaving only one small line of warning. We'd assumed that warning "didn't matter". Lesson: warnings come in two kinds — "just letting you know" and "I never actually did this" — and you must learn to tell them apart.
打开就点一下,默认的歌就开始放。手机竖屏横屏都行,第二次打开几乎是秒开(页面会把自己存在你手机里)。
Open it and tap once — the default song starts. Portrait or landscape both work on a phone, and the second visit opens almost instantly (the page saves itself onto your device).
放你自己的歌:电脑上把 mp3 直接拖进窗口;手机上点「文件」选一首。你的歌只存在你自己的浏览器里,不会上传到任何地方。
Play your own music: on a computer, drag an mp3 straight into the window; on a phone, tap "文件 / File" and pick one. Your song stays in your own browser — it is never uploaded anywhere.
| 空格Space | 播放 / 暂停play / pause |
| L | 歌单playlist |
| ← → | 上一首 / 下一首previous / next track |
| F | 全屏fullscreen |
| S | 把这一帧存成图片save this frame as an image |
| R | 录 30 秒视频record 30 seconds of video |
| 画质滑块quality slider | 往左更流畅,往右更清晰。不动它,它会自己找到你这台机器最舒服的档位left is smoother, right is sharper. Leave it alone and it finds the sweet spot for your machine by itself |