貼花效果,就和名字的直接意思類似,把一張圖貼到另一個(gè)物體上顯示,經(jīng)常被用于表現(xiàn)一些重復(fù)出現(xiàn)的圖案,比如彈孔,涂鴉,污漬等。效果圖:

常規(guī)貼花實(shí)現(xiàn)
Unity官方提供了一個(gè)工程,這個(gè)工程主要是用來說明CommandBuffer是怎么使用的,其中有貼花的一些展示,主要是用CommandBuffer在Deferred渲染路徑下實(shí)現(xiàn)貼花效果。使用CommandBuffer是因?yàn)樾枰袯uiltinRenderTextureType.GBuffer2中存儲的法線信息傳給Shader,而這次測試主要為了驗(yàn)證原理,不使用法線信息,所以可以不用CommandBuffer(即使使用法線信息也可以在Shader中通過_CameraDepthNormalsTexture結(jié)合DecodeDepthNormal方法來獲取到法線信息)。原工程通過 cam.AddCommandBuffer (CameraEvent.BeforeLighting, buf); 這句實(shí)現(xiàn)把CommandBuffer插入到延遲渲染的光照計(jì)算Pass前面,也可以去掉不用。所以原工程的C#代碼基本可以不使用,在Forward渲染路徑下,完全在Shader中實(shí)現(xiàn)貼花效果。之前看文檔說如果Shader中使用深度圖的話需要在C#代碼中設(shè)置相機(jī)的depthTextureMode,即 mainCam.depthTextureMode = DepthTextureMode.Depth;,但是我試了下不寫這行代碼在Shader中也可以正常使用深度圖,有知道原因的同學(xué)可以告訴我下哈。
貼花效果的原理是建立一個(gè)立方體物體作為貼花物體(也有使用球體的),在貼花物體和被貼花的物體相交的XZ平面計(jì)算UV,顯示貼花圖案。具體的邏輯如下:
1. 在頂點(diǎn)著色器中記錄頂點(diǎn)在視空間的坐標(biāo)(即相機(jī)到該點(diǎn)的方向向量,因?yàn)橄鄼C(jī)在視空間的原點(diǎn))
2. 在片元著色器中根據(jù)遠(yuǎn)裁切平面的距離(_ProjectionParams.z)和深度值重建片元在視空間的坐標(biāo)
3. 再根據(jù)unity_CameraToWorld和unity_WorldToObject矩陣計(jì)算出片元在模型空間的坐標(biāo)
4. 把模型空間坐標(biāo)的xz分量映射到 [0,1] 區(qū)間,作為UV去讀取貼花圖案
代碼截圖:


代碼中需要注意的一些地方:
1.計(jì)算視空間方向時(shí)要乘以 float3(-1,-1,1),這一點(diǎn)沒有想明白,試了其他值效果不對
2. i.ray = i.ray * (_ProjectionParams.z / i.ray.z); 是為了求在當(dāng)前ray方向上延伸到攝像機(jī)遠(yuǎn)平面位置的向量,這張圖能清晰地說明問題,圖片出自 這篇文章

3. o.screenUV = ComputeScreenPos (o.pos);?計(jì)算結(jié)果的xy分量到片元著色器中需要除以w分量才能使用,除以w后xy分量在[0,1]區(qū)間,用來作為UV去讀取_CameraDepthTexture。為什么在frag除以w可以參考?文章
4. clip (float3(0.5,0.5,0.5) - abs(opos.xyz))?的意思是剔除在物體外的片元,opos為轉(zhuǎn)換到模型空間下的坐標(biāo),該模型是一個(gè)立方體,其模型空間坐標(biāo)范圍是 [-0.5, 0.5]。
5. depth = Linear01Depth (depth);?是為了得到線性的深度值,為了?float4 vpos = float4(i.ray * depth,1)?計(jì)算時(shí)能夠得到正確的向量。SAMPLE_DEPTH_TEXTURE?方法取得的深度值是非線性的。參考文章。
6. float2 texUV = opos.xz + 0.5;?把坐標(biāo)映射到 [0, 1] 區(qū)間,這里使用xz坐標(biāo),因?yàn)橘N花要顯示在xz平面上。
效果圖:

考慮y方向偏移的貼花
在擺弄貼花物體時(shí)發(fā)現(xiàn)在拐角和邊緣處顯示效果不對,出現(xiàn)圖片邊緣被clamp的效果,如圖:

原因在于在計(jì)算紋理坐標(biāo)時(shí) (float2 texUV = opos.xz + 0.5;)沒有考慮y方向的變化,導(dǎo)致在邊緣處的片元xz坐標(biāo)都一樣,和clamp對紋理坐標(biāo)的處理一樣。這種情況可以通過把貼花旋轉(zhuǎn)一定的角度來消除,像這樣(x軸旋轉(zhuǎn)了-50度):

也可以通過使用法線圖來確定模型空間坐標(biāo)在y方向上的偏差,使這部分偏差參與到UV的計(jì)算中,主要步驟有:
1. 求出視空間的深度值和法線(通過_CameraDepthNormalsTexture屬性和DecodeDepthNormal方法)
2. 把法線轉(zhuǎn)換到模型空間
3. 把模型空間法線和模型空間Up方向(float3(0,1,0))點(diǎn)乘,求出垂直方向上的偏移程度,
4. 把偏移程度加入到UV的計(jì)算中
Shader代碼:



C#中需要加上 mainCam.depthTextureMode = DepthTextureMode.DepthNormals;, 這樣可以在Shader中使用 _CameraDepthNormalsTexture 屬性,結(jié)合 DecodeDepthNormal 方法可以獲取到視空間中的深度值和法線。官方文檔。
效果圖:

效果圖中Scene窗口部分的貼花看起來很扭曲,非常不對,Game窗口的部分變化不大,但是也能看到有一些鋸齒存在,關(guān)于這個(gè)問題我查了一些文章,大概猜測是深度值精度問題導(dǎo)致,_CameraDepthNormalsTexture 中的RG通道用來存儲法線信息(16位),BA通道用來存儲深度值(16位),而 _CameraDepthTexture 中32位都用來存儲深度,所以通過 _CameraDepthTexture 讀取的深度值比通過 _CameraDepthNormalsTexture 讀取的精確度更高。
但是y方向偏移的方法也有一些問題,比如要通過貼花物體的y坐標(biāo)來控制垂直部分紋理顯示的多少,還有在計(jì)算decalUV時(shí)要考慮到X和Z兩個(gè)方向坐標(biāo)對y偏移的計(jì)算,上述例子的代碼中為了簡便只給Z方向上考慮了Y的偏移,可以在Shader中設(shè)置一個(gè)Enum,在場景同學(xué)擺放貼花時(shí)根據(jù)擺放位置來控制具體在哪個(gè)方向上考慮Y偏移,那么這樣一來其實(shí)也可以直接用第一種常規(guī)方式來實(shí)現(xiàn),反正都需要人工干預(yù),而且第一種方式還少進(jìn)行了一次矩陣乘法和點(diǎn)乘。
HDRP中的貼花效果
原本在默認(rèn)管線中工作正常的Shader在導(dǎo)入到使用了HDRP的工程中后效果變的很錯(cuò)亂,大概是這樣:

直觀感覺應(yīng)該是深度值的原因?qū)е轮亟ㄊ澜缱鴺?biāo)時(shí)出現(xiàn)了錯(cuò)誤,在用Frame Debug查看具體的渲染過程后發(fā)現(xiàn)深度圖是這樣的:

在HDRP中深度圖的存儲有點(diǎn)類似于一個(gè)mipmap的圖集,里面存儲了不同分辨率的多張深度圖,所以使用 SAMPLE_DEPTH_TEXTURE 方法去獲取深度值時(shí)得到的數(shù)據(jù)是錯(cuò)誤的。那么現(xiàn)在面對問題就變成了 “如何在HDRP中正確的獲得深度值”。這個(gè) 提問 里也遇到了同樣的問題,回答中的解決方案是使用 ShaderVariables.hlsl 文件中的 SampleCameraDepth 方法,于是我就按照這個(gè)方法去做了,在Shader中引用了這個(gè)hlsl文件:

在frag中增加了?float depth = SampleCameraDepth(uv);,然后出現(xiàn)報(bào)錯(cuò):

把pass中的CGPROGRAM?和?ENDCG?替換成?HLSLPROGRAM?和?ENDHLSL,報(bào)錯(cuò)變成了:

按照報(bào)錯(cuò)提示又修改了UnityShaderUtilities.cginc文件中的宏,這次又報(bào)錯(cuò):

連 fixed4 類型都要未識別了??到此我覺著應(yīng)該是走錯(cuò)了方向,如何在HLSLPROGRAM和ENDHLSL中正確的寫代碼可能是另一個(gè)話題了。而在Shader中如何使用.hlsl文件中的方法,或者更具體的在HDRP中怎么獲取深度值,還需要再繼續(xù)研究下,目前搜了一大堆文章和網(wǎng)頁并沒有確切的答案。既然使用自己編寫的Shader實(shí)現(xiàn)貼花這條路卡住了,那么現(xiàn)在應(yīng)該換一種方式,即使用Unity HDRP中自帶的貼花組件,Decal Projector Component。
HDRP自帶的貼花組件
HDRP中新增的貼花組件讓貼花效果的實(shí)現(xiàn)變的非常方便,新建一個(gè)空物體,然后把?Decal Projector Component?組件添加到物體上,指定上貼花圖案就可以顯示出貼花,還可以使用法線圖使貼花產(chǎn)生凹凸感,使用遮罩圖用來控制法線生效的區(qū)域。


DBufferRender
在Frame Debug中查看渲染過程,發(fā)現(xiàn)Decal的渲染在 DBufferRender 中進(jìn)行,最終顯示到屏幕上的每個(gè)Decal都會對應(yīng)在 DBufferRender 對應(yīng)一個(gè) Draw Mesh 事件,其使用的Shader是 HDRenderPipeline/Decal,使用的pass是 DBufferProjector_S。

該pass也用到了_CameraDepthTexture,為了搞清楚這個(gè)Shader是怎么成功獲取深度值的,我查看了 HDRenderPipeline/Decal 的代碼,在跳轉(zhuǎn)了一系列文件后終于找到了對應(yīng)的vert 和 frag方法,都在 ShaderPassDBuffer.hlsl 文件中。

可以看到雖然使用的方法和一些宏變了,但是大致思路還是一樣的,也是根據(jù)深度值還原世界坐標(biāo),再到模型空間坐標(biāo)(這個(gè)方法里叫Decal Space,用 positionDS變量表示)。
float depth = LOAD_TEXTURE2D(_CameraDepthTexture, input.positionSS.xy).x; 這句話是用來獲取深度值的,終于看到了在HLSL中獲取深度的方法了,但是很遺憾我在自定義的Shader中使用這行代碼時(shí)又發(fā)生了一系列目前還不能解決的報(bào)錯(cuò),我的感覺是使用HLSL文件和常見的cginc文件的差異還是很大的,并不像想象的那么無縫銜接。
DBuffer Normal
在 DBufferRender 事件后面是 DBuffer Normal,其中只有一個(gè) Draw Procedural 子事件,使用的Shader是 Hidden/HDRenderPipeline/Material/Decal/DecalNormalBuffer,同樣的,查看Shader源碼。


這個(gè)Shader文件還是比較友好的,少量的幾個(gè)#include文件,vert和frag方法也都在當(dāng)前文件里,而不是跳轉(zhuǎn)到其他的包含文件中,vert方法中不是常規(guī)的 UnityObjectToClipPos 操作,而是獲取 全屏三角形的頂點(diǎn)位置和紋理坐標(biāo),感覺像是一個(gè)屏幕后處理類似的操作,但是全屏的話不應(yīng)該是兩個(gè)三角形4個(gè)頂點(diǎn)嗎,在Frame Debug中查看是3個(gè)頂點(diǎn),這就有點(diǎn)搞不懂了。

frag中主要操作是:
1. 從GBuffer中獲取法線信息
2. 把GBuffer的法線和貼花組件中指定的法線圖的法線疊加一下,這樣貼花的法線就可以影響物體表面的表現(xiàn)了
3. 最后把修改后的法線再Encode到GBuffer中
總結(jié):
1. 在使用默認(rèn)渲染管線的工程中,可以使用自定義的Shader來實(shí)現(xiàn)貼花。
2. 在使用HDRP的工程中使用HDRP自帶的 Decal Projector Component。以后如果研究明白了正確獲取深度值的方法后可以嘗試使用自定義Shader。
3. 目前關(guān)于DBuffer相關(guān)的資料很少,基本沒有查到什么可用的信息,建議可以通過查看SRP的源碼,包括CoreRP,LWRP,HDRP中的C#代碼,Shader文件以及HLSL文件,源碼地址。
4. 鑒于貼花在物體拐角和邊緣處表現(xiàn)的不是很好,建議的使用方式是在離線時(shí)布置好貼花的位置旋轉(zhuǎn)和縮放,這樣可以根據(jù)不同物體的旋轉(zhuǎn)縮放等條件來調(diào)整貼花物體,來達(dá)到良好的表現(xiàn)。盡量避免在運(yùn)行時(shí)動(dòng)態(tài)生成,或者只在有限的場景條件里動(dòng)態(tài)生成,比如平地,墻面之類,以減少不確定性以及避免出現(xiàn)預(yù)期以外的奇怪效果。
參考鏈接:
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