前言

这几天学习了下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吞噬了)

image-20210402141830649

正确结果应该是这样

image-20210402142049117

模型空间相机到顶点的射线算法示例

QQ截图20210401230440

(为了方便画图这里没有追求严谨性,变换后的射线所在坐标系是会与Cube的Local基向量一致的)如上图,相机为点A,以Cube的B顶点为目标发出射线,然后以B为起点,向相机A的Z轴做垂线,构成一个直角三角形:ABC△ABCABC\bigtriangleup ABCACAC即为BB的LinearEyeDepth值,当我们把射线和相机都转换到模型空间后,可以用前面的方法构造一个新的直角三角形:ABC\bigtriangleup AB''C''ABC\bigtriangleup AB''C''ACAC''即为BB''相机空间的深度值-z(因为相机空间是右手坐标系,所以z要取反),考虑到Model2WorldModel2World矩阵(从模型空间转世界空间的矩阵)的缩放系数,即有ABC    ABC\bigtriangleup ABC\;\sim\;\bigtriangleup AB''C'',根据相似三角形定理有ABAC  =  ABAC\frac{\displaystyle AB}{\displaystyle AC}\;=\;\frac{\displaystyle AB''}{\displaystyle AC''},所以我们顶点B在模型空间坐标就是    AB=ABACAC\;\;{AB}=\frac{\displaystyle{AB''}\cdot{AC}}{\displaystyle{AC''}},所以最终B点的模型空间坐标就是:

1
decalSpaceScenePos = i.cameraPosOS.xyz + i.viewRayOS.xyz * rcp(i.viewRayOS.w) * sceneDepthVS;

雀食,雀食蟀啊!

为什么不可以在VS中进行透视除法

因为经过透视相机下的投影矩阵变换后,Z分量的操作是非线性的(缩放+平移,会导致不满足线性变换定义之一——变换后原点不变,事实上这也正是为什么需要透视矫正插值(Perspective-Correct Interpolation)的原因),对其进行的任何操作也是非线性的,那么再经过线性插值(透视校正矫的也不是很正)后,得到的结果也是错误的。

裁剪多余UV

image-20210402152045010

由于我们只需要投射Cube与物体贴合的那部分UV,所以一些额外的UV需要裁剪掉,如上图,INSIDE区域是我们需要的,OUTSIDE部分是需要被裁剪的。出现这个问题的根本原因是深度值的不同导致最终计算出的模型空间坐标超出了原本-0.5~0.5的区间。

1
clip(0.5 - abs(decalSpaceScenePos));

过度拉伸裁剪支持

考虑一种情况,当一个投射器Cube被放在了一个拐角处,就会出现因为平面投影导致的UV超采样问题(根本原因是一个纹理坐标对应多个片元导致的极度拉伸,详情参见Fundamentals of Computer Graphic 纹理映射-平面投影),而这里我们没有办法很好的去处理这个问题(其实这里可以引出当前大世界PCG的经典峭壁问题),要么将这种情况进行裁剪,要么旋转Cube,让UV与切面有一定的倾斜角而不是直接垂直可以缓解

image-20210402161247074

1
2
3
4
5
6
7
8
9
float shouldClip = 0;
#if _ProjectionAngleDiscardEnable

float3 decalSpaceHardNormal = normalize(cross(ddx(decalSpaceScenePos), ddy(decalSpaceScenePos)));
shouldClip = decalSpaceHardNormal.z < _ProjectionAngleDiscardThreshold ? 0 : 1;

#endif

clip(0.5 - abs(decalSpaceScenePos) - shouldClip);

原理就是利用偏导数求出X,Y轴斜率然后叉乘得到法线信息,再通过法线信息与我们设定的临界值比较,如果太离谱就直接裁剪

image-20210402164454285

对于偏导数的几何意义,我找出了我的早年作品:(这是人写的字?)

image-20210402164642336

正交相机支持

这个SSD还支持了正交相机,主要对于正交相机的深度值处理,我们需要手动对它进行线性插值,最后是重建世界坐标的时候我想了很久也没有明白为什么要对相机的世界坐标*2,如果有知道的大佬请不吝赐教。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#if _SupportOrthographicCamera
// static uniform branch depends on unity_OrthoParams.w, w = 1.0 if camera is ortho, 0.0 if perspective
// (should we use UNITY_BRANCH here?) decided NO because https://forum.unity.com/threads/correct-use-of-unity_branch.476804/
if(unity_OrthoParams.w)
{
// if orthographic camera, _CameraDepthTexture store scene depth linearly within [0,1]
// if platform use reverse depth, make sure to 1-depth also
// https://docs.unity3d.com/Manual/SL-PlatformDifferences.html
#if defined(UNITY_REVERSED_Z)
sceneRawDepth = 1-sceneRawDepth;
#endif
// simply lerp(near,far, [0,1] linear depth) to get view space depth
float sceneDepthVS = lerp(_ProjectionParams.y, _ProjectionParams.z, sceneRawDepth);
//***Used a few lines from Asset: Lux URP Essentials by forst***
//----------------------------------------------------------------------------
// reconstruct posVSOrtho
float2 viewRayEndPosVS_xy = float2(unity_OrthoParams.xy * (i.screenPos.xy * 2 - 1));
float3 posVSOrtho = float3(-viewRayEndPosVS_xy, -sceneDepthVS);
// convert posVSOrtho to posWS
float3 posWS = mul(unity_CameraToWorld, float4(posVSOrtho,1)).xyz;
posWS -= _WorldSpaceCameraPos * 2; // Don't understand this, Why * 2?
posWS *= -1;
//----------------------------------------------------------------------------
// transform world to object space(decal space)
decalSpaceScenePos = mul(UNITY_MATRIX_I_M, float4(posWS, 1)).xyz;
}

Stencil Test

我们会有这样的需求,人物走在贴花上时不会被贴花,而是直接踩在上面(下图来自:http://rainyeve.com/wordpress/?p=663)

image-20210402142232440

所以就需要进行模板测试

1
2
3
4
5
Stencil
{
Ref[_StencilRef]
Comp[_StencilComp]
}

这里设置引用值为1,对比操作为NotEqual,意思是每个片元拿当前模板缓冲区值(默认值为0)与1对比,如果不等于1,即为测试通过,否则将丢弃片元

image-20210402142350995

那么我们就可以为我们的人物Shader加上模板测试

1
2
3
4
5
6
Stencil
{
Ref 1
Comp NotEqual
Pass Replace
}

即可达成效果

image-20210402142656335

总结

从Colin大神的Shader中学到许多,理解到自己平时写的Shader和工业级Shader的差距(需要多方面考虑健壮性),也是头一次知道Shader也可以写的这么优雅,希望自己再接再厉,有朝一日自己也能跟着Siggraph,GDC复刻出想要的效果。

参考

游戏中的Decal(贴花)

UnityURPUnlitScreenSpaceDecalShader

Screen Space Decals in Warhammer 40,000: Space Marine

Fundamentals of Computer Graphic 纹理映射

大世界PCG的经典峭壁问题