URP下屏幕空间贴花(ScreenSpaceDecal)学习笔记
前言
这几天学习了下Colin大神的屏幕空间贴花实现,感觉其中的算法实现和坐标转换让本笨比拍案叫绝,故记录分享一下。
大体思路
根据在任意坐标系中,已知一个物体A的坐标,以及另一个物体B相对于物体A的偏移量,即可得知物体B坐标的定理。我们可以将Camera坐标和从Camera到顶点的射线从相机空间转换到模型空间,然后利用从VS到PS的线性插值(其实是类似我们从深度图重建世界坐标时,构造四条从相机到屏幕面片四个顶点的射线,利用VS到PS的线性插值得到片元数目的射线原理,即可得知屏幕面片每一个片元对应的世界空间坐标),即可得知物体每一个片元对应的模型空间坐标,又因为我们使用了单位长度为1的Cube作为投射器,所以就可以直接用这个坐标当成UV去采样贴图(由于我们只是需要类似一个面片一样的贴花,所以需要裁剪掉除0~1范围外的所有UV),达成贴花的效果。
正文
基础设置
RenderQueue
1 | "Queue" = "Transparent-499" |
为了避免渲染顺序问题,把其渲染队列放到Transparent,并且在任何半透明物体之前渲染
ZWrite off
为了支持透明度混合,关闭深度写入
考虑:一个透明物体A深度小于当前贴花所用的投射器Cube,如果开启深度写入,就会导致A物体渲染错误(下图半透明砖块就是A,可以看到由于深度测试失败被透明的投射器Cube吞噬了)
正确结果应该是这样
模型空间相机到顶点的射线算法示例
(为了方便画图这里没有追求严谨性,变换后的射线所在坐标系是会与Cube的Local基向量一致的)如上图,相机为点A,以Cube的B顶点为目标发出射线,然后以B为起点,向相机A的Z轴做垂线,构成一个直角三角形:,的即为的LinearEyeDepth值,当我们把射线和相机都转换到模型空间后,可以用前面的方法构造一个新的直角三角形:,的即为相机空间的深度值-z(因为相机空间是右手坐标系,所以z要取反),考虑到矩阵(从模型空间转世界空间的矩阵)的缩放系数,即有,根据相似三角形定理有,所以我们顶点B在模型空间坐标就是,所以最终B点的模型空间坐标就是:
1 | decalSpaceScenePos = i.cameraPosOS.xyz + i.viewRayOS.xyz * rcp(i.viewRayOS.w) * sceneDepthVS; |
为什么不可以在VS中提前进行透视除法
首先这里的透视除法特指SSD中的手动模拟操作,而并非渲染流水线的透视除法
。先明确一点,在屏幕空间插值的数据并不能保证是和之前的数据成线性关系(缩放+平移,会导致不满足线性变换定义之一——变换后原点不变),试想下,屏幕上连续的两个点他们的值完全可能和Z没有任何线性关系(实际上只和1/Z成线性关系), 如果在VS中提前手动做了透视除法,相当破坏了1/Z的线性关系,导致结果错误,所以只能等到流水线把正确的数据主动传递到VS之后,再做透视除法
PS:在更加通用的语境中,透视除法指vs之后,ps之前的vcc阶段的齐次除法,这一步是硬件来处理的。那么,为什么齐次除法要在硬件层做?这就牵扯到另一个概念:透视矫正插值,例如我们不能直接拿物体在ndc空间坐标为uv去采样,因为当物体不平行于相机时,uv值变化本身和Z之间关系是非线性的,只和1/Z成线性关系,需要通过透视矫正插值来处理,透视矫正插值也是硬件处理的,需要用到我们投影变换得到的齐次坐标的w分量。说到底是想要开发者们无需关心底层的细节,所以帮我们在硬件层做了齐次除法和透视矫正插值
至于上文的Z, 1/Z, 透视除法,更加细节的推理详见:SoftRenderer&RenderPipeline(从迷你光栅化软渲染器的实现看渲染流水线)
裁剪多余UV
由于我们只需要投射Cube与物体贴合的那部分UV,所以一些额外的UV需要裁剪掉,如上图,INSIDE区域是我们需要的,OUTSIDE部分是需要被裁剪的。出现这个问题的根本原因是深度值的不同导致最终计算出的模型空间坐标超出了原本-0.5~0.5的区间。
1 | clip(0.5 - abs(decalSpaceScenePos)); |
过度拉伸裁剪支持
考虑一种情况,当一个投射器Cube被放在了一个拐角处,就会出现因为平面投影导致的UV超采样问题(根本原因是一个纹理坐标对应多个片元导致的极度拉伸,详情参见Fundamentals of Computer Graphic 纹理映射-平面投影),而这里我们没有办法很好的去处理这个问题(其实这里可以引出当前大世界PCG的经典峭壁问题),要么将这种情况进行裁剪,要么旋转Cube,让UV与切面有一定的倾斜角而不是直接垂直可以缓解
1 | float shouldClip = 0; |
原理就是利用偏导数求出X,Y轴斜率然后叉乘得到法线信息,再通过法线信息与我们设定的临界值比较,如果太离谱就直接裁剪
对于偏导数的几何意义,我找出了我的早年作品:(这是人写的字?)
正交相机支持
这个SSD还支持了正交相机,主要对于正交相机的深度值处理,我们需要手动对它进行线性插值,最后是重建世界坐标的时候我想了很久也没有明白为什么要对相机的世界坐标*2
,如果有知道的大佬请不吝赐教。
1 |
|
Stencil Test
我们会有这样的需求,人物走在贴花上时不会被贴花,而是直接踩在上面(下图来自:http://rainyeve.com/wordpress/?p=663)
所以就需要进行模板测试
1 | Stencil |
这里设置引用值为1,对比操作为NotEqual,意思是每个片元拿当前模板缓冲区值(默认值为0)与1对比,如果不等于1,即为测试通过,否则将丢弃片元
那么我们就可以为我们的人物Shader加上模板测试
1 | Stencil |
即可达成效果
总结
从Colin大神的Shader中学到许多,理解到自己平时写的Shader和工业级Shader的差距(需要多方面考虑健壮性),也是头一次知道Shader也可以写的这么优雅,希望自己再接再厉,有朝一日自己也能跟着Siggraph,GDC复刻出想要的效果。
参考
UnityURPUnlitScreenSpaceDecalShader
Screen Space Decals in Warhammer 40,000: Space Marine