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
View Space Normal From Depth Texture Cover

从深度图中获取视空间的法线

为什么要从深度图重建视空间法线 一个很大的应用情景是在后处理的阶段,或是计算一些屏幕空间的效果(如SSR、SSAO等),只能获取到一张深度贴图,而不是每一个几何体的顶点数据,很多的计算中却又需要用到世界空间的法线或者是视空间的法线,这时我们就需要通过深度图来重建视空间的法线。(诶这段话我是不是写过一遍了) 重建视空间法线的方法 bgolus在他的WorldNormalFromDepthTexture.shader里面很全面的介绍了各种重建视空间法线的方法。其中比较值得注意的是来自Janos Turanszki的根据深度差判断当前像素属于哪个平面的方法,和来自吴彧文的横向和纵向多采样一个点来判断当前像素属于哪个平面的方法,其中吴彧文的方法能够在绝大部分情况下获取到最准确的法线(除了尖角的一个像素)。 除了bgolus介绍的方法之外,我在GameTechDev/XeGTAO中还看到了一种方法。这种方法类似于Janos Turanszki的深度差的方法,不过从深度差中获取的是0-1的边缘值(edgesLRTB,edgesLRTB.x越接近0即代表该像素的左侧越是一条边缘),再使用边缘的两两乘积对四个法线进行插值,最终计算出视空间法线。我个人认为当在两个面相接的地方不需要特别准确的法线值时,这是最好的计算法线的方法。用这个方式计算的法线,在两个面相接的地方,法线会有一种从一个面插值到另一个面的效果(且一定程度上抗锯齿),在两个面远近排布的时候,也能获取到准确的法线。 具体的实现方法 根据需要使用的方法,采样深度图。在采样比较集中的情况下,可以使用GatherRed方法来减少采样的次数。GatherRed可以得到双线性采样时的四个像素的R通道的值并封装到一个float4中,当屏幕左下角是(0, 0)时,这个float4的x分量对应采样点左上角的颜色的R通道的值,y对应右上角,z对应右下角,w对应左下角,可以在HLSL的文档中看到Gather的相关介绍。Compute Shader的话可以使用group shared memory进一步减少采样。 使用深度图和当前的uv值计算出像素的视空间的坐标,这一步尤其需要注意视空间坐标Z分量的正负性的问题。Unity的视空间变换矩阵UNITY_MATRIX_V是摄像机位于视空间(0, 0, 0),看向视空间Z轴负方向的,右手系的矩阵。即视空间的坐标Z分量往往是一个负值,其法线的Z分量在往往下是正值(即画面看上去应该多为蓝色)。 从深度图中计算视空间坐标的时候,如果Unity版本比较旧,会没有UNITY_MATRIX_I_P这个矩阵,这时可以使用unity_CameraInvProjection来代替,但需要注意DirectX平台UV上下翻转的问题。 当屏幕左下角是(0, 0)时,使用右侧的视空间坐标减去左侧的视空间坐标,使用上侧的视空间坐标减去下侧的视空间坐标。五个采样点(包括位于中心的当前像素)可以获得四个向量,对于右手系的视空间坐标来说,将这四个向量按照水平向量叉乘竖直向量的顺序,就可以获得四个当前像素的法线了。 最后使用前面介绍的获取法线的方法,从这四个法线中获取最为正确的法线。这些方法往往都会使用深度值来进行判断,这里需要注意的是透视变换带来的深度的非线性的问题。对于屏幕上等距分布的三个点ABC,当他们在世界空间中处于同一条直线时,有 $$ 2 \cdot rawDepthB = rawDepthA + rawDepthC \newline \frac 2 {linearDepthB} = \frac 1 {linearDepthA} + \frac 1 {linearDepthC} $$ 这里也稍微证明一下,如下图所示,ABC三点通过O点透视投影到了A’B’C’三点: 由于ABC三点共线,A’B’C’三点共线,不妨假设 $$ \begin{gather} k * \overrightharpoon{OA’} + (1 - k) * \overrightharpoon{OC’} = \overrightharpoon{OB’} \\ K * \overrightharpoon{OA} + (1 - K) * \overrightharpoon{OC} = \overrightharpoon{OB} \\ \overrightharpoon{OA} = a * \overrightharpoon{OA’} \\ \overrightharpoon{OB} = b * \overrightharpoon{OB’} \\ \overrightharpoon{OC} = c * \overrightharpoon{OC’} \\ \end{gather} $$...

January 27, 2022 · zznewclear13
Vertex Animated Plant Cover

使用顶点动画制作随风飘动的植物

动机和想要实现的效果 最直接的动机是看了顽皮狗在Siggraph 2016上的PPT,里面介绍了顽皮狗在神秘海域中是如何让植被随风飘荡的。他们介绍了一种将植被的每一部分的pivot的物体空间坐标写到顶点色里,然后在shader中使用这个坐标进行风的效果的计算的方法。较为震撼在风吹过草原时,植被进行弯曲后,草表面的高光会有一种时空上的起伏感(也就是说神秘海域的植被的法线也会被风影响)。所以我也想要借助写pivot的方法来制作植被受到风吹的效果,通过这个方法计算出正确的风吹之后的植被的法线(同时由于法线贴图的存在,还要计算正确的切线)。 稍微翻了一下网上的资料(也没仔细地去搜索),大部分的就是一个普通的顶点动画,有的是用的sin,有的就直接平移。这就产生了第二个需求,植被在顶点动画中应该保持差不多的长度,不然会发现很明显的拉伸的效果。 当然最好还能投射出正常的影子了,这一步只需要把顶点着色器复制一份到投射影子的pass里就可以了。 这里使用的植被模型是MegaScans上的CORDYLINE模型中的var12这个小模型。 难点和相对应的应对方法 Unity的顶点色限制 稍微测试一下就能发现,Unity的顶点色是UNorm8的格式,也就是说无论你在Maya或是3ds Max里导出的模型的顶点色信息是什么样的,导入到Unity中就会变成只有256精度的UNorm8。顽皮狗使用的是自己的引擎,所以它们能够使用全精度的顶点色,但是由于Unity的引擎限制,我们可以考虑到导出pivot的顶点坐标到模型的UV中。 但是很不幸的是,fbx导入到Unity时,即使UV是float4的类型(也就是16bytes),在Unity中只会识别UV的前两位。所以只能无奈的将pivot的顶点坐标(float3的数据)储存到两个UV的三个通道里,同时将pivot的层级存到剩下的一个通道里。我不知道顽皮狗具体是怎么计算pivot的层级关系的,他在PPT中写的是无需计算,但我在实际操作中只能一层一层的算(而且只能算两层),也希望知道具体怎么操作的人告知一下方法。 所以接下来要做的是在Maya中把pivot的物体空间坐标和pivot的层级写到对应顶点的某两套UV中,本文是写到第二套和第三套UV中(也就是TEXCOORD1和TEXCOORD2)。于是我恶补了一下maya的python脚本的写法,不过在写数值到UV中时,又遇到了一个小问题。Maya的cmds.polyEditUV这个方法,明明能传入uvSetName这个参数,用于操作对应的UV,但我实际使用时只能写数值到当前的UV中,导致最后写的脚本只能僵硬的操作当前UV,每次切换UV时需要重新修改脚本再运行一次。 最终的脚本是这样的: VertexPivotWriteTool.py import maya.cmds as cmds targetVertexStr = "Select any vertex to start." vertexColorStr = "Select any vertex to start." pivotPosition = [0.0, 0.0, 0.0] def ui(): if cmds.window("VertexPivotWriteTool", exists = True): cmds.deleteUI("VertexPivotWriteTool") global targetVertexStr global targetVertexField global vertexColorStr global vertexColorField global pivotLayer vertexPivotWindow = cmds.window("VertexPivotWriteTool", widthHeight = [500, 400]) form = cmds.formLayout(numberOfDivisions = 100) pivotLayerLable = cmds.text("Pivot Layer (0 for root pivot)") pivotLayer = cmds....

January 6, 2022 · zznewclear13
Equal Width Bezier Curve

在Unity的UI中绘制等宽的贝赛尔曲线

动机和贝塞尔曲线相关的背景知识 动机当然是要在UI上绘制一个贝塞尔曲线的形状了,想做的效果大概就和虚幻引擎蓝图连接节点的线差不多了。这里要绘制的是一种较为特殊的贝塞尔曲线,它的两个端点的切线是水平的,且拥有旋转对称的特性。 我们想要绘制的S形曲线是三阶的贝塞尔曲线。三阶贝塞尔曲线有四个控制点\(P_0, P_1, P_2, P_3\),对于一个从0到1的变量\(t\),贝塞尔曲线的做法是对这四个点按照顺序以\(t\)做插值生成三个新的点,然后对这三个点按照顺序以\(t\)做插值生成新的两个点,再对这两个点以t做插值生成最后的点,当t在0到1中变化时,这个点的轨迹就构成了贝塞尔曲线。贝塞尔曲线上的点可以用四个控制点和\(t\)来表示: $$ P_{bezier} = P_0 \cdot (1 - t)^3 + 3 P_1 \cdot (1 - t)^2 \cdot t + 3 P_2 \cdot (1 - t) \cdot t^2 + P_3 \cdot t^3 $$ 如果初始四个点分别是(0.0, 1.0), (d, 1.0), (1.0 - d, 0.0),和(1.0, 0.0)的话,也就是我们所要绘制的特殊的贝塞尔曲线,可以算出贝塞尔曲线的坐标为 $$ P_{bezier} = ((3 t - 9t ^ 2 + 6t^3) \cdot d + 3t^2 - 2t^3, 1 - 3t^2 + 2t^3) $$ 但是即使得到了贝塞尔曲线的参数方程,想要将其表达成\(f(x)\)的形式仍然是相当困难的。Alan Wolfe在他的博客中提到了一种一维贝塞尔曲线,也是一种贝塞尔曲线的特殊情况,四个控制点在水平方向上等距排开,这样子贝塞尔曲线的参数方程的水平分量就刚好是\(t\),它的竖直分量也就是我们需要的\(f(x)\)。唯一美中不足的是,能轻易得到\(f(x)\)的一维贝赛尔曲线,往往是一个“躺倒”的贝赛尔曲线,感官上看上去是横着的,Shadertoy上有相关的演示。但这种一维贝塞尔曲线又有一种特殊情况,也就是前两个控制点的竖直高度相等,后两个控制点的竖直高度也相等,这时这种特殊的一维贝塞尔曲线就是我们耳熟能详的smoothstep曲线了(数学真奇妙啊)。可惜smoothstep不能满足我们随意控制曲线形状的需求,只能另求他法。...

December 15, 2021 · zznewclear13
zznewclear13 技术美术 图形学 个人博客 technical art computer graphics