我有一个使用 Direct3D 绘制 3D 模型的 C++ 程序。该程序按预期工作,即 3D 模型的图像被正确渲染。下一步是编写一个计算着色器,它将顶点和索引缓冲区作为输入。然后,当用户执行某个工作流程时,它会使用该数据进行一些计算。
要启用此功能,我需要使顶点和索引缓冲区可供计算着色器使用。这就是出错的地方,即一旦我更改代码以便计算着色器可以使用缓冲区,程序就不再渲染图像。
3D 模型由以下类型的顶点组成:
struct VertexWithNormal
{
::DirectX::XMFLOAT4 coordinates;
::DirectX::XMFLOAT4 normal;
int whatever1;
int whatever2;
int whatever3;
int whatever4;
};
然后通过以下代码将该布局提供给 GPU:
static const D3D11_INPUT_ELEMENT_DESC vertexDesc[] =
{
{ "POSITION" , 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, offsetof(VertexWithNormal, coordinates), D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "NORMAL" , 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, offsetof(VertexWithNormal, normal) , D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "WHATEVER1", 0, DXGI_FORMAT_R32_SINT , 0, offsetof(VertexWithNormal, whatever1) , D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "WHATEVER2", 0, DXGI_FORMAT_R32_SINT , 0, offsetof(VertexWithNormal, whatever2) , D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "WHATEVER3", 0, DXGI_FORMAT_R32_SINT , 0, offsetof(VertexWithNormal, whatever3) , D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "WHATEVER4", 0, DXGI_FORMAT_R32_SINT , 0, offsetof(VertexWithNormal, whatever4) , D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
winrt::check_hresult(_d3dDevice->CreateInputLayout(vertexDesc, ARRAYSIZE(vertexDesc), shaderCode, shaderSize, _inputLayout.put()));
_d3dContext->IASetInputLayout(_inputLayout.get());
然后通过以下代码创建顶点缓冲区:
UINT stride = sizeof(VertexWithNormal);
UINT offset = 0;
D3D11_SUBRESOURCE_DATA vertexBufferData{};
vertexBufferData.pSysMem = _model->GetVertexPointer();
D3D11_BUFFER_DESC vertexBufferDesc{};
vertexBufferDesc.Usage = D3D11_USAGE_DEFAULT;
vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vertexBufferDesc.ByteWidth = 1234;
vertexBufferDesc.StructureByteStride = stride;
winrt::check_hresult(_d3dDevice->CreateBuffer(&vertexBufferDesc, &vertexBufferData, _vertexBuffer.put()));
ID3D11Buffer* vertexBuffer = _vertexBuffer.get();
_d3dContext->IASetVertexBuffers(0, 1, &vertexBuffer, &stride, &offset);
最后,以下代码创建由 unsigned int 组成的索引缓冲区:
D3D11_SUBRESOURCE_DATA indexBufferData{};
indexBufferData.pSysMem = _model->GetIndexPointer();
D3D11_BUFFER_DESC indexBufferDesc{};
indexBufferDesc.Usage = D3D11_USAGE_DEFAULT;
indexBufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER;
indexBufferDesc.ByteWidth = 4321;
winrt::check_hresult(_d3dDevice->CreateBuffer(&indexBufferDesc, &indexBufferData, _indexBuffer.put()));
_d3dContext->IASetIndexBuffer(_indexBuffer.get(), DXGI_FORMAT_R32_UINT, 0);
现在,计算着色器的 HLSL 代码应该如下所示:
struct Vertex
{
float4 Coordinates;
float4 Normal;
int Whatever1;
int Whatever2;
int Whatever3;
int Whatever4;
};
StructuredBuffer<Vertex> VertexBuffer : register(t0);
StructuredBuffer<uint> IndexBuffer : register(t1);;
[numthreads(128, 1, 1)]
void main(uint3 dispatchThreadId : SV_DispatchThreadID)
{
uint vertexIndex = dispatchThreadId.x * 3;
// Retrieve the actual values from the vertex and index buffers
Vertex vertex0 = VertexBuffer[IndexBuffer[vertexIndex]];
Vertex vertex1 = VertexBuffer[IndexBuffer[vertexIndex + 1u]];
Vertex vertex2 = VertexBuffer[IndexBuffer[vertexIndex + 2u]];
// Calculations with the vertices...
}
我的理解是,顶点和索引缓冲区需要映射到 HLSL 代码中的
StructuredBuffer
。显然,从计算着色器的角度来看,这是一个只读缓冲区,这正是我所需要的。我还尝试将这些缓冲区映射为常量缓冲区 (cbuffer
),但尝试失败。
现在,我的理解是,我需要为顶点和索引缓冲区创建一个
ID3D11ShaderResourceView
,以便我可以通过调用 Direct3D 上下文的 CSSetShaderResources
使它们可供着色器使用。我尝试通过更改创建顶点缓冲区的代码来实现此目的,如下所示:
UINT stride = sizeof(VertexWithNormal);
UINT offset = 0;
D3D11_SUBRESOURCE_DATA vertexBufferData{};
vertexBufferData.pSysMem = _model->GetVertexPointer();
D3D11_BUFFER_DESC vertexBufferDesc{};
vertexBufferDesc.Usage = D3D11_USAGE_DEFAULT;
// Compared to the working code, I'm ORing D3D11_BIND_SHADER_RESOURCE to D3D11_BIND_VERTEX_BUFFER
vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER | D3D11_BIND_SHADER_RESOURCE;
// Seems to be necessary, but adding the below line results in empty images, nothing is rendered anymore
vertexBufferDesc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED;
vertexBufferDesc.ByteWidth = 1234;
vertexBufferDesc.StructureByteStride = stride;
winrt::check_hresult(_d3dDevice->CreateBuffer(&vertexBufferDesc, &vertexBufferData, _vertexBuffer.put()));
ID3D11Buffer* vertexBuffer = _vertexBuffer.get();
_d3dContext->IASetVertexBuffers(0, 1, &vertexBuffer, &stride, &offset);
// Code that seems to be necessary to make the vertex buffer available to the compute shader
D3D11_SHADER_RESOURCE_VIEW_DESC computeShaderResourceDesc{};
computeShaderResourceDesc.Format = DXGI_FORMAT_UNKNOWN;
computeShaderResourceDesc.ViewDimension = D3D11_SRV_DIMENSION_BUFFEREX;
computeShaderResourceDesc.BufferEx.NumElements = <actual size...>;
winrt::check_hresult(_d3dDevice->CreateShaderResourceView(vertexBuffer, &computeShaderResourceDesc, _vertexBufferShaderResourceView.put()));
ID3D11ShaderResourceView* vertexBufferShaderResourceView = _vertexBufferShaderResourceView.get();
_d3dContext->CSSetShaderResources(0, 1, &vertexBufferShaderResourceView);
但是,修改如上所示的代码会破坏渲染,即不再渲染 3D 模型的图像。
编辑:
启用 DirectX 调试层会导致我运行程序时发出以下错误:
D3D11 ERROR: ID3D11Device::CreateBuffer: Buffers created with D3D11_RESOURCE_MISC_BUFFER_STRUCTURED cannot specify any of the following listed bind flags. The following BindFlags bits (0x9) are set: D3D11_BIND_VERTEX_BUFFER (1), D3D11_BIND_INDEX_BUFFER (0), D3D11_BIND_CONSTANT_BUFFER (0), D3D11_BIND_STREAM_OUTPUT (0), D3D11_BIND_RENDER_TARGET (0), or D3D11_BIND_DEPTH_STENCIL (0). [ STATE_CREATION ERROR #68: CREATEBUFFER_INVALIDMISCFLAGS]
D3D11 ERROR: ID3D11Device::CreateBuffer: CreateBuffer returning E_INVALIDARG, meaning invalid parameters were passed. [ STATE_CREATION ERROR #69: CREATEBUFFER_INVALIDARG_RETURN]
删除
vertexBufferDesc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED;
然后会发出以下错误:
D3D11 ERROR: ID3D11Device::CreateShaderResourceView: The Format (0, UNKNOWN) cannot be used, when creating a View of a Buffer. [ STATE_CREATION ERROR #127: CREATESHADERRESOURCEVIEW_INVALIDFORMAT]
D3D11 ERROR: ID3D11Device::CreateShaderResourceView: Returning E_INVALIDARG, meaning invalid parameters were passed. [ STATE_CREATION ERROR #131: CREATESHADERRESOURCEVIEW_INVALIDARG_RETURN]
现在的问题是,需要将什么作为格式放在那里。目前还不知道。
我发现的唯一方法以及注释中暗示的从 DirectX 11 计算着色器访问顶点和索引缓冲区的方法是将它们绑定为
ByteAddressBuffer
。这不是很方便,因为顾名思义,所有读取操作都是通过表示为字节偏移量的地址完成的,并且从缓冲区中获取值很快就会变得很麻烦。
因为从着色器的角度来看,
ByteAddressBuffer
是只读缓冲区,因此需要创建着色器资源视图(SRV)才能将缓冲区绑定到着色器。为了实现这一目标,我必须修改我在问题中发布的代码,如下所述。
首先,必须使用附加标志(杂项标志
D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS
和绑定标志 D3D11_BIND_SHADER_RESOURCE
)创建顶点缓冲区。之后,必须创建它的 SRV,然后才能将其绑定到着色器:
UINT stride = sizeof(VertexWithNormal);
UINT offset = 0;
D3D11_SUBRESOURCE_DATA vertexBufferData{};
vertexBufferData.pSysMem = _model->GetVertexPointer();
D3D11_BUFFER_DESC vertexBufferDesc{};
vertexBufferDesc.Usage = D3D11_USAGE_DEFAULT;
vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER | D3D11_BIND_SHADER_RESOURCE;
vertexBufferDesc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS;
vertexBufferDesc.ByteWidth = 1234;
vertexBufferDesc.StructureByteStride = stride;
winrt::check_hresult(_d3dDevice->CreateBuffer(&vertexBufferDesc, &vertexBufferData, _vertexBuffer.put()));
ID3D11Buffer* vertexBuffer = _vertexBuffer.get();
_d3dContext->IASetVertexBuffers(0, 1, &vertexBuffer, &stride, &offset);
D3D11_SHADER_RESOURCE_VIEW_DESC shaderResourceViewDesc{};
shaderResourceViewDesc.Format = DXGI_FORMAT_R32_TYPELESS;
shaderResourceViewDesc.ViewDimension = D3D11_SRV_DIMENSION_BUFFEREX;
shaderResourceViewDesc.BufferEx.Flags = D3D11_BUFFEREX_SRV_FLAG_RAW;
shaderResourceViewDesc.BufferEx.NumElements = 1234 / 4; // The size of the buffer here seems to be specified in 4-byte elements, thus divide by 4
winrt::check_hresult(_d3dDevice->CreateShaderResourceView(vertexBuffer, &shaderResourceViewDesc, _vertexBufferShaderResourceView.put()));
ID3D11ShaderResourceView* vertexBufferShaderResourceView = _vertexBufferShaderResourceView.get();
_d3dContext->CSSetShaderResources(0, 1, &vertexBufferShaderResourceView);
索引缓冲区创建代码必须以相同的方式修改:
D3D11_SUBRESOURCE_DATA indexBufferData{};
indexBufferData.pSysMem = _model->GetIndexPointer();
D3D11_BUFFER_DESC indexBufferDesc{};
indexBufferDesc.Usage = D3D11_USAGE_DEFAULT;
indexBufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER | D3D11_BIND_SHADER_RESOURCE;
indexBufferDesc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS;
indexBufferDesc.ByteWidth = 4321;
winrt::check_hresult(_d3dDevice->CreateBuffer(&indexBufferDesc, &indexBufferData, _indexBuffer.put()));
_d3dContext->IASetIndexBuffer(_indexBuffer.get(), DXGI_FORMAT_R32_UINT, 0);
D3D11_SHADER_RESOURCE_VIEW_DESC shaderResourceViewDesc{};
shaderResourceViewDesc.Format = DXGI_FORMAT_R32_TYPELESS;
shaderResourceViewDesc.ViewDimension = D3D11_SRV_DIMENSION_BUFFEREX;
shaderResourceViewDesc.BufferEx.Flags = D3D11_BUFFEREX_SRV_FLAG_RAW;
shaderResourceViewDesc.BufferEx.NumElements = 4321 / 4; // The size of the buffer here seems to be specified in 4-byte elements, thus divide by 4
winrt::check_hresult(_d3dDevice->CreateShaderResourceView(_indexBuffer.get(), &shaderResourceViewDesc, _indexBufferShaderResourceView.put()));
ID3D11ShaderResourceView* indexBufferShaderResourceView = _indexBufferShaderResourceView.get();
_d3dContext->CSSetShaderResources(1, 1, &indexBufferShaderResourceView);
由于着色器现在需要访问具有字节偏移量的元素,因此 HLSL 代码中的
Vertex
结构不再可用,因此被删除。修改后的着色器代码如下所示:
ByteAddressBuffer VertexBuffer : register(t0);
ByteAddressBuffer IndexBuffer : register(t1);
[numthreads(128, 1, 1)]
void main(uint3 dispatchThreadId : SV_DispatchThreadID)
{
uint triangleIndex = dispatchThreadId.x * 3;
// The below size is calculated based on the "VertexWithNormal" struct
// defined in the C++ code:
// 2 * sizeof(XMFLOAT4) + 4 * sizeof(int) = 2 * 4 * 4 + 4 * 4 = 48 bytes
const uint vertexSizeInBytes = 48;
// A triangle consists of 3 vertices. Thus, 3 vertex indices of the index
// buffer form one triangle:
// 3 * sizeof(int) = 3 * 4 = 12 bytes
const uint indexOffset = triangleIndex * 12;
const uint vertexOffset0 = IndexBuffer.Load(indexOffset) * vertexSizeInBytes;
const uint vertexOffset1 = IndexBuffer.Load(indexOffset + 4) * vertexSizeInBytes;
const uint vertexOffset2 = IndexBuffer.Load(indexOffset + 8) * vertexSizeInBytes;
const float4 vertexCoordinates0 = asfloat(VertexBuffer.Load4(vertexOffset0));
const float4 vertexCoordinates1 = asfloat(VertexBuffer.Load4(vertexOffset1));
const float4 vertexCoordinates2 = asfloat(VertexBuffer.Load4(vertexOffset2));
// Calculations with the vertices...
}
以这种方式从顶点和索引缓冲区中提取信息很容易出错,但不幸的是我没有找到更好的方法。显然,DirectX 着色器编译器确实提供了更简化的获取值的方法,如here所述,但这似乎是比我可用的编译器版本更新的一部分。