1、从视觉观察效果来看,平行线最终会相交于一点(消失点,又称为灭点),因此我们可以得出结论:随着深度(z方向)的增加,物体会显得越来越小、(dx是左手坐标系,OpenGL是右手坐标系)。
2、我们都知道物体重叠,这是一个重要的概念,即不透明的物体可以遮挡住其后侧物体的局部或整体,它传达了不同物体在场景中的深度顺序关系
3、光照和阴影的处理在刻画3D物体的实体形状和立体感中扮演着至关重要的角色,其中阴影担负着两个重要的任务,即暗示了光源在场景中的相对位置和物体相对于某物的大概位置。
实际上,实体3D对象是借助三角形网格来近似表示的,三角形是3D物体建模的基石,我们可以用三角形网格模拟出任何真实世界中的3D物体。当然,点和线也是必不可少的,例如,我们可以用一系列宽度为1像素的线段绘制出一条近似曲线。
在游戏开发的过程中,我们一般都不会自己手动列出三角形来模拟3D物体,这样太累了,一般来说,除了最简单的模型,我们大部分模型都是在3D建模工具(3D )中完成。在游戏开发中流行的建模软件有:3D Studio Max、LightWave 3D、Maya、Softimage和Blender。
计算机显示器中每一个像素发出的颜色都是红绿蓝三色混合光,当混合的光线进入观察者眼中,照射到视网膜的特定区域时,视锥细胞便受到刺激产生神经冲动,并通过视神经传到大脑,大脑继而解释传来的信号并感知颜色。因为混合光的变化各异,所以细胞受到的刺激也不尽相同,所以我们就能感知到不同颜色间的差异了
每款显示器能发出的红绿蓝三种颜色的强度是有限的,为了便于描述光的强度,我们通常将它量化为范围在0-1归一化区间中的值,0代表无强度,1代表最强的强度,由此我们可以使用3D向量(r,g,b)来表示颜色,这三个分量分别表示红绿蓝三色光在混合光中的强度
略
事实上,我们通常会使用到一种名为alpha分量(alpha component)的颜色分量,alpha分量常用于表示颜色的不透明度(0.0表示完全透明,1.0表示完全不透明),它在混合技术中将会起到至关重要的作用。加上这个分量之后,我们便可以使用4D向量(r,g,b,a)来表示每一种颜色
为了使用128位数据来表示一种颜色,每一个分量都要使用浮点值。由于每一种颜色都可以使用4D向量来表示,所以我们在代码中可以使用XMVECTIR类型来描述他们,然后通过DriectXMath向量函数来进行颜色运算。因此我们也可以借助SIMD技术加快数据的处理速度。
为了用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位颜色。
渲染流水线:若给出某一个3D场景的几何描述,并在其中放置一台具有确定位置和朝向的虚拟摄像机,那么渲染流水线则是以此摄像机为观察视角而生成的2D图像的一系列步骤
先上图:
图片为手机拍摄,望见谅(图片来源:DirectX 12 3D 游戏开发实战)
上图中左侧表示的是组成渲染流水线的所有阶段,右侧则是显存资源,从资源内存池指向渲染目标流水线阶段的箭头表示该阶段可以读取资源并可以以资源作为输入,从渲染流水线指向资源内存池的箭头则表示该阶段可以向GPU资源写入数据。接下来我们将详细介绍渲染流水线每一个阶段。如我们所见,大多数阶段可以进行读取资源的操作,但是只有少部分阶段才可以对GPU资源进行写操作。(渲染流水线中每一个阶段所输出的数据往往都是下一个阶段的输入)
输入装配器会从显存中读取几何数据(顶点和索引(vertex and index)),再将它们装配成几何图元。这些概念我们将会在后面陆续进行介绍
在数学上,三角形的顶点是两条边的交点,线段的顶点是它的两个端点,对于单个的点来说,它本身就是一个顶点
在Direct3D中,顶点不仅可以用来表示位置信息,还可以包含其他的信息。例如:我们将在第八章为顶点添加法向量依次来实现光照效果,在第九章中我们会为顶点添加纹理坐标从而实现纹理贴图。Direct3D为用户自定义顶点格式提供了很高的灵活性,在第六章我们会讲解一些和顶点有关的代码
在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);
通过枚举项D3D_PRIMITIVE_TOPOLOGY_POINTLIST来指定点列表,当使用该图元拓扑时,所有的顶点都将在绘制过程中绘制成一个单独的点
通过枚举项D3D_PRIMITIVE_TOPOLOGY_LINESTRIP来指定线条带,当使用该图元拓扑时,顶点将会在绘制调用的过程中被绘制成一系列连续的线段,所以在这种模式下,n + 1个顶点就会生成n条线段
通过枚举项D3D_PRIMITIVE_TOPOLOGY_LINELIST来指定线列表,当使用该图元拓扑时,顶点在绘制调用时会被绘制成一系列单独的线段,所以在这种模式下,2n个顶点就会生成n条线段
通过枚举项D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP来指定三角形带,当使用该图元拓扑时,顶点在绘制调用时会被绘制成一系列连续的三角形,所以在这种模式下,n个顶点可以生成n - 2个三角形(三角形带的绕序为为顺时针方向)
通过枚举项D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST来指定三角形列表,当使用该图元拓扑时,顶点在绘制调用时会被绘制成一系列独立的三角形,所以在这种模式下,3n个顶点就会生成n个三角形
略
D3D_PRIMITIVE_TOPOLOGY_N_CONTROL_POINT_PATCHLIST拓扑类型表示:将顶点数据解释为具有N个控制点(control point)的面片列表,(此图元常用与渲染流水线的曲面细分阶段,此阶段为可选阶段),这种图元拓扑类型我们将会在第十四章再次进行讨论
在前面我们提到过,三角形是3D实体对象的基本组成部分,所以为三角形指定顶点顺序是十分重要的工作,我们把这个顺序称之为绕序。
构成3D物体的不同三角形之间会共用许多顶点,比如一个四边形,从数学上来看它只有四个顶点,但是在Direct3D中,绘制一个四边形必须要使用两个三角形,即有六个顶点,如果不重复利用顶点数据,那么我们需要创建六个顶点(其中有两个顶点数据是重复的)来绘制一个四边形,这明显不是我们希望看到的结果,所以我们一般都会借助三角形带或者是索引来解决这个问题
三角形带:借助三角形带可以改善顶点数据的重复创建问题,因为三角形带图元拓扑可以使顶点在绘制调用的过程中绘制成一系列连续的三角形。但如果想使用三角形带改善这个问题,前提是这些几何体必须是能够组织成带状的
索引:这是我们推荐的解决方法,整个工作流程是这样的:
通过以上的操作,我们就把“复用的顶点数据”转换为索引列表了,这样便可以对一个顶点数据进行多次调用。
待图元被装配完毕之后,其顶点会被送到顶点着色器阶段(vertex shader state),我们可以把顶点着色器看成一种输入数据和输出数据都是单个顶点的函数,每一个要被绘制的顶点都必须要经过顶点着色器的处理之后才可以送往后续阶段。事实上,我们可以认为在硬件中执行的是下列处理操作
for (UINT i = 0; i < numVertices; ++i)
{
outputVertex[i] = VertexShader(intputVertex[i]);
}
其中的顶点着色器函数(VertexShader)就是我们要实现的部分,由于这个阶段的操作是由GPU完成的,所以一般速度都会很快
在后续的章节中,我们会看到各种不同的顶点着色器示例,所以在学习完本书之后,我们应该要对顶点着色器可以时间的具体功能有一个深刻的认识。接下来,我们将介绍几种常用的空间变换
设想:如果我们正在拍一部电影,我们所在的团队需要为一些特效镜头打造一个和火车有关的微缩场景,其中我们的具体任务是制作一架袖珍小桥,当然,我们不会把小桥直接搭建在场景之中,否则我们便需要在一个极其复杂的环境中小心翼翼的工作,以防止破坏场景中的其他物体,一旦失手便会功亏一篑,相对来讲,我们更加愿意在远离场景的工作室内制作这个袖珍小桥,等制作完成之后在将它以恰当的角度放在场景中合适的位置
3D美工在工作的时候也和上述设想一样,他们不会直接在世界坐标系(世界空间)中直接构建物体的几何形状,而是选择在相对于局部坐标系(局部空间)来创建物体。只要在局部空间定义了3D模型的各顶点,我们就能将它变换到世界空间中,为了做到这一点,我们必须要定义局部空间和世界空间两者之间的联系。具体的做法是:根据物体的位置和朝向,指定其局部空间坐标系的原点和每个坐标轴相对于世界坐标系的坐标,再运用坐标变换即可将物体从局部空间转换到世界空间了。将局部坐标系内的坐标转换到世界坐标系中的过程称为世界变换,所使用的矩阵称为世界矩阵,由于场景中,每一个物体的朝向和位置都可以各不相同,因此它们都会有属于自己特定的世界矩阵。
在每一个3D模型各自的局部坐标系中的优点如下:
1、易于使用,在局部坐标系中定义物体可以很轻松的确定各个顶点的坐标
2、物体应该要可以跨越多个场景重复使用,如果将物体坐标相对于某一个特定场景进行硬编码则会多出很多麻烦
3、我们有时候需要在同一场景中绘制多个同一物体,但是它们的位置、方向和大小不一样,将物体绘制在局部空间可以解决这个问题。(我们通常将存储一份几何体相对于其局部空间的副本,接着按需求次数来绘制该物体,并使用不同的世界矩阵来指定物体在世界空间中的位置、大小和方向,这种方法称为实例化)
一般来说,为了构建一个世界矩阵,我们必须要弄清局部空间中原点和各坐标轴相对于世界空间的坐标关系,但实际上,获取这个关系并不容易而且也不直观,所以我们一般采用一种更直观的方式:定义一系列的变换组合W,即W = SRT;
S:缩放矩阵,将物体缩放到世界空间
R:旋转矩阵,用来定义物体在局部空间相对于世界空间的朝向
T:平移矩阵,定义的是物体在局部空间相对于世界空间的位置
通过定义一系列的变换组合W,便可以在不指明局部空间的原点以及各坐标轴相对于世界空间的齐次坐标的情况下,直接通过复合一系列简单的变换来建立世界矩阵
为了构建场景的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 //输入世界空间向上方向的向量
)
前一节我们介绍了摄像机在世界空间的位置和朝向,除此之外,虚拟摄像机还有一个重要的组成要素,那就是摄像机可以观测到的空间体积(volume of space),此范围可以用一个由四棱锥截取的平截头体(四棱台)表示。所以我们的下一个任务便是将平截头体内的3D几何体投影到2D投影窗口中,根据前文的透视投影的原理可知,投影必定会随着众平行线汇聚于消失点,其投影的尺寸也将会随着物体3D深度的增加而变小。
图片为手机拍摄,望见谅(图片来源:DirectX 12 3D 游戏开发实战)
在观察空间中,我们可以通过近平面n(near plane)、元平面f(far plane)、垂直视场角a(vertical field of view Angle)以及投影窗口的纵横比r这四个参数来定义一个以原点为中心,并且沿z轴正方向进行观察的平截头体。接下来我们将介绍这四个参数的意义:
图片为手机拍摄,望见谅(图片来源:DirectX 12 3D 游戏开发实战)
1、近平面n:如图
2、远平面f:如图
3、垂直视场角a:如图
4、投影窗口的纵横比r:投影窗口实际是就是观察空间中场景的2D图像,由于该图像最终将会被映射到后台缓冲区中,因此,我们希望投影窗口和后台缓冲区两者的纵横比保持一致,所以我们通常把投影窗口的纵横比指定为后台缓冲区的纵横比。假如后台缓冲区的大小为800 x 600,则投影窗口的纵横比为800 / 600。
下面还有很多内容:略
略
略
略
略
我们可以利用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);
}
曲面细分阶段(tessellation stage)是利用镶嵌化处理技术对网格中的三角形进行细分,以此来增加物体表面的三角形数量。然后将这些三角形偏移到合适的位置,就可以使网格展现出更加细腻的细节。使用曲面细分的优点主要有以下几方面:
曲面细分是一个可选的渲染阶段,我们将在第十四章对此阶段进行详细的讲解。
几何着色器阶段是一个可选的渲染阶段,由于我们在第十二章才会用到它,所以我们这里只对它进行简单的介绍。
完全位于视椎体(用户在3D空间内的可视范围,形状类似于平截头体,视椎体也被称为视平截头体)之外的几何体需要被完全抛弃,而处于视平截头体交界的几何体也要接受此裁剪的操作。因此,只有在视平截头体之内的物体对象才会被完全保留下来。
由于裁剪操作是由硬件负责完成的,我们在这里并不会进行过多的讲解,如果对裁剪过程有兴趣的话,可以了解一下苏泽兰-霍启曼裁剪算法,这个算法的整体思路是找到平面和多边形的所有交点,然后将这些顶点按顺序组织成新的裁剪多边形。
光栅化阶段(rasterization stage)的主要任务是为投影到主屏幕上的3D三角形计算出相应的像素颜色
当裁剪操作完成之后,硬件会通过透视除法将物体从齐次裁剪空间变换到规格化设备坐标(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渲染状态的设置,我们也可以把这个约定颠倒过来。
回顾前文可知,我们要通过顶点来定义三角形,除了位置信息之外,我们还可以给顶点附加颜色、法向量、纹理坐标、深度值等其他属性,经过视口变换之后,我们需要为求取三角形内每一个像素所附的属性进行线性插值运算,为了得到屏幕空间中各个顶点的属性插值,我们一般都会使用一种名为透视校正插值。从本质上来说,插值法即利用三角形的三个顶点属性值计算出其内部像素的属性值
我们无需考虑透视校正插值法处理像素属性的数学细节,因为硬件会自动的完成相应的处理,如果有人对其中的数学细节感兴趣。可以在[Eberly01]中找到相应的数学推导过程
我们编写的像素着色器(pixel shader)是一种由GPU执行的程序,他会针对每一个像素片段进行处理(即每处理一个像素都要执行一次像素种着色器),并根据顶点的插值属性作为输入来计算出对应的像素颜色。像素着色器既可以返回一种单一的恒定颜色,也可以实现如逐像素光照、反射以及阴影等更为复杂的效果。
通过像素着色器生成的像素片段会被移送至渲染流水线的输出合并阶段,在此阶段,一些像素片段可能会被丢弃(比如未通过深度测试或模板测试的像素片段)。而没有被丢弃的像素片段则会被写入后台缓冲区。
混合操作也是在输出合并阶段完成的,这项技术可以使当前处理的像素片段与后台缓冲区中原有的像素片段进行融合,而不是简单的对后台缓冲区进行覆盖。
手机扫一扫
移动阅读更方便
你可能感兴趣的文章