#渲染流水线算法
##学习目标:数据结构
##5.1 3D视觉即错觉? 一、从视觉观察效果来看,平行线最终会相交于一点(消失点,又称为灭点),所以咱们能够得出结论:随着深度(z方向)的增长,物体会显得愈来愈小、(dx是左手坐标系,OpenGL是右手坐标系)。函数
二、咱们都知道物体重叠,这是一个重要的概念,即不透明的物体能够遮挡住其后侧物体的局部或总体,它传达了不一样物体在场景中的深度顺序关系工具
三、光照和阴影的处理在刻画3D物体的实体形状和立体感中扮演着相当重要的角色,其中阴影担负着两个重要的任务,即暗示了光源在场景中的相对位置和物体相对于某物的大概位置。学习
##5.2 模型的表示 实际上,实体3D对象是借助三角形网格来近似表示的,三角形是3D物体建模的基石,咱们能够用三角形网格模拟出任何真实世界中的3D物体。固然,点和线也是必不可少的,例如,咱们能够用一系列宽度为1像素的线段绘制出一条近似曲线。测试
在游戏开发的过程当中,咱们通常都不会本身手动列出三角形来模拟3D物体,这样太累了,通常来讲,除了最简单的模型,咱们大部分模型都是在3D建模工具(3D )中完成。在游戏开发中流行的建模软件有:3D Studio Max、LightWave 3D、Maya、Softimage和Blender。动画
##5.3 计算机色彩基础 计算机显示器中每个像素发出的颜色都是红绿蓝三色混合光,当混合的光线进入观察者眼中,照射到视网膜的特定区域时,视锥细胞便受到刺激产生神经冲动,并经过视神经传到大脑,大脑继而解释传来的信号并感知颜色。由于混合光的变化各异,因此细胞受到的刺激也不尽相同,因此咱们就能感知到不一样颜色间的差别了编码
每款显示器能发出的红绿蓝三种颜色的强度是有限的,为了便于描述光的强度,咱们一般将它量化为范围在0-1归一化区间中的值,0表明无强度,1表明最强的强度,由此咱们可使用3D向量(r,g,b)来表示颜色,这三个份量分别表示红绿蓝三色光在混合光中的强度spa
##5.3.1 颜色运算 略code
##5.3.2 128位颜色 事实上,咱们一般会使用到一种名为alpha份量(alpha component)的颜色份量,alpha份量经常使用于表示颜色的不透明度(0.0表示彻底透明,1.0表示彻底不透明),它在混合技术中将会起到相当重要的做用。加上这个份量以后,咱们即可以使用4D向量(r,g,b,a)来表示每一种颜色
为了使用128位数据来表示一种颜色,每个份量都要使用浮点值。因为每一种颜色均可以使用4D向量来表示,因此咱们在代码中可使用XMVECTIR类型来描述他们,而后经过DriectXMath向量函数来进行颜色运算。所以咱们也能够借助SIMD技术加快数据的处理速度。
##5.3.3 32位颜色 为了用32位数据表示一种颜色,每个份量尽能够分配到一个字节,所以每个占用8位字节的颜色份量就能够分别描述256中不一样的颜色强度(0表明无强度,256表明最强强度),这样四个颜色份量一共能够产生255的四次方种颜色,在DirectXMath库中提供了XMCOLOR结构体来存储32位颜色。
128位颜色和32位颜色之间是能够相互转换的,只要将32位颜色向量的每个份量值/255就能够获得对应的128位颜色向量。因为在XMCOLOR中一般将4个8位颜色份量封装成一个32位整数值(例如一个unsigned int类型的值),所以在32位颜色和128位颜色之间的转换一般要进行一些额外的运算,对此,DirectXMath库提供了一个获取XMCOLOR类型实例而且返回对应的XMVECTOR类型值的函数:
XMVECTOR XM_CALLONV PackedVector::XMLoadColor(const XMCOLOR* pSource);
除此以外,DirectXMath库还提供了一个能够将XMVECTOR类型值转换成XMCOLOR类型值的函数:
void XM_CALLCONV PackedVector::XMStoreColor(XMCOLOR pDestination, FXMVECTOR V);
128位颜色一般用于高精度的颜色运算,例如位于像素着色器内的各类运算。不过最终存储在后台缓冲区中的颜色通常都是32位颜色。
##5.4 渲染流水线概述 渲染流水线:若给出某一个3D场景的几何描述,并在其中放置一台具备肯定位置和朝向的虚拟摄像机,那么渲染流水线则是以此摄像机为观察视角而生成的2D图像的一系列步骤
先上图:
图片为手机拍摄,望见谅(图片来源:DirectX 12 3D 游戏开发实战)
上图中左侧表示的是组成渲染流水线的全部阶段,右侧则是显存资源,从资源内存池指向渲染目标流水线阶段的箭头表示该阶段能够读取资源并能够以资源做为输入,从渲染流水线指向资源内存池的箭头则表示该阶段能够向GPU资源写入数据。接下来咱们将详细介绍渲染流水线每个阶段。如咱们所见,大多数阶段能够进行读取资源的操做,可是只有少部分阶段才能够对GPU资源进行写操做。(渲染流水线中每个阶段所输出的数据每每都是下一个阶段的输入)
输入装配器会从显存中读取几何数据(顶点和索引(vertex and index)),再将它们装配成几何图元。这些概念咱们将会在后面陆续进行介绍
##5.5.1 顶点 在数学上,三角形的顶点是两条边的交点,线段的顶点是它的两个端点,对于单个的点来讲,它自己就是一个顶点
在Direct3D中,顶点不只能够用来表示位置信息,还能够包含其余的信息。例如:咱们将在第八章为顶点添加法向量依次来实现光照效果,在第九章中咱们会为顶点添加纹理坐标从而实现纹理贴图。Direct3D为用户自定义顶点格式提供了很高的灵活性,在第六章咱们会讲解一些和顶点有关的代码
##5.5.2 图元拓扑 在Direct3D中,咱们要经过一种名为顶点缓冲区的特殊数据结构来将顶点和渲染流水安绑定在一块儿,顶点缓冲区利用连续的内存来存储一系列顶点,可是仅凭顶点缓冲区是没法说明这些顶点将如何组成几何图元,所以咱们须要指定图元拓扑(primitive topology)来告知Direct3D要以何种方式来表示几何图元。
void ID3D12GraphicsCommandList::IASetPrimitiveTopology{ D3D_PRIMITIVE_TOPOLOGY PrimitiveTopology }; typedef enum D3D_PRIMITIVE_TOPOLOGY { D3D_PRIMITIVE_TOPOLOGY_UNDEFINED = 0, D3D_PRIMITIVE_TOPOLOGY_POINTLIST = 1, D3D_PRIMITIVE_TOPOLOGY_LINELIST = 2, D3D_PRIMITIVE_TOPOLOGY_LINESTRIP = 3, D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP = 4, D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP = 5, D3D_PRIMITIVE_TOPOLOGY_LINELIST_ADJ = 10, D3D_PRIMITIVE_TOPOLOGY_LINESTRIP_ADJ = 11, D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST = 12, D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP = 13, . . . D3D_PRIMITIVE_TOPOLOGY_32_CONTROL_POINT_PATCHLIST = 64, }D3D_PRIMITIVE_TOPOLOGY;
在用户经过命令列表修改图元拓扑以前,全部的绘制调用都会沿用当前设置的图元拓扑,下面将举一个经过命令列表对图元拓扑进行修改的代码示例:
//经过三角形列表的方式来绘制对象 mCommandList->IASetPrimitiveTopology(D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
##5.2.2.1 点列表 经过枚举项D3D_PRIMITIVE_TOPOLOGY_POINTLIST来指定点列表,当使用该图元拓扑时,全部的顶点都将在绘制过程当中绘制成一个单独的点
##5.2.2.2 线条带 经过枚举项D3D_PRIMITIVE_TOPOLOGY_LINESTRIP来指定线条带,当使用该图元拓扑时,顶点将会在绘制调用的过程当中被绘制成一系列连续的线段,因此在这种模式下,n + 1个顶点就会生成n条线段
##5.2.2.3 线列表 经过枚举项D3D_PRIMITIVE_TOPOLOGY_LINELIST来指定线列表,当使用该图元拓扑时,顶点在绘制调用时会被绘制成一系列单独的线段,因此在这种模式下,2n个顶点就会生成n条线段
##5.2.2.4 三角形带 经过枚举项D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP来指定三角形带,当使用该图元拓扑时,顶点在绘制调用时会被绘制成一系列连续的三角形,因此在这种模式下,n个顶点能够生成n - 2个三角形(三角形带的绕序为为顺时针方向)
##5.2.2.5 三角形列表 经过枚举项D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST来指定三角形列表,当使用该图元拓扑时,顶点在绘制调用时会被绘制成一系列独立的三角形,因此在这种模式下,3n个顶点就会生成n个三角形
##5.2.2.6 具备邻接数据的图元拓扑 略
##5.2.2.7 控制点面片列表 D3D_PRIMITIVE_TOPOLOGY_N_CONTROL_POINT_PATCHLIST拓扑类型表示:将顶点数据解释为具备N个控制点(control point)的面片列表,(此图元经常使用与渲染流水线的曲面细分阶段,此阶段为可选阶段),这种图元拓扑类型咱们将会在第十四章再次进行讨论
##5.5.3 索引 在前面咱们提到过,三角形是3D实体对象的基本组成部分,因此为三角形指定顶点顺序是十分重要的工做,咱们把这个顺序称之为绕序。
构成3D物体的不一样三角形之间会共用许多顶点,好比一个四边形,从数学上来看它只有四个顶点,可是在Direct3D中,绘制一个四边形必需要使用两个三角形,即有六个顶点,若是不重复利用顶点数据,那么咱们须要建立六个顶点(其中有两个顶点数据是重复的)来绘制一个四边形,这明显不是咱们但愿看到的结果,因此咱们通常都会借助三角形带或者是索引来解决这个问题
三角形带:借助三角形带能够改善顶点数据的重复建立问题,由于三角形带图元拓扑可使顶点在绘制调用的过程当中绘制成一系列连续的三角形。但若是想使用三角形带改善这个问题,前提是这些几何体必须是可以组织成带状的
索引:这是咱们推荐的解决方法,整个工做流程是这样的:
经过以上的操做,咱们就把“复用的顶点数据”转换为索引列表了,这样即可以对一个顶点数据进行屡次调用。
##5.6 顶点着色器阶段 待图元被装配完毕以后,其顶点会被送到顶点着色器阶段(vertex shader state),咱们能够把顶点着色器当作一种输入数据和输出数据都是单个顶点的函数,每个要被绘制的顶点都必需要通过顶点着色器的处理以后才能够送日后续阶段。事实上,咱们能够认为在硬件中执行的是下列处理操做
for (UINT i = 0; i < numVertices; ++i) { outputVertex[i] = VertexShader(intputVertex[i]); }
其中的顶点着色器函数(VertexShader)就是咱们要实现的部分,因为这个阶段的操做是由GPU完成的,因此通常速度都会很快
在后续的章节中,咱们会看到各类不一样的顶点着色器示例,因此在学习完本书以后,咱们应该要对顶点着色器能够时间的具体功能有一个深入的认识。接下来,咱们将介绍几种经常使用的空间变换
##5.6.1 局部空间和世界空间 设想:若是咱们正在拍一部电影,咱们所在的团队须要为一些特效镜头打造一个和火车有关的微缩场景,其中咱们的具体任务是制做一架袖珍小桥,固然,咱们不会把小桥直接搭建在场景之中,不然咱们便须要在一个极其复杂的环境中当心翼翼的工做,以防止破坏场景中的其余物体,一旦失手便会功亏一篑,相对来说,咱们更加愿意在远离场景的工做室内制做这个袖珍小桥,等制做完成以后在将它以恰当的角度放在场景中合适的位置
3D美工在工做的时候也和上述设想同样,他们不会直接在世界坐标系(世界空间)中直接构建物体的几何形状,而是选择在相对于局部坐标系(局部空间)来建立物体。只要在局部空间定义了3D模型的各顶点,咱们就能将它变换到世界空间中,为了作到这一点,咱们必需要定义局部空间和世界空间二者之间的联系。具体的作法是:根据物体的位置和朝向,指定其局部空间坐标系的原点和每一个坐标轴相对于世界坐标系的坐标,再运用坐标变换便可将物体从局部空间转换到世界空间了。将局部坐标系内的坐标转换到世界坐标系中的过程称为世界变换,所使用的矩阵称为世界矩阵,因为场景中,每个物体的朝向和位置均可以各不相同,所以它们都会有属于本身特定的世界矩阵。
在每个3D模型各自的局部坐标系中的优势以下: 一、易于使用,在局部坐标系中定义物体能够很轻松的肯定各个顶点的坐标
二、物体应该要能够跨越多个场景重复使用,若是将物体坐标相对于某一个特定场景进行硬编码则会多出不少麻烦
三、咱们有时候须要在同一场景中绘制多个同一物体,可是它们的位置、方向和大小不同,将物体绘制在局部空间能够解决这个问题。(咱们一般将存储一份几何体相对于其局部空间的副本,接着按需求次数来绘制该物体,并使用不一样的世界矩阵来指定物体在世界空间中的位置、大小和方向,这种方法称为实例化)
通常来讲,为了构建一个世界矩阵,咱们必需要弄清局部空间中原点和各坐标轴相对于世界空间的坐标关系,但实际上,获取这个关系并不容易并且也不直观,因此咱们通常采用一种更直观的方式:定义一系列的变换组合W,即W = SRT;
S:缩放矩阵,将物体缩放到世界空间
R:旋转矩阵,用来定义物体在局部空间相对于世界空间的朝向
T:平移矩阵,定义的是物体在局部空间相对于世界空间的位置
经过定义一系列的变换组合W,即可以在不指明局部空间的原点以及各坐标轴相对于世界空间的齐次坐标的状况下,直接经过复合一系列简单的变换来创建世界矩阵
##5.6.2 观察空间 为了构建场景的2D图像,咱们必须在场景中架设一台虚拟摄像机,该摄像机肯定了观察者能够见到的视野,也就是生成2D图像所须要的场景空间范围,因此咱们要为该摄像机赋予一个局部坐标系,该坐标系称为观察坐标系,也称为观察空间。虚拟摄像机会位于观察坐标系的原点,而且朝z轴的正方向进行观察,y轴位于摄像机的上方,x轴位于摄像机的右侧(dx使用左手坐标系,OpenGL使用右手坐标系)。观察空间用于在渲染流水线后续阶段描述这些顶点相对于观察坐标系的位置,由世界空间到观察空间的坐标变换称之为观察变换(视图变换),所使用的矩阵称为观察矩阵
假设矩阵w为物体从观察空间转换到世界空间的变换矩阵,则从观察空间转换到世界空间的变换矩阵V = W的逆矩阵。
接下来咱们将介绍一种构建观察矩阵的直观方法:
假设Q为摄像机的位置,T为被观察的目标点的坐标,j表示世界空间y轴方向的单位向量,则
观察坐标系的z轴方向的单位向量w为:(T - Q) / ||T - Q||
观察坐标系的x轴方向的单位向量u为:(j x w) / ||j x w||
观察坐标系的y轴方向的单位向量v为:w x v
综上所述,只有给定摄像机的位置,观察目标点以及世界空间中y轴方向的向量,就能够构建出该世界空间对应的观察空间的观察矩阵,DirectXMath库针对上述计算观察矩阵的方法提过了一个函数:
//输出对应的观察矩阵 XMMATRIX XM_CALLCONV XMMatrixLookAtLH( FXMVECTOR EyePosition, //输入虚拟摄像机的位置 FXMVECTOR ForcusPosition, //输入观察目标点的位置 FXMVECTOR UpDriecttion //输入世界空间向上方向的向量 )
##5.6.3 投影和齐次裁剪空间 前一节咱们介绍了摄像机在世界空间的位置和朝向,除此以外,虚拟摄像机还有一个重要的组成要素,那就是摄像机能够观测到的空间体积(volume of space),此范围能够用一个由四棱锥截取的平截头体(四棱台)表示。因此咱们的下一个任务即是将平截头体内的3D几何体投影到2D投影窗口中,根据前文的透视投影的原理可知,投影一定会随着众平行线汇聚于消失点,其投影的尺寸也将会随着物体3D深度的增长而变小。
图片为手机拍摄,望见谅(图片来源:DirectX 12 3D 游戏开发实战)
##5.6.3.1定义平截头体 在观察空间中,咱们能够经过近平面n(near plane)、元平面f(far plane)、垂直视场角a(vertical field of view Angle)以及投影窗口的纵横比r这四个参数来定义一个以原点为中心,而且沿z轴正方向进行观察的平截头体。接下来咱们将介绍这四个参数的意义:
图片为手机拍摄,望见谅(图片来源:DirectX 12 3D 游戏开发实战)
一、近平面n:如图
二、远平面f:如图
三、垂直视场角a:如图
四、投影窗口的纵横比r:投影窗口实际是就是观察空间中场景的2D图像,因为该图像最终将会被映射到后台缓冲区中,所以,咱们但愿投影窗口和后台缓冲区二者的纵横比保持一致,因此咱们一般把投影窗口的纵横比指定为后台缓冲区的纵横比。假如后台缓冲区的大小为800 x 600,则投影窗口的纵横比为800 / 600。
下面还有不少内容:略
##5.6.3.2 投影顶点 略
##5.6.3.3 规格化设备坐标 略
略
##5.6.3.5 归一化深度值 略
##XMMatrixPerspectiveFovLH函数 咱们能够利用DirectXMath库里的XMMatrixPerSpectiveFovLH函数构建对应的投影矩阵
//构建投影矩阵 XMMATRIX XM_CALLCONV XMMatrixPerspectiveFovLH( float FovAngleY, //用弧度制表示的垂直视场角 float Aspect, //投影窗口的纵横比(通常都与后台缓冲区的纵横比相等) float NearZ, //虚拟摄像机的位置(观察点)到近平面的距离 float FarZ //虚拟摄像机的位置(观察点)到远平面的距离 );
下面的代码片断为XMMatrixPerspectiveFovLH函数构建一个对应垂直视场角为45度,近平面位于z = 1.0f,远平面位于z = 1000.0f的平截头体的投影矩阵的用法:
XMMATRIX p = XMMatrixPerspectiveFovLH(0.25*XM_PI, AspectRatio(), 1.0f, 1000.0f);
纵横比采用的是咱们窗口的宽高比:
float D3DApp::AspectRatio()const { return static_cast<float>(mClientWidth / mClientHeight); }
##5.7 曲面细分阶段 曲面细分阶段(tessellation stage)是利用镶嵌化处理技术对网格中的三角形进行细分,以此来增长物体表面的三角形数量。而后将这些三角形偏移到合适的位置,就可使网格展示出更加细腻的细节。使用曲面细分的优势主要有如下几方面:
曲面细分是一个可选的渲染阶段,咱们将在第十四章对此阶段进行详细的讲解。
##5.8几何着色器阶段 几何着色器阶段是一个可选的渲染阶段,因为咱们在第十二章才会用到它,因此咱们这里只对它进行简单的介绍。
##5.9 裁剪 彻底位于视椎体(用户在3D空间内的可视范围,形状相似于平截头体,视椎体也被称为视平截头体)以外的几何体须要被彻底抛弃,而处于视平截头体交界的几何体也要接受此裁剪的操做。所以,只有在视平截头体以内的物体对象才会被彻底保留下来。
因为裁剪操做是由硬件负责完成的,咱们在这里并不会进行过多的讲解,若是对裁剪过程有兴趣的话,能够了解一下苏泽兰-霍启曼裁剪算法,这个算法的总体思路是找到平面和多边形的全部交点,而后将这些顶点按顺序组织成新的裁剪多边形。
##5.10 光栅化阶段 光栅化阶段(rasterization stage)的主要任务是为投影到主屏幕上的3D三角形计算出相应的像素颜色
##5.10.1 视口变换 当裁剪操做完成以后,硬件会经过透视除法将物体从齐次裁剪空间变换到规格化设备坐标(NDC),一旦物体的顶点位于NDC空间以内,构成2D图像的2D顶点x,y坐标就会被变换到后台缓冲区中称为视口的矩形之中,此变换完成会后,这些x,y坐标都会以像素为单位进行表示。(通常来讲,因为z坐标常常在深度缓冲技术里面用做为深度值,因此视口变换通常都不会影响到z坐标)
##背面剔除 每个三角形都有两个面,在Direct3D中会采用如下约定对这两个面进行区分,假设组成三角形顺序的顶点为v1,v2,v3,那么咱们会经过如下计算来获得这个三角形的法线:
e1 = v2 - v1;
e2 = v3 - v1;
n = (e1 x e2) / (||e1 x e2||)
法向量由正面射出,则另外一面为三角形的背面。在这种约定之下,根据观察者的视角看过去,顶点绕序为顺时针方向的为正面朝向,顶点绕序为逆时针方向的位背面朝向。因为背面朝向的三角形都会被正面朝向的三角形遮挡,因此绘制背面朝向的三角形是没有意义的,背面剔除就是用于将背面朝向的三角形从渲染流水线中剔除的流程,这种操做能够将待处理的三角形数量减小一半。
在默认的状况下,Direct3D将以观察者的视角把顺时针绕序的三角形看做是正面朝向的,把逆时针绕序的三角形视为是背面朝向的,可是,经过对Direct3D渲染状态的设置,咱们也能够把这个约定颠倒过来。
##5.10.3 顶点属性插值 回顾前文可知,咱们要经过顶点来定义三角形,除了位置信息以外,咱们还能够给顶点附加颜色、法向量、纹理坐标、深度值等其余属性,通过视口变换以后,咱们须要为求取三角形内每个像素所附的属性进行线性插值运算,为了获得屏幕空间中各个顶点的属性插值,咱们通常都会使用一种名为透视校订插值。从本质上来讲,插值法即利用三角形的三个顶点属性值计算出其内部像素的属性值
咱们无需考虑透视校订插值法处理像素属性的数学细节,由于硬件会自动的完成相应的处理,若是有人对其中的数学细节感兴趣。能够在[Eberly01]中找到相应的数学推导过程
##5.11 像素着色器阶段 咱们编写的像素着色器(pixel shader)是一种由GPU执行的程序,他会针对每个像素片断进行处理(即每处理一个像素都要执行一次像素种着色器),并根据顶点的插值属性做为输入来计算出对应的像素颜色。像素着色器既能够返回一种单一的恒定颜色,也能够实现如逐像素光照、反射以及阴影等更为复杂的效果。
##5.12 输出合并阶段 经过像素着色器生成的像素片断会被移送至渲染流水线的输出合并阶段,在此阶段,一些像素片断可能会被丢弃(好比未经过深度测试或模板测试的像素片断)。而没有被丢弃的像素片断则会被写入后台缓冲区。
混合操做也是在输出合并阶段完成的,这项技术可使当前处理的像素片断与后台缓冲区中原有的像素片断进行融合,而不是简单的对后台缓冲区进行覆盖。
#小结