Shaders: 顶点和片元程序
此教程将会教你书写Unity Shader里的顶点和片元程序的基础。请查看 开始学习 以获取更多关于ShaderLab的介绍。如果你想编写与灯光相互作用的着色器,那么请去阅读 [表面着色器](file:///D:/Users/hoxily/Documents/UnityDocs/Documentation.2018.4/en/Manual/SL-SurfaceShaders.html)。
让我们简要地概括一下着色器的一般结构:
Shader "MyShaderName"
{
Properties
{
// 材质的属性列表
}
SubShader // 兼容图形硬件A的子着色器
{
Pass
{
// Pass命令 ...
}
// 如有需要此处书写更多Pass指令
}
// 如有需要此处书写更多SubShader
FallBack "VertexLit" // 可选的回退指令
}
在这里我们介绍了一个新的指令:FallBack “VertexLit”。FallBack
可以用在shader的结尾处,它指出了当该着色器没有一个SubShader能在用户图形硬件上使用时该使用哪个着色器。它的效果正如将回退着色器的所有SubShader包含到该着色器的末尾一样。举例来说,如果你正在编写一个花俏的法线映射着色器,那么相比再写一个非常基本的无法线映射的SubShader用于老显卡,你可以直接使用FallBack指令回退到内建的VertexLit着色器。
虽然我们已经在上一章介绍了着色器的基本构建单元,但是完整的 [Properties](file:///D:/Users/hoxily/Documents/UnityDocs/Documentation.2018.4/en/Manual/SL-Properties.html), [SubShaders](file:///D:/Users/hoxily/Documents/UnityDocs/Documentation.2018.4/en/Manual/SL-SubShader.html), [Passes](file:///D:/Users/hoxily/Documents/UnityDocs/Documentation.2018.4/en/Manual/SL-Pass.html) 的文档也是可用的。
一个快速构建SubShader的方式就是使用其他着色器里定义好的Pass。UsePass
指令就是干这件事的。因此你可以以简洁的方式重用着色器代码。例如接下来的这个指令使用了来自内建的Specular着色器的FORWARD Pass:UsePass "Specular/FORWARD"
。
为了使UsePass起作用,需要给被引用的Pass命名。在一个Pass内使用Name
指令可以给该Pass设置名字:Name "MyPassName"
。
顶点和片元程序
我们在上一章介绍了一个仅仅使用一个纹理combine指令的Pass。现在是时候展示一下我们该如何在Pass中使用顶点和片元程序。
当你使用顶点和片元程序(这被称之为可编程管线)时,大部分硬编码的功能(固定函数管线)就会被图形硬件禁用。比如,使用顶点程序就会彻底关闭标准的3D变换、光照和纹理的坐标生成。类似的,使用片元程序就会替换任何SetTexture指令里的纹理combine模式,因此SetTexture就不再需要了。
书写顶点、片元程序需要精通3D变换、光照以及坐标空间,这些OpenGL API提供的固定函数都需你自己重写。换句话说,相比内建的固定函数,你可以做的东西多得多!
在ShaderLab里使用Cg/HLSL
在ShaderLab里着色器通常使用Cg/HLSL编程语言书写。Cg和DX9风格的HLSL可用于所有使用目的,并且它们是同一种语言,因此我们将互换地使用这两种说法(file:///D:/Users/hoxily/Documents/UnityDocs/Documentation.2018.4/en/Manual/SL-ShadingLanguage.html)。
通过在着色器文本里嵌入Cg/HLSL片段来书写着色器代码。这些片段被Unity Editor编译成低级着色器汇编,然后在你的游戏数据文件里只包含这些平台相关的低级汇编或字节码。当你在Project视图选中一个着色器时,Inspector面板会显示一个“show compiled code”的按钮,这可以辅助你调试。Unity自动编译Cg片段,用于所有相关的平台(Direct3D 9,OpenGL,Direct3D 11, OpenGL ES等)。请注意,由于Cg/HLSL是由Editor编译,因此你无法在运行时创建着色器。
一般而言,片段嵌入到Pass块里面。它们看起来就像这样子:
Pass {
// ... 常规的Pass状态设置 ...
CGPROGRAM
// 此片段的编译指令
#pragma vertex vert
#pragma fragment frag
// Cg/HLSL 代码
ENDCG
// ... 剩下的Pass设置 ...
}
下面的例子展示了一个完整的着色器,它可以以颜色的方式渲染物体的法线:
Shader "Tutorial/Display Normals" {
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f {
float4 pos : SV_POSITION;
fixed3 color : COLOR0;
};
v2f vert (appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.color = v.normal * 0.5 + 0.5;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return fixed4 (i.color, 1);
}
ENDCG
}
}
}
当将此着色器应用到物体上时,产生的效果如下所示:
我们的“Display Normals”着色器没有任何属性,只包含一个SubShader,SubShader里面只有一个Pass,这个Pass里面除了Cg/HLSL代码就是空的。让我们来逐项地剖析代码:
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// ...
ENDCG
整个代码片段都是写在 CGPROGRAM
和 ENDCG
两个关键词之间。在开始处给出了 #pragma
编译指令:
- #pragma vertex name 指出了顶点函数的名字。
- #pragma fragment name 指出了片元函数的名字。
接下来的编译指令就是简单的Cg/HLSL代码。我们使用了 #include
指令包含了一个内建的包含文件:
#include "UnityCG.cginc"
UnityCG.cginc
文件包含了常用的声明和函数,可以减少书写着色器的代码量(详见 [着色器包含文件](file:///D:/Users/hoxily/Documents/UnityDocs/Documentation.2018.4/en/Manual/SL-BuiltinIncludes.html) 页面)。此处我们将会使用UnityCG.cginc里的appdata_base结构体。当然我们也可直接定义这个结构体,而不包含那个文件。
接下来我们定义了一个“顶点到片元”的结构(此处命名为v2f
),这个结构用于将数据从顶点函数传送到片元函数。我们传送了position和color两个参数。这个颜色将会在顶点程序里计算,然后直接在片元程序里输出。
接下来我们定义了顶点程序——一个vert函数。在这里,我们计算了position参数并将输入的法线转换成了颜色值:o.color = v.normal * 0.5 + 0.5;
法线的各分量的范围在 -1 ~ 1 之间,而颜色的各分量则是在0~1之间,因此在上面的代码里我们将它先缩放再偏移。接下来我们定义了片元程序——frag函数,它直接将计算出来的颜色输出,并使用了1作为颜色的alpha分量:
fixed4 frag (v2f i) : SV_Target
{
return fixed4(i.color, 1);
}
就这样,我们的着色器完成啦!即使是这样一个简单的着色器对于可视化网格的法线也是非常有用的。
当然这个着色器不会对灯光做出任何反应,而与灯光交互正是事情变得有趣的关键,请参阅 [表面着色器](file:///D:/Users/hoxily/Documents/UnityDocs/Documentation.2018.4/en/Manual/SL-SurfaceShaders.html) 。
Cg/HLSL代码里使用着色器属性
当你在着色器里定义属性时,你给它们取了类似 _Color
或者 _MainTex
的名字。你需要在Cg/HLSL里定义与其相匹配的名字和类型的变量才能够使用它们。详见 [着色器程序里的属性](file:///D:/Users/hoxily/Documents/UnityDocs/Documentation.2018.4/en/Manual/SL-PropertiesInPrograms.html) 。
接下来是一个完整的着色器,展示了一个被颜色调制过的纹理。当然,你也可以在纹理combiner指令里干同样的事情,但是此处的关注点是演示了在Cg里如何使用属性:
Shader "Tutorial/Textured Colored" {
Properties {
_Color ("Main Color", Color) = (1,1,1,0.5)
_MainTex ("Texture", 2D) = "white" { }
}
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
fixed4 _Color;
sampler2D _MainTex;
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
float4 _MainTex_ST;
v2f vert (appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX (v.texcoord, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 texcol = tex2D (_MainTex, i.uv);
return texcol * _Color;
}
ENDCG
}
}
}
此着色器的结构与之前的例子相当。在这里我们定义了两个属性,名为 _Color
和 _MainTex
。在Cg/HLSL代码里我们定义了相应的变量:
fixed4 _Color;
sampler2D _MainTex;
参见 [在Cg/HLSL里访问着色器属性](file:///D:/Users/hoxily/Documents/UnityDocs/Documentation.2018.4/en/Manual/SL-PropertiesInPrograms.html) 以获取更多关于信息。
这里的顶点和片元程序并没有任何花俏的地方。顶点程序使用了来自UnityCG.cginc的 TRANSFORM_TEX
宏,用来确保正确应用了纹理的缩放和偏移。片元程序仅仅对纹理进行了采样,并乘上了_Color
属性。
总结
我们展示了如何在简单的几个步骤里编写自定义的着色器程序。虽然这里展示的例子都是非常简单的,但是并没有限制你编写任何复杂的着色器程序!这能帮助你充分利用Unity的优势,获取最佳的渲染结果。
完整的ShaderLab参考手册[在这里](file:///D:/Users/hoxily/Documents/UnityDocs/Documentation.2018.4/en/Manual/SL-Reference.html),[顶点和片元着色器例子](file:///D:/Users/hoxily/Documents/UnityDocs/Documentation.2018.4/en/Manual/SL-VertexFragmentShaderExamples.html)页面有更多的例子。我们同时还有讨论着色器的论坛,地址是 forum.unity3d.com,你可以去那里获取编写着色器的帮助!编程快乐,享受Unity和ShaderLab的能力吧。