非交错顶点缓冲 DirectX11

4
如果我的顶点位置是共享的,但法线和UV被分开存储(以保留锐利边缘等特性),那么在DirectX11中使用非交错缓冲区来解决这个内存表示是否可行,从而可以使用索引缓冲区?还是应该坚持在交错缓冲区中重复顶点位置?
此外,交错与非交错顶点缓冲区之间是否存在性能问题?谢谢!

只是想澄清一些问题:我目前一直在使用交错缓冲区,唯一需要使用非交错缓冲区的情况是播放顶点缓存文件(将它们直接发送到GPU而不重新排序步幅以匹配交错设置)。 - Erunehtar
3个回答

15

如何

有几种方式。我将描述最简单的一种。

只需创建单独的顶点缓冲区:

ID3D11Buffer* positions;
ID3D11Buffer* texcoords;
ID3D11Buffer* normals;

创建输入布局元素,每个组件都逐个增加 InputSlot 成员:

{ "POSITION",  0,  DXGI_FORMAT_R32G32B32_FLOAT,  0, 0,                            D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "TEXCOORD",  0,  DXGI_FORMAT_R32G32_FLOAT,     1, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "NORMAL",    0,  DXGI_FORMAT_R32G32B32_FLOAT,  2, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 },
                                             //  ^
                                             // InputSlot

将缓冲区绑定到它们的插槽(最好一次性全部完成):

ID3D11Buffer** vbs = {positions, texcoords, normals};
unsigned int strides[] = { /*strides go here*/ };
unsigned int offsets [] = { /*offsets go here*/ };
m_Context->IASetVertexBuffers(0, 3, vbs, strides, offsets );

按照惯例进行绘制。 您不需要更改HLSL代码(HLSL将认为它只有单个缓冲区)。

请注意,代码片段是即时编写的,可能包含错误。

编辑:您可以通过更新率组合缓冲区来改进此方法:如果texcoordsnormals从未更改,请合并它们。

性能方面

这完全取决于引用局部性:数据越接近,访问速度越快。

在大多数情况下,交错缓冲区对GPU端(即渲染)性能的提升要大得多:每个顶点的每个属性都靠近彼此。但是,单独的缓冲区可以更快地访问CPU:数组是连续的,每个下一个数据都靠近前一个。

因此,总体而言,性能问题取决于您多频繁写入缓冲区。如果您的限制因素是CPU写入,则应使用单独的缓冲区。否则,使用交错缓冲区。

如何知道?唯一的方法是分析。无论是CPU还是GPU端(通过您GPU供应商的图形调试器/分析器)。

其他因素

最佳实践是限制CPU写入,因此,如果您发现受到缓冲区更新的限制,您可能需要重新审视自己的方法。如果我们每秒有500帧,我们是否需要在每个帧更新时都更新缓冲区?如果您将缓冲区更新速率降至30-60次每秒,则用户不会看到任何区别(将缓冲区更新与帧更新解绑)。因此,如果您的更新策略合理,则很可能永远不会受到CPU限制,最佳方法是经典的交错。

您还可以考虑重新设计数据流程,甚至某种方式的离线准备数据(我们称之为“烘焙”),以便您无需处理非交错的缓冲区。这也是相当合理的。

减少内存占用还是提高性能?

内存和性能之间的平衡取舍。这是永恒的问题。复制内存以利用交错的优势?或者干脆不这样做呢?

答案是...“那要看情况”。您正在编写面向配备了数GB内存的顶级GPU的CryEngine新版本?还是您正在为内存资源受限且缓慢的嵌入式系统或移动平台编程?1 MB的内存是否真的值得麻烦?或者您有巨大的模型,每个模型100 MB?我们不知道。

决定权在于您。但请记住:没有免费的糖果。如果您发现内存经济效益超过性能损失,请这样做。进行分析和比较以确保。

希望它在某种程度上能帮助您。愉快的编码!=)


1
这是我正在思考的所有问题的非常好的总结。在我的情况下,预烘焙不太合适;我们希望文件能够快速打开,并且不想浪费时间和内存来创建一个与原始文件相互交错格式的新点缓存文件。现在我认为你是对的,我可能需要对其进行分析。现在我只使用交错缓冲区,所以我必须使用for循环将我的新顶点位置复制到缓冲区中。我将尝试使用非交错缓冲区,并查看是否可以使用简单的memcpy更好地工作。此外,场景大小可能会有很大差异。 - Erunehtar
1
哦,我忘了提到,我的渲染器可以在传统的桌面电脑和移动设备上运行。换句话说,我需要支持这两种情况。此外,正在被渲染的内容完全不受我的控制,因为该工具用于查看任意的3D场景,大小从1kb到数百兆字节(有时甚至是几个千兆字节)。 - Erunehtar

3

Interleaved/Separate主要影响您的输入装配阶段(GPU端)。

当您的缓冲区内存排列完全符合顶点着色器输入时,交错是一个完美的场景。因此,您的输入装配程序可以简单地获取数据。

在这种情况下,即使使用大型模型进行测试(相同数据的两个版本,一个交错,一个分离),时间戳查询也没有报告任何重大差异(一些非常小的顶点处理和基本像素着色器)。

现在,如果使用单独的缓冲区,则更容易对其进行微调,以便在不同上下文中使用几何图形。

假设您有位置/法线/UV(就像您的情况一样)。

现在,您的管道中还有一个仅需要位置的着色器(Shadow Map将是一个很好的例子)。

使用单独的缓冲区,您可以简单地创建一个新的输入布局,其中仅包含位置,并绑定该缓冲区。您的IA阶段只需加载该缓冲区。最重要的是,您甚至可以使用着色器反射动态执行此操作。

如果绑定交错数据,则由于必须使用步幅进行加载,会产生一些开销。

当我测试时,使用分离而不是交错获得了约20%的增益,这可能相当不错,但由于此类处理在很大程度上取决于架构,请勿将其视为理所当然(NVidia 740M用于测试)。

因此,简单地说,要进行大量分析,并检查哪种方法可以在GPU和CPU负载之间实现最佳平衡。

还请注意,如果应用一些重型计算+添加一些镶嵌+一些体面的着色,输入装配程序的开销将随着着色器的复杂性而减少,交错/非交错之间的时间差异将逐渐变得无意义。


1
你应该坚持使用交错缓冲区。任何其他技术都需要对非重复位置缓冲区进行某种形式的间接引用,这将损失性能和缓存效率。

好的,那如果我想改变顶点位置怎么办?我需要编写一个_for循环_来更新值,而不是使用memcpy吗?这样做会使它变得更慢吗? - Erunehtar
@Deathicon 你是否在程序上生成顶点缓冲区?如果不是,应该合并重复的顶点并进行复制。如果是这样,希望你在每次生成时多次渲染此几何体,以便 CPU 的开销变得微不足道。如果你每帧都在生成新的几何图形,那么你很可能受到 CPU 的限制,因此间接性的 GPU 效率并不重要。 - MooseBoys
我正在从点缓存中读取顶点位置,这显然会返回非交错数据(位置、法线等都在单独的缓冲区中),因此我必须使用for循环将该数据复制到交错顶点缓冲区中。我只是想知道是否有更好的方法来做到这一点。 - Erunehtar
@Deathicon 这是一个3ds点缓存吗?你最好的选择可能是有一些离线预处理,将点缓存文件转换为更友好(即平面)的格式。点缓存应该被视为作者时间格式,而不是运行时格式。 - MooseBoys

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