本文主要是介绍【UnityShader入门精要学习笔记】第五章(2)优化你的Shader,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
本系列为作者学习UnityShader入门精要而作的笔记,内容将包括:
- 书本中句子照抄 + 个人批注
- 项目源码
- 一堆新手会犯的错误
- 潜在的太监断更,有始无终
总之适用于同样开始学习Shader的同学们进行有取舍的参考。
文章目录
- 复习
- 知识点复习
- 如何Debug
- 使用假彩色图像
- 使用Visual Studio
- FrameDebugger
- 小心渲染平台差异
- Shader的语法差异
- Shader整洁之道
- 浮点类型
- 规范语法
- 避免不必要的计算
- 慎用分支和循环语句
- 不要除以0
复习
知识点复习
在上节课中,我们编写了一个Shader并将其由简单逐步完善,从中我们理解了Shader的基本使用方法:
- shader的结构包括 Shader Name,SubShader块,pass块FallBack四部分
- 我们通过预定义指令 #pragma 指定不同着色器阶段使用的函数,在Shader中我们可以通过语义来为变量赋值,一些语义让我们访问Untiy提供的数据。而一些语义具有特殊含义,在定义着色器函数的时候必须使用,例如
a2v阶段的输出需要使用语义SV_POSITION
,而v2f阶段的输出需要使用语义SV_Target
。 - 我们可以自定义着色器函数的输入和输出,通过自定义结构体来包含我们需要使用的变量集合。
- 我们可以通过
#include "XXX.cginc"
代码,来包含unity提供的.cginc
头文件,使用内置的函数(例如UnityObjectToClipPos
)和变量便利Shader代码的编写。 - 想要在材质面板上使用属性,我们需要在
Properties
中定义属性及其默认值,并在SubShader块中定义其对应的变量并在函数中进行处理。
如何Debug
对一个Shader进行调试是十分困难的,如果发现效果不对,我们可能要花非常多的时间来找到问题所在,因为在Shader中可以选择的调试方式非常优先,甚至连简单的输出都不行。通常的调试方法,例如print出关键数据,在编译器里找报错,断点调试这些在Shader中都做不到。
下面介绍了两种对Shader的调试方法:
使用假彩色图像
假彩色图像指的是将需要调试的变量映射到[0,1]之间,将它们作为颜色输出到屏幕上,然后通过屏幕上显示的像素颜色来判断这个值是否正确。由于我们没法打印Shader中的值,只能将值作为颜色输出进行判断。
由于颜色值的分量在 [ 0 , 1 ] [0,1] [0,1],因此我们需要将要调试的值域范围映射为 [ 0 , 1 ] [0,1] [0,1]之后再输出,如果你不了解一个变量的范围,这说明你不了解shader中的计算,就只能不停尝试,一个提示是:颜色分量中任何大于1的值都会被设置为1,而小于0的值都会设置为0。因此如果不断尝试映射方式也能够找出颜色范围。
如果需要调试的是一个一维数据,那么可以选择一个单独的颜色分量(R分量)进行输出,其他分量置为0,(当然a分量是1)。如果是多维数据,则可以选择对它的每一个分量单独调试,或者一次性输出多个颜色分量。
在Github的项目demo的Scence5_2中展示了使用假彩色图像可视化一些模型数据的方法,如法线,切线,纹理坐标,顶点颜色,以及他们的运算结果等等:
Shader "Unity Shaders Book/Chapter 5/False Color" {SubShader {Pass {CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct v2f {float4 pos : SV_POSITION;fixed4 color : COLOR0;};v2f vert(appdata_full v) {v2f o;o.pos = UnityObjectToClipPos(v.vertex);// 可视化法线方向o.color = fixed4(v.normal * 0.5 + fixed3(0.5, 0.5, 0.5), 1.0);// 可视化切线方向o.color = fixed4(v.tangent.xyz * 0.5 + fixed3(0.5, 0.5, 0.5), 1.0);// 可视化副切线方向fixed3 binormal = cross(v.normal, v.tangent.xyz) * v.tangent.w;o.color = fixed4(binormal * 0.5 + fixed3(0.5, 0.5, 0.5), 1.0);// 可视化第一组纹理坐标o.color = fixed4(v.texcoord.xy, 0.0, 1.0);// 可视化第二组纹理坐标o.color = fixed4(v.texcoord1.xy, 0.0, 1.0);// 可视化第一组纹理坐标的小数部分o.color = frac(v.texcoord);if (any(saturate(v.texcoord) - v.texcoord)) {o.color.b = 0.5;}o.color.a = 1.0;// 可视化第二组纹理坐标的小数部分o.color = frac(v.texcoord1);if (any(saturate(v.texcoord1) - v.texcoord1)) {o.color.b = 0.5;}o.color.a = 1.0;// 可视化顶点颜色
// o.color = v.color;return o;}fixed4 frag(v2f i) : SV_Target {return i.color;}ENDCG}}
}
其中用到的appdata_full
结构体是Unity内置的结构体,可以在UnityCG.cginc
中找到它的定义:
struct appdata_full{float4 vertex:POSITION;float4 tangent : TANGENT;float3 normal: NORMAL;float4 texcoord : TEXCOORD0;float4 texcoord1 : TEXCOORD1;float4 texcoord2 : TEXCOORD2;float4 texcoord3 : TEXCOORD3;
#if defined(SHADER_API XBOX360)half4 texcoord4 : TEXCOORD4;half4 texcoord5 : TEXCOORD5;
#endiffixed4 color : COLOR;
}
可以看出,appdata_full
结构体几乎包含了所有的模型数据,和其命名也相称,代表了从应用阶段能够给出的所有模型数据。
用项目中自带的拾色器脚本,来检测屏幕的RGB假彩色图像值。
使用Visual Studio
VS中也提供了一种对UnityShader的调试功能——Graphics Debugger。
其原理是截取屏幕上帧并通过GPU反编译获取Shader信息。
通过“视图”->“选项”->“图形诊断”->“启用 GPU 反汇编收集”来启用 GPU 反汇编。
若要使更改生效,必须重新打开 vsglog。
FrameDebugger
或者我们也可以使用Unity原生的帧调试器,它可以用于查看渲染该帧时进行的各种渲染事件(Event),包括DrawCall序列,或者清空帧缓存等操作。
通过最上方滑动条检查渲染时发生的顺序事件。我们可以通过前进后退按钮查看事件的发生。
左侧是事件树,我们可以看到不同阶段都是什么事件。右侧则展示了事件的参数。通常以Draw开头的事件都是一个DrawCall,一个Gameobject的DrawCall渲染结果会在Hierarchy面板上高亮显示。
如果选中的DrawCall是对一个渲染纹理(RenderTexture)的渲染操作,那么这个渲染纹理就可以在Game视图中看到。
帧调试器的原理其实比较简单,就是用停止渲染的方法来查看事件渲染的结果。
小心渲染平台差异
之前我们讲过OpenGL和DirectX的屏幕坐标空间是不同的,在渲染时,我们不仅可以把渲染结果输出到屏幕上,还可以输出到不同的渲染目标(Render Target)。大多数情况下,这样的差异不会造成任何影响。但当我们要使用渲染到纹理技术(Render Texture)时,如果不采取任何措施,就会出现纹理翻转的情况,不过大部分情况下Unity会根据平台类型为我们自动翻转屏幕图像纹理(Graphic.Blit(source, dest)方法),所以直接导出游戏时不会因为平台不同导致渲染问题了。
但是如果开启了抗锯齿后并在此时使用了渲染纹理技术,在这种情况下,Unity首先渲染得到屏幕图像,再由硬件进行抗锯齿处理后,得到一张渲染纹理来供我们进行后续处理。在这种情况下,Unity就不能在渲染纹理时为我们翻转图像纹理了。而如果只处理一张渲染图像时仍然不影响,只有当我们开启了抗锯齿,且需要处理多张图像,且在DirectX平台上,才有可能出现不同渲染图像的纹理坐标不同的情况。
当出现上述情况,我们就需要手动翻转顶点着色器中的纹理坐标:
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)uv.y = 1 - uv.y;
#endif
其中,UNITY_UV_STARTS_AT_TOP用于判断是否是DirectX平台,如果是,我们通过_MainTex_TexelSize.y 来判断是否开启了抗锯齿,若<0代表了开启,此时则手动翻转UV。(有时上述代码可能出现问题,处理后反而颠倒了,往往是因为Graphic.Blit方法的使用,如果发生问题就往这个思路去Debug)
Shader的语法差异
在不同平台的Shader有语法差异,有时会因为某些平台对Shader的语义严格而造成问题,因此我们定义时需要使用严格的语法,一般的要求是对使用的变量进行符合格式的初始化,例如:
incorrect number of arguments to numeric-type constructor (compiling for d3dll)
(不同平台对语义检查更严格,导致编译错误)
简略的定义,在某些平台可能报错
float4 v = float4(0.0);
完整定义
float4 v = float4(0.0, 0.0, 0.0, 0.0);
output parameter ' o ' not completely ini alized {compi ng for d3dll)
(报错原因是由于使用变量o时未初始化赋值,导致编译错误)
void vert (inout appdata_full v , out Input o) {// 使用Unity内置的UNITY_INITIALIZE_OUTPUT宏对输出结构体o进行初始化UNITY_INITIALIZE_OUTPUT(Input,o);// ...
}
除此之外,tex2D是一个对纹理进行采样的函数,但在DIrectX 9/11也是不支持的,需要使用tex2Dlod来代替
tex2Dlod(tex, float4(uv, 0, 0))
在之前章节中我们也说过不同语义在不同平台可能不能编译,因此推荐使用SV_POSITION,SV_Target
语义来描述输入输出变量。
Shader整洁之道
本章给出了一些写出规范Shader代码的建议,整洁的代码是提升效率所必须的。
浮点类型
在CG/HLSL中,有三种精度类型的数值:float,half和fixed。这些精度将决定计算结果的数值范围。
类型 | 精度 |
---|---|
float | 32位浮点数 |
half | 16位浮点数 |
fixed | 11位浮点数 |
大多数GPU对不同类型都会按照最高精度进行计算,特别在PC平台上很呐看出不同精度的区别,而在移动平台的GPU上可能会有些差异。
一个基本的建议是,尽可能使用精度较低的类型,这样可以优化Shader的性能。我们可以用fixed类型来存储颜色和单位向量。
规范语法
规范语法的要求无非之前讲到的几点:
- 进行严格的语法定义
- 使用变量前保证初始化
- 使用
SV_POSITION,SV_Target
等语义
避免不必要的计算
如果我们毫无节制地在Shader中(尤其是片元着色器)中进行了大量计算,那么可能会报错:
temporary register limit of 8 exceeded
或
'Arithmetic instruction limit of 64 exceeded; 65 arithmetic instructions needed to compile program
出现这些信息往往是因为我们在Shader中进行了过多的运算,使得需要的临时寄存器数目或者指令数目超过了当前可支持的数目。
不同的Shader Target、不同的着色器阶段,我们可以使用的临时寄存器和指令数目都是不同的。这些往往是由Shader Model的等级来决定的。Shader Model的等级越高,Shader能使用的运算指令数目、寄存器个数等特性和能力越强。
使用Shader Target宏可以指定使用的Shader Model等级:
指令 | 描述 |
---|---|
#pragma target 2.0 | 默认的Shader Target等级,相当于Direct3D 9上的ShaderModel 2.0 |
#pragma target 3.0 | 相当于Direct3D 9上的ShaderModel 3.0 |
#pragma target 4.0 | 相当于Direct3D 10上的ShaderModel 4.0 (只在DirectX 11 和Xbox One/PS4 平台上支持) |
#pragma target 5.0 | 相当于Direct3D 11上的ShaderModel 5.0 (只在DirectX 11 和Xbox One/PS4 平台上支持) |
慎用分支和循环语句
使用分支和循环语法,在其他高级语言中是基础中的基础了,但是在编写Shader的时候,我们要对它们格外小心。
在Shader上使用这些流程控制语句,往往导致Shader的性能下降,一个解决方法是:我们应该将这些计算向流水线上端移动,例如把放在片元着色器中的计算放到顶点着色器中,或者直接放在CPU中计算,再把结果传递给Shader。
如果真的不可避免需要使用分支语句进行计算,那么一些建议是:
- 分支判断语句使用的条件变量最好是常数,即在Shader运行过程中不会变换
- 每个分支中包含的操作指令数尽可能少
- 分支的嵌套层数尽可能少
- 如果某些库已经实现了宏或者方法,就直接去用它们的方法,尽量避免自己实现
不要除以0
在编译Shader的时候,我们可能会忘记不能除以0这个基本常识,更糟糕的是编译器可能不会报错,这些代码的结果往往是不可预测的:
fixed4 frag(v2f i):SV_Target
{return fixed4(0.0/0.0, 0.0/0.0, 0.0/0.0, 1.0)
}
这些代码可能导致Shader的崩溃,即使不崩溃,得到的结果也是不确定的。
一个解决方法是,对那些除数可能为0的情况,强制截取到非0范围。
这篇关于【UnityShader入门精要学习笔记】第五章(2)优化你的Shader的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!