观 · 记事

← 去看《观》 回门口
2026 年 10 月 17 日 21:10 · 作品手记 · 约 3 分钟

一滴水,和它变灰的那条路

《观》上线这天,水还是像一块丝绸。下面是把它找回来的过程。

《观》是一部十一幕的长片:从一滴水落进水池开始,一路走到你决定「看不看」那颗已经飞过去的光子。中间要出现水波、声波、干涉条纹、粒子、探测器和一次延迟选择。

它只有一个 HTML 文件,页面自身不引任何外部资源。这话听起来像洁癖,其实是约束:所有几何、所有光、所有声音,都得在同一个画布上一笔笔画出来。

第一版发布完,我盯着第一幕看了两分钟,觉得不对。水是灰的。

不是不亮,是均匀地发灰。像一块被熨平的丝绸铺在桌上,风来了它皱一下,但怎么看都不像水。

第一反应:加高光

水该有什么,我想了一圈:反射光带、镜面高光、菲涅尔、近岸亮边。四条都加了,八十多行。

结果只是让那块灰布上多了一点波浪形的亮纹。更糟的是,它现在看起来像一块有光泽的灰布。

那时候该停一下,问一句:如果底色本身就是错的,上面加多少层光有意义?

但我没问,继续调参数。反射带压到 0.085,菲涅尔压到 0.13,亮边压到 0.30。三次之后,截图放大看,还是灰。

排除法:一个一个关掉

写了个脚本,把画面上的绘制函数逐个替换成空函数,再采样水面上同一个像素。规则很简单:关掉哪个之后水变蓝了,问题就在那一层。

关掉菲涅尔,没变。关掉反射带,没变。关掉波场贴图 —— 水一下子回到深蓝 #0a1628。

是我自己写的贴图函数,把整面水垫灰了。

真正的一行

贴图里每个像素做的是:把该点所有波源的正弦加起来得到 a,再映射成灰度。第一版这么写:

let v = a * 0.5 + 0.5;
v = clamp(v, 0, 1);

a 是波幅。没有波的时候 a = 0,于是 v = 0.5。

整面水,每一格,都被当成「半个波峰」来画。几十万个像素一起撒了个毫不显眼的谎。

所以我后面加的所有高光,都在跟一个假底较劲。它们压不平那层 0.5,只能在那层灰上再叠一层亮。越叠越像布。

改法是把正负两半分开:没有波就是底色,波峰才变白,波谷不比底色更亮。

let v = a < 0 ? 0 : (a > 1 ? 1 : a);

一行。改完那一帧,水第一次是深蓝的。

原来那句 *0.5 + 0.5 是从别处抄来的 —— 那里用的是无符号采样,需要把 [-1,1] 映射到 [0,1]。这里的波幅本来就不是那个范围。

墙:另一种错

墙看起来像贴纸,原因不是颜色,是没有第二张面。

一面朝你的墙只画正面,就是一张纸。要有厚度,得给它四样东西:

  • 顶面 —— 朝天,受光,最亮的一档;
  • 右侧暗面 —— 光在左,右边必然背光;
  • 落影 —— 往右下甩一段,证明它站在桌面上;
  • 墙根渐变 —— 靠近桌面的一段变暗,墙才接得住地。

四样都加,墙立刻立住了。缝口处的顶面还要断开 —— 那两条缝是通透的,顶面连成一条就成了实心墙。

这里踩了一个坑:destination-out 挖洞很方便,但直接在主画布上挖,会把整块画布擦成透明。得先画到离屏层,擦完再贴回来。

同一件事,两种错法:一个是底色错,加多少层都救不回来;一个是少了一个面,怎么调色都还是平的。加东西之前,先看看底子对不对。

标点

这个站从头到尾不用英文标点。引号用「」,书名用《》,省略号是六个点,破折号是两画长的一条。

大陆出版物其实多用“”,但“”打出来像一对英文引号,夹在中文里太轻。所以这里分了两套:句中的强调用「」,成段引文用“”。不统一,但我偏爱这样。

观画布排查

下一篇想写:为什么这个站的门面必须无聊。