在本章中,我们将学习大气散射。我们建议阅读关于体积渲染和次表面散射的课程。它们与这个主题几乎共享相同的概念。大气散射可以看作是体积渲染的扩展。
介绍
几个世纪以来,天空一直是许多艺术家着迷的对象,他们试图尽可能准确地描绘其颜色。在19世纪和20世纪之前的许多代物理学家和数学家也可能痴迷于试图弄清楚是什么导致了日落和日出时天空呈现橙色,而白天呈现蓝色。历史上,大气散射的发现被归因于雷利(也被称为约翰·斯特特),他是一位英国诺贝尔物理学家,他的主要工作在19世纪末创作(雷利在剑桥大学继任了麦克斯韦,麦克斯韦以他在电磁学领域的工作而闻名。计算机图形研究的很大一部分与麦克斯韦的工作有关。雷利还与马克斯·普朗克一起研究了黑体辐射)。大气散射也会影响物体的外观。这种效应被称为空中透视,最早由莱昂纳多·达·芬奇在他的绘画中首次观察、研究和再现(事实上,这种技巧在达·芬奇之前的绘画中也可以找到)。随着物体与观察者之间的距离增加,物体的颜色被大气的颜色所取代(白天天空通常是蓝色,日出和日落时通常呈现红橙色)。这是一个重要的视觉线索,因为我们人类习惯通过比较物体的颜色来评估景观中物体的距离,以确定它们相对位置的远近。在(数字)绘画中,空中透视可以极大地增加图像的深度(大脑可以更容易地处理一个物体比另一个物体更远的信息),正如以下再现达·芬奇绘画(《石中的圣母》)所示。
在更近的历史中,也就是计算机图形的历史中,西田(Nishita)于1993年撰写了一篇重要的论文,题为《考虑大气散射的地球显示》,在其中他描述了一种模拟天空颜色的算法。有趣的是,他的这篇论文以及随后于1996年关于同一主题的论文(《考虑多次散射的天空颜色显示方法》)并不完全关于大气模拟,更多地关注了从外太空观察地球的逼真渲染(包括海洋表面、云层和大陆的渲染)。
图1:上图是来自外太空的实际地球照片。下图是使用西田的模型进行的模拟。这些图像来自西田在1993年编写的论文《考虑大气散射的地球显示》(©Siggraph)。
这让我们记起,这些年来,计算机图形技术的发展主要受到制造业的推动,以精确模拟其产品或提供3D虚拟培训环境,而不是娱乐业(电影和游戏)的推动。西田描述的模拟天空的技术自他的时代以来并没有发生太大变化。这一领域的大部分研究都集中在将他的算法实现在GPU上,而没有明显改进模拟质量(重点更多地放在速度和实时模拟上)。然而,还存在另一种天空模型,由Preetham等人在1999年Siggraph上的一篇论文中描述(《白天光线的实用分析模型》)。正如其标题所示,这是一个提供准确模拟天空颜色的分析模型;但它比西田提出的技术更加受限制(例如,该模型仅适用于地面观察者)。还值得一提的是,2011年Siggraph上发表的Jensen等人的一篇关于模拟夜空的论文(《基于物理的夜空模型》)。本章将提出西田模型的实现。在接下来的章节中,我们将研究其他算法。
在下一段中,我们将描述多种现象,一旦结合在一起,就会导致天空的颜色。然后,我们将展示西田的算法如何模拟这些现象。一旦我们在C++程序中实现了这个模型,我们将展示我们可以轻松扩展这种技术以模拟空中透视,并通过改变模型的参数,我们还可以创建外星天空。
大气模型
大气是包围行星的一层气体。这个大气层之所以保持在原位,主要是由于行星的引力。定义大气的主要特征是其厚度和组成(由哪些不同元素构成)。地球的大气厚度约为100千米(由卡门线所界定),由氧气、氮气、氩气(一种由雷利发现的气体)、二氧化碳和水蒸气组成。然而,在模拟中,我们将使用60千米的厚度(仅考虑地球大气的前两层,对流层和平流层的散射)。如果您阅读了关于体积渲染的课程(如果尚未阅读,我们建议您现在阅读),您将知道光子在与粒子碰撞时会发生散射。当光线以特定方向穿过体积(大气)时,光子在与粒子碰撞时会偏离其他方向。这个现象被称为散射。粒子多频繁地散射光线取决于粒子的特性(主要是它们的大小)以及它们在体积内的密度(一个单位体积内有多少粒子/分子)。现在我们知道了地球大气由哪些分子组成以及它们的浓度/密度。我们将使用这些科学测量值来进行大气散射模拟。围绕行星的大气的另一个关键方面是这些粒子的密度随着高度而减小。换句话说,在海平面上,每个单位空气立方体中的分子数量要比海平面以上2千米处多得多。这是一个重要的事实,因为散射的数量取决于分子的密度,正如我们刚才提到的。当光线从大气的上层向地面传播时,我们需要在光线上定期间隔采样大气密度,并根据这些采样位置的大气密度计算散射量(我们将在后面详细解释数值积分的过程)。大多数论文(包括西田的论文)假设大气密度随着高度呈指数递减。换句话说,它可以用以下形式的简单方程来建模:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> d e n s i t y ( h ) = d e n s i t y ( 0 ) e − h H density(h)=density(0) e^{-\frac{h}{H}} </math>density(h)=density(0)e−Hh
其中,density(0)
是海平面的空气密度, h
是我们测量大气密度的当前高度(海拔高度), H0
是如果大气密度均匀分布时的大气厚度(在科学论文中,H0
被称为标度高度,它与大气密度下降至原值的高度有关, ρ
是密度的温度因子)。一些论文声称这个模型是一个很好的近似。然而,也有一些其他人声称这个模型是不正确的,他们更愿意将天空密度建模为一系列同心层,每一层由其厚度和密度决定(XX请引用相关论文XX)。
将天空密度建模为一系列层(在西田的论文中称为球壳)也从计算角度有一个优势。因为我们将在光线传播到观察者的路径上执行数值积分,所以在大气中密度高的位置采样比在密度低的位置采样更为关键。执行沿射线的"恒定步长"积分将导致收敛效果不佳。如今,计算机的速度非常快,我们可以通过采取规则的步骤来强行进行这种计算,但在西田的时代,优化算法是必要的。他提出了一个模型,将大气表示为一系列球形壳,低海拔处的壳之间间隔较小,高海拔处的间隔较长。然而,在论文中发布的图表中,各层的厚度明显呈指数变化。这并不令人意外,因为在他的模型中,每个球壳的半径都由空气分子的密度分布精确确定(他用来计算密度的公式与我们上面提到的公式非常相似)。
图2:大气中存在的气溶胶是导致雾霾的原因。
图3:这是我们在本课程中将使用的大气模型的简单图形表示。它由行星的半径(Re)和大气的半径(Ra)定义。大气包括气溶胶(主要分布在较低的海拔)和空气分子。粒子的密度随着高度呈指数递减。这个图表不按比例绘制。
大气由微小颗粒(空气分子)在较低海拔与较大的颗粒,被称为气溶胶,混合而成。这些颗粒可以是由风吹起的尘土或沙子,也可能是由于空气污染而存在。它们显然会显著影响大气的外观,因为它们不会像空气分子那样以相同的方式散射光线。空气分子散射光线的现象被称为瑞利散射,而气溶胶散射光线的现象被称为米氏散射。简而言之,瑞利散射(由空气分子引起的光线散射)导致了天空的蓝色(以及在日出和日落时的红橙色)。相比之下,米氏散射(由气溶胶引起的光线散射)通常导致了你通常在受污染城市上空看到的白灰色雾霾(雾霾遮挡了天空的清晰度,见图2)。
这个模型将不完整,除非提到它需要用户指定行星的半径和大气的厚度。这些数值将计算沿视图和光线采样位置的海拔高度。对于地球,我们将使用Re = 6360千米(e代表Earth)和Ra = 6420千米(a代表大气)。我们模型中的所有距离和参数应该使用相同的单位制来表示(比如千米、米等)。
对于大气模型来说,这可能会有些棘手,因为行星的尺寸可能很大。对于这样的尺寸,千米通常是更合适的选择。然而,散射系数非常小,更容易用米或毫米来表示(甚至用纳米来表示光波长)。选择一个好的单位也受浮点精度限制的驱动(也就是说,如果数字太大或太小,计算机对这些数字的浮点表示可能会变得不准确,从而导致计算错误)。模型的输入参数可以用不同的单位表示,并由程序在内部进行转换。
图4:太阳距离地球非常遥远,以至于到达地球大气层的每一束光线都可以被视为相互平行。
最后,我们将通过指出太阳是我们天空的主要照明源来完成我们模型的描述。太阳距离地球如此遥远,以至于我们可以假设到达大气的所有光线都是平行的。我们还将展示这一观察如何简化我们模型的实现。
瑞利散射
19世纪末,瑞利发现了空气分子对光线的散射现象。他特别表明,这一现象对波长有很强的依赖性,空气分子对蓝光的散射要多于对绿光和红光的散射。这种散射形式以及他提出的用于计算由分子组成的体积(例如大气中的体积)的散射系数的方程,仅适用于其尺寸远小于可见光波长的颗粒(颗粒的大小应至少比散射波长小十分之一)。可见光的波长范围从380到780纳米;其中蓝光、绿光和红光的峰值波长分别为440、550和680纳米。在本课程的其余部分,我们将使用这些数值。瑞利散射方程提供了我们知道分子密度的体积的散射系数。在大气散射的文献中,散射系数用希腊字母β
(贝塔)表示
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> β R s ( h , λ ) = 8 π 3 ( n 2 − 1 ) 2 3 N λ 4 e − h H R \beta_R^s (h, \lambda) = \frac{8\pi^3(n^2-1)^2}{3N\lambda^4} e^{-\frac{h}{H_R}} </math>βRs(h,λ)=3Nλ48π3(n2−1)2e−HRh
这个方程中,上标S代表散射,下标R
代表瑞利(用于区分这些系数与米氏散射系数)。在这个方程中,h
是高度,λ
(lambda)是波长,n
是海平面处的分子密度,n
是空气的折射率,H
是标度高度。在我们的模拟中,我们将使用H = 8千米(如果需要更准确的测量,请参考网上的标度高度)。正如您可以从这个方程中看到,具有较短波长的光(比如蓝光)将给出较高的β值,而具有较长波长的光(红光)将给出较低的β值,这解释了为什么白天天空呈现蓝色。当太阳的光线穿过大气传播时,更多的蓝光朝着观察者散射,而绿光和红光较少。那么为什么在日出和日落时看起来是红橙色的呢?在这种特殊的配置中,太阳的光线必须穿过更长的大气层才能到达观察者的位置,而不是当太阳在头顶正上方(天顶位置)时。由于这个距离明显更长,大部分蓝光在到达观察者位置之前已经被散射掉,而一些红光,它并不像蓝光那样经常被散射,仍然存在。因此,在日出和日落时天空呈现红橙色的外观(有时也可能是紫色或略带绿色)。
图5:当太阳处于天顶位置时,阳光在到达眼睛之前只经过了短距离。在日落(或日出)时,阳光在到达观察者眼睛之前经过了更长的距离。在第一种情况下,蓝光朝向眼睛散射,天空呈蓝色。在第二种情况下,大部分蓝光在到达眼睛之前已经被散射掉。只有红绿光到达观察者的位置,这解释了为什么太阳在地平线时天空呈红橙色。
我们可以使用一些测量值 <math xmlns="http://www.w3.org/1998/Math/MathML"> N , n N,n </math>N,n来计算特定高度的β值,但我们将使用预先计算的值,这些值对应于海平面处的天空散射系数(对于波长分别为440、550和680的光,β分别为( <math xmlns="http://www.w3.org/1998/Math/MathML"> 33.1 e − 6 m − 1 33.1 \mathrm{e^{-6}} \mathrm{m^{-1}} </math>33.1e−6m−1, <math xmlns="http://www.w3.org/1998/Math/MathML"> 13.5 e − 6 m − 1 13.5 \mathrm{e^{-6}} \mathrm{m^{-1}} </math>13.5e−6m−1和 <math xmlns="http://www.w3.org/1998/Math/MathML"> 5.8 e − 6 m − 1 5.8 \mathrm{e^{-6}} \mathrm{m^{-1}} </math>5.8e−6m−1)。通过应用方程的指数部分(右侧项),我们可以为任何给定的高度调整这些系数。
Nishita's论文以及本课程中提到的其他论文的主要问题之一是它们通常不提供用于生成论文中显示的图像的N、n等值。当使用测量值时,论文的作者通常不引用出处。寻找海平面处的分子密度的科学数据并不容易,如果您有关于这个主题的任何有用链接或信息,我们很愿意听取。在本课程中使用的海平面散射系数可以在两篇论文中找到:Riley等人的"Efficient Rendering of Atmospheric Phenomena"和Bruneton等人的"Precomputed Atmospheric Scattering"。
我们不会深入讨论空气密度的细节,但有关这方面的有趣信息可以在互联网上找到。在本课程中,我们只需要海平面处的平均散射系数,这些系数对于重新创建天空的颜色非常有效,但学习如何计算这些系数将对任何希望对大气模型有更多控制的好奇读者感兴趣。
请阅读"体积渲染"课程。您将了解用于渲染参与媒体的物理模型是散射系数、吸收系数和相位函数,相位函数描述了光线碰撞颗粒时散射多少以及以哪种方向散射。在本课程中,我们仅提供了散射系数的值(并解释了如何计算),用于地球大气。对于大气散射,通常认为吸收是可以忽略的。换句话说,我们将假定大气不吸收光线。从"体积渲染"课程中记住,我们在用于渲染参与媒体的物理模型中所需的消光系数是吸收系数和散射系数之和;因此(因为吸收系数为零),我们可以写出消光系数的表达式:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> β R e = β R s \beta_R^e = \beta_R^s </math>βRe=βRs
瑞利相位函数如下所示:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> P R ( μ ) = 3 16 π ( 1 + μ 2 ) . P_R(\mu)=\frac{3}{16\pi}(1+\mu^2). </math>PR(μ)=16π3(1+μ2).
其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> μ \mu </math>μ是光线和视图方向之间的夹角的余弦(参见图8)。
米氏散射
米氏散射类似于瑞利方程,但适用于尺寸大于散射波长的颗粒,这适用于地球大气中较低高度的气溶胶。如果我们尝试将瑞利方程应用于气溶胶,我们将无法得到令人信服的图像。用于渲染散射系数的米氏方程如下:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> β M s ( h , λ ) = β M s ( 0 , λ ) e − h H M \beta_M^s(h,\lambda)=\beta_M^s(0,\lambda) e^{-\frac{h}{H_M}} </math>βMs(h,λ)=βMs(0,λ)e−HMh
其中下标 <math xmlns="http://www.w3.org/1998/Math/MathML"> M M </math>M代表米氏散射。请注意,米氏散射有一个特定的标度高度值 <math xmlns="http://www.w3.org/1998/Math/MathML"> H M H_M </math>HM(通常设置为1.2千米)。与瑞利散射不同,我们不需要一个方程来计算米氏散射系数。相反,我们将使用在海平面处进行的测量值。对于米氏散射,我们将使用 <math xmlns="http://www.w3.org/1998/Math/MathML"> β S = 210 e − 5 m − 1 \beta_S = 210 \mathrm{e^{-5}} \mathrm{m^{-1}} </math>βS=210e−5m−1。气溶胶的密度也会随着海拔高度呈指数递减。与瑞利散射类似,我们将通过在方程内部右侧包含一个指数项(由标度高度因子 <math xmlns="http://www.w3.org/1998/Math/MathML"> H M H_M </math>HM)来模拟这一效应。米氏消光系数约为其散射系数的1.1倍。米氏相位函数方程如下:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> P M ( μ ) = 3 8 π ( 1 − g 2 ) ( 1 + μ 2 ) ( 2 + g 2 ) ( 1 + g 2 − 2 g μ ) 3 2 P_M(\mu)=\frac{3}{8\pi}\frac{(1-g^2)(1+\mu^2)}{(2+g^2)(1+g^2-2g\mu)^{\frac{3}{2}}} </math>PM(μ)=8π3(2+g2)(1+g2−2gμ)23(1−g2)(1+μ2)
米氏相位函数包括一个项 <math xmlns="http://www.w3.org/1998/Math/MathML"> g g </math>g(瑞利相位函数不包括),该项控制介质的各向异性。气溶胶表现出强烈的前向定向性。一些论文使用g=0.76(我们也将使用它作为默认值)。
光学深度的概念
在我们深入研究C++中的算法实现之前,让我们将所有的要点整合在一起。首先,天空只不过是围绕一个实心球体的球形体积。因为它只是一个体积,为了渲染天空,我们可以使用射线行进算法,这是渲染参与媒体的最常见技术。我们在"体积渲染"课程中详细研究了这个算法。在渲染一个体积时,观察者(或摄像机)可以在内部或外部。
图6:摄像机可以在大气内部或外部。当摄像机在内部时,我们只关心摄像机射线与大气的交点。当摄像机在外部时,它可以在两个点处与大气相交。
当摄像机在内部时,我们必须找到视线射线离开体积的位置。当摄像机在外部时,我们需要找到视线射线进入和离开体积的位置。由于天空是一个球体,我们将使用一个射线-球体相交例程来解析计算这些点。我们在基础部分中介绍了计算射线与球体相交的一些技术(射线-二次形状相交)。我们可以通过使用地面作为参考来测试摄像机的高度,以确定摄像机是在大气内部还是外部。如果这个高度大于大气的厚度,摄像机就在大气外部,视线射线可能会在两个位置与大气相交。
图7:单次散射负责天空的颜色。观众很少直接看太阳(这是危险的)。然而,当我们背对太阳观看时,大气的颜色是阳光中的蓝光朝向观察者眼睛的方向散射的结果。
目前,我们将假设摄像机位于地面上(或高于地面一米),但该算法对于任意摄像机位置都适用(我们将在本课程末尾从外太空渲染天空的图像)。假设我们有一个视线射线(对应于帧中的一个像素),并且我们知道这条射线与大气的上限相交的位置。在这一点,我们需要解决的问题是找出有多少光沿着这条射线朝着观察者的方向传播。正如我们在以下图中所看到的,我们的摄像机或观察者不太可能直接看向太阳(这对眼睛是危险的)。
图8:为了计算天空的颜色,我们首先从Pc到Pa跟踪一条射线,然后积分计算来自太阳的光(具有方向L)沿着那条射线(V)反射的光量。
从所有的逻辑上讲,如果你朝着天空的方向看,你本不应该看到任何东西,因为你正在看向外太空,那里是空无一物的(太空中的天体之间的空间)。然而,事实是你看到了蓝色。这是因为来自太阳的光线进入大气并被空气分子偏折到观察方向。换句话说,没有直接来自太阳的光线沿着观察方向传播(除非观察方向直接指向太阳),但由于来自太阳的光线在大气中被散射,其中一部分光线最终朝着你的眼睛传播。这种现象被称为单次散射(在"体积渲染"课程中有解释)。
到目前为止,我们知道为了为帧中的一个像素渲染天空颜色,我们首先将从兴趣点 <math xmlns="http://www.w3.org/1998/Math/MathML"> P c Pc </math>Pc(摄像机位置)沿着一个视线射线( <math xmlns="http://www.w3.org/1998/Math/MathML"> V V </math>V)投射。我们将计算这条射线与大气相交的点$Pa)。最后,我们需要计算由于单次散射沿着这条射线传播的光线量。这个值可以使用我们在"体积渲染"课程中学习的体积渲染方程来计算:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> L ( P c , P a ) = ∫ P c P a T ( P c , X ) L s u n ( X ) d s L(P_c, P_a)=\int_{P_c}^{P_a} T(P_c,X)L_{sun} (X)ds </math>L(Pc,Pa)=∫PcPaT(Pc,X)Lsun(X)ds
其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> T T </math>T是点 <math xmlns="http://www.w3.org/1998/Math/MathML"> P c Pc </math>Pc和 <math xmlns="http://www.w3.org/1998/Math/MathML"> X X </math>X之间的透射率(采样位置沿视线方向), <math xmlns="http://www.w3.org/1998/Math/MathML"> L L </math>L是在点 <math xmlns="http://www.w3.org/1998/Math/MathML"> X X </math>X处体积内的光量。这只是说到达观察者点 <math xmlns="http://www.w3.org/1998/Math/MathML"> P c Pc </math>Pc的总光量等于沿着视线 <math xmlns="http://www.w3.org/1998/Math/MathML"> V V </math>V方向散射的所有阳光。这个数量可以通过对沿着视线方向 <math xmlns="http://www.w3.org/1998/Math/MathML"> V V </math>V的各个位置( <math xmlns="http://www.w3.org/1998/Math/MathML"> X X </math>X)的光线进行求和来获得。这种技术称为数值积分。我们沿着射线采样的点越多,结果就越好,但计算所需的时间就越长。方程中的透射率项( <math xmlns="http://www.w3.org/1998/Math/MathML"> T T </math>T)考虑了沿着视线方向 <math xmlns="http://www.w3.org/1998/Math/MathML"> V V </math>V到观察者方向的每个采样位置( <math xmlns="http://www.w3.org/1998/Math/MathML"> X X </math>X)处沿着方向散射的光线也会随着从 <math xmlns="http://www.w3.org/1998/Math/MathML"> P c Pc </math>Pc到 <math xmlns="http://www.w3.org/1998/Math/MathML"> P P </math>P的传播而衰减。
回顾体积渲染课程中的内容,光线在体积中从一个点传播到另一个点时会由于吸收和出散射而衰减。我们称在点 <math xmlns="http://www.w3.org/1998/Math/MathML"> P b Pb </math>Pb处的光量为 <math xmlns="http://www.w3.org/1998/Math/MathML"> L b Lb </math>Lb,而从点 <math xmlns="http://www.w3.org/1998/Math/MathML"> P a Pa </math>Pa到 <math xmlns="http://www.w3.org/1998/Math/MathML"> P b Pb </math>Pb处的光量为 <math xmlns="http://www.w3.org/1998/Math/MathML"> L a La </math>La。在存在参与介质的情况下,从点 <math xmlns="http://www.w3.org/1998/Math/MathML"> P b Pb </math>Pb接收到的光量 <math xmlns="http://www.w3.org/1998/Math/MathML"> L a La </math>La将低于 <math xmlns="http://www.w3.org/1998/Math/MathML"> L b Lb </math>Lb。透射率表示 <math xmlns="http://www.w3.org/1998/Math/MathML"> L a La </math>La与 <math xmlns="http://www.w3.org/1998/Math/MathML"> L b Lb </math>Lb的比值,因此透射率 <math xmlns="http://www.w3.org/1998/Math/MathML"> T T </math>T的取值范围为零到一。方程形式的透射率如下所示:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> T ( P a , P b ) = L a L b = e x p ( − ∑ P a P b β e ( h ) d s ) T(P_a,P_b)=\frac{L_a}{L_b}=exp(-\sum_{P_a}^{P_b} \beta_e(h)ds) </math>T(Pa,Pb)=LbLa=exp(−Pa∑Pbβe(h)ds)
简而言之,这个方程意味着我们需要在射线Pa-Pb的路径上的各个采样位置测量大气的消光系数 <math xmlns="http://www.w3.org/1998/Math/MathML"> β e \beta_e </math>βe,将这些值相加,乘以长度段 <math xmlns="http://www.w3.org/1998/Math/MathML"> d s ds </math>ds(即射线Pa-Pb的距离除以使用的采样数量),取这个值的负值并将其输入到指数函数中。
图9:当光线(Lb)从Pb到Pa传播时,由于出散射和吸收,它会减弱。结果是La。透射率是在光线穿过大气时被减弱后在Pa处从Pb处接收到的光的数量(即T=La/Lb)。
回顾我们关于瑞利散射的讨论,你会记得可以使用海平面上的散射系数 <math xmlns="http://www.w3.org/1998/Math/MathML"> β s \beta_s </math>βs经高度e除以尺度高度( <math xmlns="http://www.w3.org/1998/Math/MathML"> H H </math>H)的指数来计算(从中我们将计算)。
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> β s ( h ) = β s ( 0 ) e x p ( − h H ) \beta_s(h) = \beta_s(0)exp(-\frac {h}{H}) </math>βs(h)=βs(0)exp(−Hh)
对于瑞利散射,可以忽略吸收,我们可以写成:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> β e ( h ) = β s ( h ) + β a ( h ) = β s ( h ) + 0 = β s ( h ) \beta_e(h)=\beta_s(h)+\beta_a(h)=\beta_s(h)+0=\beta_s(h) </math>βe(h)=βs(h)+βa(h)=βs(h)+0=βs(h)
你可以将 <math xmlns="http://www.w3.org/1998/Math/MathML"> β e ( 0 ) \beta_e(0) </math>βe(0)移到积分之外(因为它是一个常数),然后你可以将透射率方程重写为:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> T ( P a , P b ) = e x p ( − β e ( 0 ) ∑ P a P b e x p ( − h H ) d s ) T(P_a, P_b)=exp(-\beta_e(0) \sum_{P_a}^{P_b} exp(-\frac{h}{H})ds) </math>T(Pa,Pb)=exp(−βe(0)Pa∑Pbexp(−Hh)ds)
指数函数中的求和可以看作是点Pa-Pb之间的平均密度值。这个方程本身通常在文献中被称为点Pa-Pb之间的大气光学深度。
添加太阳光
最后,我们将使用前面段落中介绍的渲染方程来计算天空的颜色。让我们再次写出它:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> L ( P c , P a ) = ∫ P c P a T ( P c , X ) L s u n ( X ) d s L(P_c,P_a)=\int_{P_c}^{P_a} T(P_c, X)L_{sun}(X)ds </math>L(Pc,Pa)=∫PcPaT(Pc,X)Lsun(X)ds
我们已经解释了如何渲染透射率。现在让我们来看看术语Lsun(X)并解释如何评估它。Lsun(X)对应于在采样位置 <math xmlns="http://www.w3.org/1998/Math/MathML"> X X </math>X沿视线方向散射的阳光量。要评估它,首先,我们必须计算到达 <math xmlns="http://www.w3.org/1998/Math/MathML"> X X </math>X的光线。直接获取太阳光强度是不够的。实际上,如果光线进入大气中的 <math xmlns="http://www.w3.org/1998/Math/MathML"> P s Ps </math>Ps,它也会在传播到 <math xmlns="http://www.w3.org/1998/Math/MathML"> X X </math>X期间减弱。因此,我们需要使用与我们用来计算沿着视线从 <math xmlns="http://www.w3.org/1998/Math/MathML"> P a Pa </math>Pa到 <math xmlns="http://www.w3.org/1998/Math/MathML"> P c Pc </math>Pc的光线的衰减的方程(方程1)类似的方程来计算这种衰减: 方程一
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> L s u n ( X ) = S u n I n t e n s i t y ∗ T ( X , P s ) L_{sun}(X)=Sun \: Intensity * T(X,P_s) </math>Lsun(X)=SunIntensity∗T(X,Ps)
在技术上,对于沿着视线的每个采样位置 <math xmlns="http://www.w3.org/1998/Math/MathML"> X X </math>X,我们将需要投射一条光线,指向太阳的方向( <math xmlns="http://www.w3.org/1998/Math/MathML"> L L </math>L),并找到这束光线与大气相交的位置( <math xmlns="http://www.w3.org/1998/Math/MathML"> P s Ps </math>Ps)。然后,我们将使用与用于视线的数值积分技术相同的方法来评估方程1中的透射率(或光学深度)项。光线将被划分成段,每个光段的中心的大气密度将被评估。
计算天空颜色算法的最后一个要素之一是根据光线方向和视线方向来计算散射在视线方向上的光量,这是相位函数的作用。瑞利散射和Mie散射都有它们的相位函数,我们在前面已经给出了。如果您需要关于相位函数的复习,请阅读体积渲染课程。简而言之,相位函数是一个描述从方向 <math xmlns="http://www.w3.org/1998/Math/MathML"> L L </math>L的光有多少散射到方向 <math xmlns="http://www.w3.org/1998/Math/MathML"> V V </math>V的函数。Mie相位函数包含一个额外的项 <math xmlns="http://www.w3.org/1998/Math/MathML"> g g </math>g,称为平均余弦(还有许多其他可能的名称),它定义了光主要是沿着前向方向( <math xmlns="http://www.w3.org/1998/Math/MathML"> L L </math>L)还是后向方向( <math xmlns="http://www.w3.org/1998/Math/MathML"> − L -L </math>−L)散射。对于前向散射, <math xmlns="http://www.w3.org/1998/Math/MathML"> g g </math>g在范围[0:1]内,而对于后向散射, <math xmlns="http://www.w3.org/1998/Math/MathML"> g g </math>g在范围[-1:0]内。当 <math xmlns="http://www.w3.org/1998/Math/MathML"> g g </math>g等于零时,光在所有方向上均匀散射,我们称之为散射是各向同性的。我们将为Mie散射设置 <math xmlns="http://www.w3.org/1998/Math/MathML"> g g </math>g为0.76。
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> L s u n ( X ) = S u n I n t e n s i t y ∗ T ( X , P s ) ∗ P ( V , L ) L_{sun}(X)=Sun \: Intensity*T(X,P_s)*P(V,L) </math>Lsun(X)=SunIntensity∗T(X,Ps)∗P(V,L)
最后,我们必须考虑到瑞利散射主要是沿视线方向散射蓝光。为了反映这一点,我们将通过散射系数( <math xmlns="http://www.w3.org/1998/Math/MathML"> β s \beta_s </math>βs )来乘以先前方程的结果,这给出了方程的光部分。然而,请注意 <math xmlns="http://www.w3.org/1998/Math/MathML"> β s \beta_s </math>βs 随着高度变化(其值是高度的函数),其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> h h </math>h 是关于地面 <math xmlns="http://www.w3.org/1998/Math/MathML"> X X </math>X(更准确地说是海平面)的高度:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> L s u n ( X ) = S u n I n t e n s i t y ∗ T ( X , P s ) ∗ P ( V , L ) ∗ β S ( h ) L ( X ) = ∫ 4 π L i g h t I n t e n s i t y ∗ T ( X , P s ) ∗ P ( V , L ) ∗ β S ( h ) \begin{array}{l} L_{sun}(X)=Sun \: Intensity*T(X,P_s)*P(V,L)*\beta_S(h)\\ L(X)=\int_{4\pi} Light \: Intensity * T(X, P_s)*P(V,L)*\beta_S(h) \end{array} </math>Lsun(X)=SunIntensity∗T(X,Ps)∗P(V,L)∗βS(h)L(X)=∫4πLightIntensity∗T(X,Ps)∗P(V,L)∗βS(h)
对于地球的大气,太阳是天空中唯一的光源;然而,为了编写先前方程的通用形式,我们应该考虑到光可能来自许多方向。在科学论文中,您通常会看到这个方程作为对 <math xmlns="http://www.w3.org/1998/Math/MathML"> 4 π 4\pi </math>4π的积分,描述了一组传入方向的球体。这个更通用的方程还将考虑地面反射的阳光(多次散射),但在本章中,我们将忽略它。
计算天空颜色
将所有元素合在一起,我们得到(方程2):
方程2
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> S k y C o l o r ( P c , P a ) = ∫ P c P a T ( P c , X ) L s u n ( X ) d s \begin{array}{l} Sky \: Color(P_c, P_a)=\int_{P_c}^{P_a}T(P_c,X)L_{sun}(X)ds \end{array} </math>SkyColor(Pc,Pa)=∫PcPaT(Pc,X)Lsun(X)ds
方程3
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> L s u n ( X ) = S u n I n t e n s i t y ∗ T ( X , P s ) ∗ P ( V , L ) ∗ β S ( h ) L_{sun}(X)=Sun \: Intensity * T(X,P_s)*P(V,L)*\beta_S(h) </math>Lsun(X)=SunIntensity∗T(X,Ps)∗P(V,L)∗βS(h)
如果我们在方程2中替换方程3
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> S k y C o l o r ( P c , P a ) = ∫ P c P a T ( P c , X ) ∗ S u n I n t e n s i t y ∗ P ( V , L ) ∗ T ( X , P s ) ∗ β s ( h ) d s \begin{array}{l} Sky \: Color(P_c, P_a) = \\ \int_{P_c}^{P_a} T(P_c, X) * Sun \: Intensity * P(V,L) * T(X,P_s)*\beta_s(h)ds \end{array} </math>SkyColor(Pc,Pa)=∫PcPaT(Pc,X)∗SunIntensity∗P(V,L)∗T(X,Ps)∗βs(h)ds
相位函数的结果是一个常数,太阳光强度也是如此。因此,我们可以将这两个项移到积分之外(方程4):
方程4
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> S k y C o l o r ( P c , P a ) = S u n I n t e n s i t y ∗ P ( V , L ) ∫ P c P a T ( P c , X ) ∗ T ( X , P s ) ∗ β s ( h ) d s \begin{array}{l} Sky \: Color(P_c, P_a) = \\ Sun \: Intensity * P(V,L)\int_{P_c}^{P_a} T(P_c, X) * T(X,P_s)*\beta_s(h)ds \end{array} </math>SkyColor(Pc,Pa)=SunIntensity∗P(V,L)∫PcPaT(Pc,X)∗T(X,Ps)∗βs(h)ds
这是用于渲染特定视角和光方向的天空颜色的最终方程。因此,它并不复杂。它的计算要求更高,因为我们要计算积分,并多次调用指数函数(通过透射率项)。此外,请记住天空颜色是由瑞利和米氏散射产生的。因此,我们需要为每种散射类型两次计算这个方程:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> S k y C o l o r ( P c , P a ) = S k y C o l o r R a y l e i g h ( P c , P a ) + S k y C o l o r M i e ( P c , P a ) \begin{array}{l} Sky \: Color(P_c, P_a) = \\ Sky \: Color_{Rayleigh}(P_c,P_a) +Sky \: Color_{Mie}(P_c,P_a) \end{array} </math>SkyColor(Pc,Pa)=SkyColorRayleigh(Pc,Pa)+SkyColorMie(Pc,Pa)
从微积分中记得,两个指数函数的乘积等于一个指数函数,其参数是从前两个函数的参数之和:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> e a e b = e a + b e^ae^b=e^{a+b} </math>eaeb=ea+b
我们可以利用这一特性(我们只计算一个指数,而不是两个),并重新编写方程4中两个透射率项的乘法:
方程5
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> T ( P c , X ) = e − β e 0 T ( X , P s ) = e − β e 1 T ( P c , X ) ∗ T ( X , P s ) = e − β e 0 ∗ e − β e 1 = e − ( β e 0 + β e 1 ) \begin{array}{l} T(P_c,X)=e^{ -\beta_{e0} } \\ T(X,P_s)=e^{ -\beta_{e1} } \\ T(P_c,X)*T(X,P_s)=e^{ -\beta_{e0} }*e^{ -\beta_{e1} }=e^{ -(\beta_{e0} + \beta_{e1}) } \end{array} </math>T(Pc,X)=e−βe0T(X,Ps)=e−βe1T(Pc,X)∗T(X,Ps)=e−βe0∗e−βe1=e−(βe0+βe1)
现在让我们看看如何在C++程序中实现大气模型。
实现(c++)
现在,我们已经拥有了在C++程序中实现Nishita算法的所有所需元素。和往常一样,程序将以易于理解算法的方式编写。在本段的末尾,将建议一些优化方法,以提高程序的运行速度。然而,实时性不是这里的目标;使用此程序仍然可以在几秒钟内创建天空图像。
首先,我们将创建一个Atmosphere
类,用于指定我们系统的所有参数:行星和大气层的半径(Re、Ra)、海拔处的瑞利和米氏散射系数、瑞利和米氏尺度高度(Hr和Hm)、太阳方向、太阳强度和平均余弦。所有距离以米为单位(散射系数也是如此)。
c++
class Atmosphere
{
public:
Atmosphere(
Vec3f sd = Vec3f(0, 1, 0),
float er = 6360e3, float ar = 6420e3,
float hr = 7994, float hm = 1200) :
sunDirection(sd),
earthRadius(er),
atmosphereRadius(ar),
Hr(hr),
Hm(hm)
{}
Vec3f computeIncidentLight(const Vec3f& orig, const Vec3f& dir, float tmin, float tmax) const;
Vec3f sunDirection; //The sun direction (normalized)
float earthRadius; //In the paper this is usually Rg or Re (radius ground, eart)
float atmosphereRadius; //In the paper this is usually R or Ra (radius atmosphere)
float Hr; //Thickness of the atmosphere if density was uniform (Hr)
float Hm; //Same as above but for Mie scattering (Hm)
static const Vec3f betaR;
static const Vec3f betaM;
};
const Vec3f Atmosphere::betaR(3.8e-6f, 13.5e-6f, 33.1e-6f);
const Vec3f Atmosphere::betaM(21e-6f);
我们将渲染天空,就好像它是用鱼眼镜头拍摄的一样。相机看着天空正上方,捕捉到天空的360度景象。为了创建一个渲染不同太阳位置的天空的动画,我们将渲染一系列帧。在第一帧中,太阳处于天顶(中心帧)。在最后一帧中,太阳略微位于地平线下。
c++
void renderSkydome(const Vec3f& sunDir, const char *filename)
{
Atmosphere atmosphere(sunDir);
auto t0 = std::chrono::high_resolution_clock::now();
const unsigned width = 512, height = 512;
Vec3f *image = new Vec3f[width * height], *p = image;
memset(image, 0x0, sizeof(Vec3f) * width * height);
for (unsigned j = 0; j < height; ++j) {
float y = 2.f * (j + 0.5f) / float(height - 1) - 1.f;
for (unsigned i = 0; i < width; ++i, ++p) {
float x = 2.f * (i + 0.5f) / float(width - 1) - 1.f;
float z2 = x * x + y * y;
if (z2 <= 1) {
float phi = std::atan2(y, x);
float theta = std::acos(1 - z2);
Vec3f dir(sin(theta) * cos(phi), cos(theta), sin(theta) * sin(phi));
// 1 meter above sea level
*p = atmosphere.computeIncidentLight(Vec3f(0, atmosphere.earthRadius + 1, 0), dir, 0, kInfinity);
}
}
fprintf(stderr, "\b\b\b\b%3d%c", (int)(100 * j / (width - 1)), '%');
}
std::cout << "\b\b\b\b" << ((std::chrono::duration<float>)(std::chrono::high_resolution_clock::now() - t0)).count() << " seconds" << std::endl;
// Save result to a PPM image (keep these flags if you compile under Windows)
std::ofstream ofs(filename, std::ios::out | std::ios::binary);
ofs << "P6\n" << width << " " << height << "\n255\n";
p = image;
for (unsigned j = 0; j < height; ++j) {
for (unsigned i = 0; i < width; ++i, ++p) {
#if 1
// Apply tone mapping function
(*p)[0] = (*p)[0] < 1.413f ? pow((*p)[0] * 0.38317f, 1.0f / 2.2f) : 1.0f - exp(-(*p)[0]);
(*p)[1] = (*p)[1] < 1.413f ? pow((*p)[1] * 0.38317f, 1.0f / 2.2f) : 1.0f - exp(-(*p)[1]);
(*p)[2] = (*p)[2] < 1.413f ? pow((*p)[2] * 0.38317f, 1.0f / 2.2f) : 1.0f - exp(-(*p)[2]);
#endif
ofs << (unsigned char)(std::min(1.f, (*p)[0]) * 255)
<< (unsigned char)(std::min(1.f, (*p)[1]) * 255)
<< (unsigned char)(std::min(1.f, (*p)[2]) * 255);
}
}
ofs.close();
delete[] image;
}
int main()
{
#if 1
// Render a sequence of images (sunrise to sunset)
unsigned nangles = 128;
for (unsigned i = 0; i < nangles; ++i) {
char filename[1024];
sprintf(filename, "./skydome.%04d.ppm", i);
float angle = i / float(nangles - 1) * M_PI * 0.6;
fprintf(stderr, "Rendering image %d, angle = %0.2f\n", i, angle * 180 / M_PI);
renderSkydome(Vec3f(0, cos(angle), -sin(angle)), filename);
}
#else
...
#endif
return 0;
}
这段代码用于计算方程4(特定相机射线的天空颜色)。首先,我们找到相机射线与大气的交点(第4行)。然后,我们计算Rayleigh和Mie相位函数的值(使用太阳和相机射线方向,第14和第16行)。第一个循环(第17行)在相机射线上创建样本。注意样本位置(方程中的X)是段的中点(第19行)。从那里,我们可以计算样本(X)的高度(第19行)。我们计算Rayleigh和Mie散射的exp(-h/H)
乘以ds
的值(使用Hr和Hm,第23和第24行)。这些值被累积(第23和第24行),以计算X点的光学深度。我们稍后还将使用它们来缩放方程4中的散射系数 <math xmlns="http://www.w3.org/1998/Math/MathML"> β s ( h ) \beta_s(h) </math>βs(h)(第42和第43行)。然后,我们计算X点太阳光的强度(第31到第38行)。我们投射一个指向太阳的光线(光线是平行的),并找到它与大气的交点。这个光线被划分成多个段,并且我们评估了每个光段中心的密度(第35和第36行)。累积这些值可以给我们光线的光学深度。请注意,我们测试每个光样本是否在地面上方或下方。如果在地面以下,光线处于地球的阴影中;因此,我们可以安全地丢弃该光线的贡献(第34和第39行)。请注意,Mie的消光系数约是Mie散射系数的1.1倍(第40行)。最后,使用方程5中的技巧,我们可以通过累积它们的光学深度来计算光和相机射线的累积透射率,只需进行一次指数调用(第40和第41行)。在函数的最后,我们返回特定射线的天空颜色,这是Rayleigh和Mie散射透射率的总和,乘以它们各自的相位函数和散射系数。此总和还乘以太阳的强度(第48行)。
现在,太阳的强度只是一个神奇的数字(我们使用了值20)。但在将来的版本中,我们将学习如何使用实际的物理数据)。
c++
Vec3f Atmosphere::computeIncidentLight(const Vec3f& orig, const Vec3f& dir, float tmin, float tmax) const
{
float t0, t1;
if (!raySphereIntersect(orig, dir, atmosphereRadius, t0, t1) || t1 < 0) return 0;
if (t0 > tmin && t0 > 0) tmin = t0;
if (t1 < tmax) tmax = t1;
uint32_t numSamples = 16;
uint32_t numSamplesLight = 8;
float segmentLength = (tmax - tmin) / numSamples;
float tCurrent = tmin;
Vec3f sumR(0), sumM(0); //mie and rayleigh contribution
float opticalDepthR = 0, opticalDepthM = 0;
float mu = dot(dir, sunDirection); //mu in the paper which is the cosine of the angle between the sun direction and the ray direction
float phaseR = 3.f / (16.f * M_PI) * (1 + mu * mu);
float g = 0.76f;
float phaseM = 3.f / (8.f * M_PI) * ((1.f - g * g) * (1.f + mu * mu)) / ((2.f + g * g) * pow(1.f + g * g - 2.f * g * mu, 1.5f));
for (uint32_t i = 0; i < numSamples; ++i) {
Vec3f samplePosition = orig + (tCurrent + segmentLength * 0.5f) * dir;
float height = samplePosition.length() - earthRadius;
// compute optical depth for light
float hr = exp(-height / Hr) * segmentLength;
float hm = exp(-height / Hm) * segmentLength;
opticalDepthR += hr;
opticalDepthM += hm;
// light optical depth
float t0Light, t1Light;
raySphereIntersect(samplePosition, sunDirection, atmosphereRadius, t0Light, t1Light);
float segmentLengthLight = t1Light / numSamplesLight, tCurrentLight = 0;
float opticalDepthLightR = 0, opticalDepthLightM = 0;
uint32_t j;
for (j = 0; j < numSamplesLight; ++j) {
Vec3f samplePositionLight = samplePosition + (tCurrentLight + segmentLengthLight * 0.5f) * sunDirection;
float heightLight = samplePositionLight.length() - earthRadius;
if (heightLight < 0) break;
opticalDepthLightR += exp(-heightLight / Hr) * segmentLengthLight;
opticalDepthLightM += exp(-heightLight / Hm) * segmentLengthLight;
tCurrentLight += segmentLengthLight;
}
if (j == numSamplesLight) {
Vec3f tau = betaR * (opticalDepthR + opticalDepthLightR) + betaM * 1.1f * (opticalDepthM + opticalDepthLightM);
Vec3f attenuation(exp(-tau.x), exp(-tau.y), exp(-tau.z));
sumR += attenuation * hr;
sumM += attenuation * hm;
}
tCurrent += segmentLength;
}
// We use a magic number here for the intensity of the sun (20). We will make it more
// scientific in a future revision of this lesson/code
return (sumR * betaR * phaseR + sumM * betaM * phaseM) * 20;
}
每个帧只需几秒钟的时间在2.5GHz处理器上计算(这个时间取决于您使用的视图和光线样本的数量)。该程序可以添加一些功能。一些论文在保存图像之前对结果图像进行色调映射。这有助于减小太阳周围天空的亮度(非常亮和白色)与天空其他部分之间的对比度。此外,您可以将生成的图像保存为浮点图像格式(如HDR、EXR),因为根据太阳光强度的不同,数值很容易大于一(重新映射和剪切数值并不是一个好选择)。此功能可以轻松添加到任何现有的渲染器中(有关此内容的更多详细信息,请参阅关于空气透视的段落)。
在图像中,当太阳位于地平线以上或略低于地平线时,天空的颜色会变成红橙色。您还可以评论米散射的贡献,以查看瑞利散射和米散射的贡献分别是什么。然而,通过观察生成的图像并记住您对这两种散射模型的了解,您很容易可以猜出,米散射是造成太阳周围的主要白色光晕以及地平线处天空明亮/去饱和的原因。相比之下,瑞利散射负责天空的蓝色/红色/橙色颜色。
一个有趣的项目可以包括将日期和时间传递给程序,然后基于这些数据计算太阳的位置,使用太阳的方向渲染一帧,并将结果与在同一天的相同时间拍摄的实际天空照片进行比较。它们会有多接近匹配呢?太阳的位置与它的赤纬和太阳时角(也就是一天中的时间)有关。
优化:Nishita观察到天空关于由太阳位置和地球中心定义的轴对称。基于这一观察结果,该论文提出了一种将光学深度嵌入2D表格的技术,您可以使用θ(视角和光线方向之间的余弦)和h(样本点的高度)进行访问。最好为每种散射模型(Rayleigh和Mie)创建一个表格。这些表格可以预先计算,保存到文件中,并在每次运行程序时重复使用。使用预先计算的光学深度值而不是即时计算(这需要一些昂贵的指数函数调用)将大大加快渲染速度。如果速度对您的应用程序很重要,这是您可以实施的第一个优化方法(有关详细信息,请参阅Nishita的论文)。
光束
图10:当场景中的物体遮挡体积的某些区域时,光束就会出现。在这个例子中,大气的某些区域被山脉遮挡。
大气效应有时可以产生壮观的视觉效果。光束通常会使这些效应更加戏剧化。如果光线可以自由地穿过体积,那么这个体积将呈均匀照明状态。但是,如果我们在场景中放置一些物体,那么被这些物体遮挡的体积区域将比未被遮挡的区域显得较暗。光束是指与光源(太阳)照亮的体积区域相对应的光束。但它们之所以可见,是因为场景中的物体(在地球大气中主要是山脉和云)使该体积的某些区域被遮挡。
模拟空气透视
计算空气透视不需要对我们的代码进行太多更改。正如您在下图中所看到的,山的颜色受到蓝色大气存在的影响。由于从眼睛到地面的距离对于射线A而言比射线B较短,所以大气的影响在射线A的颜色中不如射线B的颜色明显。
首先,您应该为射线渲染几何体(地形)的颜色。然后,您应该渲染透射率(也称为不透明度)和大气的颜色(使用眼睛位置和射线与几何体的交点来计算Pc和Pa),并使用以下阿尔法混合公式将这两种颜色合成在一起:
c++
Ray Color = Object Color * (1 - Transmittance) + Atmosphere Color
在本课程中,为了保持代码简洁,我们将不渲染地面颜色,其默认为黑色。如果您希望详细了解如何执行这种混合操作,请参阅关于体积渲染的课程。以下是我们在太阳的两个不同位置获得的一些结果。摄像机距离地球如此遥远,以至于地球在帧的下半部分可见。我们首先查找主射线是否与地球相交。如果相交,我们将计算从眼睛位置到该交点的大气颜色。
这些图像可以使用非常基本的光线追踪器计算。如果相机射线击中地球,我们必须在调用计算大气颜色的函数之前更新射线tmin和tmax变量(第36-39行)。
c++
void renderSkydome(const Vec3f& sunDir, const char *filename)
{
Atmosphere atmosphere(sunDir);
...
#if 1
// Render fisheye
...
#else
// Render from a normal camera
const unsigned width = 640, height = 480;
Vec3f *image = new Vec3f[width * height], *p = image;
memset(image, 0x0, sizeof(Vec3f) * width * height);
float aspectRatio = width / float(height);
float fov = 65;
float angle = std::tan(fov * M_PI / 180 * 0.5f);
unsigned numPixelSamples = 4;
Vec3f orig(0, atmosphere.earthRadius + 1000, 30000); //camera position
std::default_random_engine generator;
std::uniform_real_distribution<float> distribution(0, 1); //to generate random floats in the range [0:1]
for (unsigned y = 0; y < height; ++y) {
for (unsigned x = 0; x < width; ++x, ++p) {
for (unsigned m = 0; m < numPixelSamples; ++m) {
for (unsigned n = 0; n < numPixelSamples; ++n) {
float rayx = (2 * (x + (m + distribution(generator)) / numPixelSamples) / float(width) - 1) * aspectRatio * angle;
float rayy = (1 - (y + (n + distribution(generator)) / numPixelSamples) / float(height) * 2) * angle;
Vec3f dir(rayx, rayy, -1);
normalize(dir);
// Does the ray intersect the planetory body? (the intersection test is against the Earth here
// not against the atmosphere). If the ray intersects the Earth body and that the intersection
// is ahead of us, then the ray intersects the planet in 2 points, t0 and t1. But we
// only want to comupute the atmosphere between t=0 and t=t0 (where the ray hits
// the Earth first). If the viewing ray doesn't hit the Earth, or course, the ray
// is then bounded to the range [0:INF]. In the method computeIncidentLight() we then
// compute where this primary ray intersects the atmosphere, and we limit the max t range
// of the ray to the point where it leaves the atmosphere.
float t0, t1, tMax = kInfinity;
if (raySphereIntersect(orig, dir, atmosphere.earthRadius, t0, t1) && t1 > 0)
tMax = std::max(0.f, t0);
// The *viewing or camera ray* is bounded to the range [0:tMax]
*p += atmosphere.computeIncidentLight(orig, dir, 0, tMax);
}
}
*p *= 1.f / (numPixelSamples * numPixelSamples);
}
fprintf(stderr, "\b\b\b\b%3d%c", (int)(100 * y / (width - 1)), '%');
}
#endif
...
}
太空
通过改变大气模型的参数,可以轻松创建与地球天空完全不同的外观的天空。例如,可以重新创建火星大气或创造一个想象中的天空。改变其外观最明显的参数包括散射系数和大气的厚度。增加Mie散射的贡献很快就会使大气变得雾蒙蒙,结合光线照射,可以创建富有表现力和情感的图像。
多次散射
天空的颜色主要受到单次散射的影响,这涉及光线在大气中传播时被散射一次并进入观察者的眼睛。虽然大气中也会发生多次散射,但通常情况下它的影响较小,对天空整体外观的影响较小。因此,许多用于渲染天空图像的模型侧重于模拟单次散射,因为它已经提供了可信且视觉上逼真的结果。
在一些情况下,多次散射被忽略或未在文献中明确讨论。通常,单次散射足以捕捉天空外观的主要特征,而模拟多次散射可能显着增加计算复杂性,而视觉保真度的提高不成比例。
值得注意的是,某些模型考虑了地面反射的光对天空整体外观的贡献。在这种模型中,地球通常被视为完美的球形物体,并考虑了阳光、地面和大气之间的相互作用。这些模型可以提供对地球天空更全面的模拟,但通常需要更多的计算资源。
总结
本章需要记住的主要观点是,天空可以被渲染成一个巨大的球状体积(围绕着代表地球的另一个更大的球体)。吸收系数、散射系数和相函数可以定义任何体积(正如在体积渲染课程中学到的)。天空的外观是Rayleigh散射和Mie散射的组合。Rayleigh散射负责天空的蓝色(以及在日出和日落时的红橙色),它是由空气分子散射光线引起的,这些分子的大小远小于光的波长。空气分子更多地散射蓝光而不是绿光和红光。Mie散射则负责大气的朦胧白色外观,它是由大于光波长的分子(称为气溶胶)散射阳光引起的。气溶胶均匀地散射所有波长的光。
改进/练习的想法:
如果您想要改进/扩展代码,以下是一些想法:
-
使用程序纹理或MIP映射来渲染地球表面(陆地和海洋)。
-
向天空模型添加云层。
-
预先计算太阳与沿射线的任意点之间的光学深度(使用这种技术来加速程序 - 有关该方法的更多详细信息,请参考Nishita的论文)。
-
添加多次散射。
-
将此模型与其他天空模型进行比较(查看论文《用于实验比较太阳和天空照明的框架》)。
参考
Display of The Earth Taking into Account Atmospheric Scattering, Nishita et al., Siggraph 1993.
Display Method of the Sky Color Taking into Account Multiple Scattering, Nishita et al., Siggraph 1996.
Precomputed Atmospheric Scattering, Eric Bruneton, and Fabrice Neyret, Eurographics 2008.
A Practical Analytic Model for Daylight, A. J. Preetham, et al. Siggraph 1999.
A Framework for the Experimental Comparison of Solar and Skydive Illumination, Joseph T. Kider Jr et al., Cornell University 2014.