GraphicsLab Project 之 Screen Space Planar Reflection

做者: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 Positionui

        根据前面的操做,咱们就能够知道,此时 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);
Encode and Sort

孔洞

        根据先前算法的描述,咱们知道,咱们先要根据 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);
        }
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(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);
        }
    }
}
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

相关文章
相关标签/搜索