最近有個功能, 要渲染從主相機視角看到的另外一個相機的可視範圍和不可見範圍, 大概以下圖 : ide
簡單來講就是主相機視野和觀察者相機視野重合的地方, 能標記出觀察者相機的可見和不可見, 實現原理就跟 ShadowMap 同樣, 就是有關深度圖, 世界座標轉換之類的, 每次有此類的功能都會很悲催, 雖然它的邏輯很簡單, 但是用Unity3D作起來很麻煩...測試
原理 : 在觀察者相機中獲取深度貼圖, 儲存在某張 RenderTexture 中, 而後在主相機中每一個片元都獲取對應的世界座標位置, 將世界座標轉換到觀察者相機viewPort中, 若是在觀察者相機視野內, 就繼續把視口座標轉換爲UV座標而後取出儲存在 RenderTexture 中的對應深度, 把深度轉換爲實際深度值後作比較, 若是深度小於等於深度圖的深度, 就是觀察者可見 不然就是不可見了.this
先來看怎樣獲取深度圖吧, 無論哪一種渲染方式, 給相機添加獲取深度圖標記便可:spa
_cam = GetComponent<Camera>();
_cam.depthTextureMode |= DepthTextureMode.Depth;
而後深度圖要精度高點的, 單通道圖便可:3d
renderTexture = RenderTexture.GetTemporary(Screen.width, Screen.height, 0, RenderTextureFormat.RFloat); renderTexture.hideFlags = HideFlags.DontSave;
以後就直接把相機的貼圖通過後處理把深度圖渲染出來便可 : code
private void OnRenderImage(RenderTexture source, RenderTexture destination) { if (_material && _cam) { Shader.SetGlobalFloat("_cameraNear", _cam.nearClipPlane); Shader.SetGlobalFloat("_cameraFar", _cam.farClipPlane); Graphics.Blit(source, renderTexture, _material); } Graphics.Blit(source, destination); }
材質就是一個簡單的獲取深度的shader : orm
sampler2D _CameraDepthTexture; uniform float _cameraFar; uniform float _cameraNear; float DistanceToLinearDepth(float d, float near, float far) { float z = (d - near) / (far - near); return z; } fixed4 frag(v2f i) : SV_Target { float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv); depth = LinearEyeDepth(depth); depth = DistanceToLinearDepth(depth, _cameraNear, _cameraFar); return float4(depth, depth, depth, 1); }
這裏有點奇怪吧, 爲何返回的不是深度貼圖的深度, 也不是 Linear01Depth(depth) 的歸一化深度, 而是本身計算出來的一個深度?blog
這是由於缺乏官方文檔........其實看API裏面有直接提供 RenderBuffer 給相機的方法 : ip
_cam.SetTargetBuffers(renderTexture.colorBuffer, renderTexture.depthBuffer);
但是沒有文檔也沒有例子啊, 鬼知道你渲染出來我怎麼用啊, 還有 renderTexture.depthBuffer 到底怎樣做爲 Texture 傳給 shader 啊... 我以前嘗試過它們之間經過 IntPtr 做的轉換操做, 都是失敗...文檔
因而就只能用最穩妥的方法, 經過 Shader 渲染一張深度圖出來吧, 而後經過 SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv); 獲取到的深度值, 應該是 NDC 座標, 深度值的範圍應該是[0, 1]之間吧(非線性), 若是在其它相機過程當中獲取實際深度的話, 就須要本身實現 LinearEyeDepth(float z) 這個方法:
inline float LinearEyeDepth( float z ) { return 1.0 / (_ZBufferParams.z * z + _ZBufferParams.w); }
而在不一樣平臺, _ZBufferParams 裏面的值是不同的, 因此若是實現它的話會很麻煩, 而且通過個人測試, 結果是不對的......
double x, y; OpenGL would be this: x = (1.0 - m_FarClip / m_NearClip) / 2.0; y = (1.0 + m_FarClip / m_NearClip) / 2.0; D3D is this: x = 1.0 - m_FarClip / m_NearClip; y = m_FarClip / m_NearClip; _ZBufferParams = float4(x, y, x/m_FarClip, y/m_FarClip);
PS : 使用觀察者相機的 nearClipPlane, farClipPlane 計算出來的深度也是不正確的, 不知道爲何......
因此就有了我本身計算歸一化深度的方法了:
float DistanceToLinearDepth(float d, float near, float far) { float z = (d - near) / (far - near); return z; }
簡單好理解, 這樣就得到了我本身計算的深度圖, 只須要觀察者相機的 far/near clip panel 的值便可. 因此能夠看出, 因爲缺乏官方文檔等緣由, 作一個簡單的功能每每會很花費時間, 說真的搜索了不少例子, 直接代碼Shader複製過來都不能用, 簡直了.
接下來是難點的主相機渲染, 主相機須要作這幾件事 :
1. 經過主相機深度圖, 獲取當前片元的世界座標位置
2. 經過把世界座標位置轉換到觀察者相機視口空間, 檢測是否在觀察者相機視口以內.
3. 不在觀察者相機視口以內, 正常顯示.
4. 在觀察者相機視口以內的部分, 將視口座標轉換成觀察者相機的 UV 座標, 而後對觀察者相機渲染出來的深度圖進行取值, 變換爲實際深度值而後進行深度比較, 若是片元在觀察者相機視口的座標深度小於等於深度圖深度, 則是可見部分, 其它是不可見部分.
主相機也設置打開深度圖渲染 :
_cam = GetComponent<Camera>();
_cam.depthTextureMode |= DepthTextureMode.Depth;
那麼怎樣經過片元獲取世界座標位置呢? 最穩妥的是參考官方 Fog 相關 Shader, 由於使用的是後處理, 是沒法在頂點階段計算世界座標位置後經過插值在片元階段得到片元座標位置的, 咱們能夠經過視錐的4個射線配合深度圖來進行世界座標還原. 下圖表示了視錐體:
後處理的頂點過程能夠當作是渲染ABCD組成的近裁面的過程, 它只有4個頂點, 若是咱們能夠將OA, OB, OC, OD 四條射線存入頂點程序, 那麼在片元階段就能得到自動插值的相機到片元對應的近裁面的射線, 由於深度值 z 是一個非透視值, 而 x, y 是通過透視變換的, 那麼恰好四個射線方向就符合逆透視變換了(Z在向量插值中不變), 如今射線的方向能肯定, 但是射線的長度不符合計算需求. 咱們須要對它們進行調整.
看圖中 M 點, 它是 ABCD 的中心, 因此若是在 OM 的方向上得到一個單位向量, 而後在它的截面上獲取 OA, OB, OC, OD 方向上相應的射線, 就能獲得符合計算需求的視錐向量了. 如 OM 的長度爲1, 好比 OA 的長度爲1.7, 那麼深度爲1的片元, 在 OM 上就落在 1*OM -> M 點上, 在 OA 上就落在1*OA -> A 點上, 造成了落在同一截面上, 就能獲得原始世界座標了.
最後作出來的只是基本功能, 並無對距離深度作樣本估計, 因此會像硬陰影同樣有鋸齒邊, 還有像沒開各項異性的貼圖同樣有撕扯感, 這些都須要進行處理...