一轮整理: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.渲染流水线
2.1 应用阶段
- 输入:无
- 执行者:CPU
- 输出结果:渲染图元(点、线三角面等)
过程:
- 把数据加载到显存中:硬盘 —> 内存 —> 显存
- 内存数据加载到显存后,可以移除。但对于一些数据,如用于碰撞检测的网格数据,则不希望移除
- 场景数据:摄像机、视锥体、场景中包含模型、光源信息等。粗粒度剔除,把那些不可见的物体剔除出去。
- 设置渲染状态:定义场景中网格是怎样被渲染的
- 简单理解为Unity给Material绑定Shader,给模型绑定Material并设置参数
- 例如,指定顶点/片元着色器,光源属性,材质等
- 调用DrawCall
- 由CPU发给GPU的命令,指向一个需要被渲染的图元(primitives)列表。给定一个DrawCall,GPU会根据渲染状态(如材质、纹理、着色器等)和所有输入的顶点数据进行计算,最终输出成屏幕上显示的像素。该计算过程,就是GPU流水线
2.2 几何阶段
- 输入:CPU
- 执行者:GPU
- 输出结果:屏幕空间的顶点信息
过程:
- 顶点着色器:完全可编程控制,必须实现
- 用于实现顶点的空间变换、顶点着色等
- 坐标变换:对顶点的坐标(即位置)进行某种变换。必须完成:把顶点从模型空间转换到齐次裁剪空间。
- 曲面细分着色器:完全可编程控制,可选
- 细分图元
- 几何着色器:完全可编程控制,可选
- 用于执行逐图元的着色操作或产生更多图元
- 裁剪:可配置,不可编程
- 裁剪不在相机范围内的顶点,并剔除三角图元的面片
- 屏幕映射:GPU固定实现
- 负责把每个图元的坐标转换到屏幕坐标系中
2.3 光栅化阶段
- 输入:屏幕坐标系下的顶点位置以及和它们相关的额外信息,如深度值(z坐标)、法线方向、视角方向等
- 执行者:GPU
- 输出结果:计算每个图元覆盖了哪些像素,以及为这些像素计算它们的颜色
过程:
- 三角形设置:GPU固定实现
- 计算光栅化一个三角形网格所需的信息
- 上一阶段输出的都是三角网格的顶点,即每条边的两个端点。我们需要计算每条边上的像素坐标,得到整个三角网格对像素的覆盖情况。
- 计算三角网格表示数据的过程就叫做三角形设置
- 三角形遍历:GPU固定实现
- 片元:像素被一个三角网格覆盖,就会生成一个片元(fragment)。一个片元并不是真正意义上的像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色(利用上一阶段的三角网格的3个顶点信息对整个覆盖区域的像素进行插值)。这些状态包括(但不限于)它的屏幕坐标、深度信息,以及其他从几何阶段输出的顶点颜色,例如法线、纹理坐标等
- 找到哪些像素被三角网格覆盖的过程就是三角形遍历,这个阶段也叫做扫描变换(Scan Conversion)
片元着色器:完全可编程控制,可选
- 输入的是上一个阶段对顶点信息插值得到的结果,输出的是一个或多个颜色值。这一阶段可以完成很多重要的渲染技术,如纹理采样
解释: 上一阶段不会影响屏幕上每个像素的颜色值,而是会产生一系列的数据,用来表述一个三角网格是怎样覆盖每个像素的。一句话说,每个片元只负责存储这样一系列数据,真正会对像素产生影响的是下一阶段,逐片元操作
- 输入的是上一个阶段对顶点信息插值得到的结果,输出的是一个或多个颜色值。这一阶段可以完成很多重要的渲染技术,如纹理采样
逐片元操作:可配置,不可编程
- 很多重要操作,如修改颜色、深度缓冲、混合等
- 逐片元操作阶段所做的操作。只有通过了所有的测试后,新生成的片元才能和颜色缓冲区中已经存在的像素颜色进行混合,最后再写入颜色缓冲区中
区别:
1.模板测试和写入一起开和关;深度测试和写入可分别开启
2.模板测试未通过也可更新值;深度测试没通过不可更新值
源颜色:片元着色器的颜色值
目标颜色:已经存在于颜色缓冲区中颜色值
补充:
- 屏幕显示的是颜色缓冲区中的颜色值。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
把材质(Material)想象成组件,把UnityShader想象成源文件。
- 组件的目的:个性化
- 源文件的目的:通用性
- ShaderLab:为Unity开发者提供的高层级的渲染抽象层
3.2 Shader结构:名称
- 使用”/“控制位置
3.3 Shader结构:Properties
- 材质和UnityShader的桥梁,通过材质面板传递Shader属性
解释:
- Name:Shader访问的名字
- display name:材质面板上的名字
- Propertype:类型
- DefaultValue:默认值
Properties属性!!!
说明:
- 2D、Cube、3D的内置纹理默认值包括:white、black、gray、bump
例子
- 为了在Shader中可以访问到这些属性,我们需要在Cg代码片中定义和这些属性类型相匹配的变量。
- 需要说明的是,即使我们不在Properties语义块中声明这些属性,也可以直接在Cg代码片中定义变量。此时,我们可以通过过脚本向Shader中传递这些属性
3.4 Shader结构:SubShader
- 每个UnityShader可以包含多个SubShader语义块(至少一个),加载时Unity选择第一个可在目标平台运行的SubShader。如果都不支持,使用Fallback
解释:不同显卡具有不同的能力,写SubShader就是为了在旧显卡使用计算复杂度较低的着色器,在高级显卡使用计算复杂度较高的着色器,以便提供更出色的画面
重要组成
- SubShader:定义了一系列Pass以及可选的状态([RenderSetup])和标签([Tags])设置
- Pass:定义了一次完整的渲染流程
语法:
状态设置!!!
- SubShader和Pass语法相同
SubShader专属标签!!!
概念:SubShader标签是一个键值对,它的键和值都是字符串类型,用来告诉Unity渲染引擎:我希望怎样以及何时渲染这个对象
语法:
类型:
Pass语义块
语法:
Pass名称
- Name “MyPassName”
- 通过Pass名称,我们可以使用ShaderLab的UsePass命令来直接使用其他Unity Shader中的Pass
- 例如:UsePass “MyShader/MYPASSNAME”
Unity内部会把所有Pass名称转换成大写字母
Pass专属标签!!!
概念:Pass标签也是告诉渲染引擎我们希望怎样来渲染物体
语法:
类型:
特殊的Pass
- UsePass:复用其他UnityShader中的Pass
- GrabPass:抓取屏幕并将结果存储在一张纹理中,以用于后续的Pass处理
3.5 Shader结构:Fallback
- 所有SubShader都不能运行,使用该Shader
- Fallback还会影响阴影的投射,在渲染阴影纹理中,Unity会在每个UnityShader中寻找一个阴影投射的Pass。Fallback使用的内置Shader中包含了这样一个通用的Pass
语法:
例子:Fallback "VertexLit"
其他语义:有时间自行完善
- 扩展编辑界面:CustomEditor
- 命令分组:Category
3.6 着色器分类
表面着色器
Surface Shader
概念:Unity自己创造的一种着色器代码类型,是对顶点/片元着色器更高层的抽象,本质仍是顶点/片元着色器,定义在SubShader块(而非Pass块)中的CGPROGRAM和ENDCG之间。可以使用Cg/HLSL几乎所有语法。
顶点/片元着色器
Vertex/Fragment Shader
概念:本书使用。使用Cg/HLSL语言来编写,更复杂但更灵活。定义在Pass内
3.7 课后答疑
UnityShader != 真正的Shader
Ch4.学习Unity所需的数学基础
4.1 笛卡尔坐标系
二维笛卡尔坐标系:
三维笛卡尔坐标系:
- 基矢量:3个坐标轴
- 标准正交基:3个坐标轴之间互相垂直,且长度为1
- 正交基:坐标轴之间互相垂直但长度不为1
判断坐标系:哪只手满足就是哪个坐标系
- 大拇指:+x
- 食指:+y
- 中指:+z
判断旋转正方向:伸出坐标系对应的手,大拇指对准旋转轴正方向,握拳方向就是旋转正方向(左手顺时针,右手逆时针)
4.2 点和矢量
点:n维空间中的一个位置,没有大小、宽度的概念
矢量(向量):n维空间中一种包含了模和方向的有向线段
标量:只有模,没有方向
规定:
矢量运算:
- 矢量和标量乘法/除法:
- 矢量的加法和减法:
- 矢量的模:
- 单位矢量:
- 点积
- 公式1:
- 公式2(平行四边形面积):
- 公式1:
- 叉积
- 公式:
- 模:
- 方向:确定坐标系,根据左(右)手法则,从a到b握拳,拇指方向为叉积结果方向
- 公式:
4.3 矩阵
定义
- mxn个标量组成的长方形数组
行向量和列向量:
矩阵运算
- 矩阵和标量相乘:
- 矩阵和矩阵相乘:
矩阵乘法性质
- 矩阵乘法不满足交换律:
- 矩阵乘法满足结合律:
特殊矩阵
1.方块矩阵:简称方阵,行和列数目相等
- 对角元素:行和列号相等的元素。如m11
- 对角矩阵:除对角元素外均为0的矩阵。非常适合次方计算。
2.单位矩阵:任何矩阵和它相乘的结果还是原来的矩阵
3.转置矩阵:实际是对原矩阵的一种运算。行列对调
- 性质一:
- 性质二:
4.逆矩阵:满足
逆矩阵判断:
- 必须是方阵
- 行列式不为0
逆矩阵性质:
- 逆矩阵的逆矩阵是原矩阵本身(设M可逆)
- 单位矩阵的逆矩阵是它本身
- 转置矩阵的逆矩阵是逆矩阵的转置
- 矩阵串接相乘后的逆矩阵等于反向串接各个矩阵的逆矩阵。可推广到多维
5.正交矩阵
- 如果一个方阵M和它的转置矩阵的乘积是单位矩阵的话,我们就说这个矩阵是正交的
等价于
推论
- c1 -
:表示一个三维向量c1的行展开式
4.4 变换
变换:指的是我们把一些数据,如点、方向矢量甚至颜色等,经过某种方式进行转换的过程
线性变换:指可以保留矢量加和标量乘的变换
仿射变换:合并线性变换和平移变换。仿射变换可以使用一个4x4的矩阵表示,为此,我们需要把矢量扩展到四维空间下,就是齐次坐标空间
齐次坐标:四维向量
- 点:w=1
- 向量:w=0
1.平移矩阵
2.缩放矩阵
3.旋转矩阵(正交矩阵)
无论坐标轴如何变换,两两之间总是互相垂直,且长度为1,满足上述推论。故旋转矩阵是正交矩阵
- 绕x轴旋转θ
- 绕y轴旋转θ
- 绕z轴旋转θ
约定:先缩放,再旋转,最后平移
4.5 坐标空间变换
- 变谁就按列展开乘谁,详见P69
- 第一个”谁”:目标坐标空间,即变换到的空间
- 第二个”谁”:自己的坐标轴在目标空间下的表示
如果矩阵正交,那么只需转置即可得到逆变换
已知子坐标空间C的3个坐标轴在父坐标空间P下的表示Xc,Yc,Zc,以及原点位置Oc。给定一个子坐标空间一点Ac=(a,b,c),确定其在父空间下的位置Ap。
个人心得
记录于:2021.10.27
根据看的角度不同,分为两种变换类型
- 坐标变换:我们坐标系下的点经过一个矩阵变换到我们坐标系下另一个点
- 基变换:它们坐标系下的点,在我们坐标系下的表示
举例说明:以二维旋转变换为例
顶点的坐标变换过程
难点:投影矩阵推导
并非真正从3D投影到2D,而是投影到NDC (Normalized Device Coordinates) 中,但是投影变换不会进行齐次除法,所以结果并非真正意义上的点,而是一个待处理的四维向量(w分量非0非1,而是和z有关)。所以四维向量需经过齐次除法,才算真正成为NDC中的点。
个人小结:
- 观察空间 - 投影变换 -> 裁剪空间(手动实现)
- 裁剪空间 - 齐次除法 -> NDC空间(底层实现)
- NDC空间 - 屏幕映射 -> 屏幕空间(底层实现)
NDC和裁剪空间并非同一空间!
正交投影矩阵
透视投影矩阵
4.6 法线变换
详见P86
4.7 Shader内置变量!!!
变换矩阵
摄像机和屏幕参数
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);
注意:脚本的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)
- 例子.求视口空间坐标
法2.ComputeScreenPos函数
原理:
ComputeScreenPos内部实现
核心思想:手动模拟屏幕映射,得到未经齐次除法的视口坐标
例子.
不能在顶点着色器中先做齐次除法,具体详见P92,但是写得也不是很明白,只能隐约感觉到其中的奥妙
解释如下:
- 总之,不能在投影空间(齐次裁剪空间)中直接进行插值,因为这并不是一个线性的空间,而插值往往是线性的