Unity3D使一大部分人开发游戏更简单化了。但是,有一部分仍然还有很长的路要走,就是着色器编码。着色器是专门用于运行在GPU上的程序。根本上,它就是绘制3D模型所需的众多三角形。如果你想给你的游戏特别的外观,那么,学习如何编写着色器是至关重要的。Unity也会使用它们做后期处理,这对2D游戏也是至关重要的。这篇教程会向你介绍如何编码着色器,而且主要面向对着色器一无所知的开发者。

介绍

下面三个图代表在Unity3D渲染工作流程中起作用的不同实体:
(3D模型、材质、着色器)

unity 溶解shader unity3d shader_HLSL

在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)。

下面的图片展示了当着色器被挂载到材质上时,这些属性是如何显示在检查器中的:

unity 溶解shader unity3d shader_unity3d_02

仅仅这样,还无法使用这些“属性”。这些“属性”将会被着色器中的隐藏变量所访问,而且这些变量仍然需要在着色器中定义,且被包含在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

unity 溶解shader unity3d shader_unity3d_03

在此示例中,使用行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
}

unity 溶解shader unity3d shader_unity3d_04

vert函数将顶点从原始3D空间转换为屏幕上的2D位置。Unity引入了UNITY_MATRIX_MVP来隐藏它后面的数学计算。之后,frag函数给每个像素的返回一个红色。请记住,顶点和片段着色器的CG部分需要被包含在Pass中。