[学习记录]Unity-Shader-常量缓冲区(CBUFFER)

发布于:2025-07-31 ⋅ 阅读:(13) ⋅ 点赞:(0)

一.CBUFFER是什么

1.CBUFFER的概念

        常量缓冲区(Constant Buffer,简称 CBuffer)是现代 GPU 编程中一个非常核心且重要的概念。它是一种 GPU 内存区域,用于存储 Shader 程序在执行过程中需要访问的不变数据。常量缓冲区 (CBUFFER_START/END) 可以同时在CGPROGRAM和HLSLPROGRAM块中使用。

        这里的“不变”是指:在一次 GPU 绘制调用(Draw Call)或一个计算着色器分派(Dispatch Call)的整个执行过程中,这些变量的值是固定的。它们不会像顶点属性那样每个顶点都不同,也不会像插值变量那样每个像素都不同。

2. CBUFFER作用

(1)提高数据传输效率: CPU 到 GPU 的数据传输是性能瓶颈之一。CBuffer 允许 CPU 将多个 Shader 变量打包成一个更大的数据块,然后一次性上传到 GPU,大大减少了传输调用次数和开销,比零散地传输每个变量更高效。

(2)优化 GPU 内存管理和缓存: GPU 可以将整个常量缓冲区缓存起来。如果两个 Draw Call 之间某个常量缓冲区的内容没有改变,GPU 就可以直接使用缓存中的数据,避免重复传输,提高缓存命中率。

(3)内存对齐: CBuffer 强制或鼓励数据按照 GPU 友好的方式(通常是 16 字节对齐)进行内存布局,这有助于 GPU 更高效地访问数据。

(4)清晰的数据管理: 开发者可以根据数据的更新频率和用途,将变量组织到不同的常量缓冲区中,使得数据管理更加模块化和高效。

3. CBUFFER的组成部分

一个常量缓冲区主要由以下部分组成:

(1)区域定义宏

CBUFFER_START 和CBUFFER_END宏,标志着常量缓冲区的起始和结束。

(2)缓冲区名称

一个标识符,是给常量缓冲区起的名字(在 HLSL 中使用cbuffer关键字后的名称,或在 Unity 中使用CBUFFER_START() 宏的参数)。这个名称在 Shader 内部用于引用这个数据块,同时也是 CPU 端(C# 代码)用来设置这个缓冲区内容的标识。

(3)Uniform 变量

声明在缓冲区内部的变量。这些变量就是 Shader 在运行时可以访问的“常量”。

注意对CBUFFER中全局变量的类型是有要求的:

CBUFFER 内部的变量类型必须是

基本数值类型:

(1)float,half,fixed(及其向量和矩阵形式,如float2, float4, float2x2, ​​​​​​​float3x3)

(2)int,uint(及其向量形式(如int2,uint4)

(3)bool(及其向量形式,但在 Shader 中不常用)

这些基本类型的数组:

float4 myArr[10],float3x3 myMArr[5];

核心限制:CBUFFER 内部不能直接存储“资源句柄”或“复杂对象”。

所以不能在CBUFFER内声明像纹理和着色器资源的类型

纹理 (Textures):

  如TEXTURE2D, TEXTURE2D_ARRAY, TEXTURE3D, TEXTURECUBE 等。纹理是复杂的 GPU 资源,它们不是简单的数值数据。CBUFFER 用于存储数值常量,而纹理本身是存储在 GPU 纹理内存中的巨大数据块,Shader 通过纹理采样器来访问它们。CBUFFER 只能存储指向这些纹理的索引或间接引用(如果需要的话,但通常不需要),而不是纹理本身。

采样器状态 (Samplers / Sampler States):

  如SAMPLER, SAMPLER_STATE 等。 采样器定义了如何从纹理中读取像素(如过滤模式,寻址模式)。它们是 GPU 状态的一部分,通常绑定到特定的纹理单元或通过单独的采样器对象管理,而不是作为数值数据存储在 CBUFFER 中。

示例标准写法

// 正确的 CBUFFER 使用
CBUFFER_START(MyCustomData)
    float4 _MyColor;           
    float  _MyIntensity;      
    float4x4 _MyTransform;    
    float4 _MyFloatArray[5];   
CBUFFER_END

// 纹理和采样器声明在 CBUFFER 外部
TEXTURE2D(_MainTex);           // 纹理,在 CBUFFER 外部
SAMPLER(sampler_MainTex);      // 采样器,在 CBUFFER 外部

4.CBUFFER的意义

GPU 数据传输的效率与管理

        Shader 程序的 Uniform 变量(也就是我们在这里讨论的全局变量,如_ClipRect,_Time,材质颜色等)是在 CPU 上设置,然后传输到 GPU 上的。这个传输过程是影响性能的关键之一。

        传统的 GPU 架构在传输 Uniform 变量时,效率并不是很高。如果每个变量都单独传输,那么每次 Draw Call 都会产生大量的小数据包传输。

常量缓冲区 (Constant Buffer) 就是为了解决这个问题而引入的。它的核心思想是:

(1)打包传输 (Batch Transfer): 将多个相关的 Uniform 变量打包成一个更大的数据块。CPU 一次性将这个数据块传输到 GPU,而不是零散地传输每一个变量。这大大减少了传输的开销。

(2)GPU 端的缓存: GPU 会将这些常量缓冲区缓存起来。如果 Draw Call 之间某个常量缓冲区的内容没有改变,GPU 就不需要重新接收和处理这部分数据,直接使用缓存中的副本。

二.CBUFFER相关语法

        CBUFFER_STARTCBUFFER_END宏,两个宏是对标准 HLSL cbuffer 关键字的封装,并附加了 Unity 特定的功能和约定。

CBUFFER_START/END语法只是提供了一种明确的、语义化的方式来告诉编译器如何组织这些数据,并帮助开发者了解这些数据的更新频率。或者仅仅作为一种分区手段。

即使你把变量放在外面,它本质上仍然是作为 Uniform 数据放进一个常量缓冲区被处理的。

        在现代 GPU 架构下,所有 Uniform 变量(Shader 程序运行时不变的变量)最终都会被组织到常量缓冲区中,然后一次性上传到 GPU。


三.CBUFFER相关知识点

1.可选择不显式声明CBUFFER_START/END块

        Unity中,编译器会自动处理。如果你在 HLSL 代码中声明了全局变量(即不在任何函数内部的变量),但没有将它们包裹在 CBUFFER_START/END块中,Shader 编译器会自动为这些变量创建和管理常量缓冲区。

自动创建常量缓冲区: 编译器会根据变量的类型、数量和使用频率,智能地将它们分组到由驱动程序和硬件管理的常量缓冲区中。

默认命名和更新频率: 这些自动创建的常量缓冲区通常会有编译器生成的默认名称,并且它们的更新频率会根据变量的用途和 Unity 的内部规则来确定。例如:

Unity 内置的“全局”常量缓冲区: 像_Time,unity_MatrixVP这样的内置变量,会被 Unity 自动放置到其预定义的全局常量缓冲区中。

2.仍推荐显式声明CBUFFER_START/END

        尽管编译器会自动处理,但显式声明常量缓冲区有几个重要的原因和好处:

语义清晰和可读性:

        CBUFFER_START(UnityPerDraw) 这样的声明,清晰表明此CBUFFER里面的变量是每 Draw Call 更新,有助于提高代码的可读性和可维护性。

        当然,括号里的标识符可以自定义,虽然Unity中有内置的系统标识符(如UnityPerDrawUnityPerMaterial等),但这些大多都是用于Unity底层实现部分,日常使用可以按方便命名自定义标识符。

更好的控制和组织:

        通过显式声明可以更好地组织你的 Uniform 变量。将不同更新频率或不同用途的变量分别放入不同的CBUFFER中。例如UnityPerDraw用于每个 Draw Call 变化的属性,UnityPerFrame用于每帧变化的全局属性。这有助于编译器和驱动程序更有效地管理数据,因为它们可以根据缓冲区的更新频率进行优化,避免不必要的上传。

3.Unity如何理解预定义名称

        有关底层功能实现,日常使用特别无需关心标识符的命名问题。

        Unity有一些引擎 预定义好的、具有特定语义的 常量缓冲区名称,如UnityPerFrame、UnityPerCamera、UnityPerDraw、UnityPerMaterial。

        Unity 正是通过理解这些预定义的名字,才能知道如何自动填充这些缓冲区。

(1)内部约定: Unity 引擎的 C++ 核心代码和渲染管线在设计时,就预设了这些特定的常量缓冲区名称。当 Unity 编译你的 Shader 时,它会识别这些名称。

(2)数据填充逻辑: Unity 内部有大量的 C++ 或 C# 代码,负责在渲染循环的各个阶段(每帧、每摄像机、每 Draw Call、每材质变更时)计算相关的渲染数据,然后将其填充到对应名称的常量缓冲区中。例如:

UnityPerFrame: 当一帧开始渲染时,Unity 会计算像 _Time(游戏运行时间)、unity_DeltaTime(帧间隔时间)等全局时间信息,并将它们写入名为 UnityPerFrame 的常量缓冲区。

UnityPerDraw: 这是最频繁更新的缓冲区。在每个 Draw Call 之前,Unity 会计算当前要绘制的模型的模型-世界矩阵(unity_ObjectToWorld)、UI 元素的裁剪矩形(_ClipRect)等与当前绘制对象直接相关的信息,然后写入 UnityPerDraw

UnityPerMaterial: 当 Unity 遇到一个使用了新材质的 Draw Call 时,它会从该材质的属性中读取数据(例如 _Color_MainTex_ST 等),然后将其写入名为 UnityPerMaterial 的常量缓冲区。

(3)Shader 编译器识别: Unity 的 Shader 编译器 (CG/HLSL 编译器,如 HLSLcc 或 DXC) 在解析你的 .shader 文件时,会识别这些特殊的 CBUFFER_START 名称。它知道这些名称对应的变量是 Unity 引擎在运行时负责填充的。

        平常写Shader的时候。我们一般自定义CBuffer的标识名(可能用不上内置的标识名),并需要自己填充CBiffer中的全局变量: 

比如定义自己的CBUFFER_START(CustomData) 需要

(1)在Shader内将其中的全局变量像属性一样暴露在材质面板上

(2)在C# 代码中通过MaterialPropertyBlock或Shader.SetVector()/SetFloat() 等方式。

显式地、手动地将数据写入到这个名为CustomData的常量缓冲区中的变量里。

        当然这不意味着内置标识符是不重要的,相反Unity 正是通过理解这些重要预定义的名字,才能知道如何自动填充这些缓冲区,实现其底层功能。

本篇完——


网站公告

今日签到

点亮在社区的每一天
去签到