这个问题的起点非常简单。欧空局2013年发射了一颗卫星，叫盖亚（Gaia）。它花了十几年的时间给全天的星星拍照和测光，截止到2022年公布了18亿颗星星的方位、星等、距离、光谱等等数据。在天天下雨的西雅图，一个很有意思的实验是：如果用这18亿颗星星的数据，我能不能渲染出一张以假乱真的银河图像，卷死你们这帮摄影党？当然，这里有一个巨大的局限是，盖亚星表只包括星星的数据，不包括星云，因此成图不会包括反射和发射型星云等深空天体。但我很好奇的地方是，也许银河的结构可以出来？

这件事情并不难。每颗星星的位置、亮度和颜色都有了，画在一张图上面不是行了？于是，我让AI用最直白，最不绕弯的方式，10分钟写了一个程序出来。这个程序确实接住了我，但我发现渲染出来的结果一点都不像我们看到的星空（见图一）。接下来就是连续一个星期的改程序，翻车，改程序，翻车。连续的翻车让我意识到整件事情背后的意义比我想象的要深刻很多，它最终的目标早就超越了“模拟出来一个以假乱真的照片让我在阴天也能键盘摄影”，而在于让我意识到：其实我（们）以前从来没有思考过为什么我们头顶的星空长这个样子。

这篇文章就想展现这个探索的过程和最终的结果。我们会从最简单的画图方式开始，一步步加入更关键的物理原理，从而看到越来越真实的模拟。通过这样的过程，我们就可以理解星空之所以长这样，背后到底有哪些最关键的原理。在这个基础上还可以做一些疯狂的实验，比如因为Gaia星表给了每颗恒星的方位和距离，就给了它们在三维空间中的位置。只要我们把模拟链路跑通，我们甚至可以站在银河系的上方回望银河系，看看它的盘状结构到底是什么样的。

## 在模拟翻车中学习

所以我们就回到起点，看看下面这个图：

<div style="text-align:center; margin: 2em 0;">
  <img src="https://yage.ai/images/article_ablation_1_g11_naive.jpg" alt="图一：G<11 等恒星，每颗星一个像素，无任何后期处理" style="max-width:50%; height:auto;" />
  <p style="color:#888; font-size:0.9em; margin-top:0.5em;">图一：G<11 等恒星，每颗星一个像素，无任何后期处理</p>
</div>

这幅图就是把盖亚星表里面11的以下的星星画出来的结果：每颗星星根据赤经赤纬算出来在图上哪个位置，然后根据颜色和亮度画一个点。它看起来其实有一点银河的影子，但很显然和我们平时印象里的星空非常不一样。其中最大的区别就是亮星没了。无论是目视还是摄影，看到一个夏季银河照片的时候，我们往往可以第一眼就能注意到夏季大三角（牛郎，织女，天津四），纬度低的地方还有心宿二。但这个图上啥都没有，因此看上去就非常奇怪。

这里面其实有两个原因。第一是盖亚这颗卫星装备的是非常灵敏的测光仪器，因此它在观测的时候有意避开了全天有名的那些亮星，避免仪器读数饱和（此处感谢仓鼠和六年之后的指正）。因此我们需要用其他星表手动加入这些知名的亮星。

另一个原因是，不论是目视还是摄影，我们往往会注意到亮星看起来比暗星要大一些（即使不用柔光镜等滤镜也是这样）。这个现象其实蛮反直觉的。因为星星离我们实在太远了，因此从光学系统的角度来说每个星星都是一个点光源。而点光源经过摄影镜头放大之后，只会变亮而不会变大，没办法解释我们实际的观察。这个理论是正确的，但漏考虑了一点：虽然星星是点光源，但是光学系统不是完美的，它会把星星变成一个有一定大小的模糊的光斑。当一颗星星更亮的时候，这颗光斑能被人看见的部分就更大了。用一个比喻来看，每颗星星都像是一座山，亮的星星高一点，暗的星星矮一点。一座更高的山，它的占地面积就比一小山要更大。同时不论是视网膜还是传感器，还有一些内部反射和衍射会带来额外的光晕，二者结合就是背后的原理。

在知道这一点之后，渲染上的处理就比较简单了。我们需要从物理出发，给所有星星一个模糊的操作（术语是引入 PSF，点扩散函数）。这样亮度更亮的星星天然就有一层自然的光晕扩散出去。于是我兴冲冲地把这个原理加进了程序，结果如图二所示。

<div style="text-align:center; margin: 2em 0;">
  <img src="https://yage.ai/images/article_ablation_2_g11_bloom_legacy_color.jpg" alt="图二：加入 PSF 和亮星补全后，亮星有了光晕，但银河仍然模糊" style="max-width:50%; height:auto;" />
  <p style="color:#888; font-size:0.9em; margin-top:0.5em;">图二：加入 PSF 和亮星补全后，亮星有了光晕，但银河仍然模糊</p>
</div>

这里面可以很明显看到牛郎、织女和心宿二。从亮星的角度来说明显更像我们实际看到的星空了。但还是有一个很大的问题是：银河去哪了？我们已经渲染了11等以上（更亮）的几十万颗星星，但银河的影子仍然非常模糊。我们一开始以为是因为星星不够多，所以进一步把渲染的范围拓展到了13等以上的几百万颗星星上。但结果如下图所示，改变不大。

<div style="text-align:center; margin: 2em 0;">
  <img src="https://yage.ai/images/article_scale_g13.jpg" alt="图三：扩展到 G<13 等，几百万颗星，银河仍然模糊" style="max-width:50%; height:auto;" />
  <p style="color:#888; font-size:0.9em; margin-top:0.5em;">图三：扩展到 G<13 等，几百万颗星，银河仍然模糊</p>
</div>

经过进一步的调研，我才学习到，原来银河为什么亮，主要不是因为亮星多，而是因为有很多很多的暗星。这些暗星在视觉上无法区别，连成一个整体，就共同构成了银心附近那层淡淡的辉光。对于我们的模拟来说，有一个很简单的取巧的办法，就是在光度上有一个经验性的公式。不同的星星有不同的星等，比如我们从8等星变到9等星，虽然每颗星星变暗了，但这个亮度的星星也变多了。巧合的是，在相当大的星等范围内，他们乘起来的总光量是差不多的。换言之，所有8等星发出的光的总量（光通量的积分）和9等星发出的光的总量，在我们看来都是差不多的。所以我们虽然不知道13等以下（更暗）的星星的具体情况，但可以用13等的星星来模拟他们发出的光。反正13等的星已经是百万级别，再区分空间位置已经没有意义。所以我们用13等的星星的亮度乘以一个系数，其实可以相当真实地模拟13等以下的所有星星的光度。结果如下图所示。

<div style="text-align:center; margin: 2em 0;">
  <img src="https://yage.ai/images/article_ablation_3_g13_gain_legacy_color.jpg" alt="图四：用 13 等星乘增益代理更暗的星，银河乳光出现了，但颜色偏黄" style="max-width:50%; height:auto;" />
  <p style="color:#888; font-size:0.9em; margin-top:0.5em;">图四：用 13 等星乘增益代理更暗的星，银河乳光出现了，但颜色偏黄</p>
</div>

银河果然就出来了！这个结果非常令人振奋。但很快我们又发现两个问题。第一个问题是颜色，我们实拍的银河往往不是这么黄不拉叽的。这个问题倒比较好解决。这张图的颜色是我们用一个非常简单的公式算出来的色值。如果我们引入真的物理过程，通过主序星标定估算恒星表面温度，再用黑体辐射公式算出来它的色温，就可以得到下面这张图，颜色看起来就正常多了。

<div style="text-align:center; margin: 2em 0;">
  <img src="https://yage.ai/images/article_ablation_4_g13_gain_color_calibrated.jpg" alt="图五：引入黑体辐射色温校准后，颜色正常了" style="max-width:50%; height:auto;" />
  <p style="color:#888; font-size:0.9em; margin-top:0.5em;">图五：引入黑体辐射色温校准后，颜色正常了</p>
</div>

更大的问题出在银河的形状上。这张图你仔细看，会觉得好像有点像银河又不太像银河，在银心的部分出现了很多实拍图上没有的纹理。比如它的中央裂缝在银心附近好像分成了两条岔，最终又合为一条，远看甚至像一个汉字“中”。这是一个非常奇怪的问题，意味着要不然是我们的数据，要不然是我们渲染的过程出了比较大的问题。

在这里我卡了蛮长时间。做了很多优化都没什么用，直到最后想起来把星等从13等扩展到了15等， 18等，甚至20等，把六亿颗恒星的光度精确地积分在一起，我们熟悉的银河大裂缝才会出来。如下图所示。

<div style="text-align:center; margin: 2em 0;">
  <a href="https://grapeot.github.io/gaia_allsky/assets/fov_g20_4k.jpg" target="_blank" rel="noopener">
    <img src="https://yage.ai/images/article_scale_g18.jpg" alt="图六：扩展到 G<18 等，六亿颗星，大裂缝终于黑得干脆利落" style="max-width:50%; height:auto;" />
  </a>
  <p style="color:#888; font-size:0.9em; margin-top:0.5em;">图六：扩展到 G<18 等，六亿颗星，大裂缝终于黑得干脆利落。点击图片可打开 4K 预览。</p>
</div>

这两个图一对比，就会发现上一张图看起来不对的主要原因在于13等星和更暗的这些星星，它的空间位置还是不太一样。13等以下的星星更多地分布于裂缝两侧。虽然每颗星星的光度很弱，但是由于数量庞大，它们聚在一起产生了银心的淡淡辉光，同时让中央的裂缝变得更加明显。

这个模拟过程有点像是以力破巧，我们在上一步中引入了经验的光度公式，看起来高大上，节约了计算量，但这种近似也引入了一个很难破除的天花板。当我们有足够的数据以后，就完全不需要前面的技巧，用最朴素的公式渲染都可以得到很好的结果。在后面的文章我们还会用同样的思路计算模拟月食时候的绿松石带，那时候也会看到类似的心得。

## 光污染和星空漫游

到这里，我们的模拟本身就告一段落了。上面那个400亿像素的全分辨率银河可以在这里浏览：[https://yage.ai/gaia_milky_way/](https://yage.ai/gaia_milky_way/)

但是除了肉眼看起来像照片以外，还有什么方法能够定量地验证我们的模拟是正确的呢？有一种思路是引入光污染。一方面，光污染是一个可以定量测量的物理量。每一种波特尔暗夜等级都对应着某种强度范围（星等/平方角秒）的背景天光。这就让我们可以定量模拟不同的光污染条件下银河的观感。另一方面，不同波特尔等级下银河的观感大家也很熟悉。比如六级用眼角的旁光还可以隐隐约约看到一点点银河，7级和以上就没戏了。二者结合就很适合用来做验证。

所以我就验证了一下七级光污染下银河的观感。我的程序言之凿凿的说没问题，很清楚！如下图所示。

<div style="text-align:center; margin: 2em 0;">
  <img src="https://yage.ai/images/article_weber_off_b7.jpg" alt="图七：七级光污染下，未加 Weber 阈值——银河居然还在" style="max-width:50%; height:auto;" />
  <p style="color:#888; font-size:0.9em; margin-top:0.5em;">图七：七级光污染下，未加 Weber 阈值——银河居然还在</p>
</div>

这就很奇怪了。说明我们的渲染流程还是有问题。在经过更多的调研之后，我发现这不是一个物理问题，而是一个生理学问题。人的眼睛有一个特性，它能不能看见一个东西不取决于这个东西的绝对亮度，而取决于它相对背景亮了多少。比如我们一般说人眼在暗夜中能看见六等的星星，这并不意味着人眼也能看见总星等六等的弥散星云/辉光。而当天光背景很亮的时候，弥散结构需要远远更亮才能看见。因此，我们需要用这个特性（术语叫 Weber 阈值）来计算银河的相对亮度。在引入这个生理学模拟之后，我们很快就得到了符合实际情况的模拟结果，比如下图是七级光污染下的银河模拟：

<div style="text-align:center; margin: 2em 0;">
  <img src="https://yage.ai/images/article_weber_on_b7.jpg" alt="图八：加入 Weber 阈值后，七级光污染下银河消失，符合实际观测" style="max-width:50%; height:auto;" />
  <p style="color:#888; font-size:0.9em; margin-top:0.5em;">图八：加入 Weber 阈值后，七级光污染下银河消失，符合实际观测</p>
</div>

我们一路翻了很多次车，但每次翻车都给了我们宝贵的经验教训，纠正了之前不准确的理解，带来了新的知识。我觉得这个历练本身和带来的知识比拿到最终的照片更有意义。诚然，我学过很多天文/物理知识，可以解释很多现象，甚至做很多题。但检验知识的唯一标准是到实践中去。我们用学到的东西搓了个模拟链路，它出来的结果不对，我们学的东西就是不完整/不正确的。在这个过程中不断地加入更多的因素，发现哪些有用、哪些没用，本身就是个特别好的学习机会。

模拟还有一大好处是它比实拍要多了无数种可能性。比如我们身处银河系内部，那我们是怎么知道银河系从外面看起来是什么样的呢？盖亚星表给了我们一条途径：如果我们知道每颗星星的赤经、赤纬和离我们的距离，它们在三维空间中的位置就确定了。因而我们完全可以做一个虚拟的相机从银河系的上方向下拍摄。这样是不是就能看见一个完美的螺旋或者棒旋结构呢？遗憾的是，我照这个思路做了模拟，结果如下图所示。

<div style="text-align:center; margin: 2em 0;">
  <img src="https://yage.ai/images/article_forward_final_frame.jpg" alt="图九：从银河系上方俯视的模拟结果——看不到旋臂，因为 Gaia 视差测距只覆盖太阳附近几千光年" style="max-width:50%; height:auto;" />
  <p style="color:#888; font-size:0.9em; margin-top:0.5em;">图九：从银河系上方俯视的模拟结果——看不到旋臂，因为 Gaia 视差测距只覆盖太阳附近几千光年</p>
</div>

这并不意味着我们的银河系真的长这个样子，而是因为盖亚卫星测距的局限。它用的是视差测距，这个原理和我们用两个眼睛看东西，会有物体离我们多远的感觉一样。这种方法对近处的东西很有效，比如你可以很容易地分辨出离你一米远的东西和两米远的东西谁更远，但对很远的物体就没用了。比如我们仅凭肉眼很难分辨出100米和110米的物体谁更近。盖亚卫星也是这样，在离我们几百到几千光年内的范围内，测距还是比较准确的。但银河系的尺度是十万光年的量级，远大于这个范围。因此对于大多数恒星，我们对它距离的把握是非常粗略的，也自然看不到一个规整的螺旋形从数据中自己出现。同时因为银心的方向存在大量的尘埃遮挡，我们的数据也是不完整的。二者结合就形成了上面那幅看上去很杂乱的图。事实上，到目前为止，人类并没有任何一种途径实拍出银河系长什么样。我们在网上看到的俯视的银河结构都是间接观测推断+艺术家的想象图。我们也算是撞到了人类科技的边界上。

除此之外，我们也做了很多其他的实验。比如我们可以做个视频来演示，如何从一个广域照片用惊人的放大倍率一路推进到非常深的深空摄影。同时我们可以模拟你的相机在不同的曝光下，或者如果人类有着超人的眼睛，不同的光污染环境可以拍出怎样的银河。这些模拟的结果我们都放在这个项目的主页上了：[https://grapeot.github.io/gaia_allsky/](https://grapeot.github.io/gaia_allsky/)。所有代码也是开源的。我相信这个实现里面肯定还有很多可以改进的地方，也欢迎大家评论反馈。

<div style="text-align:center; margin: 2em 0;">
  <video controls playsinline preload="none" poster="https://grapeot.github.io/gaia_allsky/assets/zoom_poster.jpg" style="max-width:50%; height:auto; background:#000;">
    <source src="https://grapeot.github.io/gaia_allsky/assets/zoom_milkyway.mp4" type="video/mp4" />
  </video>
  <p style="color:#888; font-size:0.9em; margin-top:0.5em;">从全景推进到银心核心 1:1 的连续放大，视频无声</p>
</div>

<script async data-uid="49b08a3e73" src="https://yage-ai.kit.com/49b08a3e73/index.js"></script>
