一个最简单的着色器
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
}
}
}
- Pass
- 编译指令
#pragma
告诉Unity哪个函数包含顶点着色器的代码,哪个函数包含了片段着色器的代码,一般函数名会分别使用vert
和frag
,可读性更高
#pragma vertext func1
#pragma fragment func2
- 语义:告知系统用户需要哪些输入值,以及用户的输出值,不可省略
- POSITON:将告诉Unity要求把模型的顶点坐标填充到输入参数v中
- SV_POSITION:将告知Unity顶点着色器的输出是裁剪空间中的顶点坐标
- SV_Target:告知Unity将用户输出的颜色村粗到一个渲染目标(RT)中,这里是将输出到默认的帧缓冲中
- 数据类型
- Unity官方文档介绍:https://docs.unity.cn/cn/2020.3/Manual/SL-DataTypesAndPrecision.html
- float4和fixed4的区别:高精度和低精度(还有一个half的中等精度)
调整顶点着色器输入参数
- 背景:Vertext Shader期望能获取到更多的模型数据,例如纹理坐标、法线方向
- 方案:结构体处理
- 结构体代码模版
struct StructName {
Type Name : Semantic;
...
}
- 完整代码示例
- 注意:结构体声明后要加
;
,否则会存在Unity编译报错问题; - 数据来源:Unity会通过材质关联的Mesh Render组件中提供
- 更多数据:可以参考官网文档
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 : TEXCOORD;
};
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
}
}
}
顶点着色器和片段着色器的通信处理
- 背景:需要将顶点着色器中的模型的法线、纹理坐标等信息传入到片段着色器中进行运算
- 方案:声明结构体 -> 顶点着色器输出改为结构体 -> 片段着色器输入改为结构体
- 代码示例
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 : TEXCOORD;
};
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 + fixed3(0.5, 0.5, 0.5);
return o;
}
fixed4 frag(v2f i) : SV_Target {
return fixed4(i.color, 1.0);
}
ENDCG
}
}
}
- 效果展示:
如何使用属性(Properties)
- 坑点
- 每个属性定义后添加
;
会提示报错 - Pass中需要针对用到的属性需要重新定义,否则用到时会提示异常
- 效果展示:
强大的援手:Unity的内置文件和变量
包含文件(include file)
- 文件后缀:.cginc(盲猜:cg语言的include file)
- 使用语法:
#include "UnityCG.cginc"
- 官方文档:
- 本地查看:
Unity提供的Cg/HLSL语义
- 系统数值语义:SV(system-value)开头,一般这类数据在Unity的渲染管线中是关键的数据,一般不以SV开头也是可以使用,但一般为了跨平台可用(例如DX10中用SV_POSITION)会更推荐SV开头的语义;
程序员的烦恼:Debug
- 背景:shader的debug难度较大,连基本的输出打印都不具备,而且调试方法也相对有限
假彩色图像(false-color image)
- 方案:使用假彩色图像(false-color image):通过将调试数据转换为[0,1]之间并输出到屏幕上用以观察变量值是否符合预期
- 评价:原始社会Debug手段
- 过程编码错误
vertex
写错成vertext
;tangent.xyz * 0.5 + fixed3(0.5, 0.5, 0.5)
将+
写成,
;vert
函数遗漏返回值o
;
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 : COLOR;
};
v2f vert(appdata_full v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.color = fixed4(v.texcoord1.xy, 0.0, 1.0);
return o;
}
fixed4 frag(v2f i): SV_Target
{
return i.color;
}
ENDCG
}
}
}
利用神器:Visual Studio
最新利器:帧调试器(Frame Debugger)
- 个人观点:类似RenderDoc,但由于是Unity自己定制的,所以类似用了哪个shader文件、VP矩阵等信息都可以显示出来
渲染平台差异
- DX和OpenGL的渲染纹理结果的差异(屏幕空间坐标系不一致)
- Shader的语法差异
- Shader的语义差异
Shader整洁之道
- 精度选择float、half还是fixed
- 桌面GPU:大多数目前都是默认按最高的浮点精度计算(即这三者等价),因此不一定能够看出来差异
- 移动端GPU:有不同的精度范围,可以看出差异
- fixed精度一般是老旧的移动平台使用,可以理解为现在大部分都是将half和fixed看作等价
- 基本建议:尽可能使用较低的精度来优化shader的性能(尤其是移动端)
- 规范语法:DX对Shader的语义有更严格的要求
- 避免不必要的计算:通过预计算(离线计算)是更好的解决方案
- 慎用分支和循环语句:降低GPU并行能力,性能成倍下降
- 不要除0:出现未定义行为,例如在不同平台(软硬件环境)效果可能不一样(可能出现崩溃的情况)