图形学3D渲染管线
DX和OpenGL左右手坐标系不同,会有一些差距,得出的矩阵会不一样;
OpenGL的投影平面不是视景体的近截面;
顶点(vertexs)
顶点坐标,颜色,法线,纹理坐标(UV),连线索引;
图元(primitives)
几何顶点被组合为图元(点,线段或多边形),图元装配;
片元(fragments)
图元被分几步转换为片元:图元被适当的裁剪,颜色和纹理数据也相应作出必要的调整,相关的坐标被转换为窗口坐标。最后,光栅化将裁剪好的图元转换为片元;
一、顶点数据(Vertex)
顶点坐标,颜色,法线,纹理坐标(UV),连线索引(三角形连线顺序-左右手坐标系顺序 不同);
顶点数据在流水管线中以图元处理:
点(GL_POINTS)、线(GL_LINES)、线条(GL_LINE_STRIP)、三角面(GL_TRIANGLES);
四边形顶点信息:顶点位置、颜色、UV、四个顶点顺序;
索引可以避免共享顶点间数据的多余——顶点缓存对象(Vertex Buffer Object,VBO)。
二、顶点着色器(Vertex Shader)
1.矩阵变换
通过矩阵变换将世界坐标系下的顶点变换到视口坐标系下显示;
齐次变换:将一个原本是n维的向量用一个n+1维向量来表示(是不是很像投影);
互为逆矩阵:相乘结果为1;
转置矩阵:行列互换;
正交矩阵:行列向量互相垂直,正交矩阵的逆矩阵就是他的转置矩阵;
1)模型矩阵到世界矩阵
Local Coordinate——World Coordinate;
顶点在世界坐标系下可以进行平移(Translation),旋转(Rotation),缩放(Scaling)操作;
按顺序矩阵连乘得到世界矩阵下顶点的坐标;
如果进行缩放操作要考虑法线变换(非均匀缩放-逆矩阵的转置矩阵获得变换法线-切线空间);
OpenGL Normal Vector Transformation
2)世界矩阵到摄像机矩阵
World Coordinate——View Coordinate;
摄像机矩阵也叫观察矩阵;
摄像机的一些参数:
EyePosition:摄像机位置
FocusPosition:观察目标点(at)
UpDirection:摄像机上方向(不是Y,相对世界)
Near:近截面、Far:远截面
FOV(vertical field of view):垂直视场角
Aspect Ratio :屏幕纵横比
以上参数定义了视景体(台体);
顶点坐标转换到摄像机矩阵逆向思维,理解为将当前顶点平移摄像机当前位置向量的反向量;
摄像机*摄像机变换矩阵肯定会得到一个单位矩阵(坐标系都是单位矩阵);
摄像机当前的x,y,z轴组成的矩阵和摄像机变换矩阵互为逆矩阵;
通过摄像机参数求得摄像机的xyz轴向量;
由于正交矩阵的逆矩阵就是他的转置矩阵,再添加单位1;
将摄像机变换矩阵和平移矩阵相乘得到摄像机矩阵;
以上为左手坐标系下的摄像机矩阵;
3)摄像机矩阵到投影矩阵
View Coordinate——Projection Coordinate;
正交投影和透视投影;
在Dx中近截面就是投影平面;
正交投影每个坐标点只是丢弃了z坐标,就不推导了;
透视投影:通过相似三角形求出视景体内顶点x,y坐标在投影平面的相对位置,保留z坐标;
D3DXMATRIX matProject;
// 这个函数是设置正交投影矩阵
D3DXMatrixOrthoLH(&matProject, width, height, Znear, Zfar);
pD3dDevice->SetTransform(D3DTS_PROJECTION, &matProject);
OpenGL投影矩阵
从右到左将以上矩阵连乘获得局部空间到裁剪空间的变换矩阵:
V(clip) = M(projection)*M(view)*M(model)*V(local)
2.着色计算
Flat Shading——一个顶点代表三角形颜色,默认索引中第一个顶点颜色;
Gouraud Shading——逐顶点着色,只计算三个顶点关照信息,在光栅化阶段做插值得到各个片段光照信息,缺点:插值导致高光非线性;
Phong Shading——冯氏光照,逐片段着色(像素)法线插值出各个片元的法线信息,在片元着色器中使用法线、UV、位置等计算光照;
三、曲面细分着色器(Tessellation Shader)
可选阶段;
外壳着色器(Hull Shader)、镶嵌器(Tessellation)、域着色器(Domain Shader)
不创建高模,根据与摄像机距离使用镶嵌器细化模型,添加三角面(LOD);
四、几何着色器(Geometry Shader)
可选阶段;
以图元作为输入数据,可以创建销毁几何图元;
法线可视化——毛发效果;
根据距离摄像机远景动态调整多边形边数实现LOD效果;
公告牌技术(BillBoards)——3D图片替代模型,总是朝向摄像机;
五、图元组装(Primitive Assembly)
1.裁剪(Clipping)
1)线段裁剪
点面关系,线面关系,通过向量在面上的投影,以及点是否在矩形内;
八方位裁剪法;
2)三角形裁剪
平顶三角和平底三角形,判断出界平移顶点;
裁剪算法有Cohen-Sutherland算法、Liang-Barsky算法和Sutherland-Hodgman多边形裁剪算法
2.背面剔除(Back-Face Culling)
只和三角形与摄像机的距离有关,不依赖摄像机朝向;
根据三角形顶点顺序,使用左右手坐标系,叉乘求出法向量;
顶点顺时针法向量朝外剔除,逆时针朝内剔除;
3屏幕映射 (Screen Mapping)
1)透视剔除(Perspective Division)
将视景体空间顶点,除以w分量获得标准设备空间,-1到1的立方体空间(CVV-标准视体);
w分量保留的z坐标的信息,正交投影w分量为1,w分量也就是z值代表了深度信息;
OpenGL变换和OpenGL投影矩阵;
2)视口变换(View Transform)
通过执行透射除法获得归一化的设备坐标NDC(Normalized Device Coordinate);
z值在视口变换过程中被映射,OpenGL映射到[-1,1],DirectX[0,1];
NDC坐标映射到窗口坐标要通过平移和缩放过程(相机的宽高比);
视口矩阵:xy为窗口相对于屏幕的位置;
六、光栅化(Rasterization)
将图元离散为片元的过程;
图元覆盖一个片元(Overlap)——点采样(Point Sampling),像素中间点在三角形内;
超级采样和多重采样技术,涉及到抗锯齿;
1.三角形组装(Triangle Setup)
三角形组装会对顶点的输入数据(比如,颜色、法线、纹理坐标)进行插值,得到各个片段对应的数据值,为后面的片元着色器提供片元数据;
2.三角形遍历(Triangle Traversal)
遍历这些三角图元覆盖了哪些片元的采样点,随后得到该图元所对应的片元;
顶点数据插值获得片元的颜色,法线,UV,深度等信息,用于投射正交插值获得正确的透视颜色纹理等信息;
3.线段的扫描转化
-
Bresenham光栅化算法
-
数字微分画线算法DDA (Digital Differential Analyzer)
直线公式,斜率小于1,y增量为整数,大于1,x增量为整数;
4.多边形填充
- 扫描线填充算法(Scanline Filling)
- 重心计算多边形片元颜色
求多边形的几何重心,以重心为顶点,向多变的各个顶点连线,分割为三角形;
重心颜色:所有三角形顶点颜色的平均值;
计算每个三角形的重心(三个顶点的x,y分别相加除以3);
将三角形重心坐标和面积关联起来;
sx += curx*curs;
sy += cury*curs;
重心的坐标为:tatal.s总面积
center.x = sx/total.s;
center.y = sy/total.s;
七、片元着色器(Fregment Shader)
色彩混合:颜色的RGB值相加,用于混合所有光照效果
色彩调制:色彩乘法,RGB值分别相乘,相当于给RGB值都乘以一个系数,将颜色变亮或变暗;
1.Phong光照模型
1)环境光
用来模拟全局光照效果;在物体光照信息基础上叠加较小的光照常量;
2)漫反射
光线进入物体内部重新散射出来,看做均匀分部所以和观察者位置无关;只影响亮度;
-
兰伯特余弦定律(Lambert Consine Law)
取决表面法线和光线的夹角,夹角越大分量越小,漫反射越小;90度漫反射几乎为0;
-
半兰伯特模型(Half Lambert)
v社做半条命时提出,改变物体暗区域的光照信息;
将漫反射系数从[0,1]改为[0.5,1],提高暗部的亮度信息;
//法线和光线的夹角, float diflight = dot(s.Normal,lightDir); float hLambert = difLight * 0.5 + 0.5;
3)镜面反射高光
和观察者的位置有关,不同角度观察结果不同;
- Phong光照模型
光线反射向量和观察向量的夹角;
高光指数:e
在夹角大于90度的情况,会造成高光丢失现象,光线会不连续,有明显的明暗分界线;
- Blinn-Phong光照模型
光线向量和观察向量的中间位置(半角向量)和法线的夹角;
在任何角度观察,夹角都不会大于90度;不会出现高光不连续现象;
-
材质
struct Material { vector3 ambient; //环境光 vector3 diffuse; //漫反射 vector3 Specular; //镜面反射 vector3 emissive; //自发光 float e; //镜面反射系数 }
物体最终颜色 = 环境光结果*环境光反射系数 + 漫反射结果*漫反射系数+镜面反射结果(计算了高光指数)*镜面反射系数+材质自发光颜色*自放光系数;
2.纹理贴图 (Textures)
纹理映射,将图像信息映射到三角形网格;
凹凸贴图(bump mapping)、法线贴图(normal mapping)、高度纹理(height mapping)、视差贴图(parallax mapping)、位移贴图(displacement mapping)、立方体贴图(cubemap)、阴影贴图(shadowmap);
- UV坐标的寻址方式
归一化到[0,1]之间,像素大小为2的次方,方便计算处理;
寻址方式也叫平铺方式:重复寻址(repeat)、边缘钳制寻址(clamp)和镜像寻址(mirror);
uv超出0-1,该如何寻址(就是图片的平铺,边缘像素扩展,镜像);
- 纹理采样方式
纹理像素和图元像素不是一一对应,要用到纹理的滤波方式;
点过滤(point)、线性过滤(linear)、最近领点过滤(nearest neighbor point)和双线性过滤(bilinear),Unity的Trilinear滤波的技术;
- 法线贴图 (Normal Mapping)
物体空间(object space)和切线空间(tangent space)
根据物体空间计算的法线,在物体旋转移动后回得到错误的光照信息;
切线空间:相对于顶点坐标存储计算;
顶点本身法线为N轴,模型给定定义一条和该顶点相切的切线T轴,N和T叉乘得到B轴;
法线(N)、切线(T)和副切线(B) ,三个轴组成切线空间;
法线贴图就是在切线空间中记录了法线扰动的方向;
以顶点法线N为z轴坐标系,扰动后的法线z轴也总是朝向(0,0,1),所以得得到法线贴图总是淡蓝色(RGB);
法线纹理最终值需要做个映射,由于维度向量取值[-1,1],纹理通道范围在[0,1],最终记录结果为:(normal+1)/2;
切线空间坐标系到世界坐标系的转换矩阵
物体移动旋转时,法线乘以这个矩阵就可以得到改变后的法线;
因为z总是朝向(0,0,1)纹理就可以直接记录xy——纹理压缩:DXT1,DXT5;
3.锯齿和抗锯齿 (Aliasing and Anti-aliasing)
-
超级采样抗锯齿 (Super-Sampling Anti-aliasing——SSAA)
将原图分辨率放大一倍,再采样;光栅化和片元着色都是原来的4倍,渲染缓存也是4倍;
-
多重采样抗锯齿 (Multi-Sampling Anti-aliasing——MSAA)
每个片元有多个采样点,计算采样点的覆盖率(Coverage),光栅化阶段计算采样点覆盖率,在片元着色器计算颜色值后乘以这个覆盖率;
MSAA和延迟渲染(deferred render)不兼容(延迟渲染需要Geometry 和Lighting两个Pass,lighting阶段无法通过GBuffer获得片元覆盖率);
4.阴影 (Shadows)
光照烘焙获得Shadowmap;先光照烘焙获得深度信息,再通过阴影贴图判断那些片元落在阴影中;
Shadowmap的精度会导致阴影粉刺,需要便宜深度来消除粉刺现象;
阴影锯齿通过百分比渐进过滤(PCF)实现软化阴影(softshadow);
八、测试和混合(Tests & Blending)
1.裁切测试 (Scissor Test)
裁切测试可以避免当视口比屏幕窗口小时造成的渲染浪费问题;一般默认不开启,
2.Alpha测试 (Alpha Test)
片段着色器中丢弃alpha值小于0.1的片段;
-
Early-Z Culling
硬件厂商用来加速渲染的手段;在片元着色之前提出被遮挡的片元;
但是生效要求只能通过光栅化插值得到深度,不能再片元着色器阶段去修改深度缓冲;
3.模板测试 (Stencil Test)
模板测试有一个对应的缓存, 即模板缓存(Stencil Buffer), 用于记录所有像素的模板值, 默认值为0;
片元携带的参考值和模板缓存中的值比较,满足比较函数,调用操作函数更新模板值;
-
模板值: 模板缓存中已经存在的值
-
参考值: 在渲染该物体前, 由程序设置的指定值
-
比较函数: 决定如何将两个值作比较的函数
-
操作函数: 定义通过或者不通过测试后对模板值的更新操作
Unity中模板测试不可编程,可配置管线阶段;
Stencil
{
Ref refValue //参考值
Comp always //比较函数
Pass keep //模板测试和深度测试都通过后的操作
Fail keep //模板测试和深度测试都未通过后的操作
ZFail keep //模板测试通过而深度测试未通过后的操作
WriteMask 255 //使用参考值更新模板值之前, 在模板值与掩码按位与之后再更新 255代表不做处理
ReadMask 255 //读取模板值后, 将其与掩码按位与之后再与参考值作比较 255代表不做处理
}
/*
UnityEngine.Rendering.CompareFunction比较函数枚举
0(Disabled): 关闭模板测试, 等同于全部通过测试, 经过测试发现不是真的关闭.
1(Never): 全部不能通过测试
2(Less): 待比较的值小于缓存中的值时通过测试
3(Equal): 待比较的值等于缓存中的值时通过测试
4(LessEqual): 待比较的值小于等于缓存中的值时通过测试
5(Greater): 待比较的值大于缓存中的值时通过测试
6(NotEqual): 待比较的值不等于缓存中的值时通过测试
7(GreaterEqual): 待比较的值大于等于缓存中的值时通过测试
8(Always): 全部通过测试, 默认值
*/
/*
UnityEngine.Rendering.StencilOp操作函数枚举
0(Keep): 保持模板缓存中的值不变
1(Zero): 将模板缓存中的值置为0
2(Replace): 使用参考值替换模板缓存中的值
3(IncrementSaturate): 使模板缓冲区值增大, 最大限制为可表示的最大无符号值
4(DecrementSaturate): 使模板缓冲区值减小, 最小限制为0
5(Invert): 对模板缓冲区值按位求反
6(IncrementWrap): 与IncrementSaturate类似, 只是达到最大后继续增大将重新设置为 0
7(DecrementWrap): 与DecrementSaturate类似, 只是达到最小后继续减小将重新设置为可表示的最大无符号值
*/
4.深度测试 (Depth Test)
比较当前片段的深度值是否比深度缓冲中预设的值小(默认比较方式),如果是更新深度缓冲和颜色缓冲;否则丢弃片段不更新缓冲区的值;
Early-Z Culling也是利用Z-Buffer的技术来进行深度测试的,只不过该测试是在片段着色器之前进行;
深度测试是可配置的阶段——ZTest和ZWrite;
-
Z-Fighting
深度缓冲精度不够,深度值相近的片元会造成重叠模糊问题:
解决:物体不要靠太近;使用高精度的深度缓冲;
- 隐藏面消除 (Hidden Surface Removal, HSR)
前面的图元组装裁剪,背面拣选,和Z-Buff都属于HSR,裁剪针对图元,Z-Buffer针对像素点;目的都是为了减少到达片元着色器的片元个数,提高渲染性能;
1)视椎体剔除 (Viewing-Frustum Culling)
利用物体包围盒来做交差检测,常见的包围盒有轴对齐包围盒(AABB)和有向包围盒(OBB)两种;
需要是由高效数据结构来提升碰撞检测的效率——八叉树(OcTree)、二分空间划分(Binary Space Partitioning)、四叉树(Quad Tree)、场景图(Scene Graphs)、kd树(K-Dimensional Tree)和层次包围(Bounding Volume Hierarchies);
计算:
点法式判断三维空间点和面的关系,裁剪掉不在视景体内的包围盒顶点;
求视景体六个面:
通过投影矩阵,求出投影和摄像机的逆矩阵,反向求出视景体对应面;
求逆矩阵——行列式,代数余子式,伴随矩阵,行列式的值,有固定公式;
近截面裁剪:
三角形和面的关系,用向量表示三角形和面;两条共起点的向量可以表示三角形;分解成线和面的关系;
三角形和近截面的关系:
1个角在视景体内——偏移另外两个顶点到视景体平面上;
2个角在视景体内——三角形拆分为2个三角形,注意三角形顶点连线顺序;
2)入口剔除 (Portal Culling)
将室内的门或者窗户看做视椎体来进行裁剪;实现"笼中窥梦"的效果;
3)遮挡剔除 (Occlusion Culling)
通过离线烘焙的犯法来预先计算出潜在可视集合(Potentially Visible Set,PVS);
PVS记录了每个地形块(Tiles)可能看到的物体的集合,用于运行时查找计算;
场景划分为小地形块,每个块上随机取N个采样点;
从采样点发射射线获取场景中和射线相交的物体,记录物体ID;
根据摄像机位置,使用采样点几率的物体id列表进行渲染;
采样精度和烘焙效率问题;
6.Alpha混合 (Alpha Blending)
经过前面所有的测试才能进入Alpha混合阶段,一个测试过不了都到不了Alpha混合;
Alpha混合实现物体半透明效果,渲染顺利非常重要,可能需要手动改Queue(渲染队列)的值;
- 渲染顺序:
先渲染不透明物体,从前往后渲染;——不透明物体渲染前先进行深度检测,先后近的物体,远处物体通不过深度检测,就不用进行深度写入操作;
再渲染半透明物体,从后往前渲染;——半透明物体渲染需要知道前一层的颜色信息进行混合,先渲染远处物体,在画近处物体时可以通过颜色缓存获取前一层颜色信息;
画家算法:先画物体会被后画物体覆盖;
Unity ShaderLab中渲染队列设置;
Queue 其他预定义的值为:Background = 1000 , AlphaTest = 2450,Overlay = 4000。默认值是Geometry =2000;
ShaderLab默认开启深度测试和深度写入;
//将本 Shader 计算出的颜色值(源颜色值,即蓝色) * 源Alpha值(0.6) + 目标颜色值(可以理解为背景色) * (1-0.6)
Blend SrcAlpha OneMinusSrcAlpha
// Transparent (透明) = 3000,值越小越先渲染,而后渲染( Queue 值大)的物体会覆盖先渲染的物体
Tags {"Queue" = "Transparent"}
ZTest LEqual //小于等于
ZWrite On //打开
//ZTest 可取值为:Greater , GEqual , Less , LEqual , Equal , NotEqual , Always , Never , Off,默认是 LEqual,ZTest Off 等同于 ZTest Always
//ZWrite 可取值为:On , Off,默认是 On
- 顺序无关半透明算法(Order-independent transparency,OIT)
深度剥离(Depth Peeling)
双向剥离(Dual Depth Peeling)——两个方向剥离,一个从前往后,一个从后往前,两个方向效率提高;