渲染流水线
渲染流水线
如果有确定的虚拟相机位置,以及某个3D场景的描述,我们就可以通过这个虚拟摄像机生成给定的2D与3D场景图形,当然,使用过unity与unreal engine等其他游戏开发引擎的朋友,对于这一块应该有所了解的。
3D视觉即错觉?
我们应当怎么将3D场景放在2D平面的显示器呢,我们不妨看看下图:
假设我们这看到的是俩条平行的轨道,但是当我们向远处看去时候,会发现,这俩条轨道无限接近于相交,最终会消失在消失点,而这种平行线在世界上辉与消失点,艺术家们常常称之为线性透视。
还有一种物体重叠,当然,我们之前已经了解过深度缓冲,那么此时对于前后像素的绘制,那么就是很得心应手的事情了。
我们再看看下面俩幅图,左边俩个球,一个受光照一个没有,我们甚至感觉最左侧的球像是2D的图,而右边的阴影映射出了飞机离地的高度,它也能告知我们光的相对位置。
模型表示
实际上,实体3D对象借助的是三角网格体来近似表示的,一个模型的三角形越多,那么模型就与目标物越接近,随之而来的也有许多丰富的细节。
计算机色彩基础
计算机的显示器每个像素都是由红绿蓝三色光混合生成的,如下图是从unreal engine截图来的,由左边的RGB混合出了右边的颜色
每款显示器所能发出的红绿蓝三色光强度是有限的,为了描述光的强度,我们常将它的值归一化在0~1的区间内。
颜色运算
(R,G,B)来表示一个颜色向量,分别代表红绿蓝,向量的运算规则同样使用与颜色向量,如:
也就是中等强度的绿色加低等强度的蓝色得到了深绿色。
标量的乘法也是有效的
但点积叉积运算就不适合颜色向量了,但是它有属于自己的分量式,如下:
例:
不透明度
我们会采用alpha分量来表示颜色的不透明度,通常计算机中描述窗户的透明效果时候,就会用到这alpha分量,这时候,我们用(r,g,b,a)4D向量表示一个颜色。
XXX位颜色
计算机中通常会以不同分量范围表示一个颜色值,例如有的以128位代表一个颜色,有的用256位等等。
渲染流水线概述
若给出某个3D场景的几何描述,并在其中架设一台确定位置与朝向的摄像机,那么渲染流水线是以此相机为观察视角生成2D图形的一系列完整步骤。
下图左侧代表渲染阶段,右侧代表显存资源。
输入装配器阶段
输入装配器阶段会从显存中读取稽核数据(顶点和索引),再将他们装配成几何图元。
顶点
在数学中,三角形的顶点是俩条边的交点,线段的顶点是它的俩个端点,对于单个点来说,它本身就是一个顶点。而顶点似乎近是图元的一种特殊点,但顶点的意义并不止于此,它除了空间位置,还包含了其他的元素,比如颜色等等……
图元拓扑
在D3D中,我们要通过顶点缓冲区的特殊数据结构将顶点与渲染流水线绑定。但,我们又如何将顶点作为图元呢,例如,我们应将顶点缓冲区的顶点两两解释成一组线段,还是没3个一组解释为三角形呢?对此我们要通过指定的图元拓扑(primitive topology)来告知D3D如何用顶点数据来表示几何图元:
void ID3D12GraphicsCommandList::IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY Topology);
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_TRIANGLELIST = 4,
D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP = 5,
D3D_PRIMITIVE_TOPOLOGY_LINELIST_ADJ = 10,
D3D_PRIMITIVE_TOPOLOGY_LINESTRIP_ADJ = 11,
D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST_ADJ = 12,
D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP_ADJ = 13,
D3D_PRIMITIVE_TOPOLOGY_1_CONTROL_POINT_PATCHLIST = 33,
D3D_PRIMITIVE_TOPOLOGY_2_CONTROL_POINT_PATCHLIST = 34,
D3D_PRIMITIVE_TOPOLOGY_32_CONTROL_POINT_PATCHLIST = 64,
} D3D_PRIMITIVE_TOPOLOGY;
用户在通过命令列表修改图元拓扑之前,所有的绘制调用都会沿用当前设置的图元拓扑方式,下面是图元拓扑具体的配置方法示例:
mCommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_LINELIST);//通过线列表来绘制对象
mCommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);//通过三角形 列表 来绘制对象
mCommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);//通过三角形 带 来绘制对象
点列表
通过枚举D3D_PRIMITIVE_TOPOLOGY_POINTLIST
来指定点列表,当使用点列表拓扑时,所有的顶点都将绘制调用的过程中被绘制为一个单独的点。
线条带
通过枚举D3D_PRIMITIVE_TOPOLOGY_LINESTRIP
来指定线条带,在使用线条带拓扑时,顶点将在绘制调用过程中被连接为一系列的连续线段,所有在这种拓扑模式下,若有n+1个顶点就会生成n条线段。
线列表
通过枚举D3D_PRIMITIVE_TOPOLOGY_LINELIST
来指定线列表,当使用线列表拓扑时,每对顶点绘制调用过程中都会组成单独的线段,所以,2n个顶点就会生成n条线段。线列表与线条带的区别是,线列表中的线段可以彼此分开,线条带中的线段则是相连的。因此则是共用顶点与否的区别。
三角形带
通过枚举D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP
来指定三角形带,当使用三角形拓扑时,所绘制的三角形将被连成带状,可以看到在这种三角形连接结构中,处于中间位置的顶点将被相邻的三角形共同使用,因此利用n个顶点即可生成n-2个三角形。
经过观察发现,三角形带中次序为偶数的三角形与次序为技术的三角形的顺时针逆时针方向不同的,这就是剔除(消隐)由来,为了解决这个问题,GPU内部会对偶数三角形中前俩个顶点的顺序进行调换,以此使他们与技术三角形的方向一致。
三角形列表
通过D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST
来指定三角形列表,它与线列表一致,它是以三个点生成一个三角形,三角形直接是彼此可分离的,即3n个顶点会生成n个三角形。
具有邻接数据的图元拓扑
对于存有邻接数据的三角形列表而言,每个三角形都有三个与之相邻的邻接三角形。在几何着色器中往往需要访问这些邻接三角形来实现特定的几何着色算法,为了使几何着色器可以顺利地获得这些邻接三角形的信息,我们就需要借助顶点缓冲区与索引区将他们随主三角形一并提交至渲染流水线,此外,此时一定要将拓扑类型指定为D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST_ADJ
,只有这样,渲染流水线才能得知如何以顶点缓冲区中的顶点来构建主三角形与其邻接三角形。邻接图元的顶点只能用作几何着色器的输入数据,却不会被绘制出来,即便程序没有用到几何着色器,但依旧不会绘制邻接图元。
控制点面片列表
D3D_PRIMITIVE_TOPOLOGY_N_CONTROL_POINT_PATCHLIST
拓扑类型表示:将顶点数据解释为具有N个控制点的面片列表,此图元常用与渲染流水线的曲面细分。
索引
如前所述,三角形是3D实体的基本组成对象。对于三角形的三个点绘制顺序(0,1,2),与(0,2,1)不同并且他们一个顺时针一个逆时针绘制,这个顺序我们称为绕序。
有些时候我们绘制三角形的部分顶点数据需要重复复制使用,但是拷贝三角形顶点数据是不合理的,毕竟它还该其他信息,这时候我们就采用的是索引的方式。我们在顶点列表中收录衣服独立的顶点,并在索引列表中存储顶点列表索引值,这些索引定义了顶点列表是如何组合在一起的,从而构成了三角形。
顶点着色器节点
待图元装配完毕之后,其顶点就会被送入顶点着色器阶段,我们可以把顶点着色器看做一种输入输出数据皆为单个顶点的函数,每个要绘制的顶点必须进过顶点着色器的处理再送往后续阶段。事实上我们可以认为硬件是对顶点进行了遍历过程:
其中顶点着色器函数就是我们要实现的那一部分,我们可以使用顶点着色器来实现许多特效,例如变换、光照和位移截图等。
局部空间和世界空间
例如我们在开发一个游戏,美术在建造人物模型时候并不会在场景中建造人物模型,而是在一个相对空间进行搭建,最后再将人物移动到场景的某个位置,而这时候场景便是世界场景坐标系(世界空间),美术在搭建人物模型时候使用到的就是局部坐标(局部空间),而将局部空间转换到世界空间的过程叫世界变换,所用到的变换矩阵叫世界矩阵,这些变换不外呼就平移旋转缩放矩阵。
由于场景中每个物体的场景和位置肯能各有不同,它们都有自己特定的世界矩阵。当每个物体都从各自的内部空间变换到世界空间后,它们的坐标将位移统一坐标系中。
在每个3D模型各自的局部坐标系来定义它们有若干优点:
易于使用。例如通常物体的中心通常位于局部空间的原点,并且关于主轴对称,就像我们采用一个立方体的中心作为原点,坐标轴与某一边平行,这时候我们要计算其他的点就相较于容易
可以跨越多个场景重复使用。就像我们可以将模型可以放到场景不同位置一样。
当我们在场景中要多次绘制同一个物体时候,他们的位置、大小和方向各不同会极其消耗资源,那么当我们存储它的坐标系与不同位置的变换矩阵,那么久能获得我们想要的数据
我们可以记录从原点变换到物体的坐标轴的矩阵,然后在记录物体在相对坐标空间中从相对空间原点变换到当前位置,那么有这俩矩阵,我们就可以得到物体相较于世界空间的位置。
观察空间
为了构建场景2D图像,我们必须在场景中架设一台虚拟摄像机,该摄像机确定了观察者可见的事业,也就是生成2D图像所需的空间范围。对此,我们应该给摄像机赋予一个如下图所示的局部空间(这被称为观察空间,观察坐标、视图空间、视觉空间或摄像机空间)。
在此坐标系中,改虚拟相机位置位于原点,并沿z轴正方形观察,x轴指向摄像机的右侧,y轴则指向摄像机的上方。与相对于世界空间来描述场景中的不同物体顶点不同,观察空间用于在渲染流水线的后续阶段中描述这些顶点相对于摄像机坐标系的坐标。由世界空间至观察空间的坐标变换称为取景变换(也译作观察变换、视图变换等),此变换所用的矩阵则称为观察矩阵(视图矩阵)。
如果:
分别表示了观察空间的原点、x轴、y轴、z轴。那么则有观察空间到世界空间的变换矩阵为:
但这并不是我们想要的变换,刚好相反,我们需要的是从世界空间到观察空间这一逆变换。这时候我们可以由逆矩阵来求得
W
−
1
W^{-1}
W−1.
世界坐标系和观察坐标系通常只有位置和朝向这两点差异,所以表示为
W
=
R
T
W=RT
W=RT
设Q为虚拟摄像机位置,T为此摄像机对准的目标,那么虚拟摄像机的观察方向:
用其代表观察方向的z轴,指向x轴为:
j为时间空间向上的单位向量。
最后的y轴用叉乘计算得到,即法线方向:
投影和齐次剪裁空间
组成摄像机的关键还有一个要素,就是摄像机可观察到的空间体积,此范围可用四棱柱截取的平截头体(四棱台)来表述:
下一个任务是将平截体投影到一个2D投影窗口之中。
定义平截头体
在观察空间中,我们可以通过近裁剪面
n
n
n、远裁剪面
f
f
f、垂直视场角
a
a
a及纵横比
r
r
r这四个参数来定义一个以原点作为投影中心,并沿
z
z
z轴正方向进行观察的平截头体。值得注意的是,位于远近平面皆平行于平面xy,因此我们能方面地计算出它们分别沿
z
z
z轴到原点的距离。纵横比的定义为
r
=
w
/
h
r=w/h
r=w/h其中,
w
w
w为投影窗口的宽度,
h
h
h为投影窗口的高度(以观察空间的单位为准)。投影窗口实质上即为观察空间中场景的2D图像由于改图像最终被映射到后台缓冲区中,因此,我们希望令投影窗口与后台缓冲区俩者的纵横比保持一致。通常,我们将投影窗口的纵横比指定为后台缓冲区的纵横比。如若俩者的纵横比不一致,那么在映射过程中就需要对投影窗口不等比缩放,就会导致图像拉伸变形。
我们通过垂直视场角
a
a
a和冲横臂
r
r
r来确定水平视场角
β
\beta
β,如上图所示,出于方便,我们将高定位2,则宽比必满足:
为了求出具体的垂直视场角
a
a
a,我们假定投影窗口到原点的距离为
d
d
d:
则
最后如果给定垂直视场角
a
a
a和纵横比
r
r
r,我们必能求出水平视场角
β
\beta
β:
投影顶点
我们希望球给定点
(
x
,
y
,
z
)
(x,y,z)
(x,y,z),球其在平面
z
=
d
z=d
z=d,那么我们可以用像是三角形求出点的位置:
规格化设备坐标
上面我们对于高依赖于高为2的时候,但是这样意味着我们还需要纵横比告知硬件,如果能出去投影窗口对纵横比的依赖,那么处理会更简单,对此,我们的解决办法是将
x
x
x的坐标上的投影区间从
[
−
r
,
r
]
[-r,r]
[−r,r]缩放至归一化区间
[
−
1
,
1
]
[-1,1]
[−1,1],即:
这样经过处理后的坐标,就成了规格化设备坐标(Normalized Device Coordinates ,NDC)。
我们可以把由观察空间到NDC空间变换视为一种单位换算,在
x
x
x轴上一个NDC单位等于观察空间中的r个单位,所以我们得到了新的计算:
(VS为观察空间,即 view space的缩写)
用矩阵来表示投影公式
为了表示变换的一致性,我们采用投影矩阵来表示投影变换,投影矩阵:
点归一化之后投影:
归一化深度值
深度值的映射我们也处理在[0,1]范围内,这是我们的透视投影矩阵:
[n,f]的区间归一化到[0x,1]
DX投影矩阵构建(XMMatrixPerspectiveFovLH)
// 返回投影矩阵
XMMATRIX XM_CALLCONV XMMatrixPerspectiveFovLH(
float FovAngleY, // 用弧度制表示的垂直视场角
float Aspect, // 纵横比
float NearZ, // 到近平面的距离
float FarZ); // 到远平面的距离
曲面细分阶段
曲面细分阶段是利用镶嵌化处理技术对网格体重的三角形进行细分,以此来增加物体表面上的三角形数量。再将这些新增的三角形偏移到合适的位置,使网格体表现出更加细腻的细节,如下图:
左边是原始的网格体,右边呈现的是经过曲面细分阶段处理后的网格
使用曲面细分的有点有一下几方面:
1.我们能借此实现一种细节层级机制,是离虚拟摄像机较近的三角形经镶嵌化处理得到更加丰富的细节,而对于摄像机较远的三角形不进行任何更改。通过这种方式,即可只针对用户关注的高度部分网格增添三角形,从而提示其细节效果
2.我们再内存维护中仅维护简单的地膜网格,在根据需求为它动态地额外得增添三角形,以此节省内存资源
3.我们可以再处理动画和物理模拟之时采用简单的低模网格,而仅在渲染过程中使用经镶嵌化处理的高模
几何着色器阶段
顶点着色器接受的输入应当是完整的图元,假设我们正在绘制三角形列表,那么向几何着色器传入的将是三个顶点。
裁剪
完全位于视椎体(平截头体)之外的几何体都要被裁减掉,下图是裁剪前后对比:
我们可以把视椎体看作,顶、底、左、右、近、远6个平面范围。
光栅化阶段
它的主要任务是投影主屏幕上的3D三角形计算对应的像素颜色。
视口变换
当裁剪操作完成后,硬件会通过透视除法将物体从齐次裁剪空间变换为规格化设备坐标(NDC)。一旦物体位于NDC空间内,构成2D图像的2D顶点x,y,坐标就会被变换到后台缓冲区中,称为视口。待此变换后,这些坐标都讲以像素单位表示,通常来讲,由于z坐标常在深度缓冲技术中作深度值,因此视口变换是不会影响此值的。当然可以去手动修改。
背面剔除
每个三角形都有俩个面,我们采用以下约定进行区分。组成三角形的顶点顺序为
v
0
v_0
v0、
v
1
v_1
v1、
v
2
v_2
v2,那么我们通过下述方法来计算三角形的法线
法向量由正面射出,另一面则为背面,如果观察者看到的是三角形的背面,则称此三角形是背面朝向。
在大多数3D世界空间中的物体皆为实体对象,摄像机看不到的地方就会被剔除(背面剔除),这样待处理的三角形总量消减一半。
在默认情况下,D3D将以观察者视角把顺时针绕序的三角形看作正面朝向,逆时针绕序叫背面朝向,但通过对D3D渲染状态的设置,我们也可以将这个约定颠倒过来。
顶点属性插值
我们要通过指定的顶点来定义三角形,除了位置信息以外,我们还能通过给顶点附着颜色、法向量、和纹理坐标等其他属性。经过视口变换之后,我们需要为求取三角形内诸像素所附的属性进行插值运算。而且除了上述顶点属性,我们还需对顶点的深度值进行插值,继而得到每个像素参与实现深度缓冲算法的深度值。为了得到屏幕空间中各个顶点的插值属性,往往要通过一种名为透视校正插值的方法,对空间中三角形的属性进行线性插值,本质上来说,插值法,即利用三角形顶点的属性值计算其内部像素的属性值。
下图中对三角形上上个顶点进行线性插值,即可求出该三角形内任意一点所具的属性值
p
(
s
,
t
)
p(s,t)
p(s,t)
像素着色器阶段
我们编写的像素着色器(pixel shader ,PS)是由一种GPU来执行的程序。它会针对每一个像素片段进行处理,并根据顶点的插值属性作为输入来计算出对应的像素颜色,像素着色器既可以直接返回一种单一的恒定颜色,也可以实现如逐像素光照、反射、阴影等更为复杂的果。
输出合并阶段
通过像素着色器阶段生成的像素片段会被送至渲染流水线的输出合并阶段。在此阶段中,一些像素片段可能会被丢弃,而后剩下的像素片段将会被写入后台缓冲区中,操作也是在此阶段实现的,此技术可令当前处理的像素与后台缓冲区对应的像素相融合,而不是仅是对后者完全覆写。一些如透明这样的特殊效果,也是由混合技术来实现的。