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

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

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=ICT(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=ICT(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} PAB的贡献之和
在这里插入图片描述
我们可以近似看成数值积分的形式把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.5041025 代表标准大气分子数的密度,单位是分子数/立方米
    ρ ( 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}) (12β)相乘的形式
在这里插入图片描述
在这里插入图片描述
当近似拆解成很多次时
在这里插入图片描述
用等价无穷小可以得到
在这里插入图片描述
所以结果就是,经过了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) (CO)代表三维向量而不是距离
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

总结在一起列一下,也就是我们需要这些条件
在这里插入图片描述
写成函数的话,类似如下,可以用来判断相交,输出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) g1g1,决定了前后散射的相对强度,代表散射方向的平均余弦。正的 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修改光照模型根据公式推导渲染大气。

  • 20
    点赞
  • 60
    收藏
    觉得还不错? 一键收藏
  • 25
    评论
以下是一个基本的Unity URP次表面散射(SSS)着色器示例: ``` Shader "Custom/URP SSS" { Properties { _MainTex ("Albedo (RGB)", 2D) = "white" {} _NormalMap ("Normal Map", 2D) = "bump" {} _SubsurfaceColor ("Subsurface Color", Color) = (1,1,1,1) _SubsurfaceRadius ("Subsurface Radius", Range(0.0,1.0)) = 0.25 _ThicknessMap ("Thickness Map", 2D) = "white" {} _SpecularColor ("Specular Color", Color) = (0.5,0.5,0.5,1) _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 } SubShader { Tags {"RenderType"="Opaque"} LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal : NORMAL; float4 tangent : TANGENT; }; struct v2f { float2 uv : TEXCOORD0; float3 worldPos : TEXCOORD1; float3 worldNormal : TEXCOORD2; float3 worldTangent : TEXCOORD3; float3 worldBitangent : TEXCOORD4; UNITY_VERTEX_INPUT_INSTANCE_ID }; sampler2D _MainTex; sampler2D _NormalMap; sampler2D _ThicknessMap; float4 _MainTex_ST; float4 _ThicknessMap_ST; float _SubsurfaceRadius; float4 _SubsurfaceColor; float4 _SpecularColor; float _Glossiness; float _Metallic; float3 CalculateTangent(float3 n, float3 p, float2 uv, float3 dpdu) { float3 dpdv = cross(n, dpdu); float3 t = normalize(dpdv - uv.y * dpdu); return t; } float3 CalculateBitangent(float3 n, float3 p, float2 uv, float3 dpdu) { float3 dpdv = cross(n, dpdu); float3 b = normalize(cross(n, dpdv)); return b; } float3 CalculateSubsurface(float3 worldPos, float3 worldNormal, float3 worldTangent, float3 worldBitangent) { // Calculate tangent space vectors float3x3 TBN = float3x3(worldTangent, worldBitangent, worldNormal); // Calculate thickness float thickness = tex2D(_ThicknessMap, TRANSFORM_TEX(worldPos, _ThicknessMap)).r; // Calculate subsurface scattering float3 subsurface = _SubsurfaceColor.rgb * thickness * _SubsurfaceRadius; // Transform subsurface scattering to world space subsurface = mul(TBN, subsurface); return subsurface; } float4 LightingURP(SurfaceOutputStandard s, float3 lightDir, float3 worldPos, float atten) { // Calculate subsurface scattering float3 subsurface = CalculateSubsurface(worldPos, s.Normal, s.Tangent, s.Bitangent); // Calculate diffuse lighting float diff = max(0.0, dot(s.Normal, lightDir)); float3 diffuse = (s.Albedo * _LightColor0.rgb * diff); // Calculate specular lighting float3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos)); float3 halfDir = normalize(lightDir + viewDir); float spec = pow(max(0.0, dot(s.Normal, halfDir)), s.Smoothness); float3 specular = _SpecularColor.rgb * _LightColor0.rgb * spec; // Combine lighting float3 finalColor = ((diffuse + specular) * atten) + subsurface; return float4(finalColor, s.Alpha); } v2f vert (appdata v) { v2f o; o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldTangent = UnityObjectToWorldDir(UnityObjectToWorldDir(v.tangent.xyz, UNITY_MATRIX_IT_MV), UNITY_MATRIX_IT_MV); o.worldBitangent = cross(o.worldNormal, o.worldTangent) * v.tangent.w; o.uv = TRANSFORM_TEX(v.uv, _MainTex); UNITY_SETUP_INSTANCE_ID(v); return o; } fixed4 frag (v2f i) : SV_Target { SurfaceOutputStandard s = SurfaceOutputStandard(); s.Albedo = tex2D(_MainTex, i.uv).rgb; s.Normal = UnpackNormal(tex2D(_NormalMap, i.uv)); s.Smoothness = _Glossiness; s.Metallic = _Metallic; float3 worldPos = i.worldPos; float3 worldNormal = normalize(i.worldNormal); float3 worldTangent = normalize(i.worldTangent); float3 worldBitangent = normalize(i.worldBitangent); float4 finalColor = LightingURP(s, normalize(_WorldSpaceLightPos0.xyz), worldPos, _LightColor0.a); return finalColor; } ENDCG } } FallBack "Diffuse" } ``` 这个着色器包含了一个基本的表面纹理、法线贴图、次表面散射颜色和半径、厚度贴图、高光颜色、光滑度和金属度选项。在顶点着色器中,我们还计算了Tangent和Bitangent向量,并传递给片段着色器以计算次表面散射。在片段着色器中,我们计算了次表面散射、漫反射和高光光照,并将它们合并以产生最终颜色。请注意,此着色器是基于URP版本的标准表面着色器,并使用UnityCG.cginc文件中定义的LightingURP函数来计算光照。
评论 25
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值