一、ShaderLab 语法
(一)材质与 Shader
1、Unity Shader 与 Shader 的区别
Unity Shader 和 Shader 的概念有很大区别,在 Unity 里,Unity Shader 实际上指的就是一个 ShaderLab 文件,以 .shader作为文件后缀。
Shader 是一个更通用的概念,用于描述图形渲染程序中的着色器程序,而 Unity Shader 特指在 Unity 中使用的着色器。可以简单认为 Unity Shader 是对 Shader 的一种封装,它对底层图形渲染技术进行封装,并提供了一种叫做 ShaderLab 的语言,来让我们更加轻松的编写和管理着色器。后续提到 Shader 这个词时,主要指的就是 Unity Shader。

2、材质与 Shader
如果我们想要在 Unity 中体现出一个 Shader 的渲染效果,必须配合使用材质(Material)和 Unity Shader。
一般的使用流程是:
- 创建一个材质。
- 创建一个 Unity Sahder,把该 Shader 赋给上一步中创建的材质。
- 将材质赋予给想要渲染的对象。
- 在材质面板中调整 Unity Shader 的相关属性,以达到最终效果。
有了材质和 Shader 后,将材质设置为使用对应的着色器,并设置着色器提供的可编辑的相关属性,我们就可以渲染出我们想要的各种效果了。

3、材质
材质决定了我们的游戏对象看起来是什么样子的(当然需要配合 Unity Shader),创建好材质之后,一般拖给对象上的 Renderer 组件即可赋予其材质。
在 Project 窗口右键,然后点击 Create —> Material 即可创建材质。材质默认会有一个内置的 Shader,然后关于材质,它的 Inspector 面板参数分为两部分:
- 上方的 Shader 选项中可以选择对应的着色器。
- 下方的内容就是对应 Shader 提供的可编辑的相关变量,他们会直接影响渲染结果。

4、Unity Shader
Unity Shader 定义了渲染所需的各种代码(比如顶点着色器和片元着色器)、属性和指令,这样材质就可以调用这些属性,并最终赋给相应的模型。
在 Project 窗口右键,然后点击 Create —> Shader 即可创建 Unity Shader。这里一共有五种 Shader 模板可以选择:
- Standard Surface Shader:标准曲面着色器,包含标准光照模型的表面着色器模板。
- Unlit Shader:不包含光照的基本顶点/片元着色器。
- Image Effect Shader:用于实现屏幕后处理效果的基本模板。
- Compute Shader:利用GPU并行计算一些和常规渲染流水线无关的内容。
- Ray Tracing Shader:用于实现光线追踪效果的着色器。
我们学习重点主要是顶点/片元着色器,因此我们会更多的去学习 Unlit Shader 着色器的编写。接下来简单认识一下 Shader 的 Inspector 窗口,以 Standard Surface Shader 为例:
- Default Maps:默认纹理,第一次使用该 Shader 时默认使用的纹理。
- Imported Object:导入的对象,可以查看和编辑与着色器对象本身相关的设置。
- Surface Shader:表面着色器,如果有
Show generated code按钮,点击后可以查看对应代码。 - Fixed function:是否使用固定管线进行渲染(非常老的版本使用的方式)。
- Preprocess only:是否开启预处理阶段执行特定代码标记。
- Strip #line directives:开启后,编译器会在生成的代码中删除所有的 “#line” 指令,确保调试和错误定位的准确性。
- Compile and show code:编译并显示代码,可以查看不同平台的不同图形接口程序时编译出来的代码,可以帮助我们优化着色器性能。
- Cast shadows:是否投射阴影。
- Render queue:该Shader的渲染队列,决定渲染顺序。
- LOD:LOD级别。
- Ignore projector:是否忽略投影器。
- Disable batching:是否禁用批处理。
- Keywords:一种用于在渲染过程中启用或禁用特定功能的设置,通过在 Shader 中定义关键字,你可以在运行时动态地切换不同的渲染通道、特效或选项,以实现多样化的视觉效果。
- Properties:在 Unity Shader 中用于定义在材质面板中显示的可编辑属性。
- Surface Shader:表面着色器,如果有

(二)ShaderLab 基本结构
ShaderLab 主要由4个部分组成:
- ShaderName:Shader的名字。
- Properties:Shader的属性。
- SubShader:1 到 n 个子着色器。
- Fallback:备用着色器。
它的结构如下:
// 1、Shader 的名字
Shader "ShaderName"
{
// 2、属性
Properties
{
// Shader 的属性,即材质面板上可以看到的属性
}
// 3、子着色器
SubShader
{
// 此处是定义子着色器的代码,比如顶点着色器或表面着色器等。
}
SubShader
{
// 子着色器可以有多个,若发现前一个子着色器不可用,则会往后寻找可用的 SubShader
// 多个的目的是为了适配不同设备
}
// 4、备用着色器
// 若前面的 SubShader 都不支持,则会使用备用 Shader
Fallback "ExampleFallbackShader"
}
(三)Shader 命名
Shader 的命名很简单,上面已经讲过了,Shader 的名字决定了在材质面板的选择路径。
Shader "Examples/ShaderSyntax"
注意事项:
- Shader 名不可用中文命名。
- Shader 名最好和文件名保持一致。
(四)Shader 属性
1、属性
在 Shader 编写时我们经常会用到不同类型的变量或贴图等资源,为了增加 Shader 的可调节性,有些变量不会直接在 Shader 程序中写死,而是作为开放的属性显示在我们的材质面板上,这些属性就是通过 Properties 来定义的。Shader 的属性具有两个特点:
- 可以在材质面板被编辑。
- 可以在后续当作输入变量提供给所有子着色器使用。
2、属性的基础语法
Shader 属性位于 Shader 语句块中的 Properties 属性语句块,其基本语法如下:
- _Name:属性名字,规则是需要在前面加一个下划线。
- Display Name:在材质 Inspector 面板上显示的名字。
- type:属性的类型。
- defaultValue:默认值。
_Name("Display Name", type) = defaultValue[{options}]
Unity Shader 的属性主要分成三大类:数值、颜色和向量、纹理贴图。
(1)数值类型属性
包括整型、浮点型、范围浮点型。
注:Unity Shader 中的数值类型属性基本都是浮点型数据,虽然提供了整数 Int,但是编译时最终都会转换为浮点型
因此我们更多的使用的还是 Float 类型。
Properties
{
// 数值类型
_Int("Int", Int) = 1
_Float("Float", Float) = 0.5
_Range("Range", Range(0.0, 5.0)) = 3.0
}
(2)颜色和向量类型属性
包括颜色、向量。
- 颜色由 RGBA 四个分量代表,其中颜色值中的 RGBA 取值范围是0到1(映射0到255)。
- 向量由 XYZW 四个分量代表,范围没有限制。
Properties
{
// 颜色与向量
_Color("Color", Color) = (0.5, 0.5, 0.5, 1)
_Vector("Vector", Vector) = (2, 3, 6, 1)
}
(3)纹理贴图类型属性
包括2D纹理、2DArray纹理、Cube纹理、3D纹理。
- 2D纹理:最常用的纹理,漫反射贴图、法线贴图都属于2D纹理。
- 2DArray纹理:纹理数组,允许在纹理中存储多层图像数据,每层看做一个2D图像,一般使用脚本创建,较少使用。
- Cube纹理:立方体纹理,由前后左右上下6张有联系的2D贴图拼成的立方体,比如天空盒和反射探针。
- 3D纹理:一般使用脚本创建,极少使用,了解即可。
补充:还有一个 CubeArray,即立方体纹理贴图数组,这里不阐述了,想了解可以去官方文档查看。
纹理类型的基本格式如下:
- 关于默认取值
defaultTexture:"":默认贴图为空。"white":默认白色贴图(RGBA:1,1,1,1)。"black":默认黑色贴图(RGBA:0,0,0,1)。"gray":默认灰色贴图(RGBA:0.5,0.5,0.5,1)。"bump":默认凸贴图(RGBA:0.5,0.5,1,1),一般用于法线贴图默认贴图。"red":默认红色贴图(RGBA:1,0,0,1)。
- 关于默认值后面的
{}:是固定写法,老版本中括号内可以控制固定函数纹理坐标的生成,但是新版本中没有该功能了。
_Name("Display Name", textureType) = "defaultTexture"{}
各个纹理的属性编写如下:
Properties
{
// 纹理贴图
_2D("2D", 2D) = "black"{}
_2DArray("2DArray", 2DArray) = ""{}
_Cube("Cube", Cube) = ""{}
_3D("3D", 3D) = ""{}
}
3、在材质面板上的显示
上面编写的所有类型属性在材质面板上的显示如下图所示:

(五)Shader 子着色器——概念
1、SubShader 语句块
每一个 Shader 中都会包含至少一个 SubShader,当Unity想要显示一个物体的时候,就会在 Shader 文件中去检测这些 SubShader 语句块,然后选择第一个能够在当前显卡运行的 SubShader 执行。因此有时一个 Shader 会有多个 SubShader 语句块,去适配这些低端设备。
SubShader 中包含最终的渲染相关代码,决定了最终的渲染效果。
2、SubShader 基本构成
一个 SubShader 包含三部分:
- 渲染标签 Tags:通过标签来确定什么时候以及如何对物体进行渲染。
- 渲染状态 States:通过状态来确定渲染时的剔除方式、深度测试方式、混合方式等内容。
- 渲染通道 Pass:具体实现着色器代码的地方,每个 SubShader 语句块中至少有一个渲染通道,可以有多个。
注意:在 SubShader 中每定义一个渲染通道,就会让物体执行一次渲染,因此我们需要尽量减少 Pass 的数量,因为更多的 Pass 会增加性能消耗。
SubShader
{
// 渲染标签(可选)
[Tags]
// 渲染状态(可选)
[RenderSetup]
// 渲染通道
Pass
{
}
// 其他更多的 Passes...
}
(六)Shader 子着色器——渲染标签
1、Tags 渲染标签
渲染标签可以用来告诉 Unity 我们希望如何以及何时渲染这个对象。
渲染标签都是以键值对的形式出现的配置,键和值都是字符串类型,并且没有数量限制,可以使用任意多个标签,用空格或换行进行分割。其基本结构如下:
Tags{ "TagName1" = "Value1" "TagName2" = "Value2" }
标签类型有很多种,接下来依次进行讲解。
注:之后讲解的 Pass 渲染通道语句块中也可以声明渲染标签,但本节的渲染标签都不能在 Pass 中声明,因为 Pass 有自己专门的标签类型。
2、Queue 渲染队列
该标签用于确定物体的渲染顺序,Unity预先定义好的渲染队列标签值如下:
- Background:背景(队列号:1000),最早被渲染的物体的队列,一般用来渲染天空盒或者背景。
- Geometry:几何(队列号:2000),不透明的几何体通常使用该队列,没有声明渲染队列时,Unity会默认使用这个队列。
- AlphaTest:透明测试(队列号:2450),有透明通道的,需要进行 Alpha 测试的几何体会使用该队列。当所有 Geometry 队列实体绘制完后再绘制 AlphaTest 队列,效率更高。
- Transparent:透明的(队列号:3000),该队列中几何体按照由远到近的顺序进行绘制,半透明物体的渲染队列、
所有进行透明混合的几何体都应该使用该队列,比如:玻璃材质,粒子特效等。 - Overlay:覆盖(队列号:4000),放在最后渲染的队列,常用于叠加渲染的效果,比如镜头光晕等。
- 自定义队列:基于 Unity 预先定义好的渲染队列标签来进行加减运算,从而定义自己的渲染队列。自定义队列在一些特殊情况下特别有用,比如一些水的渲染,想要在不透明物体之后,半透明物体之前进行渲染,就可以自定义该队列。
Tags{ "Queue" = "Geometry+1" } // 代表的队列号是 2001
渲染队列在材质面板中的位置如下图所示,需要注意的是,自定义队列只能基于预先定义好的类型进行计算,不能直接赋值数字(除非在材质面板中进行设置)。

补充:天空盒渲染
虽然天空盒的渲染队列值更小,意味着会更先渲染。但是实际上天空盒的渲染是在一个特殊的阶段完成的,而不是严格按照渲染队列的顺序执行,它会晚于不透明物体渲染后被渲染。
Unity 采用的一种优化策略,确保不透明物体(如地形和建筑)先被渲染。由于这些物体通常会覆盖大部分场景,Unity 通过延迟天空盒的渲染避免不必要的计算,确保只有那些没有被不透明物体覆盖的像素才会最终显示天空盒。
天空盒 Shader 通常是关闭了深度写入的。这意味着天空盒不会对深度缓冲区进行任何写入操作。这样做的原因是,天空盒本质上是渲染在场景的背景中,不需要参与深度测试。
3、RenderType 渲染类型
该标签可覆盖 Shader 对象的行为,主要用于运行时的着色器替换功能。其标签值如下(前面5个常用):
- Opaque:不透明的,用于普通 Shader,比如:不透明、自发光、反射。
- Transparent:透明的,用于半透明 Shader,比如:透明、粒子。
- TransparentCutout:透明切割,用于透明测试 Shader,比如:植物叶子。
- Background:背景,用于天空盒Shader。
- Overlay:覆盖,用于GUI纹理、Halo(光环)、Flare(光晕)。
- TreeOpaque:用于地形系统中的树干。
- TreeTransparentCutout:用于地形系统中的树叶。
- TreeBillboard:用于地形系统中的公告牌(Billboarded)树。
- Grass:用于地形系统中的草。
- GrassBillboard:用于地形系统中的公告牌草。
4、DisableBatching 禁用批处理
当使用批处理时,模型会被变换到世界空间中,模型空间会被丢弃,这可能会导致某些使用模型空间顶点数据的 Shader 最终无法实现想要的结果,可以通过开启禁用批处理来解决该问题。也就是说,只有直接使用了模型空间顶点数据的特殊的 Shader 才会禁用批处理,才会使用该标签。
该标签有3种值,默认为 False,即不禁用批处理,所有值如下:
- True:禁用批处理。
- False:不禁用批处理。
- LODFading:LOD效果激活时才会禁用批处理。主要用于地形系统上的树,了解即可。
5、ForceNoShadowCasting 禁止阴影投影
该标签用于控制该 SubShader 的物体是否会投射阴影。默认值为 False,即投射阴影,所有值如下:
- True:不投射阴影。
- False:投射阴影。
6、IgnoreProjector 忽略投影机
该标签用于决定物体是否受到 Projector(投影机)的投射。Projector 是 Unity 中的一种特殊的光源,它用于在场景中投射纹理(Texture)或简单的几何形状(如圆形 或方形)来模拟光照、阴影或其他视觉效果,这个以后会提到。
默认值为 False,即不忽略投影机,所有值如下:
- True:忽略投影机,一般半透明 Shader 需要开启该标签。
- False:不忽略投影机。
7、CanUseSpriteAtlas 是否用于精灵
想要将该 SubShader 用于 Sprite 时,将该标签设置为 False。
它的默认值为True,同样只有 True 和 False 两种取值,用的比较少。
8、PreviewType 预览类型
该标签用于决定材质在预览窗口的类型。默认为球体,如果想要改变为平面或天空盒,只需要改成如下两个值:
- Panel:平面。
- SkyBox:天空盒。
(七)Shader 子着色器——渲染状态
1、States 渲染状态
渲染状态可以影响最终我们看到的渲染效果。
比如剔除方式决定了模型正面背面是否能够被渲染、深度缓冲和深度测试决定了景深关系以及透明效果的正确表达等、混合方式决定了透明半透明颜色的正确表现等等。
多个渲染状态之间可以通过换行隔开,其基本结构如下:
渲染状态 状态类型
渲染状态也有很多种,接下来依次进行讲解。
以下渲染状态不仅可以在 SubShader 语句块中声明,也可以在 Pass 语句块中声明。如果在 SubShader 语句块中使用,会影响之后的所有 Pass 渲染通道;如果在 Pass 语句块中使用则只会影响当前 Pass 渲染通道。
2、Cull 剔除方式
所谓剔除,其实就是不渲染。它的作用主要是设置多边形的剔除方式。它的状态类型如下:
- Cull Back:背面剔除(默认)。
- Cull Front:正面剔除。
- Cull Off:不剔除。
这里的正背面我以立方体为例进行说明:立方体表面就是正面,立方体内部就是背面。
3、补充:深度测试流程
深度测试是光栅化阶段中逐片元操作的其中一步。渲染时,场景中的物体通常都会存在一些遮挡关系,它们在屏幕中会有重叠的区域,可以简单理解为,深度测试就是用来确认不同模型所占的同一块屏幕区域,到底需要渲染哪一个模型上对应的颜色。
深度测试的流程如下图所示,我会配着图来讲解。
- 深度缓冲(Depth Buffer):深度缓冲是一个与屏幕像素对应的缓冲区,它用于存储每个像素的深度值(距离相机的距离)。
- 深度测试:如果开启了深度测试,则会依次将模型所占屏幕上的片元的深度值(Z-Buffer)与对应像素的深度缓冲的深度值比较,这个比较函数可以自行设置(比如小于时通过测试),若测试通过,则会被渲染。
- 深度写入:只有通过了深度测试才有资格选择是否进行深度写入,如果模型开启了深度写入,则该片元的深度值将被写入到深度缓冲内,与后面的片元进行比较。
深度测试的主要目的是确保在渲染时,像素按照正确的深度顺序进行绘制,从而创建正确的遮挡关系和透视效果。

4、ZWrite 深度写入
用于表示是否开启深度写入,属于深度测试流程的一部分。它的状态类型如下:
- ZWrite On:写入深度缓冲(默认)。
- ZWrite Off:不写入深度缓冲。
我们在做透明等特殊效果时,会设置为不写入,因为我们不希望前面的透明物体完全挡住后面的物体。
5、ZTest 深度测试
用于设置深度测试的对比方式,属于深度测试流程的一部分。
在渲染场景前,深度缓冲被初始化为最大深度值,表示所有像素都在相机之外。一般情况下,如果当前像素的深度值小于深度缓冲中的值,说明当前像素在其他物体之前,则它会被绘制,反之则会被丢弃。它的状态类型如下:
- ZTest Less:小于当前深度缓冲中的值,就通过测试。
- ZTest Greater:大于当前深度缓冲中的值,就通过测试。
- ZTest LEqual:小于等于当前深度缓冲中的值,就通过测试(默认)。
- ZTest GEqual:大于等于当前深度缓冲中的值,就通过测试。
- ZTest Equal:等于当前深度缓冲中的值,就通过测试。
- ZTest NotEqual:不等于当前深度缓冲中的值,就通过测试。
- ZTest Always:始终通过深度测试。
一般只有在实现特殊效果时才会修改深度测试方式,比如透明物体渲染会修改为 Less,描边效果会修改为 Greater 等。
6、补充:混合流程
混合是光栅化阶段中逐片元操作的其中一步。如果一个片元通过了前面的所有测试(模板测试、深度测试),那么接下来就会进行混合(Blending)。
混合的流程如下图所示,其实很简单:混合操作决定了这个片段的最终颜色如何根据其透明度与背后的颜色合成。
- 若开启混合:则按照一定方式将源颜色与缓冲区颜色进行混合后,更新对应的颜色缓冲区的值。
- 若不开启混合:则直接使用该片元的颜色值,即覆盖之前的结果。
混合操作通常用于实现物体的透明度效果,比如玻璃、水、烟雾等。

7、Blend 混合方式
用于设置渲染图像的混合方式,在混和流程中使用。
Blend 关键词后边填写的是两个混合因子,关于这个在后面会讲到,这里有个印象即可。默认不进行混合,常用的混合方式有这几个:
- Blend One One:线性减淡。
- Blend SrcAlpha OneMinusSrcAlpha:正常透明混合。
- Blend OneMinusDstColor One:滤色。
- Blend DstColor Zero:正片叠底。
- Blend DstColor SrcColor:X光片效果。
- Blend One OneMinusSrcAlpha:透明度混合。
8、其他渲染状态
还有很多渲染状态没有提及到,因为相比上面的这几个渲染状态来说,使用的频率比较低,这里只简单提两个:
(1)LOD
LOD(Level Of Detail)即细节层次,Unity引擎会根据不同的 LOD 值在使用不同的 SubShader。我们可以在 Pass 语句块里使用它,用于控制模型在不同距离下的渲染方式,一般越远的模型渲染细节越少。
LOD 100
(2)ColorMask
ColorMask即颜色蒙版,用于设置颜色通道的写入蒙版,默认蒙版为 RGBA。
ColorMask RGBA
(八)Shader 子着色器——渲染通道
1、Pass 渲染通道
Pass 就是具体实现着色器代码的地方(每个 SubShader 语句块中至少有一个 Pass,可以有多个)。
一个 Pass 可以由这四个部分组成:
- Name:Pass 名。
- Tags:Pass 标签,此标签和 SubShader 中的渲染标签不同,它有自己独立的渲染标签。
- RenderSetup:Pass 渲染状态,此渲染状态和 SubShader 使用的一样,只是影响区域不同。
- Code:着色器代码,我们之后会使用 CG/HLSL 语言编写。
Pass
{
// 名字
[Name]
// 标签
[Tags]
// 渲染状态
[RenderSetup]
// Other Code...
}
2、Pass 名
我们对 Pass 命名的主要目的,就是利用 UsePass 命令在其他 Shader 中复用该 Pass 的代码,关于该命令后面就会提到。Pass 命名的语法如下:
Name "MyPassName"
3、Pass 渲染标签
Pass 渲染标签的语法和 SubShader 的相同,但是所使用的标签不同,所以 Pass 不能用我们上面学习的渲染标签。它独有的标签如下:
(1)LightMode
该标签指定了该 Pass 应该在哪个阶段执行,可以将着色器代码分配给适当的渲染阶段,以实现所需的效果。
- Always:始终渲染,不应用光照。
- ForwardBase:在前向渲染中使用,应用环境光、主方向光、顶点/SH 光源和光照贴图。
- ForwardAdd:在前向渲染中使用,应用附加的每像素光源(每个光源有一个通道)。
- Deferred:在延迟渲染中使用,渲染 G 缓冲区。
- ShadowCaster:将对象深度渲染到阴影贴图或深度纹理中。
- MotionVectors:用于计算每个对象运动矢量。
- PrepassBase:在旧版延迟光照中使用,渲染法线和镜面反射指数。
- PrepassFinal:在旧版延迟光照中使用,通过组合纹理、光照和反光来渲染最终颜色。
- Vertex:当对象不进行光照贴图时,在旧版顶点光照渲染中使用,应用所有顶点光源。
- VertexLMRGBM:当对象不进行光照贴图时,在旧版顶点光照渲染中使用,在光照贴图为 RGBM 编码的平台上(PC 和游戏主机)。
- VertexLM:当对象不进行光照贴图时,在旧版顶点光照渲染中使用,在光照贴图为双 LDR 编码的平台上(移动平台)。
这里提及了很多概念,比如前向渲染、延迟渲染、旧版光照等,后面都会讲解,这里有个印象即可。
(2)RequireOptions
用于指定当满足某些条件时才渲染该 Pass。目前 Unity 只支持一个值,表示仅当 Quality 窗口中开启了 SoftVegetation时 才渲染此通道。
Tags{ "RequireOptions" = "SoftVegetation" }
(3)PassFlags
作用是可指示一些标志来更改渲染管线向 Pass 传递数据的方式。目前 Unity 只支持一个值。
在 ForwardBase 前向渲染的通道类型中使用时,此标志的作用是仅允许主方向光和环境光/光照探针数据传递到着色器。简单来说就是限制一些特殊光源数据给到着色器。
Tags{ "PassFlags" = "OnlyDirectional" }
4、Pass 渲染状态
我们之前在 SubShader 语句块中学习的渲染状态同样适用于 Pass,区别就是在 Pass 中声明的渲染状态只影响该 Pass。不仅如此,Pass 中还可以使用固定管线着色器的命令。
5、着色器代码
这部分就是实现着色器的核心代码部分,我们之后会用 CG/HLSL 等着色器语言来进行逻辑编写。
6、通道命令
在 SubShader 中使用这些命令可定义具有特定用途的渲染通道,命令有两种:
- UsePass
利用 UsePass 命令可以在其他 Shader 中复用指定 Pass 的代码,语法如下,传入 Shader 路径 / Pass 名 即可。
需要注意的就是 Unity 内部会把 Pass 名称转换为大写形式,因此在使用 UsePass 命令时必须使用大写形式的名字。
SubShader
{
UsePass "Custom/MyShader/MYPASSNAME"
}
- GrabPass
利用 GrabPass 命令把即将绘制对象时的屏幕内容抓取到纹理中(如同截屏一样),在后续通道中即可使用此纹理,从而执行基于图像的高级效果。
举个例子:将屏幕内容抓取到 _BackgroundTexture 纹理中,在之后的 Pass 中即可以利用该变量进行处理。
SubShader
{
GrabPass
{
"_BackgroundTexture"
}
}
(十)Shader 备用着色器
1、备用着色器
我们知道 ShaderLab 中允许有多个 SubShader 子着色器,执行渲染时,会从上到下使用第一个能够正常执行的 SubShader 子着色器来渲染对象。但是当所有子着色器都不能够在显卡上执行时,我们就需要有一个备用方案了,确保对象能够正常渲染出来,他就是备用着色器,可以简单理解为是 Shader 的“救命稻草”。
2、备用着色器的语法
它的语法非常简单,只需要在关键字后面填入对应的 Shader 名的路径即可。
Fallback "ShaderName"
若不使用备用着色器,则可以使用该语法:
Fallback Off
(十一)Shader 的编写形式
1、概念
前面我们已经学习完了 ShaderLab 的语法知识,对 shader 的文件结构有了一定了解。
我们知道,我们可以通过 ShaderLab 去设置很多内容,比如属性、渲染状态、渲染标签等,但是它最主要的作用是指定着色器所需的代码,这些着色器代码可以放在 SubShader 语句块中,也可以放在 Pass 语句块中,不同的 Shader 放置着色器代码的位置也有所不同,也就是它们的编写形式不同。
我们一般会用以下3种形式来编写 Unity Shader,接下来会简单的认识它们:
- 表面着色器(可控性较低)。
- 顶点/片元着色器(重点学习)。
- 固定函数着色器(基本已弃用,了解即可)。
2、表面着色器
我们创建 Shader 时选择 Standard Surface Shader 即可创建表面着色器模板。
表面着色器是 Unity 自己创造的一种着色器代码类型,它本质是对顶点/片元着色器的一层封装。它的优点在于帮助我们处理了很多光照细节,我们可以直接使用而无需自己计算实现光照细节;但它的缺点是渲染的消耗较大,可控性较低。
我们打开默认的着色器代码,可以发现着色器代码被放在 SubShader 语句块中的 CGPROGRAM 和 ENDCG 之间,没有 Pass:
Shader "Custom/MySurfaceShader"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
// 着色器代码...
ENDCG
}
FallBack "Diffuse"
}
表面着色器的特点是:
- 直接在 SubShader 语句块中编写着色器逻辑,无需关心和使用多个 Pass、每个 Pass 如何渲染,Unity会在内部帮助我们去处理。
- 可以使用 CG 或 HLSL 两种 Shader 语言去编写 Shader 逻辑。
- 代码量较少,可控性较低,性能消耗较高。
- 适用于处理需要和各种光源打交道的着色器。(主机、PC平台时更适用,移动平台需要考虑性能消耗)
3、顶点/片元着色器
我们创建 Shader 时选择 Unlit Shader 即可创建顶点/片元着色器模板。
我们打开默认的着色器代码,可以发现着色器代码是编写在 Pass 语句块中的,我们需要自己定义每个 Pass 需要使用的Shader代码:
Shader "Unlit/MyUnlitShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
// 着色器代码...
ENDCG
}
}
}
虽然比起表面着色器来说我们需要编写的代码较多,但是好处是灵活性更高,可控性更强,可以控制更多的渲染细节,可以自己决定对性能影响的高低。
顶点/片元着色器的特点是:
- 需要在Pass渲染通道中编写着色器逻辑.
- 可以使用 CG 或 HLSL 两种 Shader 语言去编写 Shader 逻辑。
- 代码量较多,灵活性较强,性能消耗更可控,可以实现更多渲染细节。
- 适用于光照处理较少,自定义渲染效果较多时。(移动平台首选)
4、固定函数着色器
表面着色器 和 顶点/片元着色器 这两种 Unity Shader 形式都使用了可编程管线,而对于一些老设备(DX7.0、OpenGL1.5 或 OpenGL ES 1.1),它们不支持可编程管线着色器,此时就需要使用固定函数着色器来进行渲染,这些着色器只能实现一些非常简单的效果。
它的特点是:
- 需要在 Pass 渲染通道中编写着色器逻辑
- 需要使用 ShaderLab 语法中的渲染设置命令来编写,而非 CG 和 HLSL 着色器语言。
由于这些旧设备目前市面上几乎已经没有了,所以固定函数着色器几乎不会再使用,只做了解即可。即使在 Unity 中使用固定函数着色器来编写 Shader,在内部也会被编译为顶点/片元着色器,因此真正意义的固定函数着色器已经不存在了。
二、CG 语法
(一)概念
1、着色器语言
着色器语言(Shader Language)是用于在 GPU 上编写着色器程序的图形编程语言,各自对应不同的图形 API 和平台,目前主要有 3 种语言:
- 基于 OpenGL 的 OpenGL Shading Language,简称 GLSL;
- 基于 DirectX 的 High-Level Shading Language,简称 HLSL;
- NVIDIA 公司的 C for Graphic,简称 Cg 语言。
那么 Unity 使用的是哪种语言呢?Unity 最初使用 Cg 语言,因此会使用 Unity 某些关键字的名称 (如 CGPROGRAM) 和文件扩展名 (如 .cginc)。虽然 Unity 现在不再使用 Cg,使用的是 HLSL,但这些名称仍在使用。
2、关于 Cg 和 HLSL
我们知道,在 Unity Shader 中,Cg 和 HLSL 代码是嵌套在 CGPROGRAM 和 ENDCG 之间的,由于 Cg 和 DX9 风格的 HLSL 从写法上来说几乎是同一种语言,因此在 Unity 中 Cg 和 HLSL 是等价的,所以一般就写在一起:Cg/HLSL。
Unity 现在推荐使用的都是 HLSL,用的是类似 HLSLPROGRAM 的关键字,而 Cg 更多是为了兼容旧管线而保留的,虽然教程里教的是 Cg 语法,但是两者之间语法层面差别不大,所以学习 Cg 语法也没有问题。
3、关于 CG
CG 的语法基于 C 语言并适配 GPU 编程模式,因此和 C 的语法高度相似。
- CG 语言的官方文档:Cg Toolkit Documentation
那么 CG 语言写在哪里呢?对于顶点/片元着色器来说,CG 语句需要写在 Pass 渲染通道语句块中,在 CGPROGRAM 和 ENDCG 之间书写 CG 代码。
4、重要的编译指令
真正书写 CG 代码之前,我们需要先使用 #pragma 声明编译指令,我们目前只学习指定顶点\片元着色器函数的编译指令,其他相关编译指令我们以后再学习。
定义实现顶点/片元着色器代码的函数名称:
#pragma vertex name(name 是实现顶点着色器的函数名,一般叫 vert)。#pragma fragment name(name 是实现片元着色器的函数名,一般叫 frag)。
这两个编译指令的作用是将顶点/片元着色器实现定位到两个函数中,我们只需要在这两个函数中书写 Shader 逻辑即可。
(二)基础变量类型
1、基础数据类型
uint:32位无符号整形。int:32位整形。float:32位浮点数,后缀符号:f。half:16位浮点数,后缀符号:h。fixed:12位浮点数,Shader 最常用的浮点数类型,不需要加后缀。bool:布尔类型。string:字符串。sampler:纹理对象句柄。下面都是用于处理纹理数据的数据类型,他们的主要区别是纹理的维度和类型。sampler: 通用的纹理采样器,可以用于处理各种不同维度和类型的纹理。sampler1D:用于一维纹理,通常用于对一维纹理进行采样,例如从左到右的渐变色。sampler2D:用于二维纹理,最常见的纹理类型之一。它用于处理二维图像纹理,例如贴图。sampler3D:用于三维纹理,通常用于体积纹理,例如体积渲染。samplerCUBE:用于立方体纹理,通常用于处理环境映射等需要立方体贴图的情况。samplerRECT:用于处理矩形纹理,通常用于一些非标准的纹理映射需求。
// 整形
uint ui = 12;
int i = -3;
// 浮点型
float f = 1.2f;
half h = 1.2h;
fixed fix = 3.2;
// 布尔类型
bool b = true;
// 字符串
string str = "123";
// 纹理对象句柄
sampler sam;
2、基础复合数据类型
- 一维数组
int a[4] = { 1, 2, 3, 4 };
- 二维数组
int b[3][3] = {{1,2,3},
{4,5,6},
{7,8,9}};
- 结构体
struct s
{
int i;
float f;
bool b;
};
(三)特殊数据类型
1、向量
向量类型属于 CG 语言的内置数据类型,内置的向量类型是基于基础数据类型声明的,向量的最大维度不超过4维,数据类型可以是任意数值类型。
基本构成:
<数据类型><维度> <变量名> = <数据类型><维度>(...);
比如下面这几个向量:
int2 i2 = int2(1,2);
float3 f3 = float3(1.2,3.2,1);
half4 h4 = half4(2,4,1,4);
bool 类型的特殊使用:bool 类型同样可以用于向量声明,它可以用于存储一些逻辑判断结果,比如下面这个
float3 a = (0.5, 0.0, 1.0); float3 b = (0.6, 0.1, 0.9); bool3 c = a < b; // 结果为 bool3(true,true,false)
2、矩阵
矩阵类型属于 CG 语言的内置数据类型,矩阵的最大行列不大于4,不小于1,数据类型可以是任意数值类型。可以简单理解为矩阵是个二维数组。
基本构成:(注意这个 x 是小写字母)
<数据类型><行数>x<列数> <变量名> = {<元素列表>};
比如下面几个矩阵:
int2x3 mInt2x3 = {1, 2, 3,
4, 5, 6};
float3x3 mFloat3x3 = {1, 2, 3,
4, 5, 6,
7, 8, 9};
(四)Swizzle 操作符
1、概念
Swizzle操作符通常以点号 . 的形式使用,后面跟着所需的分量顺序。
对于四维向量来说我们可以通过 vec.xyzw 或 vec.rgba 两种分量的写法来从左到右表示向量中的四个值。
其中这个 xyzw 和 rgba 分别代表的意义是坐标和颜色。
2、Swizzle 操作符的使用
- 提取分量:可以提取向量中的某一个分量。
fixed4 f4 = fixed4(1,2,3,4);
fixed f = f4.x; // 提取第一个分量,即数字 1
f = f4.g // 提取第二个分量,即数字 2
- 重新排列分量:可以对向量的分量进行重新排列,按照自己想要的方式调整分量位置。
fixed4 f4 = fixed4(1,2,3,4);
f4 = f4.yzxw; // 重新排列后的值为 fixed4(2,3,1,4)
- 创建新向量:可以用高维向量创建低维向量,指明需要哪些分量即可;反之低维向量也可以创建高维向量,补全相应分量即可。
fixed4 f4 = fixed4(1,2,3,4);
fixed3 f3 = f4.xyz; // 高维向量转低维:前三个分量作为新向量 fixe3(1,2,3)
fixed4 f4_2 = fixed4(f3,4); // 低维向量转高维:只需要补全所需分量个数即可
3、向量和矩阵的更多用法
这些都是后续会用到的一些用法。
- 利用向量声明矩阵:可以用向量来代替矩阵中的某行元素。
fixed4x4 f4x4 = {fixed4(1,2,3,4),
fixed4(1,2,3,4),
fixed4(1,2,3,4),
fixed4(1,2,3,4)};
- 获取矩阵元素:类似二维数组的用法。
fixed f = f4x4[0][0]; // 第一行第一列元素
- 利用向量获取矩阵某一行:可以用向量来接收矩阵中的某行元素。
fixed4 f4 = f4x4[0]; // 第一行元素
- 高维转低维:向量和矩阵都可以用低维存储高维,会按照顺序依次存储,并自动舍去多余元素。
fixed3x3 f3x3 = f4x4; // 矩阵,存储前三行前三列元素
fixed3 f3 = f4; // 向量,存储前三个分量
(五)运算符
1、比较运算符
| 含义 | 运算符 |
|---|---|
| 大于 | > |
| 小于 | < |
| 大于等于 | >= |
| 小于等于 | <= |
| 等于 | == |
| 不等于 | != |
2、条件运算符
| 含义 | 运算符 |
|---|---|
| 三目运算符 | condition ? value_if_true : value_if_false |
3、逻辑运算符
注意:逻辑运算符在 CG 中不存在“短路”操作,意思就是会把条件都判断完。
| 含义 | 运算符 |
|---|---|
| 逻辑或 | || |
| 逻辑与 | && |
| 逻辑非 | ! |
4、数学运算符
注意:CG 中取余符号只能用于整数。
| 含义 | 运算符 |
|---|---|
| 加法 | + |
| 减法 | - |
| 乘法 | * |
| 除法 | / |
| 取余 | % |
| 自增 | ++ |
| 自减 | – |
5、位运算符
| 含义 | 运算符 |
|---|---|
| 按位与 | & |
| 按位或 | | |
| 按位异或 | ^ |
| 按位取反 | ~ |
| 左移 | << |
| 右移 | >> |
(六)流程控制语句
1、条件分支语句
- if 语句。
- switch 语句。
2、循环语句
- for 循环。
- while 循环。
- do…while 循环。
3、注意事项
在使用它们时要更多的考虑性能消耗:
- 尽量少的使用循环语句,如果一定要用要减少次数和复杂度。
- 要利用 GPU 并行性这一特点来替代循环。
- 尽量避免复杂的条件分支。
(七)函数
1、无返回值的函数
void 函数名(in 参数类型 参数名, out 参数类型 参数名)
{
// 函数体
}
这里和 C 语言不同的是它有 in 和 out 关键字,这个下面会提到,除此之外其他都一样。
2、有返回值的函数
返回值类型 函数名(in 参数类型 参数名)
{
// 函数体
return 返回值;
}
虽然可以在有返回值的函数中使用 out 参数,但这并不是常见做法,对于顶点/片元着色器函数只会使用单返回值的方式进行处理。
3、关于 in 和 out
- in:表示输入参数,由函数外部传递给函数内部,内部不会修改该参数,只会使用该参数进行计算,允许有多个。
- out:表示输出参数,由函数内部传递给函数调用者,在函数内部必须对该参数值进行初始化或修改,允许有多个。
- inout:表示输入输出参数,参数的值可以在函数内部被修改,并且修改会在函数调用结束后保留在参数中,这个用的比较少。
in 和 out 可以都不写,这样就没有了 in 和 out 相关的限制,当然如果没有这种需求,一般还是建议写上 in 和 out,可以提高代码的可读性和可维护性。
(八)顶点/片元着色器的基本结构
为了方便后面知识的学习,这里先讲下顶点/片元着色器的一个基本结构,代码中会有一些陌生的知识:比如语义、CG 内置函数等等。不过别担心,后面都会讲的,这里先有个印象。
- 顶点着色器的基本结构:
- POSITION 和 SV_POSITION 是 CG 的语义,用于限定输入参数和返回值。POSITION 表示把模型的顶点坐标填充到输入的参数 v 当中;SV_POSITION 表示输出的内容是裁剪空间中的顶点坐标。
mul(UNITY_MATRIX_MVP, *)是旧写法,表示 MVP 变换,新版本中会替换成UnityObjectToClipPos(*),效果是一样的。
CGPROGRAM
#pragma vertex myVert // 指定实现顶点着色器的函数
float4 myVert(float4 v : POSITION) : SV_POSITION
{
return UnityObjectToClipPos(v); // 对顶点坐标进行变换,将变换后的顶点坐标返回出去
}
ENDCG
- 片元着色器的基本结构:
- SV_Target 是 CG 的语义,表示把用户输出颜色存储到一个渲染目标中,这里将输出到默认的帧缓存中。
CGPROGRAM
#pragma fragment myFrag // 指定实现片元着色器的函数
fixed4 myFrag() : SV_Target
{
return fixed4(1, 0, 0, 1); // 直接让材质全部显示为红色
}
ENDCG
我们将上述的两个着色器整合到一起,就得到了最简单的顶点/片元着色器的示例:让模型空间下的顶点坐标转换到裁剪空间,同时让使用该着色器的材质直接完全显示为红色。
Shader "Unlit/NewUnlitShader"
{
Properties { }
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex myVert // 指定顶点着色器的实现函数
#pragma fragment myFrag // 指定片元着色器的实现函数
// 顶点着色器
float4 myVert(float4 v : POSITION) : SV_POSITION
{
return UnityObjectToClipPos(v); // 将模型空间下的顶点坐标转换到裁剪空间下
}
// 片元着色器
fixed4 myFrag() : SV_Target
{
return fixed4(1, 0, 0, 1); // 直接返回红色
}
ENDCG
}
}
}
(九)语义
1、概念
CG 语言中提供了语义这种特殊关键字用于修饰函数中的传入参数和返回值,它的主要作用是让 Shader 知道从哪里读取数据,并把数据输出到哪里。
注意:
- Unity 中只支持 CG 中的部分语义。
- 只有顶点/片元着色器的函数相关参数和返回值才需要通过语义修饰,因为需要让渲染器知道这些数据的意义;而一般的自定义函数是不需要语义的,因为我们自己知道自定义函数每个参数和返回值的含义。
2、常用语义
接下来讲解一些 Unity 常用的语义,这里将按阶段进行划分。
(1)应用阶段->顶点着色器
将应用阶段传递的模型数据给顶点着色器时的语义,一般在顶点着色器函数的传入参数中使用。
- POSITION:模型空间中的顶点位置,通常是
float4类型。 - NORMAL:顶点法线,通常是
float3类型,常用于计算光照消息。 - TANGENT:顶点切线,通常是
float4类型,常用于计算光照消息。 - TEXCOORD[n]:该顶点的纹理坐标,通常是
float2或者float4类型,比如TEXCOORD0、TEXCOORD1等等。(TEXCOORD0表示第一组纹理坐标,依次类推)- 纹理坐标:也称 UV 坐标,表示该顶点对应纹理图像上的位置。
- COLOR:顶点颜色,通常是
fixed4或float4类型。
(2)顶点着色->片元着色器
从顶点着色器传递数据给片元着色器时的语义,一般在顶点着色器函数的返回值中使用。
- SV_POSITION:裁剪空间中的顶点坐标(必备)。
- COLOR0:通常用于输出第一组顶点颜色(不是必须的)。
- COLOR1:通常用于输出第二组顶点颜色(不是必须的)。
- TEXCOORD0 ~ TEXCOORD7:通常用于输出纹理坐标,当不明确传递的数据为何种语义都可以使用此语义(不是必须的)。
(3)片元着色器输出
片元着色器输出时的语义,一般在片元着色器函数的返回值中使用。
- SV_Target:输出值会存储到渲染目标中。
DirectX 10 后出现了新的语义类型:系统数值语义(system-value semantics),这类语义都是以 SV 开头的,并且这些语义在渲染流水线中都有特殊含义。
比如 SV_POSITION 用于修饰顶点着色器的输出变量 pos,这个 pos 就是齐次裁剪空间中的坐标,用这些语义描述的变量不可以随便赋值,因为流水线需要它们来完成特定目的,比如这个 SV_POSITION 修饰的变量经过光栅化阶段后会显示在屏幕上。
如果 SV_POSITION 用 POSITION 替代,有些平台的 shader 是无法正常工作的。为了让 Shader 有更好的跨平台性,对于这些有特殊含义的变量,最好用 SV 开头的语义进行修饰。
3、更多语义
HLSL语义汇总:语义 - Win32 apps | Microsoft Learn
CG 的语义大部分和 HLSL 的相同,在此做了解即可,以后我们用到其他语义时再进行讲解。
(十)顶点/片元着色器传递更多参数
1、概念
在之前基本结构那节的顶点/片元着色器代码中,顶点着色器只传递了一个参数 v,那如果我想在顶点/片元着色器中获取更多模型相关信息时要怎么做呢?
实现方法就是使用结构体对数据进行封装,每个成员变量写好对应的语义即可,从而实现传递更多参数。
2、声明用于传递给顶点着色器的结构体
比如顶点着色器函数不仅要获取顶点坐标信息,还要获取法线、纹理坐标等信息时,可以这么做:
// a2v:application to vertex,即应用阶段输出给顶点着色器的数据
struct a2v
{
float4 vertex:POSITION; // 顶点坐标(基于模型空间)
float3 normal:NORMAL; // 顶点法线(基于模型空间)
float2 uv:TEXCOORD0; // 纹理坐标(uv坐标)
};
3、声明用于传递给片元着色器的结构体
这里实现也是类似的,要什么数据写封装进去就好了。
// v2f:vertex to fragment,即顶点着色器输出给片元着色器的数据
struct v2f
{
float4 position:SV_POSITION; // 裁剪空间下的坐标
float3 normal:NORMAL; // 顶点法线(基于模型空间)
float2 uv:TEXCOORD0; // 纹理坐标(uv坐标)
};
4、完整示例
当然目前的这些数据没什么处理和特别意义,大家只需要知道能这么用即可。
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 从应用阶段传递给顶点着色器的结构体数据
struct a2v
{
float4 vertex:POSITION;
float3 normal:NORMAL;
float2 uv:TEXCOORD0;
};
// 从顶点着色器传递给片元着色器的结构体数据
struct v2f
{
float4 position:SV_POSITION;
float3 normal:NORMAL;
float2 uv:TEXCOORD0;
};
// 顶点着色器
v2f vert(a2v data)
{
// 需要传递给片元着色器的数据
v2f v2fData;
v2fData.position = UnityObjectToClipPos(data.vertex);
v2fData.normal = data.normal;
v2fData.uv = data.uv;
return v2fData;
}
// 片元着色器
fixed4 frag(v2f data):SV_Target
{
return fixed4(0,1,0,1);
}
ENDCG
(十一)CG 使用 ShaderLab 属性
1、CG 变量类型与 ShaderLab 的属性对应关系
想要在 CG 中使用 ShaderLab 的属性,我们就需要在 CG 中声明和属性对应类型的同名变量即可,首先要掌握的就是两者的对应关系:
| ShaderLab属性类型 | CG变量类型 |
|---|---|
Color、Vector |
float4、half4、fixed4 |
Range、Float、Int |
float、half、fixed |
2D |
sampler2D |
Cube |
samplerCube |
3D |
sampler3D |
2DArray |
sampler2DArray |
2、使用属性
属性的使用很简单,只需要在 CG 语句块中声明和属性中对应类型的同名变量即可,比如我想要使用颜色属性:
Shader "ShaderName"{
Properties{
_Color("BackGroundColor", Color) = (1,1,1,1)
}
SubShader{
Pass{
CGPROGRAM
//使用属性
float4 _Color;
ENDCG
}
}
Fallback "VertexLit"
}
(十二)CG 内置函数
Unity Shader 中的 CG 语言提供了各种用于图形编程的函数,这些函数是 CG 为我们封装好的逻辑,我们可以直接使用它们来编写 Unity Shader。
这里只记录了常用的内置函数,你可以通过官方文档来查看更多的函数:内部函数 - Win32 apps | Microsoft Learn。
当然这是 HLSL 对应的内置函数,但 CG 和它差别不大。(注意:不是所有函数都在Unity中被支持)
1、数学函数
- 三角函数相关
| 函数 | 说明 |
|---|---|
sincos(float x, out s, out c) |
该函数同时计算x的sin值和cos值通过s和c进行返回(比分别运算快很多) |
sin(x) |
正弦函数 |
cos(x) |
余弦函数 |
tan(x) |
正切函数 |
sinh(x) |
双曲正弦函数 |
cosh(x) |
双曲余弦函数 |
tanh(x) |
双曲正切函数 |
asin(x) |
反正弦函数,输入参数范围[-1,1],返回[-π/2,π/2]区间的角度值 |
acos(x) |
反余弦函数,输入参数范围[-1,1],返回[0,π]区间的角度值 |
atan(x) |
反正切函数,输入参数范围[-1,1],返回[-π/2,π/2]区间的角度值 |
atan2(y,x) |
计算y/x的反正切值。和atan功能一样,只是输入参数不同。atan(x)=atan2(x,1) |
- 向量、矩阵相关
| 函数 | 说明 |
|---|---|
cross(A,B) |
叉乘(注意:传入向量必须是三维向量) |
dot(A,B) |
点乘(注意:传入向量必须是三维向量) |
mul(M,N) |
计算两个矩阵相乘 |
mul(M,v) |
计算矩阵和向量相乘 |
mul(v,M) |
计算向量和矩阵相乘 |
transpose(M) |
M为矩阵,计算M的转置矩阵 |
determinant(m) |
计算矩阵的行列式因子 |
- 数值相关
| 函数 | 说明 |
|---|---|
abs(x) |
返回输入参数的绝对值 |
ceil(x) |
对输入参数向上取整 |
floor(x) |
对输入参数向下取整 |
clamp(x,a,b) |
如果x小于a,则返回a;x大于b,则返回b;否则返回x(“夹紧"函数) |
radians(x) |
角度转弧度 |
degrees(x) |
弧度转角度 |
max(a,b) |
返回最大值 |
min(a,b) |
返回最小值 |
sqrt(x) |
求x的平方根,x必须大于0 |
exp(x) |
计算e的x次方的值,e=2.71828182845904523536 |
exp2(x) |
计算2的x次方的值 |
fmod(x,y) |
返回x/y的余数,y不为0 |
frac(x) |
返回标量或每个矢量分量的小数部分 |
frexp(x,out) |
将浮点数x分解为尾数和直属,即 x = m * 2的exp次方,返回m,将指数存储exp中 |
isfinite(x) |
判断标量或者向量中的每个数据是否是有限数,如果是返回true,否则返回false |
isinf(x) |
判断标量或者向量中的每个数据是否是无限,如果是返回true,否则返回false |
isnan(x) |
判断标量或者向量中的每个数据是否是非数据,如果是返回true,否则返回false |
ldexp(x,n) |
计算x * 2的n次方 的值 |
log(x) |
计算ln(x)的值,x必须大于0 |
log2(x) |
计算log2(x次方)的值,x必须大于0 |
log10(x) |
计算log2(x次方)的值,x必须大于0 |
saturate(x) |
如果x小于0,返回0;如果x大于1,返回1;否则,返回x |
sign(x) |
如果x大于0,返回1;如果x小于0,返回-1;否则,返回0 |
smoothstep(min,max,x) |
值x位于min、max区间内,如果x=min,返回0;如果x=max,返回1;如果在两者之间,返回-2* ((x-min)/(max - min))的三次方 + 3* ((x - min)/(max - min))的2次方 |
step(a,x) |
如果x<a,返回0;否则,返回1 |
all(x) |
输入参数均不为0,则返回true;否则返回False。相当于逻辑与&& |
any(x) |
输入参数只要有其中一个不为0,则返回true。相当于逻辑或|| |
- 其他
| 函数 | 说明 |
|---|---|
lit(NdotL,NdotH,m) |
N表示法向量;L表示入射光向量;H表示半角向量;m表示高光系数;该函数计算环境光、散射光、镜面光的贡献,返回4维向量;x位表示环境光贡献;y位表示散射光贡献;z位表示镜面光贡献;w始终为1 |
noise(x) |
噪声函数,返回值始终是0~1之间;对于相同的输入,始终返回相同值,不是真正意义的随机噪声 |
2、几何函数
| 函数 | 说明 |
|---|---|
length(v) |
返回一个向量的模长 |
normalize(v) |
归一化向量 |
distance(p1,p2) |
计算两点之间的距离 |
reflect(I,N) |
计算反射光方向向量,I为入射光,N为顶点法向量。I是指向顶点的,I和N必须被归一化,必须是3维向量 |
refract(I,N,eta) |
计算折射向量,I为入射光,N为顶点法向量,eta为折射系数。I是指向顶点的,I和N必须被归一化,必须是3维向量 |
3、纹理函数
注意:这些纹理采样函数返回值为 fixed4 类型的颜色值。
- 二维纹理
| 函数 | 说明 |
|---|---|
tex2D(sampler2D tex, float2 s) |
二维纹理查询 |
tex2D(sampler2D tex, float2 s, float2 dsdx, float2 dsdy) |
使用导数值查询二维纹理 |
tex2D(sampler2D tex, float3 sz) |
二维纹理查询,并进行深度值比较 |
tex2D(sampler2D tex, float3 sz, float2 dsdx, float2 dsdy) |
使用导数值查询二维纹理,并进行深度值比较 |
tex2Dproj(sampler2D tex, float3 sq) |
二维投影纹理查询 |
tex2Dproj(sampler2D tex, float4 szq) |
二维投影纹理查询,并进行深度值比较 |
- 立方体纹理
| 函数 | 说明 |
|---|---|
texCUBE(samplerCUBE tex, float3 s) |
查询立方体纹理 |
texCUBE(samplerCUBE tex, float3 s, float3 dsdx, float3 dsdy) |
结合导数值查询立方体纹理 |
texCUBEDproj(samplerCUBE tex, float4 sq) |
查询立方体投影纹理,并进行深度值比较 |
- 其他纹理
| 函数名称 | 描述 |
|---|---|
tex1D(sampler1D tex, float s) |
一维纹理查询 |
tex1D(sampler1D tex, float s, float dsdx, float dsdy) |
使用导数值查询一维纹理 |
tex1D(sampler1D tex, float2 sz) |
一维纹理查询,并进行深度值比较 |
tex1D(sampler1D tex, float2 sz, float dsdx, float dsdy) |
使用导数值查询一维纹理,并进行深度值比较 |
tex1Dproj(sampler1D tex, float2 sq) |
一维投影纹理查询 |
tex1Dproj(sampler1D tex, float3 szq) |
一维投影纹理查询,并进行深度值比较 |
texRECT(samplerRECT tex, float2 s) |
矩形纹理查询 |
texRECT(samplerRECT tex, float2 s, float2 dsdx, float2 dsdy) |
使用导数值查询矩形纹理 |
texRECT(samplerRECT tex, float3 sz) |
矩形纹理查询,并进行深度值比较 |
texRECT(samplerRECT tex, float3 sz, float2 dsdx, float2 dsdy) |
使用导数值查询矩形纹理,并进行深度值比较 |
texRECTproj(samplerRECT tex, float3 sq) |
矩形投影纹理查询 |
texRECTproj(samplerRECT tex, float3 szq) |
矩形投影纹理查询,并进行深度值比较 |
tex3D(sampler3D tex, float3 s) |
查询三维纹理 |
tex3D(sampler3D tex, float3 s, float3 dsdx, float3 dsdy) |
结合导数值查询三维纹理 |
tex3DDproj(sampler3D tex, float4 sq) |
查询三维投影纹理,并进行深度值比较 |
(十三)CG 内置文件
1、概念
CG 中的内置文件和内置函数一样,是用于帮助我们进行 Shader 开发的,它们是预定义的一些文件,里面包含了一些已经写好的 Shader 相关逻辑,比如函数、宏、全局变量等,可以提升我们 Shader 的开发效率。
如果想要了解更多的内置内容可以参阅Unity官网的资料:
- 内置文件相关:内置着色器 include 文件 - Unity 手册
- 函数相关:内置着色器 helper 函数 - Unity 手册
- 宏相关:内置宏 - Unity 手册
- 变量相关:内置着色器变量 - Unity 手册
宏:通常指的是一种预处理指令或代码片段,用于在代码中进行文本替换。宏允许程序员定义一个标识符(通常以大写字母表示)来代表一个代码片段,然后在编译时将这个标识符替换为相应的代码,这种替换过程称为宏展开。
2、内置文件的位置
我们可以在 Unity 编辑器的安装目录中找到 CG 内置文件,在 Unity编辑器安装目录/Data/CGIncludes 中:
- 后缀为
.cginc的文件为 CG 语言内置文件。 - 后缀为
.glslinc的文件为 GLSL 语言内置文件。
3、如何使用内置文件
在 CG 语句块中,通过编译指令 include 即可引用对应的文件:
#include "内置文件名.cginc"
常用的内置文件如下:
UnityCG.cginc:包含最常用的帮助函数、宏和结构体等。Lighting.cginc:包含各种内置光照模型。如果编写的是 Surface Shader (标准表面着色器),会自动包含进来。UnityShaderVariables.cginc:包含许多内置的全局变量。编译 Unity Shader 时,会自动包含进来。HLSLSupport.cginc:声明了很多用于跨平台编译的宏和定义。编译 Unity Shader 时,会自动包含进来。
4、常用内容
- 常用方法(来自 UnityCG.cginc)
| 方法名 | 描述 |
|---|---|
float3 WorldSpaceViewDir(float4 v) |
输入模型空间中的顶点位置,返回世界空间中从该点到摄像机的观察方向 |
float3 ObjSpaceViewDir(float4 v) |
输入模型空间中的顶点位置,返回模型空间中从该点到摄像机的观察方向 |
float3 WorldSpaceLightDir(float4 v) |
输入模型空间中的顶点位置,返回世界空间中从该点到光源的光照方向(未归一化) |
float3 ObjSpaceLightDir(float4 v) |
输入模型空间中的顶点位置,返回模型空间中从该点到光源的光照方向(未归一化) |
float3 UnityObjectToWorldNormal(float3 norm) |
将法线方向从模型空间转换到世界空间中 |
float3 UnityObjectToWorldDir(in float3 dir) |
将方向矢量从模型空间转换到世界空间中 |
float3 UnityWorldToObjectDir(float3 dir) |
将方向矢量从世界空间转换到模型空间中 |
- 常用结构体(来自 UnityCG.cginc)
| 结构体名 | 描述 |
|---|---|
appdata_base |
用于顶点着色器输入,包括顶点位置、顶点法线、第一组纹理坐标 |
appdata_tan |
用于顶点着色器输入,包括顶点位置、顶点法线、顶点切线、第一组纹理坐标 |
appdata_full |
用于顶点着色器输入,包括顶点位置、顶点法线、顶点切线、四组(或更多)纹理坐标 |
appdata_img |
用于顶点着色器输入,仅包含顶点位置和第一组纹理坐标 |
v2f_img |
用于顶点着色器输出,包含裁剪空间中的位置和纹理坐标 |
- 变换矩阵宏(来自 UnityShaderVariables.cginc)
坐标空间变换顺序:模型空间 -> 世界空间 -> 观察空间 -> 裁剪空间 -> 屏幕空间。
| 宏名 | 描述 |
|---|---|
UNITY_MATRIX_MVP |
当前的模型观察投影矩阵,用于将顶点/方向向量从模型空间变换到裁剪空间 |
UNITY_MATRIX_MV |
当前的模型*观察矩阵,用于将顶点/方向向量从模型空间变换到观察空间 |
UNITY_MATRIX_V |
当前的观察矩阵,用于将顶点/方向向量从世界空间变换到观察空间 |
UNITY_MATRIX_P |
当前的投影矩阵,用于将顶点/方向向量从观察空间变换到裁剪空间 |
UNITY_MATRIX_VP |
当前的观察*投影矩阵,用于将顶点/方向向量从世界空间变换到裁剪空间 |
UNITY_MATRIX_T_MV |
UNITY_MATRIX_MV的转置矩阵 |
UNITY_MATRIX_IT_MV |
UNITY_MATRIX_MV的逆转置矩阵,用于将法线从模型空间变换到观察空间 |
_Object2World |
当前的模型矩阵,用于将顶点/方向矢量从模型空间变换到世界空间 |
_World2Object |
_Object2World的逆矩阵,用于将顶点/方向矢量从世界空间变换到模型空间 |
- 常用变量
| 变量名 | 描述 |
|---|---|
_Time |
自关卡加载以来的时间,用于对着色器内的事物进行动画处理 |
_LightColor0 |
光源的颜色(向前渲染时,UnityLightingCommon.cginc;延迟渲染,UnityDeferredLibrary.cginc) |