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 Data
D3D11_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 shader
ID3DBlob* pVSBlob = nullptr;
hr = CompileShaderFromFile( L"Tutorial02.fx", "VS", "vs_4_0", &pVSBlob );

2.将两个着色器封装到着色器对象中。
// global
ID3D11VertexShader* 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 triangle
g_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 layout
g_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个顶点





comments powered by Disqus