贴花效果,就和名字的直接意思类似,把一张图贴到另一个物体上显示,经常被用于表现一些重复出现的图案,比如弹孔,涂鸦,污渍等。效果图:
常规贴花实现
Unity官方提供了一个工程,这个工程主要是用来说明CommandBuffer是怎么使用的,其中有贴花的一些展示,主要是用CommandBuffer在Deferred渲染路径下实现贴花效果。使用CommandBuffer是因为需要把BuiltinRenderTextureType.GBuffer2中存储的法线信息传给Shader,而这次测试主要为了验证原理,不使用法线信息,所以可以不用CommandBuffer(即使使用法线信息也可以在Shader中通过_CameraDepthNormalsTexture结合DecodeDepthNormal方法来获取到法线信息)。原工程通过 cam.AddCommandBuffer (CameraEvent.BeforeLighting, buf); 这句实现把CommandBuffer插入到延迟渲染的光照计算Pass前面,也可以去掉不用。所以原工程的C#代码基本可以不使用,在Forward渲染路径下,完全在Shader中实现贴花效果。之前看文档说如果Shader中使用深度图的话需要在C#代码中设置相机的depthTextureMode,即 mainCam.depthTextureMode = DepthTextureMode.Depth;,但是我试了下不写这行代码在Shader中也可以正常使用深度图,有知道原因的同学可以告诉我下哈。
贴花效果的原理是建立一个立方体物体作为贴花物体(也有使用球体的),在贴花物体和被贴花的物体相交的XZ平面计算UV,显示贴花图案。具体的逻辑如下:
1. 在顶点着色器中记录顶点在视空间的坐标(即相机到该点的方向向量,因为相机在视空间的原点)
2. 在片元着色器中根据远裁切平面的距离(_ProjectionParams.z)和深度值重建片元在视空间的坐标
3. 再根据unity_CameraToWorld和unity_WorldToObject矩阵计算出片元在模型空间的坐标
4. 把模型空间坐标的xz分量映射到 [0,1] 区间,作为UV去读取贴花图案
代码截图:
代码中需要注意的一些地方:
1.计算视空间方向时要乘以 float3(-1,-1,1),这一点没有想明白,试了其他值效果不对
2. i.ray = i.ray * (_ProjectionParams.z / i.ray.z); 是为了求在当前ray方向上延伸到摄像机远平面位置的向量,这张图能清晰地说明问题,图片出自 这篇文章
3. o.screenUV = ComputeScreenPos (o.pos); 计算结果的xy分量到片元着色器中需要除以w分量才能使用,除以w后xy分量在[0,1]区间,用来作为UV去读取_CameraDepthTexture。为什么在frag除以w可以参考 文章
4. clip (float3(0.5,0.5,0.5) - abs(opos.xyz)) 的意思是剔除在物体外的片元,opos为转换到模型空间下的坐标,该模型是一个立方体,其模型空间坐标范围是 [-0.5, 0.5]。
5. depth = Linear01Depth (depth); 是为了得到线性的深度值,为了 float4 vpos = float4(i.ray * depth,1) 计算时能够得到正确的向量。SAMPLE_DEPTH_TEXTURE 方法取得的深度值是非线性的。参考文章。
6. float2 texUV = opos.xz + 0.5; 把坐标映射到 [0, 1] 区间,这里使用xz坐标,因为贴花要显示在xz平面上。
效果图:
考虑y方向偏移的贴花
在摆弄贴花物体时发现在拐角和边缘处显示效果不对,出现图片边缘被clamp的效果,如图:
原因在于在计算纹理坐标时 (float2 texUV = opos.xz + 0.5;)没有考虑y方向的变化,导致在边缘处的片元xz坐标都一样,和clamp对纹理坐标的处理一样。这种情况可以通过把贴花旋转一定的角度来消除,像这样(x轴旋转了-50度):
也可以通过使用法线图来确定模型空间坐标在y方向上的偏差,使这部分偏差参与到UV的计算中,主要步骤有:
1. 求出视空间的深度值和法线(通过_CameraDepthNormalsTexture属性和DecodeDepthNormal方法)
2. 把法线转换到模型空间
3. 把模型空间法线和模型空间Up方向(float3(0,1,0))点乘,求出垂直方向上的偏移程度,
4. 把偏移程度加入到UV的计算中
Shader代码:
C#中需要加上 mainCam.depthTextureMode = DepthTextureMode.DepthNormals;, 这样可以在Shader中使用 _CameraDepthNormalsTexture 属性,结合 DecodeDepthNormal 方法可以获取到视空间中的深度值和法线。官方文档。
效果图:
效果图中Scene窗口部分的贴花看起来很扭曲,非常不对,Game窗口的部分变化不大,但是也能看到有一些锯齿存在,关于这个问题我查了一些文章,大概猜测是深度值精度问题导致,_CameraDepthNormalsTexture 中的RG通道用来存储法线信息(16位),BA通道用来存储深度值(16位),而 _CameraDepthTexture 中32位都用来存储深度,所以通过 _CameraDepthTexture 读取的深度值比通过 _CameraDepthNormalsTexture 读取的精确度更高。
但是y方向偏移的方法也有一些问题,比如要通过贴花物体的y坐标来控制垂直部分纹理显示的多少,还有在计算decalUV时要考虑到X和Z两个方向坐标对y偏移的计算,上述例子的代码中为了简便只给Z方向上考虑了Y的偏移,可以在Shader中设置一个Enum,在场景同学摆放贴花时根据摆放位置来控制具体在哪个方向上考虑Y偏移,那么这样一来其实也可以直接用第一种常规方式来实现,反正都需要人工干预,而且第一种方式还少进行了一次矩阵乘法和点乘。
HDRP中的贴花效果
原本在默认管线中工作正常的Shader在导入到使用了HDRP的工程中后效果变的很错乱,大概是这样:
直观感觉应该是深度值的原因导致重建世界坐标时出现了错误,在用Frame Debug查看具体的渲染过程后发现深度图是这样的:
在HDRP中深度图的存储有点类似于一个mipmap的图集,里面存储了不同分辨率的多张深度图,所以使用 SAMPLE_DEPTH_TEXTURE 方法去获取深度值时得到的数据是错误的。那么现在面对问题就变成了 “如何在HDRP中正确的获得深度值”。这个 提问 里也遇到了同样的问题,回答中的解决方案是使用 ShaderVariables.hlsl 文件中的 SampleCameraDepth 方法,于是我就按照这个方法去做了,在Shader中引用了这个hlsl文件:
在frag中增加了 float depth = SampleCameraDepth(uv);,然后出现报错:
把pass中的CGPROGRAM 和 ENDCG 替换成 HLSLPROGRAM 和 ENDHLSL,报错变成了:
按照报错提示又修改了UnityShaderUtilities.cginc文件中的宏,这次又报错:
连 fixed4 类型都要未识别了??到此我觉着应该是走错了方向,如何在HLSLPROGRAM和ENDHLSL中正确的写代码可能是另一个话题了。而在Shader中如何使用.hlsl文件中的方法,或者更具体的在HDRP中怎么获取深度值,还需要再继续研究下,目前搜了一大堆文章和网页并没有确切的答案。既然使用自己编写的Shader实现贴花这条路卡住了,那么现在应该换一种方式,即使用Unity HDRP中自带的贴花组件,Decal Projector Component。
HDRP自带的贴花组件
HDRP中新增的贴花组件让贴花效果的实现变的非常方便,新建一个空物体,然后把 Decal Projector Component 组件添加到物体上,指定上贴花图案就可以显示出贴花,还可以使用法线图使贴花产生凹凸感,使用遮罩图用来控制法线生效的区域。
DBufferRender
在Frame Debug中查看渲染过程,发现Decal的渲染在 DBufferRender 中进行,最终显示到屏幕上的每个Decal都会对应在 DBufferRender 对应一个 Draw Mesh 事件,其使用的Shader是 HDRenderPipeline/Decal,使用的pass是 DBufferProjector_S。
该pass也用到了_CameraDepthTexture,为了搞清楚这个Shader是怎么成功获取深度值的,我查看了 HDRenderPipeline/Decal 的代码,在跳转了一系列文件后终于找到了对应的vert 和 frag方法,都在 ShaderPassDBuffer.hlsl 文件中。
可以看到虽然使用的方法和一些宏变了,但是大致思路还是一样的,也是根据深度值还原世界坐标,再到模型空间坐标(这个方法里叫Decal Space,用 positionDS变量表示)。
float depth = LOAD_TEXTURE2D(_CameraDepthTexture, input.positionSS.xy).x; 这句话是用来获取深度值的,终于看到了在HLSL中获取深度的方法了,但是很遗憾我在自定义的Shader中使用这行代码时又发生了一系列目前还不能解决的报错,我的感觉是使用HLSL文件和常见的cginc文件的差异还是很大的,并不像想象的那么无缝衔接。
DBuffer Normal
在 DBufferRender 事件后面是 DBuffer Normal,其中只有一个 Draw Procedural 子事件,使用的Shader是 Hidden/HDRenderPipeline/Material/Decal/DecalNormalBuffer,同样的,查看Shader源码。
这个Shader文件还是比较友好的,少量的几个#include文件,vert和frag方法也都在当前文件里,而不是跳转到其他的包含文件中,vert方法中不是常规的 UnityObjectToClipPos 操作,而是获取 全屏三角形的顶点位置和纹理坐标,感觉像是一个屏幕后处理类似的操作,但是全屏的话不应该是两个三角形4个顶点吗,在Frame Debug中查看是3个顶点,这就有点搞不懂了。
frag中主要操作是:
1. 从GBuffer中获取法线信息
2. 把GBuffer的法线和贴花组件中指定的法线图的法线叠加一下,这样贴花的法线就可以影响物体表面的表现了
3. 最后把修改后的法线再Encode到GBuffer中
总结:
1. 在使用默认渲染管线的工程中,可以使用自定义的Shader来实现贴花。
2. 在使用HDRP的工程中使用HDRP自带的 Decal Projector Component。以后如果研究明白了正确获取深度值的方法后可以尝试使用自定义Shader。
3. 目前关于DBuffer相关的资料很少,基本没有查到什么可用的信息,建议可以通过查看SRP的源码,包括CoreRP,LWRP,HDRP中的C#代码,Shader文件以及HLSL文件,源码地址。
4. 鉴于贴花在物体拐角和边缘处表现的不是很好,建议的使用方式是在离线时布置好贴花的位置旋转和缩放,这样可以根据不同物体的旋转缩放等条件来调整贴花物体,来达到良好的表现。尽量避免在运行时动态生成,或者只在有限的场景条件里动态生成,比如平地,墙面之类,以减少不确定性以及避免出现预期以外的奇怪效果。
参考链接:
https://blog.csdn.net/NotMz/article/details/78712346
https://forum.unity.com/threads/camera-depth-texture-sampling-with-2018-3-and-hdrp-4-x-mip-map-issue.594160/
https://forum.unity.com/threads/decodedepthnormal-linear01depth-lineareyedepth-explanations.608452/
https://forum.unity.com/threads/hdrp-how-to-render-anything-custom.592093/
https://docs.unity3d.com/Manual/SL-CameraDepthTexture.html
https://docs.unity3d.com/Manual/SL-DepthTextures.html
https://docs.unity3d.com/Manual/SL-DepthTextures.html
https://github.com/Unity-Technologies/ScriptableRenderPipeline
https://forum.unity.com/threads/accessing-depth-rendertexture-in-hdrp-and-pass-it-to-compute-shaders.539003/
https://docs.unity3d.com/Manual/SL-ShaderPrograms.html