DirectX11入门到实践-(2)渲染基础
本部分知识由微软directx官方教程翻译而来
渲染三角形
三角形由其三个点定义,也称为顶点。具有唯一位置的一组三个顶点定义了唯一的三角形。
为了让GPU渲染三角形,我们必须告诉它三角形的三个顶点的位置。对于2D示例,假设我们希望渲染一个三角形,例如图1中的三角形。
我们将三个顶点与位置(0,0), (0,1)和(1,0)传递给GPU,然后GPU就有足够的信息来渲染我们想要的三角形。
在Direct3D 11中,诸如位置的顶点信息存储在缓冲区资源中。用于存储顶点信息的缓冲区被称为顶点缓冲区。我们必须为三个顶点创建一个足够大的顶点缓冲区,并用顶点位置填充它。
在Direct3D 11中,应用程序必须在创建缓冲区资源时指定缓冲区大小(以字节为单位)。我们知道缓冲区必须足够大才能容纳三个顶点,但每个顶点需要多少字节?要回答这个问题,需要了解顶点布局。
输入布局
顶点有一个位置。通常,它还具有其他属性,例如法线,一种或多种颜色,纹理坐标(用于纹理映射)等。
顶点布局定义了这些属性在内存中的位置:每个属性使用的数据类型,每个属性的大小以及内存中属性的顺序。顶点通常由结构Struct表示。顶点的大小可方便地从结构的大小获得。
在本教程中,我们只处理顶点的位置。因此,我们使用XMFLOAT3类型的单个字段定义顶点结构。此类型是三个浮点组件的向量,通常是用于3D位置的数据类型。
struct SimpleVertex{XMFLOAT3 pos; // 位置};
我们现在有一个表示顶点的结构。它负责在我们的应用程序中将顶点信息存储在系统内存中。
但是,当我们向GPU提供包含顶点的顶点缓冲区时,我们只需要为它提供一块内存。GPU还必须知道顶点布局,以便从缓冲区中提取正确的属性。要实现此目的,需要使用输入布局。
在Direct3D 11中,输入布局是Direct3D对象,它以GPU可以理解的方式描述顶点的结构。可以使用D3D11_INPUT_ELEMENT_DESC结构描述每个顶点属性。应用程序定义一个或多个D3D11_INPUT_ELEMENT_DESC的数组,然后使用该数组创建输入布局对象,该对象将顶点描述为一个整体。现在我们将详细介绍D3D11_INPUT_ELEMENT_DESC的字段。
SemanticName | 语义名称是一个字符串,其中包含一个描述此元素的性质或目的(或语义)的单词。这个词可以是C标识符可以的任何形式,也可以是我们选择的任何形式。例如,顶点位置的良好语义名称是POSITION。语义名称不区分大小写。 |
SemanticIndex | 语义索引补充了语义名称。顶点可以具有相同性质的多个属性。例如,它可以具有2组纹理坐标或2组颜色。不是使用附加了数字的语义名称,例如"COLOR0"和"COLOR1",这两个元素可以共享单个语义名称"COLOR",具有不同的语义索引0和1。 |
格式 | Format定义要用于此元素的数据类型。例如,DXGI_FORMAT_R32G32B32_FLOAT的格式有三个32位浮点数,使元素长12个字节。DXGI_FORMAT_R16G16B16A16_UINT的格式有四个16位无符号整数,使元素长8个字节。 |
InputSlot | 如前所述,Direct3D 11应用程序通过使用顶点缓冲区将顶点数据传递给GPU。在Direct3D 11中,可以同时向GPU提供多个顶点缓冲区,准确地说是16。每个顶点缓冲区都绑定到0到15之间的输入槽号.InputSlot字段告诉GPU它应该为该元素获取哪个顶点缓冲区。 |
AlignedByteOffset | 顶点存储在顶点缓冲区中,顶层缓冲区只是一块内存。AlignedByteOffset字段告诉GPU开始获取此元素的数据的内存位置。 |
InputSlotClass | 该字段的值通常为D3D11_INPUT_PER_VERTEX_DATA。当应用程序使用实例化时,它可以将输入布局的InputSlotClass设置为D3D11_INPUT_PER_INSTANCE_DATA以使用包含实例数据的顶点缓冲区。Instancing是一个高级的Direct3D主题,这里不再讨论。对于我们的教程,我们将专门使用D3D11_INPUT_PER_VERTEX_DATA。 |
InstanceDataStepRate | 该字段用于实例化。由于我们没有使用实例化,因此不使用此字段,必须将其设置为0。 |
现在我们可以定义D3D11_INPUT_ELEMENT_DESC数组并创建输入布局:
InitDevice()中// Define the input layoutD3D11_INPUT_ELEMENT_DESC layout[] ={{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },};UINT numElements = ARRAYSIZE( layout );
顶点布局
在下一个教程中,我们将解释技术对象和关联的着色器。目前,我们将专注于为该技术创建Direct3D 11顶点布局对象。
但是,我们将了解顶点着色器与此顶点布局紧密耦合。原因是创建顶点布局对象需要顶点着色器的输入签名。我们使用从D3DX11CompileFromFile返回的ID3DBlob对象来检索表示顶点着色器的输入签名的二进制数据。
获得此数据后,我们可以调用ID3D11Device :: CreateInputLayout()来创建顶点布局对象,并使用ID3D11DeviceContext :: IASetInputLayout()将其设置为活动顶点布局。执行所有这些操作的代码如下所示:
// Create the input layouthr = g_pd3dDevice->CreateInputLayout( layout, numElements, pVSBlob->GetBufferPointer(),pVSBlob->GetBufferSize(), &g_pVertexLayout );pVSBlob->Release();if( FAILED( hr ) )return hr;// Set the input layoutg_pImmediateContext->IASetInputLayout( g_pVertexLayout );
创建顶点缓冲区
在初始化期间我们还需要做的一件事是创建保存顶点数据的顶点缓冲区。
要在Direct3D 11中创建顶点缓冲区,我们填写两个结构,D3D11_BUFFER_DESC和D3D11_SUBRESOURCE_DATA,然后调用ID3D11Device :: CreateBuffer()。
D3D11_BUFFER_DESC描述了要创建的顶点缓冲区对象,D3D11_SUBRESOURCE_DATA 描述了在创建过程中将复制到顶点缓冲区的实际数据。顶点缓冲区的创建和初始化是一次完成的,因此我们以后不需要初始化缓冲区。
将复制到顶点缓冲区的数据是顶点,即三个简单结构的数组。选择顶点数组中的坐标,以便在使用着色器渲染时在应用程序窗口的中间看到一个三角形。创建顶点缓冲区后,我们可以调用ID3D11DeviceContext :: IASetVertexBuffers() 将其绑定到设备。完整的代码如下所示:
// Create vertex bufferSimpleVertex vertices[] ={XMFLOAT3( 0.0f, 0.5f, 0.5f ),XMFLOAT3( 0.5f, -0.5f, 0.5f ),XMFLOAT3( -0.5f, -0.5f, 0.5f ),};D3D11_BUFFER_DESC bd;ZeroMemory( &bd, sizeof(bd) );bd.Usage = D3D11_USAGE_DEFAULT;bd.ByteWidth = sizeof( SimpleVertex ) * 3;bd.BindFlags = D3D11_BIND_VERTEX_BUFFER;bd.CPUAccessFlags = 0;D3D11_SUBRESOURCE_DATA InitData;ZeroMemory( &InitData, sizeof(InitData) );InitData.pSysMem = vertices;hr = g_pd3dDevice->CreateBuffer( &bd, &InitData, &g_pVertexBuffer );if( FAILED( hr ) )return hr;// Set vertex bufferUINT stride = sizeof( SimpleVertex );UINT offset = 0;g_pImmediateContext->IASetVertexBuffers( 0, 1, &g_pVertexBuffer, &stride, &offset );
原始拓扑
原始拓扑是指GPU如何获得渲染三角形所需的三个顶点。
我们在上面讨论过,为了渲染单个三角形,应用程序需要向GPU发送三个顶点。因此,顶点缓冲区中有三个顶点。如果我们想渲染两个三角形怎么办?一种方法是将6个顶点发送到GPU。前三个顶点定义第一个三角形,后三个顶点定义第二个三角形。此拓扑称为三角形列表。
三角形列表具有易于理解的优点,但在某些情况下它们效率非常低。当连续渲染的三角形共享顶点时会发生这种情况。例如,左图3a显示了一个由两个三角形组成的正方形:ABC和CBD.
请注意,B和C在顶点缓冲区中出现两次,因为它们由两个三角形共享。
图3a包含一个由两个三角形组成的正方形; 图3b包含由三个三角形组成的五边形形状。
如果我们可以告诉GPU在渲染第二个三角形时,我们可以使顶点缓冲区更小,而不是从顶点缓冲区获取所有三个顶点,使用前一个三角形中的2个顶点,并从顶点缓冲区中仅获取1个顶点。
事实证明,这是由Direct3D支持的,拓扑结构称为三角形条带。渲染三角形条时,第一个三角形由顶点缓冲区中的前三个顶点定义。下一个三角形由前一个三角形的最后两个顶点加上顶点缓冲区中的下一个顶点定义。以图3a中的方块为例,使用三角形条带,顶点缓冲区看起来像:A B C D
顶点缓冲区大小从6个顶点变为4个顶点。类似地,对于三个三角形,如图3b中的三角形,使用三角形列表将需要顶点缓冲区,
例如:
ABCCBDCDE
使用三角形条带,顶点缓冲区的大小显着减少:
ABCDE
您可能已经注意到,在三角形条带示例中,第二个三角形被定义为BCD.这三个顶点不形成顺时针顺序。这是使用三角形条带的自然现象。为了克服这个问题,GPU会自动交换来自前一个三角形的两个顶点的顺序。它只对第二个三角形,第四个三角形,第六个三角形,第八个三角形等执行此操作。这确保每个三角形由顶点以正确的缠绕顺序定义(在这种情况下为顺时针方向)。除了三角形列表和三角形条带外,Direct3D 11还支持许多其他类型的原始拓扑。我们不会在本教程中讨论它们。
在我们的代码中,我们有一个三角形,所以我们指定的并不重要。但是,我们必须指定一些内容,因此我们选择了三角形列表。
// Set primitive topologyg_pImmediateContext->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST );
渲染三角形
缺少的最后一项是执行三角形实际渲染的代码。我们创建了两个用于渲染的着色器,顶点着色器和像素着色器。
顶点着色器负责将三角形的各个顶点转换为正确的位置。像素着色器负责计算三角形的每个像素的最终输出颜色。
在下一个教程中将对此进行更详细的介绍。要使用这些着色器,我们必须分别调用ID3D11DeviceContext :: VSSetShader() 和ID3D11DeviceContext :: PSSetShader()。我们做的最后一件事是调用ID3D11DeviceContext :: Draw() ,它命令GPU使用当前顶点缓冲区,顶点布局和原始拓扑进行渲染。
第一个参数Draw()是要发送到GPU的顶点数,第二个参数是要开始发送的第一个顶点的索引。因为我们渲染一个三角形并且我们从顶点缓冲区的开头渲染,所以我们分别使用3和0作为两个参数。整个三角形渲染代码如下所示:
Reander()中:// Render a triangleg_pImmediateContext->VSSetShader( g_pVertexShader, nullptr, 0 );g_pImmediateContext->PSSetShader( g_pPixelShader, nullptr, 0 );g_pImmediateContext->Draw( 3, 0 );
Shader的创建在InitDevice()中进行
着色器和效果系统
图形管道
在上一个教程中,我们设置了顶点缓冲区,然后我们将顶点布局与顶点着色器相关联。现在,我们将解释着色器是什么以及它是如何工作的。为了完全理解各个着色器,我们将退后一步,查看整个图形管道。
在教程2中,当我们调用VSSetShader() 和PSSetShader() 时,我们实际上将着色器绑定到管道中的一个阶段。然后,当我们调用Draw时,我们开始处理传递到图形管道的顶点数据。以下部分详细描述了Draw命令之后发生的情况。
着色器
在Direct3D 11中,着色器位于图形管道的不同阶段。它们是由GPU执行的短程序,它接收某些输入数据,处理该数据,然后将结果输出到管道的下一阶段。
Direct3D 11支持三种基本类型的着色器:顶点着色器,几何着色器和像素着色器。
顶点着色器将顶点作为输入。对于通过顶点缓冲区传递给GPU的每个顶点,它运行一次。
几何着色器将基元作为输入,并对传递给GPU的每个基元运行一次。基元是点,线或三角形。
像素着色器将像素(或有时称为片段)作为输入,并且对于我们希望渲染的图元的每个像素运行一次。
顶点,几何和像素着色器一起是动作的主要部分。使用Direct3D 11渲染时,GPU必须具有有效的顶点着色器和像素着色器。几何着色器是Direct3D 11中的高级功能,是可选的,因此我们不会在本教程中讨论几何着色器。在Direct3D 11中,还有用于细分的外壳和域着色器以及用于计算的计算着色器。有关这些的更多信息,请参阅其他示例。
顶点着色器
顶点着色器是GPU在顶点上执行的短程序。将顶点着色器视为C函数,将每个顶点作为输入,处理输入,然后输出修改后的顶点。应用程序以顶点缓冲区的形式将顶点数据传递给GPU后,GPU遍历顶点缓冲区中的顶点,并为每个顶点执行一次活动顶点着色器,将顶点数据作为输入参数传递给顶点着色器。
虽然顶点着色器可用于执行许多任务,但顶点着色器最重要的工作是变换。转换是将矢量从一个坐标系转换为另一个坐标系的过程。例如,3D场景中的三角形可以使其顶点位于(0,0,0)(1,0,0)(0,1,0)的位置。当在2D纹理缓冲区上绘制三角形时,GPU必须知道缓冲区上应该绘制顶点的点的2D坐标。正是转型帮助我们实现了这一目标。转换将在下一个教程中详细讨论。对于本教程,我们将使用一个简单的顶点着色器,除了将输入数据作为输出传递之外什么都不做。
在Direct3D 11教程中,我们将使用高级着色语言(HLSL)编写着色器。回想一下,我们的顶点数据有一个3D位置元素,顶点着色器根本不对输入进行处理。生成的顶点着色器如下所示:
float4 VS( float4 Pos : POSITION ) : SV_POSITION{return Pos;}
这个顶点着色器看起来很像C函数。HLSL使用类C语法使C / C ++程序员更容易学习。
我们可以看到这个名为VS的顶点着色器采用float4类型的参数并返回一个float4值。在HLSL中,float4是一个4分量向量,其中每个分量都是一个浮点数。冒号定义参数的语义以及返回值。
如上所述,HLSL中的语义描述了数据的性质。在上面的着色器中,我们选择POSITION作为Pos输入参数的语义,因为此参数将包含顶点位置。返回值的语义SV_POSITION是具有特殊含义的预定义语义。
这种语义告诉图形管道,与语义相关联的数据定义了剪辑空间位置。GPU需要此位置才能在屏幕上绘制像素。(我们将在下一个教程中讨论剪辑空间。)在我们的着色器中,我们获取输入位置数据并将完全相同的数据输出回管道。
像素着色器
现代计算机显示器通常是光栅显示器,这意味着屏幕实际上是称为像素的小点的二维网格。每个像素包含独立于其他像素的颜色。当我们在屏幕上渲染一个三角形时,我们并不真正将三角形渲染为一个实体。相反,我们点亮了三角形区域所覆盖的像素组。图2显示了这一点。
左:我们想要绘制的内容。右:屏幕上实际是什么。
将由三个顶点定义的三角形转换为由三角形覆盖的一组像素的过程称为光栅化。GPU首先确定被渲染的三角形覆盖哪些像素。然后它为每个像素调用活动像素着色器。像素着色器的主要用途是计算每个像素应具有的颜色。着色器对要着色的像素进行某些输入,计算像素的颜色,然后将该颜色输出回管道。它所采用的输入来自活动几何着色器,或者,如果不存在几何着色器,例如本教程中的情况,则输入直接来自顶点着色器。
我们在上面创建的顶点着色器输出一个带有语义SV_POSITION的float4。这将是我们的像素着色器的输入。由于像素着色器输出颜色值,因此像素着色器的输出将为float4。我们给输出语义SV_TARGET以表示输出到渲染目标格式。像素着色器如下所示:
float4 PS( float4 Pos : SV_POSITION ) : SV_Target{return float4( 1.0f, 1.0f, 0.0f, 1.0f ); // Yellow, with Alpha = 1}
创建着色器
在应用程序代码中,我们需要创建一个顶点着色器和一个像素着色器对象。这些对象代表着色器,通过调用D3DX11CompileFromFile()创建。代码如下所示:
// Compile the vertex shaderID3DBlob* pVSBlob = nullptr;hr = CompileShaderFromFile( L"Tutorial03.fx", "VS", "vs_4_0", &pVSBlob );if( FAILED( hr ) ){MessageBox( nullptr,L"The FX file cannot be compiled. Please run this executable from the directory that contains the FX file.", L"Error", MB_OK );return hr;}// Compile the pixel shaderID3DBlob* pPSBlob = nullptr;hr = CompileShaderFromFile( L"Tutorial03.fx", "PS", "ps_4_0", &pPSBlob );if( FAILED( hr ) ){MessageBox( nullptr,L"The FX file cannot be compiled. Please run this executable from the directory that contains the FX file.", L"Error", MB_OK );return hr;}
把它放在一起
在遍历图形管道之后,我们可以开始理解渲染我们在教程2开始时创建的三角形的过程。
创建Direct3D应用程序需要两个不同的步骤。第一个是在顶点数据中创建源数据,正如我们在教程2中所做的那样。第二个阶段是创建着色器,这些着色器将转换该数据以进行渲染,我们在本教程中展示了这些。
3D空间
3D空间
在上一个教程中,三角形的顶点被有策略地放置,以在屏幕上完美地对齐。但是,情况并非总是如此。因此,我们需要一个系统来表示3D空间中的对象和一个显示它们的系统。
在现实世界中,物体存在于3D空间中。这意味着要将对象放置在世界的特定位置,我们需要使用坐标系并定义与位置对应的三个坐标。
在计算机图形学中,3D空间最常用于笛卡尔坐标系。在该坐标系中,三个轴X,Y和Z彼此垂直,决定了空间中每个点的坐标。该坐标系进一步分为左手和右手系统。在左手系统中,当X轴指向右侧,Y轴指向上方时,Z轴指向前方。在右手系统中,具有相同的X和Y轴,Z轴指向后方。
图1.左手坐标系与右手坐标系
现在我们已经讨论了坐标系,请考虑3D空间。点在不同的空间中具有不同的坐标。作为一维中的一个例子,假设我们有一个标尺,我们注意到标尺的5英寸标记处的点P. 现在,如果我们将标尺向右移动1英寸,则相同的点位于4英寸标记处。通过移动标尺,参考框架已经改变。因此,当点没有移动时,它有一个新的坐标。
图2. 1D中的空间图示
在3D中,空间通常由原点和来自原点的三个唯一轴定义:X,Y和Z.计算机图形中通常使用多个空间:对象空间,世界空间,视图空间,投影空间和屏幕空间。
图3.在对象空间中定义的多维数据集
对象空间
请注意,多维数据集以原点为中心。对象空间,也称为模型空间,是指艺术家在创建3D模型时使用的空间。
通常,艺术家创建以原点为中心的模型,这样就可以更容易地执行转换,例如模型的旋转,正如我们在讨论转换时所看到的那样。八个顶点具有以下坐标:
(-1, 1, -1)
( 1, 1, -1)
(-1, -1, -1)
( 1, -1, -1)
(-1, 1, 1)
( 1, 1, 1)
(-1, -1, 1)
( 1, -1, 1)
因为对象空间是艺术家在设计和创建模型时通常使用的对象空间,所以存储在磁盘上的模型也在对象空间中。应用程序可以创建顶点缓冲区来表示这样的模型,并使用模型数据初始化缓冲区。因此,顶点缓冲区中的顶点通常也位于对象空间中。这也意味着顶点着色器接收对象空间中的输入顶点数据。
世界空间
世界空间是场景中每个对象共享的空间。它用于定义我们希望渲染的对象之间的空间关系。
为了想象世界空间,我们可以想象我们正站在朝北的长方形房间的西南角。我们将我们的脚站立的角落定义为原点,(0,0,0)。X轴向右移动; Y轴上升; 而Z轴向前,与我们面对的方向相同。当我们这样做时,房间中的每个位置都可以用一组XYZ坐标来识别。例如,可能有一把椅子在前方5英尺处,在我们右侧2英尺处。在椅子顶部的8英尺高的天花板上可能有一盏灯。然后我们可以将椅子的位置称为(2,0,5),将灯的位置称为(2,8,5)。如我们所见,
观察空间
视图空间(有时称为相机空间)类似于世界空间,因为它通常用于整个场景。但是,在视图空间中,原点位于查看器或摄像机。视图方向(观察者正在看的地方)定义正Z轴。应用程序定义的"向上"方向变为正Y轴,如下所示。
图4.世界空间(左)和视图空间(右)中的相同对象
左图显示了一个场景,该场景由类似人的物体和观察物体的观察者(相机)组成。世界空间使用的原点和轴以红色显示。右图显示了与世界空间相关的视图空间。视图空间轴显示为蓝色。为了更清楚地说明,视图空间与左图像中的世界空间的方向不同于读者。请注意,在视图空间中,查看器正在Z方向上查看。
投影空间
投影空间是指从视图空间应用投影变换后的空间。在此空间中,可见内容的X和Y坐标范围为-1到1,Z坐标范围为0到1。
屏幕空间
屏幕空间通常用于指代帧缓冲区中的位置。因为帧缓冲区通常是2D纹理,所以屏幕空间是2D空间。左上角是坐标为(0,0)的原点。正X向右,正Y向下。对于w像素宽且h像素高的缓冲区,最右下像素具有坐标(w-1,h-1)。
Space-to-space 变换
转换最常用于将顶点从一个空间转换为另一个空间。在3D计算机图形学中,管道中逻辑上有三种这样的变换:世界,视图和投影变换。下一个教程将介绍单个转换操作,如转换,旋转和缩放。
世界变换
顾名思义,世界变换将顶点从对象空间转换为世界空间。它通常包含一个或多个缩放,旋转和平移,基于我们想要给对象的大小,方向和位置。场景中的每个对象都有自己的世界变换矩阵。这是因为每个对象都有自己的大小,方向和位置。
视角变换
顶点转换为世界空间后,视图转换将这些顶点从世界空间转换为视图空间。回想一下之前的讨论,观看空间是世界从观众(或相机)的角度出现的。在视图空间中,观察者位于沿正Z轴向外看的原点。
值得注意的是,尽管视图空间是来自观察者参照系的世界,但视图变换矩阵应用于顶点,而不是观察者。因此,视图矩阵必须执行我们应用于我们的查看器或相机的相反转换。例如,如果我们想要将摄像机5个单元朝-Z方向移动,我们需要计算一个视图矩阵,它可以沿着+ 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. 因此,
图6.查看Frustum
假设两棵树的尖端恰好位于顶视图平截头体边缘。进一步假设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空间和变换,我们将修改程序,以便在对象空间中定义顶点缓冲区,就像它应该的那样。然后,我们将修改顶点着色器,将顶点从对象空间转换为投影空间。