URP – Render feature实现镜面反射

1 前言

镜面反射(Planar reflection)是游戏中比较常用的效果,例如角色的展示界面的地面倒影,物体在水面的倒影,光滑的表面等。

1.1 利用反射摄像机实现镜面反射

在URP中常规的镜面反射一般使用镜头方案来实现,也就是创造一个反射镜头,按照反射平面的位置翻转镜头,渲染一遍反射物体到RT,在绘制水面的时候,采样这个RT进行着色。

1.2 利用SSR实现镜面反射

SSR目前在URP中一般使用步进或compute shader来进行实现,步进的性能在移动端基本无法接受。而compute shader存在一定兼容性问题,需要配合例如vulkan之类的现代图形api才能获得较好的效果。

1.3 利用一个render object pass来实现镜面反射

相对于前两种方案而言,这个方案更为简单,相比镜头方案也有更好的性能,易于理解,本文主要分析这个方案,探讨一下该方案的实现。

2 Render Feature实现镜面反射

我们知道镜头方案的反射是通过翻转摄像机得到的,那么利用相同的思路我们也可以通过翻转要反射的物体来实现镜面反射。

在一开始实现这个render feature之前,也尝试了直接翻转镜头来实现,也就是在画反射平面之前,翻转一次镜头,绘制要反射的物体,再恢复摄像机原来的transform正常绘制其他物体和反射平面。但在URP下实现非常不理想,在render feature中我们能做的就是在camera setup和execute等回调函数中实现相应的翻转逻辑,而这样实现会出现闪烁的现象,猜测是一帧之内由引擎传入的各种矩阵是确定的,在一帧之内改变同一个镜头的位置不被urp支持,或是由于command buffer的渲染命令出于优化目的并非是立即执行的,所以翻转镜头的逻辑时序并不奏效。

因此,我们这里采用翻转要反射的物体进行实现。

通过绘制需要反射物体的render feature比较容易,我们首先需要一个绘制物体的render feature本身,然后在shader上额外增加一个pass来专门处理绘制反射物体。

2.1 Render feature的功能

首先render feature需要能够绘制翻转的物体,因此他的参数应该和urp内置的render objects的render feature相似,功能上比较简单:

这个render feature具有根据layer mask来过滤绘制物体的特征,也可以通过降采样和改变RT的格式来进行优化。而他绘制的层级中的物体应当具有一个reflection pass来处理反射的绘制,在这个绘制中我们对物体的坐标根据平面的位置进行翻转,再将高于平面的部分clip掉,即可得到一张反射的RT了。

2.2 绘制反射的shader

对于需要被反射的物体,我们需要额外增加一个reflection pass来处理。

首先需要明确的是我们需要在reflection pass中实现这两个功能:

根据反射平面反转物体

去除大于反射平面的部分

下面我们直接来看shader的结构:

Shader "ExampleShaderStructure"
{
    Properties
    {
       //Properties...
    }

    SubShader
    {
        //Global tags...
        //Variants...

        Pass
        {
            Name "ForwardPass"
            Tags{"LightMode" = "UniversalForward"}

            //Render state....

            HLSLPROGRAM

            //Variants...           

            #include "ForwardPass.hlsl"
            ENDHLSL
        }
        
        Pass
        {
            Name "ReflectionPass"
            Tags{"LightMode" = "ReflectionPass"}

            Cull Front
            //Render state....

            HLSLPROGRAM
            
            //Variants...          

			#define REFLECTION_PASS 1

            #include "ForwardPass.hlsl"

            ENDHLSL
        }

        //Othes passes...
    }

    FallBack "Hidden/Universal Render Pipeline/FallbackError"
    //Shader editor...
}

这里我们重用了forward pass的include代码forwardpass.hlsl,而在reflectionpass中额外做了一个宏定义来在forwardpass中增加镜面翻转的逻辑。

下面我们来看forwardpass中如何实现镜面翻转:

half _ReferencePlaneY;

struct Attributes
{
    float4 positionOS   : POSITION;
    //Other parameters...
}

struct Varyings
{
#if defined(REFLECTION_PASS)
    float3 reversedPositionWS       : TEXCROOD8;
#endif
    float4 positionCS               : SV_POSITION;
    //Other parameters...
}

Varying VertexShader()
{
    Varyings output = (Varyings)0;

    //...

#if defined(REFLECTION_PASS)
    float3 positionWS = output.positionWS;
    positionWS.y = _ReferencePlaneY - (positionWS.y - _ReferencePlaneY);
    output.reversedPositionWS = positionWS;
    output.positionCS = TransformWorldToHClip(positionWS);
#else
    output.positionCS = vertexInput.positionCS;
#endif

    return output;
}

half4 FragmentShader()
{
    half4 finalColor;
    #if defined(REFLECTION_PASS)
    clip(_ReferencePlaneY - input.reversedPositionWS.y);
    #endif

    //....

    return finalColor;
}

这里仅考虑了最简单的情况,也就是镜面反射的平面和XOZ平面平行的情况,这个时候我们仅需要根据Y对世界坐标进行修改即可。把翻转后的世界空间坐标作为参数传入fragment shader来处理高于镜面反射平面剔除的逻辑。而真正的世界空间坐标并没有改变,因此正常的光照依然可以执行。

需要注意reflection pass这里需要cull front来正确的执行剔除。

从frame buffer中可以看到渲染的RT

 对比原来的场景

3 总结

相比于传统镜头方案,相比于镜头上可能存在的冗余的render feature挂载,多余的blit或者管线上的镜头逻辑设置,这个方案更为轻量级,仅适用一个pass就可以实现。

同时他可以通过降采样,修改rt格式,简化reflection pass中的光照实现来进一步优化性能。

而相比于compute shader实现的平面镜面反射,他又可以绘制出不在镜头中的部分,同时又有更好的兼容性。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>