Unity Shader的内置变量与常量内存
TA图形学渲染引擎Shader材质球技术综合
显示全部 7
1422 1
实名

通过了实名认证的内容创造者

发布于 2022-4-11 05:58:34

您需要 登录 才可以下载或查看,没有账号?注册

x
image.png

通常来说,我们利用ShaderLab实现一个Unity Shader,需要声明一些变量,它可以是全局变量,也可以是局部变量。比方说如下Bling-Phong逐片元光照:
  1. Shader "Custom/BlingPhong" {
  2.     Properties{
  3.         _Diffuse("Diffuse", Color) = (1, 1, 1, 1)
  4.         _Specular("Specular", Color) = (1, 1, 1, 1)
  5.         _Gloss("Gloss", Range(8.0, 256)) = 20
  6.     }
  7.     Subshader{
  8.         Pass {

  9.             Tags{ "LightMode" = "ForwardBase" }

  10.             CGPROGRAM
  11.             #pragma vertex vert
  12.             #pragma fragment frag
  13.             #include "UnityCG.cginc"
  14.             #include "Lighting.cginc"

  15.             fixed4 _Diffuse;
  16.             fixed4 _Specular;
  17.             float _Gloss;

  18.             struct a2v {
  19.                 float4 vertex:POSITION;
  20.                 float3 normal:NORMAL;
  21.             };

  22.             struct v2f {
  23.                 float4 pos:SV_POSITION;
  24.                 float3 worldNormal:TEXCOORD0;
  25.                 float3 worldPos:TEXCOORD1;
  26.             };

  27.             v2f vert(a2v v) {
  28.                 v2f o;
  29.                 o.worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
  30.                 o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
  31.                 o.pos = mul(UNITY_MATRIX_VP, float4(o.worldPos, 1.0));
  32.                 return o;
  33.             }

  34.             fixed4 frag(v2f i) : SV_Target {
  35.                 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

  36.                 //Transfrom the normal from object space to world space;
  37.                 fixed3 worldNormal = normalize(i.worldNormal);

  38.                 //get the lightdirection from world space
  39.                 fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
  40.                 fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
  41.                 fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
  42.                 fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
  43.                 fixed3 halfDir = normalize(worldLightDir + viewDir);
  44.                 fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(worldNormal, halfDir)), _Gloss);
  45.                 return fixed4(ambient + diffuse + specular,1.0);
  46.             }
  47.             ENDCG
  48.         }
  49.     }
  50.     FallBack "Diffuse"
  51. }
点击此处复制文本
这个代码写的人太多了,这里就不多解释。可以注意到,我们用的一些变量既没有在局部进行声明,也没有在全局进行声明,比如说unity_ObjectToWorld... 慢着,真的没有声明过吗?要不咱看看"UnityCG.cginc"?
v2-961cdd8d33b93a7f89008797aaa3f91a_720w.jpg
打开"UnityCG.cginc",能看到22行包含了一个叫做"UnityShaderVariables.cginc"的头文件,再打开瞧瞧。搜索unity_ObjectToWorld。找到如下代码片段
  1. CBUFFER_START(UnityPerDraw)
  2.     float4x4 unity_ObjectToWorld;
  3.     float4x4 unity_WorldToObject;
  4.     float4 unity_LODFade; // x is the fade value ranging within [0,1]. y is x quantized into 16 levels
  5.     float4 unity_WorldTransformParams; // w is usually 1.0, or -1.0 for odd-negative scale transforms
  6.     float4 unity_RenderingLayer;
  7. CBUFFER_END
点击此处复制文本
原来,这些变量是在头文件中声明的,那么猜想这个CBUFFER_START可能是某种修饰符。最终在HLSLSupport.cginc中找到了如下代码片段
  1. <blockquote>#if defined(SHADER_API_D3D11) || defined(UNITY_ENABLE_CBUFFER) || defined(SHADER_API_PSSL)
点击此处复制文本
看来这个cbuffer是一个值得研究的小课题。搜搜看cbuffer是不是某种简写呢?答案是肯定的。
Shader Constants (HLSL) - Win32 apps​docs.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-constants
这里面提到的constant buffer就是cbuffer的全称,也就是常量内存。笔者依稀记得这个名词在CUDA里提过,所以复习了一下本科选过的一门课。常量内存是一种容量很小(64KB)但是访问很快的GPU端只读、CPU端可写内存,很适合多个GPU线程同时访问。
CUDA存储模型,图片取自下方链接。

废话时间:看上图。橙底表示的存储为片外存储,红底表示的存储为片上存储。片外存储是指存储位于GPU板卡上的专用GDRAM,特点是容量大,CPU端可直接访问(除了Local Memory),但是访问延时很高;片内存储指GPU芯片内的存储,特点是容量小,访问延时低。CPU端只能先将数据写到三种存储当中。那为什么Constant Memory会比Global Memory快呢?我以我二半吊子的FPGA设计经验的理解是,由于这个访问在线程中是单向的,所以我们只需要在片外显存中存放常量内存,然后将数据缓存到片内。片内缓存可以看作是一组寄存器或者BRAM,且只有从片外->片内这一种数据流向,缓存好数据后,读取只要做译码,非常快。这也解释了常量寄存器为什么比较小,因为现在的GPU虽然面积已经很大了,但毕竟片上资源还是寸土寸金,此外额外扩容会涉及某些地址线的变动,改动较大,所以暂时还没扩容(可能也没必要扩容)。
CUDA Memory Model​www.3dgep.com/cuda-memory-model/
那么我们也许会思考几个问题。这里为什么用到常量内存?或者说用常量内存有什么好处?我们应该什么时候使用它?带着这些问题继续阅读"UnityShaderVariables.cginc"。重点先放在自己需要用到的两个片段:
  1. <blockquote>CBUFFER_START(UnityPerFrame)
点击此处复制文本
  1. CBUFFER_START(UnityPerDraw)
  2. float4x4 unity_ObjectToWorld;
  3. float4x4 unity_WorldToObject;
  4. float4 unity_LODFade; // x is the fade value ranging within [0,1]. y is x quantized into 16 levels
  5. float4 unity_WorldTransformParams; // w is usually 1.0, or -1.0 for odd-negative scale transforms
  6. float4 unity_RenderingLayer;
  7. CBUFFER_END
点击此处复制文本

注意到它们在命名上有一定的差异,一个叫UnityPerFrame,另一个叫UnityPerDraw,因此可以反过来推出,cbuffer实际上是有生命周期的。这一点也不奇怪 ,前面说过,常量内存虽然快,但是容量非常小(64KB),得省着点用。不然把顶点数据和纹理都存到常量内存里,岂不妙哉?这些数在底层应该是根据绘制的状态,由CPU发送给GPU的(一个猜想,不一定对,欢迎指正)。譬如说这个UnityPerFrame,看名字和它里面包含的变量,很显然这些变量在每一帧绘制都保持不变。那这下一切就明朗了:虽然某一帧里可能提交了很多次draw call,但是UnityPerFrame里的常量其实只需要设置一次,并且多个线程都会用到这些变量,这能加快显存访问速度,降低CPU到GPU的吞吐。UnityPerDraw也是同理,不过为什么是PerDraw而不是PerObject,这个应该与合批或者是Instancing相关。
回看到最开始的那个Bling-Phong的代码,实际上我也使用了三个全局变量:_Diffuse,_Specular和_Gloss。那能不能把用户定义的全局变量也用常量内存缓存起来呢?毕竟如果这个shader对应的材质用得很多的话,貌似能省不少。那就直接百度一下,看到有个人这么写
【SRP Batcher】How to make a Default Unlit Shader compatible?​forum.unity.com/threads/srp-batcherhow-to-make-a-default-unlit-shader-compatible.877435/
  1. CBUFFER_START(UnityPerMaterial)
  2.     fixed4 _Color;
  3. CBUFFER_END
点击此处复制文本
所以说有个UnityPerMaterial的名称,用来标识这是在每个材质中共用的全局变量。其实在Unity的Inspector中一直有提示,不过我之前没看到
加上了CBUFFER之后
有意思,原来这和Batching有关。对于相同材质的多个几何体,现在就只需要设置一次_Diffuse,_Specular和_Gloss,这能带来性能上的提升。在相应的代码里打开SRP Batching即可。
  1. GraphicsSettings.useScriptableRenderPipelineBatching = true;
点击此处复制文本
如图绘制八个球
在利用CBUFFER做优化前,绘制顺序如图
当我们将材质相关的变量用CBUFFER包起来后,新的绘制顺序如图
Draw Call减少了吗?其实没有,事实上SRP Batch只是优化了Draw Call的顺序,以减少CBUFFER的设置,从而提高性能。

收模型,杂志,教程,插图,插件,软件,拿来卖。
使用道具 <
MJ漫步  发表于 2022-4-11 23:00:41  
2#
很棒的资源
回复 收起回复
使用道具
您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表