• 引言:主要为UnityShader中较为基础的部分

一、Shader结构初识

1. 属性

1
Properties{  属性定义}

2.SubShader

1
2
3
4
5
6
7
8
9
10
         SubShader
// {
// //顶点-片段着色器 或 表面着色器 或 固定函数着色器
// }
// SubShader
// {
// //更加精简的版本
// //目的是适配旧设备
// }
// .....可以有n个SubShader代码块

3.备用的Shader

1
Fallback "备用的Shader"

二、Shader的属性

1. 属性的作用

  • 1.可以在材质面板被编辑
    2.可以在后续当作输入变量提供给所有子着色器使用

2. 属性的基本语法

1
_Name("Display Name", type) = defaultValue[{options}]
  • _Name::属性名字,规则是需要在前面加一个下划线,方便在之后获取
  • Display Name:材质面板上显示的名字
  • type:属性的类型
  • defaultValue:将Shader指定给材质的时候初始化的默认值

3. 数值类型属性

  1. 整形

    1
    _Name("Display Name", Int) = number
  2. 浮点型

    1
    _Name("Display Name", Float) = number
  3. 范围浮点型

    1
    _Name("Display Name", Range(min,max)) = number
  • 注:
    • Unity Shader中的数值类型属性基本都是浮点型(Float)数据
    • 虽然提供了整数(Int),但是编译时最终都会转换为浮点型,因此我们更多的使用的还是Float类型

4. 颜色和向量类型属性

  1. 颜色

    • :颜色值中的RGBA的取值范围是 0~1 (映射0~255)
    1
    _Name("Display Name", Color) = (number1,number2,number3,number4)
  2. 向量

    1
    _Name("Display Name", Vector) = (number1,number2,number3,number4)
    • 向量值中的XYZW的取值范围没有限制

5. 纹理贴图类型属性

  1. 2D 纹理(最常用的纹理,漫反射贴图、法线贴图都属于2D纹理)

    1
    _Name("Display Name", 2D) = "defaulttexture"{}
  2. 2DArray 纹理(纹理数组,允许在纹理中存储多层图像数据,每层看做一个2D图像,一般使用脚本创建,较少使用,了解即可)

    1
    _Name("Display Name", 2DArray) = "defaulttexture"{}
  3. Cube map texture纹理(立方体纹理,由前后左右上下6张有联系的2D贴图拼成的立方体,比如天空盒和反射探针)

    1
    _Name("Display Name", Cube) = "defaulttexture"{}
  4. 3D纹理(一般使用脚本创建,极少使用,了解即可)

    1
    _Name("Display Name", 3D) = "defaulttexture"{}
  • 注:关于defaulttexture默认值取值
    • 不写:默认贴图为空
    • white:默认白色贴图(RGBA:1,1,1,1)
    • bump:默认凸贴图(RGBA:0.5,0.5,1,1),一般用于法线贴图默认贴图
    • red:默认红色贴图(RGBA:1,0,0,1)
  • 注:关于默认值后面的 {} ,固定写法(老版本中括号内可以控制固定函数纹理坐标的生成,但是新版本中没有该功能了)

三、SubShader的基本构成

  • SubShader的作用:
    • 每一个Shader中都会包含至少一个SubShader
    • 当Unity想要显示一个物体的时候,就会在Shader文件中去检测这些SubShader语句块
    • 然后选择第一个能够在当前显卡运行的SubShader进行执行
    • 因此在一个Shader当中实现一些高级效果时,为了避免在在某些设备上无法执行,可能会存在多个SubShader语句块,用于适配这些低端设备。

1. 基本构成

  1. 渲染标签:通过标签来确定什么时候以及如何对物体进行渲染
  2. 渲染状态:通过状态来确定渲染时的剔除方式、深度测试方式、混合方式等等内容
  3. 渲染通道:具体实现着色器代码的地方(每个SubShader语句块中至少有一个渲染通道,可以有多个)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

//SubShader
//{
// 1.渲染标签
// Tags{ "标签名1" = "标签值1" "标签名2" = "标签值2" .....}
//
// 2.渲染状态
// .....
//
// 3.渲染通道
// Pass
// {
// 第一个渲染通道
// }
// Pass
// {
// 第二个渲染通道
// }
// .............
//}
  • 注意
    • 在SubShader中每定义一个渲染通道Pass,就会让物体执行一次渲染
    • n个Pass,就会有n次渲染,在实现一些复杂渲染效果时需要使用多个Pass进行组合实现
    • 但是我们要尽量减少它的数量,更多的Pass会增加性能消耗

2. Tags——渲染标签

  • 渲染标签:通过标签来确定什么时候以及如何对物体进行渲染

(1)渲染标签语法结构

  • 渲染标签是通过键值对的形式进行声明

  • 并且没有数量限制,可以使用任意多个标签

    1
    Tags{ "标签名1" = "标签值1" "标签名2" = "标签值2" "标签名2" = "标签值2" .......}

(2)渲染队列Queue

  • 主要作用: 确定物体的渲染顺序。

    1
    Tags{ "Queue" = "标签值" }
  • 常用Unity预先定义好的渲染队列标签值:

    1. Background(背景)(队列号:1000)

      • 最早被渲染的物体的队列,一般用来渲染天空盒或者背景
      1
      Tags{ "Queue" = "Background" }
    2. Geometry(几何)(队列号:2000)

      • 不透明的几何体通常使用该队列,当没有声明渲染队列时,Unity会默认使用这个队列
      1
      Tags{ "Queue" = "Geometry" }
    3. AlphaTest(透明测试)(队列号:2450)

      • 有透明通道的,需要进行Alpha测试的几何体会使用该队列
      • 当所有Geometry队列实体绘制完后再绘制AlphaTest队列,效率更高
      1
      Tags{ "Queue" = "AlphaTest" }
    4. Transparent(透明的)(队列号:3000)

      • 该队列中几何体按照由远到近的顺序进行绘制半透明物体的渲染队列,所有进行透明混合的几何体都应该使用该队列
      • 比如:玻璃材质,粒子特效等
      1
      Tags{ "Queue" = "Transparent" }
    5. Overlay(覆盖)(队列号:4000)

      • 用是放在最后渲染的队列,于叠加渲染的效果,比如镜头光晕等
      1
      Tags{ "Queue" = "Overlay" }
    6. 自定义队列

      • 基于Unity预先定义好的这些渲染队列标签来进行加减运算来定义自己的渲染队列
      • 比如:Tags{ “Queue” = “Geometry+1” } 代表的队列号就是 2001
      • 自定义队列在一些特殊情况下,特别有用
      • 比如 一些水的渲染 想要在不透明物体之后,半透明物体之前进行渲染,就可以自定义
      • 注:自定义队列只能基于预先定义好的各类型进行计算,不能在Shader中直接赋值数字。如果实在想要直接赋值数字,可以在材质面板中进行设置

(3)渲染类型 RenderType

  • 主要作用:对着色器进行分类,之后可以用于着色器替换功能

    ​ 摄像机上有对应的API,可以指定这个渲染类型来替换成别的着色器。

    1
    Tags{ "RenderType" = "标签值" }
  • 常用Unity预先定义好的渲染类型标签值:
    1. Opaque(不透明的)
      • 用于普通Shader,比如:不透明、自发光、反射等
    2. Transparent(透明的)
      • 用于半透明Shader,比如:透明、粒子
    3. TransparentCutout(透明切割)
      • 用于透明测试Shader,比如:植物叶子
    4. Background(背景)
      • 用于天空盒Shader
    5. Overlay(覆盖)
      • 用于GUI纹理、Halo(光环)、Flare(光晕)

(4)禁用批处理

  • 主要作用:当使用批处理时,模型会被变换到世界空间中模型空间会被丢弃,这可能会导致某些使用模型空间顶点数据的 Shader最终无法实现想要的结果。可以通过开启禁用批处理来解决该问题。
  1. 总是禁用批处理

    1
    Tags{ "DisableBatching" = "True" }
  2. 不禁用批处理(默认值)

    1
    Tags{ "DisableBatching" = "False" }
  3. LOD效果激活时才会禁用批处理,主要用于地形系统上的树(了解)

    1
    Tags{ "DisableBatching" = "LODFading" }

(5)禁止阴影投影

  • 主要作用:控制该SubShader的物体是否会投射阴影。
  1. 投射阴影(默认值)

    1
    Tags{ "ForceNoShadowCasting" = "False" }//不投射改为True

(6)忽略投影机

  • 主要作用:物体是否受到Projector(投影机)的投射。(Projector是Unity中的一个功能(以后讲解))
  • 忽略Projector(一般半透明Shader需要开启该标签)

    1
    Tags{ "IgnoreProjector" = "True" }//默认值为False

(7)其他标签

  1. 是否用于精灵

    • 想要将该SubShader用于Sprite时,将该标签设置为False

      1
      Tags{ "CanUseSpriteAtlas" = "False" }
  2. 预览类型

    • 材质在预览窗口默认为球形。如果想要改变为平面或天空盒,只需要改变预览标签即可

    • 平面

      1
      Tags{ "PreviewType" = "Panel" }
    • 天空盒

      1
      Tags{ "PreviewType" = "SkyBox" }

(8)注意事项

  • 以上这些标签只能在SubShader语句块中声明
  • 之后讲解的Pass渲染通道语句块中也可以声明渲染标签
  • 但是今天讲解的内容``都不能在Pass中声明
  • Pass中有自己专门的标签类型,我们会在之后讲解

3. 渲染状态

(1)语法结构

  • 渲染状态关键词+空格+状态类型

  • 如果存在多个渲染状态 可以通过空行隔开

    比如:

    1
    LOD 100

(2)剔除方式

  • 主要作用:设置多边形的剔除方式,有背面剔除、正面剔除、不剔除。

    • 所谓的剔除,就是不渲染,背面剔除就是背面不渲染,正面剔除就是正面不渲染,不剔除就是都渲染。

      1
      2
      3
      Cull Back     背面剔除
      Cull Front 正面剔除
      Cull Off 不剔除
    • 一般情况下,我们需要两面渲染时,会设置为不剔除

(3)深度缓冲

  • 主要作用:是否写入深度缓冲

  • 什么是深度缓冲????

    • 深度缓冲是一个与屏幕像素对应的缓冲区,用于存储每个像素的深度值(距离相机的距离)。

    • 在渲染场景之前,深度缓冲被初始化为最大深度值,表示所有像素都在相机之外。

    • 最后留在深度缓冲中的信息会被渲染(只要通过了深度测试就会被渲染

      1
      2
      3
      ZWrite On     写入深度缓冲
      ZWrite Off 不写入深度缓冲
      //不设置的话,默认为写入
    • 一般情况下,我们在做透明等特殊效果时,会设置为不写入

(4)深度测试

  • 主要作用:设置深度测试的对比方式

  • 什么是深度测试????

    • 深度测试的主要目的是确保在渲染时,像素按照正确的深度(距离相机的距离)顺序进行绘制。从而创建正确的遮挡关系和透视效果

    • 在渲染场景之前,深度缓冲初始化为最大深度值,表示所有像素都在相机之外。

    • 在渲染过程中,对于每个像素,深度测试会将当前像素的深度值与深度缓冲中对应位置的值进行比较。

    • 一般情况下:

      1. 如果当前像素的深度值小于深度缓冲中的值,说明当前像素在其他物体之前,它会被绘制,并更新深度缓冲。

      2. 如果当前像素的深度值大于等于深度缓冲中的值,说明当前像素在其他物体之后,它会被丢弃,不会被绘制,并保持深度缓冲不变。

        深度测试图

      1
      2
      3
      4
      5
      6
      7
      8
      //ZTest Less        小于当前深度缓冲中的值,就通过测试,写入到深度缓冲中
      //ZTest Greater 大于当前深度缓冲中的值,就通过测试,写入到深度缓冲中
      //ZTest LEqual 小于等于当前深度缓冲中的值,就通过测试,写入到深度缓冲中
      //ZTest GEqual 大于等于当前深度缓冲中的值,就通过测试,写入到深度缓冲中
      //ZTest Equal 等于当前深度缓冲中的值,就通过测试,写入到深度缓冲中
      //ZTest NotEqual 不等于当前深度缓冲中的值,就通过测试,写入到深度缓冲中
      //ZTest Always 始终通过深度测试写入深度缓冲中
      //不设置的话,默认为LEqual 小于等于
  • 一般情况下,我们只有在实现一些特殊效果时才会去修改深度测试方式,比如透明物体渲染会修改为Less,描边效果会修改为Greater等。

(5)混合方式

  • 主要作用:设置渲染图像的混合方式(利用颜色缓冲区多种颜色叠加混合,比如透明、半透明效果和遮挡的物体进行颜色混合)

    1
    2
    3
    4
    5
    6
    7
    //Blend One One                         线性减淡
    //Blend SrcAlpha OneMinusSrcAlpha 正常透明混合
    //Blend OneMinusDstColor One 滤色
    //Blend DstColor Zero 正片叠底
    //Blend DstColor SrcColor X光片效果
    //Blend One OneMinusSrcAlpha 透明度混合
    //不设置的话,默认不会进行混合
  • 一般情况下,我们需要多种颜色叠加渲染时,就需要设置混合方式,具体情况具体处理

(6)其他渲染状态

  1. LOD 控制LOD级别,在不同距离下使用不同的渲染方式处理
  2. ColorMask 设置颜色通道的写入蒙版,默认蒙版为RGBA

(7)渲染状态注意事项

  • 以上这些状态不仅可以在SubShader语句块中声明
  • 后讲解的Pass渲染通道语句块中也可以声明这些渲染状态
  • 如果在SubShader语句块中使用会影响之后的所有渲染通道Pass
  • 如果在Pass语句块中使用只会影响当前Pass渲染通道,不会影响其他的Pass

4.渲染通道

(1)渲染通道的语法结构

1
2
3
4
5
6
//Pass{
// 1.Name 名称
// 2.渲染标签
// 3.渲染状态
// 4.其他着色器代码
//}

(2)Pass的名字

  • 主要作用:可以利用UsePass命令在其他Shader当中复用该Pass的代码

    ​ 只需要在其他Shader当中使用UsePass "Shader路径/Pass名"

  • 注意:Unity内部会把Pass名称转换为大写字母

    因此在使用UsePass命令时必须使用大写形式的名字

    1
    2
    3
    4
    5
    //Pass{
    // Name MyPass
    //}
    //在其他Shader中复用该Pass代码时
    //UsePass "TeachShader/Lesson4/MYPASS" //写在Pass外

(3)Pass的标签

  • Pass中的渲染标签语法和SubShader中相同
  • Tags{ “标签名1” = “标签值1” “标签名2” = “标签值2” “标签名2” = “标签值2” …….}
  • 但是我们之前讲解过的SubShader语句块中的渲染标签不能够在Pass中使用
  • Pass当中有自己专门的渲染标签
  1. Tags{ "LightMode" = "标签值" }

    • 主要作用:指定了该Pass应该在哪个阶段执行,可以将着色器代码分配给适当的渲染阶段,以实现所需的效果。

      image-20250720095421973

  2. 2.Tags{ "RequireOptions" = "标签值" }

    • 主要作用:用于指定当满足某些条件时才渲染该Pass
    • 目前Unity仅支持Tags{ “RequireOptions” = “SoftVegetation“ }
    • 仅当Quality窗口中开启了SoftVegetation时才渲染此通道
  3. Tags{ "PassFlags" = "标签值" }

    • 主要作用:一个渲染通道Pass可指示一些标志来更改渲染管线向Pass传递数据的方式
    • 目前Unity仅支持Tags{ “PassFlags” = “OnlyDirectional“ }
    • 在 ForwardBase 向前渲染的通道类型中使用时,此标志的作用是仅允许主方向光和环境光/光照探针数据传递到着色器
      • 这意味着非重要光源的数据将不会传递到顶点光源或球谐函数着色器变量

(4)Pass的渲染状态

  • 我们上节课在SubShader语句块中学习的渲染状态同样适用于Pass。具体看上节。
  • 注意:
    • Pass语句块中使用只会影响当前Pass渲染通道,不会影响其他的Pass
    • SubShader语句块中使用会影响之后的所有渲染通道Pass
    • Pass中还可以使用固定管线着色器的命令

(5)其他着色器代码

  • 其他代码部分就是实现着色器的核心代码部分
  • 在后面会详细讲解

(6)GrabPass命令

  • 我们可以利用GrabPass命令把即将绘制对象时的屏幕内容抓取到纹理中

  • 在后续通道中即可使用此纹理,从而执行基于图像的高级效果。

  • 举例:

    • 将绘制该对象之前的屏幕抓取到 _BackgroundTexture 中

      1
      2
      3
      4
         GrabPass
      {
      "_BackgroundTexture"
      }
  • 注:该命令一般写在某个Pass前,在之后的Pass代码中可以利用_BackgroundTexture变量进行处理

5. 备用Shader

(1)备用shader的作用

  • 我们知道ShaderLab当中允许有多个SubShader子着色器
  • 当执行渲染时,会从上到下使用第一个能够正常执行的SubShader子着色器来渲染对象
  • 那有没有可能所有SubShade子着色器都不能够在显卡上执行呢?
  • 答案是肯定的
  • 有可能出现在某一个设备上,我们自定义的所有SubShader都无法正常执行(设备显卡无法支持一些api等情况)
  • 那么这时备用Shader就可以起到很大作用了,它至少可以让对象能够正常渲染出来
  • 备用Shader主要作用:就是当Shader文件中的所有SubShader子着色器都无法正常运行时,起到一个保险作用,让物体能够 使用一个最低级的Shader去渲染出来(效果略差,但至少能够显示)

(2)备用Shader的语法

  • Fallback “Shader名” 或 Fallback Off
  • 在Fallback关键词后面空格并通过一个字符串来告诉Unity
  • 也可以直接关闭Fallback功能

四、Shader的编写形式

  • 我们一般会使用以下3种形式来编写Unity Shader
    1. 表面着色器(可控性较低)
    2. 顶点/片元着色器(重点学习)
    3. 固定函数着色器(基本已弃用,了解即可)

(1)表面着色器

  • 表面着色器(Surface Shader)是Unity自己创造的一种着色器代码类型
  • 它的本质对顶点/片元着色器的一层封装
  • 它需要的代码量很少,很多工作都帮助我们去完成了
  • 缺点:渲染的消耗较大,可控性较低
  • 优点:帮助我们处理了很多光照细节,我们可以直接使用而无需自己计算实现光照细节
  • 如何使用:
    • 我们可以在创建Shader时,选择创建Standard Surface Shader
    • 通过观察该Shader文件的内部结构,你会发现
    • 着色器相关代码被放在SubShader语句块中(并非Pass)的 CGPROGRAM 和 ENDCG 之间
  • 表面着色器的特点:
    1. 直接在SubShader语句块中书写着色器逻辑。
    2. 我们不需要关心也不需要使用多个Pass,每个Pass如何渲染,Unity会在内部帮助我们去处理
    3. 可以使用CG或HLSL两种Shader语言去编写Shader逻辑
    4. 代码量较少,可控性较低,性能消耗较高
    5. 适用于处理需要和各种光源打交道的着色器(主机、PC平台时更适用,移动平台需要考虑性能消耗)

(2)顶点/片元着色器

  • 我们可以在创建Shader时,选择创建Unlit Shader来快速创建顶点/片元着色器模板
  • 顶点/片元着色器的着色器代码是编写在Pass语句块中
  • 我们需要自己定义每个Pass需要使用的Shader代码
  • 虽然比起表面着色器来说我们需要编写的代码较多
  • 但是好处是灵活性更高,可控性更强,可以控制更多的渲染细节
  • 决定对性能影响的高低
  • 顶点片元着色器的特点:
    1. 需要在Pass渲染通道中编写着色器逻辑
    2. 可以使用CG或HLSL两种Shader语言去编写Shader逻辑
    3. 代码量较多,灵活性较强,性能消耗更可控,可以实现更多渲染细节
    4. 适用于光照处理较少,自定义渲染效果较多时(移动平台首选)

(3)固定函数着色器(了解)

  • 表面着色器顶点/片元着色器 这两种Unity Shader形式都使用了可编程管线
  • 而对于一些老设备(DX7.0、OpenGL1.5或OpenGL ES 1.1),它们不支持可编程管线着色器
  • 这时就需要使用固定函数着色器来进行渲染,这些着色器只能实现一些非常简单的效果
  • 固定函数着色器的特点:
    1. 需要在Pass渲染通道中编写着色器逻辑
    2. 需要使用ShaderLab语法中的渲染设置命令来编写,而非CG和HLSL着色器语言
  • 但是由于这些旧设备目前市面上几乎已经没有了,所以固定函数着色器我们几乎不会再使用
  • 即使我们现在在Unity中使用固定函数着色器来编写Shader,在内部也会被编译为顶点/片元着色器

五、CG语法

1. CG语句写在哪里

  • 对于顶点\片元着色器来说,CG语句需要写在Pass渲染通道语句块

  • 我们需要在Pass语句块中,加入指令

    1
    2
    3
    4
    5
    CGPROGRAM

    在这两个指令之间 就是我们书写CG代码的地方

    ENDCG

2. 重要的编译指令 —— 指定着色器函数

  • 在真正书写CG代码之前,我们需要先使用 #pragma 声明编译指令

    1
    2
    #pragma vertex name(实现顶点着色器的函数名)
    #pragma fragment name(实现片元着色器的函数名)

3. 基础数据类型

  • uint 32为无符号整形
  • int 32位整形
  • float 32位浮点数 符号:f
  • half 16位浮点数 符号:h
  • fixed 12位浮点数
  • bool 布尔类型
  • string 字符串
  • sampler 纹理对象 (他们都是用于处理纹理(Texture)数据的数据类型)
    • sampler: 通用的纹理采样器,可以用于处理各种不同维度和类型的纹理
    • sampler1D: 用于一维纹理,通常用于对一维纹理进行采样,例如从左到右的渐变色
    • sampler2D: 用于二维纹理,最常见的纹理类型之一。它用于处理二维图像纹理,例如贴图
    • sampler3D: 用于三维纹理,通常用于体积纹理,例如体积渲染
    • samplerCUBE: 用于立方体纹理,通常用于处理环境映射等需要立方体贴图的情况
    • samplerRECT: 用于处理矩形纹理,通常用于一些非标准的纹理映射需求
  • 复合数据类型

    1. 一维

      int a[4] = {1,2,3,4}

    2. 二维

    `int b[2][3] = {{1,2,3},{4,5,6}}`
    
    1. 结构体

      和C#基本一样;没有访问修饰符结构体声明结束加分号

4. 复杂数据类型

  • 向量

    • 向量类型属于CG语言的内置数据类型

    • 向量的最大维度不超过4维

    • 基本构成:

      • 数据类型2 = 数据类型2(n1,n2)
      • 数据类型3 = 数据类型3(n1,n2,n3)
      • 数据类型4 = 数据类型4(n1,n2,n3,n4)
      1
      int2 i =int2(1,2);
  • 矩阵
    • 矩阵类型属于CG语言的内置数据类型
    • 矩阵的最大行列不大于4不小于1
    • 数据类型可以是任意数值类型
    • 基本构成:
      • 数据类型’n’x’m’ = {n1m1,n1m2,n1m3…..}
  • bool类型特殊使用

    • bool类型同样可以用于如同向量一样声明

    • 它可以用于存储一些逻辑判断结果

    • 比如:

      1
      2
      3
      4
      float3 a = float3(0.5, 0.0, 1.0);
      float3 b = float3(0.6, -0.1, 0.9);
      bool3 c = a < b;
      //运算后向量c的结果为bool3(true, false, false)
  • 注意:CG中向量,矩阵和数组是完全不同的,向量和矩阵内置的数据类型,而数组则是一种数据结构,不是内置数据类型

5.函数

  1. 无返回值的函数

    • 基本结构

      1
      2
      3
      4
      //void name(in 参数类型 参数名, out 参数类型 参数名)
      //{
      // 函数体
      //}
    • in:表示是输入参数,表示由函数外部传递给函数内部,内部不会修改该参数,只会使用该参数进行计算,允许有多个

    • out:表示是输出参数,表示由函数内部传递给函数的调用者,在函数内部必须对该参数值进行初始化或修改,允许有多个

    • 注意:

      • in和out都可以省略,省略后就没有了in和out相关的限制
      • 虽然可以省略,但是为了代码可读性,建议大家在编写Shader时不要省略in和out
  2. 有返回值的函数

    • 基本结构

      1
      2
      3
      4
      5
      //type name(in 参数类型 参数名)
      //{
      // 函数体
      // return 返回值;
      //}
    • 注意:

      • 虽然可以在有返回值的函数中使用out参数,但是这并不是常见做法,除非是一些自定义逻辑函数。
      • 对于顶点/片元着色器函数只会使用单返回值的方式进行处理

6. 顶点/片元着色器基本结构

  • 顶点着色器
1
2
3
4
5
6
7
8
9
10
11
12
13
//顶点着色器 回调函数 
//POSITION 和 SV_POSITION是CG语言的语义
//POSITION:把模型的顶点坐标填充到输入的参数v当中
//SV_POSITION:顶点着色器输出的内容是裁剪空间中的顶点坐标
//如果没有这些语义来限定输入和输出参数的话,那么渲染器就完全不知道用户输入输出的是什么,就会得到错误的效果
float4 myVert(float4 v:POSITION):SV_POSITION
{
//mul是CG语言提供的矩阵和向量的乘法运算函数(就是一个内置的函数)
//UNITY_MATRIX_MVP 代表一个变换矩阵 是Unity内置的模型、观察、投影矩阵的集合
//UnityObjectToClipPos它的作用和之前的矩阵乘法是一样的,主要目的就是在进行坐标变换 只不过新版本将其封装起来了 使用更加方便
//mul(UNITY_MATRIX_MVP,v);
return UnityObjectToClipPos(v);
}
  • 片元着色器:
1
2
3
4
5
6
//片元着色器 回调函数
//SV_Target:告诉渲染器,把用户输出颜色存储到一个渲染目标中,这里将输出到默认的帧缓存中
fixed4 myFrag():SV_Target
{
return fixed4(0,1,0,1);
}

7. 语义

  • 语义: CG语言中提供了 语义 这种特殊关键字用于修饰函数中的传入参数和返回值
  • 它的作用:
    • 让Shader知道从哪里读取数据并把数据输出到哪里
    • 让我们在Shader开发当中可以获取到想要的数据,并且可以把数据传递出去
  • 注意:Unity中只支持CG当中的部分语义
  • 常用语义

    • 应用阶段——>顶点着色器

      应用阶段传递模型数据给顶点着色器时Unity支持的语义,一般在顶点着色器回调函数的传入参数中应用

      1. POSITION:

        模型空间中的顶点位置,通常是float4类型

      2. NORMAL:

        顶点法线,通常是float3类型

      3. TANGENT:

        顶点切线,通常是float4类型

      4. TEXCOORDn:

        比如 TEXCOORD0,TEXCOORD1….该顶点的纹理坐标,通常是float2或者float4类型

        TEXCOORD0表示第一组纹理坐标,依次类推.

        纹理坐标:也称UV坐标,表示该顶点对应纹理图像上的位置

      5. COLOR:

        顶点颜色,通常是fixed4或float4类型

    • 顶点着色器——>片元着色器

      顶点着色器传递数据给片元着色器时Unity支持的语义,一般在顶点着色器回调函数的返回值中应用。

      1. SV_POSITION:

        裁剪空间中的顶点坐标(必备)

      2. COLOR0:

        通常用于输出第一组顶点颜色(不是必须的)

      3. COLOR1:

        通常用于输出第二组顶点颜色(不是必须的)

      4. TEXCOORD0~TEXCOORD7:

        通常用于输出纹理坐标(不是必须的)

  • 片元着色器输出

    片元着色器输出时Unity支持的常用语义,一般在片元着色器回调函数的返回值中应用。

    • SV_Target:

      输出值会存储到渲染目标中

8. 顶点/片元传递更多参数

  • 顶点着色器获取更多数据信息

    用结构体对数据进行封装

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    //该结构体
    //是用于从应用阶段获取对应语义数据后
    //传递给顶点着色器回调函数的
    struct a2v
    {
    //顶点坐标(基于模型空间)
    float4 vertex:POSITION;
    //顶点法线(基于模型空间)
    float3 normal:NORMAL;
    //纹理坐标(uv坐标)
    float2 uv:TEXCOORD0;
    };

    v2f vert(a2v data)
    {
    //需要传递给片元着色器的数据
    v2f v2fData;
    v2fData.position = UnityObjectToClipPos(data.vertex);
    v2fData.normal = data.normal;
    v2fData.uv = data.uv;

    return v2fData;
    }
  • 片元着色器获取更多数据信息

    • 注意:片元着色器中获取的数据基本上都是由顶点着色器传递过来的,所以我们封装的结构体还需要作为顶点着色器的返回值 类型。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
       //从顶点着色器传递给片元着色器的 结构体数据 
      //同样这里面的成员也需要用语义去进行修饰
      struct v2f
      {
      //裁剪空间下的坐标
      float4 position:SV_POSITION;
      //顶点法线(基于模型空间)
      float3 normal:NORMAL;
      //纹理坐标(uv坐标)
      float2 uv:TEXCOORD0;
      };

      fixed4 frag(v2f data):SV_Target
      {
      return fixed4(0,1,0,1);
      }

9. ShaderLab属性类型与CG变量类型的匹配关系

  • CG中变量类型对应ShaderLab的属性类型

    • ShaderLab属性类型 CG变量类型
      Color,Vector float4,half4,fixed4
      Range,Float,Int float,half,fixed
      Range,Float,Int float,half,fixed
      2D sampler2D
      Cube samplerCube
      3D sampler3D
      2DArray sampler2DArray
  • 如何在CG语句块中使用ShaderLab中声明的属性???

    • 直接在CG语句块中,声明和属性中对应类型的同名变量即可。

      1
      2
      _MainTex ("Texture", 2D) = "white" {}
      sampler2D _MainTex

10. CG内置函数和CG内置文件

(1)CG内置函数介绍

  • 数学函数

    • 三角函数相关

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      //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)
    • 向量、矩阵相关

      1
      2
      3
      4
      5
      6
      7
      //cross(A,B)                        叉乘(注意:传入向量必须是三维向量)
      //dot(A,B) 点乘(注意:传入向量必须是三维向量)
      //mul(M,N) 计算两个矩阵相乘
      //mul(M,v) 计算矩阵和向量相乘
      //mul(v,M) 计算向量和矩阵相乘
      //transpose(M) M为矩阵,计算M的转置矩阵
      //determinant(m) 计算矩阵的行列式因子
    • 数值相关

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
              //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
      //pow(x,y) x的y次方的值
      //round(x) 对x四舍五入
      //rsqrt(x) x的反平方根,x必须大于0
      //lerp(a,b,f) 差值函数,计算(1-f)*a + b*f 或者 a + f*(b-a)的值

      //不常用

      //exp(x) 计算e的x次方的值,e=2.71828182845904523536
      //exp2(x) 计算2的x次方的值
      //fmod(x,y) 返回x/y的余数,y不为0
      //frac(x) 返回标量或每个矢量分量的小数部分
      //frexp(x,out exp) 将浮点数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。相当于逻辑或||
    • 其他

      1
      2
      3
      4
      //lit(NdotL,NdotH,m)                N表示法向量;L表示入射光向量;H表示半角向量;m表示高光系数
      // 该函数计算环境光、散射光、镜面光的贡献,返回4维向量;
      // x位表示环境光贡献;y位表示散射光贡献;z位表示镜面光贡献;w始终为1
      //noise(x) 噪声函数,返回值始终是0~1之间;对于相同的输入,始终返回相同值,不是真正意义的随机噪声
  • 内置函数

    • 几何函数

      1
      2
      3
      4
      5
      6
      //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维向量

  • 纹理函数

    注意: 这些纹理采样函数返回值为 fixed4 类型的颜色值

    • 二维纹理

      1
      2
      3
      4
      5
      6
      //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) 二维投影纹理查询,并进行深度值比较
    • 立方体纹理

      1
      2
      3
      //texCUBE(samplerCUBE tex, float3 s)                                查询立方体纹理
      //texCUBE(samplerCUBE tex, float3 s, float3 dsdx, float3 dsdy) 结合导数值查询立方体纹理
      //texCUBEDproj(samplerCUBE tex, float4 sq) 查询立方体投影纹理,并进行深度值比较
    • 其他纹理

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      //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) 查询三维投影纹理,并进行深度值比较

(2)CG内置文件

  • 如何使用内置文件???
    • 在CG语句块中进行引用,#include "内置文件名.cginc"的形式进行引用。
    • 注意:一些常用的函数、宏、变量,可以不用引用,Unity会在编译时自动识别
  • 常用内容:

    • 方法(UnigyCG.cginc中)

      1. float3 WorldSpaceViewDir(float4 v)
        • 输入一个模型空间中的顶点位置,返回世界空间中从该点到摄像机的观察方向
      2. float3 ObjSpaceViewDir(float4 v)
        • 输入一个模型空间中的顶点位置,返回模型空间中从该点到摄像机的观察方向
      3. float3 WorldSpaceLightDir(float4 v)
        • 仅用于向前渲染中。输入一个模型空间中的顶点位置,返回世界空间中从该点到光源的光照方向。(返回值没有被归一化)
      4. flaot3 ObjSpaceLightDir(float4 v)
        • 仅用于向前渲染中。输入一个模型空间中的顶点位置,返回模型空间中从该点到光源的光照方向。(返回值没有被归一化)
      5. float3 UnityObjectToWorldNormal(float3 norm)
        • 把法线方向从模型空间转换到世界空间
      6. float3 UnityObjectToWorldDir(in float3 dir)
        • 把方向矢量从模型空间转换到世界空间中
      7. float3 UnityWorldToObjectDir(float3 dir)
        • 把方向矢量从世界空间转换到模型空间中

    • 结构体(UnigyCG.cginc中)

      1. appdata_base
        • 用于顶点着色器输入
        • 内容:顶点位置、顶点法线、第一组纹理坐标
      2. appdata_tan
        • 用于顶点着色器输入
        • 内容:顶点法线、顶点切线、第一组纹理坐标
      3. appdata_full
        • 用于顶点着色器输入
        • 内容:顶点位置、顶点法线、顶点切线、四组(或更多)纹理坐标
      4. appdata_img
        • 用于顶点着色器输入
        • 顶点位置、第一组纹理坐标
      5. v2f_img
        • 用于顶点着色器输出
        • 裁剪空间中的位置,纹理坐标

    • 变换矩阵宏(UnityShaderVariables.cginc中)

      坐标空间变换顺序:

      • 模型空间 -> 世界空间 -> 观察空间 -> 裁剪空间 -> 屏幕空间
      1. UNITY_MATRIX_MVP
        • 当前的模型 * 观察* 投影矩阵,用于将顶点/方向向量从模型空间变换到裁剪空间
      2. UNITY_MATRIX_MV
        • 当前的模型 * 观察矩阵,用于将顶点/方向向量从模型空间变换到观察空间
      3. UNITY_MATRIX_V
        • 当前的观察矩阵,用于将顶点/方向向量从世界空间变换到观察空间
      4. UNITY_MATRIX_P
        • 当前的投影矩阵,用于将顶点/方向向量从观察空间变换到裁剪空间
      5. UNITY_MATRIX_VP
        • 当前的观察 * 投影矩阵,用于将顶点/方向向量从世界空间变换到裁剪空间
      6. UNITY_MATRIX_T_MV
        • UNITY_MATRIX_MV的转置矩阵
      7. UNITY_MATRIX_IT_MV
        • UNITY_MATRIX_MV的逆转置矩阵,用于将法线从模型空间变换到观察空间,也可用于得到UNITY_MATRIX_MV的逆矩阵
      8. _Object2World
        • 当前的模型矩阵,用于将顶点/方向矢量从模型空间变换到世界空间
      9. _World2Object
        • _Object2World的逆矩阵,用于将顶点/方向矢量从世界空间变换到模型空间

    • 变量

      1. _Time (不用引用,直接使用即可)
        • 自关卡加载以来的时间(t/20、t、t2、t3)用于对着色器内的事物进行动画处理
      2. _LightColor0 (向前渲染时,UnityLightingCommon.cginc;延迟渲染,UnityDeferredLibrary.cginc)
        • 光的颜色