Unity Shader入门精要学习笔记 - 第3章 Unity Shader 基础

来源作者:candycat   http://blog.csdn.net/candycat1992/article/

概述

总体来说,在Unity中我们需要配合使用材质和Unity Shader才能达到需要的效果。一个最常见的流程是。

1)创建一个材质

2)创建一个Unity Shader,并把它赋给上一步创建的材质

3)把材质赋给要渲染的对象

4)在材质面板中调整Unity Shader的属性,以得到满意的效果

下图显示了Unity Shader和材质是如何一起工作来控制物体的渲染的。

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

Unity Shader 的基础:ShaderLab

Unity Shader 为控制渲染过程提供了一层抽象。如果没有使用Unity Shader(上左),开发者需要很多文件设置打交道,才能让画面呈现想要的效果,而在Unity Shader的帮助下(上右),开发者只需要使用ShaderLab来编写Unity Shader文件就可以完成所有的工作。

在Unity中,所有的Unity Shader 都是使用ShaderLab来编写的。ShaderLab是Unity提供的编写Unity Shader 的一种说明性语言。它使用了一些嵌套在花括号内部的语义来描述一个Unity Shader文件的结构。这些结构包含了许多渲染所需的数据,例如Properties 语句块中定义了着色器所需的各种属性,这些属性将会出现在材质面板中。从设计上来说,ShaderLab类似于CgFx的Direct3D Effects (.Fx)语言,它们都定义了要显示一个材质所需要的所有东西,而不仅仅是着色器代码。

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

Unity 会在背后根据使用的平台来把这些结构编译成真正的代码和Shader 文件,而开发者只需要和Unity Shader 打交道即可。

Unity Shader 的结构

每个Unity Shader文件的第一行都需要通过Shader语义来指定该Unity Shader的名字。这个名字由一个字符串来定义,例如"MyShader"。当为材质选择使用的Unity Shader 时,这些名称就会出现在材质面板的下拉面板里。通过在字符串中添加斜杠(“/”),可以控制Unity Shader在材质面板中出现的位置。

Properties语义块中包含了一系列属性,这些属性将会出现在材质面板中。

Properties语义块通常定义如下:


Properties
{
    Name ("display name",PropertyType) = DefaultValue
}  

开发者们声明这些属性是为了在材质面板中能够方便地调整各种材质属性。如果我们需要再Shader 中访问它们,就需要使用每个属性的名字。在Unity中,这些属性的名字通常由一个下划线开始。显示的名称则是出现在材质面板上的名字。我们需要为每个属性指定它的类型,常见的属性类型如下表。除此之外,我们还需要为每个属性指定一个默认值,在我们第一次把该Unity Shader赋给某个材质,材质面板上显示的就是这些默认值。

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

Unity提供这种语义的原因在于,不同的显卡具有不同的能力。

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

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

ShaderLab提供了一系列渲染状态的设置指令,这些指令可以设置显卡的各种状态,下表给出了ShaderLab中常见的渲染状态设置选项。

当在SubShader块中设置了上述渲染状态时,将会应用到所有的Pass。如果我们不想这样(例如在双面渲染中,我们希望在第一个Pass中剔除正面来对背面进行渲染,在第二个Pass中剔除背面来对正面进行渲染),可以在Pass语义块中单独进行上面的设置。

SubShader的标签是一个键值对,它的键和值都是字符串类型。这些键值对是SubShader和渲染引擎之间的沟通桥梁。它们用来告诉Unity的引擎:SubShader我希望怎样以及何时渲染这个对象。支持的标签类型如下:

需要注意的是,上述标签仅可以在SubShader中声明,而不可再Pass块中声明。Pass块虽然也可以定义标签,但这些标签是不同于SubShader的标签类型。

Pass语义块的定义如下:

首先,我们可以在Pass中定义该Pass的名称,例如:

Name "MyPassName"  

通过这个名称,我们可以使用ShaderLab的UsePass命令来直接使用其他Unity Shader 中的Pass,例如:

UsePass "MyShader/MYPASSNAME"  

这样可以提高代码的复用性。需要注意的是,由于Unity内部会把所有Pass的名称转换成大写字母的表示,因此,在使用UsePass命令时必须使用大写形式的名字。

其次,我们可以对Pass设置渲染状态。SubShader的状态设置通用适用于Pass。除了上面提到的状态设置外,在Pass中我们还可以使用固定渲染管线的着色器命令。

Pass同样可是设置标签,但它的标签不同于SubShader的标签。这些标签页是用于告诉渲染引擎我们希望怎样来渲染该物体。下表给出了Pass中使用的标签类型。

除了上面普通的Pass定义外,Unity Shader 还支持一些特殊的Pass,以便进行代码复用或实现更复杂的效果。

紧跟着在各个SubShader语义块后面的,可以是一个Fallback指令。它用于告诉Unity,如果上面所有的SubShader在这块显卡上都不能允许,那就使用这个最低级的吧。

它的语义如下:

Fallback "name"
//或者
Fallback off  

如上所述,我们可以通过一个字符串来告诉Unity这个最低级的Shader是谁,我们也可以关闭Fallback 功能。

事实上,Fallback 还会影响阴影的投射。在渲染阴影纹理时,Unity会在每个Unity Shader中寻找一个阴影投射的Pass。通常情况下,我们不需要自己专门实现一个Pass,这是因为Fallback使用的内置Shader包含了这样一个通用的Pass。因此,为每个Unity Shader正确设置Fallback是非常重要的。

Unity Shader 的形式

尽管Unity Shader可以做的事情非常多,但其最重要的任务还是指定各种着色器所需的代码。这些着色器代码可以写在SubShader语义块中(表面着色器的做法),也可以写在Pass语义块中(顶点/片元着色器和固定函数着色器的做法)。

在Unity中,我们可以使用下面3种形式来编写Unity Shader。而不管使用哪种形式,真正意义上的Shader代码都需要包含在ShaderLab 语义块中,如下所示:

Shader "MyShader"
{
    Properties
    {
        //所需的各种属性
    }
    SubShader
    {
        //真正意义上的Shader代码会出现在这里
        //表面着色器或者顶点/片元着色器或者固定函数着色器
    }  

表面着色器是Unity自己创造的一种着色器代码类型。它需要的代码量很少,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语义块中的CGPROGRAM 和 ENDCG之间。原因是,表面着色器不需要开发者关心使用多少个Pass、每个Pass如何渲染等问题,Unity会在背后为我们做好这些事情。我们要做的只是告诉它:“使用这些纹理去填充颜色,使用这个法线纹理去填充法线,使用Lambert光照模型。”

CGPROGRAM 和 ENDCG之间的代码是使用CG/HLSL 编写的,也就是说,我们需要把CG/HLSL语言嵌套在ShaderLab语言中。值得注意的是,这里的CG/HLSL是Unity经封装后提供的,它的语法和标准的CG/HLSL语法几乎一样,但还是由细微不同的,例如有些原声的函数和用法Unity并没有提供支持。

在Unity中我们可以使用CG/HLSL 语言来编写顶点/片元着色器。它们更加复杂,但灵活性也更高。

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

Shader "Custom/Simple VertextFrament Shader"
{
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertext vert
            #pragma frament frag  

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

            float4 frag() : SV_Target
            {
                return fixed(1.0,1.0,1.0,1.0);
            }  

            ENDCG
        }
    }
}  

和表面着色器类似,顶点/片元着色器的代码也需要定义在CGPROGRAM 和 ENDCG之间,但不同的是,顶点/片元着色器是写在Pass语义块内,而非SubShader内的。原因是,我们需要自己定义每个Pass需要使用的Shader代码。虽然我们可能需要编写更多的代码,但带来的好处是灵活性很高。更重要的是,我们可以控制渲染的实现细节。同样,这里的CGPROGRAM和ENDCG之间的代码也是使用CG/HLSL编写的。

上述两种Unity Shader 形式都使用了可编程管线。而对于一些较旧的设备,它们不支持可编程管线着色器,因此,这时候我们就需要使用固定函数着色器来完成渲染。这些着色器往往只可以完成一些非常简单的效果。

一个非常简单的固定函数着色器示例代码如下:

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

可以看出,固定函数着色器的代码被定义在Pass语义块中,这些代码相当于Pass中的一些渲染设置。

对于固定函数着色器来说,我们需要完全使用ShaderLab的语法来编写,而非使用CG/HLSL。
由于现在绝大多数GPU都支持可编程的渲染管线,这种固定管线 编程方式已经逐渐被抛弃。实际上,在Unity5.2中,所有固定函数着色器都会在背后Unity被编译成对应的顶点/片元着色器,因此真正意义上的固定函数着色器已经不存在了。

如何选择哪种Unity Shader:

除非有非常明确的需求必须要使用固定函数着色器,例如需要在非常旧的设备上运行游戏,否则不要使用固定函数着色器。

如果想和各种光源打交道,你可能更喜欢使用表面着色器,但需要小心它在移动平台的性能表现。

如果光照数量少,例如只有一个平行光,那么使用顶点/片元着色器是一个更好的选择。

更重要的是,如果有很多自定义的渲染效果,那么顶点/片元着色器更好。

时间: 2024-10-28 21:33:00

Unity Shader入门精要学习笔记 - 第3章 Unity Shader 基础的相关文章

Unity Shader入门精要学习笔记 - 第4章 学习 Shader 所需的数学基础

摘录自 冯乐乐的<Unity Shader入门精要> 笛卡尔坐标系 1)二维笛卡尔坐标系 在游戏制作中,我们使用的数学绝大部分都是计算位置.距离.角度等变量.而这些计算大部分都是在笛卡尔坐标系下进行的. 一个二维的笛卡尔坐标系包含了两个部分的信息: 一个特殊的位置,即原点,它是整个坐标系的中心. 两条过原点的互相垂直的矢量,即X轴和Y轴.这些坐标轴也被称为是该坐标的矢量. OpenGL 和 DirectX 使用了不同的二维笛卡尔坐标系.如下图所示: 2)三维笛卡尔坐标系 在三维笛卡尔坐标系中,

Unity Shader入门精要学习笔记 - 第6章 开始 Unity 中的基础光照

转自冯乐乐的<Unity Shader入门精要> 通常来讲,我们要模拟真实的光照环境来生成一张图像,需要考虑3种物理现象. 首先,光线从光源中被发射出来. 然后,光线和场景中的一些物体相交:一些光线被物体吸收了,而另一些光线被散射到其他方向. 最后,摄像机吸收了一些光,产生了一张图像. 在光学中,我们使用辐照度来量化光.对于平行光来说,它的辐照度可通过计算在垂直于l的单位面积上单位时间内穿过的能量来得到.在计算光照模型时,我们需要知道一个物体表面的辐照度,而物体表面往往是和l不垂直的,我们可以

Unity Shader入门精要读书笔记(一)序章

本系列的博文是笔者读<Unity Shader入门精要>的读书笔记,这本书的章节框架是: 第一章:着手准备. 第二章:GPU流水线. 第三章:Shader基本语法. 第四章:Shader数学基础. 第五章:利用简单的顶点/片元着色器来实现辅助技巧. 第六章:基本光照模型. 第七章:法线纹理.遮罩纹理等基础纹理. 第八章:透明度测试和透明度混合. 第九章:复杂光照实现. 第十章:高级纹理(立方体纹理等). 第十一章:纹理动画.顶点动画. 第十二章:屏幕特效. 第十三章:深度纹理. 第十四章:非真

《Unity Shader入门精要》读书笔记(1)

主要是对第二章的整理 渲染流水线:由一个三维场景出发,生成(渲染)一张二维图像. 渲染流程:应用阶段.几何阶段.光栅化阶段. 应用阶段: 1. 把数据加载到显存中 渲染所需数据从硬盘,到内存,再到显存 2. 设置渲染状态 渲染状态:使用哪个顶点着色器.片元着色器.光源属性.材质等 3. 调用Draw Call 发起方CPU,接收方GPU GPU流水线 以下为几何阶段主要步骤 顶点着色器: CPU输入的每一个顶点都会调用一次顶点着色器 不创建或销毁任何顶点,且顶点之间相互独立 坐标转换:把顶点坐标

Python编程入门到实践 - 笔记( 3 章)

练习内容包括 创建并访问列表 列表的索引 使用列表中的各个值 修改列表中的元素 在列表中添加元素 append() 在列表中插入元素 insert() 在列表中删除元素 del,pop() 根据值删除列表中的元素 remove() 对列表中的元素进行排列 1)永久性修改 sort(),按字母表正向排列 2)永久性修改 sort(reverse=True),按字母表反向排列 3)临时修改 sorted(),按字母表正向排列 对列表中的元素进行反转打印 reverse() 计算列表长度 len()

Python编程入门到实践 - 笔记( 5 章)

第 5 章练习了以下内容 简单的 if 判断语句 判断字符串是否相等,还是不等 进行数字的大小比较 and,or 比较 检查列表中是否存在指定的元素 if,if-else,if-elif-else 语句写法 if 判断列表是否为空 使用多个列表进行比较判断 这一章的内容也比较简单,感觉和 shell 差不多,但还是多练习吧. 希望路过的大牛指出不足,小弟在此谢过了. 一个简单的 if 判断语句 循环打印 cars 列表中的元素,如果其中的元素等于 bmw,就全部大写打印 否则只是将元素的首字母大

Python编程入门到实践 - 笔记( 8 章)

第 8 章主要练习了各种函数,内容如下 定义一个简单的函数 向函数传递信息 什么是形参 什么是实参 位置参数 多次调用函数 关键字实参 默认值参数 返回值 return 让参数编程可选的 返回字典 结合使用函数和 while 循环 传递列表 在函数中修改列表 传递任意数量的实参 传递任意数量的参数并循环打印 结合使用位置参数和任意数量实参 使用任意数量的关键字实参 导入整个模块 导入特定的函数 使用 as 给函数指定别名 使用 as 给模块指定别名 导入模块中所有的函数 定义一个简单的函数 直接

Python编程入门到实践 - 笔记( 9 章)

第 9 章主要讲的类,这个之前在 shell 中没遇到过 一直运用的也不是很溜,不过多敲多练,应该会有进步吧 创建类和使用类 创建一个 Dog 类 --------------------------------------------------------------------- class Dog():         def __init__(self, name, age):               self.name = name               self.age

Python编程入门到实践 - 笔记( 6 章)

第 6 章主要练习了各种字典,以下内容 什么是字典 字典中 键-值 的关系 一个简单的字典 通过字典中的键查找其对应的值 在字典中添加 键-值 修改字典中的值 遍历字典中的键值对 items( ) 遍历字典中的键 keys( ) 遍历字典中的值 value( ) 遍历字典中的值并且去重复 set( ) 列表中嵌套字典 通过 for 循环将字典添加到同一个列表中 在字典中存储列表并打印 什么是字典? 我自己来个不成熟的总结吧:就是一个高级列表,为啥说是高级列表,因为列表中的元素是单一的,没有属性