第一个Shader程序:
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
// 我的第一个顶点/片元着色器
// 名称
Shader "Fan/FirstShader"
{
// 声明材质属性是非必需的,此处未声明任何材质属性
Properties
{
// 属性
// 声明一个Color类型的属性
_Color("Color Tint", Color) = (1.0, 1.0, 1.0, 1.0)
}
SubShader
{
// 针对显卡A的SubShader
Pass
{
// 设置渲染状态和标签(未设置,则表示使用默认的渲染设置和标签设置)
// 开始CG代码片段
CGPROGRAM
// 该代码片段的编译指令 (告知Unity)
#pragma vertex vert // vert函数 包含 顶点着色器代码
#pragma fragment frag // frag函数 包含 片元着色器代码
// 定义一个与Properties中声明的属性匹配的变量
fixed4 _Color;
// 用结构体定义顶点着色器的输入
// a2v = application to vertexShader (把数据从应用阶段传递到顶点着色器中)
struct a2v
{
// 形如 Type name : Semantic(语义);
// POSITION : 用模型空间的顶点坐标填充vertex
float4 vertex : POSITION;
// NORMAL : 用模型空间的法线方向填充normal
float3 normal : NORMAL;
// TEXCOORD0 : 用模型的第一套纹理坐标填充texcoord
float4 texcoord : TEXCOORD0;
};
// 用结构体定义顶点着色器的输出
struct v2f
{
// SV_POSITION : pos中包含顶点在裁剪空间中的位置信息
float4 pos : SV_POSITION;
// COLOR0 : 存储颜色信息
fixed3 color : COLOR0;
};
// CG代码
// POSITION和SV_POSITION是CG/HLSL的语义
// POSITION告诉Unity,将模型顶点坐标填充到参数v中
// SV_POSITION告诉Unity,顶点着色器的输出是裁剪空间中的顶点坐标
//输出类型+函数名 输入 输出
//float4 vert(float4 v : POSITION) : SV_POSITION
//float4 vert(a2v v) : SV_POSITION
// Unity5.4之后,顶点着色器的输出不需单独对整个顶点的输出位置进行指定,因此此处无需指定输出
v2f vert(a2v v)
{
// 将顶点坐标从模型空间转换到裁剪空间
// 用v.vertex访问模型空间的顶点坐标
// return UnityObjectToClipPos (v.vertex);
v2f o;
o.pos = UnityObjectToClipPos (v.vertex);
// v.normal的范围为 -1~1,下面式子将范围调整到 0~1
o.color = v.normal * 0.5 + fixed3(0.5, 0.5, 0.5);
return o;
}
// 输出颜色到默认的帧缓存(一个渲染目标)中
// fixed4 frag() : SV_Target
fixed4 frag(v2f i) : SV_Target
{
// 0-1 黑-白
// return fixed4(0.0, 1.0, 1.0, 1.0);
fixed3 c = i.color;
c *= _Color.rgb;
return fixed4(c, 1.0);
}
ENDCG
// 其他设置
}
// 其他Pass
}
SubShader
{
// 针对显卡B的SubShader
Pass
{}
}
// 上述SubShader都失败后,调用UnityShader
Fallback "VertexLit"
}
获取模型数据
构建一个结构体,通过语义来请求Unity填充该结构体 语义中的数据是由材质的MeshRenderer组件提供的
- 每次(帧)调用DrawCall时,MeshRenderer将自己负责渲染的模型数据发送给UnityShader
顶点着色器与片元着色器之间的通信
构建一个结构体,作为顶点着色器的输出和片元着色器的输入,从而实现二者的通信 因为顶点着色器:逐顶点调用,片元着色器:逐片元调用 因此片元着色器的真实输入是把顶点着色器的输出进行插值后的结果
使用属性
在Properties语义块中声明属性,在CG代码片段中定义新的变量以对应刚才声明的属性
Unity内置的Shader相关的内容用途
Unity提供了一些内置文件,如提前定义的函数、变量、宏等,以方便开发者的编码过程
内置的包含文件
类似于C++中的头文件,后缀名是 .cginc,可以使用#include将这些文件包含进来,使用Unity提供的变量和函数
内置的变量
Unity提供了用于访问时间、光照、雾效、环境光等目的的变量,大多位于UnityShaderVariables.cginc中
UnityShader中使用的CG语义语义的定义
一个赋给Shader输入和输出的字符串,该字符串表示该参数的含义 语义告诉Shader应从哪里读取数据,应将数据输出到哪里
Unity对某些语义的含义进行了规定
如TEXCOORD0表示模型的第一组纹理坐标(准确的说,是在输入结构体中的该语义才被强制设定为该含义) 相同名称的语义出现在不同的地方时,可能表示不会的含义
- 如TEXCOORD0在输出结构体v2f中,无特别含义
系统数值语义
SV(system-value)开头 该语义在渲染流水线中有特殊含义,用这类语义描述的变量不能随便赋值,因为流水线会使用这类语义完成特定的目的
Unity支持的语义
有些语义不是SV开头的,但是在Unity中,它们被赋予了特殊的含义
定义复杂变量类型的方法
一个语义可使用的寄存器只能处理4个浮点值,如果要定义矩阵类型,可以将其拆分为多个变量,每个变量存储一部分数据
- 如将float4x4的矩阵类型拆分为4个float4类型的变量,每个变量存储一行数据
3种方法
假彩色图像
用假彩色技术生成的一种图像,用于可视化一些数据 主要思想:将要调试的变量映射到0~1之间,将其作为颜色输出到屏幕上,通过显示的颜色值判断该值是否正确 如果要调试多维数据,则需要对其每个分量进行单独调试,或选择多个颜色分量进行输出
VisualStudio
VS中提供了对UnityShader的调试功能:Graphics Debugger 通过该功能可以对顶点着色器和片元着色器进行单步调试
帧调试器
Unity自带的针对渲染的调试器 帧调试器窗口:Windows->Frame Debugger 功能:查看渲染该帧时进行的各种渲染事件,包括DrawCall序列、清空帧缓存等操作 分3部分
- 上:开启/关闭帧调试功能,开启时,可以移动窗口最上方的滑动条来重放渲染事件
- 左:显示所有事件的树状图,每个叶节点是一个事件
- 右:单击某事件时,在右侧显示该事件的细节
渲染纹理的坐标差异
OpenGL和DirectX的屏幕空间坐标在竖直方向上是相反的
Shader的语法差异
DirectX9/11不支持在顶点着色器中使用tex2D函数(对纹理采样)
Shader的语义差异
为了让Shader能在所有平台下正常工作,需尽可能使用如下语义描述Shader的输入输出变量
- SV_POSITION:顶点着色器输出的顶点位置
- SV_Target:片元着色器的输出颜色
不同精度的数值类型的选择
- float:最高精度 32位
- half: 中等精度 16位
- fixed:最低精度 11位 常用来存储颜色和单位矢量
尽可能使用精度较低的类型
规范语法
如使用和变量类型相匹配的参数数目来对变量进行初始化(DirectX对Shader的语义要求严格)
避免不必要的计算
ShaderModel作为微软提出的一套规范,决定了Shader中各特性的能力,能力体现在能使用的运算指令数目,寄存器个数等方面 等级越高,能力越大 如果在Shader中进行了过多运算以致需要使用的临时寄存器或指令数目超过当前ShaderModel支持的数目,就会发生错误 应尽量减少Shader中的运算,通过预计算的方式提供更多数据
慎用分支和循环语句
GPU使用了不同于CPU的技术来实现分支语句,在最坏的情况下,花在一个分支语句的时间等于运行了所有分支语句的时间 最好的方式是在CPU中进行预计算,将结果传递给Shader 必须使用分支语句时,使用的条件变量最好是常数,分支中需包含尽量少的操作指令数,分支的嵌套层数需尽量少
不要除以0
会导致结果不可预测 解决方法是对除数可能为0的情况强制截取到非0范围(用if判断)