如何快速更新动态顶点缓冲区?

4

我正在尝试制作一个简单的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 是索引缓冲还是变更集缓冲? - Alex
2个回答

4

正如前面的回答所述,每次更新整个缓冲区都会很慢,具体取决于模型大小。

解决方案确实是实现部分更新,有两种可能:您想要更新单个顶点,或者您想要更新任意索引(例如,您想要一次移动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; //这允许使用计算机进行编写
  • 您需要为此缓冲区创建ID3D11UnorderedAccessView(因此着色器可以写入它)
  • 您需要以下杂项标志:D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS //这允许作为RWByteAddressBuffer编写
  • 然后创建两个动态结构化缓冲区(我更喜欢那些比字节地址,但是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的方法和你的第一种方法有什么区别? 我不明白为什么要为一个顶点创建缓冲区。 - I hate cucumber
在我的情况下,我们避免了D3D11_MAP_WRITE调用,因为它可能会导致GPU方面的停顿。因此,我们只对单个元素执行写入丢弃操作(如果资源正在使用中,则执行重命名操作,因此不会停顿),然后在GPU方面进行复制,这也是无停顿的。 - mrvux

0
为了回答这个问题,我假设您已经有了一种机制,可以基于射线投射或其他拾取方法从顶点列表中选择顶点,并且有一种机制可以创建位移向量,详细说明顶点在模型空间中的移动方式。
对于少于几百个顶点的任何情况,您更新缓冲区的方法都足够了,但是在大规模模型上,它变得非常缓慢。这是因为您正在更新所有内容,而不是您修改的单个顶点。
要解决这个问题,您应该只更新已更改的顶点,并且为此需要创建一个更改集
在概念上,更改集仅是对数据进行的更改集合-需要更新的顶点列表。由于我们已经知道哪些顶点被修改(否则我们无法操作它们),因此我们可以在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 在每个架构中都能正常工作,你可能会遇到重命名的情况,缓冲数据可能会完全失效。 - mrvux
根据微软文档:D3D11_MAP_WRITE_DISCARD资源被映射以进行写入;资源的先前内容将未定义。 - mrvux
没错,但你仍然可以使用相同的方法 - 只需使用D3D11_MAP_WRITE即可。我已经编辑了代码示例以反映这一点。 - Alex
确实,但是 D3D11_MAP_WRITE 会创建停顿,这可能会产生自己的问题。 - mrvux
是的,您只是使用changedVertices修改了缓冲区,但是,在此示例中,整个缓冲区是否都上传到GPU了呢?(我没有看到您告诉DirectX应该发送哪个数据区域到GPU的任何地方。) - BjarkeCK
这根本不是解决方案,因为: Map 不能使用 MAP_WRITE 访问,因为该资源被创建为 D3D11_USAGE_DYNAMICD3D11_USAGE_DYNAMIC 资源必须使用 MAP_WRITE_DISCARDMAP_WRITE_NO_OVERWRITEMap 一起使用。 - Măcelaru Tiberiu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接