【问题标题】:how can I update dynamic vertex buffer fastly?如何快速更新动态顶点缓冲区?
【发布时间】:2020-04-17 21:50:05
【问题描述】:

我正在尝试制作一个简单的 3D 建模工具。

为了变换模型需要移动一个顶点(或多个顶点)。

我使用了动态顶点缓冲区,因为我认为它需要大量更新。

但即使我只更改一个顶点,高多边形模型的性能也太低了。

还有其他方法吗?还是我做错了?

这是我的 D3D11_BUFFER_DESC

Usage = D3D11_USAGE_DYNAMIC;
CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
BindFlags = D3D11_BIND_VERTEX_BUFFER;
ByteWidth = sizeof(ST_Vertex) * _nVertexCount
D3D11_SUBRESOURCE_DATA d3dBufferData;
d3dBufferData.pSysMem = pVerticesInfo;
hr = pd3dDevice->CreateBuffer(&descBuffer, &d3dBufferData, &_pVertexBuffer);

还有我的更新功能

D3D11_MAPPED_SUBRESOURCE d3dMappedResource;
pImmediateContext->Map(_pVertexBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &d3dMappedResource);

ST_Vertex* pBuffer = (ST_Vertex*)d3dMappedResource.pData;

for (int i = 0; i < vIndice.size(); ++i)
{
    pBuffer[vIndice[i]].xfPosition.x = pVerticesInfo[vIndice[i]].xfPosition.x;
    pBuffer[vIndice[i]].xfPosition.y = pVerticesInfo[vIndice[i]].xfPosition.y;
    pBuffer[vIndice[i]].xfPosition.z = pVerticesInfo[vIndice[i]].xfPosition.z;
}
pImmediateContext->Unmap(_pVertexBuffer, 0);

【问题讨论】:

  • vIndice 是索引缓冲区还是变更集缓冲区?

标签: directx transformation directx-11 vertex-buffer


【解决方案1】:

如上一个答案所述,您每次都在更新整个缓冲区,这取决于模型大小。

解决方案确实是实现部分更新,有两种可能,你想更新单个顶点,或者你想更新 任意索引(例如,您想一次性移动 N 个顶点,在不同的位置,例如顶点 1,20,23。

第一个解决方案相当简单,首先使用以下描述创建缓冲区:

Usage = D3D11_USAGE_DEFAULT;
CPUAccessFlags = 0;
BindFlags = D3D11_BIND_VERTEX_BUFFER;
ByteWidth = sizeof(ST_Vertex) * _nVertexCount
D3D11_SUBRESOURCE_DATA d3dBufferData;
d3dBufferData.pSysMem = pVerticesInfo;
hr = pd3dDevice->CreateBuffer(&descBuffer, &d3dBufferData, &_pVertexBuffer);

这可确保您的顶点缓冲区仅在 gpu 可见。

接下来创建一个具有单个顶点大小的第二个动态缓冲区(在这种情况下您不需要任何绑定标志,因为它只会用于副本)

_pCopyVertexBuffer

Usage = D3D11_USAGE_DYNAMIC; //Staging works as well
CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
BindFlags = 0;
ByteWidth = sizeof(ST_Vertex);
D3D11_SUBRESOURCE_DATA d3dBufferData;
d3dBufferData.pSysMem = NULL;
hr = pd3dDevice->CreateBuffer(&descBuffer, &d3dBufferData, &_pCopyVertexBuffer);

when you move a vertex, copy the changed vertex in the copy buffer :

ST_Vertex changedVertex;

D3D11_MAPPED_SUBRESOURCE d3dMappedResource;
pImmediateContext->Map(_pVertexBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &d3dMappedResource);

ST_Vertex* pBuffer = (ST_Vertex*)d3dMappedResource.pData;

pBuffer->xfPosition.x = changedVertex.xfPosition.x;
pBuffer->.xfPosition.y = changedVertex.xfPosition.y;
pBuffer->.xfPosition.z = changedVertex.xfPosition.z;

pImmediateContext->Unmap(_pVertexBuffer, 0);

由于您使用 D3D11_MAP_WRITE_DISCARD,因此请确保将所有属性写入那里(不仅仅是位置)。

现在完成后,您可以使用ID3D11DeviceContext::CopySubresourceRegion 仅复制当前位置的修改顶点:

我假设 vertexID 是修改后顶点的索引:

pd3DeviceContext->CopySubresourceRegion(_pVertexBuffer, 
0, //must be 0
vertexID * sizeof(ST_Vertex), //location of the vertex in you gpu vertex buffer
0, //must be 0
0, //must be 0
_pCopyVertexBuffer, 
0, //must be 0
NULL //in this case we copy the full content of _pCopyVertexBuffer, so we can set to null
);

现在,如果您想更新顶点列表,事情会变得更加复杂,您有多种选择:

-首先你在一个循环中应用这个单顶点技术,如果你的变更集很小,这将工作得很好。

-如果您的变更集非常大(接近几乎完整的顶点大小,您可以改写整个缓冲区)。

-一种中间技术是使用计算着色器来执行更新(这是我通常使用的最灵活的版本)。 发布所有 c++ 绑定代码会太长,但这是概念:

  • 您的顶点缓冲区必须有 BindFlags = D3D11_BIND_VERTEX_BUFFER | D3D11_BIND_UNORDERED_ACCESS; //这允许写wioth计算
  • 您需要为此缓冲区创建一个 ID3D11UnorderedAccessView(以便着色器可以写入)
  • 您需要以下其他标志:D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS //这允许写入为 RWByteAddressBuffer
  • 然后您创建两个动态结构化缓冲区(我更喜欢那些超过 byteaddress 的缓冲区,但在 dx11 中不允许使用顶点缓冲区和结构化缓冲区,因此您需要使用原始缓冲区)
  • 第一个结构化缓冲区的步长为 ST_Vertex(这是您的变更集)
  • 第二个结构化缓冲区的步长为 4(uint,这些是索引)
  • 两个结构化缓冲区都获得任意元素计数(通常我使用 1024 或 2048),因此这将是您一次可以更新的最大顶点数量。
  • 两个结构化缓冲区都需要一个 ID3D11ShaderResourceView(着色器可见,只读)

那么更新过程如下:

  • 在结构化缓冲区中写入修改后的顶点和位置(使用地图丢弃,如果您必须复制较少的话就可以了)
  • 附加两个结构化缓冲区以供读取
  • 附加 ID3D11UnorderedAccessView 进行写入
  • 设置计算着色器
  • 呼叫调度
  • 分离 ID3D11UnorderedAccessView 以进行写入(这非常重要)

这是一个示例计算着色器代码(为简单起见,我假设您的顶点只是位置)

cbuffer cbUpdateCount : register(b0)
{
    uint updateCount;
};

RWByteAddressBuffer RWVertexPositionBuffer : register(u0);

StructuredBuffer<float3> ModifiedVertexBuffer : register(t0);
StructuredBuffer<uint> ModifiedVertexIndicesBuffer : register(t0);

//this is the stride of your vertex buffer, since here we use float3 it is 12 bytes
#define WRITE_STRIDE 12 

[numthreads(64, 1, 1)]
void CS( uint3 tid : SV_DispatchThreadID )
{
    //make sure you do not go part element count, as here we runs 64 threads at a time 
    if (tid.x >= updateCount) { return; }

    uint readIndex = tid.x;
    uint writeIndex = ModifiedVertexIndicesBuffer[readIndex];

    float3 vertex = ModifiedVertexBuffer[readIndex];
    //byte address buffers do not understand float, asuint is a binary cast.
    RWVertexPositionBuffer.Store3(writeIndex * WRITE_STRIDE, asuint(vertex));
}

【讨论】:

  • Alex的方法和你的第一种方法有什么区别?我不明白为什么要为一个顶点制作缓冲区。
  • 在我的例子中,我们避免了 D3D11_MAP_WRITE 调用,这会导致 gpu 端的停顿。所以我们只对单个元素执行写丢弃(永远不会停顿,好像资源正在使用它会执行重命名操作),然后在 gpu 端进行复制,这也是无停顿的。
【解决方案2】:

出于这个问题的目的,我将假设您已经有了一种机制,可以根据光线投射或其他一些拾取方法从顶点列表中选择一个顶点,以及一种用于创建位移向量的机制,详细说明顶点如何在模型空间中移动。

更新缓冲区的方法对于少于几百个顶点的任何东西都足够了,但在大型模型上它变得非常慢。这是因为您正在更新所有内容,而不是您修改的单个顶点。

要解决此问题,您应该只更新已更改的顶点,为此您需要创建一个更改集

从概念上讲,更改集只不过是对数据所做的一组更改 - 需要更新的顶点列表。由于我们已经知道哪些顶点被修改了(否则我们无法操纵它们),我们可以映射到 GPU 缓冲区,专门转到那个顶点,然后将这些顶点复制到 GPU 缓冲区中。

在你的顶点修改方法中,记录用户修改的顶点的索引:

//Modify the vertex coordinates based on mouse displacement
pVerticesInfo[SelectedVertexIndex].xfPosition.x += DisplacementVector.x;
pVerticesInfo[SelectedVertexIndex].xfPosition.y += DisplacementVector.y;
pVerticesInfo[SelectedVertexIndex].xfPosition.z += DisplacementVector.z;
//Add the changed vertex to the list of changes.
changedVertices.add(SelectedVertexIndex);
//And update the GPU buffer
UpdateD3DBuffer();

UpdateD3DBuffer() 中,执行以下操作:

D3D11_MAPPED_SUBRESOURCE d3dMappedResource;
pImmediateContext->Map(_pVertexBuffer, 0, D3D11_MAP_WRITE, 0, &d3dMappedResource);

ST_Vertex* pBuffer = (ST_Vertex*)d3dMappedResource.pData;

for (int i = 0; i < changedVertices.size(); ++i)
{
    pBuffer[changedVertices[i]].xfPosition.x = pVerticesInfo[changedVertices[i]].xfPosition.x;
    pBuffer[changedVertices[i]].xfPosition.y = pVerticesInfo[changedVertices[i]].xfPosition.y;
    pBuffer[changedVertices[i]].xfPosition.z = pVerticesInfo[changedVertices[i]].xfPosition.z;
}
pImmediateContext->Unmap(_pVertexBuffer, 0);
changedVertices.clear();

这仅更新已更改的顶点,而不是模型中的所有顶点。

这也允许一些更复杂的操作。您可以选择多个顶点并将它们作为一个组全部移动,选择整个面并移动所有连接的顶点,或者相对轻松地移动模型的整个区域,假设您的拾取方法能够处理此问题。

此外,如果您记录了具有足够信息(受影响的顶点和位移索引)的更改集,您可以通过简单地反转位移矢量并重新应用选定的更改集来相当轻松地实现撤消功能。

【讨论】:

  • 不要认为 D3D11_MAP_WRITE_DISCARD 可以在每个架构中工作,您最终可能会重命名并且缓冲区数据可能会完全失效
  • 根据 ms doc : D3D11_MAP_WRITE_DISCARD 资源被映射用于写入;资源的先前内容将是未定义的。
  • 没错,但您仍然可以使用相同的方法 - 只需使用 D3D11_MAP_WRITE 代替。我已经编辑了代码示例以反映这一点。
  • 确实如此,但 D3D11_MAP_WRITE 会造成停顿,这会产生自己的问题。
  • 是的,您只是在使用 changedVertices 修改缓冲区,但是,无论如何,在这个示例中,整个缓冲区都没有上传到 GPU 吗? (我没有看到您告诉 DirectX 应该将哪个数据区域发送到 GPU。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多