做者:i_dovelemonhtml
日期:2020-06-23git
主題:Screen Space Planar Reflection, Compute Shadergithub
前段時間,同事發來一篇講述特化版本的 Screen Space Reflection 實現 Planar Reflection 的文章。出於好奇,實驗了下,看看效果如何。以下是目前實現出來的基礎版本的效果:算法
對於上圖來講, Water Plane 表示水面,上半部分爲實際場景的山體,下半部分爲以水面爲鏡像進行反射以後的山體效果。windows
對於山體上某一個點(圖中白色點)來講,它對應的鏡像點爲黃色點。ide
咱們能夠從 Screen Position 以及 Depth Texture 信息,計算出來白點的世界座標位置 WorldPosition。性能
而後能夠以 Water Plane 所在的平面對該 WorldPosition 做鏡像操做,獲得 ReflectionPosition。優化
獲得 ReflectionPosition 以後,咱們就可以計算出來 ReflectionPostion 所對應的屏幕座標 Reflection Screen Position。ui
根據前面的操做,咱們就能夠知道,此時 Reflection Screen Position 所反射的顏色即爲 Screen Positon 所表示的顏色。編碼
基礎原理十分簡單,可是實際實現的時候,會發現有不少問題。接下里一一講述。
根據上面的原理,能夠想到,有多個像素可能會被反射到相同的位置,以下圖所示:
這樣因爲 GPU 執行順序的不肯定性,就會致使畫面出現閃爍,以下所示:
針對這樣的問題,咱們實際須要的反射點是最近的反射點。能夠考慮使用 HLSL 中提供的 InterlockedMin/InterlockedMax (參考[1],[2]) 之類的指令,在寫入數據時進行大小比較,從而實現保存最近反射點的功能。
前面的指令雖然可以實現大小比較,以此進行排序。可是根據前面的描述,咱們實際保存的是反射點的顏色。沒有辦法只根據顏色進行排序,因此咱們須要保存其餘便於排序的信息,這裏選擇使用反射點的 Screen Position。而且按照以下方式進行編碼,從而實現獲取最近反射點的效果:
uint2 SrcPosPixel = uint2(DepthPos.x, DepthPos.y); uint2 ReflPosPixel = ReflPosUV * uint2(ReflectWidth, ReflectHeight); int Hash = SrcPosPixel.y << 16 | SrcPosPixel.x; int dotCare = 0; InterlockedMin(HashResult[ReflPosPixel], Hash, dotCare);
根據先前算法的描述,咱們知道,咱們先要根據 Depth 信息和 Screen Position 信息計算出 World Positon,而後鏡像以後,在轉化爲新的屏幕座標。在這一系列操做中,因爲數值計算的不精確性,致使有些地方沒有存儲到有效的反射點位置信息,從而致使最終顯示時畫面上有孔洞的狀況,以下圖所示:
幸運的是,從結果看這些孔洞並不會彙集在一塊兒,造成大塊的黑塊。對於這種狀況,咱們只要在生成反射貼圖的時候,檢測到沒有保存有效位置信息時,遍歷下週圍的像素,尋找到一個擁有有效像素的值便可解決這個問題,以下代碼所示:
uint Hash = HashTexture[id.xy].x; if (Hash == 0x0FFFFFFF) Hash = HashTexture[uint2(id.x, id.y + 1)].x; if (Hash == 0x0FFFFFFF) Hash = HashTexture[uint2(id.x, id.y - 1)].x; if (Hash == 0x0FFFFFFF) Hash = HashTexture[uint2(id.x + 1, id.y)].x; if (Hash == 0x0FFFFFFF) Hash = HashTexture[uint2(id.x - 1, id.y)].x; if (Hash != 0x0FFFFFFF) { uint x = Hash & 0xFFFF; uint y = Hash >> 16; ReflectionTexture[id.xy] = ColorTexture[uint2(x, y)]; } else { ReflectionTexture[id.xy] = float4(0.0f, 0.0f, 0.0f, 0.0f); }
以下是修正孔洞以後的效果:
本文的代碼是使用 Unity 實現的,實現起來比較簡單。比較坑的地方在於 Unity 裏面獲取 Projection Matrix 要經過 GL.GetGPUProjectionMatrix (文獻[3]) 轉化一下才能變成傳遞到 GPU 上用於渲染的投影矩陣。以下是功能核心的 Compute Shader 代碼:
// Each #kernel tells which function to compile; you can have many kernels #pragma enable_d3d11_debug_symbols #pragma kernel SSPRClear_Main #pragma kernel SSPRHash_Main #pragma kernel SSPRResolve_Main //----------------------------------------------------------------- float4x4 VPMatrix; float4x4 InvVPMatrix; uint Width; uint Height; uint ReflectWidth; uint ReflectHeight; //-------------------------------------------------------------------- RWTexture2D<int> ClearHashTexture; [numthreads(8, 8, 1)] void SSPRClear_Main(uint3 id : SV_DispatchThreadID) { if (id.x < ReflectWidth && id.y < ReflectHeight) { ClearHashTexture[id.xy] = 0x0FFFFFFF; } } //--------------------------------------------------------------- Texture2D<float> DepthTex; RWTexture2D<int> HashResult; #define DownSampleFactor (1) float3 Unproject(float3 clip) { float4 clipW = float4(clip, 1.0f); clipW = mul(InvVPMatrix, clipW); clipW.xyz = clipW.xyz / clipW.w; return clipW.xyz; } float2 Project(float3 world) { float4 worldW = float4(world, 1.0f); worldW = mul(VPMatrix, worldW); worldW.xy = worldW.xy / worldW.w; worldW.xy = (worldW.xy + float2(1.0f, 1.0f)) / 2.0f; return worldW.xy; } [numthreads(8, 8, 1)] void SSPRHash_Main(uint3 id : SV_DispatchThreadID) { for (uint i = 0; i < DownSampleFactor; i++) { for (uint j = 0; j < DownSampleFactor; j++) { uint2 DepthPos = uint2(id.x * DownSampleFactor + i, id.y * DownSampleFactor + j); if (DepthPos.x < Width && DepthPos.y < Height) { float depth = DepthTex.Load(int3(DepthPos.x, DepthPos.y, 0)).x; if (depth > 0.0f) { float2 uv = (DepthPos.xy * 1.0f) / float2(Width, Height); uv = uv * 2.0f - float2(1.0f, 1.0f); uv.y = -uv.y; float3 PosWS = Unproject(float3(uv, depth)); if (PosWS.y > 0.0f) { float3 ReflPosWS = float3(PosWS.x, -PosWS.y, PosWS.z); float2 ReflPosUV = Project(ReflPosWS); uint2 SrcPosPixel = uint2(DepthPos.x, DepthPos.y); uint2 ReflPosPixel = ReflPosUV * uint2(ReflectWidth, ReflectHeight); int Hash = SrcPosPixel.y << 16 | SrcPosPixel.x; int dotCare = 0; InterlockedMin(HashResult[ReflPosPixel], Hash, dotCare); } } } } } } //------------------------------------------------------------------------------ Texture2D<int> HashTexture; Texture2D<float4> ColorTexture; RWTexture2D<float4> ReflectionTexture; [numthreads(8, 8, 1)] void SSPRResolve_Main(uint3 id : SV_DispatchThreadID) { if (id.x < ReflectWidth && id.y < ReflectHeight) { uint Hash = HashTexture[id.xy].x; if (Hash == 0x0FFFFFFF) Hash = HashTexture[uint2(id.x, id.y + 1)].x; if (Hash == 0x0FFFFFFF) Hash = HashTexture[uint2(id.x, id.y - 1)].x; if (Hash == 0x0FFFFFFF) Hash = HashTexture[uint2(id.x + 1, id.y)].x; if (Hash == 0x0FFFFFFF) Hash = HashTexture[uint2(id.x - 1, id.y)].x; if (Hash != 0x0FFFFFFF) { uint x = Hash & 0xFFFF; uint y = Hash >> 16; ReflectionTexture[id.xy] = ColorTexture[uint2(x, y)]; } else { ReflectionTexture[id.xy] = float4(0.0f, 0.0f, 0.0f, 0.0f); } } }
本文只是探索這個方法的可能性,更加複雜的實現,更加高效的優化能夠參考文獻[4][5],這也是本文主要參考的對象。
相比於傳統的繪製場景兩邊的方法來講,這個方案的性能更加高效,同時也沒有 SSR 那樣的高需求。在條件知足的狀況下,使用該方案可以帶來顯著的效果提高,推薦能夠嘗試。
完整代碼在這裏:https://github.com/idovelemon/UnityProj/tree/master/ScreenSpacePlanarReflection
[4] Screen Space Planar Reflection
[5] Optimized Pixel Projected Reflections for Planar Reflectors