DirectX11入门到实践-(3)渲染篇
从编写Shader到将顶点渲染到屏幕上的过程
来看一下Shader文件的定义
[Shader.h]#pragma once#include <d3d11.h>#include <wrl/client.h>#include <d3dcompiler.h>#include "..\Basic\ErrorLogger.h"class VertexShader{public:bool Initialize(Microsoft::WRL::ComPtr<ID3D11Device> &device, std::wstring shaderpath, D3D11_INPUT_ELEMENT_DESC * layoutDesc, UINT numElements);ID3D11VertexShader * GetShader();ID3D10Blob* GetBuffer();ID3D11InputLayout* GetInputLayout();private:Microsoft::WRL::ComPtr<ID3D11VertexShader> shader;Microsoft::WRL::ComPtr<ID3D10Blob> shaderBuffer;Microsoft::WRL::ComPtr<ID3D11InputLayout> inputLayout;};class PixelShader{public:bool Initialize(Microsoft::WRL::ComPtr<ID3D11Device> &device, std::wstring shaderpath);ID3D11PixelShader* GetShader();ID3D10Blob* GetBuffer();private:Microsoft::WRL::ComPtr<ID3D11PixelShader> shader;Microsoft::WRL::ComPtr<ID3D10Blob> shaderBuffer;};
Shader文件读取与初始化
[Shader.cpp]bool VertexShader::Initialize(Microsoft::WRL::ComPtr<ID3D11Device>& device, std::wstring shaderpath, D3D11_INPUT_ELEMENT_DESC* layoutDesc, UINT numElements){HRESULT hr = D3DReadFileToBlob(shaderpath.c_str(), this->shaderBuffer.GetAddressOf()); // 读取shaderFile.cso文件if (FAILED(hr)){std::wstring errorMsg = L"Failed to load shader: ";errorMsg += shaderpath;ErrorLogger::Log(hr, errorMsg);return false;}hr = device->CreateVertexShader(this->shaderBuffer->GetBufferPointer(), this->shaderBuffer->GetBufferSize(), NULL, this->shader.GetAddressOf());if (FAILED(hr)){std::wstring errorMsg = L"Failed to create vertex shader: ";errorMsg += shaderpath;ErrorLogger::Log(hr, errorMsg);return false;}hr = device->CreateInputLayout(layoutDesc, numElements, this->shaderBuffer->GetBufferPointer(), this->shaderBuffer->GetBufferSize(), this->inputLayout.GetAddressOf());if (FAILED(hr)){ErrorLogger::Log(hr, "Failed to create input layout.");return false;}return true;}PixelShader的读取过程与之类似,但没有输入布局及其处理过程,在此略过
我们来看下Shader文件的编写,首先要定义Vertex结构:
[Vertex.h]#include <DirectXMath.h>struct Vertex{Vertex() {}Vertex(float x, float y, float z, float u, float v) : position(x, y, z), texCoord(u, v) {}DirectX::XMFLOAT3 position;DirectX::XMFLOAT2 texCoord;};
它的存在实际上是为了在C++这边与HLSL的结构对应:
[VertexShader.hlsl]struct VS_INPUT // HLSL输入结构体{float3 inPos : POSITION;float2 inTexCoord : TEXCOORD; // 贴图UV};struct VS_OUTPUT // 输出结构体{float4 outPosition : SV_POSITION;float2 outTexCoord : TEXCOORD;};VS_OUTPUT main(VS_INPUT input) // 逻辑执行{VS_OUTPUT output;output.outPosition = mul(float4(input.inPos, 1.0f), mat);output.outTexCoord = input.inTexCoord;return output; // 输出修改后的顶点}
我们再看下Shader文件是在哪里初始化的(Graphics::InitializeShaders)
[Graphics.cpp]bool Graphics::InitializeShaders(){// 文件路径std::wstring shaderfolder = L"..\\x64\\Release\\";// 定义输入布局,来描述我们声明的hlsl结构体每一个成员的 用途、语义、大小等信息D3D11_INPUT_ELEMENT_DESC layout[] ={{"POSITION", 0, DXGI_FORMAT::DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_CLASSIFICATION::D3D11_INPUT_PER_VERTEX_DATA, 0 },{"TEXCOORD", 0, DXGI_FORMAT::DXGI_FORMAT_R32G32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_CLASSIFICATION::D3D11_INPUT_PER_VERTEX_DATA, 0 },};UINT numElements = ARRAYSIZE(layout);if (!vertexshader.Initialize(this->device, shaderfolder + L"VertexShader.cso", layout, numElements)) // CreateShader等接口需要通过device调用return false;if (!pixelshader.Initialize(this->device, shaderfolder + L"PixelShader.cso"))return false;return true;}
由此可见,HLSL、C++ Struct、InputLayout 需要一一对应
> 这里的输入布局也可以直接定义在VertexShader.h中,作为static成员(不占用结构体空间),在需要创建InputLayout时取用
接下来,在实际渲染之前,我们要准备 顶点缓冲区、索引缓冲区和常量缓冲区
顶点缓冲区
用于保存顶点数据,这对于三角形绘制必不可少
[VertexBuffer.h]#include <d3d11.h>#include <wrl/client.h>#include <memory>template<class T = Vertex> // 将顶点缓冲区封装成模板,然后一行Initialize就能执行原来N行的缓冲区创建、device绑定class VertexBuffer{private:Microsoft::WRL::ComPtr<ID3D11Buffer> buffer;std::shared_ptr<UINT> stride;UINT bufferSize = 0;public:VertexBuffer() {}VertexBuffer(const VertexBuffer<T>& rhs){this->buffer = rhs.buffer;this->bufferSize = rhs.bufferSize;this->stride = rhs.stride;}VertexBuffer<T> & operator=(const VertexBuffer<T>& a){this->buffer = a.buffer;this->bufferSize = a.bufferSize;this->stride = a.stride;return *this;}ID3D11Buffer* Get() const;ID3D11Buffer* const* GetAddressOf() const;UINT BufferSize() const;const UINT Stride() const;const UINT* StridePtr() const;HRESULT Initialize(ID3D11Device *device, T* data, UINT numVertices){if (buffer.Get() != nullptr)buffer.Reset();this->bufferSize = numVertices;if (this->stride.get() == nullptr)this->stride = std::make_shared<UINT>(sizeof(T));D3D11_BUFFER_DESC vertexBufferDesc; // 缓冲区描述ZeroMemory(&vertexBufferDesc, sizeof(vertexBufferDesc));vertexBufferDesc.Usage = D3D11_USAGE_DEFAULT;vertexBufferDesc.ByteWidth = sizeof(T) * numVertices;vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;vertexBufferDesc.CPUAccessFlags = 0;vertexBufferDesc.MiscFlags = 0;D3D11_SUBRESOURCE_DATA vertexBufferData; // 存放的顶点数据ZeroMemory(&vertexBufferData, sizeof(vertexBufferData));vertexBufferData.pSysMem = data;HRESULT hr = device->CreateBuffer(&vertexBufferDesc, &vertexBufferData, this->buffer.GetAddressOf()); // 创建缓冲区return hr;}};
索引缓冲区
其实和顶点缓冲区写法非常相似。索引(index)是指顶点的渲染顺序,索引缓冲区用于保存顶点绘制顺序,这样可以支持"共享顶点",提高顶点缓冲区的利用率
class IndexBuffer{private:IndexBuffer(const IndexBuffer& rhs);private:Microsoft::WRL::ComPtr<ID3D11Buffer> buffer;UINT indexCount = 0;public:IndexBuffer() {}HRESULT Initialize(ID3D11Device *device, DWORD * data, UINT indexCount){if (buffer.Get() != nullptr)buffer.Reset();this->indexCount = indexCount;// Load Index DataD3D11_BUFFER_DESC indexBufferDesc;ZeroMemory(&indexBufferDesc, sizeof(indexBufferDesc));indexBufferDesc.Usage = D3D11_USAGE_DEFAULT;indexBufferDesc.ByteWidth = sizeof(DWORD) * indexCount;indexBufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER;indexBufferDesc.CPUAccessFlags = 0;indexBufferDesc.MiscFlags = 0;D3D11_SUBRESOURCE_DATA indexBufferData;indexBufferData.pSysMem = data;HRESULT hr = device->CreateBuffer(&indexBufferDesc, &indexBufferData, buffer.GetAddressOf());return hr;}};
常量缓冲区
在HLSL中,常量缓冲区的变量类似于C++这边的全局常量,供着色器代码使用。示例:
[VertexShader.hlsl]cbuffer mycBuffer : register(b0) // cbuffer用于声明常量缓冲区,register(b0)表示该缓冲区位于寄存器索引为0处的缓冲区{float4x4 mat; // matrix与float4x4作用等价};
然后在C++这边写对应的ConstantBuffer处理:
[ConstantBufferType.h]struct CB_VS_VertexShader // 对应HLSL中定义的结构{DirectX::XMMATRIX matrix;};struct CB_PS_Light{DirectX::XMFLOAT3 ambientLightColor;float ambientLightStrength;};struct CB_PS_PixelShader{float alpha = 1.0f;};[ConstantBuffer.h]template<class T>class ConstantBuffer{private:ConstantBuffer(const ConstantBuffer<T>& rhs);private:Microsoft::WRL::ComPtr<ID3D11Buffer> buffer;ID3D11DeviceContext * deviceContext = nullptr;public:ConstantBuffer() {}T data;ID3D11Buffer* Get() const;ID3D11Buffer* const* GetAddressOf() const;HRESULT Initialize(ID3D11Device *device, ID3D11DeviceContext * deviceContext){if (buffer.Get() != nullptr)buffer.Reset();this->deviceContext = deviceContext;D3D11_BUFFER_DESC desc;desc.Usage = D3D11_USAGE_DYNAMIC;desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;desc.MiscFlags = 0;desc.ByteWidth = static_cast<UINT>(sizeof(T) + (16 - (sizeof(T) % 16)));desc.StructureByteStride = 0;HRESULT hr = device->CreateBuffer(&desc, 0, buffer.GetAddressOf());return hr;}bool ApplyChanges(){D3D11_MAPPED_SUBRESOURCE mappedResource;HRESULT hr = this->deviceContext->Map(buffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource);if (FAILED(hr)){ErrorLogger::Log(hr, "Failed to map constant buffer.");return false;}CopyMemory(mappedResource.pData, &data, sizeof(T));this->deviceContext->Unmap(buffer.Get(), 0);return true;}};
目前资源有两种动态更新方式:允许常量缓冲区从GPU写入(deviceContext::UpdateSubresource),或从CPU写入(ApplyChanges)。其他缓冲区和资源纹理更新也同样可以用上述两种方式
HLSL常量缓冲区打包规则:略
最后一步,设置着色器等绘制工作
[Graphics.cpp] // 每帧干的事void Graphics::RenderFrame(){float bgcolor[] = { 1.0f, 1.0f, 1.0f, 0.0f };this->deviceContext->ClearRenderTargetView(this->renderTargetView.Get(), bgcolor);this->deviceContext->ClearDepthStencilView(this->depthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);this->deviceContext->IASetInputLayout(this->vertexshader.GetInputLayout()); // 设置输入布局this->deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY::D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST); // 使用三角形列表形式的拓扑结构,而非三角形条带this->deviceContext->RSSetState(this->rasterizerState.Get());this->deviceContext->OMSetDepthStencilState(this->depthStencilState.Get(), 0);this->deviceContext->OMSetBlendState(NULL, NULL, 0xFFFFFFFF);this->deviceContext->PSSetSamplers(0, 1, this->samplerState.GetAddressOf());this->deviceContext->VSSetShader(vertexshader.GetShader(), NULL, 0); // 设置着色器this->deviceContext->PSSetShader(pixelshader.GetShader(), NULL, 0);
顶点着色器主要工作是变换,
而Pixel着色器主要作用是计算每个像素应具有的颜色:
[PixelShader.hlsl]// 添加光照需要的ConstantBuffer(删掉了alpha因为一个shader只能有一个buffer)cbuffer lightBuffer : register(b0){float3 ambientLightColor;float ambientLightStrength;}struct PS_INPUT{float4 inPosition : SV_POSITION;float2 inTexCoord : TEXCOORD;};Texture2D objTexture : TEXTURE : register(t0);SamplerState objSamplerState : SAMPLER : register(s0);float4 main(PS_INPUT input) : SV_TARGET{float3 sampleColor = objTexture.Sample(objSamplerState, input.inTexCoord);// 光照与纹理颜色混合float3 ambientLight = ambientLightColor * ambientLightStrength;float3 finalColor = sampleColor * ambientLight;return float4(finalColor, 1.0f);}
然后我们就真的可以开始Draw了(处理传递到图形管道的顶点数据)
[Graphics.cpp]// Graphics::RenderFrame()中XMMATRIX matrix = camera.GetViewMatrix() * camera.GetProjectionMatrix(); // 视角this->plane.Draw(matrix);this->skybox.Draw(camera, matrix);this->car.Draw(matrix);this->swapchain->Present(0, NULL);}
这里我们将要画的内容封装成了具体的对象,后续再详述其是怎么绘制的
简单代码示例教程
绘制三角形
使用着色器
1.从.shader文件加载并编译两个着色器。
HRESULT D3DX11CompileFromFile(LPCTSTR pSrcFile,//源代码文件D3D10_SHADER_MACRO * pDefines,//高级LPD3D10INCLUDE pInclude,//高级LPCSTR pFunctionName,//着色器的名称LPCSTR pProfile,//着色器配置代码UINT FLAGS1,//高级UINT Flags2,//高级ID3DX11ThreadPump * pPump,//高级ID3D10Blob ** ppShader,// blob包含已编译的着色器ID3D10Blob ** ppErrorMsgs,//高级HRESULT * pHResult); //高级// Compile the vertex shaderID3DBlob* pVSBlob = nullptr;hr = CompileShaderFromFile( L"Tutorial02.fx", "VS", "vs_4_0", &pVSBlob );
2.将两个着色器封装到着色器对象中。
// globalID3D11VertexShader* g_pVertexShader = nullptr;ID3D11PixelShader* g_pPixelShader = nullptr;// InitDevice()hr = g_pd3dDevice->CreateVertexShader( pVSBlob->GetBufferPointer(), pVSBlob->GetBufferSize(), nullptr, &g_pVertexShader );hr = g_pd3dDevice->CreatePixelShader( pPSBlob->GetBufferPointer(), pPSBlob->GetBufferSize(), nullptr, &g_pPixelShader );
这里有四个参数,显而易见,第一个是编译数据的地址。第二个是文件数据的大小。第四个是着色器对象的地址。
第三个是先进的,稍后将介绍。
3.将两个着色器设置为活动着色器。
Render():// Render a triangleg_pImmediateContext->VSSetShader( g_pVertexShader, nullptr, 0 );g_pImmediateContext->PSSetShader( g_pPixelShader, nullptr, 0 );g_pImmediateContext->Draw( 3, 0 );
第一个参数是要设置的着色器对象的地址,而其他两个是高级的。
请记住,pVS和pPS都是COM对象,因此必须释放它们。
顶点缓冲区
Direct3D使用所谓的输入布局。输入布局是包含顶点的位置和属性的数据的布局。
创建顶点
//定义结构体struct VERTEX{FLOAT X,Y,Z; //位置D3DXCOLOR颜色; // color};//顶点VERTEX OurVertex = {0.0f,0.5f,0.0f,D3DXCOLOR(1.0f,0.0f,0.0f,1.0f)};//三角形VERTEX OurVertices [] ={{0.0f,0.5f,0.0f,D3DXCOLOR(1.0f,0.0f,0.0f,1.0f)},{0.45f,-0.5,0.0f,D3DXCOLOR(0.0f,1.0f, 0.0f,1.0f)},{ - 0.45f,-0.5f,0.0f,D3DXCOLOR(0.0f,0.0f,1.0f,1.0f)}};
创建顶点缓冲区
在C ++中创建结构时,数据存储在系统内存中。但我们需要它在显存中,而我们无法轻松访问显存。
为了允许我们访问显卡内存,Direct3D为我们提供了一个特定的COM对象,它可以让我们在系统和显卡内存中保持缓冲区。
缓冲区如何存在于系统和显卡内存中?好吧,最初这种缓冲区中的数据将存储在系统内存中。在为其渲染调用时,Direct3D会自动将其复制到显卡内存中。如果显卡内存不足,Direct3D将删除暂时未使用的缓冲区,或被视为"低优先级",以便为更新的资源腾出空间。
为什么我们需要Direct3D为我们这样做?嗯,这很难自己做,因为访问显卡内存会因显卡和操作系统版本而异。让Direct3D为我们管理这个非常非常方便。
此COM对象称为ID3D11Buffer。要创建它,我们使用CreateBuffer()函数。这是代码的样子:
// global
ID3D11Buffer* g_pVertexBuffer = nullptr;
D3D11_BUFFER_DESC bd;
ZeroMemory( &bd, sizeof(bd));
bd.Usage = D3D11_USAGE_DEFAULT; //控制CPU和GPU访问权限
bd.ByteWidth = sizeof( SimpleVertex ) * 3; // 要创建的缓冲区的大小。size是VERTEX结构* 3
bd.BindFlags = D3D11_BIND_VERTEX_BUFFER; //用作顶点缓冲区
bd.CPUAccessFlags = 0; //允许CPU写入缓冲区
D3D11_SUBRESOURCE_DATA InitData;
ZeroMemory( &InitData, sizeof(InitData) );
InitData.pSysMem = vertices;
hr = g_pd3dDevice->CreateBuffer( &bd, &InitData, &g_pVertexBuffer ); //创建缓冲区
CreateBuffer(&bd,NULL,&pVBuffer)
第一个参数是描述struct的地址。第二个参数可用于在创建时使用某些数据初始化缓冲区,但我们在此处将其设置为NULL。第三个参数是缓冲区对象的地址。
填充顶点缓冲区
现在我们需要做的是将顶点复制到缓冲区中。
但是,因为Direct3D可能在后台使用缓冲区,所以缓冲区永远不会让您直接访问它。
要访问它,必须映射缓冲区。这意味着Direct3D允许完成缓冲区的任何操作,然后阻止GPU使用缓冲区,直到它被取消映射。
1. 映射顶点缓冲区(从而获得缓冲区的位置)。
2. 将数据复制到缓冲区(使用memcpy())。
3. 取消映射缓冲区。
D3D11_MAPPED_SUBRESOURCE ms;devcon-> Map(pVBuffer,NULL,D3D11_MAP_WRITE_DISCARD,NULL,&ms); //映射缓冲区memcpy(ms.pData,OurVertices,sizeof(OurVertices)); //复制数据devcon-> Unmap(pVBuffer,NULL); //取消映射缓冲区
……
验证输入布局
到目前为止,在本课中我们有
A)加载和设置着色器以控制管道,
B)使用顶点创建形状并准备好供GPU使用。
当我们将它们放在我们自己创建的结构中时,您可能想知道GPU是如何读取顶点的。怎么知道我们在颜色之前放置了位置?怎么知道我们没有别的意思呢?
答案是输入布局。
如前所述,输入布局是一个包含顶点结构布局的对象,让GPU可以适当有效地组织数据。我们能够选择每个顶点存储的信息,以提高渲染速度。
创建输入元素
顶点布局由一个或多个输入元素组成。输入元素是顶点的一个属性,例如位置或颜色。
每个元素都由一个名为D3D11_INPUT_ELEMENT_DESC的结构定义。此结构描述单个顶点属性。
要创建具有多个属性的顶点,我们只需将这些结构放入数组中。
D3D11_INPUT_ELEMENT_DESC ied [] ={{"POSITION",0,DXGI_FORMAT_R32G32B32_FLOAT,0,0,D3D11_INPUT_PER_VERTEX_DATA,0},{"COLOR",0,DXGI_FORMAT_R32G32B32A32_FLOAT,0,12,D3D11_INPUT_PER_VERTEX_DATA,0},};
结构体:语义,语义索引,数据的格式,输入槽,元素到结构中的字节数(偏移量),元素的用途,0
创建输入布局对象
调用CreateInputLayout(),从而创建一个表示顶点格式的对象。
hr = g_pd3dDevice->CreateInputLayout( layout, numElements, pVSBlob->GetBufferPointer(),pVSBlob->GetBufferSize(), &g_pVertexLayout );// Set the input layoutg_pImmediateContext->IASetInputLayout( g_pVertexLayout );HRESULT CreateInputLayout(D3D11_INPUT_ELEMENT_DESC * pInputElementDescs, //指向元素描述数组的指针UINT NumElements, //数组中元素的数量void * pShaderBytecodeWithInputSignature, //指向管道中第一个着色器的指针,它是顶点着色器SIZE_T BytecodeLength, //着色器文件的长度ID3D11InputLayout ** pInputLayout); //指向输入布局对象的指针
在我们前进之前的最后一件事。在设置之前,创建输入布局不会执行任何操作。
要设置输入布局,我们调用IASetInputLayout() 函数。它唯一的参数是输入布局对象。
原始绘制
我们调用了三个简单的函数来进行渲染。
第一个设置我们打算使用的顶点缓冲区。
第二个设置我们打算使用哪种类型的基元(例如三角形列表,线条等)。
第三个实际绘制形状。
// Set vertex buffer
UINT stride = sizeof( SimpleVertex );
UINT offset = 0;
g_pImmediateContext->IASetVertexBuffers( 0, 1, &g_pVertexBuffer, &stride, &offset );
// Set primitive topology
g_pImmediateContext->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST );
IASetVertexBuffers()
这些函数中的第一个是IASetVertexBuffers()。这将告诉GPU在渲染时要读取哪些顶点。它有几个简单的参数,所以让我们来看看原型:
void IASetVertexBuffers(UINT StartSlot, //advanced
UINT NumBuffers, //设置了多少个缓冲区
ID3D11Buffer ** ppVertexBuffers, //指向顶点缓冲区数组的指针
UINT * pStrides, //指向UINT数组
UINT * pOffsets); //应该开始渲染的顶点缓冲区的字节数
IASetPrimitiveTopology()
第二个函数告诉Direct3D使用哪种类型的基元。
旗 | 描述 |
D3D11_PRIMITIVE_TOPOLOGY_POINTLIST | 显示一系列点,每个点对应一个点。 |
D3D11_PRIMITIVE_TOPOLOGY_LINELIST | 显示一系列分隔的线条。 |
D3D11_PRIMITIVE_TOPOLOGY_LINESTRIP | 显示一系列连接线。 |
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST | 显示一系列分离的三角形。 |
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP | 显示一系列连接的三角形。 |
Draw()
现在我们告诉Direct3D要渲染什么样的基元,以及要读取什么顶点缓冲区,我们告诉它绘制顶点缓冲区的内容。
此函数将顶点缓冲区中的基元绘制到后台缓冲区。这是原型:
void Draw(UINT VertexCount,//要绘制的顶点数
UINT StartVertexLocation); //要绘制的第一个顶点
devcon-> Draw(3,0); //从顶点0开始绘制3个顶点