一些学习笔记和效果制作的记录
Volumetric Atmospheric Scattering - Alan Zucconi
首先还是推荐上面链接国外大佬的概念介绍
rayMarching和signed distance functions不能有效的模拟大气散射,渲染大气这种实心半透明物体,更合适的方法是volumetric single scattering体积单次散射。实时渲染中更多的还是考虑体积单次散射。
Out-Scattering 射向相机的光线被偏转
In-Scattering 没有射向相机的光线被偏转向相机,其会导致光源周围出现光晕
体积单次散射的整个流程就是:
当然,实际情况肯定有很多别的光线通过其他路径到达相机,但是体积单次散射只考虑view direction上的in-scattering的光线
假设光线从C->P->A
T表示衰减系数 表示某段路径上光照的衰减程度
S表示散射系数 表示有多少光散射的角度为θ,λ为波长,h为高度
所以单个P点最终到人眼的A的光照为:
最终到人眼的光照为ViewDir路径上的P值的积分
由于太阳光来自很远的地方,所以到达大气的每一个点的光照方向都可以视作一样的,所以对于A点in-Scattering的θ角度是一样的,这样就是个定值可以优化表达式。实际操作中的具体优化会在之后提及
以上是单次散射的流程,那么针对现实中真实的散射效果,目前主流的是俩种数学散射模型来进行模拟
散射模型
Rayleigh Scattering
大小远小于光线波长的粒子,由于粒子比波长还要小很多,所以光的波长也会影响散射程度
Mie Scattering
大小远大于光线波长的粒子,由于光的波长相较于这些粒子大小来说是可以忽略的,所以认为Mie Scattering和光线波长无关,Mie Scattering是太阳周围光晕的主要成因
Rayleigh Scattering
蓝线表示散射光的首选方向
散射函数 表示入射光散射到特定方向上的光
λ光的波长,θ散射角度,h高度,n=1.00029空气的折射率,N=2.54*10的24次方,标准大气密度,ρ(h)密度比,在海平面为1,随高度呈指数下降
一般使用指数函数对其真实曲线进行数学拟合,对于Rayleigh Scattering,通常设H=8500米,对于Mie Scattering H一般在1200米左右
上图显示三种不同波长,h=0时的散射系数
Rayleigh散射方程表示有多少光向特定方向散射,但并不能知道一共散射了多少光。所以需要做球面积分得到总的散射系数:
公式推导:
上图所示是基于二维的函数图像,要计算空间中的总散射,需要做球面积分
考虑海平面0时,密度比为1,上式可化为
所以蓝光散射的比较厉害,所以白天的时候天空为蓝色。而当黄昏的时候,由于光线几乎平行于地平线传播,所以阳光要穿越更厚的大气层,大多数蓝光都被散射未进入人眼,所以天空会偏红。
Rayleigh 散射原始方程可分解为俩个分量,一是β(λ,h)定义强度,二是γ(θ),表示有多少光散射到θ角度上,称为相位函数。
Rayleigh 散射的相位函数为:
可以看出散射函数就等于总散射系数×相位函数
Rayleigh 散射方程分解:
Mie Scattering
散射系数
上图式子β(λ)也是考虑海平面为0时的情况,考虑p(h)时为下式
相位函数
衰减系数 T
以上散射系数是考虑散射到某一特定角度上的光线比例,现在需要考虑经过一次散射之后,入射方向上还剩下多少光
设I0在一次散射后损失了β比例的光,这个β就是上面提到的散射系数。则剩余的光为I1
根据该式,得到衰减一段距离x后剩余的光为
公式推导:
所以如果β是定值的情况下,从C到P的光量为
如果是手游中的实时渲染,可能恒定的大气密度就可以满足一个计算就能实现比较不错的效果。但是如果是精度要求较高的话,由于大气密度是随高度变化的,所以需要每个计算点的散射系数来计算,用积分表示
简化,可以直接代入β散射系数方程得到
就是那段积分的值,称之为光学距离
所以最后式子就简化为:
如果只考虑Rayleigh时的最终方程:
之前都是理论,实际应用的时候,我们需要考虑怎样简化计算:
以Raylieigh散射方程为例,上式中θ λ 和密度比ρ不是定值,其他都是定值。所以在实际运用中的计算流程是先在CPU端计算散射系数在海平面为0时的β(λ)
上式为定值,算好后穿值给材质。密度比ρ和散射角度θ这些变量则在材质中计算。
并且如参考文章和GPU精粹2中大气散射优化部分所说,进一步的优化,可以将这些复杂的计算预处理存到LUT中,运行时直接读数据即可。
在实际游戏应用中,大气散射效果分成天空盒和雾效俩个部分。关于雾效的部分有几个需要注意的点,首先由于光线的来源方向是viewDirection,所以使用后处理材质来添加雾效时,获取viewDir的方式为用深度图重新构建世界坐标,再用worldPos-camPos算出viewDir。那么不写深度的半透物件如果也需要受到雾效影响的话,就需要单独在它们的材质中用worldPosition去计算散射雾效。同时恒定空气密度的雾效没有做到密度随高度增高而减低的影响,所以为了模拟低处雾效更浓的效果,一般还需要添加一个高度雾配合使用。
GitHub - SlightlyMad/AtmosphericScattering: Atmospheric Scattering for Unity
这里极力推荐这位大佬的工程,其包括了raymarch计算+LUT优化+LightShaft等,是非常好的学习资料
这里简单记录下他的做法:
天空盒:
在shader中,方式一是做raymarching计算density,优化版本的方式二是通过LUT存储in scattering的计算信息,用的是3D LUT,Rayleigh和Mie是分开的,有俩张LUT
LUT中存储的值通过compute shader计算,在Start()中计算LUT传值给shader。compute中计算LUT的时候也需要for循环计算in scattering的部分
雾效:
在shader中,与上面相同,同样方式一是做raymarching计算in scattering,优化版本的方式二是通过LUT存储Inscattering和Extinction,这俩LUT也是在compute shader中计算,并且在一个for循环中就能算好。
并且需要拿到深度图用来和视线方向相乘来作为步进距离,无论是raymarch方式还是LUT方式,都需要用到深度信息。
InscatteringLUT存储compute计算好的整个inscattering部分的颜色,ExtinctionLUT存储衰减部分。最终颜色:
并且还有前置的AmbientLight部分计算采样随机_RandomVectors贴图 DirLight部分采样ParticleDensityLUT DensityLUT计算
Pass0 密度比预计算
Pass1 根据随机方向vector 计算模拟环境光带来的inScattering部分
Pass2 光学距离预计算
Pass3 雾效计算 同样也需要用到深度图重建世界坐标,其用到的Inscatter和衰减系数信息同样来自于俩张3D LUT
所有的LUT都在compute shader中计算
当然 以上雾效还未考虑物件遮挡产生的体积光效果,计算LightShaft部分需要将世界坐标转换到shadowmap空间判断可见性,然后就是正常体积光dither、模糊之类的操作
可以看出 就算使用了LUT存储的方式,计算LUT的过程中compute中也会有大量的for运算,并且要声明非常多的LUT,所以对于手机平台而言,可能恒定密度比简化计算是不错的选择。既能保证不做过多的计算,又能有一个相对不错的效果
延伸到如果是做24小时的TOD,那么就需要定义一些颜色曲线根据时间变化动态改值。如我这里就简单调整了一些曲线。如散射的颜色,云的颜色,海水相关颜色如整体颜色、SSS颜色和反射颜色等。凌晨的雾浓度应该比较高,也可以添加float曲线控制雾效的距离。总之到这一步就是根据美术效果调参即可。
我如下图中的实现,就用的恒定的密度比,没有raymarch的计算,其实效果也不差。但如之前所说,用恒定密度的雾效一般需要额外加一个高度雾的效果。
简单调的颜色曲线
效果如下,动图大小有限制所以压的比较糊:
顺便提一下云的效果,用的是2D面片云,这里没有使用法线贴图。而是用houdini出云的厚度图,然后用视差的步进方法计算自阴影,这里就不展开说了。这样算出来的效果其实并不逊于法线图的效果,并且更风格化一些~
一些云的效果参考
Parallax Mapping with self-shadowing implementation solution here ? · Issue #1009 · UPBGE/upbge · GitHub
houdini中就主要用自带的cloud相关那几个节点就行,可以建个简模来做大型
视频效果 B站1080压缩也很糊:
TOD效果_哔哩哔哩_bilibili
参考:
[Rendering] 基于物理的大气渲染 - 知乎
基于物理的大气散射 in Unity URP - 知乎
GitHub - SlightlyMad/AtmosphericScattering: Atmospheric Scattering for Unity
【实战】从零实现一套完整单次大气散射_一 - 知乎