一轮整理:2021年6月16日17:19:23

二轮整理:2021年10月28日20:24:37

作者:聪头

本书Unity版本:v5.2.1

Github:https://github.com/candycat1992/Unity_Shaders_Book

本文档为独自整理的笔记,故章节的主题和书本不一致!

Ch1.欢迎来到Shader的世界

介绍各章节内容,本文档为独自整理的笔记,故章节的主题和书本不一致!

Ch2.渲染流水线

《Shader入门精要》基础篇笔记 - 图1

2.1 应用阶段

  • 输入:无
  • 执行者:CPU
  • 输出结果:渲染图元(点、线三角面等)

过程:

  1. 把数据加载到显存中:硬盘 —> 内存 —> 显存
    • 内存数据加载到显存后,可以移除。但对于一些数据,如用于碰撞检测的网格数据,则不希望移除
    • 场景数据:摄像机、视锥体、场景中包含模型、光源信息等。粗粒度剔除,把那些不可见的物体剔除出去。
  2. 设置渲染状态:定义场景中网格是怎样被渲染的
    • 简单理解为Unity给Material绑定Shader,给模型绑定Material并设置参数
    • 例如,指定顶点/片元着色器,光源属性,材质等
  3. 调用DrawCall
    • 由CPU发给GPU的命令,指向一个需要被渲染的图元(primitives)列表。给定一个DrawCall,GPU会根据渲染状态(如材质、纹理、着色器等)和所有输入的顶点数据进行计算,最终输出成屏幕上显示的像素。该计算过程,就是GPU流水线

2.2 几何阶段

  • 输入:CPU
  • 执行者:GPU
  • 输出结果:屏幕空间的顶点信息

《Shader入门精要》基础篇笔记 - 图2

过程:

  1. 顶点着色器:完全可编程控制,必须实现
    • 用于实现顶点的空间变换、顶点着色等
    • 坐标变换:对顶点的坐标(即位置)进行某种变换。必须完成:把顶点从模型空间转换到齐次裁剪空间。
  2. 曲面细分着色器:完全可编程控制,可选
    • 细分图元
  3. 几何着色器:完全可编程控制,可选
    • 用于执行逐图元的着色操作或产生更多图元
  4. 裁剪:可配置,不可编程
    • 裁剪不在相机范围内的顶点,并剔除三角图元的面片
    • 《Shader入门精要》基础篇笔记 - 图3
  5. 屏幕映射:GPU固定实现
    • 负责把每个图元的坐标转换到屏幕坐标系中

2.3 光栅化阶段

  • 输入:屏幕坐标系下的顶点位置以及和它们相关的额外信息,如深度值(z坐标)、法线方向、视角方向等
  • 执行者:GPU
  • 输出结果:计算每个图元覆盖了哪些像素,以及为这些像素计算它们的颜色

过程:

  1. 三角形设置:GPU固定实现
    • 计算光栅化一个三角形网格所需的信息
    • 上一阶段输出的都是三角网格的顶点,即每条边的两个端点。我们需要计算每条边上的像素坐标,得到整个三角网格对像素的覆盖情况。
    • 计算三角网格表示数据的过程就叫做三角形设置
  2. 三角形遍历:GPU固定实现
    • 片元:像素被一个三角网格覆盖,就会生成一个片元(fragment)。一个片元并不是真正意义上的像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色(利用上一阶段的三角网格的3个顶点信息对整个覆盖区域的像素进行插值)。这些状态包括(但不限于)它的屏幕坐标、深度信息,以及其他从几何阶段输出的顶点颜色,例如法线、纹理坐标等
    • 找到哪些像素被三角网格覆盖的过程就是三角形遍历,这个阶段也叫做扫描变换(Scan Conversion)
  3. 片元着色器:完全可编程控制,可选

    • 输入的是上一个阶段对顶点信息插值得到的结果,输出的是一个或多个颜色值。这一阶段可以完成很多重要的渲染技术,如纹理采样

      解释: 上一阶段不会影响屏幕上每个像素的颜色值,而是会产生一系列的数据,用来表述一个三角网格是怎样覆盖每个像素的。一句话说,每个片元只负责存储这样一系列数据,真正会对像素产生影响的是下一阶段,逐片元操作

  4. 逐片元操作:可配置,不可编程

    • 很多重要操作,如修改颜色、深度缓冲、混合等
    • 《Shader入门精要》基础篇笔记 - 图4
    • 逐片元操作阶段所做的操作。只有通过了所有的测试后,新生成的片元才能和颜色缓冲区中已经存在的像素颜色进行混合,最后再写入颜色缓冲区中

《Shader入门精要》基础篇笔记 - 图5

区别:

1.模板测试和写入一起开和关;深度测试和写入可分别开启

2.模板测试未通过也可更新值;深度测试没通过不可更新值

《Shader入门精要》基础篇笔记 - 图6

源颜色:片元着色器的颜色值

目标颜色:已经存在于颜色缓冲区中颜色值

补充:

  • 屏幕显示的是颜色缓冲区中的颜色值。GPU会使用双重缓冲的策略,对场景的渲染是在幕后发生的,即在后置缓冲区中。一旦场景已经被渲染到了后置缓冲区中,GPU就会交换前置缓冲区和后置缓冲区中的内容,前置缓冲区就是显示在屏幕上的图像。由此,保证我们看到的图像总是连续的

2.4 课后答疑

DrawCall

性能瓶颈:CPU(而非GPU),每次调用DrawCall,CPU需要向GPU发送很多内容,包括数据、状态和命令等。在这一阶段,CPU需要完成很多工作,例如检查渲染状态等。而GPU的渲染能力很强,因此渲染速度往往快于CPU提交命令的速度。如果DrawCall数量太多,CPU就会把大量时间花费在提交DrawCall上,造成CPU过载

减少DrawCall的办法:

  • 避免使用大量很小的网格,当不可避免地使用很小的网格时,考虑是否可以合并它们
  • 避免使用过多的材质。尽量在不同的网格之间共有同一个材质

什么是Shader

  • GPU流水线上一些可高度编程的阶段,而由着色器编译出来的最终代码是会在GPU上运行的(对于固定管线的渲染来说,着色器有时等同于一些特定的渲染设置)

Ch3.UnityShader基础

3.1 UnityShader概述

  • Unity Material和Shader

《Shader入门精要》基础篇笔记 - 图7

把材质(Material)想象成组件,把UnityShader想象成源文件。

  • 组件的目的:个性化
  • 源文件的目的:通用性
  • ShaderLab:为Unity开发者提供的高层级的渲染抽象层

3.2 Shader结构:名称

  • 使用”/“控制位置

《Shader入门精要》基础篇笔记 - 图8

3.3 Shader结构:Properties

  • 材质和UnityShader的桥梁,通过材质面板传递Shader属性

《Shader入门精要》基础篇笔记 - 图9

解释:

  • Name:Shader访问的名字
  • display name:材质面板上的名字
  • Propertype:类型
  • DefaultValue:默认值

Properties属性!!!

《Shader入门精要》基础篇笔记 - 图10

说明:

  • 2D、Cube、3D的内置纹理默认值包括:white、black、gray、bump

例子

《Shader入门精要》基础篇笔记 - 图11

  • 为了在Shader中可以访问到这些属性,我们需要在Cg代码片中定义和这些属性类型相匹配的变量。
  • 需要说明的是,即使我们不在Properties语义块中声明这些属性,也可以直接在Cg代码片中定义变量。此时,我们可以通过过脚本向Shader中传递这些属性

3.4 Shader结构:SubShader

  • 每个UnityShader可以包含多个SubShader语义块(至少一个),加载时Unity选择第一个可在目标平台运行的SubShader。如果都不支持,使用Fallback

解释:不同显卡具有不同的能力,写SubShader就是为了在旧显卡使用计算复杂度较低的着色器,在高级显卡使用计算复杂度较高的着色器,以便提供更出色的画面

重要组成

  • SubShader:定义了一系列Pass以及可选的状态([RenderSetup])和标签([Tags])设置
  • Pass:定义了一次完整的渲染流程

语法:

《Shader入门精要》基础篇笔记 - 图12

状态设置!!!

  • SubShader和Pass语法相同

《Shader入门精要》基础篇笔记 - 图13

SubShader专属标签!!!

概念:SubShader标签是一个键值对,它的键和值都是字符串类型,用来告诉Unity渲染引擎:我希望怎样以及何时渲染这个对象

语法:《Shader入门精要》基础篇笔记 - 图14

类型:

《Shader入门精要》基础篇笔记 - 图15

Pass语义块

语法:

《Shader入门精要》基础篇笔记 - 图16

Pass名称

  • Name “MyPassName”
  • 通过Pass名称,我们可以使用ShaderLab的UsePass命令来直接使用其他Unity Shader中的Pass
    • 例如:UsePass “MyShader/MYPASSNAME”

Unity内部会把所有Pass名称转换成大写字母

Pass专属标签!!!

概念:Pass标签也是告诉渲染引擎我们希望怎样来渲染物体

语法:《Shader入门精要》基础篇笔记 - 图17

类型:

《Shader入门精要》基础篇笔记 - 图18

特殊的Pass

  • UsePass:复用其他UnityShader中的Pass
  • GrabPass:抓取屏幕并将结果存储在一张纹理中,以用于后续的Pass处理

3.5 Shader结构:Fallback

  • 所有SubShader都不能运行,使用该Shader
  • Fallback还会影响阴影的投射,在渲染阴影纹理中,Unity会在每个UnityShader中寻找一个阴影投射的Pass。Fallback使用的内置Shader中包含了这样一个通用的Pass

语法:《Shader入门精要》基础篇笔记 - 图19

例子:Fallback "VertexLit"

其他语义:有时间自行完善

  • 扩展编辑界面:CustomEditor
  • 命令分组:Category

3.6 着色器分类

表面着色器

Surface Shader

概念:Unity自己创造的一种着色器代码类型,是对顶点/片元着色器更高层的抽象,本质仍是顶点/片元着色器,定义在SubShader块(而非Pass块)中的CGPROGRAM和ENDCG之间。可以使用Cg/HLSL几乎所有语法。

《Shader入门精要》基础篇笔记 - 图20

顶点/片元着色器

Vertex/Fragment Shader

概念:本书使用。使用Cg/HLSL语言来编写,更复杂但更灵活。定义在Pass内

3.7 课后答疑

UnityShader != 真正的Shader

《Shader入门精要》基础篇笔记 - 图21

Ch4.学习Unity所需的数学基础

4.1 笛卡尔坐标系

二维笛卡尔坐标系:

《Shader入门精要》基础篇笔记 - 图22

三维笛卡尔坐标系:

  • 基矢量:3个坐标轴
  • 标准正交基:3个坐标轴之间互相垂直,且长度为1
  • 正交基:坐标轴之间互相垂直但长度不为1

判断坐标系:哪只手满足就是哪个坐标系

  • 大拇指:+x
  • 食指:+y
  • 中指:+z

判断旋转正方向:伸出坐标系对应的手,大拇指对准旋转轴正方向,握拳方向就是旋转正方向(左手顺时针,右手逆时针)

4.2 点和矢量

点:n维空间中的一个位置,没有大小、宽度的概念

矢量(向量):n维空间中一种包含了模和方向的有向线段

标量:只有模,没有方向

规定:

《Shader入门精要》基础篇笔记 - 图23

矢量运算:

  • 矢量和标量乘法/除法:《Shader入门精要》基础篇笔记 - 图24
  • 矢量的加法和减法:《Shader入门精要》基础篇笔记 - 图25
  • 矢量的模:《Shader入门精要》基础篇笔记 - 图26
  • 单位矢量:《Shader入门精要》基础篇笔记 - 图27
  • 点积
    • 公式1:《Shader入门精要》基础篇笔记 - 图28
    • 公式2(平行四边形面积):《Shader入门精要》基础篇笔记 - 图29
  • 叉积
    • 公式:《Shader入门精要》基础篇笔记 - 图30
    • 模:《Shader入门精要》基础篇笔记 - 图31
    • 方向:确定坐标系,根据左(右)手法则,从a到b握拳,拇指方向为叉积结果方向

4.3 矩阵

定义

  • mxn个标量组成的长方形数组

行向量和列向量:

《Shader入门精要》基础篇笔记 - 图32

矩阵运算

  • 矩阵和标量相乘:《Shader入门精要》基础篇笔记 - 图33
  • 矩阵和矩阵相乘:《Shader入门精要》基础篇笔记 - 图34 《Shader入门精要》基础篇笔记 - 图35

矩阵乘法性质

  1. 矩阵乘法不满足交换律:《Shader入门精要》基础篇笔记 - 图36
  2. 矩阵乘法满足结合律:《Shader入门精要》基础篇笔记 - 图37

特殊矩阵

1.方块矩阵:简称方阵,行和列数目相等

  • 对角元素:行和列号相等的元素。如m11
  • 对角矩阵:除对角元素外均为0的矩阵。非常适合次方计算。

《Shader入门精要》基础篇笔记 - 图38

2.单位矩阵:任何矩阵和它相乘的结果还是原来的矩阵

《Shader入门精要》基础篇笔记 - 图39

3.转置矩阵:实际是对原矩阵的一种运算。行列对调

  • 性质一:《Shader入门精要》基础篇笔记 - 图40
  • 性质二:《Shader入门精要》基础篇笔记 - 图41

4.逆矩阵:满足

《Shader入门精要》基础篇笔记 - 图42

逆矩阵判断:

  1. 必须是方阵
  2. 行列式不为0

逆矩阵性质:

  1. 逆矩阵的逆矩阵是原矩阵本身(设M可逆)
    《Shader入门精要》基础篇笔记 - 图43
  2. 单位矩阵的逆矩阵是它本身
    《Shader入门精要》基础篇笔记 - 图44
  3. 转置矩阵的逆矩阵是逆矩阵的转置
    《Shader入门精要》基础篇笔记 - 图45
  4. 矩阵串接相乘后的逆矩阵等于反向串接各个矩阵的逆矩阵。可推广到多维
    《Shader入门精要》基础篇笔记 - 图46
    《Shader入门精要》基础篇笔记 - 图47

5.正交矩阵

  • 如果一个方阵M和它的转置矩阵的乘积是单位矩阵的话,我们就说这个矩阵是正交的

《Shader入门精要》基础篇笔记 - 图48

等价于

《Shader入门精要》基础篇笔记 - 图49

推论

- c1 -:表示一个三维向量c1的行展开式

《Shader入门精要》基础篇笔记 - 图50

《Shader入门精要》基础篇笔记 - 图51

4.4 变换

变换:指的是我们把一些数据,如点、方向矢量甚至颜色等,经过某种方式进行转换的过程

线性变换:指可以保留矢量加和标量乘的变换

《Shader入门精要》基础篇笔记 - 图52

仿射变换:合并线性变换和平移变换。仿射变换可以使用一个4x4的矩阵表示,为此,我们需要把矢量扩展到四维空间下,就是齐次坐标空间

齐次坐标:四维向量

  • 点:w=1
  • 向量:w=0

1.平移矩阵

《Shader入门精要》基础篇笔记 - 图53

2.缩放矩阵

《Shader入门精要》基础篇笔记 - 图54

3.旋转矩阵(正交矩阵)

无论坐标轴如何变换,两两之间总是互相垂直,且长度为1,满足上述推论。故旋转矩阵是正交矩阵

  • 绕x轴旋转θ

《Shader入门精要》基础篇笔记 - 图55

  • 绕y轴旋转θ

《Shader入门精要》基础篇笔记 - 图56

  • 绕z轴旋转θ

《Shader入门精要》基础篇笔记 - 图57

约定:先缩放,再旋转,最后平移

4.5 坐标空间变换

  • 变谁就按列展开乘谁,详见P69
    • 第一个”谁”:目标坐标空间,即变换到的空间
    • 第二个”谁”:自己的坐标轴在目标空间下的表示

如果矩阵正交,那么只需转置即可得到逆变换

已知子坐标空间C的3个坐标轴在父坐标空间P下的表示Xc,Yc,Zc,以及原点位置Oc。给定一个子坐标空间一点Ac=(a,b,c),确定其在父空间下的位置Ap。

《Shader入门精要》基础篇笔记 - 图58

个人心得

记录于:2021.10.27

根据看的角度不同,分为两种变换类型

  1. 坐标变换:我们坐标系下的点经过一个矩阵变换到我们坐标系下另一个点
  2. 基变换:它们坐标系下的点,在我们坐标系下的表示

举例说明:以二维旋转变换为例

《Shader入门精要》基础篇笔记 - 图59

顶点的坐标变换过程

《Shader入门精要》基础篇笔记 - 图60

难点:投影矩阵推导

并非真正从3D投影到2D,而是投影到NDC (Normalized Device Coordinates) 中,但是投影变换不会进行齐次除法,所以结果并非真正意义上的点,而是一个待处理的四维向量(w分量非0非1,而是和z有关)。所以四维向量需经过齐次除法,才算真正成为NDC中的点。

个人小结:

  • 观察空间 - 投影变换 -> 裁剪空间(手动实现)
  • 裁剪空间 - 齐次除法 -> NDC空间(底层实现)
  • NDC空间 - 屏幕映射 -> 屏幕空间(底层实现)

NDC和裁剪空间并非同一空间!

正交投影矩阵

《Shader入门精要》基础篇笔记 - 图61

《Shader入门精要》基础篇笔记 - 图62

透视投影矩阵

《Shader入门精要》基础篇笔记 - 图63

《Shader入门精要》基础篇笔记 - 图64

《Shader入门精要》基础篇笔记 - 图65

4.6 法线变换

详见P86

4.7 Shader内置变量!!!

变换矩阵

《Shader入门精要》基础篇笔记 - 图66

摄像机和屏幕参数

《Shader入门精要》基础篇笔记 - 图67

《Shader入门精要》基础篇笔记 - 图68

4.8 答疑解惑(了解)

Cg中的矢量和矩阵类型

矢量初始化:

  • float4 a = float4(1.0, 2.0, 3.0, 4.0);

矩阵初始化(行优先填充):

  • float3x3 M = float3x3(1.0, 2.0, 3.0, 4.0, 5.0, 6.0,7.0, 8.0, 9.0);

《Shader入门精要》基础篇笔记 - 图69

注意:脚本的Matrix4x4(列优先填充)

获取Unity中的屏幕坐标

法1.frag函数直接声明语义

在片元着色器的输入中声明VPOS(HLSL)或WPOS(Cg)语义

  • 设屏幕分辨率400x300,VPOS、WPOS各分量取值范围如下
  • xy值代表在屏幕空间中的像素坐标
    • x∈[0.5,400.5]
    • y∈[0.5,300.5]
    • z∈[0,1],近平面z为0,远平面z为1
    • 透视:w∈[1/Near,1/Far](正交:w=1。这些值是通过投影矩阵变换后的w分量取倒数得到的)

原点在(0.5,0.5),即像素中心对应的是浮点值

视口坐标:屏幕空间除以屏幕分辨率得到的。左下角(0,0),右上角(1,1)

  • 例子.求视口空间坐标

《Shader入门精要》基础篇笔记 - 图70

法2.ComputeScreenPos函数

原理:

《Shader入门精要》基础篇笔记 - 图71

ComputeScreenPos内部实现

《Shader入门精要》基础篇笔记 - 图72

核心思想:手动模拟屏幕映射,得到未经齐次除法的视口坐标

例子.

《Shader入门精要》基础篇笔记 - 图73

不能在顶点着色器中先做齐次除法,具体详见P92,但是写得也不是很明白,只能隐约感觉到其中的奥妙

解释如下:

  • 总之,不能在投影空间(齐次裁剪空间)中直接进行插值,因为这并不是一个线性的空间,而插值往往是线性的