GPUInstance

676 阅读6分钟

Shader支持GPUInstance

  • 第一步先在预处理指令那里添加上下面的代码,会根据是否开启GPU Instance生成不同的shader变体
//定义INSTANCING_ON的shader keyworld
#pragma multi_compile_instancing

// UnityInstancing.hlsl
#if defined(UNITY_SUPPORT_INSTANCING) && defined(INSTANCING_ON)
    #define UNITY_INSTANCING_ENABLED
#endif

  • 第二步分别在输入输出结构体内都添加下面的宏,用于在结构体中定义SV_InstanceID的元素
//定义 uint instanceID : SV_InstanceID;
UNITY_VERTEX_INPUT_INSTANCE_ID

#if !defined(UNITY_VERTEX_INPUT_INSTANCE_ID)
#   define UNITY_VERTEX_INPUT_INSTANCE_ID DEFAULT_UNITY_VERTEX_INPUT_INSTANCE_ID
#endif

#define DEFAULT_UNITY_VERTEX_INPUT_INSTANCE_ID  uint instanceID : SV_InstanceID;
  • 第三步在顶点着色器开头声明输出结构体后添加如下代码,从输入结构体v中读取Instance ID. 然后给输出结构体o中的相关元素赋值
//将instance id保存在unity_InstanceID中
UNITY_SETUP_INSTANCE_ID(v);
//instance id 从顶点着色器传入片元着色器
UNITY_TRANSFER_INSTANCE_ID(v,o);

#if !defined(UNITY_SETUP_INSTANCE_ID)
#   define UNITY_SETUP_INSTANCE_ID(input) DEFAULT_UNITY_SETUP_INSTANCE_ID(input)
#endif

#if defined(UNITY_INSTANCING_ENABLED) || defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) || defined(UNITY_STEREO_INSTANCING_ENABLED)
    void UnitySetupInstanceID(uint inputInstanceID)
    {
        #ifdef UNITY_STEREO_INSTANCING_ENABLED  //VR 双眼时使用
           //.....省略 VR游戏的处理
        #else
            unity_InstanceID = inputInstanceID + unity_BaseInstanceID;
        #endif
    }
    
//instance id 从顶点着色器传入片元着色器
#define UNITY_TRANSFER_INSTANCE_ID(input, output)   output.instanceID = UNITY_GET_INSTANCE_ID(input)
#define UNITY_GET_INSTANCE_ID(input)    input.instanceID
  • 第四步,在片元着色器开头添加如下代码,从输入结构体i中读取instance的元素,让片元着色器能够访问并使用Instance的属性.
//instance id 从顶点着色器传入片元着色器
UNITY_SETUP_INSTANCE_ID(i);

案例

Shader "SimplestInstancedShader"
{
    Properties
    {
        _Color ("Color", Color) = (1, 1, 1, 1)
    }

    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_instancing // 开启多实例的变量编译
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                UNITY_VERTEX_INPUT_INSTANCE_ID //顶点着色器的 InstancingID定义
            };
            struct v2f
            {
                float4 vertex : SV_POSITION;
                UNITY_VERTEX_INPUT_INSTANCE_ID //片元着色器的 InstancingID定义
            };

            UNITY_INSTANCING_BUFFER_START(Props) // 定义多实例变量数组
                UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
            UNITY_INSTANCING_BUFFER_END(Props)
           
            v2f vert(appdata v)
            {
                v2f o;

                UNITY_SETUP_INSTANCE_ID(v); //装配 InstancingID
                UNITY_TRANSFER_INSTANCE_ID(v, o); //输入到结构中传给片元着色器
                float3 worldPos = TransformObjectToWorld(input.positionOS);
    			output.positionCS = TransformWorldToHClip(worldPos);
                return o;
            }
           
            fixed4 frag(v2f i) : SV_Target
            {
                UNITY_SETUP_INSTANCE_ID(i); //装配 InstancingID
                return UNITY_ACCESS_INSTANCED_PROP(Props, _Color); //提取多实例中的当前实例的Color属性变量值
            }
            ENDCG
        }
    }
}
  • 定义:使用相同材质,相同mesh的情况下,unity会自动收集视野中所有符合要求的对象,将其材质属性,矩阵信息(如物体到世界空间的矩阵),uv偏移等收集到结构数组中,一并传到GPU存储在Constant Buffer中,同时只需要传递一个mesh数据,绘制某个物体时通过为其分配的instance id去寻找结构体数组中寻找对应的信息。
  • Constant Buffer
  • 如何在结构体数组中定义变量?
UNITY_INSTANCING_BUFFER_START(Props) // 定义多实例变量数组
	UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
UNITY_INSTANCING_BUFFER_END(Props)

// buf和CBuffer的名字有关
#define UNITY_INSTANCING_BUFFER_START(buf)      UNITY_INSTANCING_CBUFFER_SCOPE_BEGIN(UnityInstancing_##buf) struct {
//arr和结构体数组的名字有关
#define UNITY_INSTANCING_BUFFER_END(arr)        } arr##Array[UNITY_INSTANCED_ARRAY_SIZE]; UNITY_INSTANCING_CBUFFER_SCOPE_END
#define UNITY_DEFINE_INSTANCED_PROP(type, var)  type var;
  • 如何访问结构体数组中的变量?
//Props结构体数组名称,_Color结构中的变量
UNITY_ACCESS_INSTANCED_PROP(Props, _Color); //提取多实例中的当前实例的Color属性变量值

#define UNITY_ACCESS_INSTANCED_PROP(arr, var)   arr##Array[unity_InstanceID].var

Graphics.DrawMeshInstanced

  • 由于数据都存储在Constant Buffer中,而Constant Buffer有大小限制,Windows一个最大为64kB,一个shader中可以有多个CB,所有一次提交的物体信息的数量不可能太多,PC为500,移动端为250左右。
#if (defined(SHADER_API_VULKAN) && defined(SHADER_API_MOBILE)) || defined(SHADER_API_SWITCH)
     #define UNITY_INSTANCED_ARRAY_SIZE  250
#else
     #define UNITY_INSTANCED_ARRAY_SIZE  500
#endif
  • 500个限制的原因?

    • 因为在名为unity_Builtins0的结构数组中最少需要包含两个矩阵,object to world(物体到世界),world to object(世界到物体,用于变换法线) 。所以64000 / 128 = 500.
 #ifdef UNITY_ASSUME_UNIFORM_SCALING
    #define UNITY_WORLDTOOBJECTARRAY_CB 1
#else
    #define UNITY_WORLDTOOBJECTARRAY_CB 0
#endif
//上面两句,若shader中声明了 #pragma instancing_options assumeuniformscaling
//这句话,则会自动生成一个名为UNITY_ASSUME_UNIFORM_SCALING的宏
// ---------------------------------------------
UNITY_INSTANCING_BUFFER_START(PerDraw0)
    #ifndef UNITY_DONT_INSTANCE_OBJECT_MATRICES
        UNITY_DEFINE_INSTANCED_PROP(float4x4, unity_ObjectToWorldArray)
        #if UNITY_WORLDTOOBJECTARRAY_CB == 0
            UNITY_DEFINE_INSTANCED_PROP(float4x4, unity_WorldToObjectArray)
        #endif
    #endif
//... 忽略其他不是必须的数据 
UNITY_INSTANCING_BUFFER_END(unity_Builtins0)


#ifndef UNITY_DONT_INSTANCE_OBJECT_MATRICES
    #undef UNITY_MATRIX_M
    #undef UNITY_MATRIX_I_M
    #define MERGE_UNITY_BUILTINS_INDEX(X) unity_Builtins##X
   //..省略不重要的东西
    #else
        #define UNITY_MATRIX_M      UNITY_ACCESS_INSTANCED_PROP(unity_Builtins0, unity_ObjectToWorldArray)
        #define UNITY_MATRIX_I_M    UNITY_ACCESS_INSTANCED_PROP(MERGE_UNITY_BUILTINS_INDEX(UNITY_WORLDTOOBJECTARRAY_CB), unity_WorldToObjectArray)
    #endif
#endif
//根据unity_InstanceID访问结构数组中的 object to world matrix,world to object matrix。
  • 所以当调用Unity自带的API--Graphics.DrawMeshInstanced(最多可以绘制1023个)绘制物体时,若绘制1023个物体,会分三次提交,产生三个batch,图中为四个是因为其中有一个为天空盒的数据。

Graphics.DrawMeshInstancedIndirect

若要可以通过一次提交绘制无限个物体,则需要使用Graphics.DrawMeshInstancedIndirectUnity接口。同时还需要我们避开使用Constant Buffer,其实可以使用StructuredBuffer代替,将所需要的数据一次都提交到shader后(这些数据最起码可以产生物体到世界的矩阵,从而将顶点从物体空间变化到世界空间,至于法线,可以提前在Cpu计算世界空间法线)。

视锥剔除1

  • 公式:点到平面的距离

  • 当法向量为单位向量时,分母可以去掉,并且这里加绝对值是为了保证值为正,视情况判断是否需要加绝对值。

  • GeometryUtility.CalculateFrustumPlanes(Camera.main),该方法返回摄像机的6个裁剪平面。顺序为[0] = Left, [1] = Right, [2] = Down, [3] = Up, [4] = Near, [5] = Far。所以可以通过如下方式判断一个点是否再视锥体中。

  • 案例

 public bool TestInCamreaView1(Vector3 pos)
    {
        Vector4[] planeInfo = new Vector4[6];
        var planes = GeometryUtility.CalculateFrustumPlanes(Camera.main);
        for (int i = 0; i < 6; i++)
        {
            Plane p = planes[i];
            //此时的normal已经为归一化的单位向量
            planeInfo[i] = new Vector4(p.normal.x,p.normal.y,p.normal.z,p.distance);
        }

        for (int i = 0; i < 6; i++)
        {
            var normal = new Vector3(planeInfo[i].x,planeInfo[i].y,planeInfo[i].z);
            var dist = planeInfo[i].w;
            //根据点到平面距离的公式,此处不加绝对值,若点在视锥体外,距离为负数
            if (Vector3.Dot(normal,pos) + dist <= 0)
            {
                return false;
            }
        }

        return true;
    }

视锥剔除2

  • 计算每个面的法向量,然后计算从摄像机位置到物体位置的向量与平面的法线点积是否为正,取交集即可判断是否在视锥体内。

  • 当相机指向物体的向量与视锥体面重合时,正好与视锥体面垂直,此时二者点积为0;若在视锥体外,二者夹角为钝角,点积小于0

  • 案例

   public bool TestCameraView2(Vector3 pos)
    {
        var camera = Camera.main;
        var farClipPlane = camera.farClipPlane;
        //w为相机远剪裁平面宽的一半
        var w = Mathf.Tan(camera.fieldOfView * 0.5f * Mathf.Deg2Rad) * farClipPlane * camera.aspect;
        //向量相加
        Vector3 vR =farClipPlane * transform.forward + w * transform.right;
        //向量相减
        Vector3 vL =farClipPlane * transform.forward - w * transform.right;
        //向量叉积,在左手坐标系遵从左手定则,右手坐标系遵从右手定则,unity为左手坐标系,所以遵从左手定则。
        Vector3 RightPlane_N = Vector3.Cross(vR,transform.up);//视椎体右平面法线
        Vector3 LeftPlane_N = Vector3.Cross(transform.up,vL);//视椎体左平面法线

        //摄像机到物体的向量
        var vect = pos - camera.transform.position;
        if (Vector3.Dot(vect,RightPlane_N) > 0 && Vector3.Dot(vect,LeftPlane_N) > 0)
        {
            return true;
        }
        return false;
    }

SRP Batcher

使用相同shader变体(也就是Keyword)的物体就可以参与合批。

  • 原理

    • 每当场景种产生一个新的材质时,unity会自动收集材质的属性,并将其保存在GPU中,并且只有属性改变时才会更新。
    • 将每个物体的引擎属性(例如物体到世界矩阵,世界到物体矩阵等等)缓存在GPU中,每次只需要更新,不需要重新创建.
    • 所以SRP Batcher只是降低了Batch的成本。
  • 限制

    • You must declare all built-in engine properties in a single CBUFFER named “UnityPerDraw”. For example, unity_ObjectToWorld, or unity_SHAr.
    • You must declare all Material properties in a single CBUFFER named UnityPerMaterial.
    • 渲染物体必须是 a mesh or skinned mesh. 不能是 a particle.
    • 不支持使用Material Property Block。

合批优先级

Static Batch > SRP Batcher > GPUInstance > Dynamic Batch。

Instance 案例

public class Draw1023Object : MonoBehaviour
{
    [Range(0, 1023)] public int number;
    private MaterialPropertyBlock _block;
    private static int _baseColorId = Shader.PropertyToID("_BaseColor");
    private static int _cutOffId = Shader.PropertyToID("_CutOff");
    private Vector4[] colors;
    private Matrix4x4[] matrixs;
    private float[] cutOffs;
    public GameObject prefab;
    private Mesh _mesh;
    private Material _material;
    void Start()
    {
        colors = new Vector4[number];
        matrixs = new Matrix4x4[number];
        cutOffs = new float[number];
        for (int i = 0; i < number; i++)
        {
            matrixs[i] = Matrix4x4.TRS(
                Random.insideUnitSphere * 10f, Quaternion.identity, Vector3.one
            );
            colors[i] =
                new Vector4(Random.value, Random.value, Random.value, Random.value);

            cutOffs[i] = Random.value;
        }

        _material = prefab.GetComponent<MeshRenderer>().sharedMaterial;
        _mesh = prefab.GetComponent<MeshFilter>().sharedMesh;
    }

    // Update is called once per frame
    void Update()
    {
        if (_block == null)
        {
            _block = new MaterialPropertyBlock();
            _block.SetVectorArray(_baseColorId,colors);
            _block.SetFloatArray(_cutOffId,cutOffs);
        }
        Graphics.DrawMeshInstanced(_mesh,0,_material,matrixs,number,_block);
    }
}

#ifndef CUSTOM_UNLIT_PASS
#define CUSTOM_UNLIT_PASS

#include "../ShaderLibrary/Common.hlsl"

#ifdef INSTANCING_ON
    UNITY_INSTANCING_BUFFER_START(UnityPerMaterial) //声明名为UnityPerMaterial的结构
        UNITY_DEFINE_INSTANCED_PROP(float4,_BaseColor) //结构中声明变量type var 
        UNITY_DEFINE_INSTANCED_PROP(float4,_BaseMap_ST)
        UNITY_DEFINE_INSTANCED_PROP(float,_CutOff)
    UNITY_INSTANCING_BUFFER_END(UnityPerMaterial    ) //结构结束,并且声明一个名为UnityPerMaterial的结构数组
#else
    CBUFFER_START(UnityPerMaterial)
        float4 _BaseColor;
        float4 _BaseMap_ST;
        float _CutOff;
    CBUFFER_END
#endif

TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);

struct Attributes
{
    float3 positionOS : POSITION;
    float2 uv : TEXCOORD0;
    UNITY_VERTEX_INPUT_INSTANCE_ID  //相当于uint instanceID : SV_InstanceID;
};

struct Varying
{
    float4 positionCS : SV_POSITION;
    float2 uv:TEXCOORD0;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

Varying UnlitVertex(Attributes input) 
{
    Varying output;
    UNITY_SETUP_INSTANCE_ID(input); //将instanceID保存在一个名为unity_InstanceID的全局变量中,供其他有关instance有关的宏使用
    UNITY_TRANSFER_INSTANCE_ID(input,output); // output.instanceID = UNITY_GET_INSTANCE_ID(input),将instanceID从Vertex传入Fragment着色器
                                              //以供片元着色器使用
    float3 worldPos = TransformObjectToWorld(input.positionOS);
    output.positionCS = TransformWorldToHClip(worldPos);
    #ifdef INSTANCING_ON 
        float4 st = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial,_BaseMap_ST);
        output.uv = input.uv * st.xy + st.zw;
    #else
        output.uv = TRANSFORM_TEX(input.uv,_BaseMap);
    #endif
    return output;
}

float4 UnlitFragment(Varying input) : SV_Target
{
    UNITY_SETUP_INSTANCE_ID(input)
    float4 texColor = SAMPLE_TEXTURE2D(_BaseMap,sampler_BaseMap,input.uv);
    float4 reslutColor;
    float cutOff;
    #ifdef INSTANCING_ON
        reslutColor = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial,_BaseColor) * texColor;//通过保存的InstanceID从结构数组中获取对应的项中的_BaseColor变量
        cutOff = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial,_CutOff);
    #else
        reslutColor =  _BaseColor * texColor;
        cutOff = _CutOff;
    #endif
    #ifdef _CLIPPING
        clip(reslutColor.a - cutOff);
    #endif
    return reslutColor;
}
#endif