本案例学习资料来源
Volumetric Atmospheric Scattering
案例学习——Unity基于体绘制的大气散射shader
0 效果展示
先看看实现的效果(米氏散射和瑞利散射结合的地球大气+云层+地球昼夜)
1 引入
抬头望天,我们会发现天空经常是蓝色
日出日落时天空经常会是红色
这样的光学现象主要是光进行了瑞利散射(Rayleigh scattering),本文依照AlanZucconi大神的教程,介绍如何对大气散射进行数学建模,来重现这些大气中的视觉效果。
1.0 介绍
对于传统的渲染技术,会假设物体是一个空的外壳,我们在外壳表面即可进行所有效果的计算。这种简化方式对于渲染物体表面来说是很高效的。
但是有些效果是由于光穿透了物体才能实现的,比如半透明物体正是如此。不过聪明的人们也研究了一些快速近似半透明物体的方法(GDC 2011 – Approximating Translucency for a Fast, Cheap and Convincing Subsurface Scattering Look)。
但是大气就很难这么做了,因为天空并不是一个“物体”,我们不能只是渲染表面的壳,我们还需要模拟光线在大气中到底经历了什么。
不光光渲染壳,同时也要渲染物体内部的光线传播,这个技术通常被称为体积渲染/体绘制(volumetric rendering),在本文的上一篇文章做出过一些小的探讨(比如光线步进和有向距离函数)。
不过仅是这样还不足以模拟大气散射,接下来会介绍一种更合适的方法,被称为体积单次散射(volumetric single scattering)。
1.1 单次散射(Single Scattering)
在游戏引擎的基本光照模型实现中,都假设光线在真空中传播,所以物体的表面属性往往是影响光照的唯一因素。
然而在物理上,传播媒介对光的影响也是很重要的,对于大气来说,穿过的空气多少或者空气的成分就很能影响光照的效果。
在图形学进行大量的简化之后,空气影响我们感知光线的方式,通常会分为两种,分别是出射散射(Out-Scattering)和入射散射(In-Scattering)
1.1.1 出射散射(Out-Scattering)
出射散射也就是光子打中空气分子后,原本要射向摄像机的光线进行了偏转,光线的方向进行了改变。
对于一个大光源来说,能够发射出很多很多光子,因而在空气中传播时经常会发生出射散射。
空气密度越大,光线传播的距离越长,出射散射发生的光线偏转越多,对光线的削弱效果越明显。
1.1.2 入射散射
与出射散射相反,本来不指向相机的光线也可能被偏转到相机的方向,这就是入射散射。
入射散射允许相机看到不在可视范围内的光源,比较明显的例子就是光晕,比如下图由于Particle的偏转,会让人觉得沿着视线和Particle的连线处有光线,看起来也就是光晕。
1.2 基于体绘制的单次散射模型(Volumetric Single Scattering)
光线在实际传播当中会偏转很多次,就像下图这样
进行仿真是光线追踪的内容,对于本文来说,我们只考虑单次散射——即光线从太阳发出过、只经过一次散射改变方向后射入我们的眼睛。
对于渲染一个行星的大气,我们会逐步构建一个模型,首先我们考虑相机
- 摄像机的视线从A处进入大气,经过B处
- 我们计算光线从B到A的过程中,每一个位置P造成的出射散射的衰减贡献&入射散射的叠加贡献
该模型中也存在光源的贡献,我们通常假设是太阳的光线经过入射散射的偏转来到我们眼前
太阳在照射到P的过程中也存在出射散射
总结这些部分,混合在一起,也就是如下的样子
- 相机的视线从A进入,经过行星大气到B
- 我们需要去计算AB段上每一点P的散射贡献
- P点接受的光线都来自于太阳
- P点接受的光线进入大气层时会受到出射散射的影响
- P点接受的光线经过入射散射偏转进入摄像机,途中又会经过出射散射偏离
2 大气散射背后的理论
2.1 透射函数
为了计算传入相机的光线有多少,我们可以先考虑一段光线,如下图所示
- 光线从太阳传到C,此时是真空,所以没有损失,我们用 I C I_C IC指代C点的光强
- 在从C到P的过程中,经过出射散射,光强会衰减,我们用 I P I_P IP指代P点的光强
- I P I_P IP比 I C I_C IC的值也就是透射率(transmittance) 。
- 透射率 T T T表示在某段路径上的对光照的衰减程度。 T ( C P ‾ ) = I P I C T(\overline{CP})=\frac{I_P}{I_C} T(CP)=ICIP
- 对于某一个值的透射率 T T T,P点的光强也就是 I P = I C ∗ T ( C P ‾ ) I_P=I_C*T(\overline{CP}) IP=IC∗T(CP)
2.2 散射函数
P点在接受光照后,会因为散射偏转一部分
于是我们需要散射函数
S
S
S来表示某一方向上光线的偏转情况。
如下图,那些偏转了
θ
θ
θ角的光线才能被指向A点的摄像机
用
S
(
λ
,
θ
,
h
)
S(λ,θ,h)
S(λ,θ,h)的值来代表发生了
θ
θ
θ度偏转的光线比重。
λ λ λ代表光线的波长, θ θ θ是偏转角度, h h h是P点的离地高度(高度会影响大气密度,密度会影响散射程度)
于是我们可以得到从P点出发到A点的光强了
带入之前求出的
I
P
=
I
C
∗
T
(
C
P
‾
)
I_P=I_C*T(\overline{CP})
IP=IC∗T(CP)
整个过程是这样
- 光从太阳进入 C C C点,真空中无影响
- 从 C C C点进入大气传到 P P P点,受到出射散射影响只有占比 T ( C P ‾ ) T(\overline{CP}) T(CP)的光线到达了 P P P点
- P P P点的光线被入射散射偏转到摄像机中,偏转比率是 S ( λ , θ , h ) S(λ,θ,h) S(λ,θ,h)
- 偏转完成后,从 P P P到 A A A,受到出射散射影响只有占比 T ( P A ‾ ) T(\overline{PA}) T(PA)的光线到达了 A A A点
2.3 数值积分
I
P
A
I_{PA}
IPA是代表从
P
P
P点到
A
A
A点的光强,而
A
A
A点的所有光强是无数个
P
P
P点叠加起来的
I
A
I_A
IA是所有
P
∈
A
B
‾
P∈\overline{AB}
P∈AB的贡献之和
我们可以近似看成数值积分的形式把AB这一段加在一起
在大气shader中,我们遍历若干个大气的点
P
i
P_i
Pi累计贡献
2.4 方向光
因为太阳离我们很远,所以我们可以假设太阳光是方向光,所有在AB线上的点P接受的光都是来自同一个方向。
之前对于某一个
C
C
C点入射的光强我们定义是
I
C
I_C
IC,那么我们可以将所有
I
C
I_C
IC都看成是一个常量
I
S
I_S
IS
如果入射光的方向都相同,其实
S
(
λ
,
θ
,
h
)
S(λ,θ,h)
S(λ,θ,h)中的
θ
θ
θ也是一个常数,之后会进行讨论
2.5 吸收系数
光线和空气分子的相互作用除了偏转还有吸收,有些波长被吸收于是大气会呈现出某些独特的颜色。
我们在实现中会忽视吸收系数的影响,用其他的方式给大气调色。
3 瑞利散射中的数学
在之前的两节中,我们得到一个用于在shader中近似重现大气散射的框架公式,我们需要在框架的基础上更进一步。
光和物质的作用非常复杂,对于大气散射的建模是比较困难的,因此我们有许多特定的散射模型用于各种各样的情况
比如通过考虑 瑞利散射(Rayleigh Scattering) 和 米氏散射(Mie Scattering) ,我们可以重现行星大气的大多数效果。
瑞利散射用来进行小分子的建模,比如氧分子和氮分子。
- 小分子指大小远小于光线波长的粒子。这些分子大小比波长还要小很多,因此光的波长也会影响Rayleigh散射的程度。
米氏散射用来进行大小远大于光线波长的粒子的建模,比如花粉,灰尘。
- 大粒子在发生散射的时候会把更多的光散射到前向,因此我们认为Mie散射跟光线波长无关。
瑞利散射导致蓝天与红色的日出日落,米氏散射使得云彩变成白色。
我们需要深入了解一下它们。
3.1 瑞利散射概念
对于足够小的粒子,光线在撞击它后会经历什么?我们通常使用瑞利散射来建模。
其效果如图所示,一部分光线不受影响,一小部分散射到四面八方,蓝色的线条表示了每个方向的散射分布情况。
我们可以用瑞利散射的方程,也就是之前没解释的的散射函数 S ( λ , θ , h ) S(λ,θ,h) S(λ,θ,h)去描述这一现象
对于光线
I
0
I_0
I0,对于某方向
θ
θ
θ的散射比例为
其中的参数如下:
-
λ
λ
λ:入射光的波长
θ θ θ:散射角度
h h h:散射点的高度,为0时体现海平面附近的散射情况
n = 1.00029 n=1.00029 n=1.00029 代表空气折射率
N = 2.504 ∗ 1 0 25 N=2.504*10^{25} N=2.504∗1025 代表标准大气分子数的密度,单位是分子数/立方米
ρ ( h ) ρ(h) ρ(h):密度比,海平面时该值为1,以h为指数递减,后续会进行讨论 - 这并不是真正的瑞利散射方程,如果去百科上查,可能发现公式和这个是截然不同的
本文的公式来自于该论文Display of The Earth Taking into Account Atmospheric Scattering
可以配合文章里的一幅图理解一下上面的公式
- 这个结果怎么推导出来的?在此不多阐述
可以看看文章(反正拿了结果就好(x
那为什么会以蓝线这样的形状散射呢?
瑞利散射的实质并不是真的因为光和粒子发生了“碰撞”。
光是一种电磁波,它与某些粒子中本来就存在着的电荷不平衡相互作用。大气中的分子在入射光电矢量作用下会极化,从而产生偶极辐射。
对于瑞利散射,我们主要关注几个问题
一是它的散射方向,呈现两瓣的样子
二是散射的光强取决于入射光的波长
λ
λ
λ
下图展示了三个波长的
S
(
λ
,
θ
,
h
)
S(λ,θ,h)
S(λ,θ,h)(h=0)
下面这张图展示了可见光谱各个连续波长的
S
(
λ
,
θ
,
h
)
S(λ,θ,h)
S(λ,θ,h)渲染情况(黑色为不可见光),该效果在ShaderToy可以找到
3.1.1 瑞利散射系数
瑞利散射的公式 S ( λ , θ , h ) S(λ,θ,h) S(λ,θ,h) 表示在某个特定的方向上的有多少光
但是并没有说在这个位置上整体的能量情况,是少了还是不变,少了的话少了多少,所以我们需要算一下。
我们对瑞利散射的
S
(
λ
,
θ
,
h
)
S(λ,θ,h)
S(λ,θ,h) 做球面积分,得到总散射的能量
β
(
λ
,
h
)
β(λ,h)
β(λ,h)
最后得到
β
(
λ
,
h
)
β(λ,h)
β(λ,h)表示了与单个粒子碰撞后散射损失的能量,被称为瑞利散射系数(Rayleigh scattering coefficient.),也就是之前说的线段上的透射率T
计算
β
β
β的消耗比较大,出于这个原因,我们进行简化,使用海平面(h=0)的瑞利散射系数
下图展示了这个函数的对应关系
这个函数关系图也是夕阳红容易出现的原因,瑞利散射告诉我们,蓝色光更容易受到大气中分子的影响被散射掉,如果光在大气中走的路径越长,那么大部分蓝色光都会被散射掉,就像日落的时候,太阳在地平线,光线几乎平行地面过来,走了很长的路径。
同样的,天空呈现蓝色,也是因为蓝色光容易被散射,我们看天时,看见的就是被散射到各个方向的蓝色光。
3.1.2 瑞利散射相位函数
瑞利散射的原方程可以被分解为两个分量,一个是上面的有关能量强度的散射系数,另一个分量则和它几何形状有关
γ
(
θ
)
γ(θ)
γ(θ)可以用前两者之比得到
该表达式不依赖于波长,
γ
(
θ
)
γ(θ)
γ(θ)构造的是之前两瓣的偶极子的形状。
之后会对瑞利散射的这两个分量继续分离,让方程更有效。
3.2 回顾
做一个小的总结
首先,我们从论文中了解了瑞利散射方程,它也能写出系数与相位函数相乘的形式
瑞利散射系数表示的是一次碰撞后散射的能量占总能量有多少
海平面上的瑞利散射系数指的是把
h
h
h取0时的
β
β
β值
举例来说,对于红绿蓝的波长,可以得到这些结果
瑞利散射相位函数,控制散射的几何形状
4 大气密度
本节介绍如何对不同高度的大气密度进行建模。
4.1 大气密度比
对于大气密度比 ρ ρ ρ ,通常认为散射强度和大气密度大致成正比关系,更高的密度意味着更多的散射。
大气的组成是比较复杂的,瑞利散射通常发生在60KM内的大气层,下图展示了这些底层大气中密度和高度的关系
ρ
(
h
)
ρ(h)
ρ(h)表示的是当前高度大气和海平面大气的密度比
正如之前提到过,计算高为
h
h
h时的大气密度是消耗比较大的,我们会选择利用一个指数曲线去近似模拟
H
H
H作为参考量,被称为标准高度,对于瑞利散射来说,我们认为
H
=
8500
M
H=8500M
H=8500M,近似的效果如下图所示
4.2 指数衰减
假设一束光具有初始强度
I
0
I_0
I0,那么穿过散射系数为
β
β
β的大气,剩下没有被散射的光量就是
如果经过X的距离,怎么算剩下的光量呢?直接乘以距离的话,
β
β
β就不变了,我们可以用无穷来近似。
我们知道
I
1
=
I
0
(
1
−
β
)
I_1=I_0(1-β)
I1=I0(1−β)
这个
(
1
−
β
)
(1-β)
(1−β)可以当做是一个粗略的估计值,我们为了便于推导,将其近似写做两个
(
1
−
β
2
)
(1-\frac{β}{2})
(1−2β)相乘的形式
当近似拆解成很多次时
用等价无穷小可以得到
所以结果就是,经过了x距离后,剩余的光量为
4.3 均匀密度的透射率
在以上概念介绍后,我们拿原本的模型计算一下
C
C
C点是光线入射大气的点,光强为
I
S
I_S
IS,到达
P
P
P点后光强变为
I
P
I_P
IP,假设先不管密度
我们得到
4.4 大气密度的透射率
散射系数 β β β依赖于大气密度,空气分子越多,越会影响光线。
然而大气密度随着高度变化,我们也就无法一步算出,于是我们计算每个点进行累加近似。
便于推导,首先将CP划分为CQ和QP两部分
C到Q,得到
I
Q
I_Q
IQ
Q到P,得到
I
P
I_P
IP
若CQ和QP线段相等,都作为
d
s
ds
ds
如果把CP分成无数段,那么就可以累加起来数值积分
如果假设初始光量是1,那么大气透射率的公式就有了
带入瑞利散射的
β
β
β
把常数提取出来
后面的累加结果
D
(
C
P
‾
)
D(\overline{CP})
D(CP)也被叫做光学深度(optical depth ),这也是我们要在shader里去算的东西。
前面部分的常数系数,也就是海平面附近的散射系数,我们算一次就好。
总的来说,我们在shader中只要计算光学深度,然后海平面的散射系数常量
β
β
β作为输入使用就好
5 大气球体shader框架思路
注:该节的示例代码,不是直接使用的,只是示范思路用
5.1 双Pass
我们渲染一颗行星上的大气,所以假定这个shader作用的是一个球体
下图是NASA拍的一副大气图
我们使用surf shader,分为两个pass去渲染,一个pass用来渲染星球,一个pass用一个比原来稍大的球体来渲染大气
一个类似的结构如下
Shader "Custom/NewSurfaceShader" {
Properties {
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
}
SubShader {
// --- First pass ---
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
// Cg code here
ENDCG
// ------------------
// --- Second pass ---
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
// Cg code here
ENDCG
// -------------------
}
FallBack "Diffuse"
}
5.2 法线挤出
在第二个pass中,我们需要渲染一个稍大的球,可以采用法线挤出来操作
一个类似的操作如下,挤出操作取决于一个值——大气的大小和行星的大小
#pragma surface surf StandardScattering vertex:vert
void vert (inout appdata_full v, out Input o)
{
UNITY_INITIALIZE_OUTPUT(Input,o);
v.vertex.xyz += v.normal * (_AtmosphereRadius - _PlanetRadius);
}
我们也可以拿到球体的中心,也就是unity的世界变化矩阵最后一列是存的Transform里的Position
struct Input
{
float2 uv_MainTex;
float3 worldPos; // Initialised automatically by Unity
float3 centre; // Initialised in the vertex function
};
void vert (inout appdata_full v, out Input o)
{
UNITY_INITIALIZE_OUTPUT(Input,o);
v.vertex.xyz += v.normal * (_AtmosphereRadius - _PlanetRadius);
o.centre = mul(unity_ObjectToWorld, half4(0,0,0,1));
}
5.3 加法混合
第二个pass采用加法混合的形式处理大气的透明度
Tags { "RenderType"="Transparent"
"Queue"="Transparent"}
LOD 200
Cull Back
Blend One One
5.4 自定义光照函数
我们不修改surf函数,我们替换原本的光照模型
比如我们建立一个LightingStandardScattering的光照模型,分别写实时光和全局光,在实时光中进行…的计算
#pragma surface surf StandardScattering vertex:vert
#include "UnityPBSLighting.cginc"
inline fixed4 LightingStandardScattering(SurfaceOutputStandard s, fixed3 viewDir, UnityGI gi)
{
float3 L = gi.light.dir;
float3 V = viewDir;
float3 N = s.Normal;
float3 S = L; // Direction of light from the sun
float3 D = -V; // Direction of view ray piercing the atmosphere
...
}
void LightingStandardScattering_GI(SurfaceOutputStandard s, UnityGIInput data, inout UnityGI gi)
{
LightingStandard_GI(s, data, gi);
}
5.5 精度
如果是完全模拟,那么地球半径就要6371000米,显然我们是不能直接这么创建球体的,所以我们要调整系数。
比如简单的说,如果星球大小是6.371米,那么散射系数
β
β
β就要变成原来的1000000倍大,标准高度缩小1000000倍。也可以用其他方式修改,只要比例上对,方便显示就可。
6 相交
6.1 光线与大气相交
之前公式中,我们的思路是利用数值积分计算大气中某一段 d s ds ds线段的的光学深度,然后假设这几个小段的密度不变,然后计算每个区域的光学深度
比如在计算下图的线段AB的光学深度时,采用了段数为4段进行计算,每个采样仅考虑中点的密度。
于是,开始计算的第一步是找到A点和B点。
在用表面着色器渲染我们的球体时,每个屏幕上的像素都会对应球面上一点,也就是下图的点O,也就是Input里的worldPos。
我们还会需要视线方向D,以及半径为R的大气球体它的中心C。
也就是(上划线代表长度)
O
T
‾
\overline{OT}
OT可以用点积计算,
(
C
−
O
)
(C-O)
(C−O)代表三维向量而不是距离
总结在一起列一下,也就是我们需要这些条件
写成函数的话,类似如下,可以用来判断相交,输出bool和AO以及BO的数据
bool rayIntersect
(
// Ray
float3 O, // Origin
float3 D, // Direction
// Sphere
float3 C, // Centre
float R, // Radius
out float AO, // First intersection time
out float BO // Second intersection time
)
{
float3 L = C - O;
float DT = dot (L, D);
float R2 = R * R;
float CT2 = dot(L,L) - DT*DT;
// Intersection point outside the circle
if (CT2 > R2)
return false;
float AT = sqrt(R2 - CT2);
float BT = AT;
AO = DT - AT;
BO = DT + BT;
return true;
}
6.2 光线与行星相交
视线也会因为行星的阻挡而停止,我们可以运行两次相交函数,一次判断大气,一次判断行星
// Intersections with the atmospheric sphere
float tA; // Atmosphere entry point (worldPos + V * tA)
float tB; // Atmosphere exit point (worldPos + V * tB)
if (!rayIntersect(O, D, _PlanetCentre, _AtmosphereRadius, tA, tB))
return fixed4(0,0,0,0); // The view rays is looking into deep space
// Is the ray passing through the planet core?
float pA, pB;
if (rayIntersect(O, D, _PlanetCentre, _PlanetRadius, pA, pB))
tB = pA;
7 大气散射shader
7.1 采样视线
我们之前得到的公式是这样
光线总量等于太阳光乘以线段AB上每一个点P的贡献之和
为了简化函数,我们先把散射函数S分成两个基本构成
相位函数
γ
(
θ
)
γ(θ)
γ(θ)和海平面的散射函数
β
(
λ
)
β(λ)
β(λ)是常量
,因此我们可以得到
线段上有无数个P点,为了近似求解
I
I
I,我们需要把线段划分为数个长度为
d
s
ds
ds的小段
AB被划分的段数就是视线采样数(view samples),在shader中可以类似这么做来遍历所有分段
// Numerical integration to calculate
// the light contribution of each point P in AB
float3 totalViewSamples = 0;
float time = tA;
float ds = (tB-tA) / (float)(_ViewSamples);
for (int i = 0; i < _ViewSamples; i ++)
{
// Point position
// (sampling in the middle of the view sample segment)
float3 P = O + D * (time + ds * 0.5);
// T(CP) * T(PA) * ρ(h) * ds
totalViewSamples += viewSampling(P, ds);
time += ds;
}
// I = I_S * β(λ) * γ(θ) * totalViewSamples
float3 I = _SunIntensity * _ScatteringCoefficient * phase * totalViewSamples;
7.2 光学深度PA
线段AB上的每一点都会对最终效果有所贡献,也就是求和符号内部的东西
首先我们看透射率T的定义
所以
指数里是两段光学深度之和乘以海平面的散射系数,于是回顾一下光学深度D的定义
我们在之前代码的循环中累加计算光学深度
D
(
P
A
)
D(PA)
D(PA)
// Accumulator for the optical depth
float opticalDepthPA = 0;
// Numerical integration to calculate
// the light contribution of each point P in AB
float time = tA;
float ds = (tB-tA) / (float)(_ViewSamples);
for (int i = 0; i < _ViewSamples; i ++)
{
// Point position
// (sampling in the middle of the view sample segment)
float3 P = O + D * (time + viewSampleSize*0.5);
// Optical depth of current segment
// ρ(h) * ds
float height = distance(C, P) - _PlanetRadius;
float opticalDepthSegment = exp(-height / _ScaleHeight) * ds;
// Accumulates the optical depths
// D(PA)
opticalDepthPA += opticalDepthSegment;
...
time += ds;
}
7.3 光线采样
累加符号里P点的光量贡献还剩下CP的光学深度
我们在求CP的光学深度时,建立一个lightSampling方法,也就从P点出发指向太阳进行采样,把太阳的出射点叫做C
当然其中也会碰到星球,所以要忽视
C
0
C_0
C0的贡献
所以我们会检查该点是否有效
下文代码中,用rayInstersect计算了C,把PA划分成_LightSamples段,每段Ds
bool lightSampling
( float3 P, // Current point within the atmospheric sphere
float3 S, // Direction towards the sun
out float opticalDepthCA
)
{
float _; // don't care about this one
float C;
rayInstersect(P, S, _PlanetCentre, _AtmosphereRadius, _, C);
// Samples on the segment PC
float time = 0;
float ds = distance(P, P + S * C) / (float)(_LightSamples);
for (int i = 0; i < _LightSamples; i ++)
{
float3 Q = P + S * (time + lightSampleSize*0.5);
float height = distance(_PlanetCentre, Q) - _PlanetRadius;
// Inside the planet
if (height < 0)
return false;
// Optical depth for the light ray
opticalDepthCA += exp(-height / _RayScaleHeight) * ds;
time += ds;
}
return true;
}
利用上面的函数,我们会在主函数中检测是否击中了星球
// D(CP)
float opticalDepthCP = 0;
bool overground = lightSampling(P, S);
if (overground)
{
// Combined transmittance
// T(CP) * T(PA) = T(CPA) = exp{ -β(λ) [D(CP) + D(PA)]}
float transmittance = exp
(
-_ScatteringCoefficient *
(opticalDepthCP + opticalDepthPA)
);
// Light contribution
// T(CPA) * ρ(h) * ds
totalViewSamples += transmittance * opticalDepthSegment;
}
8 米氏散射
光线的散射是很复杂的,瑞利散射对原子和分子可以有效模拟,但是对光线和大物体(气溶胶)的交互没什么作用。
地球大气布满了气溶胶,所以仅是瑞利散射是不足以复现的,为此常用的就是米氏散射(Mie scattering),这种散射类型倾向于扩散光线,让光源看起来比实际大,但不会改变光的颜色。
8.1 米氏散射系数
上一节我们定义了瑞利散射的散射系数,但是米氏散射不能这么做。
米氏散射是麦克斯韦方程组的一个解,麦克斯韦方程组描述了电磁波的表现。
事实上,我们无法用一个简单的方程来计算米氏散射系数 β m β_m βm,最准确的方法是无穷级数近似,所以米氏散射是只能近似计算的。
如果对准确性没这么大要求,那么有一个相似的方程来模拟米氏散射,这个和描述瑞利散射的方程很像
和瑞利散射比,也就是少了
λ
λ
λ
当然,本文里我们对地球的假设就是
β
M
=
0.0021
β_M=0.0021
βM=0.0021
8.2 米氏散射相位函数
相位函数和系数的理论差不多,无法直接求
然而幸运的是我们又可以近似了,我们可以利用Henyey-Greenstein 函数,该函数在1941年提出
在1992年,Cornette对其进行了改进,我们会使用这个改进版
该函数引入了一个参数
g
(
−
1
<
g
<
1
)
g(-1<g<1)
g(−1<g<1),决定了前后散射的相对强度,代表散射方向的平均余弦。正的
g
g
g值会增加正向散射的光量,负的
g
g
g值表示向后散射的光量。
g
=
0
g=0
g=0的值导致各向同性散射。
g
g
g值的目的是控制米氏散射的各向异性程度,也就是散射几何形状的不对称性质。
对于地球的大气层,一般用-0.758,实际的值取决于大气中气溶胶的密度和大小,所以根据地区不同它也不同。比如污染的城市就会比乡下有更多的米氏散射。
Cornette和Henyey-Greenstein函数的计算成本都很高。成本的很大一部分归因于分母的指数。
对于需要快速应用,不需要确切形状的的时候,也会使用近似值,也就是Schlick相位函数
8.3 组合
在之前的七节,我们得到这么个方程来控制大气单次散射
我们把瑞利散射和米氏散射拟合到单个方程里,在计算时每个模型计算一遍,然后加起来
在代码中也就是从
// I = I_S * β(λ) * γ(θ) * totalViewSamples
float3 I = _SunIntensity * _ScatteringCoefficient * phase * totalViewSamples;
变成
// I = I_S [βr(λ) * γr(θ) * totalRayViewSamples + βm(λ) * γm(θ) * totalMieViewSamples)
float3 I = _SunIntensity *
(
_RayScatteringCoefficient * rayPhase * totalRayViewSamples +
_MieScatteringCoefficient * miePhase * totalMieViewSamples
);
8.4 外星球
对于其他的星球,调参数都可以实现
比如天王星和海王星上有足够的甲烷,甲烷能够吸收红光,于是穿过甲烷的光线会通常呈现蓝绿色,天王星和海王星也就成为蓝绿色了。
9 实现上的总结
并不会直接给出整个shader文件,主要阐述思路
对与Properties上,划分出几个部分
- Standard Shader部分:星球的贴图,包括基础颜色,粗糙度贴图,粗糙值,法线贴图
- Planet部分:云层贴图,alpha值,速度,是否启动;夜晚贴图(指城市的范围),夜晚颜色,强度
- Scattering部分:
大气的比例调整系数,散射的比例调整系数,大气颜色
球体半径单位(比如1m代表行星多少km)
行星半径,大气高度
瑞利散射系数,高度
米氏散射系数,g值,高度 - 其他的比如阳光强度,视线和光线采样数
在第一个pass中,是Opaque
- 采样行星的tex,采样云朵的tex(给云朵来个time做动画)
混合云朵和行星的颜色
然后正常的采样法线和粗糙度图alpha值
在显示夜间的时候,采用法线和视线的光线作为切换依据,做个反转再saturate作为强度值,把夜间贴图和颜色叠加到自发光项里,表示城市的发光(发光在白天有一些,让夜晚全都有)
接下来是散射的pass
- vert着色器进行放大球体,获得大气层
因为显示的单位和真实的米数是不一样的,所以需要球体半径除以行星半径得到换算比例
然后进行法线挤出,此处可留一个乘数来自己调节 - 自定义一个判断光线与大气相交的函数rayInstersect,前文有提
- 自定义一个lightSampling方法,里面分成瑞利散射和米氏散射的两个部分
- 自定义光照模型和GI,GI不用改,
光照模型导入UnityPBSLighting.cginc后,自己建立一个方法LightingStandardScattering
内部进行视线采样,依然是分成瑞利散射和米氏散射的两个部分,根据公式合并起来即可 - surf函数里正常采样贴图,传一传各种参数
总结来说就是使用表面着色器,双pass渲染,第一个pass渲染行星昼夜云朵,第二个pass修改光照模型根据公式推导渲染大气。