在上一个教程中,咱们在应用程序窗口的中心成功渲染了一个三角形。 咱们没有太注意咱们在顶点缓冲区中拾取的顶点位置。 在本教程中,咱们将深刻研究3D位置和转换的细节。git
本教程的结果将是渲染到屏幕的3D对象。 虽然以前的教程侧重于将2D对象渲染到3D世界,但在这里咱们展现了一个3D对象。github
(SDK root)\Samples\C++\Direct3D11\Tutorials\Tutorial04数组
Github仓库框架
在上一个教程中,三角形的顶点被有策略地放置,以在屏幕上完美地对齐。 可是,状况并不是老是如此。 所以,咱们须要一个系统来表示3D空间中的对象和一个显示它们的系统。函数
在现实世界中,物体存在于3D空间中。 这意味着要将对象放置在世界中的特定位置,咱们须要使用坐标系并定义与位置对应的三个坐标。 在计算机图形学中,3D空间最经常使用于笛卡尔坐标系。 在该坐标系中,三个轴X,Y和Z彼此垂直,决定了空间中每一个点的坐标。 该坐标系进一步分为左手系统和右手系统。 在左手系统中,当X轴指向右侧,Y轴指向上方时,Z轴指向前方。 在右手系统中,具备相同的X和Y轴,Z轴指向后方。布局
图1.左手坐标系与右手坐标系spa
如今咱们已经讨论过坐标系,考虑3D空间。 点在不一样的空间中具备不一样的坐标。 做为一维中的一个例子,假设咱们有一个标尺,咱们注意到标尺的5英寸标记处的点P. 如今,若是咱们将标尺向右移动1英寸,则相同的点位于4英寸标记处。 经过移动标尺,参考框架已经改变。 所以,当点没有移动时,它有一个新的坐标。设计
图2. 1D中的空间图示3d
在3D中,空间一般由原点和来自原点的三个惟一轴定义:X,Y和Z.计算机图形中一般使用多个空间:对象空间,世界空间,视图空间,投影空间和屏幕空间。指针
图3.在对象空间中定义的立方体
(-1, 1, -1) ( 1, 1, -1) (-1, -1, -1) ( 1, -1, -1) (-1, 1, 1) ( 1, 1, 1) (-1, -1, 1) ( 1, -1, 1)
左图显示了一个场景,该场景由相似人的物体和观察物体的观察者(相机)组成。 世界空间使用的原点和轴以红色显示。 右图显示了与世界空间相关的视图空间。 视图空间轴显示为蓝色。 为了更清楚地说明,视图空间与左图像中的世界空间的方向与读者不一样。 请注意,在视图空间中,查看器正在Z方向上查看。
投影空间是指从视图空间应用投影变换后的空间。 在此空间中,可见内容的X和Y坐标范围为-1到1,Z坐标范围为0到1。
屏幕空间一般用于指代帧缓冲区中的位置。 由于帧缓冲区一般是2D纹理,因此屏幕空间是2D空间。 左上角是坐标为(0,0)的原点。 正X向右,正Y向下。 对于w像素宽且h像素高的缓冲区,最右下像素具备坐标(w-1,h-1)。
转换最经常使用于将顶点从一个空间转换为另外一个空间。 在3D计算机图形学中,管道中逻辑上有三种这样的变换:世界,视图和投影变换。 下一个教程将介绍单个转换操做,如转换,旋转和缩放。
顾名思义,世界转换将顶点从对象空间转换为世界空间。 它一般由一个或多个缩放,旋转和平移组成,基于咱们想要给对象的大小,方向和位置。 场景中的每一个对象都有本身的世界变换矩阵。 这是由于每一个对象都有本身的大小,方向和位置。
顶点转换为世界空间后,视图转换将这些顶点从世界空间转换为视图空间。 回想一下以前的讨论,观看空间是世界从观众(或相机)的角度出现的。 在视图空间中,观察者位于沿正Z轴向外看的原点。
值得注意的是,尽管视图空间是来自观察者参照系的世界,但视图变换矩阵应用于顶点,而不是观察者。 所以,视图矩阵必须执行咱们应用于咱们的查看器或相机的相反转换。 例如,若是咱们想要将摄像机朝向-Z方向移动5个单元,咱们须要计算一个视图矩阵,它能够沿着+ Z方向将顶点平移5个单位。 虽然相机向后移动,但从相机的角度来看,顶点已向前移动。 在XNA Math中,一个方便的API调用XMMatrixLookAtLH()一般用于计算视图矩阵。 咱们只须要告诉它观察者在哪里,在哪里看,以及表示观察者顶部的方向,也称为向上矢量,以得到相应的视图矩阵。
投影变换将顶点从诸如世界和视图空间的3D空间转换为投影空间。 在投影空间中,顶点的X和Y坐标是从3D空间中该顶点的X / Z和Y / Z比得到的。
图5.投影
在3D空间中,事物以透视的方式出现。 也就是说,物体越近,它出现的越大。 如图所示,在远离观察者眼睛的d个单位处高h单位的树的尖端将出如今与另外一棵树的尖端2h单位高和2d单位远的相同点处。 所以,在2D屏幕上出现顶点的位置与其X / Z和Y / Z比率直接相关。
定义3D空间的参数之一称为视场(FOV)。 FOV表示在特定方向上查看哪些对象从特定位置可见。 人类有一个前瞻性的FOV(咱们没法看到咱们背后的东西),咱们看不到太近或太远的物体。 在计算机图形学中,FOV包含在视锥体中。 视锥体由3D中的6个平面定义。 这些平面中的两个平行于XY平面。 这些被称为近Z和远Z平面。 其余四个平面由观察者的水平和垂直视野定义。 视场越宽,视锥体体积越宽,观察者看到的物体越多。
GPU会过滤掉视锥体外的对象,这样就没必要花时间渲染没法显示的内容。 此过程称为裁剪。 视锥体是一个四面金字塔,顶部被切掉。 剪切此卷是很复杂的,由于要剪切一个视锥体平面,GPU必须将每一个顶点与平面的等式进行比较。 相反,GPU一般首先执行投影变换,而后针对视锥体量进行剪辑。 投影变换对视锥体的影响是金字塔形视锥体成为投影空间中的盒子。 这是由于,如前所述,在投影空间中,X和Y坐标基于3D空间中的X / Z和Y / Z. 所以,点a和点b在投影空间中将具备相同的X和Y坐标,这就是视锥体成为盒子的缘由。
图6.查看平截头体
假设两棵树的尖端刚好位于顶视图平截头体边缘。进一步假设d = 2h。沿投影空间中顶边的Y坐标将为0.5(由于h / d = 0.5)。所以,任何大于0.5的Y投影后Y值都将被裁剪。这里的问题是0.5由程序选择的垂直视场肯定,而且不一样的FOV值致使GPU必须剪切的不一样值。为了使这个过程更加方便,3D程序一般缩放顶点的投影X和Y值,以即可见的X和Y值的范围从-1到1.换句话说,任何X或Y坐标都在[-1]以外1]范围将被删除。为了使该剪切方案起做用,投影矩阵必须经过h / d或d / h的倒数来缩放投影顶点的X和Y坐标。 d / h也是FOV一半的余切。经过缩放,视锥体的顶部变为h / d * d / h = 1.大于1的任何内容都将被GPU裁剪。这就是咱们想要的。
一般也对投影空间中的Z坐标进行相似的调整。 咱们但愿近和远Z平面分别在投影空间中为0和1。 当Z = 3D空间中的近Z值时,Z在投影空间中应为0; 当Z = 3D空间中的远Z时,Z在投影空间中应为1。 完成此操做后,GPU [0 1]之外的任何Z值都将被裁剪掉。
在Direct3D 11中,获取投影矩阵的最简单方法是调用XMMatrixPerspectiveFovLH()方法。 咱们只提供4个参数-FOVy,Aspect,Zn和Zf-并返回一个矩阵,它能够完成上面提到的全部必要操做。 FOVy是Y方向的视野。 Aspect是宽高比,即视图宽度与高度的比率。 从FOVy和Aspect,能够计算FOVx。 该纵横比一般从渲染目标宽度与高度的比率得到。 Zn和Zf分别是视图空间中的近和远Z值。
在上一个教程中,咱们编写了一个程序,用于渲染单个三角形。 当咱们建立顶点缓冲区时,咱们使用的顶点位置直接在投影空间中,这样咱们就没必要执行任何变换。 如今咱们已经了解了3D空间和变换,咱们将修改程序,以便在对象空间中定义顶点缓冲区,就像它应该的那样。 而后,咱们将修改顶点着色器以将顶点从对象空间转换为投影空间。
因为咱们开始以三维方式表示事物,所以咱们将前一个教程中的平面三角形更改成多维数据集。 这将使咱们可以更清楚地展现这些概念。
SimpleVertex vertices[] = { { XMFLOAT3( -1.0f, 1.0f, -1.0f ), XMFLOAT4( 0.0f, 0.0f, 1.0f, 1.0f ) }, { XMFLOAT3( 1.0f, 1.0f, -1.0f ), XMFLOAT4( 0.0f, 1.0f, 0.0f, 1.0f ) }, { XMFLOAT3( 1.0f, 1.0f, 1.0f ), XMFLOAT4( 0.0f, 1.0f, 1.0f, 1.0f ) }, { XMFLOAT3( -1.0f, 1.0f, 1.0f ), XMFLOAT4( 1.0f, 0.0f, 0.0f, 1.0f ) }, { XMFLOAT3( -1.0f, -1.0f, -1.0f ), XMFLOAT4( 1.0f, 0.0f, 1.0f, 1.0f ) }, { XMFLOAT3( 1.0f, -1.0f, -1.0f ), XMFLOAT4( 1.0f, 1.0f, 0.0f, 1.0f ) }, { XMFLOAT3( 1.0f, -1.0f, 1.0f ), XMFLOAT4( 1.0f, 1.0f, 1.0f, 1.0f ) }, { XMFLOAT3( -1.0f, -1.0f, 1.0f ), XMFLOAT4( 0.0f, 0.0f, 0.0f, 1.0f ) }, };
若是你注意到咱们所作的只是指定立方体上的八个点,但咱们实际上没有描述各个三角形。 若是咱们按原样传递,输出将不是咱们所指望的。 咱们须要经过这八个点指定造成立方体的三角形。
在立方体上,许多三角形将共享相同的顶点,而且一次又一次地从新定义相同的点将浪费空间。 所以,有一种方法只指定八个点,而后让Direct3D知道要为三角形选择哪些点。 这是经过索引缓冲区完成的。 索引缓冲区将包含一个列表,该列表将引用缓冲区中的顶点索引,以指定在每一个三角形中使用哪些点。 下面的代码显示了构成每一个三角形的点。
// Create index buffer WORD indices[] = { 3,1,0, 2,1,3, 0,5,4, 1,5,0, 3,4,7, 0,4,3, 1,6,5, 2,6,1, 2,7,6, 3,7,2, 6,4,5, 7,4,6, };
如您所见,第一个三角形由点3,1和0定义。这意味着第一个三角形的顶点位于:( - 1.0f,1.0f,1.0f),(1.0f,1.0f,-1.0) f),和(-1.0f,1.0f,-1.0f)。 立方体上有六个面,每一个面由两个三角形组成。 所以,您会看到此处定义的12个三角形。
因为每一个顶点都是明确列出的,而且没有两个三角形共享边(至少,它已经被定义),这被认为是一个三角形列表。 总的来讲,对于三角形列表中的12个三角形,咱们将须要总共36个顶点。
索引缓冲区的建立与顶点缓冲区很是类似,咱们在结构中指定了诸如大小和类型之类的参数,并称为CreateBuffer。 类型是D3D11_BIND_INDEX_BUFFER,由于咱们使用DWORD声明了咱们的数组,因此咱们将使用sizeof(DWORD)。
D3D11_BUFFER_DESC bd; ZeroMemory( &bd, sizeof(bd) ); bd.Usage = D3D11_USAGE_DEFAULT; bd.ByteWidth = sizeof( WORD ) * 36; // 36 vertices needed for 12 triangles in a triangle list bd.BindFlags = D3D11_BIND_INDEX_BUFFER; bd.CPUAccessFlags = 0; bd.MiscFlags = 0; InitData.pSysMem = indices; if( FAILED( g_pd3dDevice->CreateBuffer( &bd, &InitData, &g_pIndexBuffer ) ) ) return FALSE;
一旦咱们建立了这个缓冲区,咱们就须要设置它,以便Direct3D知道在生成三角形时引用这个索引缓冲区。 咱们指定缓冲区的指针,格式和缓冲区中的偏移量以开始引用。
// Set index buffer g_pImmediateContext->IASetIndexBuffer( g_pIndexBuffer, DXGI_FORMAT_R16_UINT, 0 );
在上一个教程的顶点着色器中,咱们采用输入顶点位置并输出相同的位置而不进行任何修改。咱们能够这样作,由于输入顶点位置已经在投影空间中定义。如今,由于输入顶点位置是在对象空间中定义的,因此咱们必须在从顶点着色器输出以前对其进行变换。咱们经过三个步骤完成此任务:从对象转换到世界空间,从世界转换到视图空间,以及从视图转换到投影空间。咱们须要作的第一件事是声明三个常量缓冲区变量。常量缓冲区用于存储应用程序须要传递给着色器的数据。在渲染以前,应用程序一般会将重要数据写入常量缓冲区,而后在渲染过程当中能够从着色器中读取数据。在FX文件中,常量缓冲区变量在C ++结构中声明为全局变量。咱们将使用的三个变量是HLSL类型“矩阵”的世界,视图和投影变换矩阵。
一旦咱们声明了咱们须要的矩阵,咱们就会更新顶点着色器以使用矩阵变换输入位置。 经过将矢量乘以矩阵来变换矢量。 在HLSL中,这是使用mul()内部函数完成的。 咱们的变量声明和新的顶点着色器以下所示:
cbuffer ConstantBuffer : register( b0 ) { matrix World; matrix View; matrix Projection; } // // Vertex Shader // VS_OUTPUT VS( float4 Pos : POSITION, float4 Color : COLOR ) { VS_OUTPUT output = (VS_OUTPUT)0; output.Pos = mul( Pos, World ); output.Pos = mul( output.Pos, View ); output.Pos = mul( output.Pos, Projection ); output.Color = Color; return output; }
在顶点着色器中,每一个 mul()将一个变换应用于输入位置。 世界,视图和投影变换按顺序依次应用。 这是必要的,由于向量和矩阵乘法不是可交换的。
咱们更新了顶点着色器以使用矩阵进行变换,但咱们还须要在程序中定义三个矩阵。 这三个矩阵将存储渲染时要使用的变换。 在渲染以前,咱们将这些矩阵的值复制到着色器常量缓冲区。 而后,当咱们经过调用Draw()启动渲染时,咱们的顶点着色器读取存储在常量缓冲区中的矩阵。 除了矩阵以外,咱们还须要一个表明常量缓冲区的ID3D11Buffer对象。 所以,咱们的全局变量将添加如下内容:
ID3D11Buffer* g_pConstantBuffer = NULL; XMMATRIX g_World; XMMATRIX g_View; XMMATRIX g_Projection;
要建立ID3D11Buffer对象,咱们使用 ID3D11Device :: CreateBuffer()并指定D3D11_BIND_CONSTANT_BUFFER
D3D11_BUFFER_DESC bd; ZeroMemory( &bd, sizeof(bd) ); bd.Usage = D3D11_USAGE_DEFAULT; bd.ByteWidth = sizeof(ConstantBuffer); bd.BindFlags = D3D11_BIND_CONSTANT_BUFFER; bd.CPUAccessFlags = 0; if( FAILED(g_pd3dDevice->CreateBuffer( &bd, NULL, &g_pConstantBuffer ) ) ) return hr;
咱们须要作的下一件事是提出三个矩阵,咱们将用它来进行转换。咱们但愿三角形位于原点上,与XY平面平行。这正是它如何存储在对象空间中的顶点缓冲区中。所以,世界变换不须要作任何事情,咱们将世界矩阵初始化为单位矩阵。咱们想要设置咱们的相机,使其位于[0 1 -5],查看点[0 1 0]。咱们可使用向上矢量[0 1 0]调用 XMMatrixLookAtLH()来方便地为咱们计算视图矩阵,由于咱们但愿+ Y方向始终保持在顶部。最后,为了获得投影矩阵,咱们称之为XMMatrixPerspectiveFovLH(),具备90度垂直视场(pi / 2),宽高比为640/480,来自咱们的后缓冲区大小,以及近和远Z分别为0.1和110。这意味着屏幕上将看不到小于0.1或超过110的任何内容。这三个矩阵存储在全局变量g_World,g_View和g_Projection中。
咱们有矩阵,如今咱们必须在渲染时将它们写入常量缓冲区,以便GPU能够读取它们。 要更新缓冲区,咱们可使用 ID3D11DeviceContext :: UpdateSubresource()API并将指针传递给以与着色器常量缓冲区相同的顺序存储的矩阵。 为了作到这一点,咱们将建立一个与着色器中的常量缓冲区具备相同布局的结构。 另外,因为矩阵在C ++和HLSL中的内存排列方式不一样,咱们必须在更新以前转置矩阵。
// // Update variables // ConstantBuffer cb; cb.mWorld = XMMatrixTranspose( g_World ); cb.mView = XMMatrixTranspose( g_View ); cb.mProjection = XMMatrixTranspose( g_Projection ); g_pImmediateContext->UpdateSubresource( g_pConstantBuffer, 0, NULL, &cb, 0, 0 );