之前,您在使用DirectX设备资源中了解了如何创建可用于绘制的窗口。现在,您将学习如何构建图形管道,以及可以在哪里挂钩。
您会记得有两个定义图形管道的Direct3D接口:ID3D11Device,它提供GPU及其资源的虚拟表示;和ID3D11DeviceContext,表示管道的图形处理。通常,您使用ID3D11Device的实例来配置和获取开始处理场景中图形所需的GPU资源,并使用ID3D11DeviceContext在图形管道中的每个适当着色器阶段处理这些资源。您通常不经常调用ID3D11Device方法——在您设置场景或设备更改时。另一方面,每次处理显示帧时,都会调用ID3D11DeviceContext。
此示例创建并配置适合显示简单旋转,顶点阴影立方体的最小图形管道。它演示了显示所需的最小资源集。在阅读此处的信息时,请注意给定示例的限制,您可能需要对其进行扩展以支持要渲染的场景。
此示例包含两个用于图形的C ++类:设备资源管理器类和3D场景渲染器类。本主题专门针对3D场景渲染器。
多维数据集渲染器有什么作用?
图形管道由3D场景渲染器类定义。场景渲染器能够:
- 定义常量缓冲区以存储统一数据。
- 定义顶点缓冲区以保存对象顶点数据,以及相应的索引缓冲区,以使顶点着色器能够正确地处理三角形。
- 创建纹理资源和资源视图。
- 加载着色器对象。
- 更新图形数据以显示每个帧。
- 将图形渲染(绘制)到交换链。
前四个进程通常使用ID3D11Device接口方法初始化和管理图形资源,后两个使用ID3D11DeviceContext接口方法来管理和执行图形管道。
Renderer类的实例作为主项目类的成员变量创建和管理。 DeviceResources实例作为跨多个类的共享指针进行管理,包括主项目类,App视图提供程序类和Renderer。如果用自己的类替换Renderer,请考虑将DeviceResources实例声明并指定为共享指针成员:
std::shared_ptr<DX::DeviceResources> m_deviceResources;
在App类的Initialize方法中创建DeviceResources实例后,只需将指针传递给类构造函数(或其他初始化方法)。如果您希望主类完全拥有DeviceResources实例,也可以传递weak_ptr引用。
创建多维数据集渲染器
在此示例中,我们使用以下方法组织场景渲染器类:
- CreateDeviceDependentResources:每当必须初始化或重新启动场景时调用。此方法加载初始顶点数据,纹理,着色器和其他资源,并构造初始常量和顶点缓冲区。通常,这里的大部分工作都是使用ID3D11Device方法完成的,而不是ID3D11DeviceContext方法。
- CreateWindowSizeDependentResources:每当窗口状态发生变化时调用,例如调整大小或方向更改时。此方法可重建变换矩阵,例如相机的变换矩阵。
- Update:通常从管理即时游戏状态的程序部分调用;在这个例子中,我们只是从Main类中调用它。让此方法从影响渲染的任何游戏状态信息中读取,例如对象位置或动画帧的更新,以及任何全局游戏数据,如光照级别或游戏物理变化。这些输入用于更新每帧常量缓冲区和对象数据。
- Render:通常从管理游戏循环的程序部分调用;在这种情况下,它是从Main类调用的。此方法构造图形管道:它绑定着色器,将缓冲区和资源绑定到着色器阶段,并调用当前帧的绘图。
这些方法包含使用您的资源和Direct3D渲染场景的行为体。如果使用新的渲染类扩展此示例,请在主项目类中声明它。所以这:
std::unique_ptr<Sample3DSceneRenderer> m_sceneRenderer;
应变成这个:
std::unique_ptr<MyAwesomeNewSceneRenderer> m_sceneRenderer;
再次注意,此示例假定方法在您的实现中具有相同的签名。如果签名已更改,请查看“Main”循环并相应地进行更改。
让我们更详细地看一下场景渲染方法。
创建设备相关资源
CreateDeviceDependentResources使用ID3D11Device调用合并初始化场景及其资源的所有操作。 此方法假定Direct3D设备刚刚为场景初始化(或已重新创建)。 它重新创建或重新加载所有特定于场景的图形资源,例如顶点和像素着色器,对象的顶点和索引缓冲区以及任何其他资源(例如,作为纹理及其对应的视图)。
这是CreateDeviceDependentResources的示例代码:
void Renderer::CreateDeviceDependentResources(){
// Compile shaders using the Effects library.
auto CreateShadersTask=Concurrency::create_task([this](){
CreateShaders();
}
);
// Load the geometry for the spinning cube.
auto CreateCubeTask=CreateShadersTask.then([this](){
CreateCube();
}
);
}
void Renderer::CreateWindowSizeDependentResources(){
// Create the view matrix and the perspective matrix.
CreateViewAndPerspective();
}
无论何时从磁盘资源(如编译的着色器对象(CSO或.cso)文件或纹理)加载资源,都要异步执行。 这允许您同时保持其他工作(与其他设置任务一样),并且因为主循环未被阻止,您可以继续向用户显示视觉上感兴趣的内容(如游戏的加载动画)。 此示例使用从Windows 8开始可用的Concurrency :: Tasks API; 请注意用于封装异步加载任务的lambda语法。 这些lambdas表示称为off-thread的函数,因此显式捕获指向当前类对象(this)的指针。
这是一个如何加载着色器字节码的示例:
HRESULT hr=S_OK;
// Use the Direct3D device to load resources into graphics memory.
ID3D11Device* device=m_deviceResources->GetDevice();
// You'll need to use a file loader to load the shader bytecode. In this
// example, we just use the standard library.
FILE *vShader,*pShader;
BYTE *bytes;
size_t destSize=4096;
size_t bytesRead=0;
bytes=new BYTE[destSize];
fopen_s(&vShader,"CubeVertexShader.cso","rb");
bytesRead=fread_s(bytes,destSize,1,4096,vShader);
hr=device->CreateVertexShader(bytes,bytesRead,nullptr,&m_pVertexShader);
D3D11_INPUT_ELEMENT_DESC iaDesc[]={
{"POSITION",0,DXGI_FORMAT_R32G32B32_FLOAT,0,0,D3D11_INPUT_PER_VERTEX_DATA,0},
{"COLOR",0,DXGI_FORMAT_R32G32B32_FLOAT,0,12,D3D11_INPUT_PER_VERTEX_DATA,0},
};
hr=device->CreateInputLayout(iaDesc,ARRAYSIZE(iaDesc),bytes,bytesRead,&m_pInputLayout);
delete bytes;
bytes=new BYTE[destSize];
bytesRead=0;
fopen_s(&pShader,"CubePixelShader.cso","rb");
bytesRead=fread_s(bytes,destSize,1,4096,pShader);
hr=device->CreatePixelShader(bytes,bytesRead,nullptr,m_pPixelShader.GetAddressOf());
delete bytes;
CD3D11_BUFFER_DESC cbDesc(sizeof(ConstantBufferStruct),D3D11_BIND_CONSTANT_BUFFER);
hr=device->CreateBuffer(&cbDesc,nullptr,m_pConstantBuffer.GetAddressOf());
fclose(vShader);
fclose(pShader);
这是一个创建顶点和索引缓冲区的示例:
HRESULT Renderer::CreateCube(){
HRESULT hr=S_OK;
// Use the Direct3D device to load resources into graphics memory.
ID3D11Device* device=m_deviceResources->GetDevice();
// Create cube geometry.
VertexPositionColor CubeVertices[]={
{DirectX::XMFLOAT3(-0.5f,-0.5f,-0.5f),DirectX::XMFLOAT3(0,0,0),},
{DirectX::XMFLOAT3(-0.5f,-0.5f,0.5f),DirectX::XMFLOAT3(0,0,1),},
{DirectX::XMFLOAT3(-0.5f,0.5f,-0.5f),DirectX::XMFLOAT3(0,1,0),},
{DirectX::XMFLOAT3(-0.5f,0.5f,0.5f),DirectX::XMFLOAT3(0,1,1),},
{DirectX::XMFLOAT3(0.5f,-0.5f,-0.5f),DirectX::XMFLOAT3(1,0,0),},
{DirectX::XMFLOAT3(0.5f,-0.5f,0.5f),DirectX::XMFLOAT3(1,0,1),},
{DirectX::XMFLOAT3(0.5f,0.5f,-0.5f),DirectX::XMFLOAT3(1,1,0),},
{DirectX::XMFLOAT3(0.5f,0.5f,0.5f),DirectX::XMFLOAT3(1,1,1),},
};
// Create vertex buffer:
CD3D11_BUFFER_DESC vDesc(sizeof(CubeVertices),D3D11_BIND_VERTEX_BUFFER);
D3D11_SUBRESOURCE_DATA vData;
ZeroMemory(&vData,sizeof(D3D11_SUBRESOURCE_DATA));
vData.pSysMem=CubeVertices;
vData.SysMemPitch=0;
vData.SysMemSlicePitch=0;
hr=device->CreateBuffer(&vDesc,&vData,&m_pVertexBuffer);
// Create index buffer:
unsigned short CubeIndices[]={
0,2,1, // -x
1,2,3,
4,5,6, // +x
5,7,6,
0,1,5, // -y
0,5,4,
2,6,7, // +y
2,7,3,
0,4,6, // -z
0,6,2,
1,3,7, // +z
1,7,5,
};
m_indexCount=ARRAYSIZE(CubeIndices);
CD3D11_BUFFER_DESC iDesc(sizeof(CubeIndices),D3D11_BIND_INDEX_BUFFER);
D3D11_SUBRESOURCE_DATA iData;
ZeroMemory(&iData,sizeof(D3D11_SUBRESOURCE_DATA));
iData.pSysMem=CubeIndices;
iData.SysMemPitch=0;
iData.SysMemSlicePitch=0;
hr=device->CreateBuffer(&iDesc,&iData,&m_pIndexBuffer);
return hr;
}
此示例不加载任何网格或纹理。您必须创建用于加载特定于游戏的网格和纹理类型的方法,并异步调用它们。
此处也为每个场景的常量缓冲区填充初始值。每场景常量缓冲区的示例包括固定灯或其他静态场景元素和数据。
实现CreateWindowSizeDependentResources方法
每次窗口大小,方向或分辨率更改时,都会调用CreateWindowSizeDependentResources方法。
窗口大小资源更新如下:静态消息proc获取指示窗口状态更改的几个可能事件之一。然后,您的主循环将被告知事件,并在主类实例上调用CreateWindowSizeDependentResources,然后在场景渲染器类上调用CreateWindowSizeDependentResources实现。
此方法的主要工作是确保视觉效果不会因窗口属性的更改而变得混乱或无效。在此示例中,我们使用新的视野(FOV)更新项目矩阵,用于调整大小或重定向的窗口。
我们已经看到了在DeviceResources中创建窗口资源的代码——即交换链(带后台缓冲区)和渲染目标视图。以下是渲染器如何创建与宽高比相关的变换:
void Renderer::CreateViewAndPerspective(){
// Use DirectXMath to create view and perspective matrices.
DirectX::XMVECTOR eye=DirectX::XMVectorSet(0.0f,0.7f,1.5f,0.f);
DirectX::XMVECTOR at=DirectX::XMVectorSet(0.0f,-0.1f,0.0f,0.f);
DirectX::XMVECTOR up=DirectX::XMVectorSet(0.0f,1.0f,0.0f,0.f);
DirectX::XMStoreFloat4x4(&m_constantBufferData.view,DirectX::XMMatrixTranspose(DirectX::XMMatrixLookAtRH(eye,at,up)));
float aspectRatio=m_deviceResources->GetAspectRatio();
DirectX::XMStoreFloat4x4(&m_constantBufferData.projection,DirectX::XMMatrixTranspose(DirectX::XMMatrixPerspectiveFovRH(DirectX::XMConvertToRadians(70),aspectRatio,0.01f,100.0f)));
}
如果您的场景具有取决于宽高比的特定组件布局,则可以重新排列它们以匹配该宽高比。您可能还想在此处更改后处理行为的配置。
实现Update方法
每个游戏循环调用一次Update方法——在本例中,它由主类的同名方法调用。它有一个简单的目的:根据自上一帧以来经过的时间(或经过的时间步长)更新场景几何和游戏状态。在这个例子中,我们只是每帧旋转一次立方体。在真实游戏场景中,此方法包含更多用于检查游戏状态,相应地更新每帧(或其他动态)常量缓冲区,几何缓冲区和其他内存中资源的代码。由于CPU和GPU之间的通信会产生开销,因此请确保只更新自上一帧以来实际已更改的缓冲区——可以根据需要对常量缓冲区进行分组或拆分,以提高效率。
void Renderer::Update(){
// Rotate the cube 1 degree per frame.
DirectX::XMStoreFloat4x4(&m_constantBufferData.world,DirectX::XMMatrixTranspose(DirectX::XMMatrixRotationY(DirectX::XMConvertToRadians((float)m_frameCount++))));
if(m_frameCount==MAXUINT)
m_frameCount=0;
}
在这种情况下,Rotate使用新的多维数据集转换矩阵更新常量缓冲区。在顶点着色器阶段期间,矩阵将按顶点相乘。由于每个帧都调用此方法,因此这是聚合更新动态常量和顶点缓冲区的任何方法,或执行任何其他操作以准备场景中的对象以供图形管道进行转换的好地方。
实现Render方法
调用Update后,每个游戏循环调用一次此方法。与Update类似,Render方法也是从主类调用的。这是使用ID3D11DeviceContext实例上的方法为帧构造和处理图形管道的方法。这最终调用了ID3D11DeviceContext::DrawIndexed。重要的是要理解这个调用(或在ID3D11DeviceContext上定义的其他类似的Draw*调用)实际上执行了管道。具体来说,这是Direct3D与GPU通信以设置绘图状态,运行每个管道阶段,并将像素结果写入渲染目标缓冲区资源以供交换链显示。由于CPU和GPU之间的通信会产生开销,因此如果可以的话,将多个绘制调用合并为一个,特别是如果您的场景有很多渲染对象。
void Renderer::Render(){
// Use the Direct3D device context to draw.
ID3D11DeviceContext *context=m_deviceResources->GetDeviceContext();
ID3D11RenderTargetView *renderTarget=m_deviceResources->GetRenderTarget();
ID3D11DepthStencilView *depthStencil=m_deviceResources->GetDepthStencil();
context->UpdateSubresource(m_pConstantBuffer.Get(),0,nullptr,&m_constantBufferData,0,0);
// Clear the render target and the z-buffer.
const float teal[]={0.098f,0.439f,0.439f,1.000f};
context->ClearRenderTargetView(renderTarget,teal);
context->ClearDepthStencilView(depthStencil,D3D11_CLEAR_DEPTH|D3D11_CLEAR_STENCIL,1.0f,0);
// Set the render target.
context->OMSetRenderTargets(1,&renderTarget,depthStencil);
// Set up the IA stage by setting the input topology and layout.
UINT stride=sizeof(VertexPositionColor);
UINT offset=0;
context->IASetVertexBuffers(0,1,m_pVertexBuffer.GetAddressOf(),&stride,&offset);
context->IASetIndexBuffer(m_pIndexBuffer.Get(),DXGI_FORMAT_R16_UINT,0);
context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
context->IASetInputLayout(m_pInputLayout.Get());
// Set up the vertex shader stage.
context->VSSetShader(m_pVertexShader.Get(),nullptr,0);
context->VSSetConstantBuffers(0,1,m_pConstantBuffer.GetAddressOf());
// Set up the pixel shader stage.
context->PSSetShader(m_pPixelShader.Get(),nullptr,0);
// Calling Draw tells Direct3D to start sending commands to the graphics device.
context->DrawIndexed(m_indexCount,0,0);
}
最好按顺序在上下文中设置各种图形管道阶段。通常,顺序是:
- 根据需要使用新数据刷新常量缓冲区资源(使用Update中的数据)。
- 输入程序集(IA):这是我们附加定义场景几何的顶点和索引缓冲区的地方。您需要为场景中的每个对象附加每个顶点和索引缓冲区。因为这个例子只有立方体,所以非常简单。
- 顶点着色器(VS):附加将在顶点缓冲区中变换数据的任何顶点着色器,并为顶点着色器附加常量缓冲区。
- 像素着色器(PS):附加将在光栅化场景中执行每像素操作的像素着色器,并附加像素着色器的设备资源(常量缓冲区,纹理等)。
- 输出合并(OM):这是在着色器完成后混合像素的阶段。这是规则的一个例外,因为您在设置任何其他阶段之前附加深度模板并渲染目标。如果您有额外的顶点和像素着色器生成纹理(如阴影贴图,高度贴图或其他采样技术),则可能有多个模板和目标 - 在这种情况下,每个绘图过程都需要在调用之前设置相应的目标绘制功能。
接下来,在最后一节(使用着色器和着色器资源)中,我们将查看着色器并讨论Direct3D如何执行它们。
相关话题
接下来