当前位置:   article > 正文

【UnityShader入门精要学习笔记】第五章(2)优化你的Shader

【UnityShader入门精要学习笔记】第五章(2)优化你的Shader

在这里插入图片描述
本系列为作者学习UnityShader入门精要而作的笔记,内容将包括:

  • 书本中句子照抄 + 个人批注
  • 项目源码
  • 一堆新手会犯的错误
  • 潜在的太监断更,有始无终

总之适用于同样开始学习Shader的同学们进行有取舍的参考。



复习

知识点复习

在上节课中,我们编写了一个Shader并将其由简单逐步完善,从中我们理解了Shader的基本使用方法:

  1. shader的结构包括 Shader Name,SubShader块,pass块FallBack四部分
  2. 我们通过预定义指令 #pragma 指定不同着色器阶段使用的函数,在Shader中我们可以通过语义来为变量赋值,一些语义让我们访问Untiy提供的数据。而一些语义具有特殊含义,在定义着色器函数的时候必须使用,例如a2v阶段的输出需要使用语义SV_POSITION,而v2f阶段的输出需要使用语义SV_Target
  3. 我们可以自定义着色器函数的输入和输出,通过自定义结构体来包含我们需要使用的变量集合。
  4. 我们可以通过#include "XXX.cginc"代码,来包含unity提供的.cginc头文件,使用内置的函数(例如UnityObjectToClipPos)和变量便利Shader代码的编写。
  5. 想要在材质面板上使用属性,我们需要在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
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63

其中用到的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;
#endif
	fixed4 color : COLOR;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

可以看出,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
  • 1
  • 2
  • 3
  • 4

其中,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);
  • 1
  • 2
  • 3
  • 4

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);
	// ...
}
  • 1
  • 2
  • 3
  • 4
  • 5

除此之外,tex2D是一个对纹理进行采样的函数,但在DIrectX 9/11也是不支持的,需要使用tex2Dlod来代替

tex2Dlod(tex, float4(uv, 0, 0))
  • 1

在之前章节中我们也说过不同语义在不同平台可能不能编译,因此推荐使用SV_POSITION,SV_Target语义来描述输入输出变量。


Shader整洁之道

本章给出了一些写出规范Shader代码的建议,整洁的代码是提升效率所必须的。

浮点类型

在CG/HLSL中,有三种精度类型的数值:float,half和fixed。这些精度将决定计算结果的数值范围。

类型精度
float32位浮点数
half16位浮点数
fixed11位浮点数

大多数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)
}
  • 1
  • 2
  • 3
  • 4

这些代码可能导致Shader的崩溃,即使不崩溃,得到的结果也是不确定的。

一个解决方法是,对那些除数可能为0的情况,强制截取到非0范围。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/317543
推荐阅读
相关标签
  

闽ICP备14008679号