上一篇博客重点放在了Unity Shader的基本结构,分别介绍了它包含的三个语义块,最后简单介绍了Unity Shader的形式:表面着色器、顶点/片元着色器和固定函数着色器。
趁热打铁,今天接着上一篇开写Pass语义块中的顶点着色器和片元着色器的代码基本结构,照例是跟着《Unity Shader 入门精要》学习的。
关于在Unity中创建Scene、Material和Shader的步骤我就不再展示了,专注于代码本身。
关于Unity Shader用啥打开更加舒服,我用的是Sublime,在上一篇博客中有写过【技术美术实践部分】初识Unity Shader:基础总结!
Shader "MyShaderName" {
Properties {
//属性
}
SubShader {
//针对显卡A的SubShader
Pass {
//设置渲染状态和标签
//开始Cg代码段
CGPROGRAM
//编译指令
#pragma vertex vert
#pragma fragment frag
//写入顶点/片元着色器的Cg代码段
ENDCG
//其他设置
}
//other Passes
}
SubShader {
//针对显卡B的SubShader
}
//用于回调的Unity Shader
FallBack "VertexLit"
}
//首先用Shader语义定义了UnityShader的名字
Shader "Unity Shaders Book/Chapter 5/Simple Shader" {
//并没有用到Properties语义块——它并不是必须的
SubShader {
//没进行渲染设置和标签设置,使用默认的
Pass {
//没自定义Pass的渲染设置和标签设置
CGPROGRAM
//两行非常重要的编译指令:
#pragma vertex vert //指定哪个函数包含了顶点着色器的代码 -> vert()
#pragma fragment frag //指定哪个函数包含了片元着色器的代码 -> 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
}
}
}
更通用的编译指令格式:
#pragma vertex name
#pragma fragment name
name分别是自定义的包含顶点/片元着色器代码的函数名,不一定是vert/frag,但一般大家都用它俩,比较直观。
float4 vert(float4 v : POSITION) : SV_POSITION {
return UnityObjectToClipPos(v);
}
它们都是Cg/HLSL的语义,作用:告诉系统输入参数/输出参数的值都是啥意思,没有这些语义渲染器根本不理解传入的变量都是什么意思,后面会总结一下Unity支持的常用语义。
输入的内容是POSITION语义指定的模型空间中顶点坐标的一个float4类型的变量v
先用SV_POSITION语义指定,告诉return的内容会被当作裁剪空间中顶点坐标,接着函数本体做了一个模型空间转裁剪空间的MVP矩阵变换,获得一个float4类型的变量。在上一篇博客中我已经提到过了:Unity5.6之后用UnityObjectToClipPos(v)代替了mul(UNITY_MATRIX_MVP, v)这个mul+Unity内置的MVP矩阵的操作,更加简洁了。
fixed4 frag() : SV_Target {
return fixed4(1.0, 1.0, 1.0, 1.0);
}
SV_Target限定了frag函数输出的内容(颜色值)将储存到一个渲染目标(render target)中,关于渲染目标是什么,我在【技术美术图形部分】图形渲染管线1.0-基本概念&CPU负责的应用阶段中就说到过:渲染目标就是一种可以在运行时写入的纹理,当作一个缓冲区,而这里仅储存到默认的帧缓存中。
这里片元着色器仅做了一个输出颜色的操作(输出了白色),每个分量在[0.0, 1.0]之间。
上面只是写了一个超级简单的shader,获得模型的顶点位置,现在需要获取更多的模型数据,就需要定义一个更方便的参数,很自然就能联想到万能的struct~!
Shader "Unity Shaders Book/Chapter 5/Simple Shader"
{
SubShader {
//没进行渲染设置和标签设置,使用默认的
Pass {
//没自定义Pass的渲染设置和标签设置
CGPROGRAM
//两行非常重要的编译指令:
#pragma vertex vert //指定哪个函数包含了顶点着色器的代码 -> vert()
#pragma fragment frag //指定哪个函数包含了片元着色器的代码 -> frag()
struct a2v {
float4 vertex : POSITION; //用模型空间顶点坐标填充vertex
float3 normal : NORMAL; //用模型空间法线方向填充normal
float4 texcoord : TEXCOORD0; //用模型的第一套纹理坐标填充texcoord
}
float4 vert(a2v v) : SV_POSITION {
return UnityObjectToClipPos(v.vertex); //用v.vertex来访问结构体中的顶点坐标
}
fixed4 frag() : SV_Target {
return fixed4(1.0, 1.0, 1.0, 1.0);
}
ENDCG
}
}
}
struct StructName {
Type Name : Semantic;
Type Name : Semantic;
...
}; //千万别忘记最后的这个";"!!!!
上述代码中,将结构体命名为了a2v,a代表application,v代表顶点着色器(vertex shader),a2v——把应用阶段的数据传递给顶点着色器。
上述代码中的POSITION、NROMAL等语义中的数据均来自游戏对象的Mesh Renderer 组件
在渲染管线中提到过Draw Call——CPU会在应用阶段把信息等内容放入Draw Call中,每帧调用Draw Call时,Mesh Renderer组件会把模型数据发送给Unity Shader。
上面的代码中并没有展示出顶点着色器和片元着色器之间的数据通信,但通过学习渲染管线我们了解到:顶点着色器会把处理过的顶点数据传递给片元着色器进行插值计算。
那么如何在shader中体现出来?这就需要再定义一个负责顶点着色器输出的结构体:
Shader "Unity Shaders Book/Chapter 5/Simple Shader"
{
SubShader {
//没进行渲染设置和标签设置,使用默认的
Pass {
//没自定义Pass的渲染设置和标签设置
CGPROGRAM
//两行非常重要的编译指令:
#pragma vertex vert //指定哪个函数包含了顶点着色器的代码 -> vert()
#pragma fragment frag //指定哪个函数包含了片元着色器的代码 -> frag()
struct a2v {
float4 vertex : POSITION; //用模型空间顶点坐标填充vertex
float3 normal : NORMAL; //用模型空间法线方向填充normal
float4 texcoord : TEXCOORD0; //用模型的第一套纹理坐标填充texcoord
};
struct v2f {
float4 pos : SV_POSITION; //pos中包含了裁剪空间的位置信息
fixed3 color : COLOR0; //把颜色信息储存给color
};
v2f vert(a2v v) { //这里因为输出的是一个结构体v2f,所以前面的返回类型就不是float4了
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.color = v.normal * 0.5 + fixed3(0.5, 0.5, 0.5); //pixel=(normal+1)/2
return o;
}
fixed4 frag(v2f i) : SV_Target {
return fixed4(i.color, 1.0);
}
ENDCG
}
}
}
我在【技术美术图形部分】纹理基础2.0-凹凸映射中就有总结,这个其实就是法线和颜色值之间的映射,例如把法线信息储存给法线贴图中需要做一次normal->pixel的映射,使用时就需要做一次pixel->normal的映射。上述代码中就是把模型的法线信息转换成颜色值并储存给o.color中。
第4章刚开始我就提到了:顶点着色器会把处理过的顶点数据传递给片元着色器进行插值计算,啊!也就是片元着色器获取的信息是顶点着色器输出的信息插值后的结果。学习渲染管线已经了解到,顶点着色器到片元着色器需要经历:
顶点着色器 --> 裁剪 --> 屏幕映射 --> 三角形设置和遍历 --> 片元着色器
这一整个流程,插值就是在三角形设置和遍历的光栅化部分进行的,因此片元着色器拿到的信息其实是对顶点信息插值后的结果。
在上一篇博客中就提到了Properties语义块的作用——将参数可视化,方便调整。那么在shader中如何使用呢?
Shader "Unity Shaders Book/Chapter 5/Simple Shader"
{
Properties {
_Color ("Color Tint", Color) = (1.0, 1.0, 1.0, 1.0)
}
SubShader {
//没进行渲染设置和标签设置,使用默认的
Pass {
//没自定义Pass的渲染设置和标签设置
CGPROGRAM
//两行非常重要的编译指令:
#pragma vertex vert //指定哪个函数包含了顶点着色器的代码 -> vert()
#pragma fragment frag //指定哪个函数包含了片元着色器的代码 -> frag()
fixed4 _Color;
struct a2v {
float4 vertex : POSITION; //用模型空间顶点坐标填充vertex
float3 normal : NORMAL; //用模型空间法线方向填充normal
float4 texcoord : TEXCOORD0; //用模型的第一套纹理坐标填充texcoord
};
struct v2f {
float4 pos : SV_POSITION; //pos中包含了裁剪空间的位置信息
fixed3 color : COLOR0; //把颜色信息储存给color
};
v2f vert(a2v v) { //这里因为输出的是一个结构体v2f,所以前面的返回类型就不是float4了
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.color = v.normal * 0.5 + fixed3(0.5, 0.5, 0.5); //pixel=(normal+1)/2
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 c = i.color;
c *=_Color.rgb;
return fixed4(c, 1.0);
}
ENDCG
}
}
}
不知道有没有注意到,上面代码中一般关于颜色值的变量都用的是fixed3/fixed4类型的,但是其他的是float3/float4(half目前没遇到)。
三者其实是Cg/HLSL中的三种精度的数值类型。
类型 | 精度(范围并不是绝对的) |
float | 最高精度浮点,通常使用32位储存 |
half | 中等精度,16位储存,-60000~+60000 |
fixed | 最低精度,11位储存,-2.0~+2.0 |
《入门精要》给了建议:尽可能使用低精度的类型,可以优化Unity的性能,一般可以用fixed类型来储存颜色和单位矢量。