第五章 开始Unity Shader学习之旅
自己在学习的同时记录的笔记吗,随着之后学习陆续更新每一章(如果能同时帮到一些其他人就更好了),欢迎批评和提出建议。5.1 本书所使用的软件和环境
本书使用的unity版本是 Unity 5.2.1 免费版,为避免一些问题建议使用该版本或更高版本(因为一些函数、宏 或者说变量是该版本或更高版本才有的, 同时unity版本不同底层代码可能也不同)
本书使用的是 Mac OS X10.9.5 系统, 若系统不同,需要注意当系统所使用的图像编程接口不同时所导致的问题。比如OpenGL渲染纹理(Render Texture)的(0,0)点是在左下角,而在DirectX中,其(0,0) 点则是在左上角。
5.2 一个最简单的顶点/片元着色器
顶点/片元着色器结构:
Shader "MyShaderName"{
Properties{
//属性
}
SubShader{
//针对显卡A的SubShader
Pass{
//设置渲染状态和标签
//开始CG代码片段
CGPROGRAM
//该代码片段的编译指令
#pragma vertex vert
#pragma fragment frag
//CG代码
ENDCG
//其它设置
}
//其它需要的Pass
}
SubShader{
//针对显卡B的 SubShader
}
//上述SubShader 都失败后用于回调的Unity Shader
FallBack"VertexLit"
}在这里复习下 Unity Shader 文本文件的结构
第一行的Shader语义为该shader命名,并可指定搜索该shader的路径
之后是 Properties 属性语义块,该语义块是材质和Unity Shader的桥梁,里面存放该shader文件所用到的属性(同时这里面的属性会显示在材质编辑面板里),同时我们也可以扩展材质编辑面板所可以显示的数据的类型,具体方法在之后文章中(。
每个Unity Shader文件至少含有一个SubShader语义块,当然也可以含有多个,当运行该Unity Shader时会选择第一个能够在目标平台上运行的SubShader运行。通过该机制使得该Unity Shader可以适应不同的显卡
SubShader 包含的定义通常如下
SubShader{
//可选的
//可选的
Pass{
}
//Other Passes
} 你可以为每个SubShader 设置标签和相关状态, 同时每个SubShader可以含有1个或多个Pass,在每个渲染流程会运行所有的Pass,所以若Pass过多可能会导致渲染性能的下降,因此应尽量保证使用最少数量的Pass。 你也可以在每个Pass里面设置标签和状态,但SubShader中的一些标签是特定的,也就是说这些标签和Pass里的标签是不同的。不过对于状态,其使用的语法在SubShader和Pass中是相同的。但是如果我们在SubShader中进行了这些设置,这些设置会应用于所有Pass。
Pass语义块
Pass{
//Other Code
} 首先我们可以定义该Pass的名字,若定义了Pass的名字,我们就可以在其他地方通过UsePass 语句来直接使用已经写好的Pass,以实现代码的复用,需要注意的是,Unity会自动将所有Pass的名字转换为大写形式,因此在使用UsePass时你也应使用大写的名字。之后是Pass中内置的标签,该标签与SubShader中定义的标签不同。对于渲染状态设置,除了SubShader中也可以进行的设置,Pass中还可以使用固定管线的着色器的命令。
除了普通的Pass,UnityShader 还支持一些特殊的Pass,入UsePass 和 GrabPass
FallBack 语义
当所有SubShader都无法执行时,则会去访问FallBack所指定的Shader文件进行运行,我们也可以关闭FallBack。 正确的使用FallBack 进行设置在一些情况下可以很大方便我们开发,更多注意在之后提及。
当然,ShaderLap还有更多的语义,例如CustomEditor语义,该语义用来自定义材质编辑面板,还有Category语义,该语义用来对Shader中的命令进行分组,更多内容自行了解。
我们的大部分代码都是写在Pass语义块中的,下面代码展示了一个最简单的顶点/片元着色器。
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader"Unity Shader Book/Chapter 5/Simple Shader"{
SubShader{
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
float4 vert(float4 v :POSITION) : SV_POSITION{
return UnityObjectToClipPos(v);
}
fixed4 frag():SV_Target{
return fixed4(1.0,1.0,1.0,1.0);
}
ENDCG
}
}
} 接下来详细解释一下CGPROGRAMENDCG部分,首先里面是两条编译指令,这两条指令分别指明了 顶点着色器对应函数名字与片元着色器对应函数名字(习惯上分别命名为vert与frag),然后则是相应函数,这两个函数指明了顶点着色器与片元着色器对应的代码,
POSITION 与 SV_POSITION 都是CG/HLSL 对应的语义,这些语义告诉了系统用户需要哪些输入值以及用户的输出值是什么。POSITION意思是将模型顶点坐标输入到这里,SV_POSITION表示用户的返回值是裁剪空间中的坐标,SV_Target 表示将返回的颜色存储到一个渲染目标中,在这里默认为帧缓存中。
5.2.2模型数据从哪里来
如果我们想要在顶点着色器阶段获得更多的模型数据呢? 可以通过定义结构体的方式实现此操作
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader"Unity Shader Book/Chapter 5/Simple Shader"{
SubShader{
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
struct a2v{
float4 vertex : POSITION;
float3 normal :NORMAL;
float4 texcoord : TEXCOORD0;
};
float4 vert(a2v v ) : SV_POSITION{
return UnityObjectToClipPos(v.vertex);
}
fixed4 frag():SV_Target{
return fixed4(1.0,1.0,1.0,1.0);
}
ENDCG
}
}
} NORMAL 语义表示将顶点的法线坐标填充到此位置,TEXCOORD0语义表示将模型的第一组纹理坐标填充到这里。
我们的模型数据来自相关的Mesh Render组件,在每帧调用Draw Call时,Mesh Render将模型的相关数据发送给Unity Shader。
5.2.3 顶点着色器和片元着色器之间如何通信
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader"Unity Shader Book/Chapter 5/Simple Shader"{
SubShader{
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
struct a2v{
float4 vertex : POSITION;
float3 normal :NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f{
float4 pos : SV_POSITION;
fixed3 color : COLOR0;
};
v2f vert(a2v v ) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.color = v.normal * 0.5 + float3(0.5,0.5,0.5);
return o;
}
fixed4 frag(v2f o):SV_Target{
return fixed4(o.color , 1.0);
}
ENDCG
}
}
} 从上述代码可以看到,主要是通过函数返回值与函数参数的方式实现通信的,其中顶点着色器的返回值中必须包含一个SV_POSITION 语义的变量,否则渲染器将无法得到裁剪空间中的顶点坐标,将无法将顶点渲染到屏幕上。
COLOR0语义中数据由用户自定义,一般情况下存储颜色, 需要注意的是片元着色器的接收值实际上是本次片元着色器调用对应的三角形的三个顶点插值得到的。
5.2.4 如何使用属性
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader"Unity Shader Book/Chapter 5/Simple Shader"{
Properties{
_Color("Color Tint",Color) = (1.0,1.0,1.0,1.0)
}
SubShader{
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
fixed4 _Color; //
struct a2v{
float4 vertex : POSITION;
float3 normal :NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f{
float4 pos : SV_POSITION;
fixed3 color : COLOR0;
};
v2f vert(a2v v ) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.color = v.normal * 0.5 + float3(0.5,0.5,0.5);
return o;
}
fixed4 frag(v2f o):SV_Target{
fixed3 c = o.color;
c*=_Color.rgb;
return fixed4(c, 1.0);
}
ENDCG
}
}
} 在使用属性前,需要在CG代码片段里提前定义与属性中变量相对应的变量,要求类型相对应且名字相同
----------------------------------------------------------------------------------
以上是3种浮点数据类型的区别,现在大多数的电脑GPU会把所有的计算都按最高的浮点精度来计算,float,half,fixed在这些平台上实际是等价的,目前移动平台基本会把half和fixed当作同等精度,但是在移动平台,GPU会有不同的精度范围,而且不同精度的浮点值运算速度也会有所差异,所以我们应尽量选择精度范围较低的数据类型,以提高性能。
参考:https://www.jianshu.com/p/7b2103325822
-------------------------------------------------------------------------------------
5.3 强大的援手 :Unity提供的内置文件和变量
unity提供了一些内置文件以方便我们的开发,可以通过 # include "" 来引入
5.4 Unity提供的CG/HLSL 语义
实际上这些是CG/HLSL 提供的语义,语义实际上就是赋给shader输入和输出的一个字符串,他表示该变量应该从哪里来,到哪里去。 Unity并非支持所有的语义。
需要注意的是,同一个语义在不同的地方表示的含义可能会不同,
系统数值语义: 在DirectX10以后,有了一个新的语义类型,系统数值语义,该语义以SV开头,比如SV_POSITION,这种语义都有特殊的含义。 在一些平台上这种系统数值语义与对应的普通语义含义、作用相同,但在某些平台上,其只支持相应的系统数值语义,所以为了更好的兼容性应多使用系统数值语义
5.4.2Unity支持的语义
TEXCOORD 的n 与所在的shader model有关
shader model是什么?这里放个参考链接 (4 封私信 / 8 条消息) Shader Model具体是什么意思? - 知乎 (zhihu.com)
在这里TEXCOORD 语义没有特殊含义,我们可以自行决定赋值给他的值, 一般当我们想要返回一些自定义的数据时会使用TEXCOORD语义
5.4.3如何定义复杂的变量类型
每个语义可以使用的寄存器最多只能处理4个浮点值,因此对于一些类型,如矩阵 float4x4 就不能直接使用语义来修饰了,一种方法是将其分解为多个float4类型变量,分别使用语义来修饰。
5.5 程序员的烦恼 Debug
Shader的调试方法很有限,连简单的输出都做不到,这里介绍Unity 中对 Unity Shader调试的两种方法
1.假彩色图像
假彩色图像指的是使用假彩色技术生成的图像,与照片那种真彩色图像相对应。 假彩色图像用来可视化一些数据,即将数据映射到0-1范围,再通过颜色输出出来,进而观察数据的正确性,当然这种方法得到的信息是模糊和有限的。
5.5.2 使用Visual Studio
对于Windows用户来说,可以使用VS自带的调试工具——Graphics Debugger
通过该工具我们可以查看每个像素的最终颜色和其位置,同时我们也可以进行单步调试。
不过该工具也存在一些缺点,首先我们需要保证Unity运行在DirectX 11平台上,同时该调试器本身也存在一些Bug
5.5.3 最新利器 :帧调试器
Unity 5中最新加入的工具,通过该工具我们可以较为详细地查看某一帧是如何渲染出来的,我们可以查看该渲染所有的渲染事件,这些事件包含了Draw Call序列,也包括了类似清空帧缓存等操作。通过单击其中某一个事件,可以查看该事件的详细信息,例如几何图形的细节以及使用了哪个shader等。
帧调试器并没有实现一个真正的帧拾取功能,他是通过暂停某一帧的渲染的方式来为你提供帧渲染中间过程的信息的,但这种方式可以获得的信息其实也是相对有限的,若你想要获得更为详细的信息,则需要借助其他外部工具,如IntelGPA、 RenderDoc、 NVIDIANSight、AMDGPU PerfStudio等。
要使用帧调试器,我们首先需要在 Window -> Frame Debugger 中打开帧调试器窗口
5.6 小心:渲染平台的差异
Unity的优点之一是其跨平台性,不过在某些时候我们需要自己动手处理一些跨平台的问题。
5.6.1 渲染纹理的坐标差异
如对于OpenGL来说,屏幕空间的原点位于左下角,而DirectX 屏幕空间的原点位于左上角。
需要注意的是我们不仅可以把渲染结果输出到屏幕上,也可以把他输出到其他渲染目标中,此时就需要用到渲染纹理来保存渲染结果。
在大多数情况下Unity会帮我们处理这个差异,但当我们开启抗锯齿操作时Unity可能不会帮我们处理这个问题。当开启抗锯齿时,若我们只是处理一张屏幕图像,此时仍不需要担心这个问题,但当需要同时处理多张渲染图像时,我们需要手动翻转其他不满足DirectX采样规则的图像,如在之后我们可以会同时处理多张渲染图像来实现一些后处理效果。总之,若开启了抗锯齿并且需要同时处理多张渲染图像时我们需要手动处理这个差异。不过对于一些纹理,如噪声纹理,其坐标轴在竖直方向上的朝向并不重要,即便不翻转也可以得到正确的结果。
5.6.2 Shader的语法差异
不同平台上的Shader语法也可能会有所差异。如Direct9/11中不支持在顶点着色器中使用tex2D函数,该函数是对纹理进行采样的一个函数。因为此时顶点着色器中无法得到uv偏导,而该函数需要使用uv偏导进行采样。此时应使用tex2Dlod函数进行采样。
而此时我们应添加#pragma target 3.0,因为tex2Dlod是Shader Model 3.0里面的特性。
5.6.3 Shader的语意差异
如SV_POSITION 和 POSITION语义,在一些平台上这两个语义完全相同,但在如PS4平台上,只能识别SV_POSITION语义。所以为了使我们代码适配更多平台,应尽可能多的使用系统数值语义来描述输入输出变量。
5.6.4 其他平台差异
更多可以去查看Unity官方文档。
5.7 Shader 整洁之道
接下来是规范Shader书写的一些建议,写出规范的代码帮助我们写出漂亮、高效的代码。
5.7.1 float、half还是fixed
在现代大多数桌面GPU上他们实际上是等价的,不过在移动平台上他们会有所差异。一个基本的建议是尽可能使用精度较低的数值类型,以节省空间和提高效率,这点在移动平台上会相对比较明显。所以在测试shader性能时,若目标平台是移动平台,应确保在移动平台上进行测试。
5.7.2 规范语法
如使用更加严格的语法标准,以保证我们的代码在DirectX等对语法要求更加严格的平台上也可以运行。
5.7.3 避免不必要的计算
过多的计算会使Shader无法正常运行,应尽可能多的在顶点着色器进行运算。当所进行的运算过多时,申请使用的寄存器和指令的数量会超出限制,导致错误,在不同的 Shader target和着色器阶段我们能使用的寄存器和指令数量是不同的,当然我们可以通过调整Shader target来增加能使用寄存器和指令数量(Shader target 类似Shader model,Shader Model 是微软提出的一套规范,一套标准,简单来说其决定了shader中各个特性的能力,这个能力通过该特性能使用的寄存器和指令的数量等标准体现。),但根本方法还是减少运算或进行预计算。
5.7.4 慎用分支和循环语句
GPU处理流程控制语句的方法与CPU完全不同,在早期GPU也并不支持流程控制语句,在GPU上运行流程控制语句的代价往往是很大的,若使用过多,会造成Shader性能成倍下降。一个解决方法是尽可能将运算向上级移动,能在CPU计算就在CPU计算,能在顶点着色器计算就别在片元着色器计算,若一定需要使用流程控制语句,应尽量遵守以下三点建议:
1. 条件判断语句应尽可能为常量,即在Shader运行过程中不会改变
2. 每个分支中包含的操作指令数目应尽可能少
3. 嵌套层数应尽可能少
总之,我们不鼓励在Shader中使用流程控制语句。
5.7.5 不要除以0
该操作的结果是不可预测的,即使此时没有崩溃或符合预期,在其他平台上可能会崩溃或出现其他意料之外的结果。
一个解决方法是若除数可能为0,强行将其截取到非0范围。有时也会使用if条件进行非0判断。
5.8 扩展阅读
页:
[1]