材质

在Unity中我们需要配合使用材质 (Material) 和Unity Shader才能达到需要的效果。一个最常见的流程是

  • 创建一个材质;活着导入一个材质
  • 创建一个Unity Shader,并把它赋给上一步中创建的材质;
  • 把材质赋给要渲染的对象;
  • 在材质面板中调整Unity Shader的属性,以得到满意的效果。

Unity的材质

Unity中的材质需要结合一个GameObject的Mesh或者Particle Systems组件来工作。它决定了我们的游戏对象看起来是什么样子的(这当然也需要Unity Shader的配合)。

Unity的shader

Unity一共提供了4种Unity Shader模板供我们选择

  • Standard Surface Shader: 会产生一个包含了标准光照模型的表面着色器模板
  • Unlit Shader: 会产生一个不包含光照(但包含雾效)的基本的顶点/片元着色器
  • Image Effect Shader: 为我们实现各种屏幕后处理效果(详见第12章)提供了一个基本模板
  • Compute Shade: 产生一种特殊的Shader文件,这类Shader旨在利用GPU的并行性来进行一些与常规渲染流水线无关的计算

ShaderLab

Unity为了解决初学者更好的入门shader编程,以及更方便的进行shader编程,为我们提供了一层抽象——Unity Shader。

什么是ShaderLab?

Unity Shader是Unity为开发者提供的高层级的渲染抽象层

基础结构

一个Unity Shader的基础结构如下所示

Shader "ShaderName" {
    Properties {
        // 属性
    }
    SubShader {
        // 显卡A使用的子着色器
    }
    SubShader {
        // 显卡B使用的子着色器
    }    
    Fallback "VertexLit"
}

Properties

材质和Unity Shader的桥梁

下面的代码给出了一个展示所有属性类型的例子:

代码
Shader "Custom/ShaderLabProperties" {
    Properties {
        // Numbers and Sliders
        _Int ("Int", Int) = 2
        _Float ("Float", Float) = 1.5
        _Range("Range", Range(0.0, 5.0)) = 3.0
        // Colors and Vectors
        _Color ("Color", Color) = (1,1,1,1)
        _Vector ("Vector", Vector) = (2, 3, 6, 1)
        // Textures
        _2D ("2D", 2D) = "" {}
        _Cube ("Cube", Cube) = "white" {}
        _3D ("3D", 3D) = "black" {}
    }

    FallBack "Diffuse"
}

SubShader

每一个Unity Shader文件可以包含多个SubShader 语义块,但最少要有一个。

当Unity需要加载这个Unity Shader时,Unity会扫描所有的SubShader 语义块,然后选择第一个能够在目标平台上运行的SubShader 。如果都不支持的话,Unity就会使用Fallback 语义指定的Unity Shader。

SubShader 语义块中包含的定义通常如下:

代码
SubShader {
    // 可选的
    [Tags]

    // 可选的
    [RenderSetup]

    Pass {
    }
    // Other Passes
}

  • 每个Pass 定义了一次完整的渲染流程,但如果Pass 的数目过多,往往会造成渲染性能的下降
  • 状态[RenderSetup]和标签[Tags]同样可以在Pass 声明,
  • SubShader 中的一些标签设置是特定的。也就是说,这些标签设置和Pass 中使用的标签是不一样的
  • 对于状态设置来说,其使用的语法是相同的。但是,如果我们在SubShader 进行了这些设置,那么将会用于所有的Pass

状态设置

ShaderLab中常见的渲染状态设置选项:

SubShader 的标签

标签的结构如下:

Tags { "TagName1" = "Value1" "TagName2" = "Value2" }

Pass 语义块

Pass 基本结构如下:

Pass { 
    [Name]
    [Tags] 
    [RenderSetup] 
    // Other code
}

我们可以使用ShaderLab的UsePass命令 + PassName来直接使用其他Unity Shader中的Pass,这样可以提高代码的复用性。

Name "MyPassName"
UsePass "MyShader/MYPASSNAME"

由于Unity内部会把所有Pass 的名称转换成大写字母的表示,因此,在使用UsePass 命令时必须使用大写形式的名字。

Pass 中使用的标签类型:

Unity Shader还支持一些特殊的Pass ,以便进行代码复用或实现更复杂的效果:

  • UsePass :如我们之前提到的一样,可以使用该命令来复用其他Unity Shader中的Pass ;
  • GrabPass :该Pass 负责抓取屏幕并将结果存储在一张纹理中,以用于后续的Pass 处理

留一条后路:Fallback

紧接着SubShader 语义块后面的可以是Fallback指令,它用于告诉Unity,“如果上面所有的SubShader在这块显卡上都不能运行,那么就使用这个最低级的Shader吧!”

Fallback "name"
// 或者
Fallback Off
  • Fallback 还会影响阴影的投射
  • 在渲染阴影纹理时,Unity会在每个Unity Shader中寻找一个阴影投射的Pass
  • 通常情况下,我们不需要自己专门实现一个Pass,这是因为Fallback 使用的内置Shader中包含了这样一个通用的Pass。

三种形式

真正意义上的Shader代码都需要包含在ShaderLab语义块中,如下所示:

代码
Shader "MyShader" {
    Properties {
        // 所需的各种属性
    }
    SubShader {
        // 真正意义上的Shader代码会出现在这里
        // 表面着色器(Surface Shader)或者
        // 顶点/片元着色器(Vertex/Fragment Shader)或者
        // 固定函数着色器(Fixed Function Shader)
    }
    SubShader {
        // 和上一个SubShader类似
    }
}

Unity的宠儿:表面着色器

表面着色器 (Surface Shader) 是Unity自己创造的一种着色器代码类型

  • 它需要的代码量很少,Unity在背后做了很多工作,但渲染的代价比较大
  • 当给Unity提供一个表面着色器的时候,它在背后仍旧把它转换成对应的顶点/片元着色器, 它在本质上和下面要讲到的顶点/片元着色器是一样的
  • 它存在的价值在于,Unity为我们处理了很多光照细节,使得我们不需要再操心这些“烦人的事情”

一个简单的表面着色器示例代码

Shader "Custom/Simple Surface Shader" {
    SubShader {
        Tags { "RenderType" = "Opaque" }
        CGPROGRAM
        #pragma surface surf Lambert
        struct Input {
            float4 color : COLOR;
        };
        void surf (Input IN, inout SurfaceOutput o) {
            o.Albedo = 1;
        }
        ENDCG
    }
    Fallback "Diffuse"
}
  • 表面着色器被定义在SubShader 语义块(而非Pass 语义块)中的CGPROGRAM 和ENDCG 之间。
  • 表面着色器不需要开发者关心使用多少个Pass、每个Pass如何渲染等问题,Unity会在背后为我们做好这些事情
  • CGPROGRAM 和ENDCG 之间的代码是使用Cg/HLSL编写的。

最聪明的孩子:顶点/片元着色器

一个简单的顶点/片元着色器示例代码:

代码
Shader "Custom/Simple VertexFragment Shader" {
    SubShader {
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            float4 vert(float4 v : POSITION) : SV_POSITION {
                return mul (UNITY_MATRIX_MVP, v);
            }

            fixed4 frag() : SV_Target {
                return fixed4(1.0,0.0,0.0,1.0);
            }

            ENDCG
        }
    }
}

  • 顶点/片元着色器的代码也需要定义在CGPROGRAM 和ENDCG 之间, 但是代码是写在Pass 语义块内,而非SubShader 内的

被抛弃的角落:固定函数着色器

对于一些较旧的设备(其GPU仅支持DirectX 7.0、OpenGL 1.5或OpenGL ES 1.1),例如iPhone 3,它们不支持可编程管线着色器,因此,这时候我们就需要使用固定函数着色器 (Fixed Function Shader) 来完成渲染。

简单代码:

代码
Shader "Tutorial/Basic" {
    Properties {
        _Color ("Main Color", Color) = (1,0.5,0.5,1)
    }
    SubShader {
        Pass {
            Material {
                Diffuse [_Color]
            }
            Lighting On
        }
    }
}

  • 固定函数着色器的代码被定义在Pass 语义块中
  • 需要完全使用ShaderLab的语法(即使用ShaderLab的渲染设置命令)来编写,而非使用Cg/HLSL。
  • 在Unity 5.2中,所有固定函数着色器都会在背后被Unity编译成对应的顶点/片元着色器,因此真正意义上的固定函数着色器已经不存在了

选择哪种形式

  • 除非你有非常明确的需求必须要使用固定函数着色器,例如需要在非常旧的设备上运行你的游戏(这些设备非常少见),否则请使用可编程管线的着色器,即表面着色器或顶点/片元着色器
  • 如果你想和各种光源打交道,你可能更喜欢使用表面着色器,但需要小心它在移动平台的性能表现。
  • 如果你需要使用的光照数目非常少,例如只有一个平行光,那么使用顶点/片元着色器是一个更好的选择。 最重要的是,如果你有很多自定义的渲染效果,那么请选择顶点/片元着色器

Unity Shader != 真正的Shader

在Unity里,Unity Shader实际上指的就是一个ShaderLab文件——硬盘上以.shader 作为文件后缀的一种文件

在Unity Shader(或者说是ShaderLab文件)里,我们可以做的事情远多于一个传统意义上的Shader:

  • 在传统的Shader中,我们仅可以编写特定类型的Shader,例如顶点着色器、片元着色器等。而在Unity Shader中,我们可以在同一个文件里同时包含需要的顶点着色器和片元着色器代码。
  • 在传统的Shader中,我们无法设置一些渲染设置,例如是否开启混合、深度测试等,这些是开发者在另外的代码中自行设置的。而在Unity Shader中,我们通过一行特定的指令就可以完成这些设置。
  • 在传统的Shader中,我们需要编写冗长的代码来设置着色器的输入和输出,要小心地处理这些输入输出的位置对应关系等。而在Unity Shader中,我们只需要在特定语句块中声明一些属性,就可以依靠材质来方便地改变这些属性。而且对于模型自带的数据(如顶点位置、纹理坐标、法线等),Unity Shader也提供了直接访问的方法,不需要开发者自行编码来传给着色器。

Unity Shader的缺点:

  • 由于Unity Shader的高度封装性,我们可以编写的Shader类型和语法都被限制了
  • 对于一些类型的Shader,例如曲面细分着色器(Tessellation Shader)、几何着色器(Geometry Shader)等,Unity的支持就相对差一些
  • 一些高级的Shader语法Unity Shader也不支持

--完--