本案例学习资料来源
Volumetric Atmospheric Scattering

案例学习——Unity基于体绘制的大气散射shader

  • 0 效果展示
  • 1 引入
    • 1.0 介绍
    • 1.1 单次散射(Single Scattering)
      • 1.1.1 出射散射(Out-Scattering)
      • 1.1.2 入射散射
    • 1.2 基于体绘制的单次散射模型(Volumetric Single Scattering)
  • 2 大气散射背后的理论
    • 2.1 透射函数
    • 2.2 散射函数
    • 2.3 数值积分
    • 2.4 方向光
    • 2.5 吸收系数
  • 3 瑞利散射中的数学
    • 3.1 瑞利散射概念
      • 3.1.1 瑞利散射系数
      • 3.1.2 瑞利散射相位函数
    • 3.2 回顾
  • 4 大气密度
    • 4.1 大气密度比
    • 4.2 指数衰减
    • 4.3 均匀密度的透射率
    • 4.4 大气密度的透射率
  • 5 大气球体shader框架思路
    • 5.1 双Pass
    • 5.2 法线挤出
    • 5.3 加法混合
    • 5.4 自定义光照函数
    • 5.5 精度
  • 6 相交
    • 6.1 光线与大气相交
    • 6.2 光线与行星相交
  • 7 大气散射shader
    • 7.1 采样视线
    • 7.2 光学深度PA
    • 7.3 光线采样
  • 8 米氏散射
    • 8.1 米氏散射系数
    • 8.2 米氏散射相位函数
    • 8.3 组合
    • 8.4 外星球
  • 9 实现上的总结

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,此时是真空,所以没有损失,我们用ICI_CIC​指代C点的光强
  • 在从C到P的过程中,经过出射散射,光强会衰减,我们用IPI_PIP​指代P点的光强
  • IPI_PIP​比ICI_CIC​的值也就是透射率(transmittance)
  • 透射率TTT表示在某段路径上的对光照的衰减程度。T(CP‾)=IPICT(\overline{CP})=\frac{I_P}{I_C}T(CP)=IC​IP​​
  • 对于某一个值的透射率TTT,P点的光强也就是IP=IC∗T(CP‾)I_P=I_C*T(\overline{CP})IP​=IC​∗T(CP)

2.2 散射函数

P点在接受光照后,会因为散射偏转一部分
于是我们需要散射函数SSS来表示某一方向上光线的偏转情况。

如下图,那些偏转了θθθ角的光线才能被指向A点的摄像机

用S(λ,θ,h)S(λ,θ,h)S(λ,θ,h)的值来代表发生了θθθ度偏转的光线比重。

λλλ代表光线的波长,θθθ是偏转角度,hhh是P点的离地高度(高度会影响大气密度,密度会影响散射程度)

于是我们可以得到从P点出发到A点的光强了

带入之前求出的IP=IC∗T(CP‾)I_P=I_C*T(\overline{CP})IP​=IC​∗T(CP)

整个过程是这样

  • 光从太阳进入CCC点,真空中无影响
  • 从CCC点进入大气传到PPP点,受到出射散射影响只有占比T(CP‾)T(\overline{CP})T(CP)的光线到达了PPP点
  • PPP点的光线被入射散射偏转到摄像机中,偏转比率是S(λ,θ,h)S(λ,θ,h)S(λ,θ,h)
  • 偏转完成后,从PPP到AAA,受到出射散射影响只有占比T(PA‾)T(\overline{PA})T(PA)的光线到达了AAA点

2.3 数值积分

IPAI_{PA}IPA​是代表从PPP点到AAA点的光强,而AAA点的所有光强是无数个PPP点叠加起来的
IAI_AIA​是所有P∈AB‾P∈\overline{AB}P∈AB的贡献之和

我们可以近似看成数值积分的形式把AB这一段加在一起

在大气shader中,我们遍历若干个大气的点PiP_iPi​累计贡献

2.4 方向光

因为太阳离我们很远,所以我们可以假设太阳光是方向光,所有在AB线上的点P接受的光都是来自同一个方向。

之前对于某一个CCC点入射的光强我们定义是ICI_CIC​,那么我们可以将所有ICI_CIC​都看成是一个常量ISI_SIS​

如果入射光的方向都相同,其实S(λ,θ,h)S(λ,θ,h)S(λ,θ,h)中的θθθ也是一个常数,之后会进行讨论

2.5 吸收系数

光线和空气分子的相互作用除了偏转还有吸收,有些波长被吸收于是大气会呈现出某些独特的颜色。
我们在实现中会忽视吸收系数的影响,用其他的方式给大气调色。

3 瑞利散射中的数学

在之前的两节中,我们得到一个用于在shader中近似重现大气散射的框架公式,我们需要在框架的基础上更进一步。

光和物质的作用非常复杂,对于大气散射的建模是比较困难的,因此我们有许多特定的散射模型用于各种各样的情况

比如通过考虑 瑞利散射(Rayleigh Scattering)米氏散射(Mie Scattering) ,我们可以重现行星大气的大多数效果。

瑞利散射用来进行小分子的建模,比如氧分子和氮分子。

  • 小分子指大小远小于光线波长的粒子。这些分子大小比波长还要小很多,因此光的波长也会影响Rayleigh散射的程度。

米氏散射用来进行大小远大于光线波长的粒子的建模,比如花粉,灰尘。

  • 大粒子在发生散射的时候会把更多的光散射到前向,因此我们认为Mie散射跟光线波长无关。


瑞利散射导致蓝天与红色的日出日落,米氏散射使得云彩变成白色。

我们需要深入了解一下它们。

3.1 瑞利散射概念

对于足够小的粒子,光线在撞击它后会经历什么?我们通常使用瑞利散射来建模。

其效果如图所示,一部分光线不受影响,一小部分散射到四面八方,蓝色的线条表示了每个方向的散射分布情况。

我们可以用瑞利散射的方程,也就是之前没解释的的散射函数S(λ,θ,h)S(λ,θ,h)S(λ,θ,h)去描述这一现象

对于光线I0I_0I0​,对于某方向θθθ的散射比例为

其中的参数如下:

  • λλλ:入射光的波长
    θθθ:散射角度
    hhh:散射点的高度,为0时体现海平面附近的散射情况
    n=1.00029n=1.00029n=1.00029 代表空气折射率
    N=2.504∗1025N=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 回顾

做一个小的总结

首先,我们从论文中了解了瑞利散射方程,它也能写出系数与相位函数相乘的形式

瑞利散射系数表示的是一次碰撞后散射的能量占总能量有多少

海平面上的瑞利散射系数指的是把hhh取0时的βββ值

举例来说,对于红绿蓝的波长,可以得到这些结果

瑞利散射相位函数,控制散射的几何形状

4 大气密度

本节介绍如何对不同高度的大气密度进行建模。

4.1 大气密度比

对于大气密度比ρρρ ,通常认为散射强度和大气密度大致成正比关系,更高的密度意味着更多的散射。

大气的组成是比较复杂的,瑞利散射通常发生在60KM内的大气层,下图展示了这些底层大气中密度和高度的关系

ρ(h)ρ(h)ρ(h)表示的是当前高度大气和海平面大气的密度比

正如之前提到过,计算高为hhh时的大气密度是消耗比较大的,我们会选择利用一个指数曲线去近似模拟

HHH作为参考量,被称为标准高度,对于瑞利散射来说,我们认为H=8500MH=8500MH=8500M,近似的效果如下图所示

4.2 指数衰减

假设一束光具有初始强度I0I_0I0​,那么穿过散射系数为βββ的大气,剩下没有被散射的光量就是

如果经过X的距离,怎么算剩下的光量呢?直接乘以距离的话,βββ就不变了,我们可以用无穷来近似。
我们知道I1=I0(1−β)I_1=I_0(1-β)I1​=I0​(1−β)
这个(1−β)(1-β)(1−β)可以当做是一个粗略的估计值,我们为了便于推导,将其近似写做两个(1−β2)(1-\frac{β}{2})(1−2β​)相乘的形式


当近似拆解成很多次时

用等价无穷小可以得到

所以结果就是,经过了x距离后,剩余的光量为

4.3 均匀密度的透射率

在以上概念介绍后,我们拿原本的模型计算一下
CCC点是光线入射大气的点,光强为ISI_SIS​,到达PPP点后光强变为IPI_PIP​,假设先不管密度

我们得到

4.4 大气密度的透射率

散射系数βββ依赖于大气密度,空气分子越多,越会影响光线。

然而大气密度随着高度变化,我们也就无法一步算出,于是我们计算每个点进行累加近似。

便于推导,首先将CP划分为CQ和QP两部分

C到Q,得到IQI_QIQ​

Q到P,得到IPI_PIP​


若CQ和QP线段相等,都作为dsdsds

如果把CP分成无数段,那么就可以累加起来数值积分

如果假设初始光量是1,那么大气透射率的公式就有了

带入瑞利散射的βββ

把常数提取出来

后面的累加结果D(CP‾)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 200CGPROGRAM// Cg code hereENDCG// ------------------// --- Second pass ---Tags { "RenderType"="Opaque" }LOD 200CGPROGRAM// Cg code hereENDCG// -------------------}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 Unityfloat3 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 sunfloat3 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 光线与大气相交

之前公式中,我们的思路是利用数值积分计算大气中某一段dsdsds线段的的光学深度,然后假设这几个小段的密度不变,然后计算每个区域的光学深度

比如在计算下图的线段AB的光学深度时,采用了段数为4段进行计算,每个采样仅考虑中点的密度。

于是,开始计算的第一步是找到A点和B点。
在用表面着色器渲染我们的球体时,每个屏幕上的像素都会对应球面上一点,也就是下图的点O,也就是Input里的worldPos。
我们还会需要视线方向D,以及半径为R的大气球体它的中心C。

也就是(上划线代表长度)


OT‾\overline{OT}OT可以用点积计算,(C−O)(C-O)(C−O)代表三维向量而不是距离


总结在一起列一下,也就是我们需要这些条件

写成函数的话,类似如下,可以用来判断相交,输出bool和AO以及BO的数据

bool rayIntersect
(// Rayfloat3 O, // Originfloat3 D, // Direction// Spherefloat3 C, // Centrefloat R,    // Radiusout float AO, // First intersection timeout 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 circleif (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点,为了近似求解III,我们需要把线段划分为数个长度为dsdsds的小段

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) * dstotalViewSamples += viewSampling(P, ds);time += ds;
}// I = I_S * β(λ) * γ(θ) * totalViewSamples
float3 I = _SunIntensity *  _ScatteringCoefficient * phase * totalViewSamples;

7.2 光学深度PA

线段AB上的每一点都会对最终效果有所贡献,也就是求和符号内部的东西

首先我们看透射率T的定义

所以

指数里是两段光学深度之和乘以海平面的散射系数,于是回顾一下光学深度D的定义

我们在之前代码的循环中累加计算光学深度D(PA)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) * dsfloat 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

当然其中也会碰到星球,所以要忽视C0C_0C0​的贡献
所以我们会检查该点是否有效

下文代码中,用rayInstersect计算了C,把PA划分成_LightSamples段,每段Ds

bool lightSampling
(    float3 P,    // Current point within the atmospheric spherefloat3 S,    // Direction towards the sunout float opticalDepthCA
)
{float _; // don't care about this onefloat C;rayInstersect(P, S, _PlanetCentre, _AtmosphereRadius, _, C);// Samples on the segment PCfloat 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 planetif (height < 0)return false;// Optical depth for the light rayopticalDepthCA += 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) * dstotalViewSamples += 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),决定了前后散射的相对强度,代表散射方向的平均余弦。正的ggg值会增加正向散射的光量,负的ggg值表示向后散射的光量。 g=0g=0g=0的值导致各向同性散射。

ggg值的目的是控制米氏散射的各向异性程度,也就是散射几何形状的不对称性质。

对于地球的大气层,一般用-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修改光照模型根据公式推导渲染大气。

案例学习——Unity基于体绘制的大气散射shader相关推荐

  1. 新手入门案例学习,基于C# MVC实现汽修管理系统《建议收藏:附完整源码+数据库》

    文章目录 一.登陆系统 (1)需用用户填写的登陆信息: (2)登陆操作进行了严格的判断: a:当用户没有输入任何信息就直接点击"登陆"进行登陆的时候,会根据输入框的顺序依次进行判断 ...

  2. 次表面散射材质_游戏开发者怎么做出以假乱真的画面效果?大气散射渲染了解一下...

    编者按 游戏渲染做得好不好,看看天空就知道了.本文作者Bence将和大家分享游戏中基于物理的大气散射渲染,聊聊如何做出更好看的天空.雾气等等. 文 | Bence 腾讯互动娱乐 游戏客户端开发 参与介 ...

  3. 【Unity大气渲染】关于单次大气散射的理论知识

    参考 最近在实现程序化天空盒,到了实现大气散射这一步,索性查漏补缺,把大气散射这块儿的理论知识补充明白了.跟着[实战]从零实现一套完整单次大气散射_一的推荐,学习这块我直接从Volumetric At ...

  4. 【Unity大气渲染】Unity Shader中实现大气散射(半成品)

    写在前面 这是之前在做天空盒的时候同步写的分析博客,结果后面写到一半就忘了继续了,这里先贴出当时写的半成品,有小伙伴问我怎么做的,这里只能尽力把之前的半成品先放出来了(写得很乱,勿怪orz),,后面有 ...

  5. 【Unity大气散射】GAMES104:3A中如何实现大气散射

    写在前面 前两天学习并整理的大气散射基础知识:[Unity大气渲染]关于单次大气散射的理论知识,收获了很多,但不得不承认的是,这其实已经是最早的.90年代的非常古老的方法了,后来也出现了一些优化性的计 ...

  6. MATLAB案例学习-基于雷达和摄像头的传感器融合

    文章目录 前言 生成场景 定义雷达和摄像头传感器 生成跟踪器 仿真场景 总结 帮助函数 前言 本文主要参考MATLAB自动驾驶工具箱的案例<Sensor Fusion Using Synthet ...

  7. 基于GPU预计算的大气散射

    本文转载自:https://blog.csdn.net/qq_31615919/article/details/85938076 基于GPU预计算的大气层光效渲染 前言 大气物理模型 渲染方程及其实现 ...

  8. DBMS/Database:数据库管理的简介、安装(注意事项等)、学习路线(基于SQLSever深入理解SQL命令语句综合篇《初级→中级→高级》/几十项代码案例集合)之详细攻略

    DBMS/Database:数据库管理的简介.安装(注意事项等).学习路线(基于SQLSever深入理解SQL命令语句综合篇<初级→中级→高级>/几十项代码案例集合)之详细攻略 目录 DB ...

  9. 《深度学习案例精粹:基于TensorFlow与Keras》深度学习常用训练案例合集

    #好书推荐##好书奇遇季#<深度学习案例精粹:基于TensorFlow与Keras>,京东当当天猫都有发售.本书配套示例源码.PPT课件.思维导图.数据集.开发环境与答疑服务. <深 ...

最新文章

  1. js json编辑器_程序员必备编辑器 —— Visual Studio Code
  2. [Android]为指定的应用创建桌面快捷方式
  3. Docker镜像加速器配置
  4. 雅思作文未来计算机的应用,9分考官级雅思大作文范文之电脑技术的忧虑
  5. php分布式cache,thinkPHP实现MemCache分布式缓存功能
  6. 导出Android手机应用apk
  7. python基本数据类型(四)-集合与运算符-python3笔记
  8. NuGet.org服务管理变更,提升中国用户体验
  9. web.config文件之自定义错误节
  10. mysql数据库enum部分乱码_要慎用mysql的enum字段的原因
  11. C语言实现行列式和代数余子式
  12. Memcached源码分析之memcached.h
  13. svm多分类python代码_SVM算法的理解及其Python实现多分类和二分类
  14. 21天学会Java之(Java SE第十二篇):多线程、Lambda表达式
  15. ミルシャ / 人鱼枪
  16. 10000个怎么用js写 创建li_给博客园加一个会动的小人-spig.js
  17. 为什么说用PHP开发大型系统令人不爽
  18. Linux群组与文件权限
  19. 135.如何进行离线计算-1
  20. 提升GraphicsMagick图片压缩软件性能使用心得

热门文章

  1. 2018最新版网易云音乐导入歌单
  2. 用python画福字
  3. 广电总局:每集电视剧不得以任何形式插播广告
  4. Beaglebone 中 U-Boot 的启动过程
  5. macbook pro 卸载应用
  6. linux下tar xvzf是解压tar.gz的,等效gzip -d file.gz+tar vxf file.tar,以及别的解压方式大全
  7. 基于RGB的偏色检测(实用)
  8. 办公技巧:局域网内设置固定ip
  9. java算法递归之小白走楼梯
  10. 基于STM32的倾斜仪设计(一)—— 硬件设计(1)