美文网首页
Unity自定义SRP(二):Draw Call

Unity自定义SRP(二):Draw Call

作者: Dragon_boy | 来源:发表于2020-12-14 19:09 被阅读0次

    https://catlikecoding.com/unity/tutorials/custom-srp/draw-calls/

    Shaders

    为了绘制一些东西,CPU需要告诉GPU绘制什么以及如何绘制,绘制的东西通常是网格,如何绘制通常由一个shader决定,即针对GPU的指令集。

    Unlit Shader

    新建一个Shaders文件夹,并创建一个名为Unlit的shader,shader的基本结构不用赘述:

    Shader "Custom RP/Unlit"
    {
        Properties
        {
        }
        SubShader
        {
            Pass
            {
            }
        }  
    }
    

    HLSL Program

    这里模仿URP使用HLSL作为shader pass中程序的语言:

            Pass
            {
                HLSLPROGRAM
                #pragma vertex UnlitPassVertex
                #pragma fragment UnlitPassFragment
                ENDHLSL
            }
    

    为了方便,我们将顶点着色器和片元着色器的代码置于一个.hlsl文件中:

                HLSLPROGRAM
                #pragma vertex UnlitPassVertex
                #pragma fragment UnlitPassFragment
                #include "UnlitPass.hlsl"
                ENDHLSL
    

    UnlitPass

    hlsl文件中,我们按照传统的头文件写法先写上一些宏定义,并加上着色器函数:

    #ifndef CUSTOM_UNLIT_PASS_INCLUDED
    #define CUSTOM_UNLIT_PASS_INCLUDED
    
    float4 UnlitPassVertex() : SV_POSITION
    {
        return 0.0;
    }
    
    float4 UnlitPassFragment() :SV_TARGET
    {
        return 0.0;
    }
    
    #endif
    

    空间变换

    顶点着色器中,我们先传入模型空间的坐标,

    float4 UnlitPassVertex(float3 positionOS : POSITION) : SV_POSITION
    {
        return 0.0;
    }
    

    这里我们尝试返回世界空间下的坐标,也就是需要一个进行空间变换的矩阵(Unity自带)。为方便,我们新建一个额外的文件UnityInput.hlsl,放到与Shaders同一根目录下新建的文件夹ShaderLibrary中:

    #ifndef CUSTOM_UNITY_INPUT_INCLUDED
    #define CUSTOM_UNITY_INPUT_INCLUDED
    
    float4x4 unity_ObjectToWorld;
    
    #endif
    

    同时我们定义一个空间变换的函数。新建一个Common.hlsl文件,置于ShaderLibrary文件夹下:

    #ifndef CUSTOM_COMMON_INCLUDED
    #define CUSTOM_COMMON_INCLUDED
    
    float3 TransformObjectToWorld(float3 positionOS)
    {
        return mul(unity_ObjectToWorld, float4(positionOS, 1.0)).xyz;
    }
    
    #endif
    

    这样我们就可以将顶点从模型空间转换到世界空间了:

    #include "../ShaderLibrary/Common.hlsl"
    
    float4 UnlitPassVertex(float3 positionOS : POSITION) : SV_POSITION
    {
        float3 positionWS = TransformObjectToWorld(positionOS);
        return float4(positionWS, 1.0);
    }
    

    不过我们最后所需要的是齐次裁剪空间内的坐标,即还需要View和Projection矩阵,这在UnityInput.hlsl中定义:

    float4x4 unity_ObjectToWorld;
    float4x4 unity_MatrixVP;
    

    Common.hlsl中添加相应的函数:

    float4 TransformWorldToHClip(float3 positionWS)
    {
        return mul(unity_MatrixVP, float4(positionWS, 1.0));
    }
    

    在顶点着色器中应用:

    float4 UnlitPassVertex(float3 positionOS : POSITION) : SV_POSITION
    {
        float3 positionWS = TransformObjectToWorld(positionOS);
        return TransformWorldToHClip(positionWS);
    }
    

    Core Library

    上述我们定义的两个空间变换的函数其实包括在Unity的Core RP Pipeline包中,我们直接使用自带的即可,在Common.hlsl中替换:

    #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl"
    

    这样会遇到编译错误,因为其相关矩阵皆是宏定义:



    我们自己构建一个宏定义即可:

    #define UNITY_MATRIX_M unity_ObjectToWorld
    #define UNITY_MATRIX_I_M unity_WorldToObject
    #define UNITY_MATRIX_V unity_MatrixV
    #define UNITY_MATRIX_VP unity_MatrixVP
    #define UNITY_MATRIX_P glstate_matrix_projection
    
    #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl"
    

    同时修改UnityInput.hlsl:

    float4x4 unity_ObjectToWorld;
    float4x4 unity_WorldToObject;
    real4 unity_WorldTransformParams;
    
    float4x4 unity_MatrixVP;
    float4x4 unity_MatrixV;
    float4x4 glstate_matrix_projection;
    

    unity_WorldTransformParams包含了一些变换信息。

    还有许多的别名和基本的宏在Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl中定义,记得包含:

    #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
    #include "UnityInput.hlsl"
    

    颜色

    片元着色器可以返回调色板中修改的颜色。shader中定义属性:

        Properties
        {
            _BaseColor("Base Color", Color) = (1, 1, 1, 1)
        }
    

    UnlitPass中应用:

    float4 _BaseColor;
    
    float4 UnlitPassFragment() :SV_TARGET
    {
        return _BaseColor;
    }
    

    批处理

    每个draw call要求CPU和GPU之间的通信,如果大量的数据要送往GPU,那么GPU会花费许多时间在等待数据上,与此同时CPU会花费大量时间在传递数据上,这些都会降低帧率。目前我们的绘制方法是每个物体调用一次draw call,例如,场景中若有5个正方体的话,那么共有7个draw call,5个正方体各一次,天空盒一次,清除渲染目标一次。

    SRP Batcher

    批处理是结合draw call的过程,减少CPU与GPU通信的时间,最简单的方法是开启SRP batcher,不过目前我们的Unlit shader并不能使用。

    SRP batcher采用一种更为精简的方法来减少draw call数量,它捕捉在GPU上的材质属性,这样就不必每次调用draw call都需要传输相应的数据,不过只有在shader针对unifrom数据使用特定的数据结构时才可使用。

    所有的材质属性必须定义在一个具体的内存缓冲中,即cbuffer块,名称为UnityPerMaterial

    cbuffer UnityPerMaterial
    {
        float _BaseColor;
    };
    

    这种常量缓冲并不在所有平台上都支持(如OpenGL ES2.0),因此这里使用Core RP Library中的CBUFFER_STARTCBUFFER_END宏定义:

    CBUFFER_START(UnityPerMaterial)
        float4 _BaseColor;
    CBUFFER_END
    

    对于一些变换矩阵我们也是用相似的方式定义,只不过名称改为UnityPerDraw:

    CBUFFER_START(UnityPerDraw)
        float4x4 unity_ObjectToWorld;
        float4x4 unity_WorldToObject;
        float4 unity_LODFade;
        real4 unity_WorldTransformParams;
    CBUFFER_END
    

    这样的话就可以使用SRP batcher了。接下来我们在CustomRenderPipeline中将其开启:

        public CustomRenderPipeline()
        {
            GraphicsSettings.useScriptableRenderPipelineBatching = true;
        }
    

    多种颜色

    如果我们想要每个材质的颜色不同的话,我们就不得不创建多个材质,因为Unity只会批处理那些有着相同shader变体的draw call。如果可以每个物体能各自修改自己的颜色就可以了,我们可以创建一个自定义的组件类型,命名为PerObjectMaterialProperties。方法是一个game object会有一个相应的组件,可以修改_Base Color配置,用于设置材质属性:

    using UnityEngine;
    
    [DisallowMultipleComponent]
    public class PerObjectMaterialProperties : MonoBehaviour
    {
        static int baseColorId = Shader.PropertyToID("_BaseColor");
    
        [SerializeField]
        Color baseColor = Color.white;
    }
    

    我们通过MaterialPropertyBlock对象逐物体设置材质属性:

        static MaterialPropertyBlock block;
    

    我们在OnValidate()中设置材质属性,该方法在组件加载或改变时调用:

        private void OnValidate()
        {
            if(block == null)
            {
                block = new MaterialPropertyBlock();
            }
            block.SetColor(baseColorId, baseColor);
            GetComponent<Renderer>().SetPropertyBlock(block);
        }
    

    不过SRP batcher并不能处理逐物体材质属性,因此并不会进行批处理。

    同时,想在build版本中使用的话,我们在Awake()方法中调用:

        private void Awake()
        {
            OnValidate();
        }
    
    

    GPU Instancing

    GPU Instancing可以使用逐物体材质属性来减少draw call数量,即一个draw call同时绘制多个物体。CPU会收集所有的逐物体变换和材质属性,并将它们放入队列中,送往GPU,GPU接着遍历该队列,按顺序渲染。

    为了让shader支持该特性,我们添加一行代码:

                #pragma multi_compile_instancing
                #pragma vertex UnlitPassVertex
                #pragma fragment UnlitPassFragment
    

    为支持GPU Instancing,我们包含进UnityInstancing.hlsl文件:

    #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/UnityInstancing.hlsl"
    #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl"
    

    该文件定义了一些用于接收实例化数据队列的宏。为了渲染成功,需要知道当前物体的索引,其通过顶点数据提供。为了方便数据定义,我们定义一个结构体:

    struct Attributes
    {
        float3 positionOS : POSITION;
    };
    
    float4 UnlitPassVertex(Attributes input) : SV_POSITION
    {
        float3 positionWS = TransformObjectToWorld(input.positionOS);
        return TransformWorldToHClip(positionWS);
    }
    

    在结构体中加入实例化索引:

    struct Attributes
    {
        float3 positionOS : POSITION;
        UNITY_VERTEX_INPUT_INSTANCE_ID
    };
    

    接着在顶点着色器中加入UNITY_SETUP_INSTANCE_ID(input):

    float4 UnlitPassVertex(Attributes input) : SV_POSITION
    {
        UNITY_SETUP_INSTANCE_ID(input);
        float3 positionWS = TransformObjectToWorld(input.positionOS);
        return TransformWorldToHClip(positionWS);
    }
    

    接着我们要提供逐实例材质数据:

    UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
        UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
    UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
    

    接着我们就可以使用实例索引在片元着色器中进行相关计算了。为方便,我们同样定义一个结构体,用于顶点着色器和片元着色器之间的数据传输:

    struct Varyings
    {
        float4 positionCS : SV_POSITION;
        UNITY_VERTEX_INPUT_INSTANCE_ID
    };
    

    我们使用UNITY_TRANSFER_INSTANCE_ID来传输索引:

    Varyings UnlitPassVertex(Attributes input)
    {
        Varyings output;
        UNITY_SETUP_INSTANCE_ID(input);
        UNITY_TRANSFER_INSTANCE_ID(input, output);
        float3 positionWS = TransformObjectToWorld(input.positionOS.xyz);
        output.positionCS = TransformWorldToHClip(positionWS);
        
        return output;
    }
    

    在片元着色器中,我们使用UNITY_ACCESS_INSTANCED_PROP来访问当前实例的属性:

    float4 UnlitPassFragment(Varyings input) : SV_TARGET
    {
        UNITY_SETUP_INSTANCE_ID(input);
        return UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);
    }
    

    注意,只有那些分享相同材质的物体才能使用GPU instancing,改变材质颜色也可以。

    动态批处理

    该技术会将一些使用相同材质的小网格组合成大网格绘制,当使用逐物体材质时该方法不会生效。要想使用的话只需要在drawingSettings中开启即可:

            var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings)
            {
                enableDynamicBatching = true,
                enableInstancing = false
            };
    

    同时要关闭SRP batcher,因为它优先级最高:

            GraphicsSettings.useScriptableRenderPipelineBatching = false;
    

    一般情况下,GPU instancing的效果更好。

    配置批处理

    目前介绍了三种批处理的方法,我们添加一些交互性来配置这些方法:

        void DrawVisibleGeometry(bool useDynamicBatching, bool useGPUInstancing)
        {
            var sortingSettings = new SortingSettings(camera)
            {
                criteria = SortingCriteria.CommonOpaque
            };
            var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings)
            {
                enableDynamicBatching = useDynamicBatching,
                enableInstancing = useGPUInstancing
            };
            ...
        }
    
        public void Render(ScriptableRenderContext context, Camera camera, bool useDynamicBatching, bool useGPUInstancing)
        {
            ...
            DrawVisibleGeometry(useDynamicBatching, useGPUInstancing);
            ...
        }
    
        bool useDynamicBatching, useGPUInstancing;
    
        public CustomRenderPipeline(bool useDynamicBatching, bool useGPUInstancing, bool useSRPBatcher)
        {
            this.useDynamicBatching = useDynamicBatching;
            this.useGPUInstancing = useGPUInstancing;
            GraphicsSettings.useScriptableRenderPipelineBatching = useSRPBatcher;
        }
    
        protected override void Render(ScriptableRenderContext context, Camera[] cameras)
        {
            foreach (Camera camera in cameras)
            {
                renderer.Render(context, camera, useDynamicBatching, useGPUInstancing);
            }
        }
    

    最后,我们将这些属性选项放于可配置域中:

        [SerializeField]
        bool useDynamicBatching = true, useGPUInstancing = true, useSRPBatcher = true;
    
        protected override RenderPipeline CreatePipeline()
        {
            return new CustomRenderPipeline(useDynamicBatching, useGPUInstancing, useSRPBatcher);
        }
    

    透明

    接着修改我们的Unlit shader让其同时支持不透明和透明物体。

    混合模式

    我们定义两个混合模式的属性,src和dst,即源颜色和目标颜色模式,为了方便,我们使用内置的枚举类型来定义属性:

            [Enum(UnityEngine.Rendering.BlendMode)]_SrcBlend("Src Blend", Float) = 1
            [Enum(UnityEngine.Rendering.BlendMode)]_DstBlend("Dst Blend", Float) = 0
    

    我们将Src调整为SrcAlpha,即RGB组件会预期alpha组件相乘。Dst调整为OneMinusSrcAlpha,使权重达到1。

    在Pass中我们使用Blend语句设置混合模式:

            Pass
            {
                Blend [_SrcBlend] [_DstBlend]
                HLSLPROGRAM
                ...
                ENDHLSL
            }
    

    透明度混合通常不开启深度写入,我们可以使用一个属性来控制它:

            [Enum(Off, 0, On, 1)]_Zwrite("Z Write", Float) = 1
    
            Pass
            {
                Blend [_SrcBlend] [_DstBlend]
                ZWrite [_ZWrite]
                HLSLPROGRAM
                ...
                ENDHLSL
            }
    

    纹理

    要使用纹理,我们先设置一下属性:

            _BaseMap("Texture", 2D) = "white"{}
    

    纹理需要加载到GPU内存中,这一点由Unity完成,而shader需要一个相关纹理的句柄来访问纹理,该句柄可以想uniform变量一样定义,这里使用TEXTURE2D宏。同时需要定义一个采样器,用于根据包裹和滤波模式控制采样,使用SAMPLER宏定义:

    TEXTURE2D(_BaseMap);
    SAMPLER(sampler_BaseMap);
    
    UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
        UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
    UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
    
    

    注意这两个变量不能逐实例提供,应放在全局域中。

    同时,我们还需要一个后缀为_ST的变量,用于进行纹理的拼贴和偏移,放在UnityPerMaterial缓冲中:

    UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
        UNITY_DEFINE_INSTANCED_PROP(float4, _BaseMap_ST)
        UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
    UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
    

    我们还需要纹理坐标,这是顶点属性的一部分:

    struct Attributes
    {
        float4 positionOS : POSITION;
        float2 baseUV : TEXCOORD0;
        UNITY_VERTEX_INPUT_INSTANCE_ID
    };
    

    传入到片元着色器的数据中也要包含纹理坐标,可以使用任何未使用的语义:

    struct Varyings
    {
        float4 positionCS : SV_POSITION;
        float2 baseUV : VAR_BASE_UV;
        UNITY_VERTEX_INPUT_INSTANCE_ID
    };
    

    纹理的缩放和偏移分别存储在_BaseMap_ST的xy和zw分量中,我们在顶点着色器中应用:

    Varyings UnlitPassVertex(Attributes input)
    {
        ...
        
        float4 baseST = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseMap_ST);
        output.baseUV = input.baseUV * baseST.xy + baseST.zw;
        return output;
    }
    

    在片元着色器中,我们使用SAMPLE_TEXTURE2D来进行纹理的采样:

    float4 UnlitPassFragment(Varyings input) : SV_TARGET
    {
        UNITY_SETUP_INSTANCE_ID(input);
        float4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.baseUV);
        float4 baseColor = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);
        float4 base = baseMap * baseColor;
    
        return base;
    }
    

    Alpha剔除

    我们可以根据透明度来剔除片段。定义属性,声明变量_Cutoff:

            _Cutoff ("Alpha Cutoff", Range(0.0, 1.0)) = 0.5
    
        UNITY_DEFINE_INSTANCED_PROP(float, _Cutoff)
    

    在片元着色器中使用clip函数剔除:

        UNITY_SETUP_INSTANCE_ID(input);
        float4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.baseUV);
        float4 baseColor = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);
        float4 base = baseMap * baseColor;
        clip(base.a - UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Cutoff));
        return base;
    

    不过,我们不能在材质中同时使用透明度混合和alpha剔除,毕竟前者不写入深度,后者写入,同时其使用AlphaTest队列,位于Opaque后。因此,这里添加一个属性来配置alpha剔除:

            [Toggle(_CLIPPING)]_Clipping("Alpha Clipping", Float) = 0
    
    #if defined(_CLIPPING)
        clip(base.a - UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Cutoff));
    #endif
    

    同时,在shader中定义相应的shader变体:

                #pragma shader_feature _CLIPPING
                #pragma multi_compile_instancing
    

    相关文章

      网友评论

          本文标题:Unity自定义SRP(二):Draw Call

          本文链接:https://www.haomeiwen.com/subject/mnvvgktx.html