打个广告: 喜欢研究卡渲技术的朋友欢迎进群950138189。
在之前的文章里,我们曾在
urp管线的自学hlsl之路 第十四篇 广告牌算法www.bilibili.com介绍过《入门精要》的广告牌算法,其核心原理是通过构造一个旋转矩阵来变换顶点,但是它有个缺陷,就是当镜头处理垂直状态时,镜头会瞬间翻转180;并且对于像镜头炫光这种效果在深度处理上还是使用unity自带的深度测试,并不能实现上动图中的遮挡闪烁效果。
本文章的广告牌算法和手动深度测试的算法并非本人原创,是根据colin大大的开源项目(https://github.com/ColinLeung-NiloCat/UnityURP-BillboardLensFlareShader)算法得来,colin大大还有几个其他的开源工程大家可以去瞧瞧看,此文里有2个重点:1广告牌算法;2手动进行的深度测试。 广告牌算法:广告牌算法的目的是将平面始终平行于当前的摄像机的xy轴所在平面,在unity里创建一个quad,这里即是需要将quad的xy平面始终平行于相机的xy平面。
该算法的核心思想是把模型坐标系下的坐标如点A(x0,y0)的x0和y0看做点A相对于模型坐标系下的X轴和Y轴取的数据;则把它转换到相机坐标系下的行为,可以看作A‘相对于相机坐标系下的X轴和Y轴取的数据定为(x0,y0)的操作。
故我们可以这么写:先取模型坐标系的原点坐标(0,0,0,1)变换到相机坐标系下,即使用MV矩阵,得到模型坐标系原点在相机坐标系的坐标;然后它再加上顶点相对于模型坐标系的坐标数值,就得到了顶点在相机坐标系下的数值。即:顶点A在模型坐标系下X轴取x0,Y轴取y0,那我A'在相机坐标系下的X轴也取x0,Y轴也取y0,然后修正一下2坐标系的相对平移位置关系,就可以得到一个qual始终在相机坐标系的XY平面平行的qual,然后变换到裁剪空间下,我们的广告牌技术就搞定了,是不是超简单!代码如下。
然后设置一下渲染类型和渲染队列,混合模式,关掉深度写入和深度测试(后面我们会实现自己的深度测试故需关掉unity自己的深度测试)。但是我想通过Transform的scale组件来控制缩放值,怎么实现?
这就需要上一篇文章里URP管线的自学HLSL之路 第三十一篇 更好的广告牌算法--数学篇的结论了,不需要C#传值就可以直接通过模型转世界的矩阵得到scale值(为什么是模型转世界的矩阵?因为Transform的数据全是相对数据,是模型相对于父级的数据,这里是模型相对于世界的数据),代码如下,这样就可以在Scale里控制qual的缩放了。
这里下图的需要实现的效果,不能通过unity自带的深度测试来完成,故需要关掉unity的深度测试,我们手动来实现。它的实现原理是拿轴心所在位置的深度值和轴心附近小范围的多个像素区域采样的深度图的大小进行深度测试比较,得到一个可见度的比值:全部都通过测试,则当前颜色全部显示;测试通过一部分,则当前颜色半透明;测试全部失败,当前颜色全部不显示。
这个操作我们需要采样深度值,故需要在管线配置文件里打开URP内置的深度图获取。在这篇文章
urp管线的自学hlsl之路 第十六篇 屏幕深度制作护盾特效www.bilibili.com里讲过了深度图的内容,读者不熟悉的可以去这里看看。
前面都是开胃菜,从这里开始就是本文里最复杂的内容了,深度图的深度信息必须得到当前像素的屏幕坐标,我们需要在轴心位置附近构建一个小的区域,然后在这个小的区域内采样每个像素的深度值,但是为了更好的节省性能,我决定在顶点着色器里进行这个深度采样操作:由于顶点着色器并未进行光栅化,所以我们需要手动在顶点着色器里进行简陋版的三角形遍历,做一个低精度版本的线性插值来达到我们的需求。
先定义一个比例值,即轴心附近区域和整个qual区域的比例,我在代码里定义为sampleRange=0.02,我们只需要在中心这个小小的区域里进行采样即可。
然后对这个小的区域进行线性插值,我定义一个轴的单向插值个数(sampleCounts)为3,即该轴的正方向插入3个点,则一个轴插入了2*3+1=7个值,故在这个小小的区域里,我们总共插入了(2*3+1)(2*3+1)=49个采样点供后续对深度图进行采样,这样简单的线性插值就完成了。
为了方便计算,我直接在裁剪空间里对这个小区域的49个顶点进行插值计算得到xy坐标(samplePosition),这里不取z是因为我们不需要z,我们只要得到裁剪空间下的xy进行透除得到NDC坐标再从(-1,1)重映射到(0,1)得到屏幕坐标,同时也要考虑openGL和DX平台差异去决定是否翻转y轴。代码里我们通过一个for循环去遍历全部的采样点的坐标,然后把它转换到屏幕坐标系下。一个qual的顶点数只有4个顶点所以循环的计算量可以忽略不计的。
得到屏幕坐标系下的坐标后,我们对它进行判断,如果不在屏幕内,直接进入下一个迭代;如果在屏幕里,在对深度图进行采样,同样由于未光栅化的原因我们只能采样midmap图像,然后变换到相机空间下得到线性深度,然后和相机空间下的轴位置的深度(这里取z的负值,因为相机空间是右手坐标系,z轴正向和相机朝向相反)进行深度测试比较,如果测试通过,passCounts计数增加1。
然后我们把passCounts除以总采样点数,得到的0-1之间的一个比例并赋予给color到片元着色器里去显示出来去debug:
然后我们旋转镜头,当没有建筑物遮挡时,qual就是纯白色,即所有采样点的深度测试全部通过;当半透明时,即是部分采样点通过深度测试;当为纯透明时,即没有采样点通过深度测试。
然后我们把贴图颜色也乘上color值,就可以得到合理的效果了。这里我也增加了一个相机距离的蒙版效果:当镜头太靠近时,qual也会变成全透明的渐变效果。深度小于0.1时,完全不可见,深度大于2时,完全可见。
然后我考虑到有时候的镜头光晕不一定是水平的,我在前面的代码里从摄像机坐标转换到裁剪坐标系下的过程中增加了一个旋转矩阵去旋转镜头光晕,一个qual的顶点数只有4个所以旋转矩阵的计算量可以忽略不计的,随便用。关于旋转矩阵的推导我已经讲了多次,读者可以在第三十篇文章或者第二十一篇里找到过程,但是这里要注意一下我们也在这一步进行了缩放,他两的顺序是先缩放了在旋转!和unity的处理方式先缩放在旋转在平移的顺序一致,这样才能保证缩放的均匀性。
然后我修修改改一些细节,整个代码就大功告成了,我把源码也放到了码云上供大家参考,如果我有说错的地方望读者提出讨论。码云地址如下
https://gitee.com/matrixry/codes/
cjsbpt0zgy5qxkmhu83