天空为什么是蓝的:拖一下太阳高度,看清瑞利散射怎么染色

举报
deli007 发表于 2026/09/28 16:52:33 2026/09/28
【摘要】 天空为什么是蓝的:拖一下太阳高度,看清瑞利散射怎么染色

同一颗太阳,正午把天空照成淡蓝,落到地平线时又能把天烧成橙红。答案在瑞利散射:短波散射强,路径一长,蓝光先被散射殆尽,剩下的红光就染上了天空。可我真去搜这个问题,翻出来的基本是 App 广告和几张互相矛盾的示意图——没有一张能让我拖着看颜色是怎么变的。

所以我把这件事做成了一个网页:拖一下太阳高度角,天顶、地平线、太阳圆盘的颜色实时变,右边五条波长(400 / 475 / 510 / 570 / 650 nm)各自的散射贡献条跟着动,点一下「运行自检」还能把模型算出的数字验一遍。

元信息先摆出来:

  • 源码:公开仓库 Demo Park → codearts-rayleigh-sky-lab,收录提交 e7c718b
  • 生成物:单个 index.html,纯前端、零依赖、不联网、不上传,全部计算在本地完成
  • 本地运行:双击文件即可;或 python -m http.server 8000
  • 复跑验证:页面自检 8/8 通过,airmass(90°)=0.9997、(650/450)⁴=4.353

这不是天体物理模拟器,也不是天气预报。页面上只有一件事:把太阳高度角和天空颜色之间那条公式,摆成能拖着看的。

图 1:太阳压到 2°——airmass 涨到 19.43,天顶只剩 rgb(57,35,36),五条波长里 650nm 红光的贡献占到 100%

为什么值得看:蓝其实是一段被散射掉的路

「天空是蓝的,因为瑞利散射,散射强度和 1/λ⁴ 成正比」——这句话几乎人人都听过,但它解释不了另一件人人都见过的事:同一片天,为什么正午是蓝的,日落是红的?

把太阳高度角当成一个能拖的变量,这件事就变得可算了。正午太阳在 90°,阳光垂直穿大气,大气光学质量 airmass 只有 1.00,路径最短,蓝光还没被散射完就到了眼睛里;太阳降到 10°,airmass 已经 5.59;降到 2°,是 19.43;贴到地平线,37.92。路径越长,蓝光在半路就被散射掉,最后落到视线里的只剩长波的红。

这个页面比一张示意图多出来的,是三个决定:

  1. 路径长度用 Kasten & Young (1989) 的经验公式,不用简单的 1/sin(γ)。1/sin 在低高度角会直接发散到无穷大,Kasten-Young 给的是 90° → 0.9997、30° → 1.9943、10° → 5.5860,和天文上常用的 airmass 表对得上。
  2. 散射强度不只乘 1/λ⁴,还要乘大气衰减 e^(−κ·m·(550/λ)⁴)。只乘 1/λ⁴ 的话,蓝光那 4.35 倍的优势会一直赢,日落永远红不起来;加上指数项,路径一长蓝光的贡献先掉到 0,红光才接得住。
  3. 五个波长各自算权重再混成 RGB,而不是先合成一个颜色再去调色相。所以右侧色条上的百分比和左边天空的颜色,是同一套数字算出来的,改一个滑杆两边一起动。

也说两点不足。 地平线那一圈的渐变不是第二套 airmass 公式,而是把当前 m 乘 1.35 再加 3.0 的近似,够用,但不是严格解。另外太阳高度在 5°~20° 之间时天顶会偏灰白(12° 时是 rgb(150,153,166)),简化光谱混出来的颜色比真实天空少一点饱和,看起来更像雾天。

任务描述

需求是直接粘进码道 Web 输入框的,原文如下:

开发一个单文件 index.html 的「天空实验室」网页应用,纯前端、零依赖、不联网。用瑞利散射解释天空和日落颜色:输入太阳高度角(-6°到90°),用 Kasten-Young 公式算大气光学质量 airmass(高度角 90° 时 airmass=1.00,30° 时约 2.00,10° 时约 5.6),散射强度正比于 1/λ^4(450nm 蓝光散射强度约为 650nm 红光的 4.35 倍),再按波长加权合成天空渐变颜色:太阳越高天空越蓝,越低越红。交互要有:太阳高度角滑杆(拖动即改天空颜色)、地平线以上的天空渐变和太阳圆盘、airmass 数值、蓝光红光散射比、当前 RGB、400/475/510/570/650nm 五个波长的示意色条。再加一个「自检」按钮,点下去跑 8 条数值断言(例如 airmass(90°)≈1.00、airmass(30°)≈2.00、蓝红散射比≈4.35、正午合成色蓝分量大于红分量、日落合成色红分量大于蓝分量),页面上显示「自检 8/8 通过」或具体失败条目。输入校验:高度角必须是 -6 到 90 的数字,空值或非法值要在页面上给出明确提示且不崩。界面深色简洁。完成后打开预览,让我直接看到运行效果。

图 2:需求原文、左侧任务清单和右下角的模型名(deepseek-v4-flash-0731)——贴进去回车就行,没有别的设置

这轮码道实际做了什么

没有让它去“润”一个漂亮的天气动画,动作全部围绕上面那段需求:

  • 交付单文件 /workspace/index.html,界面、物理计算、自检断言、输入校验全在一个文件里,无第三方依赖。
  • 物理核心是四个纯函数:scatterRatio(λ)=(550/λ)⁴、kastenYoung(h)=1/(sin h + 0.50572·(h+6.07995)^-1.6364)、spectrumWeights(m) 把「太阳光谱 × 散射比 × 大气衰减」乘起来,最后 blendLinear 归一化后按 RGB 混合。
  • 交互按需求补齐:滑杆和数字框双向同步,太阳圆盘随高度升降、随路径变红(sunColorStyle 用 1-e^(-m/7) 做热化),地平线以下整页转夜色。
  • 自检写成了 8 条纯函数断言,点按钮逐条渲染,最后给「自检 8/8 通过 ✓」或具体失败条目。
  • 做完自己打开内置预览确认能跑,我就是在预览里看到的运行效果。

图 3:码道回复里的「实现要点」——物理核心、交互、输入校验、自检四条都提到了,还主动写清了 450nm 和 650nm 的取值

代码亮点

整页最值钱的是三个函数,各自只干一件事。

1. airmass:低角度不用 1/sin

function kastenYoung(hDeg){
  const rad = hDeg * Math.PI / 180;
  return 1 / (Math.sin(rad) + 0.50572 * Math.pow(hDeg + 6.07995, -1.6364));
}
function airmass(hDeg){
  if (hDeg < 0) return Infinity;
  const m = kastenYoung(hDeg);
  return (Number.isFinite(m) && m > 0) ? m : Infinity;
}

1/sin(γ) 在 0° 直接发散,Kasten-Young 加的那个经验项把 0° 收在 37.92、10° 收在 5.5860,低角度也能算下去。地平线以下直接返回 Infinity,交给上层判成夜色。

2. 权重:散射和衰减必须一起算

w: SUN_SPECTRUM[l] * scatterRatio(l) * Math.exp(-K_SCATTER * m * scatterRatio(l))

K_SCATTER 取 0.13,是单位 airmass 下 550nm 的散射光学厚度。少了后面那个指数项,蓝光的 4.35 倍优势会一直保持,日落永远是蓝的。

3. 输入校验:非法值不崩、不乱显示

function parseAngle(raw){
  if (raw === null || raw === undefined) return null;
  const s = String(raw).trim();
  if (s === "") return null;
  const n = Number(s);
  if (!Number.isFinite(n)) return null;
  if (n < -6 || n > 90) return null;
  return n;
}

Number.isFinite 顺手挡掉 NaN 和 Infinity,越界一律返回 null,交给调用方决定「给提示 + 保留上一次有效值」。

图 4:90° 正午视角——airmass 1.00、蓝/红散射比 4.35、天顶 rgb(138,150,201),下面 8 条断言全绿

验证与踩坑

生成完我没有只看截图,而是把 index.html 拉到本地打开了复跑。

  • 自检 8/8 通过(页面点出来的原文):airmass(90°) = 0.9997、airmass(30°) = 1.9943、airmass(10°) = 5.5860、(650/450)⁴ = 4.353、正午合成色 B=0.59 > R=0.26、日落合成色 R=0.92 > B=0.05、黄昏亮度 lum 109 < 151、5 类非法值全部拒绝。
  • 滑杆数值扫描:90° → m=1.00、rgb(138,150,201);30° → 1.99;10° → 5.59、rgb(150,150,156);5° → 10.31;2° → 19.43、rgb(57,35,36);0° → 37.92;−6° → 无穷大、rgb(10,14,28) 转夜色。颜色是一路连续变过去的,不是几段硬编码渐变。
  • 失败路径:数字框里输入字母或越界值,页面立刻红框 + 提示 请输入 -6° ~ 90° 之间的数字(当前显示上一次的有效值),airmass 停在 4.71(上一次有效的 12°),天空颜色不动;失焦后输入框回退成 45。
  • 一个真实的小坑:<input type="number"> 会直接把字母清空,所以输入 abc 实际是走「空值」分支触发的提示——非法输入真正的分支只有两条:空值、越界。这算浏览器行为,不算代码问题,但测试时得知道,否则会以为字母那一条没被覆盖。

图 5:失败路径截图——输入框红框、提示原文在页面上、airmass 停在 4.71,页面没崩也没白屏

准备环境:进入码道 Web

浏览器打开码道 Web 版:https://devcloud.cn-north-4.huaweicloud.com/chat?source=dmzntgwsf&sourcead=dmzntgwwbwz,登录后就能在对话窗口输入需求,不需要装软件。

码道有三种使用方式:WebUI(浏览器对话)、TUI(终端命令行)和桌面 IDE(IDE 插件),本文用 WebUI 版。

本地复现

生成物只有一个 index.html,复现没有依赖:

  1. 从 Demo Park 的本案例目录下载 index.html。
  2. 双击打开,页面默认停在 45°。
  3. 想用静态服务也行:python -m http.server 8000,然后访问 http://localhost:8000。
  4. 点「运行自检」,应该看到「自检 8/8 通过 ✓」。

使用码道体会

这轮下来能带走的,主要是几条和模型打交道的习惯:

  • 把验收方式写进需求。结尾那句「完成后打开预览,让我直接看到运行效果」,是这个项目里省掉来回最多的一句话。
  • 数值断言要连着参考值一起给。我把 90° / 30° / 10° 三个 airmass 参考值写进了需求,它就把自检做到了页面上;只写「用 Kasten-Young 公式」,交出来的多半只是个好看的渐变。
  • 「不崩」也得写清楚。需求里写明「非法值要给明确提示且不崩」,比事后自己补校验省事。
  • 单文件需求要把边界写满。纯前端、零依赖、不联网这三条一写,产出就是双击能开的文件,没有安装步骤,也没有环境问题。
  • Agent 说完成不算验收。8/8 是页面里点出来的数字,不是回复里那句话;滑杆我还是自己拖了一遍,非法输入也自己输了一次。
  • 截图要带模型名和自检输出。一张干净的效果图证明不了什么,带自检结果和模型名的截图才是证据。

亲手点三下

页面打开就能验,三下够了:

  1. 看尺子:点「运行自检」,确认是 自检 8/8 通过 ✓,再核一下 (650/450)⁴ = 4.353 这条——蓝光散射强 4.35 倍,是整页的起点。
  2. 换个状态:把太阳从 90° 慢慢拖到 0°,看 airmass 从 1.00 涨到 37.92,同时 400nm 的贡献从 100% 掉到 0%、650nm 从 19% 涨到 100%。
  3. 看失败路径:数字框里输入 abc 或 200,必须看到红框 + 请输入 -6° ~ 90° 之间的数字(当前显示上一次的有效值),天空颜色停在上一帧、不白屏、控制台不报错。

直接下载试玩

总结

把「天空为什么是蓝的」做成一个能拖的页面之后,最重要的不是那张好看的渐变,而是 airmass 和衰减项这两处:一个负责让低角度也能算,一个负责让蓝光真的会被散射掉。改这两行里的任何一个,日落都会立刻变得不对——这就是公式能拖出来的好处。想接着玩,可以补上直射阳光的色温、加个「一天的时间轴自动播放」,或者把地面反射也算进去。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。