您需要 登录 才可以下载或查看,没有账号?注册
x
通常来说,我们利用ShaderLab实现一个Unity Shader,需要声明一些变量,它可以是全局变量,也可以是局部变量。比方说如下Bling-Phong逐片元光照: - Shader "Custom/BlingPhong" {
- Properties{
- _Diffuse("Diffuse", Color) = (1, 1, 1, 1)
- _Specular("Specular", Color) = (1, 1, 1, 1)
- _Gloss("Gloss", Range(8.0, 256)) = 20
- }
- Subshader{
- Pass {
- Tags{ "LightMode" = "ForwardBase" }
- CGPROGRAM
- #pragma vertex vert
- #pragma fragment frag
- #include "UnityCG.cginc"
- #include "Lighting.cginc"
- fixed4 _Diffuse;
- fixed4 _Specular;
- float _Gloss;
- struct a2v {
- float4 vertex:POSITION;
- float3 normal:NORMAL;
- };
- struct v2f {
- float4 pos:SV_POSITION;
- float3 worldNormal:TEXCOORD0;
- float3 worldPos:TEXCOORD1;
- };
- v2f vert(a2v v) {
- v2f o;
- o.worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
- o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
- o.pos = mul(UNITY_MATRIX_VP, float4(o.worldPos, 1.0));
- return o;
- }
- fixed4 frag(v2f i) : SV_Target {
- fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
- //Transfrom the normal from object space to world space;
- fixed3 worldNormal = normalize(i.worldNormal);
- //get the lightdirection from world space
- fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
- fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
- fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
- fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
- fixed3 halfDir = normalize(worldLightDir + viewDir);
- fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(worldNormal, halfDir)), _Gloss);
- return fixed4(ambient + diffuse + specular,1.0);
- }
- ENDCG
- }
- }
- FallBack "Diffuse"
- }
点击此处复制文本这个代码写的人太多了,这里就不多解释。可以注意到,我们用的一些变量既没有在局部进行声明,也没有在全局进行声明,比如说unity_ObjectToWorld... 慢着,真的没有声明过吗?要不咱看看"UnityCG.cginc"?
打开"UnityCG.cginc",能看到22行包含了一个叫做"UnityShaderVariables.cginc"的头文件,再打开瞧瞧。搜索unity_ObjectToWorld。找到如下代码片段 - CBUFFER_START(UnityPerDraw)
- float4x4 unity_ObjectToWorld;
- float4x4 unity_WorldToObject;
- float4 unity_LODFade; // x is the fade value ranging within [0,1]. y is x quantized into 16 levels
- float4 unity_WorldTransformParams; // w is usually 1.0, or -1.0 for odd-negative scale transforms
- float4 unity_RenderingLayer;
- CBUFFER_END
点击此处复制文本原来,这些变量是在头文件中声明的,那么猜想这个CBUFFER_START可能是某种修饰符。最终在HLSLSupport.cginc中找到了如下代码片段 - <blockquote>#if defined(SHADER_API_D3D11) || defined(UNITY_ENABLE_CBUFFER) || defined(SHADER_API_PSSL)
点击此处复制文本看来这个cbuffer是一个值得研究的小课题。搜搜看cbuffer是不是某种简写呢?答案是肯定的。 Shader Constants (HLSL) - Win32 appsdocs.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-constants
这里面提到的constant buffer就是cbuffer的全称,也就是常量内存。笔者依稀记得这个名词在CUDA里提过,所以复习了一下本科选过的一门课。常量内存是一种容量很小(64KB)但是访问很快的GPU端只读、CPU端可写内存,很适合多个GPU线程同时访问。
废话时间:看上图。橙底表示的存储为片外存储,红底表示的存储为片上存储。片外存储是指存储位于GPU板卡上的专用GDRAM,特点是容量大,CPU端可直接访问(除了Local Memory),但是访问延时很高;片内存储指GPU芯片内的存储,特点是容量小,访问延时低。CPU端只能先将数据写到三种存储当中。那为什么Constant Memory会比Global Memory快呢?我以我二半吊子的FPGA设计经验的理解是,由于这个访问在线程中是单向的,所以我们只需要在片外显存中存放常量内存,然后将数据缓存到片内。片内缓存可以看作是一组寄存器或者BRAM,且只有从片外->片内这一种数据流向,缓存好数据后,读取只要做译码,非常快。这也解释了常量寄存器为什么比较小,因为现在的GPU虽然面积已经很大了,但毕竟片上资源还是寸土寸金,此外额外扩容会涉及某些地址线的变动,改动较大,所以暂时还没扩容(可能也没必要扩容)。
CUDA Memory Modelwww.3dgep.com/cuda-memory-model/
那么我们也许会思考几个问题。这里为什么用到常量内存?或者说用常量内存有什么好处?我们应该什么时候使用它?带着这些问题继续阅读"UnityShaderVariables.cginc"。重点先放在自己需要用到的两个片段: - <blockquote>CBUFFER_START(UnityPerFrame)
点击此处复制文本- CBUFFER_START(UnityPerDraw)
- float4x4 unity_ObjectToWorld;
- float4x4 unity_WorldToObject;
- float4 unity_LODFade; // x is the fade value ranging within [0,1]. y is x quantized into 16 levels
- float4 unity_WorldTransformParams; // w is usually 1.0, or -1.0 for odd-negative scale transforms
- float4 unity_RenderingLayer;
- 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/
- CBUFFER_START(UnityPerMaterial)
- fixed4 _Color;
- CBUFFER_END
点击此处复制文本所以说有个UnityPerMaterial的名称,用来标识这是在每个材质中共用的全局变量。其实在Unity的Inspector中一直有提示,不过我之前没看到 加上了CBUFFER之后 有意思,原来这和Batching有关。对于相同材质的多个几何体,现在就只需要设置一次_Diffuse,_Specular和_Gloss,这能带来性能上的提升。在相应的代码里打开SRP Batching即可。 - GraphicsSettings.useScriptableRenderPipelineBatching = true;
点击此处复制文本如图绘制八个球 在利用CBUFFER做优化前,绘制顺序如图 当我们将材质相关的变量用CBUFFER包起来后,新的绘制顺序如图 Draw Call减少了吗?其实没有,事实上SRP Batch只是优化了Draw Call的顺序,以减少CBUFFER的设置,从而提高性能。
|