初识Unity的URP并用multiple pass制作半透明物体

自从2019Unity发布URP以来,我就一直想尝试下。最近公司项目迁移到了URP管线下,终于可以开始折腾URP了。

接触一个未知的东西,当然是从它的文档开始啦!首先映入眼帘的是URP只支持单个pass,如下图

再仔细看看,这个图表的意思是对于实时光光照来说,built-in管线能支持多pass而URP不支持。那么,合理的推测就是说ForwardAdd这个LightMode没有了,额外的动态光源只会在一个pass中被计算。那么,也就是说,我在URP中可以用多pass渲染物体?

带着这个疑问,我想来实际写个东西确认下。想来想去,最简单的多pass的物体就是半透明物体了,为了渲染复杂结构的半透明物体并且保持遮挡正确,我们需要一个pass先写入深度,再在另一个pass进行渲染;或者为了看到半透明物体的内部结构,我们需要两个pass,一个画背面,一个画正面,然后这个物体还能进行描边,这样就有三个pass了。那么,这样一个三pass的物体在URP中能正确绘制么?

先来看写入深度的双pass半透明物体的情况,代码如下,基本上抄下冯乐乐前辈书里的内容,再把它翻译成hlsl就行了。

注意cg语言可以用,只是不能参与SRP Batch,有时还会编译错误造成粉色材质,所以最好用hlsl语言写shader

Pass
        {
            Tags {"LightMode"="SRPDefaultUnlit"}
            ZWrite On
            ColorMask 0 
        }



        Pass
        {
            Tags {"LightMode"="UniversalForward"}
            Blend SrcAlpha OneMinusSrcAlpha
            Cull Back
            ZWrite Off

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
            #pragma multi_compile _ _SHADOWS_SOFT
            #pragma enable_cbuffer

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            struct appdata
            {
                float4 vertex : POSITION;
                // float2 uv : TEXCOORD0;

            };

            struct v2f
            {
                // float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                float fogCoord : TEXCOORD1;
                float4 shadowCoord : TEXCOORD2;


            };

            // TEXTURE2D(_MainTex);
            // SAMPLER(sampler_MainTex);

            CBUFFER_START(UnityPerMaterial)
                half4 _MainCol;
                float _Alpha;
            CBUFFER_END

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.vertex);
                float3 worldPos = TransformObjectToWorld(v.vertex);
                // o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.fogCoord = ComputeFogFactor(o.vertex.z);
                o.shadowCoord = TransformWorldToShadowCoord(worldPos);

                return o;
            }

            half4 frag (v2f i) : SV_Target
            {
                float shadow = MainLightRealtimeShadow(i.shadowCoord);

                // sample the texture
                // half4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
                half4 col = _MainCol;
                col *= _MainLightColor * shadow;
                // apply fog
                col.rgb = MixFog(col,i.fogCoord);
                col.a = _Alpha;
                return col;
                // return half4(shadow,shadow,shadow,1);
            }
            ENDHLSL
        }

通过谷歌可以知道,想用多pass那么LightMode需要设置成SRPDefaultUnlit,或者你不写URP会默认加上这个tag,这样出来半透明物体的就对了。

而如果我们把第一个pass里面的LightMode设为UniversalForward,很遗憾,URP只走第一个pass,后面的pass就不走了o(╯□╰)o

设成UniversalForward那么只走第一个pass,只写入了深度,没有输出颜色,所以什么也没有。不过这里我有shadowcaster pass,所以阴影还在

那么,我们需要额外pass做功能时,是不是那些额外pass的LightMode都设为SRPDefaultUnlit就好了呢?我们这就来试一下,下图是个最简单的cube,正面背面分两个pass渲染,又有最简单的描边pass(有硬边的物体描边会断裂,正好比较夸张,可以看效果),最终结果这样

代码如下

Pass
        {

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : NORMAL;

            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;


            };


            v2f vert (appdata v)
            {
                v2f o;
                v.vertex.xyz += v.normal * 0.2; 
                o.vertex = TransformObjectToHClip(v.vertex);
                

                return o;
            }

            half4 frag (v2f i) : SV_Target
            {
                return half4(1,0,0,1) * _MainLightColor;
            }
            ENDHLSL
        }

        Pass
        {
            Tags {"LightMode"="SRPDefaultUnlit"}
            Blend SrcAlpha OneMinusSrcAlpha
            Cull Front
            ZWrite Off

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
            #pragma multi_compile _ _SHADOWS_SOFT
            #pragma enable_cbuffer

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;

            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                float fogCoord : TEXCOORD1;
                float4 shadowCoord : TEXCOORD2;


            };

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);

            CBUFFER_START(UnityPerMaterial)
                float4 _MainTex_ST;
                float _Alpha;
            CBUFFER_END

            v2f vert (appdata v)
            {
                v2f o;
                VertexPositionInputs vertexInput = GetVertexPositionInputs(v.vertex);
                o.vertex = TransformObjectToHClip(v.vertex);
                // float3 worldPos = TransformObjectToWorld(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.fogCoord = ComputeFogFactor(o.vertex.z);
                o.shadowCoord = GetShadowCoord(vertexInput);

                return o;
            }

            half4 frag (v2f i) : SV_Target
            {
                Light mainLight = GetMainLight(i.shadowCoord);

                // sample the texture
                half4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
                col *= _MainLightColor * mainLight.shadowAttenuation * mainLight.distanceAttenuation;
                // apply fog
                col.rgb = MixFog(col,i.fogCoord);
                col.a = _Alpha;
                return col;
            }
            ENDHLSL
        }



        Pass
        {
            Tags {"LightMode"="UniversalForward"}
            Blend SrcAlpha OneMinusSrcAlpha
            Cull Back
            ZWrite Off

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
            #pragma multi_compile _ _SHADOWS_SOFT
            #pragma enable_cbuffer

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;

            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                float fogCoord : TEXCOORD1;
                float4 shadowCoord : TEXCOORD2;


            };

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);

            CBUFFER_START(UnityPerMaterial)
                float4 _MainTex_ST;
                float _Alpha;
            CBUFFER_END

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.vertex);
                float3 worldPos = TransformObjectToWorld(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.fogCoord = ComputeFogFactor(o.vertex.z);
                o.shadowCoord = TransformWorldToShadowCoord(worldPos);

                return o;
            }

            half4 frag (v2f i) : SV_Target
            {
                float shadow = MainLightRealtimeShadow(i.shadowCoord);

                // sample the texture
                half4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
                col *= _MainLightColor * shadow;
                // apply fog
                col.rgb = MixFog(col,i.fogCoord);
                col.a = _Alpha;
                return col;
            }
            ENDHLSL
        }

可以看到我第一个pass的LightMode都没写,但也绘制了,所以说你不写LightMode和写了Tags {"LightMode"="SRPDefaultUnlit"}是一样的效果。那么,现在开个脑洞,如果所有pass都打上SRPDefaultUnlittag呢?我试了下,URP只走了第一个pass,描了个边就结束了。

所以,要保证多pass物体正确绘制,需要确保有个pass打上UniversalForwardtag,其余pass有SRPDefaultUnlittag也行,没有也行。

接下来搞投影,理论上半透明的东西可以有投影,我记得built-in管线里面我都是靠Fallback来投影的,而URP似乎。。。我看自带的Lit材质里面是FallBack "Hidden/Universal Render Pipeline/FallbackError",而用了以后也不会投影,那么应该是其他的地方做了投影了。从自带的Lit材质来看,URP中的投影需要有个pass来操作,叫ShadowCaster,代码如下

Pass
        {
            Name "ShadowCaster"
            Tags{"LightMode" = "ShadowCaster"}

            ZWrite On
            ZTest LEqual
            ColorMask 0
            Cull[_Cull]

            HLSLPROGRAM
            #pragma exclude_renderers gles gles3 glcore
            #pragma target 4.5

            // -------------------------------------
            // Material Keywords
            #pragma shader_feature_local_fragment _ALPHATEST_ON
            #pragma shader_feature_local_fragment _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A

            //--------------------------------------
            // GPU Instancing
            #pragma multi_compile_instancing
            #pragma multi_compile _ DOTS_INSTANCING_ON

            #pragma vertex ShadowPassVertex
            #pragma fragment ShadowPassFragment

            #include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/Shaders/ShadowCasterPass.hlsl"
            ENDHLSL
        }

我尝试过直接把这个pass复制黏贴到我自己写的shader中,影子倒是投了,SRP Batch compatible却挂了。。。

没辙,自己写个ShadowCaster Pass吧,代码如下

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl"


float3 _LightDirection;

TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);

CBUFFER_START(UnityPerMaterial)
    float4 _MainTex_ST;
    float _Alpha;
CBUFFER_END

struct Attributes
{
    float4 positionOS   : POSITION;
    float3 normalOS     : NORMAL;
    float2 texcoord     : TEXCOORD0;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct Varyings
{
    float2 uv           : TEXCOORD0;
    float4 positionCS   : SV_POSITION;
};

float4 GetShadowPositionHClip(Attributes input)
{
    float3 positionWS = TransformObjectToWorld(input.positionOS.xyz);
    float3 normalWS = TransformObjectToWorldNormal(input.normalOS);

    float4 positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, _LightDirection));

#if UNITY_REVERSED_Z
    positionCS.z = min(positionCS.z, positionCS.w * UNITY_NEAR_CLIP_VALUE);
#else
    positionCS.z = max(positionCS.z, positionCS.w * UNITY_NEAR_CLIP_VALUE);
#endif

    return positionCS;
}

Varyings ShadowPassVertex(Attributes input)
{
    Varyings output;
    UNITY_SETUP_INSTANCE_ID(input);

    output.uv = TRANSFORM_TEX(input.texcoord, _MainTex);
    output.positionCS = GetShadowPositionHClip(input);
    return output;
}

half4 ShadowPassFragment(Varyings input) : SV_TARGET
{
    half4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);
    clip(_Alpha * 1.5 - col.r);
    return 0;
}

这样这个阴影pass还可以根据透明度来决定阴影需不需要显示(我只是随便写了下,不能满足复杂需求),又可以SRP Batch compatible,多好O(∩_∩)O~

然后用到之前半透明材质中,就可以投影了

Pass
        {
            Name "ShadowCaster"
            Tags{"LightMode" = "ShadowCaster"}

            ZWrite On
            ZTest LEqual
            ColorMask 0
            Cull[_Cull]

            HLSLPROGRAM
            #pragma exclude_renderers gles gles3 glcore
            #pragma target 4.5

            // -------------------------------------
            // Material Keywords
            #pragma shader_feature_local_fragment _ALPHATEST_ON
            #pragma shader_feature_local_fragment _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A

            //--------------------------------------
            // GPU Instancing
            #pragma multi_compile_instancing
            #pragma multi_compile _ DOTS_INSTANCING_ON


            #pragma vertex ShadowPassVertex
            #pragma fragment ShadowPassFragment
            

            #include "ShadowCaster.hlsl"
            ENDHLSL
        }

这里要注意ShadowCaster这个pass在用的时候最好保证shader property是一样的,我这边有两个shader都使用了这个ShadowCaster的pass,然而frame debugger里面显示没有合批,因为我这两个shader的property不一样,所以最好同一类材质的东西用同一个ShadowCaster,不同类的要用相同的ShadowCaster就合不了批了。

然而!!!多pass的物体在URP中不能进行SRP Batch,即使你费了老大的劲把shader改成SRP Batch compatible也不行!所以强烈建议在URP中不要使用多pass的材质,多pass的需求应该通过其他途径解决,比如说Render Feature。这个我们下次再聊。

项目地址

你可能感兴趣的:(初识Unity的URP并用multiple pass制作半透明物体)