1. 程式人生 > 實用技巧 >GraphicsLab Project 之 Screen Space Planar Reflection

GraphicsLab Project 之 Screen Space Planar Reflection

作者:i_dovelemon

日期:2020-06-23

主題:Screen Space Planar Reflection, Compute Shader

引言

前段時間,同事發來一篇講述特化版本的 Screen Space Reflection 實現 Planar Reflection 的文章。出於好奇,實驗了下,看看效果如何。如下是目前實現出來的基礎版本的效果:

原理

對於上圖來說, Water Plane 表示水面,上半部分為實際場景的山體,下半部分為以水面為映象進行反射之後的山體效果。

對於山體上某一個點(圖中白色點)來說,它對應的映象點為黃色點。

我們可以從 Screen Position

以及 Depth Texture 資訊,計算出來白點的世界座標位置 WorldPosition

然後可以以 Water Plane 所在的平面對該 WorldPosition作映象操作,得到 ReflectionPosition

得到 ReflectionPosition 之後,我們就能夠計算出來 ReflectionPostion 所對應的螢幕座標 Reflection Screen Position

根據前面的操作,我們就可以知道,此時 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 << | SrcPosPixel.x;
int dotCare = ;
InterlockedMin(HashResult[ReflPosPixel], Hash, dotCare);

Encode and Sort

孔洞

根據先前演演算法的描述,我們知道,我們先要根據 Depth 資訊和 Screen Position 資訊計算出 World Positon,然後映象之後,在轉化為新的螢幕座標。在這一系列操作中,由於數值計算的不精確性,導致有些地方沒有儲存到有效的反射點位置資訊,從而導致最終顯示時畫面上有孔洞的情況,如下圖所示:

幸運的是,從結果看這些孔洞並不會聚集在一起,形成大塊的黑塊。對於這種情況,我們只要在生成反射貼圖的時候,檢測到沒有儲存有效位置資訊時,遍歷下週圍的畫素,尋找到一個擁有有效畫素的值即可解決這個問題,如下程式碼所示:

        uint Hash = HashTexture[id.xy].x;
if (Hash == 0x0FFFFFFF)
Hash = HashTexture[uint2(id.x, id.y + )].x;
if (Hash == 0x0FFFFFFF)
Hash = HashTexture[uint2(id.x, id.y - )].x;
if (Hash == 0x0FFFFFFF)
Hash = HashTexture[uint2(id.x + , id.y)].x;
if (Hash == 0x0FFFFFFF)
Hash = HashTexture[uint2(id.x - , id.y)].x; if (Hash != 0x0FFFFFFF)
{
uint x = Hash & 0xFFFF;
uint y = Hash >> ;
ReflectionTexture[id.xy] = ColorTexture[uint2(x, y)];
}
else
{
ReflectionTexture[id.xy] = float4(0.0f, 0.0f, 0.0f, 0.0f);
}

Hole

如下是修正孔洞之後的效果:

實現

本文的程式碼是使用 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(, , )]
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(, , )]
void SSPRHash_Main(uint3 id : SV_DispatchThreadID)
{
for (uint i = ; i < DownSampleFactor; i++)
{
for (uint j = ; 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, )).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 << | SrcPosPixel.x;
int dotCare = ;
InterlockedMin(HashResult[ReflPosPixel], Hash, dotCare);
}
}
}
}
}
} //------------------------------------------------------------------------------
Texture2D<int> HashTexture;
Texture2D<float4> ColorTexture;
RWTexture2D<float4> ReflectionTexture; [numthreads(, , )]
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 + )].x;
if (Hash == 0x0FFFFFFF)
Hash = HashTexture[uint2(id.x, id.y - )].x;
if (Hash == 0x0FFFFFFF)
Hash = HashTexture[uint2(id.x + , id.y)].x;
if (Hash == 0x0FFFFFFF)
Hash = HashTexture[uint2(id.x - , id.y)].x; if (Hash != 0x0FFFFFFF)
{
uint x = Hash & 0xFFFF;
uint y = Hash >> ;
ReflectionTexture[id.xy] = ColorTexture[uint2(x, y)];
}
else
{
ReflectionTexture[id.xy] = float4(0.0f, 0.0f, 0.0f, 0.0f);
}
}
}

ScreenSpacePlanarReflection

結論

本文只是探索這個方法的可能性,更加複雜的實現,更加高效的優化可以參考文獻[4][5],這也是本文主要參考的物件。

相比於傳統的繪製場景兩邊的方法來說,這個方案的效能更加高效,同時也沒有 SSR 那樣的高需求。在條件滿足的情況下,使用該方案能夠帶來顯著的效果提升,推薦可以嘗試。

完整程式碼在這裡:https://github.com/idovelemon/UnityProj/tree/master/ScreenSpacePlanarReflection

參考文獻

[1] HLSL-InterlockedMax

[2] HLSL-InterlockedMin

[3] GL.GetGPUProjectionMatrix

[4] Screen Space Planar Reflection

[5] Optimized Pixel Projected Reflections for Planar Reflectors