开始Unity Shader的学习之旅
1 一个最简单的顶点/片元着色器
1.1 顶点/片元着色器的基本结构
Shader "MyShaderName"{
Properties{
// 属性
}
SubShader{
// 针对显卡A的SubShader
Pass{
// 设置渲染状态和标签
// 开始CG代码和片段
CGPROGRAM
// 该代码片段的编译指令。例如:
#pragma vertex vert
#pragma fragment frag
// CG代码写在这里
ENDCG
// 其他设置
}
// 其他需要的Pass
}
SubShader{
// 针对显卡B的SubShader
}
// 上述SubShader都失败后用于回调的UnityShader
Fallback "VertexLit"
}
其中最重要的部分即Pass语义块,绝大部分代码都写在这里面
创建以下的Shder并赋值给材质,将材质赋值给一个游戏对象
Shader "Unlit/Chapter5-SimpleShader" // 定义Shder名字
{
// Properties语句块不是必须的
SubShader // 声明SubShader
{
Pass // 声明Pass
{
CGPROGRAM // 声明CG代码片段
#pragma vertex vert
#pragma fragment frag
// 告诉着色器:vert函数将包含顶点着色器的代码,frag着色器将包含片元着色器的代码
/*
#pragma vertex name
#pragma fragment name
*/
// name即指定的函数名
// vert函数定义 - 顶点着色器
float4 vert(float4 v : POSITION) : SV_POSITION // 返回一个在裁剪空间中的float4类型的变量
{
return UnityObjectToClipPos (v);
// 新版本会被替换成这样的语句,把顶点坐标从模型空间通过MVP矩阵转换到裁剪空间
// return mul(UNITY_MATRIX_MVP, v);
}
// v为函数输入,包含了这个顶点的位置,通过POSITION语义指定
// POSITION告诉Unity把模型的顶点坐标填充到输入参数v中
// SV_POSITION告诉Unity,顶点着色器的输出就是裁剪空间中的顶点坐标
// POSITION和SV_POSITION用于限制输入输出参数,告诉Unity输入输出分别是什么
// frag函数定义 - 片元着色器
fixed4 frag(): SV_TARGET0
{
return fixed4(1.0, 1.0, 1.0, 1.0);
// 返回一个白色的fixed4类型的变量
}
// SV_Target告诉渲染器把用户的输出颜色存储到一个渲染目标中,这里将输出到默认的帧缓存中
ENDCG
}
}
}
1.2 获取模型数据
Shader "Unlit/Chapter5-SimpleShader" // 定义Shder名字
{
// Properties语句块不是必须的
SubShader // 声明SubShader
{
Pass // 声明Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 使用一个结构体来定义顶点着色器的输入 a2v application to vertex shader
struct a2v
{
float4 myVertex : POSITION; // 用模型的顶点空间坐标填充vertex变量
float3 normal : NORMAL; // 用模型的法线方向填充normal变量
float4 texcoord : TEXCOORD0; // 用模型的第一套纹理坐标填充texcoord变量
};
float4 vert(a2v v) : SV_POSITION // 返回一个在裁剪空间中的float4类型的变量
{
return UnityObjectToClipPos (v.myVertex);
// return mul(UNITY_MATRIX_MVP, v);
}
fixed4 frag(): SV_TARGET0
{
return fixed4(1.0, 1.0, 1.0, 1.0);
}
ENDCG
}
}
}
声明一个结构体a2v,包含了顶点着色器需要的模型数据,可以用如下格式定义结构体:
struct StructName
{
Type Name : Semantic;
Type Name : Semantic;
}; // 注意这个分号一定不能少
// Semantic中Unity支持的语句有POSITION, TANGENT, NORMAL, TEXCOORD0, TEXCOORD1, TEXCOORD2, TEXCOORD3, COLOR等
- 同时,我们需要修改vert函数的输入参数类型,把它设置为我们新定义的结构体,通过这种自定义结构体的方式,我们就可以在顶点着色器中访问模型数据
- POSITION, TANGENT, NORMAL等语义中的数据来自于MeshRenderer,在每帧调用DrawCall时,MeshRenderer会把它负责渲染的模型数据发送给UnityShader
- 一个模型的数据通常包含一组三角面片,三角面片由3个顶点构成,每个顶点又包含顶点位置、法线、切线、纹理坐标、顶点颜色等
1.3 顶点着色器和片元着色器之间的通信
定义一个新的结构体实现通信
Shader "Unlit/Chapter5-SimpleShader" // 定义Shder名字
{
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 使用一个结构体来定义顶点着色器的输入
struct a2v
{
float4 myVertex : POSITION; // 用模型的顶点空间坐标填充vertex变量
float3 normal : NORMAL; // 用模型的法线方向填充normal变量
float4 texcoord : TEXCOORD0; // 用模型的第一套纹理坐标填充texcoord变量
};
// 使用一个结构体来定义顶点着色器的输出
struct v2f
{
float4 pos : SV_POSITION; // pos包含了顶点在裁剪空间中的位置信息
fixed3 myColor : COLOR0; // COLOR0语义用于存储颜色信息
};
// 这里书上的代码有问题,返回SV_POSITION,因为v2f中只有pos才是SV_POSITION(裁剪空间顶点坐标)
v2f vert(a2v v) // 返回一个在裁剪空间中的float4类型的变量
{
v2f o;
o.pos = UnityObjectToClipPos(v.myVertex);
// 由于v.normal的xyz范围是[-1.0, 1.0],下面的代码将v.normal的范围映射到了[0,1.0]
// 再把颜色传递给片元着色器,使不同法线方向的平面有不同的颜色
o.myColor = v.normal * 0.5 + fixed3(0.5, 0.5, 0.5);
return o;
}
fixed4 frag(v2f i): SV_Target
{
return fixed4(i.myColor, 1.0);
}
ENDCG
}
}
}
声明一个新的结构体v2f,用于在顶点着色器和片元着色器之间传递信息
1.4 如何使用属性
材质给予了我们一个可以方便调节Unity参数的方式,需要写在Properties语义块中
Shader "Unlit/Chapter5-SimpleShader" // 定义Shder名字
{
Properties
{
// 声明一个color类型的属性
_Color ("Color Tint", Color) = (1.0, 1.0, 1.0, 1.0)
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
fixed4 _Color; // CG中需要定义一个属性和名称都匹配的变量
struct a2v
{
float4 myVertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
fixed3 myColor : COLOR0;
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.myVertex);
o.myColor = v.normal * 0.5 + fixed3(0.5, 0.5, 0.5);
return o;
}
fixed4 frag(v2f i): SV_Target
{
fixed3 c = i.myColor;
// 使用Color属性来控制输出颜色
c *= _Color.rgb;
return fixed4(c, 1.0);
}
ENDCG
}
}
}
- 在Properties的语句块中,我们首先声明了一个属性_Color,类型为Color,初值为白色
- 在CG代码中访问它,需要在CG代码中定义一个新的变量,名称和类型必须与Properties语义块中的属性定义相匹配
ShaderLab中属性和类型和CG中匹配关系如下:
ShaderLab属性类型 | CG变量类型 |
---|---|
Color, Vector | float4, half4, fixed4 |
Range, Float | float, half, fixed |
2D | sampler2D |
Cube | samplerCube |
3D | sampler3D |
uniform关键字:CG中修饰变量和参数的一种修饰词,仅用于提供一些关于该变量的初始值是如何知道那个和存储相关信息的,Unity Shder中可以省略
uniform fixed4 _Color;
2 Unity的内置文件和变量
2.1 内置包含文件
**包含文件(include file)**类似于C++头文件,Unity中头文件后缀为.cginc,利用#include包含
CGPROGRAM
//...
#include "UnityCG.cginc"
//...
ENDCG
Windows中CGIncludes文件的路径是"Unity安装路径/Data/CGIncludes"
以下给出一些CGIncludes中主要的包含文件和主要用处
文件名 | 描述 |
---|---|
UnityCG.cginc | 包含了常用的帮助函数,宏和结构体 |
UnityShaderVariables.cginc | 在编译Unity Shader时会被自动包含进来,包含了许多内置全局变量,如UNITY_MATRIX_MVP等 |
Lighting.cginc | 包含了各种内置光照模型,如果编写的是Surface Shader,会被自动包含进来 |
HLSLSupport.cginc | 在编译Unity Shader时会被自动包含进来,声明了许多用于跨平台编译的宏的定义 |
可以看出许多头文件即使不手动包含,也会被自动包含进来,以下展示了UnityCG.cginc中预定义的结构体
名称 | 描述 | 包含的变量 |
---|---|---|
appdata_base | 可用于顶点着色器的输入 | 顶点位置、顶点法线、第一组纹理坐标 |
appdate_tan | 可用于顶点着色器的输入 | 顶点位置、顶点切线、顶点法线、第一组纹理坐标 |
appdata_full | 可用于顶点着色器的输入 | 顶点位置、顶点切线、顶点法线、四组(或更多)纹理坐标 |
appdata_img | 可用于顶点着色器的输入 | 顶点位置、第一组纹理坐标 |
v2f_img | 可用于顶点着色器的输出 | 裁剪空间中的位置、纹理坐标 |
UnityCG.cginc中常用的帮助函数:
函数名 | 描述 |
---|---|
float3 WorldSpaceViewDir(float4 v) | 输入一个模型空间中的顶点位置,返回世界空间中从该点到摄像机的观察方向 |
float3 ObjSpaceViewDir(float v) | 输入一个模型空间中的顶点位置,返回模型空间中从该点到摄像机的观察方向 |
float3 WorldSpaceLightDir(float4 v) | 仅可用于前向渲染中,输入一个模型空间中的顶点位置,返回世界空间中从该点到光源的光照方向,没有被归一化 |
float3 ObjSpaceLightDir(float4 v) | 仅可用于前向渲染中,输入一个模型空间中的顶点位置,返回模型空间中从该点到光源的光照方向,没有被归一化 |
float3 UnityObjectToWorldNormal(float3 norm) | 把法线方向从模型空间转换到世界空间中 |
float3 UnityObjectToWorldDir(in float3 dir) | 把方向矢量从模型空间变换到世界空间中 |
float3 UnityWorldToObjectDir(float3 Dir) | 把方向矢量从世界空间变换到模型空间中 |
包含头文件可以提高代码的复用率
2.2 内置的变量
Uniy还提供了访问时间、光照、雾效和环境光等目的的变量,大多位于UnityShaderVariables.cginc中,与光照有关的内置变量还会位于Lighting.cginc、AutoLight.cginc中
3 Unity提供的CG/HLSL语义
3.1 语义
Unity为了方便对模型数据传输,对一些语义进行了特别的含义规定
例:在顶点着色器的输入结构体a2f中用TEXCOORD0来描述texcoord,Unity会识别TEXCOORD0语义,以把模型的第一组纹理坐标填充到texcoord中
注意:即使语义名称一样,如果出现的位置不同,含义也不同
例:TEXCOORD0既可用于描述顶点着色器的输入结构体a2f,也可以用于描述输出结构体v2f。但在输入结构体a2f中,TEXCOORD0有特别的含义,即把模型的第一组纹理坐标存储在改变量中,而在输出结构体v2f中TEXCOORD0修饰的变量含义就可以自定义
DiectX10之后,有了一种新的语义类型,即系统数值语义(system-value semantics),这列语义以**SV(system-value,系统数值)**开头,在渲染流水线中有特殊的含义
例:上面的代码中SV_POSITION语义去修饰顶点着色器的输出变量pos,那么就表示pos包含了可用于光栅化的变换后的顶点坐标(即齐次裁剪坐标系中的坐标),用这些语义描述的变量是不可以随便赋值的,因为流水线需要使用它们来完成特定的目的,例如渲染引擎会把用SV_POSITION修饰的变量经过光栅化后显示在屏幕上。绝大多数平台上SV_POSITION和POSITION是等价的,但是在某些平台上必须使用SV_POSITION来修饰顶点着色器的输出(例如索尼PS4),为了让Shader有更好的跨平台性,对于一些有特殊含义的变量最好用SV开头进行修饰
3.2 Unity支持的语义
应用阶段传递模型数据给顶点着色器时Unity使用的常用语义:
语义 | 描述 |
---|---|
POSITION | 模型空间中的顶点位置,通常为float4类型 |
NORMAL | 顶点法线,通常是float3类型 |
TANGENT | 顶点切线,通常是float4类型 |
TEXCOORDn | 该顶点的纹理坐标,TEXCOORD0表示第一组纹理坐标,依次类推。通常是float2或float4类型 |
COLOR | 顶点颜色,通常是fixed4或float4类型 |
其中,TEXCOORDn的数目是和ShaderModel有关的,例如一般在Shader Model 2和Shader Model 3中,n = 8,而在Shader Model 4和5中n = 16,一个模型的纹理坐标一般不超过2,往往只使用TEXCOORD0和TEXCOORD1,在Unity内置结构体appdata_full中,最多使用6个纹理坐标
顶点着色器传递给片元着色器时Unity的常用语义:
语义 | 描述 |
---|---|
SV_POSITION | 裁剪空间中的顶点坐标,结构体中必须包含一个用该语义修饰的变量。等同于DiectX9中的POSITION,但最好使用SV_POSITION |
COLOR0 | 通常用于输出第一组顶点颜色,但不是必需的 |
COLOR1 | 通常用于输出第二组顶点颜色,但不是必需的 |
TEXCOORD0~TEXCOORD7 | 通常用于输出纹理坐标,但不是必需的 |
除了SV_POSITION有特别含义外,其他语义没有明确要求,我们可以随意存储任意值到这些语义描述变量中
片元着色器的输出语义:
语义 | 描述 |
---|---|
SV_Target | 输出值将存储到渲染目标中,等同于DirectX9中的COLOR语义,但最好使用SV_Target |
3.3 如何定义复杂的变量类型
strct v2f
{
float4 pos : SV_POSITION;
fixed3 color0 : COLOR0;
fixed3 color1 : COLOR1;
half value0 : TEXCOORD0;
float2 value1 : TEXCOORD0;
}
------ 以下内容个人觉得《Shader入门精要》讲的不够仔细,可以查阅更多相关资料 -----
4 Debug
- 使用假彩色图像
假彩色图像(false-color image)指的是用假彩色技术生产的一种图像,与假彩色技术对应的是真彩色图像(true-color image),假彩色图像可以用于可视化一些数据
主要思想是将需要调试的变量映射到[0, 1]之间,把它作为颜色输出到屏幕上,然后通过屏幕上显示的颜色来判断这个值是否正确(相当于print出来,只是以颜色的形式)
- 利用VS的Graphics Debugger
可以查看每个像素的最终颜色、位置等信息,还可以对顶点和片元着色器进行单步调试
- 使用帧调试器ramDebugger
在WIndow-FrameDebugger中打开帧调试窗口,用于查看该帧进行时的各种渲染事件(event)
5 渲染平台的差异
- 渲染纹理的坐标差异
- Shader语法的差异
- Shader语义的差异
6 Shader整洁之道
- float、half还是fixed
类型 | 精度 |
---|---|
float | 最高精度的浮点值,32位 |
half | 中等精度的浮点值,16位 |
fixed | 最低精度的浮点值,11位 |
- 规范语法
- 避免不必要的计算
- 慎用分支和循环语句
- 不要除以0