Unity3D使一大部分人开发游戏更简单化了。但是,有一部分仍然还有很长的路要走,就是着色器编码。着色器是专门用于运行在GPU上的程序。根本上,它就是绘制3D模型所需的众多三角形。如果你想给你的游戏特别的外观,那么,学习如何编写着色器是至关重要的。Unity也会使用它们做后期处理,这对2D游戏也是至关重要的。这篇教程会向你介绍如何编码着色器,而且主要面向对着色器一无所知的开发者。
介绍
下面三个图代表在Unity3D渲染工作流程中起作用的不同实体:
(3D模型、材质、着色器)
在3D模型中包含:顶点位置(模型点),顶点颜色,法线(顶点方向),UV数据(纹理图映射)。
在材质中包含:纹理(Textures),着色器属性值(Shader property values)。
在着色器中包含:计算机图形学,CG / 高级着色器语言,简称HLSL 代码
3D模型,本质上是称作顶点的3D坐标的集合。它们使许许多多的三角形连接在一起。每个顶点可能还包含一些其他信息,例如:颜色,法线(点的朝向),UV数据(将坐标映射在纹理上)。
没有材质的模型是不能够被渲染的。材质内包含着色器和它的属性。因此,不同的材质可能使用同一个着色器,但是拥有不同的属性值。
剖析着色器
Unity支持两种不同的着色器:表面着色器(surface shaders)和片段、顶点着色器(fragment/vertex着色器)。其实还有第三种类型,固定功能着色器(fixed function shader),但它们现在已经过时了。
无论哪种类型符合您的需求,着色器的结构都是一样的:
Shader "MyShader"
{
Properties
{
// The properties of your shaders
// - textures
// - colours
// - parameters
// ...
}
SubShader
{
// The code of your shaders
// - surface shader
// OR
// - vertex and fragment shader
// OR
// - fixed function shader
}
}
你可以有多个SubShader
,从上到下排列,它们实际上包含对GPU执行的说明,Unity3D会尝试按顺序执行它们,直到找到一个与显卡兼容的。这对编码不同的平台非常有用,因为你可以在同一个着色器文件中,适配不同的版本。
属性
着色器的属性(Properties)与C#脚本中的公共字段相当(public fields);它们将出现在Material的Inspector上,让你有机会去调整它。与脚本不同,材质是资产:挡在编辑器中运行游戏时,对材质属性所做的更改是永久性的。即使停止游戏后,你也会发现你对材质所做的更改。
以下代码段涵盖了着色器中可以拥有的所有基本类型的属性的定义:
Properties
{
_MyTexture ("My texture", 2D) = "white" {}
_MyNormalMap ("My normal map", 2D) = "bump" {} // Grey
_MyInt ("My integer", Int) = 2
_MyFloat ("My float", Float) = 1.5
_MyRange ("My range", Range(0.0, 1.0)) = 0.5
_MyColor ("My colour", Color) = (1, 0, 0, 1) // (R, G, B, A)
_MyVector ("My Vector4", Vector) = (0, 0, 0, 0) // (x, y, z, w)
}
用于_MyTexture
和_MyNormalMap
的2D类型表示其是纹理参数。它们能被初始化为白色、黑色和灰色。你也可以使用bump指示纹理将用作法线贴图。在这种情况下,它会自动初始化为#808080
,用于表示没有bump。矢量和颜色总是有四个元素(分别为XYZW和RGBA)。
下面的图片展示了当着色器被挂载到材质上时,这些属性是如何显示在检查器中的:
仅仅这样,还无法使用这些“属性”。这些“属性”将会被着色器中的隐藏变量所访问,而且这些变量仍然需要在着色器中定义,且被包含在SubShader中。
SubShader
{
// Code of the shader
// ...
sampler2D _MyTexture;
sampler2D _MyNormalMap;
int _MyInt;
float _MyFloat;
float _MyRange;
half4 _MyColor;
float4 _MyVector;
// Code of the shader
// ...
}
纹理的类型是sampler2D。向量为float4,颜色通常为half4,分别使用32位和16位。使用这种语言编写着色器,变量的名字必须和之前定义的完全匹配。但是类型就不必这样,你完全可以声明_MyRange
作为half而不是float,却不被报错。还有些会让人迷惑的地方,比如:你可以声明一个向量属性,但是把它链接到一个float2变量,Unity会自动的忽略掉多余的2个值。
渲染顺序
如上所述,SubShader部分包含着色器的实际代码,用Cg / HLSL编写,与C代码很类似。由于着色器代码会被执行到图像的每个像素上,所以执行效率至关重要。由于GPU的架构,您可以在着色器中执行的指令数量是有限制的。
一个着色器的内容,一般来说是这样的:
SubShader
{
Tags
{
"Queue" = "Geometry"
"RenderType" = "Opaque"
}
CGPROGRAM
// Cg / HLSL code of the shader
// ...
ENDCG
}
实际的着色器代码包含在CGPROGRAM和ENDCG指令的信号部分中。
在开始着色器之前,要先介绍标签。标签是告诉Unity正在写的着色器某些属性的一种方式。比如,渲染的顺序(Queue)和怎么样去渲染(RenderType)。
当渲染三角形时,GPU通常根据它们离相机的距离进行排序,以便先绘制更远的图形。这通常足以渲染几何物体,但如果是透明对象,它通常会失败。这就是为什么Unity允许指定标签队列(tag Queue),它可以控制每种材质的渲染顺序。队列接受正整数(越小,绘制越早),也可以使用一些其他标签:
- 背景(Background - 1000):用于绘制背景和天空盒子。
- 几何(Geometry - 2000):用于大部分固态物体的默认标签。
- 透明(Transparent - 3000):用于材质中有透明属性的情况,例如玻璃、火、粒子、水等。
- 覆盖(Overlay - 4000):用于镜头上的耀斑,GUI元素和文字等效果。
Unity也允许指定相对顺序,例如:Background+2,它表示队列的值是1002。与队列交错可能会产生让人讨厌的情况,比如始终绘制对象,即使它应该被其他模型覆盖。
ZTest
请记住,“透明”的对象并不一定总是出现在几何对象的上方。默认情况下,GPU会执行一个称为ZTest的测试,它可以阻止隐藏的像素(透明对象)被绘制。为了达到这个目的,GPU需要一个额外的缓冲区,大小和要渲染的屏幕一致。每个被绘制的像素都包含深度(与摄像机的距离)。如果要写入比当前深度更远的像素,则丢弃该像素。ZTest会剔除被其他对象隐藏的像素,无论它们在屏幕上绘制的顺序如何。
表面与定点和片段(Surface/vertex&fragment)
在编写着色器前的最后一个步骤,是必须决定使用哪种类型的着色器。本节将简要介绍一下着色器,但不会真正解释它们。本教程的下一部分将覆盖表面和顶点和片段着色器。
表面着色器(The surface shader)
当使用材质去模拟物体真实受到灯光的影响时,那就可能需要一个表面着色器。表面着色器隐藏了光线反射的计算,并允许在称为surf的函数中指定属性,如反照率,法线,反射率等。这些值会被插入到照明模型中,然后为每像素输出最终的RGB值。另外,也可以编写自己的照明模型,但这只在非常高级的效果中会被用到。
一个表面着色器的典型代码如下:
CGPROGRAM
// Uses the Lambertian lighting model
#pragma surface surf Lambert
sampler2D _MainTex; // The input texture
struct Input {
float2 uv_MainTex;
};
void surf (Input IN, inout SurfaceOutput o) {
o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
}
ENDCG
在此示例中,使用行sampler2D _MainTex输入纹理; ,然后将其设置为surf函数中材料的反照率属性(Albedo)。着色器使用朗伯照明模型,这是一种非常典型的模拟光线如何反射到物体上的方式。仅使用反照率(albedo)属性的着色器通常称为漫反射(diffuse)。
顶点和片段着色器(The vertex and fragment shader
)
顶点和片段着色器与GPU渲染三角形的方式很接近,对于光的行为没有内部的支持。几何模型首先会通过一个名为vert的函数,该函数可以改变模型的顶点。然后,单个三角形通过另一个frag函数,该函数决定了每个像素的最终RGB的颜色。它们对于2D效果,后处理和特殊的3D效果非常有用,但是这些效果太复杂,无法为表面着色。
以下顶点和片段着色器仅使对象统一的红色,无照明:
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
struct vertInput {
float4 pos : POSITION;
};
struct vertOutput {
float4 pos : SV_POSITION;
};
vertOutput vert(vertInput input) {
vertOutput o;
o.pos = mul(UNITY_MATRIX_MVP, input.pos);
return o;
}
half4 frag(vertOutput output) : COLOR {
return half4(1.0, 0.0, 0.0, 1.0);
}
ENDCG
}
vert函数将顶点从原始3D空间转换为屏幕上的2D位置。Unity引入了UNITY_MATRIX_MVP
来隐藏它后面的数学计算。之后,frag函数给每个像素的返回一个红色。请记住,顶点和片段着色器的CG部分需要被包含在Pass中。