Ray Marched Volumetric Fog Cover

使用Ray Marching来渲染体积雾

为什么要用Ray Marching 要不还是别用Ray Marching了(除非是SDF Ray Marching),采样次数又多又不好debug,不过写起来比较快(如果要写二分法的话就又复杂了)。如前文所说,使用Ray Marching的体积雾只能在后处理阶段使用了,在处理不写深度的透明物体的时候,会有一些瑕疵。 体积雾相关的就参考前文就好了,这里只是作为一个方法的补充。 相关代码和说明 为了和使用3D纹理的体积雾作区分,这边所有代码的名字前加上了RM(Ray Marching)。 RMVolumetricFog.cs 这个脚本和3D纹理的体积雾的参数几乎完全一致,只是多了用于控制Ray Marching次数的step。 using System; namespace UnityEngine.Rendering.Universal { [Serializable, VolumeComponentMenu("Post-processing/RM Volumetric Fog")] public class RMVolumetricFog : VolumeComponent, IPostProcessComponent { [Tooltip("是否启用体积雾")] public BoolParameter enabled = new BoolParameter(false); [Tooltip("整体控制体积雾强度")] public ClampedFloatParameter intensity = new ClampedFloatParameter(1.0f, 0f, 1.0f); [Tooltip("体积雾最大的透明程度(用于和天空混合)")] public ClampedFloatParameter maxTransmittance = new ClampedFloatParameter(1.0f, 0f, 1.0f); [Tooltip("体积雾的颜色倾向,目前强度为0.03")] public ColorParameter fogTint = new ColorParameter(Color.white); [Tooltip("体积雾距离相机最近的距离")] public ClampedFloatParameter fogNear = new ClampedFloatParameter(0.1f, 0.01f, 10f); [Tooltip("体积雾距离相机最远的距离")] public ClampedFloatParameter fogFar = new ClampedFloatParameter(100f, 1....

August 24, 2022 · zznewclear13
Volumetric Fog Cover

使用和视锥体对齐的3D纹理来渲染体积雾

为什么要渲染体积雾 因为它就在那里。 当然了,更重要的是因为体积雾能迅速的营造出场景的真实感与氛围感,谁不喜欢光源边上还有一小圈光晕呢,如果什么高亮的物体都能影响体积雾的话,是不是就不太需要bloom效果了呢。我实际地在生活中观察了一下,发现人眼所看到的光晕的效果,是光线进入眼睛之后产生的,也就是说bloom和体积雾确确实实是两种不同的效果。 体积雾的渲染方法 体积雾一般有两种渲染方法,一种是单纯的从相机出发对场景进行Ray Marching,每次进行采样和混合。这种方法主要的缺点是Ray Marching的次数会比较高才能有较好的渲染效果。在我的测试中,开启TAA的时候,20次Ray Marching就能得到很好的体积雾效果了;但是不开启TAA的话,可能会需要60次甚至更高的Ray Marching才能得到和TAA类似的效果。同时,Ray Marching体积雾只能在后处理阶段使用,在处理不写深度的透明物体的时候,会有一些瑕疵。 另一种方法就是使用一张3D纹理,将整个场景的体积雾储存在这张3D纹理中,当绘制物体的时候使用物体的世界空间坐标采样这张3D纹理,直接在片元着色器中计算雾效之后的颜色。这种方法使用的3D纹理会占用更多的内存,但是一定程度上能够正确的渲染所有物体,和60次Ray Marching相比,性能上也说不定会有一些优势。 本文的体积雾实现,参考了EA的寒霜引擎在Siggraph 2015年时的演讲和diharaw的OpenGL的体积雾效果。值得一看的还有Bart Wronski在Siggraph 2014年的演讲,以及之后的荒野大镖客在Siggraph 2019年的课程。使用的是Unity2019.4.29的URP工程。 具体的实现方法 将场景中的需要渲染的雾的信息和阴影信息储存到一张和相机的视锥体对齐的3D纹理中。按照寒霜引擎的做法,纹理大小为(分辨率宽/8)x(分辨率高/8)x64,这样就和屏幕大小的2D纹理占用的内存大小一致了,但我看Unity官方的体积雾工程中,3D纹理的深度为128,就也把自己的设置成128了,纹理深度越深,体积雾的细节就能越高。3D纹理的宽高和视锥体对齐,这很好理解,而这张贴图的纵向深度和实际的深度要怎么对齐呢?最简单的就是和视空间的深度线性对应,但是这会导致近处体积雾的分辨率不够;另一种是和裁剪空间的深度线性对应,经过一些分析可以知道这比之前的方法更糟糕;目前我看下来最好的应该是和视空间的深度指数型对应,这样离相机越近3D纹理的像素会越多,越远则越少。本文只使用了均一的雾,但是可以使用世界空间的坐标、噪波和一系列的运算,计算出某一点的体积雾的浓度。 使用上面的雾的信息和阴影信息计算出散射的值Lscat,从下面的图可以看到Lscat是对所有的光源(本文只有主光源)计算\(f(v, l)Vis(x, l)Li(x, l)\)的和,\(Vis(x, l)\)即为在x点l光的可见性,可以通过采样阴影贴图来获得,\(Li(x, l)\)即为在x点l光的光强,可以简单的计算获得,\(f(v, l)\)用来表述在v的方向观察雾时得到l的散射量,一般被叫做Phase Function,我们使用的是Henyey-Greenstein Phase Function,其中参数g是雾的各向异性的程度,越靠近1表示光线穿过雾时越保持之前的方向,越靠近0表示光线穿过雾时均匀的散射,越靠近-1表示光线穿过雾时越会进行反射(在实际的光照中,我们会去掉\(\pi\)这一项,这样能和Unity的光照模型保持一致)。时空混合也在这一步可以完成。 $$ \tag{Henyey-Greenstein} p(\theta) = \frac 1 {4\pi} \frac {1 - g^2} {(1 + g^2 - 2g \cos \theta)^{\frac 3 2}} $$ 对3D纹理从相机近点到远点进行混合,这其实是一种Ray Marching,不过是在3D纹理的纹理空间进行Ray Marching,一次前进一个像素。当混合当前像素和上一个像素时,需要考虑符合物理的透光率(transmittance), \(\varepsilon\)是一个用于归一化的常量,l是两点之间的距离,c是介质的吸收率(一定程度上可以用雾的密度来表示)。具体的混合的计算和说明可以看EA寒霜引擎的PPT第28、29页。 $$ \tag{Beer-Lambert} transmittance = e^{-\varepsilon l c} $$ 最终在绘制物体时,使用物体的世界空间的坐标,转换到3D纹理的坐标,采样3D纹理,使用透光率乘上物体本身的颜色,再加上雾的颜色,就得到了最终的体积雾的效果了。 相关代码和说明 VolumetricFog.cs 用于Global Volume中方便添加体积雾和控制各种参数。值得考虑的是maxTransmittance的值,因为相机远裁剪面会比较远,即使雾并不是很大,在最远处也总是能变成单一的颜色,这个值用来防止这种情况,人为地限制了最大不透光率(但是还是叫maxTransmittance)。fogNear这个参数实际是影响了3D纹理和相机之间的距离,最好还是设置成0,不然时空混合时会有一些瑕疵。 using System; namespace UnityEngine....

August 23, 2022 · zznewclear13
zznewclear13 技术美术 图形学 个人博客 technical art computer graphics