Unity Toon Shader I
理论知识篇
非常基础的内容了,不过该课程会用到的知识还是做一些记录(相关知识的具体内容参见《Unity shader入门精要》)
一、渲染流水线
1.Draw Call
- Draw Call指CPU图形编程接口,如DirectX或OpenGL来命令GPU执行渲染操作
- CPU和GPU可以并行工作,这是因为存在一个命令缓冲区(Command Buffer)
命令缓冲区包含了一个命令队列,由CPU向其中添加命令,而由GPU从中读取命令。添加和读取的过程是相互独立的,因此命令缓冲区可以使CPU和GPU相互独立工作。当CPU需要渲染一些对象时,它可以向命令缓冲区添加命令,而GPU完成了上一次的渲染任务后,它就可以从命令队列里取出一个命令并执行它。命令缓冲区中的命令有很多种类,而Draw Call是其中的一种,其它命令还有改变渲染状态等命令(改变使用的Shader,使用不同的纹理等) - CPU通过图形API向GPU发送数据、状态、命令等,但GPU渲染效率往往比CPU提交命令效率高,因此Draw Call太多会导致CPU花费大量时间提交命令,造成CPU过载
- 如何解决这个问题:为了减少CPU准备Draw Call花费的时间,我们应当尽可能合并Draw Call,比如使用相同的材质贴图,在内存中合并网格等等
2.几何阶段
- 几何阶段是指:顶点着色器->细分曲面着色器->几何着色器->裁剪->屏幕映射(GPU中完成)
- 顶点着色器:顶点数据包括顶点位置,法线,切线,纹理坐标,顶点颜色等,负责对顶点坐标进行空间变换和数据输出,必须完成的任务是把顶点坐标从模型空间转换到裁剪空间
- 曲面细分着色器(非必要):当需要高精度的模型来表现细节时,过高的模型面数在每一帧都会给CPU带来负担。曲面细分着色器通过线性插值插入新的顶点,生成更多的面,使得即使是低模也能得到不错的圆滑效果(适用于DX11以上API)
- 几何着色器(非必要):增加或是销毁图元,改变模型原有形状,可以用于爆炸效果、法线可视化、毛发效果、公告板等
- 裁剪:该过程在齐次裁剪空间中完成
- 屏幕映射:将图元坐标转换到屏幕坐标系
3.光栅化阶段
- 光栅化阶段是指:三角形设置->三角形遍历->片元着色器->逐片元操作(将几何数据通过一系列变换转换为像素并呈现在显示设备上)
- 三角形设置:计算三角形每条边上的像素坐标为下一阶段做准备
- 三角形遍历:找到哪些像素会被三角形所覆盖,对应的像素会生成一个片元这个片元包含了屏幕坐标、深度信息、纹理坐标、法线等等,是个状态集合(片元并不是最终显示在屏幕上的像素内容)
- 片元着色器:也叫像素着色器,也是可选着色器,输出一个或多个颜色,最重要的是对纹理进行采样
- 逐片元操作:经过一系列测试决定片元可见性以及如何混合
- 逐片元操作中:模板测试->深度测试->混合(根据不同API实现细节不同)
- 模板测试:可以用来对多重阴影绘制的去重,限制渲染区域
- 深度测试:判断遮挡关系
二、坐标空间
1.模型空间
- 模型空间(model space)也称对象空间,是一个局部空间,当模型移动或旋转的时候,模型空间也跟着移动或旋转
- 模型的顶点包含了顶点的位置坐标,这个坐标是相对于模型空间的原点定义的,模型空间的坐标原点和坐标轴在建模软件中设置,在没有手动调整的情况下一般是在模型的重心位置
- 模型空间中,我们使用方向的概念:前后左右上下
- 左手坐标系
2.世界空间
- 世界空间(world space):游戏中的场景角色都位于这个游戏空间中,光照模型的很多计算都在世界空间中进行
- 左手坐标系
3.观察空间
- 观察空间(view space)也称为相机空间,这个空间以摄像机作为原点,相机视角正方向指向z轴负方向
- 右手坐标系
4.裁剪空间
- 裁剪空间(clip space)也称为齐次裁剪空间,用于这个变换的矩阵叫做裁剪矩阵或投影矩阵(projection matrix)
- 利用视锥体对不可见的图元进行剔除,摄像机看不到的地方不渲染
- 不同视锥体对应不同的投影类型:
- 正交投影:立方体
- 透视投影:棱台
- 左手坐标系
5.屏幕空间
- 屏幕空间(screen space):裁剪完成后需要把视锥体的图元信息投影到二维屏幕空间中
- 左手坐标系
三、Shader基础
1.unity shader基本结构
包含properties、subshader、fallback等语义块,其中最重要的是pass语义块
Shader "Path/Name" //路径、名称 { Properties{} //属性 SubShader //子着色器 { pass{} pass{} ... } Fallback "Diffuse" //回滚 }
Properties:变量名(显示名称、类型)以及默认值,设定好后会显示在Unity面板上
SubShader:一个shader中可以有多个subshader以应对不同目标平台,同时subshader至少要有一个
SubShader { [Tags]标签设置[可选] LOD设置[可选] [RenderSetup]渲染状态设置[可选] pass{} pass{} ... }
- [Tags]/标签:用于指定渲染队列,控制物体是否投影,是否接受投影等
- RenderType 渲染类型 用于对着色器进行分类
- Queue 渲染队列 指定物体渲染队列来控制渲染顺序
- background 这个队列最先渲染,它被用于skybox 队列索引号1000
- geometry 这是默认的渲染队列,它用于绝大数对象,不透明几何体使用该队列 队列索引号2000
- alpha test 需要透明测试的物体使用这个队列 队列索引号2450
- transparent 该队列的物体会在geometry和alpha test物体渲染之后,再按从后往前的顺序进行渲染,任何使用了透明度混合的物体使用这个队列 队列索引号3000
- overlay 该队列用于实现一些叠加效果,最后被渲染的对象使用该队列,例如镜头光晕效果 队列索引号4000
- IgnoreProjector 忽略投影 如果值为True表示物体不接受投影
- LOD(Level of Detail)细节级别:当系统设定的最大LOD小于SubShader制定的LOD时,该SubShader不可用
- RenderSetup 渲染状态设置:渲染状态可以写在SubShader里也可以写在Pass里,如果写在SubShader里则该状态用于所有Pass
- Cull Back 剔除背面(默认)
- Cull Front 剔除正面
- ZTest 深度测试
- ZWrite 深度写入
- Blend 混合模式
- Fallback回滚:如果subshader都不可用,则返回这个更低级的shader
- [Tags]/标签:用于指定渲染队列,控制物体是否投影,是否接受投影等
2.CG代码
预处理
#pragma vertex vert //表示名为vert的函数包含了顶点着色器代码 #pragma fragment frag //表示名为frag的函数包含了片元着色器代码 #pragma target 2.0 //shader target等级,等级越高支持功能越多 #include "UnityCG.cginc" //头文件,提供一些内置变量与函数
结构体a2v
struct a2v() { float4 vertex : POSITION; float3 normal: NORMAL; float4 texcoord : TEXCOORD0; };
- a2v(application to vertex shader)把模型数据从应用阶段传递到顶点着色器中
- float4 vertex : POSITION; 用模型空间的顶点位置填充vertex变量
- float3 normal: NORMAL; 用模型空间的法线方向填充normal变量
- float4 texcoord : TEXCOORD0; 用模型的第一套纹理填充texcoord变量,除此之外还有TEXCOORD1(第二组纹理坐标),COLOR(顶点色)、TANGENT(切线)等U3D语义
结构体v2f
struct v2f { float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; fixed3 color : COLOR0; }
- v2f(vertex shader to fragment shader)定义顶点着色器的输出
- float4 pos : SV_POSITION; 将顶点在裁剪空间的信息填充到pos变量(SV前缀是一个系统语义system-value,具有跨平台性)
- float3 worldNormal : TEXCOORD0; TEXCOORD0用于存储世界空间下模型的法线信息,TEXCOORDn相当于寄存器,其中n的数值和shader model的版本有关
vertex shader/fragment shader
v2f vert(a2v v) : SV_POSITION { ... } float4 frag(v2f i) : SV_Target { ... }
- 顶点着色器可以访问模型顶点信息并计算相关信息传递给片元着色器
- 片元着色器吧顶点着色器输出的信息进行计算,光照模型的计算大多在这里进行
- SV_Target是HLSL语义,告诉渲染器将用于输出的颜色存储到一个渲染目标中,比如帧缓存
3.基本数值类型
- CG变量类型
- float 单精度浮点数 32位
- half 半精度浮点数 16位
- fixed 低精度浮点数 11位
- int 整型 32位
- sampler 2D、3D 纹理对象
- float/half/fixed区别:float精度最高,在大多数桌面GPU,会按照最高精度来计算,此时这三者区别不大;在移动平台则不同,不同精度对于运算速度还是有影响的
- 单位矢量、颜色 可以使用fixed
- hdr颜色,短矢量 可以使用half
- 坐标、uv 可以使用float
四、光照与渲染
1.光照模型
- Unity引擎中光源是一个点,没有体积,用L表示光源方向,按类型分为平行光,点光源,聚光灯。
- 辐照度:即单位面积受到的辐射量,是对光的一种量化
- 当光照射到物体表面时,物体对光会发生反射、投射、吸收、衍射、折射、衍射等,其中被物体吸收的部分转化为热,反射、透射的光进入人的视觉系统,使我们能看见物体,为模拟这一现象,我们建立数学模型替代复杂的物理模型,这些模型就称为光照模型。不同的光照模型用于表现不同的材质效果,例如粗糙物体,金属物体,半透体等。
- BRDF经验模型:简洁高效,便于计算,应用广泛,是一种对真实光照的理想化,简单化的光照模型,并不一定满足真实的物理定律
- Lambert漫反射
- half-Lambert漫反射
- Phong模型
- Blinn-Phong模型
- BRDF物理模型:基于物理渲染(PBR,Physically-based rendering)
- Cook-Torrance BRDF模型
- Ward BRDF模型
2.标准光照模型
- 只关心直接光照,光线照射物体经过一次反射后直接进入摄像机
- ambient 环境光
- diffuse 漫反射
- specular 高光反射
- emissive 自发光
- 漫反射:漫反射光照模型就是物体表面随机散射到各个方向的辐射度进行建模。符合兰伯特定律
$$
C_{diffuse} = (C_{light} * M_{diffuse}) max(0,NL)
\\
C_{diffuse} = (C_{light} * M_{diffuse})saturate(NL)
$$- $C_{light}$为光源的颜色和强度
- $M_{diffuse}$材质的漫反射颜色
- N向量代表法向量,L向量代表光线向量(指向光源)
半兰伯特定律
$$
C_{diffuse} = (C_{light} * M_{diffuse})(0.5 * (N*L) + 0.5)
$$高光反射
phone模型的高光反射在已知光源方向,视角方向,表面法线的情况下,还需要求得反射方向
$$
R = 2(NL)N-L
\\
C_{specular} = (C_{light} * M_{specular})max(0,VR)^{M_{gloss}}
$$
- R向量代表反射方向,V向量表示视角方向
- $M_{gloss}$表示材质高光度
- $M_{specular}$表示材质高光颜色
Blinn-phone模型对phone模型进行了简化,为了避免计算反射方向R,引入新的矢量H并归一化
$$
H = \frac{V + L}{|V+L|}
\\
C_{specular} = (C_{light} * M_{specular})max(0,N*H)^{M_{gloss}}
$$
自发光:直接使用材质自发光颜色
$$
C_{emissive} = M_{emissive}
$$环境关:模拟所有间接光照,在这里是一个全局变量
$$
C_{ambient} = M_{ambient}
$$最终颜色由以上几项合成
$$
FinalColor = C_{diffuse} + C_{specular} + C_{emissive} + C_{ambient}
$$
3.渲染顺序
- 深度缓冲:存储每个像素的深度值,通过深度缓冲可以进行深度测试(ZTest),来确定像素的遮挡关系。进行深度测试完成比较之后,距离摄像机更远的片元将会被舍弃,距离摄像机近的部分将会被渲染,此时该片元的颜色被更新到颜色缓冲里,然后它的深度值会被更新到深度缓冲里。深度值更新到深度缓冲的过程,称为深度写入(ZWrite)
- 颜色缓冲:存储每个像素的最终颜色,然后提交由显示硬件显示
- 渲染顺序详细参考《unity shader入门精要》
- 当渲染半透明效果时,先渲染不透明物体,然后在渲染半透明物体
- 当两个物体都是半透明时,先渲染后面的半透明物体
Unity Toon Shader I
https://baifabaiquan.cn/2021/05/27/unity卡通渲染基础1/