从波函数到点云:我是怎么做「氢原子电子云」这个网页的

一个静态页面、30 000 个采样点,把氢原子的概率密度实时画成一团可以旋转的电子云。

在线体验:打开电子云实验室

氢原子电子云桌面端界面

一、目标:不是一张会动的海报

最初的想法很具体:做一个深空紫黑背景、发光的品红点云、带 HUD 仪表盘的「氢原子电子云」页面,还要能切换量子数。

但我不想把它做成一张「会动的海报」。点云必须来自真实的波函数——切换 n、ℓ、m 时,云的结构要跟着物理规律变化;同时要足够流畅,手机上也能拖动旋转。

最后定下的约束是:

  • 单文件 HTML,无框架、无构建、无 CDN;
  • 真实的氢原子波函数采样,不是预设模型;
  • WebGL 渲染,30 000 个点实时旋转;
  • 桌面端右侧仪表盘,移动端上下堆叠;
  • 支持 PNG 导出,WebGL 不可用时自动降级为 2D。

最终文件约 62 KB。

二、物理:先把波函数算对

2.1 分离变量

氢原子(Z = 1)的定态波函数可以写成径向部分和角向部分的乘积:

ψnℓm(r, θ, φ) = Rnℓ(r) · Yℓm(θ, φ)

我们看到的电子云,本质是概率密度 |ψ|² 的分布。所以只要把 R 和 Y 都算对,采样出来的点云自然就是对的。

2.2 径向波函数

采用原子单位(a₀ = 1),径向函数为:

Rnℓ(r) = √[ (2/n)³ · (n−ℓ−1)! / (2n·(n+ℓ)!) ] · e^(−ρ/2) · ρ^ℓ · L^(2ℓ+1)_(n−ℓ−1)(ρ)

其中 ρ = 2r/n,L 是关联拉盖尔多项式。拉盖尔多项式用递推计算,避免直接展开阶乘和幂:

(q+1)·L(q+1) = (2q+1+k−x)·L(q) − (q+k)·L(q−1)

代码很短:

function radialR(n, l, r) {
  const rho = 2 * r / n;
  const norm = Math.sqrt(
    Math.pow(2 / n, 3) * fact(n - l - 1) / (2 * n * fact(n + l))
  );
  return norm * Math.exp(-rho / 2)
       * Math.pow(rho, l)
       * laguerre(n - l - 1, 2 * l + 1, rho);
}

2.3 角向:实球谐与相位

为了画出「正相位 / 负相位」两种颜色,我用的是实球谐函数,而不是复数形式:

  • m = 0:Y = N · Pℓ(cosθ)
  • m > 0:Y = √2 · N · Pℓ^m(cosθ) · cos(mφ)
  • m < 0:Y = √2 · N · Pℓ^|m|(cosθ) · sin(|m|φ)

归一化系数:

N = √[ (2ℓ+1)/(4π) · (ℓ−|m|)!/(ℓ+|m|)! ]

Pℓ^m 用标准的连带勒让德递推算。函数值的符号就是相位:R 和 Y 同号时标成品红,异号时标成青蓝。这样 2s 会出现「内层粉、外层蓝」,3d、4f 的相邻花瓣也会自动交替颜色。

2.4 可验证的物理量

除了云本身,界面上还实时显示这些量,方便对照:

物理量 公式 说明
平均半径 ⟨r⟩ [3n² − ℓ(ℓ+1)] / 2 单位 a₀
能级 En −13.606 / n² 单位 eV
径向节面 n − ℓ − 1 球壳状节点
角向节面 ℓ 通过原子核的节面

三、采样:把 |ψ|² 变成 30 000 个点

3.1 为什么不能均匀撒点

如果在一个立方体里均匀随机取点,得到的只是一团均匀的雾,完全看不出轨道形状。正确做法是按 |ψ|² 采样:概率密度大的地方点更密。

径向和角向可以分开采样,因为 |ψ|² = |R(r)|² · |Y(θ,φ)|²。

3.2 径向:累积分布 + 逆变换

径向的概率分布是 P(r) = r²|R(r)|²。先在 [0, rmax] 上以 6 500 个步长积分,得到归一化的累积分布 CDF,然后用逆变换采样:

function sampleR(rad, u) {
  let lo = 0, hi = rad.steps;
  while (lo + 1 < hi) {
    const mid = (lo + hi) >> 1;
    if (rad.cdf[mid] < u) lo = mid;
    else hi = mid;
  }
  const a = rad.cdf[lo], b = rad.cdf[hi];
  const t = b > a ? (u - a) / (b - a) : 0;
  return (lo + t) * rad.dr;
}

这个方法没有拒绝、不会浪费样本,而且 ⟨r⟩ 的抽样误差能稳定在 1% 以内。

3.3 角向:|P|² 逆变换 + φ 拒绝采样

角向部分 |Y|² 可以拆成 |Pℓ^m(cosθ)|² 和 cos²(mφ)(或 sin²(mφ))的乘积。

  • cosθ:同样建一条 1 600 步的 CDF,逆变换采样;
  • φ:m = 0 时均匀采样;m ≠ 0 时用拒绝采样,接受概率是 cos²(mφ) 或 sin²(mφ),平均两次就能接受一个。

这样每个点只需要两次 CDF 查找(径向和 cosθ)、一次 Legendre 计算和少量三角函数。30 000 个点的生成在几十毫秒量级,切换量子数时几乎感觉不到等待。

3.4 相位决定颜色

采样时同时记录两个符号:径向函数 R(r) 的符号,以及角向函数 Y 的符号。两者相乘为负,就写入「负相位」标记。渲染时按标记选择品红或青蓝,这就是画面里颜色交替的来源。

四、渲染:WebGL 点精灵

4.1 为什么不用 Canvas 2D

30 000 个点、每个点还要有柔和的辉光,用 Canvas 2D 的 arc 或 drawImage 每帧重绘会非常吃力。WebGL 的 gl.POINTS 可以一次性提交所有点,光栅化和混合都交给 GPU,还能用点精灵(point sprite)在片元着色器里画出圆形柔光。

4.2 顶点着色器:旋转、投影、景深

每一帧只更新一个 3×3 的旋转矩阵,点的位置不变。顶点着色器负责旋转、正交投影,以及按深度调整点的大小和亮度:

vec3 p = uRot * aPos;
float t = clamp(p.z / uZ, -1.0, 1.0);
gl_Position = vec4(
  p.x * uScale / (uRes.x * 0.5),
  p.y * uScale / (uRes.y * 0.5),
  -t * 0.8, 1.0
);
gl_PointSize = uPoint
  * (0.62 + 0.72 * (t * 0.5 + 0.5))
  * (0.70 + 0.60 * aRand);

aRand 是每个点自带的随机数,让点的大小略有差异,看起来更自然;深度越靠前的点越大、越亮,云就有了体积感。

4.3 片元着色器与加法混合

点精灵的圆形柔光来自一个高斯衰减:

vec2 d = gl_PointCoord - 0.5;
float r2 = dot(d, d) * 4.0;
if (r2 > 1.0) discard;
float a = exp(-r2 * 2.9) * vA;
gl_FragColor = vec4(vColor * a, a);

混合模式用 ONE, ONE 加法混合。大量点重叠的地方会自然累积成亮白色核心,边缘则保持透明,这正是电子云「中间亮、四周淡」的观感来源。

4.4 三层辉光与曝光补偿

单个点只有 1.85 px 左右,直接画会显得很「沙」。所以每帧画三遍:

层 点大小 透明度 作用
外层辉光 6.5× 0.028 大范围雾状光晕
中层光晕 2.8× 0.082 让点与点之间连成云
核心点 1× 0.86 保留颗粒质感

三层叠加后,既有细密的颗粒,又有柔和的整体辉光。

还有一个容易被忽略的问题:手机屏幕小,同样 30 000 个点挤在更小的面积里,重叠会更多,中心容易过曝成一大块白色。所以我按点云在屏幕上的实际半径做了一次曝光补偿:

exposure = clamp((visR / 232)^1.1, 0.42, 1.02)

屏幕越小,单点亮度越低,中心就能保持一个紧凑的白色核心,而不是糊成一片。

4.5 背景与 HUD

点云之外的部分基本都是 CSS:

  • 星野用一块独立的 2D canvas 画,带缓慢闪烁;
  • 背景是径向渐变叠加网格和噪点,中心留出紫色光晕;
  • 扫描线、四角括号、准星、坐标刻度都是伪元素或小 div;
  • 原子核是一个带 box-shadow 光晕的小圆点,外加呼吸动画。

这样 GPU 只负责最重的点云,界面装饰交给浏览器合成,性能和效果都比较平衡。

五、界面:把实验室装进一屏

5.1 布局

桌面端是一个两栏网格:左边是占满剩余空间的 3D 舞台,右边是 344–412 px 的仪表盘。窗口宽度小于 940 px 时切换成单栏,舞台固定约 54vh,仪表盘依次堆叠在下面。

移动端上下堆叠布局

右侧面板从上到下依次是:

  • 轨道名与 n、ℓ、m 三个量子数胶囊;
  • ⟨r⟩、En、节面数量;
  • 径向分布曲线;
  • 量子数选择器和预设。

5.2 径向分布曲线

曲线用 Canvas 2D 画,采样 460 个点,填充渐变、描边发光,并在 ⟨r⟩ 处画一条虚线。切换轨道时用裁剪区域做从左到右的绘制动画,看起来像仪器在重新扫描。

横轴刻度用「1 / 2 / 2.5 / 5 × 10^k」的规则取整,避免出现 3.333 这种刻度。

5.3 交互

  • 拖拽旋转:记录指针速度,松手后有轻微惯性;
  • 滚轮缩放:限制在 0.55–2.8 倍;
  • 双击复位:用缓动把视角转回初始角度;
  • 自动旋转:默认开启,拖拽时暂停,松手 1.7 秒后恢复;
  • 快捷键:1–6 切换主量子数,空格暂停旋转,R 复位视角。

量子数选择器会随 n、ℓ 的变化重建:n 增大时 ℓ 的可选范围变长,ℓ 变化时 m 的范围跟着变,选中的值超出范围会自动收缩到合法值。

5.4 导出与降级

「导出 PNG」会把星野、点云和标题重新合成到一张 2 倍分辨率的画布上,下载为图片。

如果浏览器不支持 WebGL,页面会自动改用 Canvas 2D:在 JS 里做同样的旋转和投影,用 fillRect 和 lighter 混合画出点云。效果不如 WebGL 细腻,但至少能看、能转。

另外,prefers-reduced-motion 为真时会自动关闭旋转,避免不适。

六、验证:让物理站得住

只靠「看起来像」是不够的,所以我加了一个自检模式(?selftest=1):在很宽的范围上数值积分 R²r²,检查归一化;再用 3 000 个点抽样,比较抽样 ⟨r⟩ 和理论值。

轨道 归一化积分 抽样 ⟨r⟩ / 理论 ⟨r⟩ 相对误差
1s 1.0000 1.49 / 1.50 0.6%
2s 1.0000 6.07 / 6.00 1.2%
2p 1.0000 5.01 / 5.00 0.2%
3d 1.0000 10.50 / 10.50 0.0%
4f 1.0000 17.89 / 18.00 0.6%
6h 1.0000 39.06 / 39.00 0.2%

归一化全部是 1.0000,抽样均值误差都在 1.2% 以内(界面里用 30 000 点,误差更小)。

视觉上也逐项核对过:2s 是粉色内层加蓝色外层;3d 是四瓣;4f 是八瓣;6h 的 m = 5 会形成一圈交替相位的高阶花瓣。

七、部署到 WordPress

页面本身是纯静态的,所以部署非常简单:把 index.html、style.css、app.js 三个文件放到站点的 /cloud/ 目录,nginx 直接作为静态文件返回,不经过 PHP。

入口挂在 Argon 主题的两个菜单位置上:

  • toolbar_menu:桌面端顶部导航;
  • leftbar_menu:左侧卡片和手机抽屉。

两个位置共用同一个「站点导航」菜单,里面有「电子云实验室 /cloud/」和「结构式画板 /structure/」两个自定义链接。用相对路径的好处是,无论站点以后绑定 IP 还是域名,入口都不会失效。

Argon 默认开启了 PJAX。实测点击入口时,静态页找不到主题需要的 #primary 容器,PJAX 会回退成整页加载,因此可以正常打开,不会出现半截页面。

改动菜单前我先备份了数据库,确认没问题后才继续。

八、还能怎么改进

  • 轨道切换动画:现在切换是淡入淡出,可以进一步在两组采样点之间插值,让「变形」过程连续可见;
  • 更多轨道:把 n 放开到 8,或者加入时间依赖态、叠加态的干涉;
  • 采样搬进 Web Worker:避免高 n、大点数时主线程卡顿;
  • 离屏渲染 + 色调映射:用 FBO 做真正的 Bloom,而不是多层叠加模拟;
  • WebGPU:把 30 000 点提到几十万点,同时保留 60 fps;
  • 移动端手势:双指捏合缩放、单指旋转,目前只做了拖拽和滚轮。

结语

这个页面最花时间的部分其实不是写 CSS,而是把物理、采样和渲染三件事对齐:波函数必须正确,采样必须忠实于 |ψ|²,渲染则要在「看得清结构」和「看起来像一团云」之间找平衡。

当 2s 的内外两层、3d 的四瓣、6h 的一圈花瓣随着量子数切换实时出现时,那种「公式真的在屏幕上长出来了」的感觉,是做这个页面最大的乐趣。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
下一篇