声明:本篇为博主阅读冯乐乐所著《Unity Shader入门精要》所记录的笔记
原作者博客地址 https://me.csdn.net/candycat1992
如有不实请指正,如有侵权请联系本人删除。
看书学习无法避免的脱离最新版本-.-!
书上Unity5.2.1(我自己电脑2018.3)…
Windows10 …
一个UnityShader的基本机构包含了Shader、Properties、SubShader、Fallback等语义块。顶点/片元中色起的结构与之大体类似。
Shader "MyFirstShader"{
Properties{
//属性
}
SubShader{
//针对显卡A的subShader
Pass{
//设置渲染状态和标签
//开始CG代码片段
CGPROGRAM
//该代码片段的编译指令,例如
#pragma vertex vert
#pragma fragment frag
//CG代码写在这里
ENDCG
//其他设置
}
//其它pass (如果有)
}
SubShader{
//针对显卡B的SubShader
}
//上述SubShader都失败后用于回调的UnityShader
Fallback “VertexLit”
}
其中,最重要的就是Pass语义块,我们把绝大部分的代码写在这里。
我们创建一个3D工程,删去天空盒,创建一个UnityShader、材质、和一个球体。
把材质赋给球,UnityShader赋给材质。
编辑UnityShader
Shader "Unity Shaders Book/Chapter 5/SimlpShader"{
SubShader{
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
float4 vert(float4 v : POSITION) : SV_POSITION{
return mul(UNITY_MATRIX_MVP,v);
}
float4 frag() :SV_Target{
return fixed4(1.0,1.0,1.0,1.0);
}
ENDCG
}
}
}
得到一个白色的球。
首先,代码的第一行通过Shader语义定义了这个shader的名字“Unity Shaders Book/Chapter 5/SimlpShader”保持良好的命名习惯有助于快速寻找。值得注意的是,上面的代码我们并没有用到Properties语义块。可以选择不使用任何属性。
然后,我们声明了SubShader和Pass语义块。在本例中,我们不需要进行任何渲染设置和标签设置,因此SubShader会使用默认的渲染设置和标签设置。
接着,就是由CGPROGRAM和ENDCG包围的CG代码片段,这是我们的重点,首先,我们遇到了两行非常重要的编译指令:
#pragma vertex vert
#pragma fragment frag
他们将告诉Unity,哪个函数包含了定点着色器的代码,那个函数包含了片元着色器的代码。
#pragma vertex name
#pragma fragment name
其中name就是我们指定的函数名,两个函数的名字不一定是vert这样,可以是任何合法的函数名。
接下来我们看一下vert函数的定义:
float4 vert(float4 v : POSITION) : SV_POSITION{
return mul(UNITY_MATRIX_MVP,v);
}
这就是本例使用的顶点着色器代码,他是逐顶点执行的。
vert函数的输入v包含了这个而顶点的位置,只是通过POSITION语义指定的。它的返回值是float4类型的变量,是该顶点在裁剪空间中的位置。POSITON和SV_POSITION都是CG/HSLS中的语义,POSITION告诉Unity把模型的顶点坐标填充到输入参数v中,SV_POSITION告诉Unity顶点着色器输出的是裁剪空间的顶点坐标,如果没有这些语义来限定输入和输出参数的话,渲染器就不知道用户的输入输出是什么,因此会得到错误的效果。
然后我们看一下frag函数
fixed4 frag() : SV_Target{
return fixed4(1.0,1.0,1.0,1.0);
}
在本例中,frag函数咩有任何输入,它的输出是一个fixed4的变量,并且使用了SV_Target语义进行限定,SV_Target也是HLSL的一个系统语义:
把用户的输出颜色存储到一个渲染目标(render target)中。
这里是输出到默认的帧缓存中。片元着色器的代码很见到,返回了一个表示白色的fixed4类型的变量。片元着色器输出的颜色的每个分量范围在[0,1] 其中 (0,0,0)为黑色,(1,1,1)为白色。
从上面的例子中,在定点着色器中我们使用POSITION语义得到了模型的顶点位置。那么更多的数据如何获取呢。
现在我们想获取模型上每个顶点的纹理坐标和法线方向。这个需求是很常见的,我们需要使用纹理坐标来访问纹理,而法线可用于计算光照。因此,我们需要为顶点着色器定义一个新的输入参数。这个参数不是一个简单的数据类型,而是一个结构体:
Shader "Unity Shaders Book/Chapter 5/Simple Shader"{
SubShader{
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//使用一个结构体来定义定点着色器的输入
struct a2v{
//POSITION语义告诉Unity,用模型空间的顶点坐标填充vertex变量。
float4 vertex :POSITION;
//NORMAL语义告诉Unity,用模型空间的法线方向填充normal变量
float3 normal : NORMAL;
//TEXCOORD0语义告诉Unity,用模型的第一套纹理坐标填充texcoord变量。
float4 texcoord : TEXCOORD0;
};
float vert(a2v v ) : SV_POSITION{
//使用v.vertex来访问模型空间的顶点坐标
return mul ( UNITY_MATRIX_MVP , v.vertex );
}
fixed4 frag():SV_Target{
return fixed(1.0,1.0,1.0,1.0);
}
ENDCG
}
}
}
在上面的代码中,我们声明了一个新的结构体a2v,它包含了定点着色器需要的模型数据,在a2v的定义中,我们用到了更多Unity支持的语义,如NORMAL和TEXCOORD0,当他们作为定点着色器的输入时都是有特定含义的,因为Unity会根据这些语义来填充这个结构体。对于定点着色器的输出,Unity支持的语义有:POSITION,TANGENT,NORMAL,TEXCOORD0,TEXCOORD1,TEXCOORD2,TEXCOORD3,COLOR等,为了创建一个自定义的结构体,我们必须使用如下格式来定义它:
struct StructName{
Type Name: Semantic;
Type Name: Semantic;
......
};
其中,语义是不可以被省略的。后面会提供这些我语义的含义及用法。
然后,我们修改了vert函数的输入类型参数,把他设置为我们新定义的结构体,a2v(application vertex shader 应用阶段传递到顶点着色器)。
那么填充到POSITION、TANGENT、NORMAL这些语义中的数据究竟是从哪里来的呢》在Unity中,他们是由使用该材质的mesh render组件提供的。在每帧调用DrawCall的时候,MeshRender会把他所负责渲染的模型数据发送给UnityShader。我们知道,一个模型通常包含了一组三角面片,每个三角面片由3个顶点构成,而每个顶点又包含一些数据,例如定点数据、法线、切线、纹理坐标、顶点坐标、纹理颜色等。通过上面的方法,我们就可以在顶点着色器中访问顶点的这些模型数据。
在实践中,我们往往希望从顶点着色器输出一些数据,例如把模型的法线、纹理坐标等传递给片元着色器,这就需要涉及定点着色器和片元着色器之间的通信。
为此我们需要再定义一个新的结构体:
Shader "Unity Shaders 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{
//SV_POSITION语义告诉Unity,pos里包含了顶点在裁剪空间中的位置信息
float4 pos : SV_POSITION;
//COLOR0表示可以用于储存颜色信息
float3 color :COLOR0;
};
//返回值类型
v2f vert(a2v v ) : SV_POSITION{
//声明输出结构
v2f o;
o.pos = mul(UNITY_MATRIX_MVP , v.vertex);
//v.normal包含了顶点的法线方向,其分量范围在[-1.0,1.0]
//下面的代码把分量范围映射到了[0.0,1.0]
//储存到o.color中传递给片元合作色情
o.color = v.normal * 0.5 + fixed3(0.5,0.5,0.5);
return o
}
fixed4 frag( v2f : f ):SV_Target{
return fixed(f.color,1.0);
}
ENDCG
}
}
}
在上面的代码中,我们声明了一个新的结构体v2f,用于在顶点着色器和片元着色器之间传递信息。同样的,v2f也需要致命每个变量的语义,在本例中,我们使用的SV_POSITIONhe COLOR0的语义。顶点着色器的输出结构中,必须包含一个变量,它的语义是SV_POSITION。否则渲染器将无法得到裁剪空间中的顶点坐标,也就无法把顶点渲染到屏幕上。COLOR0的语义中的数据则可以由用户定义,但一般都是存储颜色,例如逐顶点的漫反射颜色或逐顶点的高光反射颜色,类似的语义还有COLOR1等。
注意定点着色器是逐顶点调用、而片元着色器是逐片元调用。片元着色器中的输入实际上是把定点着色器的输出进行插值后得到的结果。
材质给提供给我们一个可以方便地调节UnityShader中参数的方式,通过这些参数没我们可以随时调整材质的效果。而这些参数就需要写在Properties语义块中。
现在我们有了新的需求。我们想要在材质面板显示一个颜色拾取器,从而可以直接控制模型在屏幕上显示的颜色。为此,我们需要继续修改上面的代码。
Shader "Unity Shaders Book/Chapter 5/Simple Shader"{
Properties{
//声明一个Color类型的属性
_Color("Color Tint",Color)=(1.0,1.0,1.0)
}
SubShader{
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//在CG代码中,我们需要定义一个与属性名和类型都匹配的变量
fixed4 _Color;
struct a2v{
float4 vertex :POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f{
float4 pos : SV_POSITION;
float3 color :COLOR0;
};
v2f vert(a2v v ) : SV_POSITION{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP , v.vertex);
o.color = v.normal * 0.5 + fixed3(0.5,0.5,0.5);
return o
}
fixed4 frag( v2f : f ):SV_Target{
fixed3 c = f.color;
//使用_Color属性来控制输出颜色
c *=Color.rgb;
return fixed4(c,1.0);
}
ENDCG
}
}
}
在上面的代码中,我们首先添加了Propeities语义块中,并在其中声明了一个初始值为(1.0,1.0,1.0)的Color类型的属性_Color,对应白色,我们还需要在CG代码片段中提前定义一个新的变量,这个变量的名称和类型必须与Properties语义中的属性定义相匹配。
ShaderLab中属性的类型和CG中变量的类型之间匹配关系
ShaderLab属性类型 | CG变量类型 |
---|---|
Color,Vector | float4,half4,fixed4 |
Range, Float | float,half,fixed |
2D | sampler2D |
Cube | samplerCube |
3D | sampler3D |
uniform关键词是CG中修饰变量和参数的一种修饰词,它仅仅用于提供一些关于该变量的初始值是如何指定和存储的相关信息(这和其他一些图像编程接口中 的uniform关键词的作用不太一样),在unityshader中,uniform也是可以省略的。
为了方便开发者的编码过程,Unity提供了很多内置文件,这些文件包含了很多提前定义的函数、变量和宏等。对于在学习他人编写的unityshader时遇到从未见过的变量函数而又无法找到对应的声明和定义,很可能就是使用了内置的函数与变量。
类似c/c++ 使用#include “文件名.cginc” 把这些文件包含进来。
从官网 上下载内置着色器来直接下载这些文件。
CGIncludes文件夹中包含了所有的内置包含文件;
DefaultResources:内置组件或功能所需要的UnityShader;
DefaultResourcesExtra:所有Unity内置Shader;
Editor:只包含一个脚本(-.- 这本书可能有点古老)
路径
Unity的路径/Data/CGIncludes/
文件名 | 描述 |
---|---|
UnityCG.cginc | 包含了最常使用的帮助函数、宏、结构体等 |
UnityShaderVariables.cginc | 在编译UnityShader时,会自动被包含进来,包含了许多内置的全局变量,如UNITY_MATRIX_MVP等 |
Lighting.cginc | 包含了各种内置光照模型,如果编写的时SurfaceShader的话,会自动包含进来 |
HLSLSupport.cginc | 在编译UnityShader时,会被自动包含进来。声明了许多用于跨平台编译的宏和定义。 |
可以看出,有些文件即使没有#include指令也会被自动包含。因此,在前面的例子中我们可以直接使用 UNITY_MATRIX_MVP变量来进行定点变换,除了上表所列出的文件外,Unity5 (有没有2019的书啊 啊啊 啊啊啊 啊啊啊 啊啊啊 估计只能看文档了,好想赶紧去看高清渲染管线的用法) 包含了许多新的,诸如 UnityStandardBRDF.cginc UnityStandardCore.cginc 这些包含文件用于实现基于物理的渲染。
UnityCG.cginc 是我们最常接触的包含文件,下表为一部分常用结构体
名称 | 描述 | 包含变量 |
---|---|---|
appdata_base | 可用于顶点着色器的输入 | 顶点位置、顶点法线、第一组纹理坐标 |
appdata_tan | 可用于顶点着色器的输入 | 顶点位置、顶点切线、顶点法线、第一组纹理坐标 |
appdata_full | 可用于顶点着色器的输入 | 顶点位置、顶点切线、顶点发现、四组(或更多)纹理坐标 |
appdata_img | 可用于顶点着色器的输入 | 顶点位置、第一组纹理坐标 |
v2f_img | 可用于着色器的输出 | 裁剪空间中的位置、纹理坐标 |
下表是UnityCG常用的帮助函数
函数名 | 描述 |
---|---|
float3 WorldSpaceViewDir(float4 v) | 输入一个模型空间中的顶点位置,返回世界空间中从该点到摄像机的观察方向 |
float3 ObjSpaceViewDir(float4 v) | 输入一个模型空间中的顶点位置,返回模型空间中从改点到摄像机的观察方向 |
float3 WorldSpaceLightDir(float4 v) | 仅可用于前向渲染中,输入一个模型空间中的顶点位置,返回世界空间 中从该点到光源的照射方向,没有被归一化。 |
float3 objSpaceLightDir(float4 v) | 仅可用于前向渲染中 ,输入一个模型空间中的顶点位置,返回模型空间中从该点到光源的照射方向。没有被归一化。 |
float3 UnityObjectToWorldNormal(float norm) | 把法线方向从模型空间转换到世界空间中 |
float3 UnityObjectToWorldDir(int float3 dir) | 把方向矢量从模型空间变换到世界空间中 |
float3 UnityWorldToObjectDir(float3 dir) | 把方向适量从世界空间变换到模型空间中 |
用于访问时间、光照、雾效、环境光等目的的变量大多位于UnityshaderVariables.cginc中,与光照有关的还会位于Lighting.cginc, AutoLight.cginc等文件中。
实际上就是一个赋给shader输入和输出的字符串。通过他们可以使Shader知道从哪里读取数据,并把数据输出到哪里,他们在CG/HSLS的Shader流水线中是不可或缺的。需要注意的是,Unity并没有支持所欲的语义。
通常情况下、这些输入输出变量并不需要有特别的意义,也就是说我们可以自行决定这些变量的用途,至于变量本身存储了什么,shader流水线并不关心。
而Unity为了方便对模型数据的传输,对一些语义进行了特别的含义规定。例如、在顶点着色器a2f用TEXCOORD0来描述texcoord,unity会识别TEXCOORD0语义以把模型的第一组纹理坐标填充到texcoord中。 要注意的是,即便语义的名称一样,如果出现的位置不同,含义也不同。例如在上面的代码中,TEXCOORD0既可以描述顶点着色器的输入结构体a2f,也可用于描述输出结构体v2f。但在输出结构体a2f中,TEXCOORD0有特殊的含义,即把模型的第一组纹理坐标存储起来。而在输出结构体v2f中,其变量含义就可以由我们自由决定。
在DirectX 10 以后 有了一种新的语义类型即 系统数值语义(system-value semantics) 这类语义是以SV开头的,SV含义即为系统数值(system-value) 。这些语义在渲染流水线中有特殊含义。例如上面的SV_POSITION 语义去修饰顶点着色器的输出变量pos,那么就表示pos包含了可用于光栅化的变换后的坐标(齐次裁剪空间中的坐标),用这些语义描述的变量是不可以随便赋值的,因为流水线需要使用他们来完成特别的目的。当然有时会看到POSITION而非SV_POSITION,因为大多数平台上他们都是等价的,但在某些平台(例如ps4)必须用SV_POSITION.
语义 | 描述 |
---|---|
POSITION | 模型空间中的顶点位置,通常是float4类型 |
NORMAL | 顶点法线、通常是float3类型 |
TANGENT | 顶点切线、通常是float4类型 |
TEXCOORDn | 该顶点的纹理坐标、TEXCOORD0是第一组坐标。。。以此类推,通常是float2或float4类型。 |
COLOR | 顶点颜色、通常是fixed4或者float4类型 |
其中TEXCOORDn中n的数目是和ShaderMode 有关的,一般在shader model 2(即Unity默认编译到shadermodel版本) 和shader model3中 , n=8 而在shadermodel 4、5中 n=16 通常情况下每一个模型的纹理做标书一般不超过2,即我们往往只是用 0、1 。 在Unity内置的数据结构体 appdata_full中,他最多使用了6个坐标纹理组。
下表为从顶点着色器传递给片元着色器时unity常使用的语义
语义 | 描述 |
---|---|
SV_POSITION | 裁剪空间的顶点坐标,结构体中必须包含一个用该语义修饰的变量,等同于DirectX9中的POSITION语义,最好使用SV_POSITION |
COLOR0 | 通常用于输出第一组顶点颜色,但不是必需 |
COLOR1 | 通常用于输出第二组顶点颜色,但不是必需 |
TEXCOORD0~7 | 通常用于输出纹理坐标,但不是必需 |
上面的语义中、除了SV_POSITION是有特别含义外、其他语义对变量的含义没有明确的要求,也就是说,我们可以存储任意值到这些语义描述变量中。通常,如果我们需要把一些自定义的数从顶点着色器传递给片元着色器,一般选用TEXCOORD0。
片元着色器输出时Unity支持的常用语义
语义 | 描述 |
---|---|
SV_Target | 输出值将会存储到渲染目标(render target)中,等同于DirectX9中的COLOR语义,最好使用SV_Target |
上面提到的大多数都是使用标量和矢量类型的,例如fixed2,float,float4,fixed4等,下面代码给出了一个使用语义来修饰不同类型变量的例子:
struct v2f{
float4 pos : SV_POSITION;
fixed3 color0 : COLOR0;
fixed3 color0 : COLOR1;
half value0 : TEXCOORD0;
float2 value1 : TEXCOORD1;
}
一个语义可以使用的寄存器只能处理4个浮点值(float),因此如果我们想要定义矩阵类型,如float3x4,float4x4 等变量就需要使用更多的空间。一种方法是,把这些变量拆分成多个变量,例如对于float4x4的矩阵类型,我们可以拆分成4个float4类型的变量,每个变量存储一行数据。
??????为什么我这次要在括号里写汉语注释???????
指的是使用假彩色技术生成的一种图像,与假彩色对应的是真彩色图像(你猜他怎么拼)。那么如何使用假彩色图像对shader进行调试呢?
主要思想是:我们可以把需要调试的变量映射到[0,1]之间,把他作为颜色输出到屏幕上,然后通过屏幕上的像素颜色来判断这个值输出是否正确。(值得注意的是,超过1的都会被视为1,所以如果不熟悉shader就要不断尝试区间)。
Shader "Unity Shaders Book.Chapter 5/False Color"{
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
struct v2f {
float4 pos : SV_POSITION
fixed4 color : COLOR0;
}
v2f vert(appdata_full v){
v2f o;
o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
//可视化法线方向
o.color = fixed4(v.normal * 0.5 + fixed3(0.5,0.5,0.5) , 1.0 );
//可视化切线方向
o.color = fixed4(v.tangent * 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;
}
ENDCG
}
}
在上面的代码中,我们使用了Unity的内置结构体 appdata_full 我们可以在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;
}
可以看出、appdata_full几乎包含了所有的模型数据。
我们把计算得到的假彩色储存到了顶点着色器的输出结构体—v2f中的color变量里,并且在片元着色器中输出了这个颜色。读者可以使用类似的颜色拾取器的脚本得到屏幕上某点RGBA值,从而推断出调试信息。
官网文档 -----------------
https://docs.unity3d.com/2018.4/Documentation/Manual/SL-DebuggingD3D11ShadersWithVS.html
这里是2018.4的
Window -> FrameDebugger
帧调试器可以用于查看渲染该帧时进行的渲染事件(event) ,这些时间包含了DrawCall序列、也包括了类似清空帧缓存等操作。
如果被选中的DrawCall是对一个渲染纹理(RenderTexture)的渲染操作,那么这个渲染纹理就会显示在Game师徒中,而且,此时右侧面板上方的工具栏中也会出现更多的选项。
之前有提到 OpenGL和DirectX水平方向相同、竖直方向相反。
大多数情况下,这样的差异不会对我们造成影响。但当我们要是用渲染到纹理技术,把屏幕图像渲染到一张渲染纹理(Render Texture)时,如果我们不采取任何措施的话 , 就会出现纹理翻转的情况,幸运的是Unity在DirectX平台上使用渲染到纹理技术时,会为我们翻转屏幕图像纹理,以便在不同平台上达到一致性。
一种情况下unity不会为我们进行这个翻转操作,这种情况就是我们开启了抗锯齿(Eidt-Project Settings - Quality - Anti Aliasing中开启) 并在此时使用了渲染到纹理技术。在这种请款下,Unity首先渲染得到屏幕图像,再有硬件进行抗锯齿处理后,得到一张渲染纹理来供我们进行后续处理。此时,在DirectX平台下, 我们得到的输入屏幕图像并不会被Unity翻转,也就是说,此时对屏幕图像的采样坐标需要符合DirectX平台规定,如果我们的屏幕特效只需要处理一张渲染图像,我们仍然不需要在意纹理翻转问题,这是因为在我们调用Graphics.Blit函数时,Unity已经为我们对屏幕图像的采样坐标进行了处理,我们只需要按正常的采样过程处理屏幕图像即可。但如果我们需要同时处理多张渲染图像(前提是开启了抗锯齿)。但如果我们需要同时处理多张渲染图像(前提是开了抗锯齿),例如需要同时处理屏幕图像和法线纹理。这些图像在数值方向的朝向就可能是不同的(只有在DirectX这样的平台才有这样的问题)。这种时候我们就需要在即在顶点着色器中翻转某些渲染纹理(例如深度纹理或由其它脚本传递过来的纹理)的纵坐标,使之都符合DirectX的平台规则例如
#if UNITY_UV_STARTS_AT_TOP
if(_MainTex_TexelSize.y < 0 )
uv.y= 1- uv.y
其中,UNITY_UV_STARTS_AT_TOP用于判断当前平台是否是DirectX类型的平台,而当在这样的平台下开启了抗锯齿后,主纹理的纹素大小会在竖直方向上变成负值,以方便我们为对祝我森立进行正确的采样。因此我们不需要考虑平台差异化的问题。
DirectX9/11 往往更严格
在OpenGL中
float4 v = float4(0.0) 是合法的
但在DirectX应该写为
float4 v = float4(0.0 ,0.0 , 0.0 ,0.0 );
DirectX使用out的参数需对该参数所有变量进行初始化
SV_POSITION 与POSITION 的shader 后者无法在PS4平台上或使用了细分着色器的情况下正常工作。
类型 | 精度 |
---|---|
float | 最高精度浮点值 32位 |
half | 中等精度浮点值 16位 范围[-60000 ~ +60000] |
fixed | 最低精度浮点值 11位 范围[-2.0 ~ 2.0] |
上面的精度范围不是绝对正确的,尤其是在不同平台和GPU上,他们的实际精度可能和上面给出的范围不一致,
通常来讲:
大多数现代桌面GPU会把计算都按最高的浮点精度进行计算,也就是说三中浮点数在这些平台上实际是等价的,也意味着,我们在PC上很难看出因为精度带来的不同。
但在移动GPU上他们的确有不同的精度范围,而且不同精度的浮点值得运算速度也会有所差异,因此我们应该确保在正正的移动平台上验证我们的shader。
fixed精度实际上只在一些较旧的移动平台上有用,在大多数先到GPU上,他们的内部吧fixed和half当成同等精度来对待。
尽管有上面的不同,但是有一个基本建议就是还是要尽可能的使用低精度类型,因为这可以优化shader的性能,这在移动平台尤其重要。
使用和变量类型相匹配的参数对变量进行初始化
如果我们毫无节制地在Shader(尤其是片元着色器)上进行大量计算,那么我们可能很快会收到unity的错误提示。
原因是过大计算使需要的临时寄存器数目或指令超过了当前可支持的数目。
不同的shaderTarget、着色器阶段,win介意使用的临时寄存器和指令书面是不同的。通常我们可以通过制定更高级的shaderTarget来消除这些错误。
unity支持的shaderTarget如下
指令 | 描述 |
---|---|
#pragma target2.0 | 默认的shadertarget等级,相当于Direct3D9中的shadermodel2.0 |
#pragma target3.0 | 相当于Direct3D9中的shadermodel3.0 |
#pragma target4.0 | 相当于Direct3D10中的shadermodel4.0 目前只在DirectX11和xboxone/ps4上提供了支持。 |
#pragma target5.0 | 相当于Direct3D11中的shadermodel5.0 目前只在DirectX11和xboxone/ps4上提供了支持。 |
需要注意的是,所有类似OpenGL的平台(包括移动平台)被当成是支持到ShaderModel 3.0的,而WP8/WinRT平台只支持到ShaderModel2.0
Shadermodel是由微软提出的一套规范,通俗的理解就是他们决定了Shader中哥哥特性(feature)的能力(capability)。这些特性和能力体现在SHader能视同的运算指令数目、寄存器个数等各个方面,shaderModel登记越高,Shader能力越大。
他们在GPU和CPU上的实现有很大不同。大体来说,GPU使用了不同于CPU的技术来实现分支语句,在最坏的情况下,我们花在一个分支语句的时间相当于运行了所有分置于句的时间。因此,我们不鼓励在Shader中使用流程控制语句,因为他们会降低GPU的并行处理操作(尽管现代GPU已经有了一定的改进)。下面是一些建议
【1】分支判断语句中使用的条件变量最好是常数,即在Shader运行过程中不会发生变化
【2】每个分支包含的操作指令数尽可能的少;
【3】分支的嵌套层数尽可能少;
虽然在用类似c#等高级语言进行编程的时候,我们会谨记不要除以0这种基本常识,但在shader中可能会忽略这种问题。
这样的代码结果往往是不可预测的,在某些渲染平台上,上面的代码不会造成Shader的崩溃,但即便不会炳奎得到的结果也是不确定的,有些会得到白色(由无限大截取到1.0) 但在另一些平台上,我们的shader会直接崩溃,因此会出现很多不确定性。
一个解决方案是,对那些除数可能为0的轻快,强制截取到非0范围。在一些资料中读者可能也会看到使用if语句来判断除数是否为0的例子。