返回
Featured image of post 多例化技术

多例化技术

什么是多例化技术

使用 GPU 多例化可使用少量绘制调用一次绘制(或渲染)同一网格的多个副本。它对于绘制诸如建筑物、树木和草地之类的在场景中重复出现的对象非常有用。

GPU 多例化在每次绘制调用时仅渲染相同的网格,但每个实例可以具有不同的参数(例如,颜色或比例)以增加变化并减少外观上的重复。

GPU 多例化可以降低每个场景使用的绘制调用数量。可以显著提高项目的渲染性能。

如何启用多例化技术

要对材质启用 GPU 实例化 (GPU Instancing),请在 Project 窗口中选择材质,然后在 Inspector 中勾选 Enable Instancing 复选框。

开启多例化前后性能对比

添加逐实例数据

Shader "FoxShader/InstancedColorSurfaceShader"
{
    Properties
    {
        _Color ("Color", Color) = (1, 1, 1, 1)
        _MainTex ("Albedo (RGB)", 2D) = "white" { }
        _Glossiness ("Smoothness", Range(0, 1)) = 0.5
        _Metallic ("Metallic", Range(0, 1)) = 0.0
    }
    
    SubShader
    {
        Tags { "RenderType" = "Opaque" }
        LOD 200
        CGPROGRAM
        
        // 基于物理的标准光照模型,并对所有光照类型启用阴影
        #pragma surface surf Standard fullforwardshadows
        // 使用 Shader Model 3.0 目标
        #pragma target 3.0
        sampler2D _MainTex;
        struct Input
        {
            float2 uv_MainTex;
        };
        half _Glossiness;
        half _Metallic;
        //去除原本采样颜色
        //fixed4 _Color;
        
        // 宏开始宣告要使用GPU多例化技术的变量
        UNITY_INSTANCING_BUFFER_START(Props)
        
        // 宏声明_Color变量使用技术
        UNITY_DEFINE_INSTANCED_PROP(fixed4, _Color)
        
        // 宏结束使用多例化技术
        UNITY_INSTANCING_BUFFER_END(Props)
        void surf(Input IN, inout SurfaceOutputStandard o)
        {
            //这里不是原来的乘以颜色,而改为乘以宏坐标颜色
            fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * UNITY_ACCESS_INSTANCED_PROP(Props, _Color);
            o.Albedo = c.rgb;
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG
        
    }
    FallBack "Diffuse"
}

C#改变对象的多例化材质颜色属性

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class InstancedColorSurface : MonoBehaviour
{
    public GameObject obj;

    private void Start()
    {
        MaterialPropertyBlock props = new MaterialPropertyBlock();
        MeshRenderer renderer;

        for (int i = 0; i < 10000; i++)
        {
            GameObject obj = GameObject.Instantiate(this.obj);
            obj.transform.position = new Vector3(Random.Range(-100, 100), Random.Range(-100, 100), Random.Range(-100, 100));


            float r = Random.Range(0.0f, 1.0f);
            float g = Random.Range(0.0f, 1.0f);
            float b = Random.Range(0.0f, 1.0f);

            props.SetVector("_Color", new Color(r, g, b));
            renderer = obj.GetComponent<MeshRenderer>();
            renderer.SetPropertyBlock(props);
        }   
    }
}

在顶点着色器和片元着色器中使用多例化技术

以下Shader在顶点着色器和片元着色器中启用了GPU多实例化技术

Shader "FoxShader/SimplestInstancedShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" { }
        _Color ("Color", Color) = (1, 1, 1, 1)
    }
    SubShader
    {
        Tags { "RenderType" = "Opaque" }
        LOD 100
        
        Pass
        {
            CGPROGRAM
            
            #pragma vertex vert
            #pragma fragment frag
            //1.让编译指示符宣告使用GPU多例化技术
            #pragma multi_compile_instancing
            
            #include "UnityCG.cginc"
            
            struct appdata
            {
                float4 vertex: POSITION;
                float2 uv: TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };
            
            struct v2f
            {
                float2 uv: TEXCOORD0;
                float4 vertex: SV_POSITION;
                //2.如果要访问片元着色器中的多例化属性变量,需要使用此宏
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };
            
            //3.开启宏定义
            // 宏开始宣告要使用GPU多例化技术的变量
            UNITY_INSTANCING_BUFFER_START(Props)
            // 宏声明_Color变量使用技术
            UNITY_DEFINE_INSTANCED_PROP(fixed4, _Color)
            // 宏结束使用多例化技术
            UNITY_INSTANCING_BUFFER_END(Props)
            
            sampler2D _MainTex;
            float4 _MainTex_ST;
            
            v2f vert(appdata v)
            {
                v2f o;
                //4.如果要访问片元着色器中的多例化属性变量,需要使用此宏
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_TRANSFER_INSTANCE_ID(v, o);
                
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }
            
            fixed4 frag(v2f i): SV_Target
            {
                //5.如果要访问片元着色器中的多例化属性变量,需要使用此宏
                UNITY_SETUP_INSTANCE_ID(i);
                
                fixed4 col = tex2D(_MainTex, i.uv) * UNITY_ACCESS_INSTANCED_PROP(Props, _Color);
                return col;
            }
            ENDCG
            
        }
    }
}

可以看到在代码中添加了不少的宏定义,下面将对这些宏定义进行解释

宏定义功能表

功能
#pragma multi_compile_instancing 用于命令 Unity 生成实例化变体。对于表面着色器来说是不需要的。
UNITY_VERTEX_INPUT_INSTANCE_ID 用于在顶点着色器输入/输出结构中定义实例 ID。请参阅 SV_InstanceID 以了解更多信息。
UNITY_INSTANCING_BUFFER_START(name) / UNITY_INSTANCING_BUFFER_END(name) 必须在特殊命名的常量缓冲区中定义每个实例的属性。使用这对宏来包装对每个实例唯一的属性。
UNITY_DEFINE_INSTANCED_PROP(float4, _Color) 用于根据类型和名称定义每个实例着色器的属性。在此示例中,_Color 属性是唯一的。
UNITY_SETUP_INSTANCE_ID(v); 用于使着色器函数可以访问实例 ID。它必须在顶点着色器的最开头使用,并且对于片元着色器是可选的。
UNITY_TRANSFER_INSTANCE_ID(v, o); 用于将实例 ID 从顶点着色器的输入结构体复制到输出结构体中。仅当需要访问片元着色器中的每个实例的数据时才有必要这样做。
UNITY_ACCESS_INSTANCED_PROP(arrayName, color) 用于访问在实例化常量缓冲区中声明的每个实例着色器的属性。它使用实例 ID 来索引到实例数据数组。该宏中的 arrayName 必须与 UNITY_INSTANCING_BUFFER_END(name) 宏中的匹配。

UNITY_VERTEX_INPUT_INSTANCE_ID宏的定义:

文件位置:UnityInstancing.cginc 200-202

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

UNITY_VERTEX_INPUT_INSTANCE_ID 宏就是DEFAULT_UNITY_VERTEX_INPUT_INSTANCE_ID宏。

DEFAULT_UNITY_VERTEX_INPUT_INSTANCE_ID宏的定义:

文件位置:UnityInstancing.cginc 76-101

#if defined(UNITY_INSTANCING_ENABLED) || defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) || defined(UNITY_STEREO_INSTANCING_ENABLED)
    //每一个多例化对象对应的instance id
    // A global instance ID variable that functions can directly access.
    static uint unity_InstanceID;

    // Don't make UnityDrawCallInfo an actual CB on GL
    #if !defined(SHADER_API_GLES3) && !defined(SHADER_API_GLCORE)
        UNITY_INSTANCING_CBUFFER_SCOPE_BEGIN(UnityDrawCallInfo)//着色器常量缓冲区
    #endif
            int unity_BaseInstanceID;//当前渲染批次中是在多例化对象数组中的起始位置
            int unity_InstanceCount;//当前渲染批次中的多例化对象的个数,如果是立体渲染,则以双次渲染之前的个数为准
    #if !defined(SHADER_API_GLES3) && !defined(SHADER_API_GLCORE)
        UNITY_INSTANCING_CBUFFER_SCOPE_END
    #endif

    #ifdef SHADER_API_PSSL
        #define DEFAULT_UNITY_VERTEX_INPUT_INSTANCE_ID uint instanceID;
        #define UNITY_GET_INSTANCE_ID(input)    _GETINSTANCEID(input)
    #else
        #define DEFAULT_UNITY_VERTEX_INPUT_INSTANCE_ID uint instanceID : SV_InstanceID;
        #define UNITY_GET_INSTANCE_ID(input)    input.instanceID
    #endif

#else
    #define DEFAULT_UNITY_VERTEX_INPUT_INSTANCE_ID
#endif // UNITY_INSTANCING_ENABLED || UNITY_PROCEDURAL_INSTANCING_ENABLED || UNITY_STEREO_INSTANCING_ENABLED

可见,假如以非PlayStation平台的Direct3D为例,UNITY_VERTEX_INPUT_INSTANCE_ID宏本质上就是一行代码。

unit instanceID : SV_InstanceID;  //当前渲染批次下的顶点实例id

在Shader中展开如下

struct appdata
{
    float4 vertex : POSITION;
    unit instanceID : SV_InstanceID;  //当前渲染批次下的顶点实例id
};

UNITY_INSTANCING_BUFFER_START及另外两个配套的宏的定义:

文件位置:UnityInstancing.cginc 63-70

#if defined(SHADER_API_GLES3) || defined(SHADER_API_GLCORE) || defined(SHADER_API_METAL) || defined(SHADER_API_VULKAN)
    // These platforms have constant buffers disabled normally, but not here (see CBUFFER_START/CBUFFER_END in HLSLSupport.cginc).
    #define UNITY_INSTANCING_CBUFFER_SCOPE_BEGIN(name)  cbuffer name {
    #define UNITY_INSTANCING_CBUFFER_SCOPE_END          }
#else
    #define UNITY_INSTANCING_CBUFFER_SCOPE_BEGIN(name)  CBUFFER_START(name)
    #define UNITY_INSTANCING_CBUFFER_SCOPE_END          CBUFFER_END
#endif

UNITY_INSTANCED_APPLY_SIZE宏的定义:

文件位置:UnityInstancing.cginc 208-220

    #ifdef UNITY_FORCE_MAX_INSTANCE_COUNT
        #define UNITY_INSTANCED_ARRAY_SIZE  UNITY_FORCE_MAX_INSTANCE_COUNT
    #elif defined(UNITY_INSTANCING_SUPPORT_FLEXIBLE_ARRAY_SIZE)
        #define UNITY_INSTANCED_ARRAY_SIZE  2 // minimum array size that ensures dynamic indexing
    #elif defined(UNITY_MAX_INSTANCE_COUNT)
        #define UNITY_INSTANCED_ARRAY_SIZE  UNITY_MAX_INSTANCE_COUNT
    #else
        #if defined(SHADER_API_VULKAN) && defined(SHADER_API_MOBILE)
            #define UNITY_INSTANCED_ARRAY_SIZE  250
        #else
            #define UNITY_INSTANCED_ARRAY_SIZE  500
        #endif
    #endif

可以看出,UNITY_INSTANCING_BUFFER_START等三个宏的功能是定义一个着色器常量缓冲区,并对应定义一些着色器要使用的变量。Shader中这三句话宏的定义在Direct3D 11平台上可以展开为

cbuffer UnityInstancingProps{
	float4 _Color[500];
}

UNITY_SETUP_INSTANCE_ID宏的定义

文件位置:UnityInstancing.cginc 200-202

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

DEFAULT_UNITY_SETUP_INSTANCE_ID和UNITY_TRANSFER_INSTANCE_ID宏的定义

文件位置:UnityInstancing.cginc 160-198

#if defined(UNITY_INSTANCING_ENABLED) || defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) || defined(UNITY_STEREO_INSTANCING_ENABLED)
    void UnitySetupInstanceID(uint inputInstanceID)
    {
        #ifdef UNITY_STEREO_INSTANCING_ENABLED
            #if defined(SHADER_API_GLES3)
                // We must calculate the stereo eye index differently for GLES3
                // because otherwise,  the unity shader compiler will emit a bitfieldInsert function.
                // bitfieldInsert requires support for glsl version 400 or later.  Therefore the
                // generated glsl code will fail to compile on lower end devices.  By changing the
                // way we calculate the stereo eye index,  we can help the shader compiler to avoid
                // emitting the bitfieldInsert function and thereby increase the number of devices we
                // can run stereo instancing on.
                unity_StereoEyeIndex = round(fmod(inputInstanceID, 2.0));
                unity_InstanceID = unity_BaseInstanceID + (inputInstanceID >> 1);
            #else
                // stereo eye index is automatically figured out from the instance ID
                unity_StereoEyeIndex = inputInstanceID & 0x01;
                unity_InstanceID = unity_BaseInstanceID + (inputInstanceID >> 1);
            #endif
        #else
            unity_InstanceID = inputInstanceID + unity_BaseInstanceID;
        #endif
    }
    void UnitySetupCompoundMatrices();
    #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
        #ifndef UNITY_INSTANCING_PROCEDURAL_FUNC
            #error "UNITY_INSTANCING_PROCEDURAL_FUNC must be defined."
        #else//过程函数的前置声明
            void UNITY_INSTANCING_PROCEDURAL_FUNC(); // forward declaration of the procedural function
            #define DEFAULT_UNITY_SETUP_INSTANCE_ID(input)      { UnitySetupInstanceID(UNITY_GET_INSTANCE_ID(input)); UNITY_INSTANCING_PROCEDURAL_FUNC(); UnitySetupCompoundMatrices(); }
        #endif
    #else
        #define DEFAULT_UNITY_SETUP_INSTANCE_ID(input)          { UnitySetupInstanceID(UNITY_GET_INSTANCE_ID(input)); UnitySetupCompoundMatrices(); }
    #endif
    #define UNITY_TRANSFER_INSTANCE_ID(input, output)   output.instanceID = UNITY_GET_INSTANCE_ID(input)
#else
    #define DEFAULT_UNITY_SETUP_INSTANCE_ID(input)
    #define UNITY_TRANSFER_INSTANCE_ID(input, output)
#endif

UNITY_PROCEDURAL_INSTANCING_ENABLED宏表示,如果启用自定义程序处理顶点实例化,那就需要使用者自行提供一个名为UNITY_INSTANCING_PROCEDURAL_FUNC的宏,以设置顶点的实例ID。如代码中的“第一步”注释所标注的语句提示。如果UNITY_PROCEDURAL_INSTANCING_ENABLED宏未定义,则DEFAULT_UNITY_SETUP_INSTANCE_ID宏定义为调用UnitySetupInstancingID函数,Unity3D定义了一个名为unity_InstanceID的着色器变量,此变量的定义如下:

static uint unity_InstanceID;

unity_InstanceID就是用来索引某一个由输入组装生成的用户定义的顶点属性实例,即示例中的_Color属性的某一个示例。而为了访问这些属性的具体某一个实例值,在UnityInstancing.cginc文件中定义了一个UNITY_ACCESS_INSTANCED_PROP宏。此宏的功能是利用unity_InstanceID取得实例值,具体代码如下:

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

unity_InstanceID的具体计算操作在UnitySetupInstanceID函数中执行。如上面代码所示,如果在非立体渲染的情况下,unity_InstanceID为当前输入的顶点实例id与变量unity_BaseInstanceID之和。unity_BaseInstanceID的定义如下。

着色器常量缓冲区 UnityDrawCallInfo的定义

文件位置:UnityInstancing.cginc 81-89

    // Don't make UnityDrawCallInfo an actual CB on GL
    #if !defined(SHADER_API_GLES3) && !defined(SHADER_API_GLCORE)
        UNITY_INSTANCING_CBUFFER_SCOPE_BEGIN(UnityDrawCallInfo)//着色器常量缓冲区
    #endif
            int unity_BaseInstanceID;//当前渲染批次中是在多例化对象数组中的起始位置
            int unity_InstanceCount;//当前渲染批次中的多例化对象的个数,如果是立体渲染,则以双次渲染之前的个数为准
    #if !defined(SHADER_API_GLES3) && !defined(SHADER_API_GLCORE)
        UNITY_INSTANCING_CBUFFER_SCOPE_END
    #endif

**Unity引入了用来提高渲染效率的批次化渲染(batch render)技术。此技术的基本原理就是通过将一些渲染状态一致的待渲染对象组成一组,一次性提交给GPU进行绘制,而不需要来回地设置渲染状态,这可以显著地节省绘制调用。**使用了GPU多例化技术的待渲染对象的渲染状态基本是一致的。因此对多例化的顶点进行分批次时,引擎底层会指明该批次的起始实例id及该批次有多少个实例id。着色器常量缓冲区中的两个变量便记录了这两个信息。而UNITY_GET_INSTANCE_ID宏则是获取到UNITY_VERTEX_INPUT_INSTANCE_ID宏所表示的当前渲染批次下的实例id。

综上,Shader中标注的UNITY_SETUP_INSTANCE_ID和UNITY_TRANSFER_INSTANCE_ID宏在Direct3D 11平台上展开后的代码如下:

unity_InstanceID=v.instanceID+unity_BaseInstanceID;
o.instanceID=v.instanceID;

展开多例化相关的宏之后的代码

Shader "FoxShader/SimplestInstancedShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" { }
        _Color ("Color", Color) = (1, 1, 1, 1)
    }
    SubShader
    {
        Tags { "RenderType" = "Opaque" }
        LOD 100
        
        Pass
        {
            CGPROGRAM
            
            #pragma vertex vert
            #pragma fragment frag
            //1.让编译指示符宣告使用GPU多例化技术
            #pragma multi_compile_instancing
            
            #include "UnityCG.cginc"
            
            struct appdata
            {
                float4 vertex: POSITION;
                float2 uv: TEXCOORD0;
                //UNITY_VERTEX_INPUT_INSTANCE_ID
                uint instanceID: SV_INSTANCE_ID;   //当前渲染批次下的顶点实例id
            };
            
            struct v2f
            {
                float2 uv: TEXCOORD0;
                float4 vertex: SV_POSITION;
                //2.如果要访问片元着色器中的多例化属性变量,需要使用此宏
				//UNITY_VERTEX_INPUT_INSTANCE_ID
                uint instanceID: SV_INSTANCE_ID;               
            };
            
            //3.开启宏定义
            cbuffer UnityInstancingProps
            {
                float4 _Color[500];
            };
            /*
            // 宏开始宣告要使用GPU多例化技术的变量
            UNITY_INSTANCING_BUFFER_START(Props)
            // 宏声明_Color变量使用技术
            UNITY_DEFINE_INSTANCED_PROP(fixed4, _Color)
            // 宏结束使用多例化技术
            UNITY_INSTANCING_BUFFER_END(Props)
            */
            sampler2D _MainTex;
            float4 _MainTex_ST;
            
            v2f vert(appdata v)
            {
                v2f o;
                //4.如果要访问片元着色器中的多例化属性变量,需要使用此宏
                unity_InstanceID = v.instanceID + unity_BaseInstanceID;
                o.instanceID = v.instanceID;
                /*
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_TRANSFER_INSTANCE_ID(v, o);
                */
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }
            
            fixed4 frag(v2f i): SV_Target
            {
                //5.如果要访问片元着色器中的多例化属性变量,需要使用此宏
                unity_InstanceID = v.instanceID + unity_BaseInstanceID;
                fixed4 col = tex2D(_MainTex, i.uv) * _Color[unity_InstanceID];
                /*
                UNITY_SETUP_INSTANCE_ID(i);
                fixed4 col = tex2D(_MainTex, i.uv) * UNITY_ACCESS_INSTANCED_PROP(Props, _Color);
                */
                return col;
            }
            ENDCG
            
        }
    }
}

Licensed under CC BY-NC-SA 4.0
0