Shader 是更通用的概念,用于描述图形渲染程序中的着色器程序;而 Unity Shader 是特指在 Unity 中使用的着色器,可以认为 Unity Shader 是对 Shader 的一种封装。
Unity Shader 对底层图形渲染技术进行封装,并提供名为 ShaderLab 的语言,使得编写和管理着色器更加轻松。之后提到 Shader 时,主要指的就是 Unity Shader。
如果想要在 Unity 中体现 Shader 的渲染效果,必须配合使用材质(Material)和 Shader(Unity Shader)才能达到目标。一般的使用流程是:
也就是说,Unity 中的 Shader 必须配合材质才能正常使用。
在 Project 窗口中右键创建材质:Create ->
Material。
Unity Shader 中提供了对应的可编辑属性,能够直接在材质中进行编辑,而不需要去修改 Shader 代码来达到不同效果。
我们可以在 Project 窗口中右键创建 Shader:Create ->
Shader。
Standard Surface Shader(标准曲面着色器)
包含标准光照模型的表面着色器模板。
Unlit Shader
不包含光照的基本顶点/片元着色器。
Image Effect Shader
用于实现屏幕后处理效果的基本模板。
Compute Shader
利用 GPU 并行计算一些和常规渲染流水线无关的内容。
Ray Tracing Shader
用于实现光线追踪效果的着色器。
之后的学习重点主要是顶点/片元着色器,因此重点学习 Unlit Shader 着色器的编写。
ShaderLab 是 Unity 自定义的语法规则,是专门用于在 Untiy 中编写和管理着色器的语言。
它提供一种结构化的方式来描述 Unity 着色器的各个部分,从而更轻松地创建和管理着色器。
Unity 通过 ShaderLab 语言对 Shader 进行包装和组织。
ShaderLab 主要由 4 个部分组成:
// 第一部分
Shader "着色器名字"
{
// 第二部分
Properties
{
// 材质面板上可以看到的属性
}
// 第三部分
SubShader
{
// 顶点-片段着色器 / 表面着色器 / 固定函数着色器
}
SubShader
{
// 更加精简的版本
// 目的是适配旧设备
}
..... 可以有 n 个 SubShader 代码块
// 第四部分
Fallback "备用的Shader"
}
为了增加 Shader 的可调节性,某些变量不会直接在 Shader 程序中写死,而是作为开放的属性显示在材质面板上,供使用时调节。这些开放的属性通过属性来定义。
Shader 的属性具有两个特点:
属性声明在 Shader 文件中
Shader 属性存在于 Shader 语句块中的 Properties 属性语句块,需要在 Properties 语句块中按照语法规则声明属性。
Unity Shader 的属性主要分成三大类
属性的基本语法
_Name("Display Name", type) = defaultValue[{options}]
数值类型有三种:
整形
_Name("Display Name", Int) = number
浮点型
_Name("Display Name", Float) = number
范围浮点型
_Name("Display Name", Range(min,max)) = number
注意:
Unity Shader 中的数值类型属性基本都是浮点型(Float)数据。虽然提供了整数(Int),但是编译时最终都会转换为浮点型,因此更多使用的还是 Float 类型。
颜色和向量类型属性之所以归纳在一起,是因为都可以由一个四个数组成的类型表示。
颜色
_Name("Display Name", Color) = (number1, number2, number3, number4)
注意:颜色值中的 R、G、B、A 的取值范围是 0 ~ 1 (映射 0 ~ 255)。
向量
_Name("Display Name", Vector) = (number1, number2, number3, number4)
注意:向量值中的 X、Y、Z、W 的取值范围没有限制。
纹理贴图类型有四种
2D 纹理
最常用的纹理,漫反射贴图、法线贴图都属于 2D 纹理。
_Name("Display Name", 2D) = "defaulttexture"{}
2D Array 纹理
纹理数组,允许在纹理中存储多层图像数据,每层看做一个 2D 图像。一般使用脚本创建,较少使用,了解即可。
_Name("Display Name", 2DArray) = "defaulttexture"{}
Cube map texture 纹理
立方体纹理,由前后左右上下 6 张有联系的 2D 贴图拼成的立方体,比如天空盒和反射探针。
_Name("Display Name", Cube) = "defaulttexture"{}
3D 纹理
一般使用脚本创建,极少使用,了解即可。
_Name("Display Name", 3D) = "defaulttexture"{}
注意:
关于 defaulttexture 默认值取值
关于默认值后面的 {}
为固定写法。老版本中括号内可以控制固定函数纹理坐标的生成,但是新版本中没有该功能了。
每个 Shader 中都会包含至少一个 SubShader,当 Unity 显示物体时,会在 Shader 文件中去检测 SubShader 语句块,选择第一个能够在当前显卡运行的 SubShader 执行。
因此在 Shader 当中实现高级效果时,为了避免在在某些设备上无法执行,可能会存在多个 SubShader 语句块,用于适配这些低端设备。
SubShader 当中包含最终的渲染相关代码,决定了最终的渲染效果。
SubShader 语句块中主要由 3 部分构成:
渲染标签
通过标签来确定什么时候以及如何对物体进行渲染。
渲染状态
通过状态来确定渲染时的剔除方式、深度测试方式、混合方式等等内容。
渲染通道
具体实现着色器代码的地方(每个 SubShader 语句块中至少有一个渲染通道,可以有多个)。
// 第三部分
SubShader
{
// 1.渲染标签 Tags
Tags{ "标签名1" = "标签值1" "标签名2" = "标签值2" .....}
// 2.渲染状态 States
.....
// 3.渲染通道 Pass
Pass
{
// 第一个渲染通道
}
Pass
{
// 第二个渲染通道
}
.............
}
注意:
渲染标签通过键值对的形式进行声明,并且没有数量限制,可以使用任意多个标签。
主要作用:确定物体的渲染顺序。
Tags{ "Queue" = "标签值" }
常用 Unity 预先定义好的渲染队列标签值:
Background (队列号:1000)
最早被渲染的物体的队列,一般用来渲染天空盒或者背景。
Tags{ "Queue" = "Background" }
Geometry (队列号:2000)
不透明的几何体通常使用该队列,当没有声明渲染队列时,Unity 会默认使用这个队列。
Tags{ "Queue" = "Geometry" }
AlphaTest (队列号:2450)
有透明通道的,需要进行 Alpha 测试的几何体会使用该队列。
当所有 Geometry 队列实体绘制完后再绘制 AlphaTest 队列,效率更高。
Tags{ "Queue" = "AlphaTest" }
Transparent (队列号:3000)
该队列中几何体按照由远到近的顺序进行绘制,半透明物体的渲染队列,所有进行透明混合的几何体都应该使用该队列。比如:玻璃材质,粒子特效等。
Tags{ "Queue" = "Transparent" }
Overlay (队列号:4000)
用是放在最后渲染的队列,于叠加渲染的效果,比如镜头光晕等。
Tags{ "Queue" = "Overlay" }
自定义队列
基于 Unity 预先定义好的这些渲染队列标签来进行加减运算(加减号两旁不允许空格)来定义自己的渲染队列。比如:
Tags{ "Queue" = "Geometry+1" }
,代表的队列号就是 2001。
Tags{ "Queue" = "Transparent-1" }
,代表的队列号就是 2999。
自定义队列在一些特殊情况下,特别有用。比如,水的渲染想要在不透明物体之后,半透明物体之前进行渲染,就可以自定义。
注意:
自定义队列只能基于预先定义好的各类型进行计算,不能在 Shader 中直接赋值数字。如果实在想要直接赋值数字,可以在材质面板中进行设置。
主要作用:对着色器进行分类,之后可以用于着色器替换功能。
摄像机上有对应的 API,可以指定这个渲染类型来替换成别的着色器。
Tags{ "RenderType" = "标签值" }
常用 Unity 预先定义好的渲染类型标签值:
渲染类型 | 用处 | 说明 |
---|---|---|
Opaque (不透明的) | 用于普通 Shader,比如:不透明、自发光、反射等 | |
Transparent (透明的) | 用于半透明 Shader,比如:透明、粒子 | |
TransparentCutout (透明切割) | 用于透明测试 Shader,比如:植物叶子 | |
Background (背景) | 用于天空盒 Shader | |
Overlay (覆盖) | 用于 GUI 纹理、Halo(光环)、Flare(光晕) | |
TreeOpaque | 用于地形系统中的树干 | 不常用 |
TreeTransparentCutout | 用于地形系统中的树叶 | 不常用 |
TreeBillboard | 用于地形系统中的 Billboarded 树 | 不常用 |
Grass | 用于地形系统中的草 | 不常用 |
GrassBillboard | 用于地形系统中的 Billboarded 草 | 不常用 |
主要作用:当使用批处理时,模型会被变换到世界空间中,模型空间会被丢弃,这可能会导致某些使用模型空间顶点数据的 Shader 最终无法实现想要的结果。可以通过开启禁用批处理来解决该问题(了解即可)。
总是禁用批处理
Tags{ "DisableBatching" = "True" }
不禁用批处理(默认值)
Tags{ "DisableBatching" = "False" }
LOD 效果激活时才会禁用批处理,主要用于地形系统上的树
Tags{ "DisableBatching" = "LODFading" }
主要作用:控制该 SubShader 的物体是否会投射阴影。
不投射阴影
Tags{ "ForceNoShadowCasting" = "True" }
投射阴影(默认值)
Tags{ "ForceNoShadowCasting" = "False" }
主要作用:物体是否受到 Projector(投影机)的投射。
Projector 是 Unity 中的一个功能。
忽略 Projector(一般半透明 Shader 需要开启该标签)
Tags{ "IgnoreProjector" = "True" }
不忽略 Projector(默认值)
Tags{ "IgnoreProjector" = "False" }
是否用于精灵
想要将该 SubShader 用于 Sprite 时,将该标签设置为 False。
Tags{ "CanUseSpriteAtlas" = "False" }
预览类型
材质在预览窗口默认为球形,如果想要改变为平面或天空盒,只需要改变预览标签即可。
平面
Tags{ "PreviewType" = "Panel" }
天空盒
Tags{ "PreviewType" = "SkyBox" }
以上这些标签只能在 SubShader 语句块中声明,Pass 渲染通道语句块中也可以声明渲染标签。
但是这节内容的标签都不能在 Pass 中声明,Pass 中有专门的标签类型。
渲染状态通过 渲染状态关键词 + 空格 + 状态类型
决定。如果存在多个渲染状态,可以通过空行隔开。
主要作用:设置多边形的剔除方式,有背面剔除、正面剔除、不剔除。
剔除,即不渲染,背面剔除即背面不渲染,正面剔除即正面不渲染,不剔除即都渲染。
Cull Back:背面剔除。
Cull Front:正面剔除。
Cull Of:不剔除。
不设置的话,默认为背面剔除。一般情况下,需要两面渲染时,会设置为不剔除。
主要作用:是否写入深度缓冲。
深度缓冲(Depth Buffer):深度缓冲是与屏幕像素对应的缓冲区,用于存储每个像素的深度值(距离相机的距离)。在渲染场景前,深度缓冲被初始化为最大深度值,表示所有像素都在相机之外。最后留在深度缓冲中的信息会被渲染。
不设置的话,默认为写入。一般情况下,在做透明等特殊效果时,会设置为不写入。
主要作用:设置深度测试的对比方式。
深度测试的主要目的是确保在渲染时,像素按照正确的深度(距离相机的距离)顺序进行绘制,从而创建正确的遮挡关系和透视效果。在渲染场景之前,深度缓冲被初始化为最大深度值,表示所有像素都在相机之外。在渲染过程中,对于每个像素,深度测试会将当前像素的深度值与深度缓冲中对应位置的值进行比较。
一般情况下 :
不设置的话,默认为 LEqual 小于等于。一般情况下,只有在实现一些特殊效果时才会区修改深度测试方式,比如透明物体渲染会修改为 Less,描边效果会修改为 Greater 等。
主要作用:设置渲染图像的混合方式(多种颜色叠加混合,比如透明、半透明效果和遮挡的物体进行颜色混合)。
不设置的话,默认不会进行混合。一般情况下,需要多种颜色叠加渲染时,需要设置混合方式,具体情况具体处理。
目前主要掌握剔除方式、深度缓冲、深度测试、混合方式即可。
以上这些状态不仅可以在 SubShader 语句块中声明,Pass 渲染通道语句块中也可以声明这些渲染状态。
如果在 SubShader 语句块中使用,会影响之后的所有渲染通道 Pass。如果在 Pass 语句块中使用只会影响当前 Pass 渲染通道,不会影响其他的 Pass。
Pass{
1. Name 名称
2. 渲染标签
3. 渲染状态
4. 其他着色器代码
}
主要作用:对 Pass 命名的主要目的是利用 UsePass 命令在其他 Shader 中复用该 Pass。只需要在其他 Shader 当中使用
UsePass "Shader路径/Pass名"
注意:
Pass{
Name MyPass
}
在其他 Shader 中复用该 Pass 代码时,使用命令:UsePass "TeachShader/Lesson4/MYPASS"
。
Pass 中的渲染标签语法和 SubShader 中相同
Tags{ "标签名1" = "标签值1" "标签名2" = "标签值2" "标签名2" = "标签值2" .......}
但是 SubShader 语句块中的渲染标签不能够在 Pass 中使用,Pass 当中有自己专门的渲染标签。
Tags{ "LightMode" = "标签值" }
主要作用:指定了该 Pass 应该在哪个阶段执行。
可以将着色器代码分配给适当的渲染阶段,以实现所需的效果。
标签值 | 说明 |
---|---|
Always | 始终渲染;不应用光照 |
ForwardBase | 在前向渲染中使用;应用环境光、主方向光、顶点 / SH 光源和光照贴图 |
ForwardAdd | 在前向渲染中使用;应用附加的每像素光源(每个光源有一个通道) |
Deferred | 在延迟渲染中使用;渲染 G 缓冲区 |
ShadowCaster | 将对象深度渲染到阴影贴图或深度纹理中 |
MotionVectors | 用于计算每对象运动矢量 |
PrepassBase | 在旧版延迟光照中使用;渲染法线和镜面反射指数 |
PrepassFinal | 在旧版延迟光照中使用;通过组合纹理、光照和反光来渲染最终颜色 |
Vertex | 当对象不进行光照贴图时在旧版顶点光照渲染中使用;应用所有顶点光源 |
VertexLMRGBM | 对象不进行光照贴图时在旧版顶点光照渲染中使用;在光照贴图为 RGBM 编码的平台上(PC 和游戏主机) |
VertexLM | 当对象不进行光照贴图时在旧版顶点光照渲染中使用;在光照贴图为双 LDR 编码的平台上(移动平台) |
关于向前渲染、延迟渲染、旧版光照等概念了解:https://docs.unity.cn/cn/2019.4/Manual/RenderingPaths.html。
Tags{ "RequireOptions" = "标签值" }
主要作用:用于指定当满足某些条件时才渲染该 Pass。
目前 Unity 仅支持:
Tags{ "RequireOptions" = "SoftVegetation" }
仅当 Quality 窗口中开启了 SoftVegetation 时才渲染此通道。
Tags{ "PassFlags" = "标签值" }
主要作用:一个渲染通道 Pass 可指示一些标志来更改渲染管线向 Pass 传递数据的方式。
目前 Unity 仅支持:
Tags{ "PassFlags" = "OnlyDirectional" }
在 ForwardBase 向前渲染的通道类型中使用时,此标志的作用是仅允许主方向光和环境光/光照探针数据传递到着色器。这意味着非重要光源的数据将不会传递到顶点光源或球谐函数着色器变量。
5.3 节中 SubShader 语句块的渲染状态同样适用于 Pass,比如:
这些渲染状态都可以在单个 Pass 中进行设置,需要注意:
其他代码部分是实现着色器的核心代码,会用到 CG 或 HLSL 等着色器语言来进行逻辑书写。
可以利用 GrabPass 命令把即将绘制对象时的屏幕内容抓取到纹理中,在后续通道中可使用此纹理,从而执行基于图像的高级效果。
举例:
将绘制该对象之前的屏幕抓取到 _BackgroundTexture 中:
GrabPass
{
"_BackgroundTexture"
}
注意:
该命令一般写在某个 Pass 前,在之后的 Pass 代码中可以利用 _BackgroundTexture 变量进行处理。
复习:ShaderLab 当中允许有多个 SubShader 子着色器,当执行渲染时,从上到下使用第一个能够正常执行的 SubShader 子着色器来渲染对象。
备用 Shader 主要作用是当 Shader 文件中的所有 SubShader 子着色器都无法正常运行时,让物体能够使用一个最低级的 Shader 渲染出来(效果略差,但至少能够显示)。
Fallback "Shader名"
或者
Fallback Off
在 Fallback 关键词后面空格并通过一个字符串来告诉 Unity “最低级的 Unity Shader” 在哪,也可以直接关闭 Fallback 功能,但这意味着“放弃治疗”。
Shader 最主要的作用是指定各种着色器所需的代码。这些着色器代码既可以放在 SubShader 子着色器语句块中,也可以放在其中的 Pass 渲染通道语句块中,不同的 Shader 形式放置着色器代码的位置也有所不同。
我们一般会使用以下 3 种形式来编写 Unity Shader:
表面着色器(Surface Shader)是 Unity 创造的一种着色器代码类型,本质是对顶点/片元着色器的一层封装。它需要的代码量很少,很多工作由 Unity 帮助完成。其缺点是渲染的消耗较大,可控性较低;优点在于,帮助我们处理很多光照细节,可以直接使用而无需自己计算实现光照细节。
创建 Shader 时,选择创建 Standard Surface Shader。通过观察 Shader 文件的内部结构,发现着色器相关代码被放在 SubShader 语句块中(并非 Pass)的 CGPROGRAM 和 ENDCG 之间。
表面着色器的特点:
在创建 Shader 时,选择创建 Unlit Shader 来快速创建顶点/片元着色器模板。
通过观察,发现顶点/片元着色器的着色器代码编写在 Pass 语句块中,需要自己定义每个 Pass 需要使用的 Shader 代码。
虽然比起表面着色器来说我们需要编写的代码较多,但是好处是灵活性更高,可控性更强,可以控制更多的渲染细节,决定对性能影响的高低。
顶点/片元着色器的特点:
表面着色器、顶点/片元着色器这两种 Unity Shader 形式都使用了可编程管线。
而对于一些老设备(DX7.0、OpenGL1.5 或 OpenGL ES 1.1),它们不支持可编程管线着色器,这时就需要使用固定函数着色器来进行渲染,这些着色器只能实现一些非常简单的效果。
固定函数着色器的特点:
但是由于这些旧设备目前市面上几乎已经不存在,所以固定函数着色器几乎不会再使用,了解即可。
即使现在在 Unity 中使用固定函数着色器来编写 Shader,在内部也会被编译为顶点/片元着色器,因此真正意义的固定函数着色器已经不存在了。